From 4dcdad50c3126f5cfbcd272013ff2c42395b53ee Mon Sep 17 00:00:00 2001 From: Script Generator Date: Fri, 26 Jun 2026 14:05:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=BA=A7=E5=93=81=E6=96=B9=E6=A1=88):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AE=A1=E5=88=92=E5=8D=A1=E7=89=87=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E5=92=8C=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/components/version/PlanTab.tsx | 78 +++++++++++++++++-------- apps/web/lib/version-plan.test.ts | 37 ++++++++++++ apps/web/lib/version-plan.ts | 20 +++++++ 3 files changed, 112 insertions(+), 23 deletions(-) create mode 100644 apps/web/lib/version-plan.test.ts diff --git a/apps/web/components/version/PlanTab.tsx b/apps/web/components/version/PlanTab.tsx index 9c43f7d..04e7631 100644 --- a/apps/web/components/version/PlanTab.tsx +++ b/apps/web/components/version/PlanTab.tsx @@ -9,6 +9,7 @@ import { calcTotalDuration, calcPlanProgress, calcLinkedReqProgress, + sortPlansNewestFirst, PRODUCT_PLAN_KIND_LABEL, PRODUCT_PLAN_REVIEW_FAILURE_OPTIONS, PRODUCT_PLAN_REVIEW_RESULT_LABEL, @@ -71,7 +72,7 @@ export function PlanTab({ plans, versionId, version, versionDeadline, currentUse const [transferPlanId, setTransferPlanId] = useState(null); const [transferTo, setTransferTo] = useState(''); - const typePlans = plans.filter((p) => p.versionId === versionId && p.type === planType); + const typePlans = sortPlansNewestFirst(plans.filter((p) => p.versionId === versionId && p.type === planType)); const totalDuration = calcTotalDuration(typePlans); return ( @@ -116,36 +117,59 @@ export function PlanTab({ plans, versionId, version, versionDeadline, currentUse onUpdate(plan.id, { status: 'in_progress' }); } return ( -
-
-
-
- {plan.title} - +
+
+
+
+ {plan.title} + {STATUS_LABEL[effectiveStatus]} {plan.type === 'product' && ( - + {PRODUCT_PLAN_KIND_LABEL[getProductPlanKind(plan)]} )} {plan.type === 'product' && plan.reviewResult && ( - + {PRODUCT_PLAN_REVIEW_RESULT_LABEL[plan.reviewResult]} )}
-
- 计划:{formatDateTime(plan.startTime)} → {formatDateTime(plan.endTime)} - {plan.actualStartAt && 实际开始:{formatDateTime(plan.actualStartAt)}} - {plan.completedAt && 完成:{formatDateTime(plan.completedAt)}} - 已耗时 {durText} -
+
+
+
负责人
+
{plan.owner}
+
+
+
计划时间
+
{formatDateTime(plan.startTime)} → {formatDateTime(plan.endTime)}
+
+
+
已耗时
+
{durText}
+
+ {plan.actualStartAt && ( +
+
实际开始
+
{formatDateTime(plan.actualStartAt)}
+
+ )} + {plan.completedAt && ( +
+
完成时间
+
{formatDateTime(plan.completedAt)}
+
+ )} +
{plan.overdueReason && ( -
超期原因:{plan.overdueReason}
+
超期原因:{plan.overdueReason}
)} - {(plan.type === 'product' || plan.type === 'ui') && ( -
负责人:{plan.owner}
+ {plan.remark && ( +
+
备注
+
{plan.remark}
+
)} {plan.status === 'completed' && plan.resultUrl && (
@@ -207,18 +231,26 @@ export function PlanTab({ plans, versionId, version, versionDeadline, currentUse {/* 关联需求 */} {plan.linkedRequirementIds && plan.linkedRequirementIds.length > 0 && requirementOptions.length > 0 && (
+
+
+
引用需求
+
+ 已覆盖 {(plan.completedRequirementIds || []).filter((id) => plan.linkedRequirementIds?.includes(id)).length} / {plan.linkedRequirementIds.length} +
+
+ {calcLinkedReqProgress(plan.linkedRequirementIds, plan.completedRequirementIds)}% +
- {calcLinkedReqProgress(plan.linkedRequirementIds, plan.completedRequirementIds)}%
-
+
{plan.linkedRequirementIds.map((rid) => { const req = requirementOptions.find((r) => r.id === rid); const isDone = (plan.completedRequirementIds || []).includes(rid); return req ? ( -
+
- {req.code} - {req.title} + {req.code} + {req.title}
) : null; })} diff --git a/apps/web/lib/version-plan.test.ts b/apps/web/lib/version-plan.test.ts new file mode 100644 index 0000000..f7ba361 --- /dev/null +++ b/apps/web/lib/version-plan.test.ts @@ -0,0 +1,37 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import * as versionPlan from './version-plan'; +import type { VersionPlan } from './version-plan'; + +function plan(patch: Partial): VersionPlan { + return { + id: 'plan-1', + versionId: 'version-1', + type: 'product', + title: '产品方案', + owner: 'PM', + startTime: '2026-06-25T09:00', + endTime: '2026-06-25T18:00', + status: 'pending', + createdAt: '2026-06-25', + addedBy: 'PM', + ...patch, + }; +} + +test('sortPlansNewestFirst places newly created plans before older plans', () => { + const sortPlansNewestFirst = (versionPlan as any).sortPlansNewestFirst as undefined | ((plans: VersionPlan[]) => VersionPlan[]); + assert.equal(typeof sortPlansNewestFirst, 'function'); + + const plans = [ + plan({ id: 'plan-100', createdAt: '2026-06-26' }), + plan({ id: 'manual-old', createdAt: '2026-06-25' }), + plan({ id: 'plan-300', createdAt: '2026-06-26' }), + ]; + + const sorted = (sortPlansNewestFirst as (items: VersionPlan[]) => VersionPlan[])(plans); + + assert.deepEqual(sorted.map((item) => item.id), ['plan-300', 'plan-100', 'manual-old']); + assert.deepEqual(plans.map((item) => item.id), ['plan-100', 'manual-old', 'plan-300']); +}); diff --git a/apps/web/lib/version-plan.ts b/apps/web/lib/version-plan.ts index d207c25..bae1cf3 100644 --- a/apps/web/lib/version-plan.ts +++ b/apps/web/lib/version-plan.ts @@ -78,6 +78,26 @@ export interface VersionPlan { export type PlanType = VersionPlan['type']; +function getPlanCreatedAtTime(plan: VersionPlan): number { + const time = new Date(plan.createdAt).getTime(); + return Number.isFinite(time) ? time : 0; +} + +function getPlanIdTime(plan: VersionPlan): number { + const match = /^plan-(\d+)$/.exec(plan.id); + if (!match) return 0; + const time = Number(match[1]); + return Number.isFinite(time) ? time : 0; +} + +export function sortPlansNewestFirst(plans: VersionPlan[]): VersionPlan[] { + return [...plans].sort((a, b) => { + const createdDiff = getPlanCreatedAtTime(b) - getPlanCreatedAtTime(a); + if (createdDiff !== 0) return createdDiff; + return getPlanIdTime(b) - getPlanIdTime(a); + }); +} + export function calcPlanProgress(tasks?: PlanTask[]): number { if (!tasks || tasks.length === 0) return 0; const completed = tasks.filter((t) => t.status === 'completed').length;