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 { const map: Record = {}; for (const v of versions) { map[v.id] = calcVersionProgress(v.id, plans, requirements, devTasks, testCases); } return map; }