之前用日期+工作日×8h 计算耗时太粗糙,现改为: - DevTask startDate/completedAt 使用完整 ISO 时间戳(含时分秒) - TestCase startedAt/completedAt 同上 - 耗时 = (endTimestamp - startTimestamp) / 3600000,精确到0.5h - 详情抽屉显示精确到分钟的时间(YYYY-MM-DD HH:mm) 例如:08:00 开始,10:30 提测,耗时 2.5h(不再是8h) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
112 lines
3.4 KiB
TypeScript
112 lines
3.4 KiB
TypeScript
'use client';
|
|
import { create } from 'zustand';
|
|
import type { DevTask, DevTaskStatus } from '@/lib/dev-task';
|
|
import { canTransition, generateTaskNo } from '@/lib/dev-task';
|
|
|
|
const STORAGE_KEY = 'ftb_dev_tasks_v1';
|
|
|
|
function saveLocal(items: DevTask[]) {
|
|
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(items)); } catch {}
|
|
}
|
|
|
|
function loadLocal(): DevTask[] | null {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (raw) return JSON.parse(raw);
|
|
} catch {}
|
|
return null;
|
|
}
|
|
|
|
interface DevTaskState {
|
|
tasks: DevTask[];
|
|
fetchTasks: () => void;
|
|
createTask: (data: Omit<DevTask, 'id' | 'taskNo' | 'createdAt' | 'updatedAt' | 'actualHours' | 'isBlocked'>) => DevTask;
|
|
updateTask: (id: string, data: Partial<DevTask>) => void;
|
|
deleteTask: (id: string) => void;
|
|
changeStatus: (id: string, to: DevTaskStatus) => { ok: boolean; message?: string };
|
|
setBlocked: (id: string, blocked: boolean, reason?: string, blockedById?: string) => void;
|
|
syncActualHours: (taskId: string, hours: number) => void;
|
|
getByRequirement: (requirementId: string) => DevTask[];
|
|
getByVersionViaRequirements: (requirementIds: string[]) => DevTask[];
|
|
getByAssignee: (assigneeId: string) => DevTask[];
|
|
}
|
|
|
|
export const useDevTaskStore = create<DevTaskState>((set, get) => ({
|
|
tasks: [],
|
|
|
|
fetchTasks: () => {
|
|
const cached = loadLocal();
|
|
if (cached) set({ tasks: cached });
|
|
},
|
|
|
|
createTask: (data) => {
|
|
const list = get().tasks;
|
|
const now = new Date().toISOString();
|
|
const task: DevTask = {
|
|
...data,
|
|
id: `task-${Date.now()}`,
|
|
taskNo: generateTaskNo(list),
|
|
actualHours: 0,
|
|
isBlocked: false,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
const updated = [...list, task];
|
|
set({ tasks: updated });
|
|
saveLocal(updated);
|
|
return task;
|
|
},
|
|
|
|
updateTask: (id, data) => {
|
|
const updated = get().tasks.map((t) =>
|
|
t.id === id ? { ...t, ...data, updatedAt: new Date().toISOString() } : t,
|
|
);
|
|
set({ tasks: updated });
|
|
saveLocal(updated);
|
|
},
|
|
|
|
deleteTask: (id) => {
|
|
const updated = get().tasks.filter((t) => t.id !== id);
|
|
set({ tasks: updated });
|
|
saveLocal(updated);
|
|
},
|
|
|
|
changeStatus: (id, to) => {
|
|
const task = get().tasks.find((t) => t.id === id);
|
|
if (!task) return { ok: false, message: '任务不存在' };
|
|
if (!canTransition(task.status, to)) {
|
|
return { ok: false, message: `不允许从「${task.status}」流转到「${to}」` };
|
|
}
|
|
const now = new Date().toISOString();
|
|
const startDate = (to === 'in_progress' && !task.startDate) ? now : task.startDate;
|
|
const completedAt = (to === 'submitted' && !task.completedAt) ? now : task.completedAt;
|
|
get().updateTask(id, { status: to, startDate, completedAt });
|
|
return { ok: true };
|
|
},
|
|
|
|
setBlocked: (id, blocked, reason, blockedById) => {
|
|
get().updateTask(id, {
|
|
isBlocked: blocked,
|
|
blockReason: blocked ? reason : undefined,
|
|
blockedById: blocked ? blockedById : undefined,
|
|
});
|
|
},
|
|
|
|
syncActualHours: (taskId, hours) => {
|
|
get().updateTask(taskId, { actualHours: hours });
|
|
},
|
|
|
|
getByRequirement: (requirementId) => {
|
|
return get().tasks.filter((t) => t.requirementId === requirementId);
|
|
},
|
|
|
|
getByVersionViaRequirements: (requirementIds) => {
|
|
const idSet = new Set(requirementIds);
|
|
return get().tasks.filter((t) => idSet.has(t.requirementId));
|
|
},
|
|
|
|
getByAssignee: (assigneeId) => {
|
|
return get().tasks.filter((t) => t.assigneeId === assigneeId);
|
|
},
|
|
}));
|