<script lang="ts" setup>
|
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
|
|
import { Result, Spin } from 'ant-design-vue';
|
|
import { loadDocsApi } from './loadDocsApi';
|
|
const props = defineProps<{
|
/** 后端签发的完整 config,原样透传。前端只追加 events,改任何字段都会让 DS 验签失败 */
|
config: Record<string, any>;
|
}>();
|
|
const emit = defineEmits<{
|
/** 编辑器请求关闭(DS 自带的关闭按钮) */
|
close: [];
|
error: [message: string];
|
/** 文档内容被改动过(DS 的 onDocumentStateChange),关闭时据此决定要不要通知业务侧刷新 */
|
modified: [modified: boolean];
|
ready: [];
|
/** 文档由已修改恢复为未修改;关闭自动保存后,该状态变化来自用户手动保存 */
|
save: [];
|
}>();
|
|
let seed = 0;
|
const containerId = `onlyoffice-editor-${Date.now()}-${++seed}`;
|
|
const loading = ref(true);
|
const errorMessage = ref('');
|
|
let editor: any = null;
|
let documentModified = false;
|
// 递增标记:卸载或重挂后让在途的异步加载失效,避免往已销毁的容器里塞编辑器
|
let mountRequestId = 0;
|
|
async function mount() {
|
const currentRequestId = ++mountRequestId;
|
loading.value = true;
|
errorMessage.value = '';
|
|
try {
|
await loadDocsApi();
|
if (currentRequestId !== mountRequestId) return;
|
await nextTick();
|
if (currentRequestId !== mountRequestId) return;
|
|
const DocsAPI = (window as any).DocsAPI;
|
if (!DocsAPI?.DocEditor) {
|
throw new Error('api.js 已加载但未挂出 DocsAPI,确认 VITE_GLOB_ONLYOFFICE_URL 指向的是 Document Server');
|
}
|
|
// 编辑器一旦构造出来就撤掉自己的遮罩:DS 有它自己的加载动画和错误界面,
|
// 继续盖着只会把它要说的话挡住。2026-08-08 实踩——回源失败时用户只看到我们的
|
// 「正在打开文档...」转圈,DS 在下面显示的真实报错完全看不见,白白多绕了一圈排查。
|
loading.value = false;
|
|
editor = new DocsAPI.DocEditor(containerId, {
|
...props.config,
|
events: {
|
onDocumentReady() {
|
emit('ready');
|
},
|
onDocumentStateChange(event: any) {
|
const nextModified = Boolean(event?.data);
|
if (documentModified && !nextModified) emit('save');
|
documentModified = nextModified;
|
emit('modified', nextModified);
|
},
|
onError(event: any) {
|
const description = event?.data?.errorDescription || `错误码 ${event?.data?.errorCode ?? '未知'}`;
|
loading.value = false;
|
errorMessage.value = description;
|
emit('error', description);
|
},
|
// key 与文档版本一一对应,DS 报此事件说明拿到的是过期版本(多半是 doc_key 策略出了问题)
|
onOutdatedVersion() {
|
errorMessage.value = '文档已有新版本,请关闭后重新打开';
|
emit('error', '文档已有新版本');
|
},
|
onRequestClose() {
|
emit('close');
|
},
|
},
|
});
|
} catch (error: any) {
|
if (currentRequestId !== mountRequestId) return;
|
const message = error?.message || '加载编辑器失败';
|
loading.value = false;
|
errorMessage.value = message;
|
emit('error', message);
|
}
|
}
|
|
/**
|
* 必须显式销毁:DocEditor 会往 document.head 塞样式、与 DS 保持长连接,
|
* 只移除 DOM 不调 destroyEditor 会留下连接与全局副作用(社区版还有约 20 连接的上限)。
|
*/
|
function destroy() {
|
mountRequestId++;
|
try {
|
editor?.destroyEditor?.();
|
} catch (error) {
|
console.error('[OnlyOfficeEditor] destroyEditor failed:', error);
|
}
|
editor = null;
|
documentModified = false;
|
}
|
|
function requestClose() {
|
editor?.requestClose?.();
|
}
|
|
defineExpose({ requestClose });
|
|
onMounted(mount);
|
onUnmounted(destroy);
|
</script>
|
|
<template>
|
<div class="onlyoffice-editor">
|
<div v-if="loading && !errorMessage" class="onlyoffice-editor__mask">
|
<Spin size="large" tip="正在打开文档..." />
|
</div>
|
|
<Result v-if="errorMessage" status="error" title="文档打开失败" :sub-title="errorMessage" class="onlyoffice-editor__error" />
|
|
<!-- DocEditor 会把这个 div 整个替换成 iframe,尺寸由外层容器决定 -->
|
<div v-show="!errorMessage" :id="containerId" class="onlyoffice-editor__container"></div>
|
</div>
|
</template>
|
|
<style lang="scss" scoped>
|
.onlyoffice-editor {
|
position: relative;
|
width: 100%;
|
height: 100%;
|
background: #fff;
|
|
&__container {
|
width: 100%;
|
height: 100%;
|
}
|
|
&__mask {
|
position: absolute;
|
z-index: 1;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
width: 100%;
|
height: 100%;
|
background: #fff;
|
}
|
|
&__error {
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
height: 100%;
|
}
|
}
|
</style>
|