<script lang="ts" setup>
|
import type { ActionItem, BasicColumn } from '@jnpf/ui/vxeTable';
|
|
import type { LocalTemplateForm } from './templateDetail';
|
|
import type { TemplateListItem, TemplatePageQuery } from '#/api/x/eln/templates';
|
import type { OfficeDocumentFile } from '#/components/GlobalOfficeDocumentModal/types';
|
|
import { computed, nextTick, ref } from 'vue';
|
|
import { useMessage } from '@jnpf/hooks';
|
import { BasicDrawer, useDrawer, useDrawerInner } from '@jnpf/ui/drawer';
|
import { BasicModal, useModal } from '@jnpf/ui/modal';
|
import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
|
|
import {
|
disableTemplate,
|
enableTemplate,
|
getTemplateDetail,
|
getTemplateHealth,
|
getTemplatePage,
|
publishTemplate,
|
switchCurrentTemplate,
|
} from '#/api/x/eln/templates';
|
import GlobalFormModal from '#/components/GlobalFormModal/index.vue';
|
import { onlineUtils } from '#/utils/jnpf';
|
|
import HealthDrawer from './HealthDrawer.vue';
|
import StepSettingsDrawer from './StepSettingsDrawer.vue';
|
import { openTemplateDetailForm } from './templateDetail';
|
import VersionModal from './VersionModal.vue';
|
|
defineOptions({ name: 'ElnTemplateVersionDrawer' });
|
|
const emit = defineEmits(['register', 'reload']);
|
|
const { createMessage } = useMessage();
|
const [registerHealthDrawer, { openDrawer: openHealthDrawer }] = useDrawer();
|
const [registerStepSettingsDrawer, { openDrawer: openStepSettingsDrawer }] = useDrawer();
|
const [registerVersionModal, { openModal: openVersionModal }] = useModal();
|
const templateCode = ref('');
|
const reason = ref('');
|
const reasonAction = ref<'disable' | 'enable' | 'publish' | 'setCurrent'>();
|
const targetVersion = ref<TemplateListItem>();
|
const openedHealth = ref<{ bizStatus: TemplateListItem['bizStatus']; file: OfficeDocumentFile; templateCode: string; templateId: string }>();
|
const localFormRef = ref<LocalTemplateForm>();
|
const drawerTitle = computed(() => (templateCode.value ? `模板版本 - ${templateCode.value}` : '模板版本'));
|
const reasonPrompt = computed(() => {
|
const version = targetVersion.value;
|
if (!version) return '';
|
const prompts = {
|
disable: `确认停用模板 ${version.mubanBianhao} 的 ${version.mubanBanben} 版本?`,
|
enable: `确认启用模板 ${version.mubanBianhao} 的 ${version.mubanBanben} 版本?`,
|
publish: `确认发布模板 ${version.mubanBianhao} 的 ${version.mubanBanben} 版本?`,
|
setCurrent: `确认将 ${version.mubanBanben} 设为模板 ${version.mubanBianhao} 的当前版本?`,
|
};
|
return reasonAction.value ? prompts[reasonAction.value] : '';
|
});
|
const reasonTitle = computed(() => {
|
const titles = { disable: '停用模板版本', enable: '启用模板版本', publish: '发布模板版本', setCurrent: '切换当前模板版本' };
|
return reasonAction.value ? titles[reasonAction.value] : '模板版本操作';
|
});
|
const requiredAnnotationDiagnosticCodes = new Set(['TEST_CONCLUSION_BINDING_REQUIRED', 'TEST_RESULT_BINDING_REQUIRED']);
|
|
const columns: BasicColumn[] = [
|
{ dataIndex: 'mubanBanben', minWidth: 120, title: '业务版本' },
|
{ dataIndex: 'bizStatus', slots: { default: 'bizStatus' }, title: '发布状态', width: 110 },
|
{ dataIndex: 'bizEnabled', slots: { default: 'bizEnabled' }, title: '启用状态', width: 110 },
|
{ dataIndex: 'shengxiaoRiqi', format: 'date|YYYY-MM-DD', title: '生效时间', width: 130 },
|
{ dataIndex: 'shifouDangqian', slots: { default: 'shifouDangqian' }, title: '当前版本', width: 110 },
|
{ align: 'right', dataIndex: 'ziduanShu', slots: { default: 'ziduanShu' }, title: '字段数', width: 90 },
|
{ dataIndex: 'gengxinShijian', format: 'date|YYYY-MM-DD HH:mm:ss', minWidth: 165, title: '更新时间' },
|
{ dataIndex: 'chuangjianShijian', format: 'date|YYYY-MM-DD HH:mm:ss', minWidth: 165, title: '创建时间' },
|
];
|
|
const [registerTable, { reload }] = useVxeTable({
|
actionColumn: { dataIndex: 'action', fixed: 'right', title: '操作', width: 260 },
|
api: fetchVersionPage,
|
columns,
|
immediate: false,
|
pagination: {
|
pageSize: 20,
|
pageSizeOptions: ['20', '50', '100'],
|
showQuickJumper: true,
|
showSizeChanger: true,
|
showTotal: (total) => `共 ${total} 个版本`,
|
},
|
rowKey: 'id',
|
showIndexColumn: false,
|
showTableSetting: false,
|
});
|
const [registerReasonModal, { closeModal: closeReasonModal, openModal: openReasonModal, setModalProps: setReasonModalProps }] = useModal();
|
const [registerDrawer] = useDrawerInner(init);
|
|
async function init(data: { mubanBianhao: string }) {
|
templateCode.value = data.mubanBianhao;
|
reason.value = '';
|
reasonAction.value = undefined;
|
targetVersion.value = undefined;
|
await nextTick();
|
reload({ page: 1, searchInfo: {} });
|
}
|
|
async function fetchVersionPage(raw: Record<string, unknown>) {
|
const currentPage = Math.max(1, Number(raw.currentPage) || 1);
|
const pageSize = Math.max(1, Number(raw.pageSize) || 20);
|
const query: TemplatePageQuery = { currentPage, mubanBianhao: templateCode.value, pageSize, versionScope: 'ALL' };
|
const response = await getTemplatePage(query);
|
return { data: response.data };
|
}
|
|
function getTableActions(version: TemplateListItem): ActionItem[] {
|
const canAnnotate = version.bizStatus === 'DRAFT' && version.bizEnabled === 'enabled';
|
return [
|
{ label: '详情', onClick: () => openVersionDetail(version) },
|
{ label: canAnnotate ? '标注' : '查看', onClick: () => openDocument(version, canAnnotate) },
|
{ label: '体检', onClick: () => showHealth(version) },
|
{ label: '创建新版本', onClick: () => openVersionModal(true, { sourceTemplateId: version.id }) },
|
];
|
}
|
|
function getTableDropdownActions(version: TemplateListItem): ActionItem[] {
|
const actions: ActionItem[] = [{ label: '步骤设置', onClick: () => openStepSettingsDrawer(true, { version }) }];
|
if (version.bizStatus === 'DRAFT') actions.push({ label: '发布', onClick: () => openReason(version, 'publish') });
|
if (version.bizStatus === 'PUBLISHED' && version.bizEnabled === 'enabled' && !version.shifouDangqian) {
|
actions.push({ label: '设为当前', onClick: () => openReason(version, 'setCurrent') });
|
}
|
actions.push(
|
version.bizEnabled === 'enabled'
|
? { color: 'error', label: '停用', onClick: () => openReason(version, 'disable') }
|
: { color: 'success', label: '启用', onClick: () => openReason(version, 'enable') },
|
);
|
return actions;
|
}
|
|
async function openVersionDetail(version: TemplateListItem) {
|
try {
|
await openTemplateDetailForm(localFormRef.value, version, 'modal');
|
} catch (error) {
|
createMessage.error(formatRequestError(error));
|
}
|
}
|
|
async function resolveTemplateFile(version: TemplateListItem) {
|
const response = await getTemplateDetail(version.id);
|
const raw = response.data.mubanFile;
|
const files = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
if (!Array.isArray(files)) throw new Error('模板附件格式无效');
|
const docx = files.filter((file) => file?.fileId && (file.fileExtension?.toLowerCase() === 'docx' || file.name?.toLowerCase().endsWith('.docx')));
|
if (docx.length !== 1) throw new Error('模板必须且只能包含一个 DOCX 附件');
|
return docx[0] as OfficeDocumentFile;
|
}
|
|
async function openDocument(version: TemplateListItem, canAnnotate: boolean) {
|
try {
|
const file = await resolveTemplateFile(version);
|
if (!canAnnotate) {
|
onlineUtils.openOfficeDocument({
|
bizDataId: version.id,
|
bizModule: 'eln.template',
|
bizScene: 'eln.template.annotate',
|
file,
|
mode: 'view',
|
});
|
return;
|
}
|
const validateRequiredSystemFields = () => validateAnnotationCompletion(version.id);
|
onlineUtils.openOfficeDocument({
|
bizDataId: version.id,
|
bizModule: 'eln.template',
|
bizScene: 'eln.template.annotate',
|
file,
|
mode: 'edit',
|
onManualSave: validateRequiredSystemFields,
|
onSave: () => reloadHealth(version.id),
|
});
|
} catch (error) {
|
createMessage.error(formatRequestError(error));
|
}
|
}
|
|
async function validateAnnotationCompletion(templateId: string) {
|
const response = await getTemplateHealth(templateId);
|
const diagnostics = (response.data?.diagnostics || []).filter((item) => item.severity === 'ERROR' && requiredAnnotationDiagnosticCodes.has(item.code));
|
if (diagnostics.length) {
|
throw new Error(`标注未完成:${diagnostics.map((item) => item.message).join(';')}`);
|
}
|
}
|
|
async function showHealth(version: TemplateListItem) {
|
try {
|
const file = await resolveTemplateFile(version);
|
openedHealth.value = { bizStatus: version.bizStatus, file, templateCode: version.mubanBianhao, templateId: version.id };
|
openHealthDrawer(true, openedHealth.value);
|
} catch (error) {
|
createMessage.error(formatRequestError(error));
|
}
|
}
|
|
function reloadHealth(templateId: string) {
|
if (openedHealth.value?.templateId === templateId) openHealthDrawer(true, openedHealth.value);
|
else void getTemplateHealth(templateId).catch((error) => createMessage.error(formatRequestError(error)));
|
}
|
|
function openReason(version: TemplateListItem, action: 'disable' | 'enable' | 'publish' | 'setCurrent') {
|
targetVersion.value = version;
|
reasonAction.value = action;
|
reason.value = '';
|
openReasonModal(true);
|
}
|
|
async function confirmReasonAction() {
|
const version = targetVersion.value;
|
const action = reasonAction.value;
|
const actualReason = reason.value.trim();
|
if (!version || !action || !actualReason) {
|
createMessage.warning('请输入操作原因');
|
return;
|
}
|
setReasonModalProps({ confirmLoading: true });
|
try {
|
switch (action) {
|
case 'disable': {
|
await disableTemplate(version.id, actualReason);
|
createMessage.success('模板版本已停用');
|
break;
|
}
|
case 'enable': {
|
await enableTemplate(version.id, actualReason);
|
createMessage.success('模板版本已启用');
|
break;
|
}
|
case 'publish': {
|
await publishTemplate(version.id, actualReason);
|
createMessage.success('模板版本已发布');
|
break;
|
}
|
case 'setCurrent': {
|
await switchCurrentTemplate(version.mubanBianhao, version.id, actualReason);
|
createMessage.success(`已将 ${version.mubanBanben} 设为当前版本`);
|
break;
|
}
|
}
|
closeReasonModal();
|
reloadAll();
|
} catch (error) {
|
createMessage.error(formatRequestError(error));
|
} finally {
|
setReasonModalProps({ confirmLoading: false });
|
}
|
}
|
|
function reloadAll() {
|
reload();
|
emit('reload');
|
}
|
|
function formatRequestError(error: unknown) {
|
return error instanceof Error && error.message ? error.message : '操作失败,请稍后重试';
|
}
|
</script>
|
|
<template>
|
<BasicDrawer v-bind="$attrs" @register="registerDrawer" class="full-drawer" keyboard :show-footer="false" :title="drawerTitle" width="920px">
|
<div class="version-drawer">
|
<BasicVxeTable @register="registerTable">
|
<template #bizStatus="{ record }">
|
<a-tag :color="record.bizStatus === 'PUBLISHED' ? 'success' : 'processing'">
|
{{ record.bizStatus === 'PUBLISHED' ? '已发布' : '草稿' }}
|
</a-tag>
|
</template>
|
<template #bizEnabled="{ record }">
|
<a-tag :color="record.bizEnabled === 'enabled' ? 'success' : 'default'">
|
{{ record.bizEnabled === 'enabled' ? '启用' : '停用' }}
|
</a-tag>
|
</template>
|
<template #shifouDangqian="{ record }">{{ record.shifouDangqian ? '是' : '否' }}</template>
|
<template #ziduanShu="{ record }">{{ record.ziduanShu == null ? '-' : record.ziduanShu }}</template>
|
<template #action="{ record }">
|
<TableAction :actions="getTableActions(record)" :drop-down-actions="getTableDropdownActions(record)" />
|
</template>
|
</BasicVxeTable>
|
</div>
|
|
<GlobalFormModal ref="localFormRef" :listen-global="false" width="1000px" />
|
<HealthDrawer @register="registerHealthDrawer" @reload="reloadAll" />
|
<StepSettingsDrawer @register="registerStepSettingsDrawer" @saved="reloadAll" />
|
<VersionModal @register="registerVersionModal" @reload="reloadAll" />
|
|
<BasicModal @register="registerReasonModal" :title="reasonTitle" destroy-on-close @ok="confirmReasonAction">
|
<a-alert :message="reasonPrompt" show-icon type="warning" />
|
<a-form-item class="reason-input" label="操作原因" required>
|
<a-textarea v-model:value="reason" :maxlength="500" :rows="4" placeholder="请输入操作原因" show-count />
|
</a-form-item>
|
</BasicModal>
|
</BasicDrawer>
|
</template>
|
|
<style lang="scss" scoped>
|
.version-drawer {
|
height: 100%;
|
padding: 16px 24px;
|
}
|
|
.reason-input {
|
margin-top: 16px;
|
}
|
</style>
|