Files
writer-work-flow/apps/web/components/review/AcceptPanel.tsx

170 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import Link from "next/link";
import { ArrowRight, CheckCircle2, ClipboardCheck, ListTree } from "lucide-react";
import { Button } from "@/components/ui/Button";
import { StatusNote } from "@/components/ui/StatusNote";
import type { AcceptResponse } from "@/lib/api/types";
import { buildAcceptPreview } from "@/lib/review/accept-preview";
import { ThinkingIndicator } from "@/components/ThinkingIndicator";
import { reviewChapterHref } from "@/lib/review/chapterNav";
import { buttonClass } from "@/lib/ui/variants";
interface AcceptPanelProps {
projectId: string;
chapterNo: number;
// 本页是否已有审稿结果(进页带留痕 或 本次重审完成)。用于区分
// 「审过且 0 冲突→可直接验收」与「未审稿→先审稿」,避免把"未知"当"无冲突"。
reviewed: boolean;
conflictCount: number;
unresolvedCount: number;
// R3当前伏笔建议数验收前清单提醒只读不自动落库
foreshadowCount: number;
accepting: boolean;
result: AcceptResponse | null;
onAccept: () => void;
}
// 验收 gateUX §9 / §8.4):未决禁验收(置灰 + 文案),验收后呈现「本次将更新」清单。
export function AcceptPanel({
projectId,
chapterNo,
reviewed,
conflictCount,
unresolvedCount,
foreshadowCount,
accepting,
result,
onAccept,
}: AcceptPanelProps) {
const blocked = unresolvedCount > 0;
const preview = buildAcceptPreview({
conflictCount,
foreshadowSuggestions: foreshadowCount,
});
if (result) {
return (
<div className="rounded border border-pass/50 bg-panel p-4">
<StatusNote variant="success" title="本章已验收">
<p className="text-sm leading-6">
稿
</p>
</StatusNote>
<dl className="mt-3 grid grid-cols-3 gap-2 text-center">
<div className="rounded border border-line bg-bg/60 p-2">
<dt className="text-[11px] text-ink-soft"></dt>
<dd className="mt-1 font-mono text-sm text-ink">
v{result.accepted_version}
</dd>
</div>
<div className="rounded border border-line bg-bg/60 p-2">
<dt className="text-[11px] text-ink-soft"></dt>
<dd className="mt-1 text-sm text-ink">
{result.digest_added ? "已新增" : "未变更"}
</dd>
</div>
<div className="rounded border border-line bg-bg/60 p-2">
<dt className="text-[11px] text-ink-soft"></dt>
<dd className="mt-1 font-mono text-sm text-ink">
{result.decisions_recorded}
</dd>
</div>
</dl>
{result.review_id ? (
<p className="mt-2 truncate text-[11px] text-ink-soft">
<span className="font-mono">{result.review_id}</span>
</p>
) : null}
{/* 验收闭环 → 写下一章入口(修「验收后无路可走」的断点)。 */}
<div className="mt-3 flex flex-wrap items-center gap-2">
<Link
href={`/projects/${projectId}/write?chapter=${chapterNo + 1}`}
className={buttonClass({ variant: "primary" })}
>
<ArrowRight className="h-4 w-4" aria-hidden="true" />
{chapterNo + 1}
</Link>
<Link
href={reviewChapterHref(projectId, chapterNo + 1)}
className={buttonClass({ variant: "secondary" })}
>
<ClipboardCheck className="h-4 w-4" aria-hidden="true" />
{chapterNo + 1}
</Link>
<Link
href={`/projects/${projectId}/outline`}
className={buttonClass({ variant: "secondary" })}
>
<ListTree className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
);
}
return (
<div
className={`rounded border bg-panel p-4 ${
blocked ? "border-conflict/35" : "border-line"
}`}
>
{blocked ? (
<StatusNote className="mb-3" variant="danger" role="status">
{unresolvedCount}
</StatusNote>
) : (
<>
<StatusNote className="mb-3" variant={reviewed ? "success" : "info"}>
{conflictCount === 0
? reviewed
? "无冲突,可直接验收。"
: "尚未审稿——建议先点「重新审稿」确认本章无冲突,再验收。"
: "全部冲突已裁决,可验收本章。"}
</StatusNote>
{/* R3验收前「本次将更新」清单预期落库口径以验收回执为准。 */}
<div className="mb-3 rounded border border-line/70 bg-bg/50 p-3">
<h4 className="mb-1.5 text-xs font-semibold text-ink">
</h4>
<ul className="space-y-1.5">
{preview.map((item) => (
<li key={item.label} className="flex gap-2 text-xs text-ink-soft">
<span className="text-cinnabar" aria-hidden="true">
</span>
<span>
<span className="text-ink">{item.label}</span>
<span className="text-ink-soft">{item.detail}</span>
</span>
</li>
))}
</ul>
<p className="mt-2 text-[11px] text-ink-soft/70">
</p>
</div>
</>
)}
<Button
onClick={onAccept}
disabled={blocked || accepting}
aria-disabled={blocked || accepting}
variant="primary"
className="w-full"
>
{accepting ? (
<ThinkingIndicator label="验收中" className="justify-center" />
) : (
<>
<CheckCircle2 className="h-4 w-4" aria-hidden="true" />
</>
)}
</Button>
</div>
);
}