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,71 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { api } from "@/lib/api/client";
import type { ForeshadowView } from "@/lib/api/types";
import {
classifyChapterForeshadow,
type ChapterForeshadowGroups,
} from "./chapterForeshadow";
// 写作台「本章伏笔」提醒的数据层拉本项目全量伏笔GET /foreshadow只读
// 再按当前章号本地分类。项目内一次拉取,切章仅重新分类(不重复请求)。
export type ChapterForeshadowStatus = "loading" | "ready" | "error";
export interface UseChapterForeshadow {
groups: ChapterForeshadowGroups;
status: ChapterForeshadowStatus;
reload: () => void;
}
const FORESHADOW_PATH = "/projects/{project_id}/foreshadow" as const;
const EMPTY_ITEMS: ForeshadowView[] = [];
export function useChapterForeshadow(
projectId: string,
chapterNo: number,
): UseChapterForeshadow {
const [items, setItems] = useState<ForeshadowView[]>(EMPTY_ITEMS);
const [status, setStatus] = useState<ChapterForeshadowStatus>("loading");
// reload 触发器:自增即重新拉取。
const [nonce, setNonce] = useState(0);
useEffect(() => {
let cancelled = false;
setStatus("loading");
void (async () => {
try {
const { data, error } = await api.GET(FORESHADOW_PATH, {
params: { path: { project_id: projectId } },
});
if (cancelled) return;
if (error || !data) {
setItems(EMPTY_ITEMS);
setStatus("error");
return;
}
setItems(data.foreshadow ?? EMPTY_ITEMS);
setStatus("ready");
} catch {
// 网络层失败(后端不可达/CORSopenapi-fetch 直接抛而非返回 error 信封。
if (cancelled) return;
setItems(EMPTY_ITEMS);
setStatus("error");
}
})();
return () => {
cancelled = true;
};
}, [projectId, nonce]);
// 切章不重拉,仅重新分类(伏笔窗口相对章号变化)。
const groups = useMemo(
() => classifyChapterForeshadow(items, chapterNo),
[items, chapterNo],
);
const reload = useCallback(() => setNonce((n) => n + 1), []);
return { groups, status, reload };
}