fix: 概览排版优化 + 加班原因改为饼图

1. 参与人员/相关链接排版修复:
   - 去掉 grid-cols-3 不等高布局
   - 改为:总耗时独占一行 + 参与人员/相关链接并排双列
   - 不再有被撑高留空白的问题

2. 加班原因占比改为环形饼图:
   - SVG 环形饼图 + 右侧图例
   - 各原因用不同颜色区分
   - 图例显示名称 + 百分比

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Script Generator
2026-06-15 14:40:10 +08:00
parent 2c1b68c46f
commit b30c460571

View File

@@ -365,8 +365,7 @@ export default function VersionDetailPage() {
})()} })()}
{/* 4. 项目总耗时 + 参与人员 + 相关链接 */} {/* 4. 项目总耗时 + 参与人员 + 相关链接 */}
<div className="grid grid-cols-3 gap-4"> <div className="space-y-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="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
{(() => { {(() => {
// 实际开始日期 // 实际开始日期
@@ -426,6 +425,7 @@ export default function VersionDetailPage() {
); );
})()} })()}
</div> </div>
<div className="grid grid-cols-2 gap-4">
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4"> <div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="text-[11px] text-[var(--ink-muted)] font-medium"></div> <div className="text-[11px] text-[var(--ink-muted)] font-medium"></div>
@@ -449,9 +449,7 @@ export default function VersionDetailPage() {
); );
})()} })()}
</div> </div>
</div> <div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<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="text-[11px] text-[var(--ink-muted)] mb-3 font-medium"></div>
{(() => { {(() => {
const vPlansCompleted = plans.filter((p) => p.versionId === version.id && p.status === 'completed' && p.resultUrl); const vPlansCompleted = plans.filter((p) => p.versionId === version.id && p.status === 'completed' && p.resultUrl);
@@ -507,29 +505,60 @@ export default function VersionDetailPage() {
)} )}
</div> </div>
{/* 加班原因占比 */} {/* 加班原因占比 - 饼图 */}
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4"> <div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[11px] text-[var(--ink-muted)] mb-3 font-medium"></div> <div className="text-[11px] text-[var(--ink-muted)] mb-3 font-medium"></div>
{reasonRanking.length === 0 ? ( {reasonRanking.length === 0 ? (
<span className="text-[12px] text-[var(--ink-muted)]"></span> <span className="text-[12px] text-[var(--ink-muted)]"></span>
) : ( ) : (
<div className="space-y-2.5"> <div className="flex items-center gap-4">
{reasonRanking.map(([reasonId, hours]) => { {/* SVG 环形饼图 */}
<svg viewBox="0 0 100 100" className="h-28 w-28 shrink-0">
{(() => {
const colors = ['#3b82f6', '#f97316', '#8b5cf6', '#10b981', '#ef4444', '#f59e0b', '#6366f1', '#ec4899'];
let cumPercent = 0;
return reasonRanking.map(([reasonId, hours], i) => {
const percent = hours / reasonTotal;
const startAngle = cumPercent * 360;
cumPercent += percent;
const endAngle = cumPercent * 360;
const largeArc = percent > 0.5 ? 1 : 0;
const startRad = ((startAngle - 90) * Math.PI) / 180;
const endRad = ((endAngle - 90) * Math.PI) / 180;
const x1 = 50 + 40 * Math.cos(startRad);
const y1 = 50 + 40 * Math.sin(startRad);
const x2 = 50 + 40 * Math.cos(endRad);
const y2 = 50 + 40 * Math.sin(endRad);
if (percent >= 1) {
return <circle key={reasonId} cx="50" cy="50" r="40" fill={colors[i % colors.length]} />;
}
return (
<path
key={reasonId}
d={`M 50 50 L ${x1} ${y1} A 40 40 0 ${largeArc} 1 ${x2} ${y2} Z`}
fill={colors[i % colors.length]}
/>
);
});
})()}
<circle cx="50" cy="50" r="22" fill="var(--bg-card)" />
</svg>
{/* 图例 */}
<div className="space-y-1.5 flex-1">
{reasonRanking.map(([reasonId, hours], i) => {
const colors = ['#3b82f6', '#f97316', '#8b5cf6', '#10b981', '#ef4444', '#f59e0b', '#6366f1', '#ec4899'];
const percent = Math.round((hours / reasonTotal) * 100); const percent = Math.round((hours / reasonTotal) * 100);
const reasonName = OVERTIME_REASON_LABEL[reasonId] || reasonId; const reasonName = OVERTIME_REASON_LABEL[reasonId] || reasonId;
return ( return (
<div key={reasonId}> <div key={reasonId} className="flex items-center gap-2 text-[11px]">
<div className="flex items-center justify-between mb-1"> <span className="h-2.5 w-2.5 rounded-sm shrink-0" style={{ backgroundColor: colors[i % colors.length] }} />
<span className="text-[12px] text-[var(--ink-soft)]">{reasonName}</span> <span className="flex-1 text-[var(--ink-soft)] truncate">{reasonName}</span>
<span className="text-[11px] tabular-nums text-[var(--ink-muted)]">{percent}%</span> <span className="tabular-nums text-[var(--ink-muted)]">{percent}%</span>
</div>
<div className="h-1.5 rounded-full bg-[var(--bg-subtle)] overflow-hidden">
<div className="h-full rounded-full bg-[var(--accent)] transition-all" style={{ width: `${percent}%` }} />
</div>
</div> </div>
); );
})} })}
</div> </div>
</div>
)} )}
</div> </div>
</div> </div>