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' && (
-
+
)}