"use client"; import { useEffect, useRef, useState } from "react"; import { api } from "@/lib/api/client"; import { useToast } from "@/components/Toast"; import { toCodexGroups, toOutlineRows, type CodexGroup, type ContextTabKey, type OutlineRow, } from "./contextDrawer"; // 速查抽屉的数据层:抽屉打开且切到对应 Tab 时【懒加载】设定库 / 大纲(只读,绝不写库)。 // 加载成功后缓存(切走再切回不重复拉);项目切换清缓存;失败可 reload 重试。 export type ResourceStatus = "idle" | "loading" | "ready" | "error"; export interface ContextResource { status: ResourceStatus; data: T; } export interface UseContextDrawerData { codex: ContextResource; outline: ContextResource; reloadCodex: () => void; reloadOutline: () => void; } const CODEX_PATH = "/projects/{project_id}/world_entities" as const; const OUTLINE_PATH = "/projects/{project_id}/outline" as const; const EMPTY_CODEX: CodexGroup[] = []; const EMPTY_OUTLINE: OutlineRow[] = []; export function useContextDrawerData( projectId: string, activeTab: ContextTabKey, open: boolean, ): UseContextDrawerData { const [codex, setCodex] = useState>({ status: "idle", data: EMPTY_CODEX, }); const [outline, setOutline] = useState>({ status: "idle", data: EMPTY_OUTLINE, }); const toast = useToast(); // 单飞哨兵:已发起过请求就不再重复(成功缓存 / 在途去重);出错时重置以允许重试。 const codexReqRef = useRef(false); const outlineReqRef = useRef(false); // reload 触发器:自增即重新拉取(配合把 req 哨兵复位)。 const [codexNonce, setCodexNonce] = useState(0); const [outlineNonce, setOutlineNonce] = useState(0); // 项目切换 → 清缓存 + 复位哨兵,允许对新项目重新拉取。声明在拉取 effect 之前, // 保证同一次提交里先复位、后续 effect 才据此触发。 useEffect(() => { codexReqRef.current = false; outlineReqRef.current = false; setCodex({ status: "idle", data: EMPTY_CODEX }); setOutline({ status: "idle", data: EMPTY_OUTLINE }); }, [projectId]); useEffect(() => { if (!open || activeTab !== "codex" || codexReqRef.current) return; codexReqRef.current = true; let cancelled = false; setCodex({ status: "loading", data: EMPTY_CODEX }); void (async () => { try { const { data, error } = await api.GET(CODEX_PATH, { params: { path: { project_id: projectId } }, }); if (cancelled) return; if (error || !data) { codexReqRef.current = false; setCodex({ status: "error", data: EMPTY_CODEX }); toast("设定库加载失败,请重试。", "error"); return; } setCodex({ status: "ready", data: toCodexGroups(data.world_entities ?? []), }); } catch { if (cancelled) return; codexReqRef.current = false; setCodex({ status: "error", data: EMPTY_CODEX }); toast("设定库加载异常,请检查网络。", "error"); } })(); return () => { cancelled = true; }; }, [open, activeTab, projectId, codexNonce, toast]); useEffect(() => { if (!open || activeTab !== "outline" || outlineReqRef.current) return; outlineReqRef.current = true; let cancelled = false; setOutline({ status: "loading", data: EMPTY_OUTLINE }); void (async () => { try { const { data, error } = await api.GET(OUTLINE_PATH, { params: { path: { project_id: projectId } }, }); if (cancelled) return; if (error || !data) { outlineReqRef.current = false; setOutline({ status: "error", data: EMPTY_OUTLINE }); toast("大纲加载失败,请重试。", "error"); return; } setOutline({ status: "ready", data: toOutlineRows(data.chapters ?? []), }); } catch { if (cancelled) return; outlineReqRef.current = false; setOutline({ status: "error", data: EMPTY_OUTLINE }); toast("大纲加载异常,请检查网络。", "error"); } })(); return () => { cancelled = true; }; }, [open, activeTab, projectId, outlineNonce, toast]); const reloadCodex = (): void => { codexReqRef.current = false; setCodex({ status: "idle", data: EMPTY_CODEX }); setCodexNonce((n) => n + 1); }; const reloadOutline = (): void => { outlineReqRef.current = false; setOutline({ status: "idle", data: EMPTY_OUTLINE }); setOutlineNonce((n) => n + 1); }; return { codex, outline, reloadCodex, reloadOutline }; }