feat(版本详情): 完善流程日志与需求覆盖

This commit is contained in:
Script Generator
2026-06-30 11:00:07 +08:00
parent d754585fe0
commit 1cd595c42e
38 changed files with 4123 additions and 237 deletions

View File

@@ -8,7 +8,6 @@ import {
formatDuration,
calcTotalDuration,
calcPlanProgress,
calcLinkedReqProgress,
sortPlansNewestFirst,
PRODUCT_PLAN_KIND_LABEL,
PRODUCT_PLAN_REVIEW_FAILURE_OPTIONS,
@@ -18,6 +17,7 @@ import { formatDateTime } from '@/lib/format';
import { FieldError } from '@/components/FieldError';
import { WorkDateTimePicker } from '@/components/WorkDateTimePicker';
import { AiDecomposeButton } from './AiDecomposeButton';
import { PlanLogTimeline, PlanRequirementCoveragePanel } from './PlanRequirementCoveragePanel';
import type { VersionWithContext } from '@/lib/derive';
import type { Requirement } from '@/lib/requirement';
import { mergeSelectedRequirementOptions } from '@/lib/requirement-selector';
@@ -119,6 +119,8 @@ export function PlanTab({ plans, versionId, version, versionDeadline, currentUse
}
return (
<div key={plan.id} className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4 shadow-[var(--shadow-sm)]">
<div className={plan.type === 'research' ? '' : 'grid gap-4 xl:grid-cols-[minmax(0,1fr)_320px]'}>
<div className="min-w-0">
<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">
@@ -229,48 +231,14 @@ export function PlanTab({ plans, versionId, version, versionDeadline, currentUse
</div>
</div>
)}
{/* 关联需求 */}
{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>
</div>
<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 min-w-0 items-center gap-2 rounded-md px-2 py-1 hover:bg-[var(--bg-card)]">
<button
disabled={!canEditCoverage}
onClick={() => {
if (!canEditCoverage) return;
const current = plan.completedRequirementIds || [];
const next = isDone ? current.filter((id) => id !== rid) : [...current, rid];
onUpdate(plan.id, { completedRequirementIds: next });
}}
className={`h-4 w-4 rounded border flex items-center justify-center shrink-0 transition-colors ${!canEditCoverage ? 'opacity-40 cursor-not-allowed' : ''} ${isDone ? 'bg-[var(--accent)] border-[var(--accent)]' : 'border-[var(--line)]'}`}
>
{isDone && <Check className="h-2.5 w-2.5 text-white" strokeWidth={3} />}
</button>
<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;
})}
</div>
</div>
<PlanRequirementCoveragePanel
plan={plan}
requirements={plan.linkedRequirementIds.map((rid) => requirementOptions.find((r) => r.id === rid)).filter(Boolean) as Requirement[]}
canEdit={canEditCoverage}
currentUserName={currentUserName}
onUpdate={onUpdate}
/>
)}
{plan.status === 'in_progress' && !completionState.canSubmitResult && (
<p className="mt-2 text-[11px] text-[var(--ink-muted)]">{completionState.missingReasons.join('、')}</p>
@@ -314,6 +282,14 @@ export function PlanTab({ plans, versionId, version, versionDeadline, currentUse
<button onClick={() => setCompletingPlan(plan)} className="text-[11px] font-medium text-green-700 hover:text-green-900 underline">{getSubmitActionLabel(plan)}</button>
</div>
)}
</div>
{plan.type !== 'research' && (
<PlanLogTimeline
logs={plan.logs}
className="border-t border-[var(--line)] pt-4 xl:border-l xl:border-t-0 xl:pt-0 xl:pl-4"
/>
)}
</div>
</div>
);
})}