fix: 实际耗时改为精确时间戳计算(精确到0.5h)
之前用日期+工作日×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>
This commit is contained in:
@@ -161,7 +161,7 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose }: Props) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Play className="h-3 w-3 text-[var(--ink-muted)]" />
|
<Play className="h-3 w-3 text-[var(--ink-muted)]" />
|
||||||
<span className="text-[var(--ink-muted)]">开始开发</span>
|
<span className="text-[var(--ink-muted)]">开始开发</span>
|
||||||
<span className="text-[var(--ink)]">{task.startDate}</span>
|
<span className="text-[var(--ink)]">{task.startDate.slice(0, 16).replace('T', ' ')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{task.dueDate && (
|
{task.dueDate && (
|
||||||
@@ -175,7 +175,7 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose }: Props) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Calendar className="h-3 w-3 text-[var(--ink-muted)]" />
|
<Calendar className="h-3 w-3 text-[var(--ink-muted)]" />
|
||||||
<span className="text-[var(--ink-muted)]">提测</span>
|
<span className="text-[var(--ink-muted)]">提测</span>
|
||||||
<span className="text-emerald-600 font-medium">{task.completedAt}</span>
|
<span className="text-emerald-600 font-medium">{task.completedAt.slice(0, 16).replace('T', ' ')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -127,8 +127,8 @@ export function TestCaseDetailDrawer({ testCaseId, onClose, onCreateBug }: Props
|
|||||||
<div><span className="text-[var(--ink-muted)]">负责人:</span><span className="text-[var(--ink)] font-medium">{tc.assigneeId || '-'}</span></div>
|
<div><span className="text-[var(--ink-muted)]">负责人:</span><span className="text-[var(--ink)] font-medium">{tc.assigneeId || '-'}</span></div>
|
||||||
<div><span className="text-[var(--ink-muted)]">创建人:</span><span className="text-[var(--ink)]">{tc.createdBy}</span></div>
|
<div><span className="text-[var(--ink-muted)]">创建人:</span><span className="text-[var(--ink)]">{tc.createdBy}</span></div>
|
||||||
<div><span className="text-[var(--ink-muted)]">创建日期:</span><span className="text-[var(--ink)]">{tc.createdAt.slice(0, 10)}</span></div>
|
<div><span className="text-[var(--ink-muted)]">创建日期:</span><span className="text-[var(--ink)]">{tc.createdAt.slice(0, 10)}</span></div>
|
||||||
{tc.startedAt && <div><span className="text-[var(--ink-muted)]">开始执行:</span><span className="text-[var(--ink)]">{tc.startedAt}</span></div>}
|
{tc.startedAt && <div><span className="text-[var(--ink-muted)]">开始执行:</span><span className="text-[var(--ink)]">{tc.startedAt.slice(0, 16).replace('T', ' ')}</span></div>}
|
||||||
{tc.completedAt && <div><span className="text-[var(--ink-muted)]">执行完成:</span><span className="text-[var(--ink)]">{tc.completedAt}</span></div>}
|
{tc.completedAt && <div><span className="text-[var(--ink-muted)]">执行完成:</span><span className="text-[var(--ink)]">{tc.completedAt.slice(0, 16).replace('T', ' ')}</span></div>}
|
||||||
{tc.startedAt && <div><span className="text-[var(--ink-muted)]">测试耗时:</span><span className="text-[var(--ink)] font-medium">{calcActualHoursByDates(tc.startedAt, tc.completedAt)}h</span></div>}
|
{tc.startedAt && <div><span className="text-[var(--ink-muted)]">测试耗时:</span><span className="text-[var(--ink)] font-medium">{calcActualHoursByDates(tc.startedAt, tc.completedAt)}h</span></div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -83,18 +83,12 @@ export function formatHours(hours: number): string {
|
|||||||
|
|
||||||
export function calcActualHoursByDates(startDate?: string, completedAt?: string): number {
|
export function calcActualHoursByDates(startDate?: string, completedAt?: string): number {
|
||||||
if (!startDate) return 0;
|
if (!startDate) return 0;
|
||||||
const end = completedAt || new Date().toISOString().slice(0, 10);
|
const end = completedAt || new Date().toISOString();
|
||||||
const start = new Date(startDate);
|
const startMs = new Date(startDate).getTime();
|
||||||
const endDate = new Date(end);
|
const endMs = new Date(end).getTime();
|
||||||
if (isNaN(start.getTime()) || isNaN(endDate.getTime()) || endDate < start) return 0;
|
if (isNaN(startMs) || isNaN(endMs) || endMs < startMs) return 0;
|
||||||
let workDays = 0;
|
const diffHours = (endMs - startMs) / (1000 * 60 * 60);
|
||||||
const current = new Date(start);
|
return Math.round(diffHours * 2) / 2; // 精确到0.5h
|
||||||
while (current <= endDate) {
|
|
||||||
const day = current.getDay();
|
|
||||||
if (day !== 0 && day !== 6) workDays++;
|
|
||||||
current.setDate(current.getDate() + 1);
|
|
||||||
}
|
|
||||||
return workDays * 8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateTaskNo(existingTasks: DevTask[]): string {
|
export function generateTaskNo(existingTasks: DevTask[]): string {
|
||||||
|
|||||||
@@ -77,10 +77,9 @@ export const useDevTaskStore = create<DevTaskState>((set, get) => ({
|
|||||||
if (!canTransition(task.status, to)) {
|
if (!canTransition(task.status, to)) {
|
||||||
return { ok: false, message: `不允许从「${task.status}」流转到「${to}」` };
|
return { ok: false, message: `不允许从「${task.status}」流转到「${to}」` };
|
||||||
}
|
}
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const now = new Date().toISOString();
|
||||||
const startDate = (to === 'in_progress' && !task.startDate) ? today : task.startDate;
|
const startDate = (to === 'in_progress' && !task.startDate) ? now : task.startDate;
|
||||||
// 开发完成时间 = 提测时间(不是测试完成时间)
|
const completedAt = (to === 'submitted' && !task.completedAt) ? now : task.completedAt;
|
||||||
const completedAt = (to === 'submitted' && !task.completedAt) ? today : task.completedAt;
|
|
||||||
get().updateTask(id, { status: to, startDate, completedAt });
|
get().updateTask(id, { status: to, startDate, completedAt });
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -74,14 +74,12 @@ export const useTestCaseStore = create<TestCaseState>((set, get) => ({
|
|||||||
if (!canTcTransition(tc.status, to)) {
|
if (!canTcTransition(tc.status, to)) {
|
||||||
return { ok: false, message: `不允许从「${tc.status}」流转到「${to}」` };
|
return { ok: false, message: `不允许从「${tc.status}」流转到「${to}」` };
|
||||||
}
|
}
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const now = new Date().toISOString();
|
||||||
const patch: Partial<TestCase> = { status: to };
|
const patch: Partial<TestCase> = { status: to };
|
||||||
// 开始执行时间
|
if (to === 'running' && !tc.startedAt) patch.startedAt = now;
|
||||||
if (to === 'running' && !tc.startedAt) patch.startedAt = today;
|
if (to === 'passed' || to === 'failed' || to === 'blocked') patch.completedAt = now;
|
||||||
// 执行完成时间(通过/失败/阻塞都算执行完成)
|
|
||||||
if (to === 'passed' || to === 'failed' || to === 'blocked') patch.completedAt = today;
|
|
||||||
if (to === 'running' || to === 'passed' || to === 'failed' || to === 'blocked') {
|
if (to === 'running' || to === 'passed' || to === 'failed' || to === 'blocked') {
|
||||||
patch.executedAt = today;
|
patch.executedAt = now;
|
||||||
}
|
}
|
||||||
// 回退到执行中时清除完成时间
|
// 回退到执行中时清除完成时间
|
||||||
if (to === 'running') { patch.failReason = undefined; patch.blockReason = undefined; patch.completedAt = undefined; }
|
if (to === 'running') { patch.failReason = undefined; patch.blockReason = undefined; patch.completedAt = undefined; }
|
||||||
|
|||||||
Reference in New Issue
Block a user