feat(产品方案): 优化计划卡片展示和排序

This commit is contained in:
Script Generator
2026-06-26 14:05:01 +08:00
parent dde7a9a623
commit 4dcdad50c3
3 changed files with 112 additions and 23 deletions

View File

@@ -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<string | null>(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 (
<div key={plan.id} className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1.5">
<span className="text-[13px] font-medium text-[var(--ink)]">{plan.title}</span>
<span className={`inline-flex items-center rounded-md px-2 py-0.5 text-[10px] font-medium border ${STATUS_STYLE[effectiveStatus]}`}>
<div key={plan.id} className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4 shadow-[var(--shadow-sm)]">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<span className="min-w-0 truncate text-[14px] font-semibold text-[var(--ink)]" title={plan.title}>{plan.title}</span>
<span className={`inline-flex shrink-0 items-center rounded-md px-2 py-0.5 text-[10px] font-medium border ${STATUS_STYLE[effectiveStatus]}`}>
{STATUS_LABEL[effectiveStatus]}
</span>
{plan.type === 'product' && (
<span className="inline-flex items-center rounded-md border border-[var(--line)] bg-[var(--bg-subtle)] px-2 py-0.5 text-[10px] font-medium text-[var(--ink-muted)]">
<span className="inline-flex shrink-0 items-center rounded-md border border-[var(--line)] bg-[var(--bg-subtle)] px-2 py-0.5 text-[10px] font-medium text-[var(--ink-muted)]">
{PRODUCT_PLAN_KIND_LABEL[getProductPlanKind(plan)]}
</span>
)}
{plan.type === 'product' && plan.reviewResult && (
<span className={`inline-flex items-center rounded-md px-2 py-0.5 text-[10px] font-medium ${plan.reviewResult === 'passed' ? 'bg-emerald-50 text-emerald-700' : 'bg-red-50 text-red-700'}`}>
<span className={`inline-flex shrink-0 items-center rounded-md px-2 py-0.5 text-[10px] font-medium ${plan.reviewResult === 'passed' ? 'bg-emerald-50 text-emerald-700' : 'bg-red-50 text-red-700'}`}>
{PRODUCT_PLAN_REVIEW_RESULT_LABEL[plan.reviewResult]}
</span>
)}
</div>
<div className="flex items-center gap-4 text-[12px] text-[var(--ink-soft)]">
<span>{formatDateTime(plan.startTime)} {formatDateTime(plan.endTime)}</span>
{plan.actualStartAt && <span className="text-blue-600">{formatDateTime(plan.actualStartAt)}</span>}
{plan.completedAt && <span className="text-green-600">{formatDateTime(plan.completedAt)}</span>}
<span className="font-medium text-[var(--ink)]"> {durText}</span>
</div>
<dl className="mt-3 grid grid-cols-1 gap-x-5 gap-y-2 border-y border-[var(--line)] py-3 text-[12px] sm:grid-cols-2 lg:grid-cols-4">
<div>
<dt className="text-[11px] text-[var(--ink-muted)]"></dt>
<dd className="mt-0.5 font-medium text-[var(--ink)]">{plan.owner}</dd>
</div>
<div className="sm:col-span-2">
<dt className="text-[11px] text-[var(--ink-muted)]"></dt>
<dd className="mt-0.5 font-medium text-[var(--ink)]">{formatDateTime(plan.startTime)} {formatDateTime(plan.endTime)}</dd>
</div>
<div>
<dt className="text-[11px] text-[var(--ink-muted)]"></dt>
<dd className="mt-0.5 font-semibold text-[var(--ink)]">{durText}</dd>
</div>
{plan.actualStartAt && (
<div>
<dt className="text-[11px] text-[var(--ink-muted)]"></dt>
<dd className="mt-0.5 font-medium text-blue-600">{formatDateTime(plan.actualStartAt)}</dd>
</div>
)}
{plan.completedAt && (
<div>
<dt className="text-[11px] text-[var(--ink-muted)]"></dt>
<dd className="mt-0.5 font-medium text-green-600">{formatDateTime(plan.completedAt)}</dd>
</div>
)}
</dl>
{plan.overdueReason && (
<div className="mt-1.5 text-[11px] text-red-600 bg-red-50 rounded px-2 py-1 inline-block">{plan.overdueReason}</div>
<div className="mt-3 rounded-lg border border-red-100 bg-red-50 px-3 py-2 text-[11px] leading-5 text-red-700">{plan.overdueReason}</div>
)}
{(plan.type === 'product' || plan.type === 'ui') && (
<div className="mt-1.5 text-[12px] text-[var(--ink-soft)]"><span className="font-medium text-[var(--ink)]">{plan.owner}</span></div>
{plan.remark && (
<div className="mt-3 border-l-2 border-[var(--accent)] pl-3">
<div className="text-[11px] font-medium text-[var(--ink-muted)]"></div>
<div className="mt-1 max-h-20 overflow-y-auto whitespace-pre-wrap pr-1 text-[12px] leading-5 text-[var(--ink-soft)]">{plan.remark}</div>
</div>
)}
{plan.status === 'completed' && plan.resultUrl && (
<div className="flex items-center gap-1.5 mt-2">
@@ -207,18 +231,26 @@ export function PlanTab({ plans, versionId, version, versionDeadline, currentUse
{/* 关联需求 */}
{plan.linkedRequirementIds && plan.linkedRequirementIds.length > 0 && requirementOptions.length > 0 && (
<div className="mt-3 space-y-2">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-[11px] font-medium text-[var(--ink-muted)]"></div>
<div className="mt-0.5 text-[11px] text-[var(--ink-soft)]">
{(plan.completedRequirementIds || []).filter((id) => plan.linkedRequirementIds?.includes(id)).length} / {plan.linkedRequirementIds.length}
</div>
</div>
<span className="shrink-0 text-[11px] font-medium tabular-nums text-[var(--ink-soft)]">{calcLinkedReqProgress(plan.linkedRequirementIds, plan.completedRequirementIds)}%</span>
</div>
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 rounded-full bg-[var(--bg-subtle)] overflow-hidden">
<div className="h-full rounded-full bg-[var(--accent)] transition-all" style={{ width: `${calcLinkedReqProgress(plan.linkedRequirementIds, plan.completedRequirementIds)}%` }} />
</div>
<span className="text-[11px] font-medium tabular-nums text-[var(--ink-soft)]">{calcLinkedReqProgress(plan.linkedRequirementIds, plan.completedRequirementIds)}%</span>
</div>
<div className="space-y-1">
<div className="max-h-44 space-y-1 overflow-y-auto rounded-lg bg-[var(--bg-subtle)] p-2 pr-1">
{plan.linkedRequirementIds.map((rid) => {
const req = requirementOptions.find((r) => r.id === rid);
const isDone = (plan.completedRequirementIds || []).includes(rid);
return req ? (
<div key={rid} className="flex items-center gap-2">
<div key={rid} className="flex min-w-0 items-center gap-2 rounded-md px-2 py-1 hover:bg-[var(--bg-card)]">
<button
disabled={!canEditCoverage}
onClick={() => {
@@ -231,8 +263,8 @@ export function PlanTab({ plans, versionId, version, versionDeadline, currentUse
>
{isDone && <Check className="h-2.5 w-2.5 text-white" strokeWidth={3} />}
</button>
<span className={`text-[11px] font-mono text-[var(--ink-muted)]`}>{req.code}</span>
<span className={`text-[12px] ${isDone ? 'line-through text-[var(--ink-muted)]' : 'text-[var(--ink)]'}`}>{req.title}</span>
<span className="shrink-0 text-[11px] font-mono text-[var(--ink-muted)]">{req.code}</span>
<span className={`min-w-0 flex-1 truncate text-[12px] ${isDone ? 'line-through text-[var(--ink-muted)]' : 'text-[var(--ink)]'}`} title={req.title}>{req.title}</span>
</div>
) : null;
})}