From e45c4d240441d43c94fdce53e23e00902dbde601 Mon Sep 17 00:00:00 2001 From: huwei <3313749341@qq.com> Date: Wed, 19 Aug 2026 09:13:49 +0800 Subject: [PATCH] =?UTF-8?q?=E9=9C=80=E6=B1=82=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/OfficePreview/index.js | 193 ++++++++++++++++++ src/components/OfficePreview/index.less | 82 ++++++++ .../Container/WpsEditor/ErrorBoundary.jsx | 52 +++++ 3 files changed, 327 insertions(+) create mode 100644 src/components/OfficePreview/index.js create mode 100644 src/components/OfficePreview/index.less create mode 100644 src/pages/Container/WpsEditor/ErrorBoundary.jsx diff --git a/src/components/OfficePreview/index.js b/src/components/OfficePreview/index.js new file mode 100644 index 0000000..e925e82 --- /dev/null +++ b/src/components/OfficePreview/index.js @@ -0,0 +1,193 @@ +import { useEffect, useRef, useState } from "react"; +import { Modal, Empty, Alert } from "antd"; +import { renderAsync } from "docx-preview"; +import "./index.less"; + +const FILE_TYPE_MAP = { + docx: "word", + pdf: "pdf", +}; + +/** 根据 url/类型识别文件类型,仅支持 docx/pdf */ +function resolveFileType(url, type) { + if (type && FILE_TYPE_MAP[type]) return type; + const ext = (url || "").split("?")[0].split(".").pop()?.toLowerCase(); + return FILE_TYPE_MAP[ext] ? ext : ""; +} + +/** 相对地址拼接文件服务前缀 */ +function resolveUrl(raw) { + if (!raw) return ""; + const u = String(raw).trim(); + if (/^https?:\/\//i.test(u)) return u; + const base = window.fileUrl || ""; + return base ? `${base}${u}` : u; +} + +/** 渲染 Word 文档到容器 */ +async function renderDocx(url, container) { + if (!container) throw new Error("预览容器尚未就绪,请稍后重试"); + const res = await fetch(url, { credentials: "same-origin" }); + if (!res.ok) throw new Error(`文档加载失败:${res.status} ${res.statusText}`); + const blob = await res.blob(); + await renderAsync(blob, container, container, { + className: "office-preview-docx", + inWrapper: true, + ignoreWidth: false, + ignoreHeight: false, + ignoreFonts: false, + breakPages: true, + renderHeaders: true, + renderFooters: true, + renderFootnotes: true, + renderEndnotes: true, + }); +} + +/** 文档预览组件:支持 Word(docx)、PDF,支持弹窗/页面两种模式 */ +export default function OfficePreview({ + url, + fileType = "auto", + mode = "modal", + open = false, + onCancel, + title = "文档预览", + width = 1000, + className = "", + style = {}, + emptyText = "暂无文档", +}) { + const containerRef = useRef(null); + const aliveRef = useRef(true); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [ready, setReady] = useState(false); + const resolvedUrl = resolveUrl(url); + const type = resolveFileType(resolvedUrl, fileType); + + useEffect(() => { + aliveRef.current = true; + return () => { + aliveRef.current = false; + }; + }, []); + + useEffect(() => { + if (mode === "modal" && !open) { + setReady(false); + return; + } + if (!resolvedUrl || type !== "docx") return; + + setReady(false); + setError(""); + + let rafId = null; + let cancelled = false; + + const tryRender = () => { + if (cancelled) return; + const node = containerRef.current; + if (!node) { + rafId = requestAnimationFrame(tryRender); + return; + } + setLoading(true); + renderDocx(resolvedUrl, node) + .then(() => { + if (aliveRef.current && !cancelled) { + setReady(true); + } + }) + .catch((e) => { + console.error("OfficePreview 文档渲染失败", e); + if (aliveRef.current && !cancelled) { + setError(e?.message || "文档渲染失败,请稍后重试"); + } + }) + .finally(() => { + if (aliveRef.current && !cancelled) { + setLoading(false); + } + }); + }; + + tryRender(); + + return () => { + cancelled = true; + if (rafId !== null) cancelAnimationFrame(rafId); + const node = containerRef.current; + if (node) node.innerHTML = ""; + }; + }, [resolvedUrl, type, open, mode]); + + const renderContent = () => { + if (!resolvedUrl) { + return ; + } + + if (!type) { + return ( + + ); + } + + if (error) { + return ; + } + + return ( +
+ {type === "docx" && ( +
+ )} + {type === "pdf" && ( +