fix: 概览排版优化 + 加班原因改为饼图
1. 参与人员/相关链接排版修复: - 去掉 grid-cols-3 不等高布局 - 改为:总耗时独占一行 + 参与人员/相关链接并排双列 - 不再有被撑高留空白的问题 2. 加班原因占比改为环形饼图: - SVG 环形饼图 + 右侧图例 - 各原因用不同颜色区分 - 图例显示名称 + 百分比 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -365,67 +365,67 @@ export default function VersionDetailPage() {
|
||||
})()}
|
||||
|
||||
{/* 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">
|
||||
{(() => {
|
||||
// 实际开始日期
|
||||
const vPlansAll = plans.filter((p) => p.versionId === version.id);
|
||||
const vReqsAll = requirements.filter((r) => r.versionId === version.id);
|
||||
const vReqIdsAll = new Set(vReqsAll.map((r) => r.id));
|
||||
const vDTs = devTasks.filter((t) => vReqIdsAll.has(t.requirementId));
|
||||
const vTCsAll = testCases.filter((c) => c.versionId === version.id);
|
||||
const vBugsAll = bugs.filter((b) => b.versionId === version.id);
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
||||
{(() => {
|
||||
// 实际开始日期
|
||||
const vPlansAll = plans.filter((p) => p.versionId === version.id);
|
||||
const vReqsAll = requirements.filter((r) => r.versionId === version.id);
|
||||
const vReqIdsAll = new Set(vReqsAll.map((r) => r.id));
|
||||
const vDTs = devTasks.filter((t) => vReqIdsAll.has(t.requirementId));
|
||||
const vTCsAll = testCases.filter((c) => c.versionId === version.id);
|
||||
const vBugsAll = bugs.filter((b) => b.versionId === version.id);
|
||||
|
||||
const startDates: string[] = [];
|
||||
vPlansAll.forEach((p) => { if (p.actualStartAt) startDates.push(p.actualStartAt); });
|
||||
vDTs.forEach((t) => { if (t.startDate) startDates.push(t.startDate); });
|
||||
vTCsAll.forEach((c) => { if (c.startedAt) startDates.push(c.startedAt); });
|
||||
const actualStart = startDates.length > 0 ? startDates.sort()[0].slice(0, 10) : (version.startDate ?? null);
|
||||
const startDates: string[] = [];
|
||||
vPlansAll.forEach((p) => { if (p.actualStartAt) startDates.push(p.actualStartAt); });
|
||||
vDTs.forEach((t) => { if (t.startDate) startDates.push(t.startDate); });
|
||||
vTCsAll.forEach((c) => { if (c.startedAt) startDates.push(c.startedAt); });
|
||||
const actualStart = startDates.length > 0 ? startDates.sort()[0].slice(0, 10) : (version.startDate ?? null);
|
||||
|
||||
// 实际截止日期
|
||||
const endDates: string[] = [];
|
||||
vPlansAll.forEach((p) => { if (p.completedAt) endDates.push(p.completedAt); });
|
||||
vDTs.forEach((t) => { if (t.completedAt) endDates.push(t.completedAt); });
|
||||
vTCsAll.forEach((c) => { if (c.completedAt) endDates.push(c.completedAt); });
|
||||
vBugsAll.forEach((b) => { if (b.closedAt) endDates.push(b.closedAt); });
|
||||
const actualEnd = endDates.length > 0 ? endDates.sort().reverse()[0].slice(0, 10) : null;
|
||||
// 实际截止日期
|
||||
const endDates: string[] = [];
|
||||
vPlansAll.forEach((p) => { if (p.completedAt) endDates.push(p.completedAt); });
|
||||
vDTs.forEach((t) => { if (t.completedAt) endDates.push(t.completedAt); });
|
||||
vTCsAll.forEach((c) => { if (c.completedAt) endDates.push(c.completedAt); });
|
||||
vBugsAll.forEach((b) => { if (b.closedAt) endDates.push(b.closedAt); });
|
||||
const actualEnd = endDates.length > 0 ? endDates.sort().reverse()[0].slice(0, 10) : null;
|
||||
|
||||
// 逾期天数
|
||||
const deadline = version.expectedReleaseDate;
|
||||
let overdueDays = 0;
|
||||
if (deadline && actualEnd) {
|
||||
overdueDays = Math.floor((new Date(actualEnd).getTime() - new Date(deadline).getTime()) / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
// 逾期天数
|
||||
const deadline = version.expectedReleaseDate;
|
||||
let overdueDays = 0;
|
||||
if (deadline && actualEnd) {
|
||||
overdueDays = Math.floor((new Date(actualEnd).getTime() - new Date(deadline).getTime()) / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 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-muted)]">开始</span>
|
||||
<span className="text-[var(--ink)]">{actualStart ?? '未开始'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[var(--ink-muted)]">预计截止</span>
|
||||
<span className="text-[var(--ink)]">{deadline ?? '未设置'}</span>
|
||||
</div>
|
||||
{actualEnd && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[var(--ink-muted)]">实际截止</span>
|
||||
<span className="text-[var(--ink)]">{actualEnd}</span>
|
||||
</div>
|
||||
)}
|
||||
{overdueDays > 0 && (
|
||||
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-red-50 text-red-600">逾期 {overdueDays} 天</span>
|
||||
)}
|
||||
<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>
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 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-muted)]">开始</span>
|
||||
<span className="text-[var(--ink)]">{actualStart ?? '未开始'}</span>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[var(--ink-muted)]">预计截止</span>
|
||||
<span className="text-[var(--ink)]">{deadline ?? '未设置'}</span>
|
||||
</div>
|
||||
{actualEnd && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[var(--ink-muted)]">实际截止</span>
|
||||
<span className="text-[var(--ink)]">{actualEnd}</span>
|
||||
</div>
|
||||
)}
|
||||
{overdueDays > 0 && (
|
||||
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-red-50 text-red-600">逾期 {overdueDays} 天</span>
|
||||
)}
|
||||
<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="grid grid-cols-2 gap-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="text-[11px] text-[var(--ink-muted)] font-medium">参与人员</div>
|
||||
@@ -449,9 +449,7 @@ export default function VersionDetailPage() {
|
||||
);
|
||||
})()}
|
||||
</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="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>
|
||||
{(() => {
|
||||
const vPlansCompleted = plans.filter((p) => p.versionId === version.id && p.status === 'completed' && p.resultUrl);
|
||||
@@ -507,28 +505,59 @@ export default function VersionDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 加班原因占比 */}
|
||||
{/* 加班原因占比 - 饼图 */}
|
||||
<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>
|
||||
{reasonRanking.length === 0 ? (
|
||||
<span className="text-[12px] text-[var(--ink-muted)]">暂无数据</span>
|
||||
) : (
|
||||
<div className="space-y-2.5">
|
||||
{reasonRanking.map(([reasonId, hours]) => {
|
||||
const percent = Math.round((hours / reasonTotal) * 100);
|
||||
const reasonName = OVERTIME_REASON_LABEL[reasonId] || reasonId;
|
||||
return (
|
||||
<div key={reasonId}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-[12px] text-[var(--ink-soft)]">{reasonName}</span>
|
||||
<span className="text-[11px] tabular-nums text-[var(--ink-muted)]">{percent}%</span>
|
||||
<div className="flex items-center gap-4">
|
||||
{/* 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 reasonName = OVERTIME_REASON_LABEL[reasonId] || reasonId;
|
||||
return (
|
||||
<div key={reasonId} className="flex items-center gap-2 text-[11px]">
|
||||
<span className="h-2.5 w-2.5 rounded-sm shrink-0" style={{ backgroundColor: colors[i % colors.length] }} />
|
||||
<span className="flex-1 text-[var(--ink-soft)] truncate">{reasonName}</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>
|
||||
|
||||
Reference in New Issue
Block a user