feat(foreshadow): 写作台本章伏笔主动提醒(前端)

- 新增 chapterForeshadow 纯分类:按当前章分 可回收/待回收/已逾期(启用闲置的 isWindowApproaching)
- useChapterForeshadow hook(复用 GET /foreshadow,切章仅重分类不重拉)
- 右栏「本章参考」+ ContextDrawer 伏笔页新增「本章伏笔」实时清单,
  分色展示 + 跳看板链接 + 空态;标注『按已验收进度』
This commit is contained in:
Yaojia Wang
2026-07-12 17:53:00 +02:00
parent 9aa0cddeaf
commit b810a3fa3c
8 changed files with 507 additions and 4 deletions

View File

@@ -0,0 +1,51 @@
// 「本章伏笔」主动提醒的纯分类逻辑(写作台右栏 / 速查抽屉共用)。
// 输入全量伏笔 + 当前章号,用 board 的 isWindowApproaching + status 分三类。
// 纯函数、不可变(返回全新对象,不改动入参);便于 node 环境单测。
import type { ForeshadowView } from "@/lib/api/types";
import { isWindowApproaching, LANE_LABELS, type ForeshadowStatus } from "./board";
// 本章伏笔三分类:
// - recyclable 可回收:本章落在 [from,to] 窗口内OPEN/PARTIAL由 isWindowApproaching 判定)。
// - pending 待回收OPEN/PARTIAL 但尚未进入回收窗口(窗口在后 / 未设窗口)。
// - overdue 已逾期status==OVERDUE后端验收后扫描得出略滞后于当前草稿
// CLOSED已回收与未知 status 不进任何一类——本章无需再提醒。
export interface ChapterForeshadowGroups {
recyclable: ForeshadowView[];
pending: ForeshadowView[];
overdue: ForeshadowView[];
}
// 按当前章号把伏笔分三类。纯函数:用 reduce + 展开返回新对象,不可变。
// 种子每次新建(不复用共享常量),空输入也返回独立的新对象。
export function classifyChapterForeshadow(
items: readonly ForeshadowView[] | undefined,
currentChapter: number,
): ChapterForeshadowGroups {
return (items ?? []).reduce<ChapterForeshadowGroups>(
(groups, item) => {
if (item.status === "OVERDUE") {
return { ...groups, overdue: [...groups.overdue, item] };
}
if (isWindowApproaching(item, currentChapter)) {
return { ...groups, recyclable: [...groups.recyclable, item] };
}
if (item.status === "OPEN" || item.status === "PARTIAL") {
return { ...groups, pending: [...groups.pending, item] };
}
return groups; // CLOSED / 未知 status本章不提醒
},
{ recyclable: [], pending: [], overdue: [] },
);
}
// 三类合计条数(是否有可提醒项,便于空态判断)。
export function chapterForeshadowTotal(groups: ChapterForeshadowGroups): number {
return groups.recyclable.length + groups.pending.length + groups.overdue.length;
}
// 单条伏笔的作者向状态文案(复用看板 LANE_LABELS未知 status 回退原值)。
export function foreshadowStatusLabel(status: string): string {
return LANE_LABELS[status as ForeshadowStatus] ?? status;
}