imeeting/components/PDFViewer/VirtualPDFViewer.jsx

268 lines
11 KiB
React
Raw Normal View History

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])
// 离线部署环境不依赖 unpkg 等外部静态资源。
const pdfOptions = useMemo(() => undefined, [])
// 根据 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 (
<div className="virtual-pdf-viewer-container">
{/* 工具栏 */}
<div className="pdf-toolbar">
<Space>
<Button
icon={<LeftOutlined />}
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage <= 1}
size="small"
/>
<Space.Compact>
<InputNumber
min={1}
max={numPages || 1}
value={currentPage}
onChange={handlePageChange}
size="small"
style={{ width: 60 }}
/>
<Button size="small" disabled>
/ {numPages || 0}
</Button>
</Space.Compact>
<Button
icon={<RightOutlined />}
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage >= (numPages || 0)}
size="small"
/>
<Button
icon={<VerticalAlignTopOutlined />}
onClick={scrollToTop}
size="small"
disabled={currentPage === 1}
>
回到顶部
</Button>
</Space>
<Space>
<Button icon={<ZoomOutOutlined />} onClick={zoomOut} size="small">
缩小
</Button>
<span style={{ minWidth: 50, textAlign: 'center' }}>
{Math.round(scale * 100)}%
</span>
<Button icon={<ZoomInOutlined />} onClick={zoomIn} size="small">
放大
</Button>
</Space>
</div>
{/* PDF内容区 - 自定义虚拟滚动 */}
<div className="pdf-content" ref={containerRef}>
<Document
file={fileConfig}
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={onDocumentLoadError}
loading={
<div className="pdf-loading">
<Spin size="large" />
<div style={{ marginTop: 16 }}>正在加载PDF...</div>
</div>
}
error={<div className="pdf-error">PDF加载失败请稍后重试</div>}
options={pdfOptions}
>
{numPages && (
<div style={{ height: numPages * pageHeight, position: 'relative' }}>
{Array.from({ length: numPages }, (_, index) => {
const pageNumber = index + 1
const isVisible = visiblePages.has(pageNumber)
return (
<div
key={pageNumber}
ref={el => pageRefs.current[pageNumber] = el}
className="pdf-page-wrapper"
style={{
position: 'absolute',
top: index * pageHeight,
left: 0,
right: 0,
height: pageHeight,
}}
>
{isVisible ? (
<>
<Page
pageNumber={pageNumber}
scale={scale}
renderTextLayer={true}
renderAnnotationLayer={true}
loading={
<div className="pdf-page-loading">
<Spin size="small" />
<div>加载第 {pageNumber} ...</div>
</div>
}
/>
<div className="pdf-page-number"> {pageNumber} </div>
</>
) : (
<div className="pdf-page-placeholder">
<div> {pageNumber} </div>
</div>
)}
</div>
)
})}
</div>
)}
</Document>
</div>
</div>
)
}
export default VirtualPDFViewer