需求修改

dev-tmp1
huwei 2026-08-19 09:13:49 +08:00
parent 69c5a16b59
commit e45c4d2404
3 changed files with 327 additions and 0 deletions

View File

@ -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>
);
}

View File

@ -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;
}
}

View File

@ -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;
}
}