<script lang="ts" setup>
|
import type { ActionItem } from '@jnpf/ui/vxeTable';
|
|
import type { HealthSeverity, TemplateHealthDiagnostic, TemplateHealthReport, TemplateStatus } from '#/api/x/eln/templates';
|
import type { OfficeDocumentFile } from '#/components/GlobalOfficeDocumentModal/types';
|
|
import { computed, ref } from 'vue';
|
|
import { useMessage } from '@jnpf/hooks';
|
import { BasicDrawer, useDrawerInner } from '@jnpf/ui/drawer';
|
import { BasicModal, useModal } from '@jnpf/ui/modal';
|
import { TableAction } from '@jnpf/ui/vxeTable';
|
|
import { Empty, List, ListItem, ListItemMeta } from 'ant-design-vue';
|
import { v4 as uuidv4 } from 'uuid';
|
|
import { confirmRemoveMissing, getTemplateHealth } from '#/api/x/eln/templates';
|
import { onlineUtils } from '#/utils/jnpf';
|
|
import { healthActions } from './templateRules';
|
|
defineOptions({ name: 'ElnTemplateHealthDrawer' });
|
|
const emit = defineEmits(['register', 'reload']);
|
|
interface DrawerContext {
|
bizStatus: TemplateStatus;
|
file: OfficeDocumentFile;
|
templateCode: string;
|
templateId: string;
|
}
|
|
const { createMessage } = useMessage();
|
const context = ref<DrawerContext>();
|
const report = ref<TemplateHealthReport>();
|
const pendingDiagnostic = ref<TemplateHealthDiagnostic>();
|
const reason = ref('');
|
const operationCode = ref('');
|
let healthRequestGeneration = 0;
|
const [registerDrawer, { changeLoading }] = useDrawerInner(init);
|
const [registerRemoveModal, { closeModal: closeRemoveModal, openModal: openRemoveModal, setModalProps: setRemoveModalProps }] = useModal();
|
|
const severityOrder: HealthSeverity[] = ['ERROR', 'WARNING', 'INFO'];
|
const severityLabels: Record<HealthSeverity, string> = { ERROR: '错误', INFO: '提示', WARNING: '警告' };
|
const severityColors: Record<HealthSeverity, string> = { ERROR: 'error', INFO: 'blue', WARNING: 'warning' };
|
const drawerTitle = computed(() => (context.value?.templateCode ? `模板体检 - ${context.value.templateCode}` : '模板体检'));
|
const groupedDiagnostics = computed(() =>
|
severityOrder
|
.map((severity) => ({ diagnostics: (report.value?.diagnostics || []).filter((item) => item.severity === severity), severity }))
|
.filter((group) => group.diagnostics.length > 0),
|
);
|
|
async function init(data: DrawerContext) {
|
context.value = data;
|
report.value = undefined;
|
await loadHealth();
|
}
|
|
async function loadHealth() {
|
const current = context.value;
|
if (!current) return;
|
const { templateId } = current;
|
const requestGeneration = ++healthRequestGeneration;
|
changeLoading(true);
|
try {
|
const response = await getTemplateHealth(templateId);
|
const nextReport = response.data;
|
if (requestGeneration !== healthRequestGeneration || context.value?.templateId !== templateId) return;
|
report.value = nextReport;
|
} finally {
|
if (requestGeneration === healthRequestGeneration && context.value?.templateId === templateId) changeLoading(false);
|
}
|
}
|
|
function getDiagnosticActions(diagnostic: TemplateHealthDiagnostic): ActionItem[] {
|
return healthActions(diagnostic).flatMap((action): ActionItem[] => {
|
if (context.value?.bizStatus === 'PUBLISHED' && action !== 'locate') return [];
|
if (action === 'repair') return [{ label: '重新标注', onClick: () => openRepair(diagnostic) }];
|
if (action === 'locate') return [{ label: '定位', onClick: () => openDocument(diagnostic.controlTag) }];
|
if (action === 'confirmRemove') {
|
const evidenceIncomplete = diagnostic.expectedVersion == null || !Array.isArray(diagnostic.dependencies);
|
return [
|
{
|
color: 'error',
|
disabled: evidenceIncomplete,
|
label: '确认删除',
|
onClick: () => requestRemove(diagnostic),
|
tooltip: evidenceIncomplete ? '体检数据缺少绑定版本或依赖信息,请刷新后重试' : undefined,
|
},
|
];
|
}
|
return [];
|
});
|
}
|
|
function openDocument(locateControlTag?: string) {
|
const current = context.value;
|
if (!current) return;
|
onlineUtils.openOfficeDocument({
|
bizDataId: current.templateId,
|
bizModule: 'eln.template',
|
bizScene: 'eln.template.annotate',
|
file: current.file,
|
mode: 'view',
|
onSave: () => {
|
if (context.value?.templateId === current.templateId) return loadHealth();
|
},
|
...(locateControlTag ? { locateControlTag } : {}),
|
});
|
}
|
|
function openRepair(diagnostic: TemplateHealthDiagnostic) {
|
const current = context.value;
|
if (!diagnostic.bindingId || current?.bizStatus !== 'DRAFT') return;
|
onlineUtils.openOfficeDocument({
|
bizDataId: current.templateId,
|
bizModule: 'eln.template',
|
bizScene: 'eln.template.annotate',
|
file: current.file,
|
mode: 'edit',
|
onSave: () => {
|
if (context.value?.templateId === current.templateId) return loadHealth();
|
},
|
repairBindingId: diagnostic.bindingId,
|
});
|
}
|
|
function requestRemove(diagnostic: TemplateHealthDiagnostic) {
|
pendingDiagnostic.value = diagnostic;
|
reason.value = '';
|
operationCode.value = `remove-missing-${uuidv4()}`;
|
openRemoveModal(true);
|
}
|
|
async function confirmRemove() {
|
if (context.value?.bizStatus !== 'DRAFT') return;
|
const diagnostic = pendingDiagnostic.value;
|
const actualReason = reason.value.trim();
|
if (!diagnostic?.bindingId) return;
|
if (diagnostic.expectedVersion == null || !Array.isArray(diagnostic.dependencies)) {
|
createMessage.warning('体检数据缺少绑定版本或依赖信息,请刷新后重试');
|
return;
|
}
|
if (!actualReason) {
|
createMessage.warning('请输入确认删除原因');
|
return;
|
}
|
setRemoveModalProps({ confirmLoading: true });
|
try {
|
await confirmRemoveMissing(diagnostic.bindingId, {
|
clientReportedMissing: true,
|
expectedVersion: diagnostic.expectedVersion,
|
operationCode: operationCode.value,
|
reason: actualReason,
|
});
|
createMessage.success('缺失控件绑定已删除');
|
closeRemoveModal();
|
await loadHealth();
|
emit('reload');
|
} finally {
|
setRemoveModalProps({ confirmLoading: false });
|
}
|
}
|
</script>
|
|
<template>
|
<BasicDrawer
|
v-bind="$attrs"
|
@register="registerDrawer"
|
class="full-drawer"
|
destroy-on-close
|
keyboard
|
:title="drawerTitle"
|
width="min(760px, calc(100vw - 16px))">
|
<div class="health-content">
|
<div v-if="report && !report.diagnostics.length" class="health-empty">
|
<Empty description="未发现模板绑定异常" />
|
</div>
|
<section v-for="group in groupedDiagnostics" :key="group.severity" class="health-group">
|
<h3>
|
<a-tag :color="severityColors[group.severity]">{{ severityLabels[group.severity] }}</a-tag>
|
{{ group.diagnostics.length }} 项
|
</h3>
|
<List :data-source="group.diagnostics" item-layout="horizontal">
|
<template #renderItem="{ item }">
|
<ListItem>
|
<ListItemMeta :description="item.controlTag || item.partName || undefined">
|
<template #title>{{ item.message }}</template>
|
</ListItemMeta>
|
<template #actions>
|
<TableAction :actions="getDiagnosticActions(item)" />
|
</template>
|
</ListItem>
|
</template>
|
</List>
|
</section>
|
</div>
|
|
<BasicModal @register="registerRemoveModal" destroy-on-close title="确认删除缺失控件绑定" @ok="confirmRemove">
|
<a-alert message="系统会再次读取 DOCX 并校验字段引用和公式依赖;校验未通过时不会删除绑定。" show-icon type="warning" />
|
<div class="dependency-section">
|
<h4>引用/公式依赖</h4>
|
<a-alert v-if="!Array.isArray(pendingDiagnostic?.dependencies)" message="依赖信息尚未返回,当前不能确认删除,请刷新体检结果。" show-icon type="error" />
|
<Empty v-else-if="!pendingDiagnostic?.dependencies.length" description="未发现字段引用或公式依赖" :image="null" />
|
<List v-else size="small" :data-source="pendingDiagnostic.dependencies">
|
<template #renderItem="{ item }">
|
<ListItem>{{ item.label || item.code || '依赖项' }}:{{ item.message }}</ListItem>
|
</template>
|
</List>
|
</div>
|
<a-form-item 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>
|
.health-content {
|
height: 100%;
|
}
|
|
.health-empty {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
height: 100%;
|
padding: 24px;
|
}
|
|
.health-group {
|
padding: 16px 20px 0;
|
|
h3 {
|
display: flex;
|
gap: 8px;
|
align-items: center;
|
margin: 0 0 8px;
|
font-size: 14px;
|
font-weight: 600;
|
}
|
}
|
|
.dependency-section {
|
margin: 16px 0;
|
|
h4 {
|
margin-bottom: 8px;
|
}
|
}
|
</style>
|