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。
68 lines
2.4 KiB
TypeScript
68 lines
2.4 KiB
TypeScript
// 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;
|
||
}
|