feat: 计划任务清单+进度联动+与我相关工作台
- 计划新增任务清单:调研支持任务CRUD,进度=完成数/总数 - 产品方案/UI:通过关联需求勾选进度,全部完成提示提交 - 版本胶囊条动态进度:调研/产品方案/UI 阶段独立计算 - 版本状态显示具体阶段名(调研中/产品设计中/UI设计中) - 计划开始日期≤今天自动进入"进行中",版本状态联动 - 与我相关重写为左右布局:左侧分组导航,右侧任务/需求勾选 - 计划支持超期原因校验(结束日期超版本截止) - 修复列表overflow裁剪问题、计划耗时当天至少1天 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
export type PlanTaskStatus = 'pending' | 'in_progress' | 'completed';
|
||||
|
||||
export interface PlanTask {
|
||||
id: string;
|
||||
title: string;
|
||||
status: PlanTaskStatus;
|
||||
}
|
||||
|
||||
export interface VersionPlan {
|
||||
id: string;
|
||||
versionId: string;
|
||||
@@ -7,6 +15,8 @@ export interface VersionPlan {
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
status: 'pending' | 'in_progress' | 'completed';
|
||||
tasks?: PlanTask[];
|
||||
completedRequirementIds?: string[];
|
||||
linkedRequirementIds?: string[];
|
||||
resultType?: 'link' | 'file';
|
||||
resultUrl?: string;
|
||||
@@ -21,11 +31,24 @@ export interface VersionPlan {
|
||||
|
||||
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 totalDays = Math.ceil((e - s) / (1000 * 60 * 60 * 24));
|
||||
if (isNaN(s) || isNaN(e) || e < s) return { days: 0, hours: 0 };
|
||||
const diff = Math.ceil((e - s) / (1000 * 60 * 60 * 24));
|
||||
const totalDays = Math.max(1, diff); // 当天开始当天结束至少1天
|
||||
return { days: totalDays, hours: totalDays * 8 };
|
||||
}
|
||||
|
||||
@@ -35,13 +58,19 @@ export function formatDuration(days: number, _hours: number): string {
|
||||
}
|
||||
|
||||
export function calcTotalDuration(plans: VersionPlan[]): string {
|
||||
const completedOrActive = plans.filter((p) => p.status !== 'pending');
|
||||
if (completedOrActive.length === 0) return '0天';
|
||||
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 = completedOrActive.map((p) => ({
|
||||
start: new Date(p.startTime).getTime(),
|
||||
end: p.completedAt ? new Date(p.completedAt).getTime() : new Date(p.endTime).getTime(),
|
||||
})).filter((i) => i.end > i.start).sort((a, b) => a.start - b.start);
|
||||
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天';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user