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:
Script Generator
2026-06-16 17:44:59 +08:00
parent 4ac0dab4c5
commit 1f5bed0c79
25 changed files with 1192 additions and 221 deletions

View File

@@ -18,8 +18,9 @@ import { DevTaskDetailDrawer } from '@/components/dev-task/DevTaskDetailDrawer';
import { TestCaseDetailDrawer } from '@/components/test-case/TestCaseDetailDrawer';
import { BugDetailDrawer } from '@/components/bug/BugDetailDrawer';
import { BugCreateModal } from '@/components/bug/BugCreateModal';
import { DEV_TASK_STATUS_LABEL, DEV_TASK_STATUS_COLOR } from '@/lib/dev-task';
import { DEV_TASK_STATUS_LABEL, DEV_TASK_STATUS_COLOR, getEstimateHours, getActualHours } from '@/lib/dev-task';
import { formatDateTime } from '@/lib/format';
import { formatShortTime, formatWorkHours } from '@/lib/work-hours';
import { TEST_CASE_STATUS_LABEL, TEST_CASE_STATUS_COLOR } from '@/lib/test-case';
import { BUG_STATUS_LABEL, BUG_STATUS_COLOR, BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug';
@@ -293,6 +294,18 @@ function ProjectNode({ name, versions, selectedVersionId, onSelect }: {
function WorkItemCard({ item, onNavigate, onClick }: { item: WorkItem; onNavigate: () => void; onClick: () => void }) {
const statusBadge = getStatusBadge(item);
const isDevTask = item.type === 'devTask';
const devTaskRaw = isDevTask ? (item.raw as any) : null;
const devEstimate = devTaskRaw ? getEstimateHours(devTaskRaw) : 0;
const devActual = devTaskRaw ? getActualHours(devTaskRaw) : 0;
const devOverrun = devActual > devEstimate && devEstimate > 0;
const devTimeRange = devTaskRaw ? (
devTaskRaw.actualStartAt
? `实际 ${formatShortTime(devTaskRaw.actualStartAt)}${devTaskRaw.actualEndAt ? formatShortTime(devTaskRaw.actualEndAt) : '进行中'}`
: (devTaskRaw.expectedStartAt && devTaskRaw.expectedEndAt
? `预计 ${formatShortTime(devTaskRaw.expectedStartAt)}${formatShortTime(devTaskRaw.expectedEndAt)}`
: '')
) : '';
return (
<div onClick={onClick} className={`rounded-xl border border-[var(--line)] bg-[var(--bg-card)] px-4 py-3 transition-colors hover:border-[var(--accent)] cursor-pointer ${item.completed ? 'opacity-60' : ''}`}>
@@ -322,7 +335,16 @@ function WorkItemCard({ item, onNavigate, onClick }: { item: WorkItem; onNavigat
<span>{item.projectName}</span>
<span className="text-[var(--line)]">/</span>
<button onClick={(e) => { e.stopPropagation(); onNavigate(); }} className="text-[var(--accent)] hover:underline">{item.versionName}</button>
{item.extra?.dueDate && <span className="ml-2"> {item.extra.dueDate}</span>}
{isDevTask && devTimeRange && (
<>
<span className="ml-2 tabular-nums">{devTimeRange}</span>
{devEstimate > 0 && (
<span className={`ml-2 tabular-nums ${devActual > 0 ? (devOverrun ? 'text-red-600' : (devActual < devEstimate ? 'text-emerald-600' : '')) : ''}`}>
{devActual > 0 ? `${formatWorkHours(devActual)} / ${formatWorkHours(devEstimate)}` : `${formatWorkHours(devEstimate)}`}
</span>
)}
</>
)}
{item.extra?.startTime && <span className="ml-2">{formatDateTime(item.extra.startTime)} {formatDateTime(item.extra.endTime)}</span>}
</div>
</div>