feat(frontend): 接入 pdf.js 预览并调整产物预览逻辑
This commit is contained in:
parent
4bd36b4603
commit
b55072b0eb
|
|
@ -81,6 +81,7 @@
|
||||||
"nextra-theme-docs": "^4.6.1",
|
"nextra-theme-docs": "^4.6.1",
|
||||||
"nuxt-og-image": "^5.1.13",
|
"nuxt-og-image": "^5.1.13",
|
||||||
"ogl": "^1.0.11",
|
"ogl": "^1.0.11",
|
||||||
|
"pdfjs-dist": "^5.6.205",
|
||||||
"pptx-preview": "^1.0.7",
|
"pptx-preview": "^1.0.7",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,9 @@ importers:
|
||||||
ogl:
|
ogl:
|
||||||
specifier: ^1.0.11
|
specifier: ^1.0.11
|
||||||
version: 1.0.11
|
version: 1.0.11
|
||||||
|
pdfjs-dist:
|
||||||
|
specifier: ^5.6.205
|
||||||
|
version: 5.6.205
|
||||||
pptx-preview:
|
pptx-preview:
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7
|
version: 1.0.7
|
||||||
|
|
@ -963,6 +966,77 @@ packages:
|
||||||
'@mermaid-js/parser@0.6.3':
|
'@mermaid-js/parser@0.6.3':
|
||||||
resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==}
|
resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==}
|
||||||
|
|
||||||
|
'@napi-rs/canvas-android-arm64@0.1.97':
|
||||||
|
resolution: {integrity: sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-arm64@0.1.97':
|
||||||
|
resolution: {integrity: sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-x64@0.1.97':
|
||||||
|
resolution: {integrity: sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.97':
|
||||||
|
resolution: {integrity: sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.97':
|
||||||
|
resolution: {integrity: sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-musl@0.1.97':
|
||||||
|
resolution: {integrity: sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.97':
|
||||||
|
resolution: {integrity: sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-gnu@0.1.97':
|
||||||
|
resolution: {integrity: sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-musl@0.1.97':
|
||||||
|
resolution: {integrity: sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-arm64-msvc@0.1.97':
|
||||||
|
resolution: {integrity: sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc@0.1.97':
|
||||||
|
resolution: {integrity: sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@napi-rs/canvas@0.1.97':
|
||||||
|
resolution: {integrity: sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
'@napi-rs/simple-git-android-arm-eabi@0.1.22':
|
'@napi-rs/simple-git-android-arm-eabi@0.1.22':
|
||||||
resolution: {integrity: sha512-JQZdnDNm8o43A5GOzwN/0Tz3CDBQtBUNqzVwEopm32uayjdjxev1Csp1JeaqF3v9djLDIvsSE39ecsN2LhCKKQ==}
|
resolution: {integrity: sha512-JQZdnDNm8o43A5GOzwN/0Tz3CDBQtBUNqzVwEopm32uayjdjxev1Csp1JeaqF3v9djLDIvsSE39ecsN2LhCKKQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
|
@ -4605,6 +4679,9 @@ packages:
|
||||||
node-mock-http@1.0.4:
|
node-mock-http@1.0.4:
|
||||||
resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==}
|
resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==}
|
||||||
|
|
||||||
|
node-readable-to-web-readable-stream@0.4.2:
|
||||||
|
resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==}
|
||||||
|
|
||||||
normalize-path@3.0.0:
|
normalize-path@3.0.0:
|
||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
@ -4789,6 +4866,10 @@ packages:
|
||||||
pathe@2.0.3:
|
pathe@2.0.3:
|
||||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
|
||||||
|
pdfjs-dist@5.6.205:
|
||||||
|
resolution: {integrity: sha512-tlUj+2IDa7G1SbvBNN74UHRLJybZDWYom+k6p5KIZl7huBvsA4APi6mKL+zCxd3tLjN5hOOEE9Tv7VdzO88pfg==}
|
||||||
|
engines: {node: '>=20.19.0 || >=22.13.0 || >=24'}
|
||||||
|
|
||||||
perfect-debounce@2.1.0:
|
perfect-debounce@2.1.0:
|
||||||
resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==}
|
resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==}
|
||||||
|
|
||||||
|
|
@ -6751,6 +6832,54 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
langium: 3.3.1
|
langium: 3.3.1
|
||||||
|
|
||||||
|
'@napi-rs/canvas-android-arm64@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-arm64@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-x64@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-musl@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-gnu@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-musl@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-arm64-msvc@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc@0.1.97':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas@0.1.97':
|
||||||
|
optionalDependencies:
|
||||||
|
'@napi-rs/canvas-android-arm64': 0.1.97
|
||||||
|
'@napi-rs/canvas-darwin-arm64': 0.1.97
|
||||||
|
'@napi-rs/canvas-darwin-x64': 0.1.97
|
||||||
|
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.97
|
||||||
|
'@napi-rs/canvas-linux-arm64-gnu': 0.1.97
|
||||||
|
'@napi-rs/canvas-linux-arm64-musl': 0.1.97
|
||||||
|
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.97
|
||||||
|
'@napi-rs/canvas-linux-x64-gnu': 0.1.97
|
||||||
|
'@napi-rs/canvas-linux-x64-musl': 0.1.97
|
||||||
|
'@napi-rs/canvas-win32-arm64-msvc': 0.1.97
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc': 0.1.97
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/simple-git-android-arm-eabi@0.1.22':
|
'@napi-rs/simple-git-android-arm-eabi@0.1.22':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
@ -10952,6 +11081,9 @@ snapshots:
|
||||||
|
|
||||||
node-mock-http@1.0.4: {}
|
node-mock-http@1.0.4: {}
|
||||||
|
|
||||||
|
node-readable-to-web-readable-stream@0.4.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
npm-run-path@5.3.0:
|
npm-run-path@5.3.0:
|
||||||
|
|
@ -11205,6 +11337,11 @@ snapshots:
|
||||||
|
|
||||||
pathe@2.0.3: {}
|
pathe@2.0.3: {}
|
||||||
|
|
||||||
|
pdfjs-dist@5.6.205:
|
||||||
|
optionalDependencies:
|
||||||
|
'@napi-rs/canvas': 0.1.97
|
||||||
|
node-readable-to-web-readable-stream: 0.4.2
|
||||||
|
|
||||||
perfect-debounce@2.1.0: {}
|
perfect-debounce@2.1.0: {}
|
||||||
|
|
||||||
performance-now@2.1.0:
|
performance-now@2.1.0:
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ export function ArtifactFileDetail({
|
||||||
}, [filepath]);
|
}, [filepath]);
|
||||||
const artifactViewerSrcDoc = useMemo(() => {
|
const artifactViewerSrcDoc = useMemo(() => {
|
||||||
if (!artifactUrl) {
|
if (!artifactUrl) {
|
||||||
return "";
|
return undefined;
|
||||||
}
|
}
|
||||||
return buildArtifactViewerSrcDoc({
|
return buildArtifactViewerSrcDoc({
|
||||||
artifactUrl,
|
artifactUrl,
|
||||||
|
|
@ -120,6 +120,11 @@ export function ArtifactFileDetail({
|
||||||
kind: artifactPreviewKind,
|
kind: artifactPreviewKind,
|
||||||
});
|
});
|
||||||
}, [artifactUrl, fileName, artifactPreviewKind]);
|
}, [artifactUrl, fileName, artifactPreviewKind]);
|
||||||
|
// Native PDF iframe rendering is intentionally disabled; PDFs are rendered via pdf.js.
|
||||||
|
const artifactViewerSrc = useMemo(() => {
|
||||||
|
return undefined;
|
||||||
|
}, []);
|
||||||
|
const artifactViewerSandbox = "allow-same-origin allow-scripts allow-downloads";
|
||||||
const { content } = useArtifactContent({
|
const { content } = useArtifactContent({
|
||||||
threadId,
|
threadId,
|
||||||
filepath: filepathFromProps,
|
filepath: filepathFromProps,
|
||||||
|
|
@ -608,7 +613,13 @@ export function ArtifactFileDetail({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isCodeFile && (
|
{!isCodeFile && (
|
||||||
isOfficePreviewKind(artifactPreviewKind) ? (
|
artifactPreviewKind === "pdf" ? (
|
||||||
|
<ArtifactPdfPreview
|
||||||
|
className="h-full mb-[207px]"
|
||||||
|
artifactUrl={artifactUrl}
|
||||||
|
fileName={fileName}
|
||||||
|
/>
|
||||||
|
) : isOfficePreviewKind(artifactPreviewKind) ? (
|
||||||
<ArtifactOfficePreview
|
<ArtifactOfficePreview
|
||||||
className="h-full mb-[207px]"
|
className="h-full mb-[207px]"
|
||||||
kind={artifactPreviewKind}
|
kind={artifactPreviewKind}
|
||||||
|
|
@ -619,8 +630,9 @@ export function ArtifactFileDetail({
|
||||||
<PreviewIframe
|
<PreviewIframe
|
||||||
className="size-full border-0"
|
className="size-full border-0"
|
||||||
containerClassName="h-full mb-[207px]"
|
containerClassName="h-full mb-[207px]"
|
||||||
|
src={artifactViewerSrc}
|
||||||
srcDoc={artifactViewerSrcDoc}
|
srcDoc={artifactViewerSrcDoc}
|
||||||
sandbox="allow-same-origin allow-scripts allow-downloads"
|
sandbox={artifactViewerSandbox}
|
||||||
title={`Artifact preview: ${fileName}`}
|
title={`Artifact preview: ${fileName}`}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
@ -841,6 +853,144 @@ function PreviewIframe({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ArtifactPdfPreview({
|
||||||
|
className,
|
||||||
|
artifactUrl,
|
||||||
|
fileName,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
artifactUrl: string;
|
||||||
|
fileName: string;
|
||||||
|
}) {
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [pageCount, setPageCount] = useState(0);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let disposed = false;
|
||||||
|
|
||||||
|
async function renderPdf() {
|
||||||
|
if (!artifactUrl || !containerRef.current) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setPageCount(0);
|
||||||
|
containerRef.current.innerHTML = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(artifactUrl);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.arrayBuffer();
|
||||||
|
|
||||||
|
const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
||||||
|
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
|
"pdfjs-dist/legacy/build/pdf.worker.min.mjs",
|
||||||
|
import.meta.url,
|
||||||
|
).toString();
|
||||||
|
|
||||||
|
const loadingTask = pdfjs.getDocument({ data });
|
||||||
|
const pdf = await loadingTask.promise;
|
||||||
|
if (disposed || !containerRef.current) {
|
||||||
|
await loadingTask.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPageCount(pdf.numPages);
|
||||||
|
|
||||||
|
const hostWidth = Math.max(
|
||||||
|
640,
|
||||||
|
Math.min(containerRef.current.clientWidth || 960, 1200),
|
||||||
|
);
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
|
||||||
|
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum += 1) {
|
||||||
|
if (disposed || !containerRef.current) break;
|
||||||
|
|
||||||
|
const page = await pdf.getPage(pageNum);
|
||||||
|
const baseViewport = page.getViewport({ scale: 1 });
|
||||||
|
const scale = (hostWidth - 32) / baseViewport.width;
|
||||||
|
const viewport = page.getViewport({ scale });
|
||||||
|
|
||||||
|
const pageWrapper = document.createElement("div");
|
||||||
|
pageWrapper.className =
|
||||||
|
"mx-auto mb-4 w-fit rounded-md border border-[#e4e7ec] bg-white p-2 shadow-sm";
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.style.width = `${viewport.width}px`;
|
||||||
|
canvas.style.height = `${viewport.height}px`;
|
||||||
|
canvas.width = Math.floor(viewport.width * dpr);
|
||||||
|
canvas.height = Math.floor(viewport.height * dpr);
|
||||||
|
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
if (!context) continue;
|
||||||
|
context.scale(dpr, dpr);
|
||||||
|
|
||||||
|
pageWrapper.appendChild(canvas);
|
||||||
|
containerRef.current.appendChild(pageWrapper);
|
||||||
|
|
||||||
|
const renderTask = page.render({
|
||||||
|
canvas,
|
||||||
|
canvasContext: context,
|
||||||
|
viewport,
|
||||||
|
});
|
||||||
|
await renderTask.promise;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to render pdf preview:", err);
|
||||||
|
if (!disposed) {
|
||||||
|
setError("无法预览该 PDF 文件,请下载后查看。");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!disposed) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void renderPdf();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposed = true;
|
||||||
|
};
|
||||||
|
}, [artifactUrl]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className={cn("relative overflow-auto bg-[#f8f9fb] p-4", className)}>
|
||||||
|
<div className="mx-auto grid max-w-xl gap-3 rounded-md border border-[#e4e7ec] bg-white p-5 text-center">
|
||||||
|
<p className="text-sm font-medium break-all">{fileName}</p>
|
||||||
|
<p className="text-muted-foreground text-sm">{error}</p>
|
||||||
|
<a
|
||||||
|
className="text-sm font-semibold text-blue-600 hover:underline"
|
||||||
|
href={artifactUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
在新标签页打开
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("relative overflow-auto bg-[#f8f9fb] p-4", className)}>
|
||||||
|
<div className="mb-3 text-center text-xs text-[#667085]">
|
||||||
|
{pageCount > 0 ? `${fileName} · ${pageCount} page(s)` : fileName}
|
||||||
|
</div>
|
||||||
|
<div ref={containerRef} />
|
||||||
|
{isLoading && (
|
||||||
|
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/70">
|
||||||
|
<LoaderIcon className="text-muted-foreground size-5 animate-spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ArtifactOfficePreview({
|
function ArtifactOfficePreview({
|
||||||
className,
|
className,
|
||||||
kind,
|
kind,
|
||||||
|
|
@ -858,7 +1008,6 @@ function ArtifactOfficePreview({
|
||||||
const [sheetNames, setSheetNames] = useState<string[]>([]);
|
const [sheetNames, setSheetNames] = useState<string[]>([]);
|
||||||
const [activeSheet, setActiveSheet] = useState<string>("");
|
const [activeSheet, setActiveSheet] = useState<string>("");
|
||||||
const docxContainerRef = useRef<HTMLDivElement | null>(null);
|
const docxContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const pptxContainerRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const workbookRef = useRef<XLSX.WorkBook | null>(null);
|
const workbookRef = useRef<XLSX.WorkBook | null>(null);
|
||||||
|
|
||||||
const canRenderDocx = kind === "docx";
|
const canRenderDocx = kind === "docx";
|
||||||
|
|
@ -972,54 +1121,12 @@ function ArtifactOfficePreview({
|
||||||
}
|
}
|
||||||
}, [activeSheet, canRenderXlsx]);
|
}, [activeSheet, canRenderXlsx]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let disposed = false;
|
if (!canRenderPptx) {
|
||||||
|
|
||||||
type PptxPreviewModule = {
|
|
||||||
init: (
|
|
||||||
container: HTMLElement,
|
|
||||||
options: { width: number; height: number },
|
|
||||||
) => {
|
|
||||||
preview: (buffer: ArrayBuffer) => Promise<void> | void;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
async function renderPptx() {
|
|
||||||
if (!canRenderPptx || !artifactUrl || !pptxContainerRef.current) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const response = await fetch(artifactUrl);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
const bytes = await response.arrayBuffer();
|
|
||||||
const pptxModule = (await import("pptx-preview")) as unknown as PptxPreviewModule;
|
|
||||||
if (disposed || !pptxContainerRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const container = pptxContainerRef.current;
|
|
||||||
container.innerHTML = "";
|
|
||||||
const previewer = pptxModule.init(container, { width: 960, height: 540 });
|
|
||||||
await Promise.resolve(previewer.preview(bytes));
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to render pptx preview:", err);
|
|
||||||
if (!disposed) {
|
|
||||||
setError("无法预览该 PPT 文件。");
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (!disposed) {
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
setError("请下载ppt文件以获得最佳效果");
|
||||||
}
|
}, [canRenderPptx]);
|
||||||
}
|
|
||||||
|
|
||||||
void renderPptx();
|
|
||||||
return () => {
|
|
||||||
disposed = true;
|
|
||||||
};
|
|
||||||
}, [artifactUrl, canRenderPptx]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("relative h-full overflow-hidden bg-white", className)}>
|
<div className={cn("relative h-full overflow-hidden bg-white", className)}>
|
||||||
|
|
@ -1056,12 +1163,6 @@ function ArtifactOfficePreview({
|
||||||
dangerouslySetInnerHTML={{ __html: xlsxHtml }}
|
dangerouslySetInnerHTML={{ __html: xlsxHtml }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canRenderPptx && (
|
|
||||||
<div
|
|
||||||
ref={pptxContainerRef}
|
|
||||||
className="pptx-preview-wrap mx-auto w-full overflow-auto"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|
@ -1099,7 +1200,7 @@ function ArtifactPreviewFallback({
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
在新标签页打开
|
点击下载
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -1239,7 +1340,11 @@ function buildArtifactViewerSrcDoc({
|
||||||
return `<div class="audio-wrap"><audio class="audio" src="${safeUrl}" controls preload="metadata"></audio></div>`;
|
return `<div class="audio-wrap"><audio class="audio" src="${safeUrl}" controls preload="metadata"></audio></div>`;
|
||||||
}
|
}
|
||||||
if (kind === "pdf") {
|
if (kind === "pdf") {
|
||||||
return `<iframe class="preview frame" src="${safeUrl}#view=FitH"></iframe>`;
|
return `<div class="fallback">
|
||||||
|
<p class="title">${safeName}</p>
|
||||||
|
<p class="desc">PDF preview is temporarily disabled. Please download the file to view it.</p>
|
||||||
|
<a class="link" href="${safeUrl}" target="_blank" rel="noopener noreferrer">Open in new tab</a>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
if (kind === "html") {
|
if (kind === "html") {
|
||||||
return `<iframe class="preview frame" src="${safeUrl}" sandbox="allow-scripts allow-forms allow-modals allow-popups allow-downloads"></iframe>`;
|
return `<iframe class="preview frame" src="${safeUrl}" sandbox="allow-scripts allow-forms allow-modals allow-popups allow-downloads"></iframe>`;
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ async function waitAfterCardClick(page: Page, kind: string): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (kind === "pptx") {
|
if (kind === "pptx") {
|
||||||
await expect(page.locator(".pptx-preview-wrap").first()).toBeVisible({
|
await expect(page.getByText("请下载ppt文件以获得最佳效果").first()).toBeVisible({
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue