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): Promise | 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; // epubjs 对失效 cfi 以 reject Promise signalling,而非同步 throw → 走 then/catch 回退 Promise.resolve(cfi ? r.display(cfi) : r.display()) .then(() => setReady(true)) .catch(() => { if (!cfi) { setErr("EPUB 无法打开"); return; } Promise.resolve(r.display()) .then(() => setReady(true)) .catch(() => setErr("EPUB 无法打开")); }); })().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 加载中…
}
); }