diff --git a/apps/web/app/versions/[id]/page.tsx b/apps/web/app/versions/[id]/page.tsx index b51e332..c8dcbcb 100644 --- a/apps/web/app/versions/[id]/page.tsx +++ b/apps/web/app/versions/[id]/page.tsx @@ -303,27 +303,33 @@ export default function VersionDetailPage() { const calcGroupProgress = (group: typeof vPlans, type: 'research' | 'product' | 'ui') => { if (group.length === 0) return 0; - // 每个 plan 按权重贡献进度:completed=1, in_progress=子任务完成率, pending=0 - let totalWeight = 0; + // 分子=已完成子任务数,分母=所有plan的子任务总数 + // completed 的 plan:其子任务全算已完成 + let totalItems = 0; + let doneItems = 0; for (const p of group) { - if (p.status === 'completed') { - totalWeight += 1; - } else if (p.status === 'in_progress') { - if (type === 'research') { - const tasks = p.tasks || []; - if (tasks.length > 0) { - totalWeight += tasks.filter((t) => t.status === 'completed').length / tasks.length; - } + if (type === 'research') { + const tasks = p.tasks || []; + const count = Math.max(tasks.length, 1); + totalItems += count; + if (p.status === 'completed') { + doneItems += count; + } else { + doneItems += tasks.filter((t) => t.status === 'completed').length; + } + } else { + const linked = p.linkedRequirementIds || []; + const count = Math.max(linked.length, 1); + totalItems += count; + if (p.status === 'completed') { + doneItems += count; } else { - const linked = p.linkedRequirementIds || []; const completed = p.completedRequirementIds || []; - if (linked.length > 0) { - totalWeight += completed.filter((id) => linked.includes(id)).length / linked.length; - } + doneItems += completed.filter((id) => linked.includes(id)).length; } } } - return Math.round((totalWeight / group.length) * 100); + return totalItems > 0 ? Math.round((doneItems / totalItems) * 100) : 0; }; const getPlanStatus = (group: typeof vPlans): 'idle' | 'active' | 'done' => { diff --git a/apps/web/components/version/PlanTab.tsx b/apps/web/components/version/PlanTab.tsx index 9a9ad2b..17c3d62 100644 --- a/apps/web/components/version/PlanTab.tsx +++ b/apps/web/components/version/PlanTab.tsx @@ -252,6 +252,7 @@ function PlanFormModal({ initial, planType, versionId, versionDeadline, currentU e.preventDefault(); if (!title.trim() || !endTime) return; if (isOverdue && !overdueReason.trim()) return; + if (planType === 'research' && tasks.length === 0) return; onSubmit({ versionId, type: planType, @@ -326,7 +327,25 @@ function PlanFormModal({ initial, planType, versionId, versionDeadline, currentU )} {planType === 'research' && (