feat: 版本概览增加阶段耗时(项目维度)+ 个人耗时排名
项目维度: - 开发阶段日历天数 = min(startDate) → max(completedAt) - 测试阶段日历天数 = min(startedAt) → max(completedAt) - 总人天投入 = 所有人个人耗时之和 个人维度: - 按人汇总开发耗时 + 测试耗时 - 排名展示,条形图区分开发(蓝)/测试(紫) - 可对比出谁花费时间长 解决了多任务日期重叠时叠加计算不合理的问题: 项目看日历跨度,个人看独立累加。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, string> = {
|
||||
P0: 'bg-red-500/10 text-red-600',
|
||||
@@ -377,6 +377,102 @@ export default function VersionDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 阶段耗时 + 个人耗时排名 */}
|
||||
{(() => {
|
||||
// 项目维度:阶段日历天数
|
||||
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<string, { dev: number; test: number }>();
|
||||
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 (
|
||||
<div className="grid grid-cols-2 gap-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="space-y-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-[12px] text-[var(--ink-soft)]">开发阶段</span>
|
||||
<span className="text-[12px] font-medium tabular-nums text-[var(--ink)]">{devCalendarDays > 0 ? `${devCalendarDays}天` : '-'}</span>
|
||||
</div>
|
||||
{devCalendarStart && <span className="text-[10px] text-[var(--ink-muted)]">{devCalendarStart} → {devCalendarEnd || '进行中'}</span>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-[12px] text-[var(--ink-soft)]">测试阶段</span>
|
||||
<span className="text-[12px] font-medium tabular-nums text-[var(--ink)]">{tcCalendarDays > 0 ? `${tcCalendarDays}天` : '-'}</span>
|
||||
</div>
|
||||
{tcCalendarStart && <span className="text-[10px] text-[var(--ink-muted)]">{tcCalendarStart} → {tcCalendarEnd || '进行中'}</span>}
|
||||
</div>
|
||||
<div className="pt-2 border-t border-[var(--line)]">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[12px] text-[var(--ink-soft)]">总人天投入</span>
|
||||
<span className="text-[12px] font-medium tabular-nums text-[var(--ink)]">{personalRanking.reduce((s, p) => s + p.total, 0)}h({Math.round(personalRanking.reduce((s, p) => s + p.total, 0) / 8)}人天)</span>
|
||||
</div>
|
||||
</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-3 font-medium">个人耗时排名</div>
|
||||
{personalRanking.length === 0 ? (
|
||||
<span className="text-[12px] text-[var(--ink-muted)]">暂无数据</span>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{personalRanking.slice(0, 8).map((item, i) => (
|
||||
<div key={item.name}>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className={`flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-semibold ${i < 3 ? 'bg-[var(--accent-soft)] text-[var(--accent)]' : 'bg-[var(--bg-subtle)] text-[var(--ink-muted)]'}`}>{i + 1}</span>
|
||||
<span className="flex-1 text-[12px] text-[var(--ink)]">{item.name}</span>
|
||||
<span className="text-[12px] font-medium tabular-nums text-[var(--ink-soft)]">{item.total}h</span>
|
||||
</div>
|
||||
<div className="ml-7 flex items-center gap-1 h-1.5">
|
||||
{item.dev > 0 && <div className="h-full rounded-full bg-blue-400" style={{ width: `${(item.dev / maxHours) * 100}%` }} title={`开发 ${item.dev}h`} />}
|
||||
{item.test > 0 && <div className="h-full rounded-full bg-purple-400" style={{ width: `${(item.test / maxHours) * 100}%` }} title={`测试 ${item.test}h`} />}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-3 pt-2 text-[10px] text-[var(--ink-muted)]">
|
||||
<span className="flex items-center gap-1"><span className="h-2 w-2 rounded-full bg-blue-400" />开发</span>
|
||||
<span className="flex items-center gap-1"><span className="h-2 w-2 rounded-full bg-purple-400" />测试</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 双栏:日期信息 + 相关链接 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2 space-y-4">
|
||||
|
||||
Reference in New Issue
Block a user