import { useEffect, useRef, useState } from 'react' import { ChevronLeft, ChevronRight, Download, X } from 'lucide-react' import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist' import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url' import { downloadPrivateFile, fetchOfficePreview, fetchPrivateAsset } from '../api' import type { CourseAsset } from '../types' const extension = (name: string) => name.split('.').pop()?.toLowerCase() || '' const documentTypes = new Set(['pdf', 'pptx', 'ppt', 'docx', 'doc']) const officeTypes = new Set(['pptx', 'ppt', 'docx', 'doc']) const imageTypes = new Set(['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp']) const videoTypes = new Set(['mp4', 'webm', 'mov', 'm4v']) export function canPreviewAsset(asset: CourseAsset) { const type = extension(asset.originalName) return documentTypes.has(type) || imageTypes.has(type) || videoTypes.has(type) || asset.mimeType.startsWith('image/') || asset.mimeType.startsWith('video/') } export function CourseAssetPreview({ asset, courseId, onClose }: { asset: CourseAsset courseId: string onClose: () => void }) { const type = extension(asset.originalName) const isDocument = documentTypes.has(type) const isImage = imageTypes.has(type) || asset.mimeType.startsWith('image/') const [objectUrl, setObjectUrl] = useState('') const [document, setDocument] = useState(null) const [page, setPage] = useState(1) const [loading, setLoading] = useState(true) const [rendering, setRendering] = useState(false) const [error, setError] = useState('') const canvas = useRef(null) const canvasWrap = useRef(null) useEffect(() => { let active = true let task: ReturnType | null = null setLoading(true) setError('') setPage(1) setDocument(null) setObjectUrl('') ;(async () => { try { const blob = officeTypes.has(type) ? await fetchOfficePreview(courseId, asset.id) : await fetchPrivateAsset(asset.url) if (!active) return if (isDocument) { const pdfjs = await import('pdfjs-dist') pdfjs.GlobalWorkerOptions.workerSrc = workerUrl task = pdfjs.getDocument({ data: new Uint8Array(await blob.arrayBuffer()) }) const pdf = await task.promise if (active) setDocument(pdf) } else { const url = URL.createObjectURL(blob) if (active) setObjectUrl(url) else URL.revokeObjectURL(url) } } catch (cause) { if (active) setError((cause as Error).message || '无法预览素材') } finally { if (active) setLoading(false) } })() return () => { active = false if (task) void task.destroy() } }, [asset.id, asset.url, courseId, isDocument, type]) useEffect(() => { if (!objectUrl) return return () => URL.revokeObjectURL(objectUrl) }, [objectUrl]) useEffect(() => { if (!document || !canvas.current || !canvasWrap.current) return let cancelled = false let renderTask: RenderTask | null = null setRendering(true) ;(async () => { try { const pdfPage = await document.getPage(page) if (cancelled || !canvas.current || !canvasWrap.current) return const canvasElement = canvas.current const context = canvasElement.getContext('2d') if (!context) throw new Error('浏览器无法绘制预览') const base = pdfPage.getViewport({ scale: 1 }) const fitWidth = Math.max(280, Math.min(canvasWrap.current.clientWidth - 32, 1300)) const viewport = pdfPage.getViewport({ scale: fitWidth / base.width }) const dpr = Math.min(window.devicePixelRatio || 1, 2) canvasElement.width = Math.round(viewport.width * dpr) canvasElement.height = Math.round(viewport.height * dpr) canvasElement.style.width = `${Math.round(viewport.width)}px` canvasElement.style.height = `${Math.round(viewport.height)}px` renderTask = pdfPage.render({ canvas: canvasElement, canvasContext: context, viewport, transform: [dpr, 0, 0, dpr, 0, 0] }) await renderTask.promise } catch (cause) { if (!cancelled && !(cause instanceof Error && cause.name === 'RenderingCancelledException')) setError((cause as Error).message) } finally { if (!cancelled) setRendering(false) } })() return () => { cancelled = true; renderTask?.cancel() } }, [document, page]) useEffect(() => { const keyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') onClose() if (!document) return if (event.key === 'ArrowRight') setPage((current) => Math.min(document.numPages, current + 1)) if (event.key === 'ArrowLeft') setPage((current) => Math.max(1, current - 1)) } window.addEventListener('keydown', keyDown) return () => window.removeEventListener('keydown', keyDown) }, [document, onClose]) return
{ if (event.target === event.currentTarget) onClose() }}>
{asset.originalName}{isDocument ? officeTypes.has(type) ? '文档预览' : 'PDF 预览' : isImage ? '图片预览' : '视频预览'}{officeTypes.has(type) && 预览可能存在字体或排版差异,请以下载的原文件为准。}
{loading &&

正在加载预览{officeTypes.has(type) ? ',首次转换可能需要一些时间' : ''}…

} {error &&

{error}

可以下载原文件查看。
} {isDocument && } {!isDocument && objectUrl && (isImage ? {asset.originalName} setError('浏览器不支持预览此图片格式')} /> :
}