feat: 开发任务时间字段重构 + 双口径耗时 + 搜索 + 性能优化
核心变更: - DevTask 字段重构:startDate/dueDate/estimateHours/actualHours 替换为 expectedStartAt/expectedEndAt/actualStartAt/actualEndAt(含时分),预计/实际工时按工作时段(9:00-12:00 + 13:00-18:00)派生计算 - 状态机自动化:到点自动切开发中、未到可手动开干、超期需填延后原因 - 新建 lib/work-hours.ts:calcWorkHours(工作时段过滤)、formatWorkHours(X h(Y 天))、calcTwoMetrics(日历/人力双口径) - 新建 lib/dev-task-transitions.ts:状态切换守卫 - 三个 Tab 顶部统计:开发任务(预/日历/人力)、测试用例 / Bug(日历 / 人力);版本概览总人天投入改双行(日历总耗时 / 人力总投入) - 三个 Tab 加搜索框:300ms debounce + 标题/编号模糊匹配 - 性能优化:requirementIds/categories/requirements/testCases 转 Map/Set 索引、Row 组件 React.memo - 全局耗时显示统一带天数换算:8h(1天)、11h(1.4天) 新增文件: SearchInput.tsx, useDebouncedValue.ts, work-hours.ts, dev-task-transitions.ts, 设计文档 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,11 @@ import { useTestCaseStore } from '@/stores/useTestCaseStore';
|
||||
import { useBugStore } from '@/stores/useBugStore';
|
||||
import { useAuthStore } from '@/stores/useAuthStore';
|
||||
import { useMemberStore } from '@/stores/useMemberStore';
|
||||
import { calcGroupProgress as calcDevTaskProgress, calcActualHoursByDates } from '@/lib/dev-task';
|
||||
import { calcGroupProgress as calcDevTaskProgress, getActualHours, getEstimateHours, aggregateDevTaskHours, devTaskIntervals } from '@/lib/dev-task';
|
||||
import { calcWorkHours, formatWorkHours, calcTwoMetrics } from '@/lib/work-hours';
|
||||
import { testCaseIntervals } from '@/lib/test-case';
|
||||
import { bugIntervals } from '@/lib/bug';
|
||||
import { planIntervals } from '@/lib/version-plan';
|
||||
import { formatDateTime } from '@/lib/format';
|
||||
|
||||
const PRIORITY_STYLE: Record<string, string> = {
|
||||
@@ -109,7 +113,7 @@ export default function VersionDetailPage() {
|
||||
if (p.actualStartAt) startDates.push(p.actualStartAt);
|
||||
else if (p.status === 'pending' && p.startTime && new Date(p.startTime) <= new Date()) startDates.push(p.startTime);
|
||||
});
|
||||
vDevTasks.forEach((t) => { if (t.startDate) startDates.push(t.startDate); });
|
||||
vDevTasks.forEach((t) => { if (t.actualStartAt) startDates.push(t.actualStartAt); });
|
||||
vTCs.forEach((c) => { if (c.startedAt) startDates.push(c.startedAt); });
|
||||
|
||||
if (startDates.length === 0 && !version.startDate) return 0;
|
||||
@@ -396,14 +400,14 @@ export default function VersionDetailPage() {
|
||||
if (p.actualStartAt) startDates.push(p.actualStartAt);
|
||||
else if (p.status === 'pending' && p.startTime && new Date(p.startTime) <= new Date()) startDates.push(p.startTime);
|
||||
});
|
||||
vDTs.forEach((t) => { if (t.startDate) startDates.push(t.startDate); });
|
||||
vDTs.forEach((t) => { if (t.actualStartAt) startDates.push(t.actualStartAt); });
|
||||
vTCsAll.forEach((c) => { if (c.startedAt) startDates.push(c.startedAt); });
|
||||
const actualStart = startDates.length > 0 ? formatDateTime(startDates.sort()[0]) : (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); });
|
||||
vDTs.forEach((t) => { if (t.actualEndAt) endDates.push(t.actualEndAt); });
|
||||
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 ? formatDateTime(endDates.sort().reverse()[0]) : null;
|
||||
@@ -702,8 +706,8 @@ export default function VersionDetailPage() {
|
||||
|
||||
// 开发阶段
|
||||
const devCal = calcCalendar(
|
||||
versionDevTasks.filter((t) => t.startDate).map((t) => t.startDate!),
|
||||
versionDevTasks.filter((t) => t.completedAt).map((t) => t.completedAt!),
|
||||
versionDevTasks.filter((t) => t.actualStartAt).map((t) => t.actualStartAt!),
|
||||
versionDevTasks.filter((t) => t.actualEndAt).map((t) => t.actualEndAt!),
|
||||
);
|
||||
|
||||
// 测试阶段
|
||||
@@ -732,16 +736,17 @@ export default function VersionDetailPage() {
|
||||
// 调研/产品/UI 用 startTime→endTime 计算
|
||||
versionPlans.forEach((p) => {
|
||||
if (!p.owner || !p.actualStartAt) return;
|
||||
const hours = calcActualHoursByDates(p.actualStartAt, p.completedAt);
|
||||
const hours = calcWorkHours(p.actualStartAt, p.completedAt ?? new Date().toISOString());
|
||||
addHours(p.owner, p.type === 'research' ? 'research' : p.type === 'product' ? 'product' : 'ui', hours);
|
||||
});
|
||||
versionDevTasks.forEach((t) => {
|
||||
if (!t.startDate || !t.assigneeId) return;
|
||||
addHours(t.assigneeId, 'dev', calcActualHoursByDates(t.startDate, t.completedAt));
|
||||
if (!t.actualStartAt || !t.assigneeId) return;
|
||||
const endIso = t.actualEndAt ?? new Date().toISOString();
|
||||
addHours(t.assigneeId, 'dev', calcWorkHours(t.actualStartAt, endIso));
|
||||
});
|
||||
versionTCs.forEach((c) => {
|
||||
if (!c.startedAt || !c.assigneeId) return;
|
||||
addHours(c.assigneeId, 'test', calcActualHoursByDates(c.startedAt, c.completedAt));
|
||||
addHours(c.assigneeId, 'test', calcWorkHours(c.startedAt, c.completedAt ?? new Date().toISOString()));
|
||||
});
|
||||
|
||||
const personalRanking = Array.from(personalHours.entries())
|
||||
@@ -750,6 +755,15 @@ export default function VersionDetailPage() {
|
||||
const maxHours = personalRanking[0]?.total || 1;
|
||||
const totalHours = personalRanking.reduce((s, p) => s + p.total, 0);
|
||||
|
||||
// 双口径:日历总耗时(合并区间)+ 人力总投入(即 totalHours)
|
||||
const allIntervals = [
|
||||
...planIntervals(versionPlans),
|
||||
...devTaskIntervals(versionDevTasks),
|
||||
...testCaseIntervals(versionTCs),
|
||||
...bugIntervals(versionBugs),
|
||||
];
|
||||
const { calendarHours: versionCalendarHours } = calcTwoMetrics(allIntervals);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* 阶段日历耗时 */}
|
||||
@@ -770,10 +784,14 @@ export default function VersionDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="pt-2 border-t border-[var(--line)]">
|
||||
<div className="pt-2 border-t border-[var(--line)] space-y-1.5">
|
||||
<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)]">{totalHours}h({Math.round(totalHours / 8)}人天)</span>
|
||||
<span className="text-[12px] text-[var(--ink-soft)]">日历总耗时</span>
|
||||
<span className="text-[12px] font-medium tabular-nums text-[var(--ink)]">{formatWorkHours(versionCalendarHours)}</span>
|
||||
</div>
|
||||
<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)]">{formatWorkHours(totalHours)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -794,11 +812,11 @@ export default function VersionDetailPage() {
|
||||
<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-0.5 h-1.5">
|
||||
{item.research > 0 && <div className="h-full rounded-full bg-orange-400" style={{ width: `${(item.research / maxHours) * 100}%` }} title={`调研 ${item.research}h`} />}
|
||||
{item.product > 0 && <div className="h-full rounded-full bg-pink-400" style={{ width: `${(item.product / maxHours) * 100}%` }} title={`产品 ${item.product}h`} />}
|
||||
{item.ui > 0 && <div className="h-full rounded-full bg-indigo-400" style={{ width: `${(item.ui / maxHours) * 100}%` }} title={`UI ${item.ui}h`} />}
|
||||
{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`} />}
|
||||
{item.research > 0 && <div className="h-full rounded-full bg-orange-400" style={{ width: `${(item.research / maxHours) * 100}%` }} title={`调研 ${formatWorkHours(item.research)}`} />}
|
||||
{item.product > 0 && <div className="h-full rounded-full bg-pink-400" style={{ width: `${(item.product / maxHours) * 100}%` }} title={`产品 ${formatWorkHours(item.product)}`} />}
|
||||
{item.ui > 0 && <div className="h-full rounded-full bg-indigo-400" style={{ width: `${(item.ui / maxHours) * 100}%` }} title={`UI ${formatWorkHours(item.ui)}`} />}
|
||||
{item.dev > 0 && <div className="h-full rounded-full bg-blue-400" style={{ width: `${(item.dev / maxHours) * 100}%` }} title={`开发 ${formatWorkHours(item.dev)}`} />}
|
||||
{item.test > 0 && <div className="h-full rounded-full bg-purple-400" style={{ width: `${(item.test / maxHours) * 100}%` }} title={`测试 ${formatWorkHours(item.test)}`} />}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user