import { useEffect, useRef, useState } from "react"; import { apiRaw } from "../api/client"; import { btn } from "../components/ui"; import { useProgressSaver, type ReaderProps } from "../lib/useProgress"; // 仅声明用到的最小子集,避免依赖 epubjs 自带类型的完整性 interface EpubLocation { start?: { cfi?: string; sectionIndex?: number }; } interface EpubRendition { display(target?: string): unknown; next(): unknown; prev(): unknown; on(ev: "relocated", cb: (loc: EpubLocation) => void): void; destroy(): void; } interface EpubBookLike { ready: Promise; spine?: unknown[]; renderTo(el: HTMLElement, opts: Record): EpubRendition; destroy(): void; } async function loadEpub(): Promise<(data: Uint8Array) => EpubBookLike> { const m = (await import("epubjs")) as { default?: unknown }; return m.default as (data: Uint8Array) => EpubBookLike; } export default function EpubReader({ book, initialLocator }: ReaderProps) { const hostRef = useRef(null); const rendRef = useRef(null); const saver = useProgressSaver(book.id); const [err, setErr] = useState(""); const [nonce, setNonce] = useState(0); const [ready, setReady] = useState(false); useEffect(() => { let dead = false; let bookObj: EpubBookLike | null = null; setErr(""); setReady(false); (async () => { const ePub = await loadEpub(); const buf = await apiRaw(book.file_url ?? "").then((r) => r.arrayBuffer()); if (dead) return; bookObj = ePub(new Uint8Array(buf)); // 字节已在内存,内部 iframe 不再回源,绕开鉴权问题 await bookObj.ready; if (dead || !hostRef.current) return; const spineLen = bookObj.spine?.length ?? 1; const r = bookObj.renderTo(hostRef.current, { width: "100%", height: "100%", flow: "paginated" }); rendRef.current = r; r.on("relocated", (loc) => { const cfi = loc.start?.cfi ?? ""; const idx = loc.start?.sectionIndex ?? 0; const pct = spineLen > 1 ? (idx + 1) / spineLen : 0.5; saver.report(cfi ? { cfi } : {}, Math.min(1, pct)); }); const cfi = typeof initialLocator?.cfi === "string" ? (initialLocator.cfi as string) : undefined; try { r.display(cfi); // cfi 失效(书被替换等)时 epubjs 会抛 } catch { r.display(); } setReady(true); })().catch((e) => !dead && setErr(e instanceof Error ? e.message : "EPUB 加载失败")); return () => { dead = true; rendRef.current?.destroy(); rendRef.current = null; bookObj?.destroy(); }; }, [book.file_url, nonce]); // eslint-disable-line react-hooks/exhaustive-deps if (err) return (

{err}

); return (
{!ready && !err &&
EPUB 加载中…
}
); }