feat(产品方案): 优化计划卡片展示和排序
This commit is contained in:
@@ -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;
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user