diff --git a/apps/web/app/projects/[id]/page.tsx b/apps/web/app/projects/[id]/page.tsx
index 83d1126..db53cf6 100644
--- a/apps/web/app/projects/[id]/page.tsx
+++ b/apps/web/app/projects/[id]/page.tsx
@@ -78,7 +78,7 @@ function VersionCard({ version, onNavigate }: { version: VersionWithContext; onN
{expanded && (
-
+
)}
@@ -92,7 +92,7 @@ function VersionCard({ version, onNavigate }: { version: VersionWithContext; onN
onNavigate(version.id)} className="text-sm font-medium text-[var(--ink)] cursor-pointer hover:text-[var(--accent)]">{version.name}
{displayStatus}
-
+
diff --git a/apps/web/app/versions/[id]/page.tsx b/apps/web/app/versions/[id]/page.tsx
index b8182cb..f9297f9 100644
--- a/apps/web/app/versions/[id]/page.tsx
+++ b/apps/web/app/versions/[id]/page.tsx
@@ -314,38 +314,38 @@ export default function VersionDetailPage() {
return totals.total > 0 ? Math.round((totals.done / totals.total) * 100) : 0;
};
- const today = new Date().toISOString().slice(0, 10);
- const calcDays = (group: typeof vPlans) => {
- if (group.length === 0) return 0;
- const starts = group.map((p) => p.startTime).sort();
- const startDate = starts[0];
- if (startDate > today) return 0;
- const diff = Math.ceil((new Date(today).getTime() - new Date(startDate).getTime()) / (1000 * 60 * 60 * 24));
- return Math.max(1, diff); // 当天开始至少算1天
+ const getPlanStatus = (group: typeof vPlans): 'idle' | 'active' | 'done' => {
+ if (group.length === 0) return 'idle';
+ if (group.every((p) => p.status === 'completed')) return 'done';
+ if (group.some((p) => p.status === 'in_progress')) return 'active';
+ return 'idle';
};
- const stageProgress: Record = {};
- if (researchPlans.length > 0) stageProgress['requirement'] = { percent: calcGroupProgress(researchPlans, 'research'), daysSpent: calcDays(researchPlans) };
- if (productPlans.length > 0) stageProgress['product_design'] = { percent: calcGroupProgress(productPlans, 'product'), daysSpent: calcDays(productPlans) };
- if (uiPlans.length > 0) stageProgress['ui_design'] = { percent: calcGroupProgress(uiPlans, 'ui'), daysSpent: calcDays(uiPlans) };
+ type StageProgressItem = { percent: number; status: 'idle' | 'active' | 'done' };
+ const stageProgress: Partial> = {};
- // 开发阶段进度由 DevTask 驱动
+ if (researchPlans.length > 0) stageProgress['requirement'] = { percent: calcGroupProgress(researchPlans, 'research'), status: getPlanStatus(researchPlans) };
+ if (productPlans.length > 0) stageProgress['product_design'] = { percent: calcGroupProgress(productPlans, 'product'), status: getPlanStatus(productPlans) };
+ if (uiPlans.length > 0) stageProgress['ui_design'] = { percent: calcGroupProgress(uiPlans, 'ui'), status: getPlanStatus(uiPlans) };
+
+ // 开发阶段
if (versionDevTasks.length > 0) {
const devProgress = calcDevTaskProgress(versionDevTasks);
- const devStart = versionDevTasks.reduce((min, t) => t.startDate && t.startDate < min ? t.startDate : min, today);
- const devDays = devStart <= today ? Math.max(1, Math.ceil((new Date(today).getTime() - new Date(devStart).getTime()) / (1000 * 60 * 60 * 24))) : 0;
- stageProgress['dev'] = { percent: devProgress, daysSpent: devDays };
+ const allSubmitted = versionDevTasks.every((t) => t.status === 'submitted');
+ const hasActive = versionDevTasks.some((t) => t.status === 'in_progress' || t.status === 'testing');
+ stageProgress['dev'] = { percent: devProgress, status: allSubmitted ? 'done' : hasActive ? 'active' : 'idle' };
}
- // 测试阶段进度由 TestCase 完成率驱动
- const versionTestCases = testCases.filter((c) => c.versionId === version.id);
- if (versionTestCases.length > 0) {
- const executed = versionTestCases.filter((c) => c.status === 'passed' || c.status === 'failed' || c.status === 'blocked').length;
- const testPercent = Math.round((executed / versionTestCases.length) * 100);
- stageProgress['testing'] = { percent: testPercent, daysSpent: 0 };
+ // 测试阶段
+ if (versionTCs.length > 0) {
+ const executed = versionTCs.filter((c) => c.status === 'passed' || c.status === 'failed' || c.status === 'blocked').length;
+ const testPercent = Math.round((executed / versionTCs.length) * 100);
+ const allPassed = versionTCs.every((c) => c.status === 'passed');
+ const hasRunning = versionTCs.some((c) => c.status === 'running');
+ stageProgress['testing'] = { percent: testPercent, status: allPassed ? 'done' : (hasRunning || executed > 0) ? 'active' : 'idle' };
}
- return ;
+ return ;
})()}
{/* 双栏:加班排名 + 原因占比 */}
diff --git a/apps/web/app/versions/page.tsx b/apps/web/app/versions/page.tsx
index 411c883..27753bb 100644
--- a/apps/web/app/versions/page.tsx
+++ b/apps/web/app/versions/page.tsx
@@ -7,7 +7,7 @@ import { useProductStore } from '@/stores/useProductStore';
import { useRequirementStore } from '@/stores/useRequirementStore';
import { flattenVersions, flattenProjects } from '@/lib/derive';
import type { VersionWithContext } from '@/lib/derive';
-import { VersionStatus, VERSION_STATUS_LABEL, VERSION_STATUS_DOT, getVersionDisplayStatus } from '@/lib/version-status';
+import { VersionStatus, VERSION_STATUS_LABEL, VERSION_STATUS_DOT, VERSION_STATUS_BG, getVersionDisplayStatus } from '@/lib/version-status';
import { STAGES } from '@/lib/stage';
import { ROLE_LABEL } from '@/lib/stage';
import { calcOverallProgress } from '@/lib/risk';
@@ -250,8 +250,7 @@ export default function VersionsPage() {
| 版本号 |
优先级 |
- 当前阶段 |
- 阶段进度 |
+ 状态 |
整体进度 |
健康度 |
风险标签 |
@@ -323,23 +322,11 @@ function VersionRow({ version, openMenuId, setOpenMenuId, onNavigate, onAction }
- {/* 当前阶段 */}
-
- {getStageLabel(version)}
- |
-
- {/* 阶段进度 */}
+ {/* 状态 */}
- {version.status === 'developing' && version.currentStage ? (
-
-
- {getStageProgress(version)}%
-
- ) : (
- -
- )}
+
+ {getVersionDisplayStatus(version.status as VersionStatus, version.currentStage)}
+
|
{/* 整体进度 */}
diff --git a/apps/web/components/version/CapsuleStages.tsx b/apps/web/components/version/CapsuleStages.tsx
index 91da15c..7633006 100644
--- a/apps/web/components/version/CapsuleStages.tsx
+++ b/apps/web/components/version/CapsuleStages.tsx
@@ -1,55 +1,20 @@
-import { Stage, Role, STAGES, STAGE_INDEX } from '@/lib/stage';
-import type { RoleProgress } from '@/lib/derive';
+import type { Stage } from '@/lib/stage';
+import { STAGES } from '@/lib/stage';
export interface StageProgressItem {
percent: number;
- daysSpent: number;
+ status: 'idle' | 'active' | 'done';
}
-export function CapsuleStages({ currentStage, progress, stageProgress }: {
- currentStage?: Stage;
- progress?: RoleProgress[];
+export function CapsuleStages({ stageProgress }: {
stageProgress?: Partial>;
}) {
- const currentIdx = currentStage !== undefined
- ? (currentStage === 'released' ? STAGES.length : STAGE_INDEX[currentStage])
- : -1;
- const progressMap = (progress ?? []).reduce>((acc, p) => {
- acc[p.role] = { percent: p.percent, daysSpent: p.daysSpent };
- return acc;
- }, {} as Record);
-
- const stageRoleMap: Record = {
- requirement: ['product'],
- product_design: ['product'],
- ui_design: ['ui'],
- dev: ['frontend', 'backend'],
- integration: ['frontend', 'backend'],
- testing: ['testing'],
- released: [],
- };
-
- function getStageInfo(stage: Stage) {
- // 优先使用 stageProgress
- if (stageProgress && stageProgress[stage]) {
- const sp = stageProgress[stage]!;
- return { percent: sp.percent, days: sp.daysSpent, hasData: true };
- }
- const roles = stageRoleMap[stage];
- if (roles.length === 0) return { percent: 0, days: 0, hasData: false };
- const items = roles.map((r) => progressMap[r]).filter(Boolean);
- if (items.length === 0) return { percent: 0, days: 0, hasData: false };
- const percent = Math.round(items.reduce((s, i) => s + i.percent, 0) / items.length);
- const days = Math.max(...items.map((i) => i.daysSpent));
- return { percent, days, hasData: true };
- }
-
return (
{STAGES.map((stage, idx) => {
- const isCompleted = idx < currentIdx;
- const isCurrent = idx === currentIdx;
- const info = getStageInfo(stage.key);
+ const info = stageProgress?.[stage.key];
+ const status = info?.status || 'idle';
+ const percent = info?.percent ?? 0;
return (
-
+
{stage.label}
-
- {isCompleted ? (info.hasData ? `${info.percent}% · ${info.days}天` : '-') : isCurrent ? (info.hasData ? `${info.percent}% · ${info.days}天` : '-') : ''}
-
+ {status !== 'idle' && (
+
+ {percent}%
+
+ )}
- {isCompleted &&
}
- {isCurrent && info.hasData && (
+ {status === 'done' &&
}
+ {status === 'active' && (
)}