import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, PanelLeft } from "lucide-react"; import { useEffect, useRef, useState, type KeyboardEvent } from "react"; import * as pdfjs from "pdfjs-dist"; import { apiRaw } from "../api/client"; import { BookmarksTab } from "../components/Bookmarks"; import { ReaderNav } from "../components/reader-nav"; import { ReaderSettings } from "../components/reader-settings"; import { Button } from "@/components/ui/button"; import { useProgressSaver, type ReaderProps } from "../lib/useProgress"; // Vite 原生 worker 语法;pdfjs v5 要求 module worker pdfjs.GlobalWorkerOptions.workerPort = new Worker(new URL("pdfjs-dist/build/pdf.worker.mjs", import.meta.url), { type: "module", }); type RenderTask = { cancel(): void; promise: Promise }; export default function PdfReader({ book, initialLocator, initialPercent }: ReaderProps) { const wrapRef = useRef(null); const canvasRef = useRef(null); const docRef = useRef(null); const docTaskRef = useRef<{ destroy(): Promise } | null>(null); const taskRef = useRef(null); const restored = useRef(false); const saver = useProgressSaver(book.id, { locator: initialLocator, percent: initialPercent }); const [nav, setNav] = useState(false); const [num, setNum] = useState(0); const [page, setPage] = useState(0); // 0 基 const [err, setErr] = useState(""); const [nonce, setNonce] = useState(0); // 仅加载重试 const [redraw, setRedraw] = useState(0); // 仅 resize 重绘(不重新下载文档) // 重新加载(换书/重试)时先清错误:React 官方推荐的「渲染期按下发 key 重置本地 state」, // 替代原先 effect 内的同步 setState(级联渲染);首挂载 key 相等,重置为空操作,行为等价 const loadKey = (book.file_url ?? "") + "#" + nonce; const [prevLoadKey, setPrevLoadKey] = useState(loadKey); if (prevLoadKey !== loadKey) { setPrevLoadKey(loadKey); setErr(""); } useEffect(() => { let dead = false; (async () => { const buf = await apiRaw(book.file_url ?? "").then((r) => r.arrayBuffer()); if (dead) return; const task = pdfjs.getDocument({ data: new Uint8Array(buf) }); docTaskRef.current = task; const doc = await task.promise; if (dead) { await task.destroy(); return; } docRef.current = doc; setNum(doc.numPages); if (!restored.current) { restored.current = true; const p = Number(initialLocator?.page); if (Number.isInteger(p) && p > 0 && p < doc.numPages) setPage(p); } })().catch((e) => !dead && setErr(e instanceof Error ? e.message : "PDF 加载失败")); return () => { dead = true; taskRef.current?.cancel(); docTaskRef.current?.destroy(); docTaskRef.current = null; docRef.current = null; }; }, [book.file_url, nonce]); // initialLocator 是查询缓存值,加载一次即稳定 // 渲染当前页 useEffect(() => { const doc = docRef.current; const canvas = canvasRef.current; const wrap = wrapRef.current; if (!doc || !canvas || !wrap || num === 0) return; let cancelled = false; (async () => { const p = await doc.getPage(page + 1); if (cancelled) return; const cssW = wrap.clientWidth; const dpr = window.devicePixelRatio || 1; const base = p.getViewport({ scale: 1 }); const vp = p.getViewport({ scale: (cssW * dpr) / base.width }); canvas.width = Math.floor(vp.width); canvas.height = Math.floor(vp.height); canvas.style.width = "100%"; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("canvas 2d 上下文不可用"); const t = p.render({ canvas, viewport: vp }) as RenderTask; taskRef.current = t; await t.promise; })().catch((e) => { if (!cancelled && (e as { name?: string }).name !== "AbortException") setErr(e instanceof Error ? e.message : "渲染失败"); }); saver.report({ page }, (page + 1) / num); return () => { cancelled = true; taskRef.current?.cancel(); }; }, [page, num, redraw, saver]); // 容器尺寸变化 → 重绘 useEffect(() => { const wrap = wrapRef.current; if (!wrap) return; let t: ReturnType; const ro = new ResizeObserver(() => { clearTimeout(t); t = setTimeout(() => setRedraw((x) => x + 1), 200); }); ro.observe(wrap); return () => { clearTimeout(t); ro.disconnect(); }; }, []); function onKey(e: KeyboardEvent) { if (e.key === "ArrowRight" || e.key === "PageDown") setPage((p) => Math.min(num - 1, p + 1)); else if (e.key === "ArrowLeft" || e.key === "PageUp") setPage((p) => Math.max(0, p - 1)); else return; e.preventDefault(); } if (err) return (

{err}

); if (num === 0) return
PDF 加载中…
; return (
{page + 1}/{num} } /> {nav && ( setNav(false)} bookmarks={ { const p = Number(l.page); setNav(false); if (Number.isInteger(p) && p >= 0 && p < num) setPage(p); }} /> } /> )}
); }