init: 信创文件安全传输系统内外网本地 Docker 部署
包含 s-file 后端、React 前端、MeshSafe 安全检测对接,以及内外网两套 compose/nginx/配置(仅前端 28180/28280 对外发布,其余端口不暴露)。 说明:deploy/config/application-iw.yml 与 application-ow.yml 按维护者 明确要求原样入库,其中含本机 Docker 用的库口令、RabbitMQ 口令与 JWT 密钥;上生产前需替换。
This commit is contained in:
137
frontend/src/components/PDFPreview.tsx
Normal file
137
frontend/src/components/PDFPreview.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user