刘光辉
6 小时以前 cf11e63b659afcc9792ab3e395f1b45d844431a8
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
 
import { useMessage } from '@jnpf/hooks';
import { Spin } from 'ant-design-vue';
 
import { getOnlyOfficeConfig, getOnlyOfficeSaveStatus } from '#/api/x/onlyoffice';
import OnlyOfficeEditor from '#/components/OnlyOfficeEditor/src/Editor.vue';
import { onlineUtils } from '#/utils/jnpf';
 
import {
  parseElnRecordFillStateMessage,
  resolveElnRecordOpenParams,
  toElnRecordOfficeConfigQuery,
  unwrapElnRecordEditorConfig,
  type ElnRecordOpenParams,
} from './recordView';
 
defineOptions({ name: 'XElnRecords' });
 
const emit = defineEmits<{ closeCancelled: []; closeReady: []; closeStarted: [] }>();
 
const editorConfig = ref<Record<string, any>>();
const editorRef = ref<null | { requestClose: () => void }>(null);
const errorMessage = ref('');
const loading = ref(true);
const closing = ref(false);
const documentChanged = ref(false);
const fillPendingCount = ref(0);
const { createConfirm, createMessage } = useMessage();
let requestId = 0;
let openParams: ElnRecordOpenParams | undefined;
let closeRequest: null | Promise<boolean | 'pending'> = null;
let releasePending = false;
let releasePollingId = 0;
let confirmHandle: null | { destroy: () => void } = null;
let confirmResolve: null | ((accepted: boolean) => void) = null;
 
const SAVE_STATUS_POLL_INTERVAL_MILLIS = 500;
const SESSION_RELEASE_TIMEOUT_MILLIS = 60_000;
 
async function openRecord() {
  const currentRequestId = ++requestId;
  loading.value = true;
  errorMessage.value = '';
  documentChanged.value = false;
  editorConfig.value = undefined;
 
  try {
    const params = resolveElnRecordOpenParams((name) => onlineUtils.getViewParam(name));
    openParams = params;
    const response = await getOnlyOfficeConfig(toElnRecordOfficeConfigQuery(params));
    if (currentRequestId !== requestId) return;
    editorConfig.value = unwrapElnRecordEditorConfig(response);
  } catch (error: any) {
    if (currentRequestId !== requestId) return;
    errorMessage.value = error?.message || '获取 ELN 记录失败';
  } finally {
    if (currentRequestId === requestId) loading.value = false;
  }
}
 
function settleDiscardConfirm(accepted: boolean) {
  const resolve = confirmResolve;
  confirmResolve = null;
  confirmHandle = null;
  resolve?.(accepted);
}
 
function confirmDiscardDrafts() {
  return new Promise<boolean>((resolve) => {
    confirmResolve = resolve;
    confirmHandle = createConfirm({
      cancelText: '返回填写',
      content: `${fillPendingCount.value} 个字段尚未应用到文档,离开后这些填写内容将丢失。`,
      iconType: 'warning',
      okText: '放弃并离开',
      title: '存在未应用内容',
      onCancel: () => settleDiscardConfirm(false),
      onOk: () => settleDiscardConfirm(true),
    }) as unknown as { destroy: () => void };
  });
}
 
async function waitForSessionRelease() {
  if (!openParams) return true;
  const currentPollingId = ++releasePollingId;
  closing.value = true;
  errorMessage.value = '';
  releasePending = true;
  editorConfig.value = undefined;
  await nextTick();
 
  const deadline = Date.now() + SESSION_RELEASE_TIMEOUT_MILLIS;
  for (;;) {
    if (currentPollingId !== releasePollingId) return false;
    try {
      const response = await getOnlyOfficeSaveStatus(openParams.document.fileId, 'annex');
      if (currentPollingId !== releasePollingId) return false;
      const status = (response as any)?.data ?? response;
      if (status?.sessionStatus === 'IDLE') {
        releasePending = false;
        closing.value = false;
        return true;
      }
      if (status?.sessionStatus === 'SAVE_FAILED') {
        await recoverAfterReleaseFailure('电子记录保存失败,请联系管理员处理后重试');
        return false;
      }
    } catch {
      // OnlyOffice 回调和文件写回仍可能在进行,确认窗口内继续查询。
    }
    const remaining = deadline - Date.now();
    if (remaining <= 0) {
      await recoverAfterReleaseFailure('电子记录保存尚未完成,请稍后重试离开');
      return false;
    }
    await new Promise((resolve) => setTimeout(resolve, Math.min(SAVE_STATUS_POLL_INTERVAL_MILLIS, remaining)));
  }
}
 
async function recoverAfterReleaseFailure(message: string) {
  releasePending = false;
  closing.value = false;
  emit('closeCancelled');
  await openRecord();
  createMessage.error(message);
  if (!editorConfig.value) errorMessage.value = message;
}
 
async function performClose() {
  if (fillPendingCount.value > 0 && !(await confirmDiscardDrafts())) return false;
  fillPendingCount.value = 0;
  if (releasePending) return waitForSessionRelease();
  if (!editorConfig.value || editorConfig.value.editorConfig?.mode !== 'edit' || !documentChanged.value) return true;
  if (!editorRef.value) return false;
  editorRef.value.requestClose();
  return 'pending' as const;
}
 
function requestClose() {
  if (!closeRequest) {
    closeRequest = performClose().finally(() => {
      closeRequest = null;
    });
  }
  return closeRequest;
}
 
function handlePluginState(event: MessageEvent) {
  if (!openParams) return;
  const message = parseElnRecordFillStateMessage(event.data, openParams);
  if (message) fillPendingCount.value = message.pendingCount;
}
 
function handleEditorModified(modified: boolean) {
  if (modified) documentChanged.value = true;
}
 
async function handleEditorClose() {
  if (closing.value) return;
  if (!documentChanged.value) {
    emit('closeReady');
    return;
  }
  emit('closeStarted');
  if (await waitForSessionRelease()) emit('closeReady');
}
 
defineExpose({ requestClose });
 
onMounted(() => {
  window.addEventListener('message', handlePluginState);
  void openRecord();
});
onUnmounted(() => {
  requestId++;
  releasePollingId++;
  window.removeEventListener('message', handlePluginState);
  confirmHandle?.destroy();
  settleDiscardConfirm(false);
});
</script>
 
<template>
  <div class="jnpf-content-wrapper eln-record-view">
    <div v-if="closing" class="eln-record-view__state" data-test="record-closing"><Spin size="large" tip="正在保存并关闭电子记录..." /></div>
    <div v-else-if="loading" class="eln-record-view__state" data-test="record-loading">正在获取编辑器配置...</div>
    <div v-else-if="errorMessage" class="eln-record-view__state" data-test="record-error" role="alert">{{ errorMessage }}</div>
    <div v-else-if="editorConfig" class="eln-record-view__editor">
      <OnlyOfficeEditor
        :key="editorConfig.document.key"
        ref="editorRef"
        :config="editorConfig"
        data-test="record-editor"
        @close="handleEditorClose"
        @modified="handleEditorModified" />
    </div>
  </div>
</template>
 
<style lang="scss" scoped>
.eln-record-view {
  display: flex;
  flex-direction: column;
  min-width: 0;
  height: 100%;
  overflow: hidden;
  background: #fff;
 
  &__editor {
    min-height: 0;
    flex: 1;
  }
 
  &__state {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    width: 100%;
    min-height: 240px;
    padding: 24px;
    color: rgb(0 0 0 / 65%);
    text-align: center;
  }
}
</style>