import { useState, useMemo, useRef, useEffect, useCallback } from 'react' import { Document, Page, pdfjs } from 'react-pdf' import { Button, Space, InputNumber, message, Spin } from 'antd' import { ZoomInOutlined, ZoomOutOutlined, VerticalAlignTopOutlined, LeftOutlined, RightOutlined, } from '@ant-design/icons' import 'react-pdf/dist/Page/AnnotationLayer.css' import 'react-pdf/dist/Page/TextLayer.css' import './VirtualPDFViewer.css' // 配置 PDF.js worker pdfjs.GlobalWorkerOptions.workerSrc = '/pdf-worker/pdf.worker.min.mjs' function VirtualPDFViewer({ url, filename }) { const [numPages, setNumPages] = useState(null) const [scale, setScale] = useState(1.0) const [pdfOriginalSize, setPdfOriginalSize] = useState({ width: 595, height: 842 }) // 默认 A4 const [currentPage, setCurrentPage] = useState(1) const [visiblePages, setVisiblePages] = useState(new Set([1])) const containerRef = useRef(null) const pageRefs = useRef({}) // 使用 useMemo 避免不必要的重新加载 const fileConfig = useMemo(() => ({ url }), [url]) // Memoize PDF.js options to prevent unnecessary reloads const pdfOptions = useMemo(() => ({ cMapUrl: 'https://unpkg.com/pdfjs-dist@5.4.296/cmaps/', cMapPacked: true, standardFontDataUrl: 'https://unpkg.com/pdfjs-dist@5.4.296/standard_fonts/', }), []) // 根据 PDF 实际宽高和缩放比例计算页面高度 const pageHeight = useMemo(() => { // 计算内容高度:缩放后的 PDF 高度 + 上下 padding (40px) + 页码文字区域 (20px) return Math.ceil(pdfOriginalSize.height * scale) + 60 }, [scale, pdfOriginalSize.height]) const onDocumentLoadError = (error) => { console.error('[PDF] Document load error:', error) message.error('PDF文件加载失败') } // Handle scroll to update visible pages const handleScroll = useCallback(() => { if (!containerRef.current || !numPages) return const container = containerRef.current const scrollTop = container.scrollTop const containerHeight = container.clientHeight // Calculate which pages are visible // Add small tolerance (1px) to handle browser scroll precision issues const pageIndex = scrollTop / pageHeight let firstVisiblePage = Math.max(1, Math.ceil(pageIndex + 0.001)) // Special case: if scrolled to bottom, show last page const isAtBottom = scrollTop + containerHeight >= container.scrollHeight - 1 if (isAtBottom) { firstVisiblePage = numPages } const lastVisiblePage = Math.min(numPages, Math.ceil((scrollTop + containerHeight) / pageHeight)) // Add buffer pages (2 before and 2 after) const newVisiblePages = new Set() for (let i = Math.max(1, firstVisiblePage - 2); i <= Math.min(numPages, lastVisiblePage + 2); i++) { newVisiblePages.add(i) } setVisiblePages(newVisiblePages) setCurrentPage(firstVisiblePage) }, [numPages, pageHeight]) const onDocumentLoadSuccess = useCallback(async (pdf) => { setNumPages(pdf.numPages) try { // 获取第一页的原始尺寸,用于计算初始缩放 const page = await pdf.getPage(1) const viewport = page.getViewport({ scale: 1.0 }) const { width, height } = viewport setPdfOriginalSize({ width, height }) // 自动适应宽度:仅当 PDF 宽度超过容器时才进行缩放 if (containerRef.current) { const containerWidth = containerRef.current.clientWidth - 40 // 减去左右内边距 if (width > containerWidth) { const autoScale = Math.floor((containerWidth / width) * 10) / 10 // 保留一位小数 setScale(Math.min(Math.max(autoScale, 0.5), 1.0)) // 限制缩放比例,最高不超 1.0 } else { setScale(1.0) // 宽度足够则保持 100% } } } catch (err) { console.error('Error calculating initial scale:', err) } // Initially show first 3 pages setVisiblePages(new Set([1, 2, 3])) // Trigger scroll calculation after a short delay to ensure DOM is ready setTimeout(() => { handleScroll() }, 200) }, [handleScroll]) // Attach scroll listener useEffect(() => { const container = containerRef.current if (!container) return container.addEventListener('scroll', handleScroll) return () => container.removeEventListener('scroll', handleScroll) }, [handleScroll, numPages, pageHeight]) const zoomIn = () => { setScale((prev) => Math.min(prev + 0.2, 3.0)) } const zoomOut = () => { setScale((prev) => Math.max(prev - 0.2, 0.5)) } const handlePageChange = (value) => { if (value >= 1 && value <= numPages && containerRef.current) { const scrollTop = (value - 1) * pageHeight const container = containerRef.current container.scrollTo({ top: scrollTop, behavior: 'auto' }) // Manually trigger handleScroll after scrolling to ensure page number updates setTimeout(() => { handleScroll() }, 50) } } const scrollToTop = () => { if (containerRef.current) { containerRef.current.scrollTo({ top: 0, behavior: 'smooth' }) } } return (