Files
senmesh-zc/frontend/src/components/PDFPreview.tsx
KodouWZF 47273ae3fd init: 信创文件安全传输系统内外网本地 Docker 部署
包含 s-file 后端、React 前端、MeshSafe 安全检测对接,以及内外网两套
compose/nginx/配置(仅前端 28180/28280 对外发布,其余端口不暴露)。

说明:deploy/config/application-iw.yml 与 application-ow.yml 按维护者
明确要求原样入库,其中含本机 Docker 用的库口令、RabbitMQ 口令与 JWT
密钥;上生产前需替换。
2026-11-02 10:59:03 +08:00

138 lines
3.8 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
type Props = {
url?: string;
};
export function PDFPreview({ url }: Props) {
const [loaded, setLoaded] = useState(false);
const [blobUrl, setBlobUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [stage, setStage] = useState<"fetch" | "viewer">("fetch");
if (!url) {
return null;
}
const fileUrl = useMemo(() => {
try {
const u = new URL(url);
if (import.meta.env.DEV && u.pathname.startsWith("/anon/")) {
return `${u.pathname}${u.search}`;
}
return url;
} catch {
return url;
}
}, [url]);
useEffect(() => {
let revokedUrl: string | null = null;
const controller = new AbortController();
const timeoutMs = 20000;
setLoaded(false);
setError(null);
setStage("fetch");
setBlobUrl((prev) => {
if (prev) URL.revokeObjectURL(prev);
return null;
});
(async () => {
try {
const resp = (await Promise.race([
fetch(fileUrl, {
credentials: "include",
signal: controller.signal,
}),
new Promise((_, reject) =>
setTimeout(() => {
try {
controller.abort();
} catch {
// ignore
}
reject(new Error("PDF fetch timeout"));
}, timeoutMs)
),
])) as Response;
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}`);
}
const buf = await resp.arrayBuffer();
const head = new Uint8Array(buf.slice(0, 5));
const isPdf =
head.length === 5 &&
head[0] === 0x25 &&
head[1] === 0x50 &&
head[2] === 0x44 &&
head[3] === 0x46 &&
head[4] === 0x2d;
if (!isPdf) {
throw new Error("Response is not a PDF");
}
const blob = new Blob([buf], { type: "application/pdf" });
const nextUrl = URL.createObjectURL(blob);
revokedUrl = nextUrl;
setBlobUrl(nextUrl);
setStage("viewer");
} catch (e) {
if ((e as Error)?.name === "AbortError") return;
setError((e as Error)?.message || "Failed to load PDF");
}
})();
return () => {
controller.abort();
if (revokedUrl) URL.revokeObjectURL(revokedUrl);
};
}, [fileUrl]);
const viewerSrc = useMemo(() => {
if (!blobUrl) return null;
const base = import.meta.env.BASE_URL;
const viewerUrl = `${base}pdfjs/web/viewer.html?file=${encodeURIComponent(blobUrl)}`;
return viewerUrl;
}, [blobUrl]);
return (
<div className="flex flex-col h-full w-full">
<div className="flex-auto overflow-auto bg-gray-100">
<div className="p-4">
<div
className="relative h-[calc(100vh-220px)] min-h-[480px] w-full overflow-auto rounded-md border bg-white"
>
{!!error && (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 p-6 text-sm text-red-600">
<div>{error}</div>
</div>
)}
{!error && !loaded && (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 p-6 text-sm text-muted-foreground">
<div>
{stage === "fetch" ? "Loading..." : "Opening viewer..."}
</div>
</div>
)}
{viewerSrc && (
<iframe
title="PDF Viewer"
src={viewerSrc}
className="h-full w-full border-0"
onLoad={() => {
setLoaded(true);
}}
/>
)}
</div>
</div>
</div>
</div>
);
}