// 大纲编辑器纯逻辑:按卷分组、伏笔窗口徽标文案、接近回收窗口提示(UX §6.7)。 // OutlineChapterView/ForeshadowWindowView 已由 OpenAPI 强类型,无需手动收窄。 import type { ForeshadowWindowView, OutlineChapterView } from "@/lib/api/types"; export interface VolumeGroup { volume: number; chapters: OutlineChapterView[]; } // 按 volume 分组(保持章号升序);卷号升序。 export function groupByVolume( chapters: readonly OutlineChapterView[] | undefined, ): VolumeGroup[] { const byVolume = new Map(); for (const ch of chapters ?? []) { const list = byVolume.get(ch.volume) ?? []; byVolume.set(ch.volume, [...list, ch]); } return [...byVolume.entries()] .sort(([a], [b]) => a - b) .map(([volume, list]) => ({ volume, chapters: [...list].sort((a, b) => a.no - b.no), })); } // 视图卷过滤值:"all" = 全部卷;具体数字 = 只看该卷(对齐后端 GET ?volume)。 export type ViewVolume = number | "all"; // 已存大纲里出现过的卷号(升序、去重)——驱动「全部 / 卷 N」选择器选项。 export function distinctVolumes( chapters: readonly OutlineChapterView[] | undefined, ): number[] { const seen = new Set(); for (const ch of chapters ?? []) seen.add(ch.volume); return [...seen].sort((a, b) => a - b); } // 按视图卷过滤:选「全部」返回全部,选具体卷只留该卷(客户端过滤,避免重复请求)。 export function filterByViewVolume( chapters: readonly OutlineChapterView[] | undefined, view: ViewVolume, ): OutlineChapterView[] { const all = [...(chapters ?? [])]; if (view === "all") return all; return all.filter((ch) => ch.volume === view); } // 伏笔窗口徽标文案(纯文本,旗标图标由渲染层的 lucide Flag 承载,不掺字形): // `F-012 (40-60)` / `F-012 (≤60)` / `F-012 (≥40)` / `F-012`。 export function windowBadgeLabel(window: ForeshadowWindowView): string { const from = window.expected_close_from; const to = window.expected_close_to; if (typeof from === "number" && typeof to === "number") { return `${window.code} (${from}-${to})`; } if (typeof to === "number") return `${window.code} (≤${to})`; if (typeof from === "number") return `${window.code} (≥${from})`; return window.code; } // 接近回收:本章号落在窗口 [from,to] 内(提示「本章可回收」,UX §6.7 ⚑ 可回收)。 export function isCloseWindow( chapter: OutlineChapterView, window: ForeshadowWindowView, ): boolean { const to = window.expected_close_to; if (typeof to !== "number") return false; const from = window.expected_close_from ?? to; return chapter.no >= from && chapter.no <= to; }