feat: 加班/版本/项目调整 + 角色权限设计
加班记录: - 加班人默认回显当前用户且不可改 - 列表加创建日期列 - 移除编辑功能(创建即提交,仅可删除) - 提交时校验结束>开始 版本列表: - 移除顶部 9 个状态 tab + 表格状态列 - 仅保留搜索、项目、优先级筛选 项目列表: - 行内显示进度未达 100% 的版本(与版本页同源算法) - 新增 lib/version-progress.ts 抽出公共进度计算 角色权限: - RoleItem 加 permissions 字段 - 新建 lib/permissions.ts: 14 组 37 个权限点 + 默认 5 角色映射 + hasPermission - 角色表单内嵌权限矩阵(主模块 4 件套 + Tab 二档 + Bug Tab 4 档)+ 全选/反选/仅查看快捷 - 新建 components/auth/Guard.tsx: RouteGuard + PermissionGuard + useHasPermission + AccessDenied - Sidebar 菜单按 view 权限过滤 - 7 个主路由(products/projects/versions/requirements/overtime/admin/members/roles)包 RouteGuard - 版本详情 Tab 按 view 权限过滤,自动跳转到第一个有权限的 Tab - 默认未登录/无角色按只读最严格 - 超管 role-admin: ['*'] 通配符,permissions 不可改 通用: - 新建 components/FieldError.tsx 统一表单错误文案 - PlanTab 提交时校验结束>开始 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
97
apps/web/lib/version-progress.ts
Normal file
97
apps/web/lib/version-progress.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { VersionPlan } from './version-plan';
|
||||
import type { Requirement } from './requirement';
|
||||
import type { DevTask } from './dev-task';
|
||||
import type { TestCase } from './test-case';
|
||||
import type { VersionWithContext } from './derive';
|
||||
import { STATUS_PROGRESS, getEstimateHours } from './dev-task';
|
||||
|
||||
/**
|
||||
* 计算单个版本的整体进度(0-100)
|
||||
* 算法:调研/产品/UI 计划进度 + 开发任务加权进度 + 测试执行率,求平均
|
||||
*/
|
||||
export function calcVersionProgress(
|
||||
versionId: string,
|
||||
plans: VersionPlan[],
|
||||
requirements: Requirement[],
|
||||
devTasks: DevTask[],
|
||||
testCases: TestCase[],
|
||||
): number {
|
||||
const vPlans = plans.filter((p) => p.versionId === versionId);
|
||||
const vReqs = requirements.filter((r) => r.versionId === versionId);
|
||||
const vReqIds = new Set(vReqs.map((r) => r.id));
|
||||
const vDevTasks = devTasks.filter((t) => vReqIds.has(t.requirementId));
|
||||
const vTestCases = testCases.filter((c) => c.versionId === versionId);
|
||||
|
||||
const segments: number[] = [];
|
||||
|
||||
const researchPlans = vPlans.filter((p) => p.type === 'research');
|
||||
if (researchPlans.length > 0) {
|
||||
const totals = researchPlans.reduce((acc, p) => {
|
||||
const tasks = p.tasks || [];
|
||||
acc.total += tasks.length;
|
||||
acc.done += tasks.filter((t) => t.status === 'completed').length;
|
||||
return acc;
|
||||
}, { total: 0, done: 0 });
|
||||
segments.push(totals.total > 0 ? (totals.done / totals.total) * 100 : 0);
|
||||
}
|
||||
|
||||
const productPlans = vPlans.filter((p) => p.type === 'product');
|
||||
if (productPlans.length > 0) {
|
||||
const totals = productPlans.reduce((acc, p) => {
|
||||
const linked = p.linkedRequirementIds || [];
|
||||
const completed = p.completedRequirementIds || [];
|
||||
acc.total += linked.length;
|
||||
acc.done += completed.filter((id) => linked.includes(id)).length;
|
||||
return acc;
|
||||
}, { total: 0, done: 0 });
|
||||
segments.push(totals.total > 0 ? (totals.done / totals.total) * 100 : 0);
|
||||
}
|
||||
|
||||
const uiPlans = vPlans.filter((p) => p.type === 'ui');
|
||||
if (uiPlans.length > 0) {
|
||||
const totals = uiPlans.reduce((acc, p) => {
|
||||
const linked = p.linkedRequirementIds || [];
|
||||
const completed = p.completedRequirementIds || [];
|
||||
acc.total += linked.length;
|
||||
acc.done += completed.filter((id) => linked.includes(id)).length;
|
||||
return acc;
|
||||
}, { total: 0, done: 0 });
|
||||
segments.push(totals.total > 0 ? (totals.done / totals.total) * 100 : 0);
|
||||
}
|
||||
|
||||
if (vDevTasks.length > 0) {
|
||||
const totalEstimate = vDevTasks.reduce((sum, t) => sum + getEstimateHours(t), 0);
|
||||
let devProgress: number;
|
||||
if (totalEstimate === 0) {
|
||||
devProgress = vDevTasks.reduce((sum, t) => sum + STATUS_PROGRESS[t.status], 0) / vDevTasks.length;
|
||||
} else {
|
||||
const weighted = vDevTasks.reduce((sum, t) => sum + getEstimateHours(t) * STATUS_PROGRESS[t.status], 0);
|
||||
devProgress = weighted / totalEstimate;
|
||||
}
|
||||
segments.push(devProgress);
|
||||
}
|
||||
|
||||
if (vTestCases.length > 0) {
|
||||
const executed = vTestCases.filter((c) => c.status === 'passed' || c.status === 'failed' || c.status === 'blocked').length;
|
||||
segments.push((executed / vTestCases.length) * 100);
|
||||
}
|
||||
|
||||
return segments.length > 0 ? Math.round(segments.reduce((s, x) => s + x, 0) / segments.length) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量为多个版本算进度,返回 versionId → 进度 (0-100) 的 Map
|
||||
*/
|
||||
export function buildVersionProgressMap(
|
||||
versions: VersionWithContext[],
|
||||
plans: VersionPlan[],
|
||||
requirements: Requirement[],
|
||||
devTasks: DevTask[],
|
||||
testCases: TestCase[],
|
||||
): Record<string, number> {
|
||||
const map: Record<string, number> = {};
|
||||
for (const v of versions) {
|
||||
map[v.id] = calcVersionProgress(v.id, plans, requirements, devTasks, testCases);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
Reference in New Issue
Block a user