refactor: 版本概览重新排版
1. 去掉顶部 Tag Row(P0/进行中/规划中/100健康/负责人缺失等标签) 因为状态胶囊已经展示了阶段信息 2. 概览内容重新排列: - 风险详情(最上方) - 统计卡片:关联需求/待调研/待方案设计/待开发/开发中/未关闭Bug/加班时长 - 状态胶囊 - 项目总耗时 + 参与人员 + 相关链接 - 加班时长排名 + 加班原因占比 - 阶段耗时 + 个人耗时排名 - 健康趋势(注释掉,代码保留) 3. 统计卡片去掉"开发任务"和"测试用例" 新增"待调研"和"待方案设计" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -224,46 +224,7 @@ export default function VersionDetailPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Tag row */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{version.priority && (
|
||||
<span className={`text-[11px] font-semibold px-2 py-0.5 rounded-full ${PRIORITY_STYLE[version.priority] || PRIORITY_STYLE.P3}`}>
|
||||
{version.priority}
|
||||
</span>
|
||||
)}
|
||||
<span className={`text-[11px] px-2 py-0.5 rounded-full ${VERSION_STATUS_BG[version.status]}`}>
|
||||
{getVersionDisplayStatus(version.status, version.currentStage)}
|
||||
</span>
|
||||
<span className={`text-[11px] font-medium px-2 py-0.5 rounded-full ${EXECUTION_STATUS_COLOR[executionStatus]}`}>
|
||||
{EXECUTION_STATUS_LABEL[executionStatus]}
|
||||
</span>
|
||||
<span className={`inline-flex items-center gap-1.5 text-[11px] font-semibold tabular-nums px-2 py-0.5 rounded-full ${healthLevel === 'critical' ? 'bg-red-50' : healthLevel === 'risk' ? 'bg-orange-50' : healthLevel === 'attention' ? 'bg-amber-50' : 'bg-emerald-50'} ${HEALTH_LEVEL_COLOR[healthLevel]}`}>
|
||||
<span className={`h-1.5 w-1.5 rounded-full ${HEALTH_LEVEL_DOT[healthLevel]}`} />
|
||||
{healthScore} {HEALTH_LEVEL_LABEL[healthLevel]}
|
||||
</span>
|
||||
{riskTags.map((tag) => (
|
||||
<span key={tag.key} className={`inline-flex items-center rounded border px-1.5 py-0.5 text-[10px] font-medium ${getTagStyle(tag.severity)}`}>
|
||||
{tag.label}
|
||||
</span>
|
||||
))}
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-[var(--bg-subtle)] px-2.5 py-0.5 text-[11px] text-[var(--ink-soft)]">
|
||||
<Package className="h-3 w-3" />{version.productName} / {version.projectName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-4 gap-3 sm:grid-cols-8">
|
||||
<StatCard label="关联需求" value={versionReqs.length} />
|
||||
<StatCard label="计划任务" value={plans.filter((p) => p.versionId === version.id).length} />
|
||||
<StatCard label="开发任务" value={versionDevTasks.length} />
|
||||
<StatCard label="测试用例" value={versionTCs.length} />
|
||||
<StatCard label="待开发" value={devTaskTodo} />
|
||||
<StatCard label="开发中" value={devTaskInProgress} accent />
|
||||
<StatCard label="未关闭Bug" value={bugOpenCount} warn={bugOpenCount > 0} />
|
||||
<StatCard label="加班时长" value={`${totalOTHours}h`} accent />
|
||||
</div>
|
||||
|
||||
{/* 风险详情(胶囊条上方) */}
|
||||
{/* 1. 风险详情 - 最上方 */}
|
||||
{riskTags.length > 0 && (
|
||||
<div className="rounded-xl border border-orange-200 bg-orange-50/40 p-4 max-h-[200px] overflow-y-auto">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
@@ -286,7 +247,25 @@ export default function VersionDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Capsule stages */}
|
||||
{/* 2. 统计卡片 */}
|
||||
{(() => {
|
||||
const vPlans = plans.filter((p) => p.versionId === version.id);
|
||||
const pendingResearch = vPlans.filter((p) => p.type === 'research' && p.status !== 'completed').length;
|
||||
const pendingProduct = vPlans.filter((p) => p.type === 'product' && p.status !== 'completed').length;
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-3 sm:grid-cols-7">
|
||||
<StatCard label="关联需求" value={versionReqs.length} />
|
||||
<StatCard label="待调研" value={pendingResearch} warn={pendingResearch > 0} />
|
||||
<StatCard label="待方案设计" value={pendingProduct} warn={pendingProduct > 0} />
|
||||
<StatCard label="待开发" value={devTaskTodo} />
|
||||
<StatCard label="开发中" value={devTaskInProgress} accent />
|
||||
<StatCard label="未关闭Bug" value={bugOpenCount} warn={bugOpenCount > 0} />
|
||||
<StatCard label="加班时长" value={`${totalOTHours}h`} accent />
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 3. 状态胶囊 */}
|
||||
{(() => {
|
||||
const vPlans = plans.filter((p) => p.versionId === version.id);
|
||||
const researchPlans = vPlans.filter((p) => p.type === 'research');
|
||||
@@ -356,7 +335,67 @@ export default function VersionDetailPage() {
|
||||
return <CapsuleStages stageProgress={stageProgress as any} />;
|
||||
})()}
|
||||
|
||||
{/* 双栏:加班排名 + 原因占比 */}
|
||||
{/* 4. 项目总耗时 + 参与人员 + 相关链接 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2 space-y-4">
|
||||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
||||
<div className="flex items-center gap-4 text-[13px]">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Calendar className="h-3.5 w-3.5 text-[var(--ink-muted)]" />
|
||||
<span className="text-[var(--ink)]">{version.startDate ?? '未设置'}</span>
|
||||
<span className="text-[var(--ink-muted)]">—</span>
|
||||
<span className="text-[var(--ink)]">{version.expectedReleaseDate ?? '未设置'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-[var(--ink-soft)]">
|
||||
<Clock className="h-3.5 w-3.5 text-[var(--ink-muted)]" />
|
||||
已耗时 <span className="font-medium text-[var(--ink)]">{elapsedDays}</span> 天
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
||||
<div className="text-[11px] text-[var(--ink-muted)] mb-2 font-medium">参与人员</div>
|
||||
{(() => {
|
||||
const planMembers = plans
|
||||
.filter((p) => p.versionId === version.id)
|
||||
.map((p) => ({ role: p.type === 'research' ? 'research' as const : p.type === 'product' ? 'product' as const : 'ui' as const, name: p.owner }));
|
||||
const roleLabel: Record<string, string> = { research: '调研', product: '产品', ui: 'UI' };
|
||||
const seen = new Set<string>();
|
||||
const dedupPlanMembers = planMembers.filter((m) => {
|
||||
const key = `${m.role}:${m.name}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
const existingKeys = new Set((version.members ?? []).map((m) => `${m.role}:${m.name}`));
|
||||
const extraMembers = dedupPlanMembers.filter((m) => !existingKeys.has(`${m.role}:${m.name}`));
|
||||
const allMembers = [...(version.members ?? []), ...extraMembers];
|
||||
if (allMembers.length === 0) return <span className="text-[12px] text-[var(--ink-muted)]">暂无成员</span>;
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{allMembers.map((m, i) => (
|
||||
<span key={`${m.name}-${i}`} className="inline-flex items-center gap-1 rounded-full bg-[var(--bg-subtle)] px-2.5 py-1 text-[11px] text-[var(--ink-soft)]">
|
||||
<span className="text-[var(--ink-muted)]">{roleLabel[m.role] ?? m.role}</span>
|
||||
<span className="font-medium text-[var(--ink)]">{m.name}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4 h-full">
|
||||
<div className="text-[11px] text-[var(--ink-muted)] mb-3 font-medium">相关链接</div>
|
||||
<div className="space-y-3">
|
||||
<LinkItem icon={<FileText className="h-3.5 w-3.5" />} label="调研报告" url={version.links?.research} />
|
||||
<LinkItem icon={<Layout className="h-3.5 w-3.5" />} label="原型地址" url={version.links?.prototype} />
|
||||
<LinkItem icon={<Palette className="h-3.5 w-3.5" />} label="UI设计稿" url={version.links?.ui} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 5. 加班时长排名 + 加班原因占比 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* 参与人员加班排名 */}
|
||||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
||||
@@ -554,72 +593,11 @@ export default function VersionDetailPage() {
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 双栏:日期信息 + 相关链接 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2 space-y-4">
|
||||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
||||
<div className="flex items-center gap-4 text-[13px]">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Calendar className="h-3.5 w-3.5 text-[var(--ink-muted)]" />
|
||||
<span className="text-[var(--ink)]">{version.startDate ?? '未设置'}</span>
|
||||
<span className="text-[var(--ink-muted)]">—</span>
|
||||
<span className="text-[var(--ink)]">{version.expectedReleaseDate ?? '未设置'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-[var(--ink-soft)]">
|
||||
<Clock className="h-3.5 w-3.5 text-[var(--ink-muted)]" />
|
||||
已耗时 <span className="font-medium text-[var(--ink)]">{elapsedDays}</span> 天
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
||||
<div className="text-[11px] text-[var(--ink-muted)] mb-2 font-medium">参与人员</div>
|
||||
{(() => {
|
||||
const planMembers = plans
|
||||
.filter((p) => p.versionId === version.id)
|
||||
.map((p) => ({ role: p.type === 'research' ? 'research' as const : p.type === 'product' ? 'product' as const : 'ui' as const, name: p.owner }));
|
||||
const roleLabel: Record<string, string> = { research: '调研', product: '产品', ui: 'UI' };
|
||||
// 按 role+name 去重(同阶段同一人只显示一次)
|
||||
const seen = new Set<string>();
|
||||
const dedupPlanMembers = planMembers.filter((m) => {
|
||||
const key = `${m.role}:${m.name}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
const existingKeys = new Set((version.members ?? []).map((m) => `${m.role}:${m.name}`));
|
||||
const extraMembers = dedupPlanMembers.filter((m) => !existingKeys.has(`${m.role}:${m.name}`));
|
||||
const allMembers = [...(version.members ?? []), ...extraMembers];
|
||||
if (allMembers.length === 0) return <span className="text-[12px] text-[var(--ink-muted)]">暂无成员</span>;
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{allMembers.map((m, i) => (
|
||||
<span key={`${m.name}-${i}`} className="inline-flex items-center gap-1 rounded-full bg-[var(--bg-subtle)] px-2.5 py-1 text-[11px] text-[var(--ink-soft)]">
|
||||
<span className="text-[var(--ink-muted)]">{roleLabel[m.role] ?? m.role}</span>
|
||||
<span className="font-medium text-[var(--ink)]">{m.name}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4 h-full">
|
||||
<div className="text-[11px] text-[var(--ink-muted)] mb-3 font-medium">相关链接</div>
|
||||
<div className="space-y-3">
|
||||
<LinkItem icon={<FileText className="h-3.5 w-3.5" />} label="调研报告" url={version.links?.research} />
|
||||
<LinkItem icon={<Layout className="h-3.5 w-3.5" />} label="原型地址" url={version.links?.prototype} />
|
||||
<LinkItem icon={<Palette className="h-3.5 w-3.5" />} label="UI设计稿" url={version.links?.ui} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 健康趋势(最底部) */}
|
||||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4 max-w-[320px]">
|
||||
{/* 健康趋势 - 暂时不显示 */}
|
||||
{/* <div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4 max-w-[320px]">
|
||||
<span className="text-[11px] font-medium text-[var(--ink-muted)] mb-2 block">健康趋势</span>
|
||||
<HealthTrend data={generateMockTrend(healthScore, 7)} />
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
|
||||
Reference in New Issue
Block a user