刘光辉
11 小时以前 0dfe84494048ce27ba8449831782128412d3eb13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
<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>