初始化模版工程

This commit is contained in:
Cloud Bot
2026-03-20 07:33:46 +00:00
commit 23717e0ecd
386 changed files with 51675 additions and 0 deletions

View File

@@ -0,0 +1,217 @@
'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>
)
}