报错复现:点击打开弹窗,修改后保存,然后再次点击编辑打开,然后报错弹窗
@electroluxcode

// 业务代码 index.jsx
import { Button, Form, Input, message, Modal, Popconfirm, Radio, Select } from 'antd';
import OfficeEditor from '@/components/officeEditor';
const AuditExecutePlan = () => {
const [editorVisible, setEditorVisible] = useState(false);
const [auditPlanFile, setAuditPlanFile] = useState(null); // 审计实施方案文件
const [documentFile, setDocumentFile] = useState(null);
const documentFileCacheRef = useRef(null);
const [type, setType] = useState('edit');
const openEditor = async (fileName) => {
console.log('documentFileCacheRef.current', documentFileCacheRef.current);
// 取消或者保存后本地缓存变更的word文件,防止每次都重新调用接口
if (documentFileCacheRef.current) {
setDocumentFile(documentFileCacheRef.current);
setEditorVisible(true);
return;
}
let res = null;
if (auditPlanFile && auditPlanFile?.id) {
// 拉取上次提交的word文件
res = await downloadFileApi(auditPlanFile.id);
} else {
// 拉取模板文件
res = await getDocumentTemplateFileApi(projectId, documentType);
}
const blob =
res instanceof Blob
? res
: new Blob([res], {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
});
const file = new File([blob], fileName, {
type: blob.type || 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
});
setDocumentFile(file);
setEditorVisible(true);
};
const closeEditor = ({ blob, fileName }) => {
console.log('closeEditor', blob, fileName);
const file = new File([blob], fileName, {
type: blob.type || 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
});
documentFileCacheRef.current = file;
setEditorVisible(false);
};
const onEditOk = async ({ blob, fileName }) => {
console.log('onEditOk', blob, fileName);
const file = new File([blob], fileName, {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
});
documentFileCacheRef.current = file;
const res = await uploadFileApi(file, 'auditExecutePlan', MODULE_KEY1);
setEditorVisible(false);
setAuditPlanFile(res.result);
message.success('保存成功');
};
return(div>
<Form.Item label="审计方案" rules={[{ required: true }]}>
{auditPlanFile && type === 'edit' && (
<div className={styles.planSaved}>
<FileWordOutlined style={{ color: '#1677ff' }} />
<span className={styles.planFileName}>审计实施方案</span>
{type === 'edit' && (
<>
<Button
className={styles.planAction}
type="link"
onClick={() => openEditor(FILE_NAME)}
>
编辑
</Button>
<Popconfirm
title="确认删除审计方案吗?"
okText="确认"
cancelText="取消"
onConfirm={handleDeletePlan}
>
<Button className={styles.planAction} danger type="link">
删除
</Button>
</Popconfirm>
</>
)}
</div>
)}
{!auditPlanFile && type === 'edit' && (
<Button
className={styles.planTrigger}
type="link"
onClick={() => openEditor(FILE_NAME)}
>
在线编辑
</Button>
)}
{type === 'view' && auditPlanFile && (
<div className="attachments">
<span className="filename" onClick={() => onPreviewFile(auditPlanFile)}>
{auditPlanFile?.fileName}
</span>
<span
className="download-icon"
onClick={() => downloadFile(auditPlanFile)}
title="下载"
>
<DownloadOutlined />
</span>
</div>
)}
</Form.Item>
{editorVisible && (
<OfficeEditor
visible={editorVisible}
onCancel={closeEditor}
onOk={onEditOk}
embedded
title="审计实施方案"
defaultFileName="审计实施方案.docx"
fileType={FILE_TYPE.DOCX}
file={documentFile}
/>
)}
<div/>)
}
// 封装的编辑器组件
import { memo, useEffect, useRef, useState } from 'react';
import {
ONLYOFFICE_CONTAINER_CONFIG,
ONLYOFFICE_ID,
OFFICE_XML_EVENT_CONFIG,
DEFAULT_OFFICE_THEME,
type FileType,
} from '../onlyoffice-web-comp/const';
import { OnlyOfficeManager } from '../onlyoffice-web-comp/core/onlyoffice-manager';
import { editorManagerFactory } from '../onlyoffice-web-comp/core/editor-manager';
import type { OfficeTheme } from '../onlyoffice-web-comp/internal/editor/types';
import { Button, Drawer, message, Space } from 'antd';
import { createConnectorDemo, type ConnectorDemo } from './connector-demo';
import { subscribeDemoResourceChange } from './resource-switcher';
type OfficePreviewPageProps = {
title: string;
defaultFileName: string;
fileType: FileType;
accept?: string;
/** public 目录下的默认文件路径,如 /test.xlsx */
initialFileUrl?: string;
/** 嵌入文档页或父容器时使用 h-full */
embedded?: boolean;
newButtonLabel?: string;
visible?: boolean;
onCancel?: ({ blob, fileName }?: { blob: Blob; fileName: string }) => void;
onOk?: ({ blob, fileName }?: { blob: Blob; fileName: string }) => void;
file: File;
};
function LoadingOverlay() {
return (
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/70 backdrop-blur-sm">
<div className="rounded-lg border border-gray-200 bg-white px-4 py-3 text-sm text-gray-700 shadow">
加载中...
</div>
</div>
);
}
const OnlyOfficeHost = memo(function OnlyOfficeHost() {
return (
<div className={`${ONLYOFFICE_CONTAINER_CONFIG.PARENT_CLASS_NAME} absolute inset-0`}>
<div id={ONLYOFFICE_ID} className="absolute inset-0" />
</div>
);
});
export default function OfficeEditor({
title,
defaultFileName,
fileType,
embedded = false,
visible = false,
onCancel,
onOk,
file,
}: OfficePreviewPageProps) {
const managerRef = useRef<OnlyOfficeManager | null>(null);
const connectorRef = useRef<ConnectorDemo | null>(null);
const [loading, setLoading] = useState(false);
const [currentTheme, setCurrentThemeState] = useState<OfficeTheme>(DEFAULT_OFFICE_THEME);
const [officeXmlEventEnabled] = useState<boolean>(OFFICE_XML_EVENT_CONFIG.default.isEnable);
const [officeXmlLimitMb] = useState(
Math.round(OFFICE_XML_EVENT_CONFIG.default.limitBytes / 1024 / 1024),
);
const connectorMessageTimerRef = useRef<number | null>(null);
const [resourceRevision, setResourceRevision] = useState(0);
const replaceConnector = (manager: OnlyOfficeManager) => {
if (connectorRef.current?.isConnected) {
connectorRef.current.disconnect();
}
const connector = createConnectorDemo(() => manager);
connectorRef.current = connector;
return connector;
};
useEffect(
() =>
subscribeDemoResourceChange((state) => {
setLoading(true);
if (connectorRef.current?.isConnected) {
connectorRef.current.disconnect();
}
connectorRef.current = null;
managerRef.current?.destroy();
managerRef.current = null;
editorManagerFactory.destroy(ONLYOFFICE_ID);
setResourceRevision(state.revision);
}),
[defaultFileName],
);
useEffect(() => {
let unsubscribeLoading: (() => void) | undefined;
let disposed = false;
let ownedManager: OnlyOfficeManager | null = null;
const containerId = ONLYOFFICE_ID;
const init = async () => {
editorManagerFactory.destroy(containerId);
const loadSession = editorManagerFactory.beginLoadSession(containerId);
const officeXmlEvent = {
isEnable: officeXmlEventEnabled,
limitBytes: officeXmlLimitMb * 1024 * 1024,
};
let manager: OnlyOfficeManager;
if (file) {
// const file = await fetchPublicFile(initialFileUrl, defaultFileName);
if (disposed) return;
manager = await OnlyOfficeManager.createWithFile(
{
containerId,
fileType,
defaultFileName,
readOnly: false,
theme: currentTheme,
loadSession,
officeXmlEvent,
},
file,
);
} else {
manager = await OnlyOfficeManager.create({
containerId,
fileType,
defaultFileName,
readOnly: false,
theme: currentTheme,
loadSession,
officeXmlEvent,
user: {
id: 'uid',
name: 'demo-user',
},
});
}
if (disposed || !editorManagerFactory.isLoadSessionActive(containerId, loadSession)) {
return;
}
ownedManager = manager;
managerRef.current = manager;
replaceConnector(manager);
setCurrentThemeState(manager.getTheme());
setLoading(false);
unsubscribeLoading = manager.onLoadingChange(({ loading: next }) => {
setLoading(next);
});
};
init().catch((err) => {
if (disposed) return;
message.error('无法加载编辑器组件');
setLoading(false);
console.error('Failed to initialize OnlyOffice:', err);
});
return () => {
disposed = true;
unsubscribeLoading?.();
ownedManager?.destroy();
if (connectorRef.current?.isConnected) {
connectorRef.current.disconnect();
}
connectorRef.current = null;
editorManagerFactory.destroy(containerId);
managerRef.current = null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [resourceRevision, file]);
useEffect(
() => () => {
if (connectorMessageTimerRef.current !== null) {
window.clearTimeout(connectorMessageTimerRef.current);
}
},
[],
);
const exportAsBlobFunc = async () => {
return await managerRef.current?.exportAsBlob();
};
const onClose = async () => {
onCancel?.(await exportAsBlobFunc());
if (connectorRef.current?.isConnected) {
connectorRef.current.disconnect();
}
connectorRef.current = null;
editorManagerFactory.destroy(ONLYOFFICE_ID);
managerRef.current = null;
};
const onConfirm = async () => {
onOk?.(await exportAsBlobFunc());
if (connectorRef.current?.isConnected) {
connectorRef.current.disconnect();
}
connectorRef.current = null;
editorManagerFactory.destroy(ONLYOFFICE_ID);
managerRef.current = null;
};
return (
<Drawer
title={title}
visible={visible}
onClose={onClose}
width={'100%'}
extra={
<Space>
<Button onClick={onClose}>取消</Button>
<Button onClick={onConfirm}>确认</Button>
</Space>
}
>
<div className={`flex flex-col bg-white ${embedded ? 'h-full min-h-0' : 'h-screen'}`}>
<div className="relative min-h-0 flex-1">
<OnlyOfficeHost />
{loading && <LoadingOverlay />}
</div>
</div>
</Drawer>
);
}
报错复现:点击打开弹窗,修改后保存,然后再次点击编辑打开,然后报错弹窗

@electroluxcode
// 业务代码 index.jsx
// 封装的编辑器组件