Files
test1/components/nova-sdk/task-panel/Preview/VirtualPdfPreview.tsx
2026-03-20 07:33:46 +00:00

218 lines
6.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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<PdfComponents>({
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<number | null>(null)
const [renderedPages, setRenderedPages] = useState<number[]>([])
const [pageHeight, setPageHeight] = useState(0)
const [aspectRatio, setAspectRatio] = useState(0)
const [errorText, setErrorText] = useState<string | null>(null)
const wrapperRef = useRef<HTMLDivElement>(null)
const parentRef = useRef<HTMLDivElement>(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 (
<div className="h-full w-full flex items-center justify-center text-sm text-destructive px-4 text-center">
PDF {errorText}
</div>
)
}
// 浏览器端尚未加载到 react-pdf给一个轻量的占位
if (!Document || !Page) {
return (
<div className="h-full w-full flex items-center justify-center text-sm text-muted-foreground px-4 text-center">
PDF ...
</div>
)
}
return (
<div ref={wrapperRef} className="w-full h-full mt-[80px]">
<div
ref={parentRef}
className="h-full overflow-scroll [&_.react-pdf__message.react-pdf__message--loading]:h-full [&::-webkit-scrollbar]:hidden scroll-smooth [-webkit-overflow-scrolling:touch]"
>
<Document
file={url}
loading={
<div className="h-full w-full flex items-center justify-center text-sm text-muted-foreground">
PDF ...
</div>
}
onLoadError={(error) => {
console.error('react-pdf load failed:', error)
setErrorText(error instanceof Error ? error.message : '未知错误')
}}
onLoadSuccess={onDocumentLoadSuccess}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
className="w-full flex justify-center duration-200 transition-transform"
>
{!renderedPages.includes(virtualRow.index + 1) && (
<div
className="absolute top-0 left-0 right-0 z-1 flex items-center justify-center bg-card/94 text-sm text-muted-foreground duration-200 transition-transform backdrop-blur-sm"
style={{
height: `${pageHeight}px`,
}}
>
PDF ...
</div>
)}
<Page
loading={
<div className="h-full w-full flex items-center justify-center text-sm text-muted-foreground">
PDF ...
</div>
}
pageNumber={virtualRow.index + 1}
width={containerWidth}
onRenderSuccess={() => {
handlePageRenderSuccess(virtualRow.index + 1)
}}
/>
</div>
))}
</div>
</Document>
</div>
</div>
)
}