diff --git a/apps/web/app/versions/[id]/page.tsx b/apps/web/app/versions/[id]/page.tsx index bf3c618..fff74bd 100644 --- a/apps/web/app/versions/[id]/page.tsx +++ b/apps/web/app/versions/[id]/page.tsx @@ -365,67 +365,67 @@ export default function VersionDetailPage() { })()} {/* 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 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 ( -
-
- - 开始 - {actualStart ?? '未开始'} -
-
- 预计截止 - {deadline ?? '未设置'} -
- {actualEnd && ( -
- 实际截止 - {actualEnd} -
- )} - {overdueDays > 0 && ( - 逾期 {overdueDays} 天 - )} -
- - 已耗时 {elapsedDays} 天 -
+ return ( +
+
+ + 开始 + {actualStart ?? '未开始'}
- ); - })()} -
+
+ 预计截止 + {deadline ?? '未设置'} +
+ {actualEnd && ( +
+ 实际截止 + {actualEnd} +
+ )} + {overdueDays > 0 && ( + 逾期 {overdueDays} 天 + )} +
+ + 已耗时 {elapsedDays} 天 +
+
+ ); + })()} +
+
参与人员
@@ -449,9 +449,7 @@ export default function VersionDetailPage() { ); })()}
-
-
-
+
相关链接
{(() => { const vPlansCompleted = plans.filter((p) => p.versionId === version.id && p.status === 'completed' && p.resultUrl); @@ -507,28 +505,59 @@ export default function VersionDetailPage() { )}
- {/* 加班原因占比 */} + {/* 加班原因占比 - 饼图 */}
加班原因占比
{reasonRanking.length === 0 ? ( 暂无数据 ) : ( -
- {reasonRanking.map(([reasonId, hours]) => { - const percent = Math.round((hours / reasonTotal) * 100); - const reasonName = OVERTIME_REASON_LABEL[reasonId] || reasonId; - return ( -
-
- {reasonName} - {percent}% +
+ {/* SVG 环形饼图 */} + + {(() => { + 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 ; + } + return ( + + ); + }); + })()} + + + {/* 图例 */} +
+ {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 ( +
+ + {reasonName} + {percent}%
-
-
-
-
- ); - })} + ); + })} +
)}