Files
ftb-project-management/apps/web/lib/version-plan.ts
Script Generator e92ec4eb0e fix: formatDuration 时间差为0时显示0h而非1天
之前 hours=0 时会落到 days 分支显示"1天",实际是开始和完成时间相同。
现在统一按小时数判断,不到1天显示Xh,整天显示X天。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 16:43:54 +08:00

99 lines
3.1 KiB
TypeScript

export type PlanTaskStatus = 'pending' | 'in_progress' | 'completed';
export interface PlanTask {
id: string;
title: string;
status: PlanTaskStatus;
}
export interface VersionPlan {
id: string;
versionId: string;
type: 'research' | 'product' | 'ui';
title: string;
owner: string;
startTime: string;
endTime: string;
status: 'pending' | 'in_progress' | 'completed';
tasks?: PlanTask[];
completedRequirementIds?: string[];
linkedRequirementIds?: string[];
resultType?: 'link' | 'file';
resultUrl?: string;
resultFileName?: string;
resultFileData?: string;
remark?: string;
overdueReason?: string;
actualStartAt?: string;
createdAt: string;
completedAt?: string;
addedBy: string;
}
export type PlanType = VersionPlan['type'];
export function calcPlanProgress(tasks?: PlanTask[]): number {
if (!tasks || tasks.length === 0) return 0;
const completed = tasks.filter((t) => t.status === 'completed').length;
return Math.round((completed / tasks.length) * 100);
}
export function calcLinkedReqProgress(linkedIds?: string[], completedIds?: string[]): number {
if (!linkedIds || linkedIds.length === 0) return 0;
const done = (completedIds || []).filter((id) => linkedIds.includes(id)).length;
return Math.round((done / linkedIds.length) * 100);
}
export function calcPlanDuration(start: string, end: string): { days: number; hours: number } {
const s = new Date(start).getTime();
const e = new Date(end).getTime();
if (isNaN(s) || isNaN(e) || e < s) return { days: 0, hours: 0 };
const diffHours = (e - s) / (1000 * 60 * 60);
const hours = Math.round(diffHours * 2) / 2; // 精确到0.5h
const days = Math.ceil(diffHours / 24);
return { days, hours };
}
export function formatDuration(days: number, hours: number): string {
if (hours <= 0) return '0h';
if (hours < 24) return `${hours}h`;
if (hours % 24 === 0) return `${hours / 24}`;
return `${hours}h`;
}
export function calcTotalDuration(plans: VersionPlan[]): string {
const today = new Date().toISOString().slice(0, 10);
const activePlans = plans.filter((p) => p.status !== 'pending' || p.startTime <= today);
if (activePlans.length === 0) return '0天';
const intervals = activePlans.map((p) => {
const start = new Date(p.startTime).getTime();
const end = p.completedAt
? new Date(p.completedAt).getTime()
: p.startTime <= today
? new Date(today).getTime()
: new Date(p.startTime).getTime();
return { start, end };
}).filter((i) => i.end > i.start).sort((a, b) => a.start - b.start);
if (intervals.length === 0) return '0天';
let totalMs = 0;
let currentStart = intervals[0].start;
let currentEnd = intervals[0].end;
for (let i = 1; i < intervals.length; i++) {
if (intervals[i].start <= currentEnd) {
currentEnd = Math.max(currentEnd, intervals[i].end);
} else {
totalMs += currentEnd - currentStart;
currentStart = intervals[i].start;
currentEnd = intervals[i].end;
}
}
totalMs += currentEnd - currentStart;
const totalDays = Math.ceil(totalMs / (1000 * 60 * 60 * 24));
return `${totalDays}`;
}