diff --git a/frontend/src/api/tools/mmsmapping/index.ts b/frontend/src/api/tools/mmsmapping/index.ts index 2dc3341..85093fd 100644 --- a/frontend/src/api/tools/mmsmapping/index.ts +++ b/frontend/src/api/tools/mmsmapping/index.ts @@ -29,3 +29,13 @@ export const getIcdMmsJsonApi = (params: MmsMapping.GetIcdMmsJsonParams) => { headers: { 'Content-Type': 'multipart/form-data' } }) } + +export const buildIndexConfirmDataApi = (params: MmsMapping.IndexCandidateGroup[]) => { + // 关键业务节点:ICD 候选数据需要先转换成前端确认弹窗模型,后续人工确认才能继续生成正式索引配置。 + return http.post('/api/mms-mapping/build-index-confirm-data', params) +} + +export const buildIndexSelectionApi = (params: MmsMapping.BuildIndexSelectionRequest) => { + // 关键业务节点:人工确认完成后,必须把 confirmData 和 confirmedData 一并提交给后端生成正式 request.indexSelection。 + return http.post('/api/mms-mapping/build-index-selection', params) +} diff --git a/frontend/src/api/tools/mmsmapping/interface/index.ts b/frontend/src/api/tools/mmsmapping/interface/index.ts index bd9a195..b73a710 100644 --- a/frontend/src/api/tools/mmsmapping/interface/index.ts +++ b/frontend/src/api/tools/mmsmapping/interface/index.ts @@ -3,6 +3,39 @@ export namespace MmsMapping { icdFile: File } + export interface IndexConfirmTarget { + reportName?: string + dataSetName?: string + reportDesc?: string + availableLnInstValues?: string[] + } + + export interface IndexConfirmLabelItem { + label?: string + required?: boolean + configurableOnce?: boolean + defaultLnInst?: string + commonLnInstValues?: string[] + targets?: IndexConfirmTarget[] + } + + export interface IndexConfirmGroup { + groupKey?: string + groupDesc?: string + labelItems?: IndexConfirmLabelItem[] + } + + export interface ConfirmedIndexLabelItem { + label: string + enabled: boolean + lnInst: string + } + + export interface ConfirmedIndexGroup { + groupKey: string + labelItems: ConfirmedIndexLabelItem[] + } + export interface IndexSelectionBinding { reportName: string dataSetName: string @@ -30,6 +63,11 @@ export namespace MmsMapping { request: GetIcdMmsJsonRequestPayload } + export interface BuildIndexSelectionRequest { + confirmData: IndexConfirmGroup[] + confirmedData: ConfirmedIndexGroup[] + } + export interface IcdDocument { [key: string]: unknown } diff --git a/frontend/src/views/tools/mmsMapping/components/MappingConfirmDialog.vue b/frontend/src/views/tools/mmsMapping/components/MappingConfirmDialog.vue new file mode 100644 index 0000000..44a13e2 --- /dev/null +++ b/frontend/src/views/tools/mmsMapping/components/MappingConfirmDialog.vue @@ -0,0 +1,533 @@ + + + + + diff --git a/frontend/src/views/tools/mmsMapping/index.vue b/frontend/src/views/tools/mmsMapping/index.vue index 742a891..4977f8e 100644 --- a/frontend/src/views/tools/mmsMapping/index.vue +++ b/frontend/src/views/tools/mmsMapping/index.vue @@ -8,7 +8,7 @@ :icd-file-accept="icdFileAccept" :request-status-text="requestStatusText" :request-status-tag-type="requestStatusTagType" - :can-reset="Boolean(selectedIcdFile || responsePayload || indexSelectionJsonText.trim())" + :can-reset="canResetPage" @file-change="handleIcdFileChange" @parse="handleParseIcd" @reset="resetPage" @@ -21,8 +21,12 @@ :can-generate="canGenerate" :json-error="indexSelectionError" :show-generate-button="showGenerateButton" + :show-confirm-button="Boolean(confirmData.length)" + :confirm-button-text="indexSelectionJsonText.trim() ? '重新确认' : '人工确认'" + :can-confirm="!isSubmitting" :has-default-json="Boolean(indexSelectionJsonText.trim())" :empty-description="configEmptyDescription" + @confirm-config="confirmDialogVisible = true" @generate="handleGenerateMapping" /> @@ -40,6 +44,14 @@ @export-mapping="handleExportMapping" /> + + @@ -47,12 +59,18 @@ import { computed, ref } from 'vue' import { ElMessage } from 'element-plus' import type { ResultData } from '@/api/interface' -import { getIcdApi, getIcdMmsJsonApi } from '@/api/tools/mmsmapping' +import { + buildIndexConfirmDataApi, + buildIndexSelectionApi, + getIcdApi, + getIcdMmsJsonApi +} from '@/api/tools/mmsmapping' import type { MmsMapping } from '@/api/tools/mmsmapping/interface' import MappingRequestPanel from './components/MappingRequestPanel.vue' import MappingResultPanel from './components/MappingResultPanel.vue' import MappingConfigPanel from './components/MappingConfigPanel.vue' -import { buildDefaultIndexSelection, formatIndexSelectionJson, parseIndexSelectionJson } from './utils/indexSelection' +import MappingConfirmDialog from './components/MappingConfirmDialog.vue' +import { formatIndexSelectionJson, parseIndexSelectionJson } from './utils/indexSelection' import { createBaseRequestPayload } from './utils/requestPayload' defineOptions({ @@ -71,8 +89,11 @@ const selectedIcdFile = ref(null) const responsePayload = ref(null) const activeResultTab = ref('mapping') const parsedCandidates = ref([]) +const confirmData = ref([]) const indexSelectionJsonText = ref('') +const confirmDialogVisible = ref(false) const isParsing = ref(false) +const isConfirmingSelection = ref(false) const isGenerating = ref(false) const icdFileAccept = '.icd,.cid,.scd,.xml' const problemEmptyText = '当前返回未包含 problems' @@ -86,7 +107,7 @@ function unwrapApiPayload(response: ResultData | T): T { } const getErrorMessage = (error: unknown) => { - if (error instanceof Error) return error.message + if (error instanceof Error && error.message) return error.message return '接口调用失败,请检查后端服务和请求参数' } @@ -100,7 +121,7 @@ const parsedIndexSelectionState = computed(() => { if (!source) { return { value: [] as MmsMapping.IndexSelectionGroup[], - error: parsedCandidates.value.length ? 'request.indexSelection 不能为空' : '' + error: '' } } @@ -117,31 +138,45 @@ const parsedIndexSelectionState = computed(() => { } }) -const isSubmitting = computed(() => isParsing.value || isGenerating.value) +const isSubmitting = computed(() => isParsing.value || isConfirmingSelection.value || isGenerating.value) +const canResetPage = computed(() => + Boolean( + selectedIcdFile.value || responsePayload.value || confirmData.value.length || indexSelectionJsonText.value.trim() + ) +) const indexSelectionError = computed(() => parsedIndexSelectionState.value.error) const canGenerate = computed( () => Boolean(selectedIcdFile.value && indexSelectionJsonText.value.trim() && !indexSelectionError.value) ) -// 关键业务节点:请求配置区只在用户已经选择 ICD 后展示,避免初始态暴露无效的 JSON 编辑区和按钮。 +// 关键业务节点:请求配置区只在用户已经选择 ICD 后展示,避免初始态暴露无效的请求编辑区。 const showConfigPanel = computed(() => Boolean(selectedIcdFile.value)) const showGenerateButton = computed(() => Boolean(selectedIcdFile.value)) +const selectedIcdFileName = computed(() => selectedIcdFile.value?.name || '') + const configEmptyDescription = computed(() => { - if (isParsing.value) return '正在根据当前 ICD 生成 request.indexSelection,请稍候。' - if (selectedIcdFile.value) return '已选择 ICD 文件,请先点击“解析 ICD”生成 request.indexSelection。' + if (isParsing.value) return '正在获取 ICD 候选数据并准备人工确认,请稍候。' + if (isConfirmingSelection.value) return '正在根据人工确认结果生成 request.indexSelection,请稍候。' + if (confirmDialogVisible.value || confirmData.value.length) { + return '请先在弹窗中完成人工确认,确认后会自动回填 request.indexSelection。' + } + if (selectedIcdFile.value) return '已选择 ICD 文件,请先点击“解析 ICD”进入人工确认流程。' return '当前 ICD 暂未生成可编辑的 request.indexSelection。' }) -const selectedIcdFileName = computed(() => selectedIcdFile.value?.name || '') const requestStatusText = computed(() => { if (isParsing.value) return '解析中' + if (isConfirmingSelection.value) return '确认中' if (isGenerating.value) return '生成中' - if (selectedIcdFile.value && indexSelectionJsonText.value.trim()) return '已生成默认配置' + if (confirmDialogVisible.value) return '待人工确认' + if (selectedIcdFile.value && indexSelectionJsonText.value.trim()) return '已确认' + if (selectedIcdFile.value && parsedCandidates.value.length) return '待确认' if (selectedIcdFile.value) return '待解析' return '未选择文件' }) const requestStatusTagType = computed(() => { - if (isParsing.value || isGenerating.value) return 'warning' + if (isParsing.value || isConfirmingSelection.value || isGenerating.value) return 'warning' + if (confirmDialogVisible.value) return 'primary' if (selectedIcdFile.value && indexSelectionJsonText.value.trim()) return 'success' if (selectedIcdFile.value) return 'primary' return 'info' @@ -191,7 +226,7 @@ const resolveResultTab = (payload: MmsMapping.MappingTaskResponse | null): Resul } const stripProblemsFromIcdPayload = (payload: MmsMapping.MappingTaskResponse): MmsMapping.MappingTaskResponse => { - // 关键业务节点:解析 ICD 只消费候选数据和文档结构,不把后端返回的 problems 绑定到结果区。 + // 关键业务节点:解析 ICD 阶段只消费候选数据和文档结构,不把后端问题列表直接绑定到结果区。 const sanitizedPayload = { ...payload } delete sanitizedPayload.problems @@ -202,7 +237,9 @@ const stripProblemsFromIcdPayload = (payload: MmsMapping.MappingTaskResponse): M const resetParsedState = () => { responsePayload.value = null parsedCandidates.value = [] + confirmData.value = [] indexSelectionJsonText.value = '' + confirmDialogVisible.value = false activeResultTab.value = 'mapping' } @@ -217,7 +254,7 @@ const handleIcdFileChange = (event: Event) => { return } - // 关键业务节点:切换 ICD 文件后先只清空旧解析结果,等用户明确点击“解析 ICD”后再请求后台。 + // 关键业务节点:切换 ICD 文件后立即清空旧确认结果和旧请求配置,避免不同文件的索引配置串用。 selectedIcdFile.value = file resetParsedState() input.value = '' @@ -231,9 +268,11 @@ const handleParseIcd = async () => { isParsing.value = true responsePayload.value = null + confirmDialogVisible.value = false + confirmData.value = [] + indexSelectionJsonText.value = '' try { - // 关键业务节点:解析 ICD 时先走 get-icd,拿到当前文件的候选数据后再生成默认 request.indexSelection。 const response = await getIcdApi({ icdFile: selectedIcdFile.value }) @@ -247,14 +286,26 @@ const handleParseIcd = async () => { if (payload.status === 'FAILED') { parsedCandidates.value = [] - indexSelectionJsonText.value = '' ElMessage.error(payload.message || 'ICD 解析失败') return } parsedCandidates.value = candidateGroups - indexSelectionJsonText.value = formatIndexSelectionJson(buildDefaultIndexSelection(candidateGroups)) - ElMessage.success(payload.message || 'ICD 解析完成,已生成 request.indexSelection') + + // 关键业务节点:拿到 ICD 候选结果后必须先走 buildIndexConfirmData,生成人工确认弹窗所需模型。 + const confirmResponse = await buildIndexConfirmDataApi(candidateGroups) + const confirmGroups = unwrapApiPayload(confirmResponse) || [] + + confirmData.value = confirmGroups + + if (!confirmGroups.length) { + indexSelectionJsonText.value = formatIndexSelectionJson([]) + ElMessage.success(payload.message || 'ICD 解析完成,当前没有待确认的索引配置') + return + } + + confirmDialogVisible.value = true + ElMessage.success(payload.message || 'ICD 解析完成,请在弹窗中完成人工确认') } catch (error) { resetParsedState() ElMessage.error(getErrorMessage(error)) @@ -263,6 +314,32 @@ const handleParseIcd = async () => { } } +const handleConfirmIndexSelection = async (confirmedData: MmsMapping.ConfirmedIndexGroup[]) => { + if (!confirmData.value.length) { + ElMessage.warning('当前没有可确认的索引配置') + return + } + + isConfirmingSelection.value = true + + try { + const response = await buildIndexSelectionApi({ + confirmData: confirmData.value, + confirmedData + }) + const indexSelection = unwrapApiPayload(response) || [] + + // 关键业务节点:只有 buildIndexSelection 返回的正式结果才能进入请求配置区,避免前端自行拼装绑定关系。 + indexSelectionJsonText.value = formatIndexSelectionJson(indexSelection) + confirmDialogVisible.value = false + ElMessage.success('人工确认完成,已回填 request.indexSelection') + } catch (error) { + ElMessage.error(getErrorMessage(error)) + } finally { + isConfirmingSelection.value = false + } +} + const handleGenerateMapping = async () => { if (!selectedIcdFile.value) { ElMessage.warning('请先选择 ICD 文件') @@ -270,7 +347,12 @@ const handleGenerateMapping = async () => { } if (!indexSelectionJsonText.value.trim()) { - ElMessage.warning('请先解析 ICD,系统会自动生成 request.indexSelection') + if (confirmData.value.length) { + ElMessage.warning('请先完成人工确认并生成 request.indexSelection') + return + } + + ElMessage.warning('请先解析 ICD,并在弹窗中完成人工确认') return } @@ -290,7 +372,7 @@ const handleGenerateMapping = async () => { responsePayload.value = null try { - // 关键业务节点:正式生成阶段只消费当前编辑区里的 request.indexSelection,确保导出的映射与页面编辑态一致。 + // 关键业务节点:正式生成阶段只消费当前请求配置区里的 request.indexSelection,确保导出的映射与最终确认结果一致。 const response = await getIcdMmsJsonApi({ icdFile: selectedIcdFile.value, request: { diff --git a/frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue b/frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue index e640e12..714d782 100644 --- a/frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue +++ b/frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue @@ -3,18 +3,29 @@

请求配置

-

这里直接编辑 request.indexSelection,默认值会在选择 ICD 文件后自动生成。

+

这里展示并可继续编辑经人工确认后生成的 request.indexSelection。

+
+
+ + {{ confirmButtonText }} + + + 生成映射 +
- - 生成映射 -
@@ -28,7 +39,7 @@ :disabled="isSubmitting" :rows="18" resize="none" - placeholder="ICD 解析完成后,这里会自动填充 request.indexSelection,可继续直接编辑。" + placeholder="人工确认完成后,这里会自动回填 request.indexSelection,仍可继续直接编辑。" @update:model-value="value => emit('update:indexSelectionJson', String(value || ''))" />
@@ -51,12 +62,16 @@ defineProps<{ canGenerate: boolean jsonError: string showGenerateButton: boolean + showConfirmButton: boolean + confirmButtonText: string + canConfirm: boolean hasDefaultJson: boolean emptyDescription: string }>() const emit = defineEmits<{ (event: 'update:indexSelectionJson', value: string): void + (event: 'confirm-config'): void (event: 'generate'): void }>() @@ -86,6 +101,12 @@ const emit = defineEmits<{ gap: 16px; } +.panel-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + .panel-title { margin: 0; font-size: 22px; @@ -156,5 +177,9 @@ const emit = defineEmits<{ flex-direction: column; align-items: flex-start; } + + .panel-actions { + width: 100%; + } } diff --git a/frontend/src/views/tools/mmsmapping/utils/indexSelection.ts b/frontend/src/views/tools/mmsmapping/utils/indexSelection.ts index dc58adf..ab9edac 100644 --- a/frontend/src/views/tools/mmsmapping/utils/indexSelection.ts +++ b/frontend/src/views/tools/mmsmapping/utils/indexSelection.ts @@ -44,32 +44,6 @@ const normalizeBindings = (value: unknown, groupIndex: number): MmsMapping.Index }) } -export const buildDefaultIndexSelection = ( - candidateGroups: MmsMapping.IndexCandidateGroup[] -): MmsMapping.IndexSelectionGroup[] => - candidateGroups - .filter(candidate => candidate.groupKey?.trim()) - .map(candidate => { - const defaultReport = (candidate.reports || []).find( - report => report.reportName?.trim() && report.dataSetName?.trim() - ) - const defaultLnInst = (defaultReport?.availableLnInstValues || []).find(item => item?.trim())?.trim() || '' - - return { - groupKey: candidate.groupKey!.trim(), - groupDesc: candidate.groupDesc?.trim() || '', - bindings: (candidate.templateLabels || []) - .map(label => label?.trim() || '') - .filter(Boolean) - .map(label => ({ - reportName: defaultReport?.reportName?.trim() || '', - dataSetName: defaultReport?.dataSetName?.trim() || '', - label, - lnInst: defaultLnInst - })) - } - }) - export const formatIndexSelectionJson = (value: MmsMapping.IndexSelectionGroup[]) => JSON.stringify(value, null, 4) export const parseIndexSelectionJson = (source: string): MmsMapping.IndexSelectionGroup[] => {