'use client' import { useEffect, useRef, useState } from 'react' import { useDebounce, useSize } from 'ahooks' import { useVirtualizer } from '@tanstack/react-virtual' import type { pdfjs as PdfJsType } from 'react-pdf' import 'react-pdf/dist/Page/TextLayer.css' import 'react-pdf/dist/Page/AnnotationLayer.css' type ReactPdfModule = typeof import('react-pdf') type PdfComponents = { Document: ReactPdfModule['Document'] | null Page: ReactPdfModule['Page'] | null pdfjs: ReactPdfModule['pdfjs'] | null } type PdfLike = { numPages: number getPage: (pageNumber: number) => Promise<{ view: number[] }> } function useReactPdf() { const [components, setComponents] = useState({ Document: null, Page: null, pdfjs: null, }) useEffect(() => { // 仅在浏览器环境下加载 react-pdf,避免 Node.js 中触发 pdf.js 的 DOM 依赖 if (typeof window === 'undefined') return let cancelled = false ;(async () => { try { const mod: ReactPdfModule = await import('react-pdf') if (cancelled) return const { Document, Page, pdfjs } = mod // 配置 pdf.js worker ;(pdfjs as typeof PdfJsType).GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs` setComponents({ Document, Page, pdfjs, }) } catch (error) { console.error('Failed to load react-pdf:', error) } })() return () => { cancelled = true } }, []) return components } export function VirtualPdfPreview({ url }: { url: string }) { const [numPages, setNumPages] = useState(null) const [renderedPages, setRenderedPages] = useState([]) const [pageHeight, setPageHeight] = useState(0) const [aspectRatio, setAspectRatio] = useState(0) const [errorText, setErrorText] = useState(null) const wrapperRef = useRef(null) const parentRef = useRef(null) const oldPageHeight = useRef(0) const size = useSize(wrapperRef) const containerWidth = useDebounce(size?.width, { wait: 200 }) const { Document, Page } = useReactPdf() const virtualizer = useVirtualizer({ count: numPages || 0, getScrollElement: () => parentRef.current, estimateSize: () => (pageHeight || 800) + 10, overscan: 4, enabled: !!pageHeight, }) useEffect(() => { setRenderedPages([]) setErrorText(null) }, [containerWidth, url]) useEffect(() => { if (containerWidth && aspectRatio) { virtualizer.shouldAdjustScrollPositionOnItemSizeChange = () => false const newHeight = !aspectRatio || !containerWidth ? 800 : containerWidth / aspectRatio const lastPageIndex = oldPageHeight.current ? Number(virtualizer.scrollOffset ?? 0) / oldPageHeight.current : 0 setPageHeight(newHeight) oldPageHeight.current = newHeight virtualizer.measure() if (parentRef.current) { setTimeout(() => { parentRef.current?.scrollTo({ top: lastPageIndex * newHeight, behavior: 'auto', }) }, 100) } } }, [containerWidth, aspectRatio, virtualizer]) const onDocumentLoadSuccess = async (pdf: PdfLike) => { setErrorText(null) setNumPages(pdf.numPages) const pageObj = await pdf.getPage(1) const pageWidth = pageObj.view[2] const firstPageHeight = pageObj.view[3] const ratio = Number((pageWidth / firstPageHeight).toFixed(2)) setAspectRatio(ratio) setRenderedPages([]) } const handlePageRenderSuccess = (pageNumber: number) => { setRenderedPages(prev => (prev.includes(pageNumber) ? prev : [...prev, pageNumber])) } if (errorText) { return (
PDF 预览加载失败:{errorText}
) } // 浏览器端尚未加载到 react-pdf,给一个轻量的占位 if (!Document || !Page) { return (
正在加载 PDF 预览组件...
) } return (
PDF 加载中...
} onLoadError={(error) => { console.error('react-pdf load failed:', error) setErrorText(error instanceof Error ? error.message : '未知错误') }} onLoadSuccess={onDocumentLoadSuccess} >
{virtualizer.getVirtualItems().map(virtualRow => (
{!renderedPages.includes(virtualRow.index + 1) && (
PDF 渲染中...
)} PDF 渲染中...
} pageNumber={virtualRow.index + 1} width={containerWidth} onRenderSuccess={() => { handlePageRenderSuccess(virtualRow.index + 1) }} />
))}
) }