feat(ux): R1+R2 审稿裁决人体学 — 冲突卡内联片段 + 分组/排序/跳下一条/主次按钮

R1: 新 lib/review/snippet.ts 从 where 解析段号取终稿命中段预览,ConflictCard 渲染引用块。
R2: 新 lib/review/grouping.ts 按 type 分组+严重度排序(仅改显示顺序,保留原始 index 守 conflict_index 不变量),组计数徽标、跳到下一条未裁决(环回)、采纳改法主按钮/忽略手改次级。
TDD: snippet 8 测 + grouping 10 测。前端门禁绿: lint/tsc/vitest 201/build。
This commit is contained in:
Yaojia Wang
2026-06-20 15:53:45 +02:00
parent 6f419b35ed
commit d81583d5d1
7 changed files with 329 additions and 22 deletions

View File

@@ -8,17 +8,27 @@ interface ConflictCardProps {
conflict: ReviewConflict;
draft: DecisionDraft;
missing: boolean;
// R1命中段内联预览无段号/越界时为 null回退到「跳转」按钮
snippet: string | null;
onVerdict: (index: number, verdict: Verdict) => void;
onNote: (index: number, note: string) => void;
onJump: (index: number) => void;
}
const VERDICT_OPTIONS: { value: Verdict; label: string }[] = [
{ value: "accept", label: "采纳改法" },
{ value: "ignore", label: "忽略" },
{ value: "manual", label: "手改" },
// R2采纳改法=朱砂主按钮primary忽略/手改=次级secondary不再三者同权。
const VERDICT_OPTIONS: { value: Verdict; label: string; primary: boolean }[] = [
{ value: "accept", label: "采纳改法", primary: true },
{ value: "ignore", label: "忽略", primary: false },
{ value: "manual", label: "手改", primary: false },
];
// 裁决按钮样式:激活=朱砂填充;未激活的主按钮=朱砂描边强调,次级=低对比描边。
function verdictClass(active: boolean, primary: boolean): string {
if (active) return "bg-cinnabar text-panel";
if (primary) return "border border-cinnabar text-cinnabar hover:bg-cinnabar/10";
return "border border-line text-ink-soft hover:border-cinnabar hover:text-ink";
}
// 单个冲突报告卡UX §6.4):五类徽标 + where + refs + suggestion + 裁决三态。
// 冲突=赭红;未决/缺判时加图标+文案不单靠色a11y §10
export function ConflictCard({
@@ -26,6 +36,7 @@ export function ConflictCard({
conflict,
draft,
missing,
snippet,
onVerdict,
onNote,
onJump,
@@ -52,6 +63,15 @@ export function ConflictCard({
<p className="flex-1 text-sm text-ink">{conflict.suggestion}</p>
</div>
{snippet ? (
<blockquote className="mt-2 border-l-2 border-conflict/40 bg-bg/50 py-1 pl-3 text-xs leading-relaxed text-ink-soft">
<span className="mr-1 text-conflict" aria-hidden="true">
</span>
{snippet}
</blockquote>
) : null}
<div className="mt-2 flex flex-wrap items-center gap-2 pl-1 text-xs text-ink-soft">
{conflict.where ? (
<button
@@ -80,11 +100,10 @@ export function ConflictCard({
type="button"
aria-pressed={active}
onClick={() => onVerdict(index, opt.value)}
className={`rounded px-3 py-1 text-xs ${
active
? "bg-cinnabar text-panel"
: "border border-line text-ink hover:border-cinnabar"
}`}
className={`rounded px-3 py-1 text-xs ${verdictClass(
active,
opt.primary,
)}`}
>
{opt.label}
</button>