Files
ftb-project-management/apps/web/lib/dev-task-transitions.ts
Script Generator 1f5bed0c79 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>
2026-06-16 17:44:59 +08:00

63 lines
2.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { DevTask } from './dev-task';
/**
* 是否可以"手动"切到 in_progress
* - 仅当 todo 且未到 expectedStartAt 时返回 true
* - 已到点自动机制接管UI 不应再显示手动按钮
* - 已超期:返回 falseUI 应要求填 delayReason
*/
export function canManualStart(task: DevTask, now: Date = new Date()): boolean {
if (task.status !== 'todo') return false;
if (!task.expectedStartAt) return true;
return now.getTime() < new Date(task.expectedStartAt).getTime();
}
/**
* 自动切换扫描:返回所有需要从 todo 切到 in_progress 的任务
* - status === 'todo'
* - now >= expectedStartAt且未超期超过 0 秒就自动切——超期未切的可单独走 needsDelayReason 流程)
*
* 注意:超期任务也包含在内,由调用方决定要不要扫;当前策略是 fetchTasks 调用时只扫"刚好到点"
* 超期任务保留在 todo 等用户填 delayReason。所以这里加 maxOverdueMs 参数,默认 24h 内的算到点。
*/
export function findTasksToAutoStart(
tasks: DevTask[],
now: Date = new Date(),
maxOverdueMs: number = 24 * 60 * 60 * 1000,
): Array<{ taskId: string; actualStartAt: string }> {
const result: Array<{ taskId: string; actualStartAt: string }> = [];
for (const t of tasks) {
if (t.status !== 'todo' || !t.expectedStartAt || t.actualStartAt) continue;
const expected = new Date(t.expectedStartAt).getTime();
if (isNaN(expected)) continue;
const diff = now.getTime() - expected;
if (diff >= 0 && diff <= maxOverdueMs) {
result.push({ taskId: t.id, actualStartAt: t.expectedStartAt });
}
}
return result;
}
/**
* 是否处于"超期手动开干需填延后原因"状态
* - todo 且 now > expectedStartAt
*/
export function needsDelayReason(task: DevTask, now: Date = new Date()): boolean {
if (task.status !== 'todo' || !task.expectedStartAt) return false;
return now.getTime() > new Date(task.expectedStartAt).getTime();
}
/**
* 进入 in_progress 时计算 actualStartAt
* - auto: 等于 expectedStartAt
* - manual: 等于 now
*/
export function deriveActualStartAt(
task: DevTask,
trigger: 'auto' | 'manual',
now: Date = new Date(),
): string {
if (trigger === 'auto' && task.expectedStartAt) return task.expectedStartAt;
return now.toISOString();
}