Files
writer-work-flow/apps/web/lib/review/grouping.ts
Yaojia Wang d81583d5d1 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。
2026-06-20 15:53:45 +02:00

68 lines
2.4 KiB
TypeScript
Raw Permalink 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.

// R2 · 冲突分组/排序 + 跳到下一条未裁决UX §6.4)。
// 分组仅按 type严重度排序已知五类给定优先级未知殿后。
// 关键:分组/排序只改「显示顺序」,每条仍携原始 index——裁决草稿与 accept 的
// conflict_index 一律以原始下标为准不变量conflict_index 覆盖 range(len(conflicts)))。
// 纯逻辑,便于 node 环境单测。
import type { DecisionDraft } from "./decisions";
import type { ReviewConflict } from "./sse";
// 严重度优先级(数字越小越靠前)。对齐 C6 五类 ConflictType。
const SEVERITY_RANK: Record<string, number> = {
设定违例: 0,
能力不符: 1,
时间线倒错: 2,
地理矛盾: 3,
性格漂移: 4,
};
const UNKNOWN_RANK = 99;
export interface ConflictItem {
conflict: ReviewConflict;
index: number;
}
export interface ConflictGroup {
type: string;
items: ConflictItem[];
}
function severityRank(type: string): number {
return SEVERITY_RANK[type] ?? UNKNOWN_RANK;
}
// 按 type 分组并按严重度排序;组内保留原始出现顺序。
export function groupConflicts(
conflicts: readonly ReviewConflict[],
): ConflictGroup[] {
const byType = new Map<string, ConflictItem[]>();
conflicts.forEach((conflict, index) => {
const items = byType.get(conflict.type) ?? [];
items.push({ conflict, index });
byType.set(conflict.type, items);
});
return [...byType.entries()]
.map(([type, items]) => ({ type, items }))
.sort((a, b) => severityRank(a.type) - severityRank(b.type));
}
// 显示顺序下的原始下标序列(用于「跳到下一条未裁决」按显示顺序推进)。
export function displayOrder(groups: readonly ConflictGroup[]): number[] {
return groups.flatMap((group) => group.items.map((item) => item.index));
}
// 显示顺序中、当前聚焦之后的下一条未裁决下标;到末尾环回;无未裁决 → null。
export function nextUnresolvedInOrder(
order: readonly number[],
drafts: readonly DecisionDraft[],
fromIndex: number | null,
): number | null {
if (order.length === 0) return null;
const pos = fromIndex === null ? -1 : order.indexOf(fromIndex);
for (let step = 1; step <= order.length; step++) {
const idx = order[(pos + step + order.length) % order.length];
if (idx !== undefined && drafts[idx]?.verdict == null) return idx;
}
return null;
}