From 4b40353d937f34a0937a3fc25f13e6355de35622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E7=99=BB=E5=B1=B1?= Date: Mon, 9 Mar 2026 17:18:46 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=20=20PDF=20preview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/globals.css | 2 + components/object/pdf-viewer.tsx | 79 ++++++++++++ components/object/preview-modal.tsx | 13 +- package.json | 2 + pnpm-lock.yaml | 192 ++++++++++++++++++++++++++++ 5 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 components/object/pdf-viewer.tsx diff --git a/app/globals.css b/app/globals.css index 6d47136..e3163e0 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,6 +1,8 @@ @import "tailwindcss"; @import "tw-animate-css"; @import "shadcn/tailwind.css"; +@import "react-pdf/dist/Page/AnnotationLayer.css"; +@import "react-pdf/dist/Page/TextLayer.css"; @custom-variant dark (&:is(.dark *)); diff --git a/components/object/pdf-viewer.tsx b/components/object/pdf-viewer.tsx new file mode 100644 index 0000000..3f2587d --- /dev/null +++ b/components/object/pdf-viewer.tsx @@ -0,0 +1,79 @@ +"use client" + +import * as React from "react" +import { useTranslation } from "react-i18next" +import { Document, Page, pdfjs } from "react-pdf" +import { Spinner } from "@/components/ui/spinner" + +pdfjs.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).toString() + +interface PdfViewerProps { + url: string +} + +export function PdfViewer({ url }: PdfViewerProps) { + const { t } = useTranslation() + const containerRef = React.useRef(null) + const [numPages, setNumPages] = React.useState(0) + const [containerWidth, setContainerWidth] = React.useState(0) + const [loadError, setLoadError] = React.useState(null) + + React.useEffect(() => { + setNumPages(0) + setLoadError(null) + }, [url]) + + React.useEffect(() => { + const container = containerRef.current + if (!container) return + + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + setContainerWidth(Math.floor(entry.contentRect.width)) + } + }) + + observer.observe(container) + setContainerWidth(Math.floor(container.getBoundingClientRect().width)) + + return () => { + observer.disconnect() + } + }, []) + + if (!url) { + return ( +
{t("Preview unavailable")}
+ ) + } + + const pageWidth = containerWidth > 0 ? Math.max(Math.min(containerWidth - 24, 960), 240) : 800 + + return ( +
+ + +
+ } + error={
{loadError ?? t("Preview unavailable")}
} + onLoadSuccess={({ numPages: pages }) => { + setNumPages(pages) + }} + onLoadError={() => { + setLoadError(t("Preview unavailable")) + }} + > +
+ {Array.from({ length: numPages }, (_, index) => ( +
+ +
+ ))} +
+ + + ) +} diff --git a/components/object/preview-modal.tsx b/components/object/preview-modal.tsx index 4169d5b..2cbf52a 100644 --- a/components/object/preview-modal.tsx +++ b/components/object/preview-modal.tsx @@ -7,13 +7,14 @@ import { Spinner } from "@/components/ui/spinner" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" import { RiFullscreenExitLine, RiFullscreenLine } from "@remixicon/react" +import { PdfViewer } from "@/components/object/pdf-viewer" const SAFE_TEXT_MIMES = ["application/json", "application/xml", "text/plain", "text/xml", "text/csv", "text/markdown"] const SAFE_TEXT_EXTENSIONS = [".txt", ".json", ".xml", ".csv", ".md", ".yml", ".yaml"] const SAFE_IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".ico", ".tif", ".tiff"] const ALLOWED_SIZE = 1024 * 1024 * 2 // 2MB -type PreviewMode = "text" | "image" | "sandbox" | "download" +type PreviewMode = "text" | "image" | "pdf" | "sandbox" | "download" interface ObjectPreviewModalProps { show: boolean @@ -51,6 +52,11 @@ function getPreviewMode(hasPreviewUrl: boolean, canRenderText: boolean, canRende return canRenderText ? "text" : "sandbox" } +function isPdfPreview(contentType: string, objectKey: string) { + const keyLower = objectKey.toLowerCase() + return contentType === "application/pdf" || keyLower.endsWith(".pdf") +} + export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreviewModalProps) { const { t } = useTranslation() const [textContent, setTextContent] = React.useState("") @@ -75,7 +81,8 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview const isJson = normalizedContentType === "application/json" || objectKeyLower.endsWith(".json") const canRenderText = hasPreviewUrl && isSafeTextPreview(normalizedContentType, objectKey, objectSize) const canRenderImage = hasPreviewUrl && isImagePreview(normalizedContentType, objectKey) - const previewMode = getPreviewMode(hasPreviewUrl, canRenderText, canRenderImage) + const canRenderPdf = hasPreviewUrl && isPdfPreview(normalizedContentType, objectKey) + const previewMode = canRenderPdf ? "pdf" : getPreviewMode(hasPreviewUrl, canRenderText, canRenderImage) const isImageMode = previewMode === "image" const getFormattedContent = () => { @@ -276,6 +283,8 @@ export function ObjectPreviewModal({ show, onShowChange, object }: ObjectPreview return (