From b3ccc17c2c2ef955ab9f77778029a3d344ff01aa Mon Sep 17 00:00:00 2001 From: Script Generator Date: Fri, 12 Jun 2026 15:48:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=89=88=E6=9C=AC=E6=A6=82=E8=A7=88?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=98=B6=E6=AE=B5=E8=80=97=E6=97=B6=EF=BC=88?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E7=BB=B4=E5=BA=A6=EF=BC=89+=20=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E8=80=97=E6=97=B6=E6=8E=92=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 项目维度: - 开发阶段日历天数 = min(startDate) → max(completedAt) - 测试阶段日历天数 = min(startedAt) → max(completedAt) - 总人天投入 = 所有人个人耗时之和 个人维度: - 按人汇总开发耗时 + 测试耗时 - 排名展示,条形图区分开发(蓝)/测试(紫) - 可对比出谁花费时间长 解决了多任务日期重叠时叠加计算不合理的问题: 项目看日历跨度,个人看独立累加。 Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/app/versions/[id]/page.tsx | 98 ++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/apps/web/app/versions/[id]/page.tsx b/apps/web/app/versions/[id]/page.tsx index 39c1c5f..913f68f 100644 --- a/apps/web/app/versions/[id]/page.tsx +++ b/apps/web/app/versions/[id]/page.tsx @@ -25,7 +25,7 @@ import { useDevTaskStore } from '@/stores/useDevTaskStore'; import { useTestCaseStore } from '@/stores/useTestCaseStore'; import { useBugStore } from '@/stores/useBugStore'; import { useAuthStore } from '@/stores/useAuthStore'; -import { calcGroupProgress as calcDevTaskProgress } from '@/lib/dev-task'; +import { calcGroupProgress as calcDevTaskProgress, calcActualHoursByDates } from '@/lib/dev-task'; const PRIORITY_STYLE: Record = { P0: 'bg-red-500/10 text-red-600', @@ -377,6 +377,102 @@ export default function VersionDetailPage() { + {/* 阶段耗时 + 个人耗时排名 */} + {(() => { + // 项目维度:阶段日历天数 + const devStarts = versionDevTasks.filter((t) => t.startDate).map((t) => t.startDate!); + const devEnds = versionDevTasks.filter((t) => t.completedAt).map((t) => t.completedAt!); + const devCalendarStart = devStarts.length > 0 ? devStarts.sort()[0] : null; + const devCalendarEnd = devEnds.length > 0 ? devEnds.sort().reverse()[0] : null; + const devCalendarDays = devCalendarStart && devCalendarEnd ? Math.max(1, Math.ceil((new Date(devCalendarEnd).getTime() - new Date(devCalendarStart).getTime()) / (1000 * 60 * 60 * 24)) + 1) : 0; + + const tcStarts = versionTCs.filter((c) => c.startedAt).map((c) => c.startedAt!); + const tcEnds = versionTCs.filter((c) => c.completedAt).map((c) => c.completedAt!); + const tcCalendarStart = tcStarts.length > 0 ? tcStarts.sort()[0] : null; + const tcCalendarEnd = tcEnds.length > 0 ? tcEnds.sort().reverse()[0] : null; + const tcCalendarDays = tcCalendarStart && tcCalendarEnd ? Math.max(1, Math.ceil((new Date(tcCalendarEnd).getTime() - new Date(tcCalendarStart).getTime()) / (1000 * 60 * 60 * 24)) + 1) : 0; + + // 个人维度:每人耗时汇总(开发 + 测试) + const personalHours = new Map(); + versionDevTasks.forEach((t) => { + if (!t.startDate || !t.assigneeId) return; + const hours = calcActualHoursByDates(t.startDate, t.completedAt); + const prev = personalHours.get(t.assigneeId) || { dev: 0, test: 0 }; + prev.dev += hours; + personalHours.set(t.assigneeId, prev); + }); + versionTCs.forEach((c) => { + if (!c.startedAt || !c.assigneeId) return; + const hours = calcActualHoursByDates(c.startedAt, c.completedAt); + const prev = personalHours.get(c.assigneeId) || { dev: 0, test: 0 }; + prev.test += hours; + personalHours.set(c.assigneeId, prev); + }); + const personalRanking = Array.from(personalHours.entries()) + .map(([name, h]) => ({ name, dev: h.dev, test: h.test, total: h.dev + h.test })) + .sort((a, b) => b.total - a.total); + const maxHours = personalRanking[0]?.total || 1; + + return ( +
+ {/* 阶段日历耗时 */} +
+
阶段耗时(项目维度)
+
+
+
+ 开发阶段 + {devCalendarDays > 0 ? `${devCalendarDays}天` : '-'} +
+ {devCalendarStart && {devCalendarStart} → {devCalendarEnd || '进行中'}} +
+
+
+ 测试阶段 + {tcCalendarDays > 0 ? `${tcCalendarDays}天` : '-'} +
+ {tcCalendarStart && {tcCalendarStart} → {tcCalendarEnd || '进行中'}} +
+
+
+ 总人天投入 + {personalRanking.reduce((s, p) => s + p.total, 0)}h({Math.round(personalRanking.reduce((s, p) => s + p.total, 0) / 8)}人天) +
+
+
+
+ + {/* 个人耗时排名 */} +
+
个人耗时排名
+ {personalRanking.length === 0 ? ( + 暂无数据 + ) : ( +
+ {personalRanking.slice(0, 8).map((item, i) => ( +
+
+ {i + 1} + {item.name} + {item.total}h +
+
+ {item.dev > 0 &&
} + {item.test > 0 &&
} +
+
+ ))} +
+ 开发 + 测试 +
+
+ )} +
+
+ ); + })()} + {/* 双栏:日期信息 + 相关链接 */}