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 { useTestCaseStore } from '@/stores/useTestCaseStore';
|
||||||
import { useBugStore } from '@/stores/useBugStore';
|
import { useBugStore } from '@/stores/useBugStore';
|
||||||
import { useAuthStore } from '@/stores/useAuthStore';
|
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> = {
|
const PRIORITY_STYLE: Record<string, string> = {
|
||||||
P0: 'bg-red-500/10 text-red-600',
|
P0: 'bg-red-500/10 text-red-600',
|
||||||
@@ -377,6 +377,102 @@ export default function VersionDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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="grid grid-cols-3 gap-4">
|
||||||
<div className="col-span-2 space-y-4">
|
<div className="col-span-2 space-y-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user