需求修改
parent
69c5a16b59
commit
e45c4d2404
|
|
@ -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 <Empty description={emptyText} image={Empty.PRESENTED_IMAGE_SIMPLE} />;
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
return (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="不支持的文件类型"
|
||||
description="当前仅支持 Word(.docx) 和 PDF(.pdf) 格式预览。"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Alert type="error" showIcon message={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`office-preview-body office-preview-${type}`} style={style}>
|
||||
{type === "docx" && (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="office-preview-docxContainer"
|
||||
style={{ display: ready ? "block" : "none" }}
|
||||
/>
|
||||
)}
|
||||
{type === "pdf" && (
|
||||
<iframe
|
||||
className="office-preview-pdfFrame"
|
||||
src={resolvedUrl}
|
||||
title={title}
|
||||
frameBorder="0"
|
||||
/>
|
||||
)}
|
||||
{loading && (
|
||||
<div className="office-preview-loading">
|
||||
<div className="office-preview-loading-text">文档加载中...</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const body = (
|
||||
<div className={`office-preview ${className}`}>{renderContent()}</div>
|
||||
);
|
||||
|
||||
if (mode === "page") {
|
||||
return body;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
width={width}
|
||||
footer={null}
|
||||
destroyOnHidden
|
||||
centered={false}
|
||||
className="office-preview-modal"
|
||||
>
|
||||
{body}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
.office-preview {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.office-preview-body {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.office-preview-docxContainer {
|
||||
/* 固定占弹窗 90% 视口高度,确保有足够可视区域同时保留滚动条 */
|
||||
height: 90vh;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.office-preview-pdfFrame {
|
||||
height: 90vh;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 4px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.office-preview-loading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.office-preview-loading-text {
|
||||
color: #1677ff;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.office-preview-modal {
|
||||
/* 弹窗顶部紧贴,让中间文档区域最大化 */
|
||||
.micro-temp-modal {
|
||||
top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.micro-temp-modal-content {
|
||||
/* 弹窗内容占满视口高度,由正文容器再以 90vh 显示 */
|
||||
height: 75vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.micro-temp-modal-header {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.micro-temp-modal-body {
|
||||
flex: 1;
|
||||
/* 覆盖 src/pages/Container/index.less 中全局 .micro-temp-modal-body { max-height: 66vh } 的限制,
|
||||
让文档预览区域能占满更大可视高度 */
|
||||
max-height: none !important;
|
||||
overflow: hidden;
|
||||
padding: 4px 8px 8px 8px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import React from "react";
|
||||
import { Empty } from "antd";
|
||||
|
||||
/**
|
||||
* 通用错误边界(React Error Boundary)
|
||||
*
|
||||
* <p>2026-08-18 新增:用于兜底 WpsEditor 等第三方 SDK 在运行时抛出的
|
||||
* "Cannot read properties of undefined (reading 'officeType')" 等未捕获异常。
|
||||
* 第三方 SDK(如 WebOfficeSDK)在内部事件回调中可能因后端数据缺失而抛
|
||||
* TypeError,本组件在 React 渲染树顶层将其捕获,渲染降级 UI,
|
||||
* 避免 React DevTools runtime error overlay 弹出红屏。</p>
|
||||
*/
|
||||
export default class ErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
// 打印详细错误便于排查,但不让 React DevTools 弹出红屏
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("[ErrorBoundary] 捕获到子组件错误:", error, info);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: 200,
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<Empty
|
||||
description={this.props.message || "文档加载失败或文件不存在"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue