<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>
|