Files
yc-modular-extractor-archived/components/PdfPreview.tsx
T
2026-11-05 01:04:53 +08:00

17 lines
4.2 KiB
TypeScript

import {useEffect,useRef,useState} from 'react';
import type {PDFDocumentProxy,RenderTask} from 'pdfjs-dist';
import {ChevronLeft,ChevronRight,Minus,Plus,Download} from 'lucide-react';
export default function PdfPreview({url,token='',name='document.pdf'}:{url:string;token?:string;name?:string}){
const canvas=useRef<HTMLCanvasElement>(null),container=useRef<HTMLDivElement>(null);
const [pdf,setPdf]=useState<PDFDocumentProxy|null>(null),[page,setPage]=useState(1),[zoom,setZoom]=useState(1),[width,setWidth]=useState(450),[error,setError]=useState(''),[loading,setLoading]=useState(true),[retry,setRetry]=useState(0);
useEffect(()=>{const node=container.current;if(!node)return;const observer=new ResizeObserver(entries=>setWidth(Math.max(100,entries[0].contentRect.width-32)));observer.observe(node);return()=>observer.disconnect();},[]);
useEffect(()=>{let alive=true;let task:ReturnType<typeof import('pdfjs-dist')['getDocument']>|undefined;const abort=new AbortController();setPdf(null);setPage(1);setZoom(1);setError('');setLoading(true);
(async()=>{const lib=await import('pdfjs-dist');if(!alive)return;lib.GlobalWorkerOptions.workerSrc='/pdfjs/pdf.worker.min.mjs';const r=await fetch(url,{signal:abort.signal,headers:token?{Authorization:`Bearer ${token}`}:{}});if(!r.ok)throw new Error(r.status===401?'请输入正确的 API 访问令牌后预览。':'文档读取失败,请重试。');const bytes=new Uint8Array(await r.arrayBuffer());if(!alive)return;task=lib.getDocument({data:bytes,cMapUrl:'/pdfjs/cmaps/',cMapPacked:true,standardFontDataUrl:'/pdfjs/standard_fonts/',wasmUrl:'/pdfjs/wasm/'});const doc=await task.promise;if(alive)setPdf(doc);})().catch(e=>{if(alive&&e.name!=='AbortError'){setError(e.name==='PasswordException'?'此 PDF 已加密,请先解密后上传。':e.message||'PDF 无法读取,请检查文件是否损坏。');setLoading(false);}});
return()=>{alive=false;abort.abort();void task?.destroy();};},[url,token,retry]);
useEffect(()=>{if(!pdf||!canvas.current)return;let alive=true,render:RenderTask|undefined;setLoading(true);
(async()=>{const p=await pdf.getPage(page);if(!alive||!canvas.current)return;const base=p.getViewport({scale:1});const viewport=p.getViewport({scale:(width/base.width)*zoom});const c=canvas.current,ratio=Math.min(window.devicePixelRatio||1,2);c.width=Math.ceil(viewport.width*ratio);c.height=Math.ceil(viewport.height*ratio);c.style.width=viewport.width+'px';c.style.height=viewport.height+'px';render=p.render({canvas:c,viewport,transform:ratio===1?undefined:[ratio,0,0,ratio,0,0]});await render.promise;if(alive)setLoading(false);})().catch(e=>{if(alive&&e.name!=='RenderingCancelledException'){setError('PDF 页面渲染失败,请重试。');setLoading(false);}});
return()=>{alive=false;render?.cancel();};},[pdf,page,width,zoom]);
async function download(){try{const r=await fetch(url,{headers:token?{Authorization:`Bearer ${token}`}:{}});if(!r.ok)throw Error();const u=URL.createObjectURL(await r.blob());const a=document.createElement('a');a.href=u;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(u),1000);}catch{setError('下载失败,请检查访问权限。');}}
return <div className="pdf-viewer" ref={container}><div className="pdf-toolbar"><div><button title="上一页" disabled={!pdf||page<=1} onClick={()=>setPage(p=>p-1)}><ChevronLeft size={16}/></button><span>{pdf?`${page} / ${pdf.numPages}`:'— / —'}</span><button title="下一页" disabled={!pdf||page>=pdf.numPages} onClick={()=>setPage(p=>p+1)}><ChevronRight size={16}/></button></div><div><button title="缩小" disabled={!pdf||zoom<=.5} onClick={()=>setZoom(z=>Math.max(.5,z-.25))}><Minus size={14}/></button><button title="适应宽度" onClick={()=>setZoom(1)}>{Math.round(zoom*100)}%</button><button title="放大" disabled={!pdf||zoom>=3} onClick={()=>setZoom(z=>Math.min(3,z+.25))}><Plus size={14}/></button><button title="下载 PDF" onClick={download}><Download size={15}/></button></div></div>{error?<div className="pdf-message" role="alert"><p>{error}</p><button onClick={()=>setRetry(n=>n+1)}>重新加载</button></div>:<><div className="pdf-status" role="status">{loading?'正在渲染 PDF…':`第 ${page} 页,共 ${pdf?.numPages||0} 页`}</div><div className="pdf-pages"><canvas aria-label={`PDF 第 ${page} 页`} ref={canvas}/></div></>}</div>;
}