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

@@ -5,9 +5,10 @@ import { X, Check, Link2, FileUp, ExternalLink, Play, ArrowRightLeft } from 'luc
import { useVersionPlanStore } from '@/stores/useVersionPlanStore';
import { useRequirementStore } from '@/stores/useRequirementStore';
import { useMemberStore } from '@/stores/useMemberStore';
import { useAuthStore } from '@/stores/useAuthStore';
import {
calcPlanProgress,
calcLinkedReqProgress,
getRequirementCoverageSummary,
PRODUCT_PLAN_KIND_LABEL,
PRODUCT_PLAN_REVIEW_FAILURE_OPTIONS,
PRODUCT_PLAN_REVIEW_RESULT_LABEL,
@@ -16,6 +17,7 @@ import { formatDateTime } from '@/lib/format';
import type { PlanTask, ProductPlanKind, ProductPlanReviewFailureType, ProductPlanReviewResult, VersionPlan } from '@/lib/version-plan';
import { canEditPlanRequirementCoverage, canTogglePlanChecklist, getPlanCompletionState } from '@/lib/version-plan-workflow';
import type { PlanResultPayload } from '@/lib/version-plan-workflow';
import { PlanLogTimeline, PlanRequirementCoveragePanel } from './PlanRequirementCoveragePanel';
interface Props {
planId: string;
@@ -49,6 +51,7 @@ export function PlanDetailDrawer({ planId, onClose, contextLabel }: Props) {
const { plans, updatePlan, completePlan } = useVersionPlanStore();
const { requirements } = useRequirementStore();
const { members } = useMemberStore();
const user = useAuthStore((s) => s.user);
const [showTransfer, setShowTransfer] = useState(false);
const [transferTo, setTransferTo] = useState('');
const [resultType, setResultType] = useState<'link' | 'file'>('link');
@@ -67,10 +70,11 @@ export function PlanDetailDrawer({ planId, onClose, contextLabel }: Props) {
const completionState = getPlanCompletionState(plan);
const isResearch = plan.type === 'research';
const progress = isResearch ? calcPlanProgress(plan.tasks) : calcLinkedReqProgress(plan.linkedRequirementIds, plan.completedRequirementIds);
const progress = isResearch ? calcPlanProgress(plan.tasks) : getRequirementCoverageSummary(plan).percent;
const linkedReqs = (plan.linkedRequirementIds || []).map((id) => requirements.find((r) => r.id === id)).filter(Boolean) as { id: string; code: string; title: string }[];
const canToggle = canTogglePlanChecklist(plan);
const canEditCoverage = canEditPlanRequirementCoverage(plan);
const currentUserName = user?.name ?? plan.owner;
const productPlanKind = plan.type === 'product' ? getProductPlanKind(plan) : undefined;
const isProductDesignPlan = productPlanKind === 'design';
const isProductReviewPlan = productPlanKind === 'review';
@@ -82,13 +86,6 @@ export function PlanDetailDrawer({ planId, onClose, contextLabel }: Props) {
updatePlan(plan.id, { tasks: updatedTasks });
};
const handleToggleReq = (reqId: string) => {
if (!canEditCoverage) return;
const current = plan.completedRequirementIds || [];
const next = current.includes(reqId) ? current.filter((id) => id !== reqId) : [...current, reqId];
updatePlan(plan.id, { completedRequirementIds: next });
};
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
@@ -235,32 +232,25 @@ export function PlanDetailDrawer({ planId, onClose, contextLabel }: Props) {
</div>
)}
{/* Product/UI: Linked Requirements */}
{linkedReqs.length > 0 && (
<div className="space-y-1.5">
<div className="text-[11px] font-medium text-[var(--ink-muted)]"></div>
{linkedReqs.map((req) => {
const isDone = (plan.completedRequirementIds || []).includes(req.id);
return (
<div key={req.id} className="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-[var(--bg-subtle)]">
<button
disabled={!canEditCoverage}
onClick={() => handleToggleReq(req.id)}
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="text-[11px] font-mono text-[var(--ink-muted)]">{req.code}</span>
<span className={`flex-1 text-[12px] ${isDone ? 'line-through text-[var(--ink-muted)]' : 'text-[var(--ink)]'}`}>{req.title}</span>
</div>
);
})}
<div>
<PlanRequirementCoveragePanel
plan={plan}
requirements={linkedReqs}
canEdit={canEditCoverage}
currentUserName={currentUserName}
onUpdate={updatePlan}
/>
{plan.status === 'in_progress' && !completionState.canSubmitResult && (
<p className="pt-1 text-[11px] text-[var(--ink-muted)]">{completionState.missingReasons.join('、')}</p>
)}
</div>
)}
{!isResearch && (
<PlanLogTimeline logs={plan.logs} className="border-l-0 border-t border-[var(--line)] pt-4 pl-0" />
)}
{/* Result */}
{plan.status === 'completed' && plan.resultUrl && (
<div className="rounded-lg bg-[var(--bg-subtle)] p-3">