新建监控功能

This commit is contained in:
2026-04-23 11:09:06 +08:00
parent 7dee9092dc
commit 287de846a6
28 changed files with 6263 additions and 703 deletions

View File

@@ -1,43 +1,383 @@
<template>
<div class="mms-mapping-view">
<div class="mms-mapping-card">
<h1 class="page-title">MMS 映射</h1>
<p class="page-description">当前页面已创建后续可在这里接入 MMS 映射配置映射预览和导入导出能力</p>
<div class="table-box mms-mapping-page">
<div class="mms-mapping-layout">
<div class="left-panel-stack">
<MappingRequestPanel
:selected-icd-file-name="selectedIcdFileName"
:is-submitting="isSubmitting"
:icd-file-accept="icdFileAccept"
:request-status-text="requestStatusText"
:request-status-tag-type="requestStatusTagType"
:can-reset="Boolean(selectedIcdFile || responsePayload || indexSelectionJsonText.trim())"
@file-change="handleIcdFileChange"
@parse="handleParseIcd"
@reset="resetPage"
/>
<MappingConfigPanel
v-if="showConfigPanel"
v-model:index-selection-json="indexSelectionJsonText"
:is-submitting="isSubmitting"
:can-generate="canGenerate"
:json-error="indexSelectionError"
:show-generate-button="showGenerateButton"
:has-default-json="Boolean(indexSelectionJsonText.trim())"
:empty-description="configEmptyDescription"
@generate="handleGenerateMapping"
/>
</div>
<MappingResultPanel
v-model:active-result-tab="activeResultTab"
:response-status-text="responseStatusText"
:response-status-tag-type="responseStatusTagType"
:mapping-meta-text="mappingMetaText"
:mapping-json-preview="mappingJsonPreview"
:problem-tab-label="problemTabLabel"
:problem-list="problemList"
:problem-empty-text="problemEmptyText"
:can-export-mapping="Boolean(mappingJsonPreview)"
@export-mapping="handleExportMapping"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { ResultData } from '@/api/interface'
import { 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 { createBaseRequestPayload } from './utils/requestPayload'
defineOptions({
name: 'MmsMappingView'
})
type ResultTab = 'mapping' | 'problem'
type TagType = 'success' | 'warning' | 'info' | 'primary' | 'danger'
const DEFAULT_REQUEST_FORM: MmsMapping.BaseRequestForm = {
version: '1.0',
author: 'system'
}
const selectedIcdFile = ref<File | null>(null)
const responsePayload = ref<MmsMapping.MappingTaskResponse | null>(null)
const activeResultTab = ref<ResultTab>('mapping')
const parsedCandidates = ref<MmsMapping.IndexCandidateGroup[]>([])
const indexSelectionJsonText = ref('')
const isParsing = ref(false)
const isGenerating = ref(false)
const icdFileAccept = '.icd,.cid,.scd,.xml'
const problemEmptyText = '当前返回未包含 problems'
function unwrapApiPayload<T>(response: ResultData<T> | T): T {
if (response && typeof response === 'object' && 'data' in response) {
return (response as ResultData<T>).data
}
return response as T
}
const getErrorMessage = (error: unknown) => {
if (error instanceof Error) return error.message
return '接口调用失败,请检查后端服务和请求参数'
}
const getFileExtension = (fileName: string) => fileName.split('.').pop()?.toLowerCase() || ''
const isSupportedIcdFile = (fileName: string) => ['icd', 'cid', 'scd', 'xml'].includes(getFileExtension(fileName))
const parsedIndexSelectionState = computed(() => {
const source = indexSelectionJsonText.value.trim()
if (!source) {
return {
value: [] as MmsMapping.IndexSelectionGroup[],
error: parsedCandidates.value.length ? 'request.indexSelection 不能为空' : ''
}
}
try {
return {
value: parseIndexSelectionJson(source),
error: ''
}
} catch (error) {
return {
value: [] as MmsMapping.IndexSelectionGroup[],
error: getErrorMessage(error)
}
}
})
const isSubmitting = computed(() => isParsing.value || isGenerating.value)
const indexSelectionError = computed(() => parsedIndexSelectionState.value.error)
const canGenerate = computed(
() => Boolean(selectedIcdFile.value && indexSelectionJsonText.value.trim() && !indexSelectionError.value)
)
// 关键业务节点:请求配置区只在用户已经选择 ICD 后展示,避免初始态暴露无效的 JSON 编辑区和按钮。
const showConfigPanel = computed(() => Boolean(selectedIcdFile.value))
const showGenerateButton = computed(() => Boolean(selectedIcdFile.value))
const configEmptyDescription = computed(() => {
if (isParsing.value) return '正在根据当前 ICD 生成 request.indexSelection请稍候。'
if (selectedIcdFile.value) return '已选择 ICD 文件,请先点击“解析 ICD”生成 request.indexSelection。'
return '当前 ICD 暂未生成可编辑的 request.indexSelection。'
})
const selectedIcdFileName = computed(() => selectedIcdFile.value?.name || '')
const requestStatusText = computed(() => {
if (isParsing.value) return '解析中'
if (isGenerating.value) return '生成中'
if (selectedIcdFile.value && indexSelectionJsonText.value.trim()) return '已生成默认配置'
if (selectedIcdFile.value) return '待解析'
return '未选择文件'
})
const requestStatusTagType = computed<TagType>(() => {
if (isParsing.value || isGenerating.value) return 'warning'
if (selectedIcdFile.value && indexSelectionJsonText.value.trim()) return 'success'
if (selectedIcdFile.value) return 'primary'
return 'info'
})
const responseStatusText = computed(() => {
if (isSubmitting.value) return '等待返回'
return responsePayload.value?.status || '暂无结果'
})
const responseStatusTagType = computed<TagType>(() => {
if (isSubmitting.value) return 'warning'
if (responsePayload.value?.status === 'FAILED') return 'danger'
if (responsePayload.value?.status === 'NEED_INDEX_SELECTION') return 'warning'
if (responsePayload.value) return 'success'
return 'info'
})
const mappingJsonPreview = computed(() => {
const source = responsePayload.value?.mappingJson?.trim()
if (!source) return ''
try {
return JSON.stringify(JSON.parse(source), null, 4)
} catch {
return source
}
})
const mappingMetaText = computed(() => {
if (!mappingJsonPreview.value) return '当前返回未包含 mappingJson'
return `mappingJson ${mappingJsonPreview.value.length} 字符`
})
const problemList = computed(() => responsePayload.value?.problems?.filter(Boolean) || [])
const problemTabLabel = computed(() => {
if (!problemList.value.length) return '问题列表'
return `问题列表(${problemList.value.length}`
})
const resolveResultTab = (payload: MmsMapping.MappingTaskResponse | null): ResultTab => {
if (payload?.mappingJson?.trim()) return 'mapping'
if (payload?.problems?.filter(Boolean).length) return 'problem'
return 'mapping'
}
const stripProblemsFromIcdPayload = (payload: MmsMapping.MappingTaskResponse): MmsMapping.MappingTaskResponse => {
// 关键业务节点:解析 ICD 只消费候选数据和文档结构,不把后端返回的 problems 绑定到结果区。
const sanitizedPayload = { ...payload }
delete sanitizedPayload.problems
return sanitizedPayload
}
const resetParsedState = () => {
responsePayload.value = null
parsedCandidates.value = []
indexSelectionJsonText.value = ''
activeResultTab.value = 'mapping'
}
const handleIcdFileChange = (event: Event) => {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
if (!isSupportedIcdFile(file.name)) {
ElMessage.warning('请选择 ICD、CID、SCD 或 XML 文件')
input.value = ''
return
}
// 关键业务节点:切换 ICD 文件后先只清空旧解析结果,等用户明确点击“解析 ICD”后再请求后台。
selectedIcdFile.value = file
resetParsedState()
input.value = ''
}
const handleParseIcd = async () => {
if (!selectedIcdFile.value) {
ElMessage.warning('请先选择 ICD 文件')
return
}
isParsing.value = true
responsePayload.value = null
try {
// 关键业务节点:解析 ICD 时先走 get-icd拿到当前文件的候选数据后再生成默认 request.indexSelection。
const response = await getIcdApi({
icdFile: selectedIcdFile.value
})
const payload = unwrapApiPayload<MmsMapping.MappingTaskResponse>(response)
const sanitizedPayload = stripProblemsFromIcdPayload(payload)
const candidateGroups = payload.indexCandidates || []
responsePayload.value = sanitizedPayload
activeResultTab.value = resolveResultTab(sanitizedPayload)
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')
} catch (error) {
resetParsedState()
ElMessage.error(getErrorMessage(error))
} finally {
isParsing.value = false
}
}
const handleGenerateMapping = async () => {
if (!selectedIcdFile.value) {
ElMessage.warning('请先选择 ICD 文件')
return
}
if (!indexSelectionJsonText.value.trim()) {
ElMessage.warning('请先解析 ICD系统会自动生成 request.indexSelection')
return
}
const { error, value } = parsedIndexSelectionState.value
if (error) {
responsePayload.value = {
status: 'NEED_INDEX_SELECTION',
message: 'request.indexSelection 格式有误,请继续修正',
problems: [error]
}
activeResultTab.value = 'problem'
ElMessage.warning(error)
return
}
isGenerating.value = true
responsePayload.value = null
try {
// 关键业务节点:正式生成阶段只消费当前编辑区里的 request.indexSelection确保导出的映射与页面编辑态一致。
const response = await getIcdMmsJsonApi({
icdFile: selectedIcdFile.value,
request: {
...createBaseRequestPayload(DEFAULT_REQUEST_FORM),
indexSelection: value
}
})
const payload = unwrapApiPayload<MmsMapping.MappingTaskResponse>(response)
responsePayload.value = payload
activeResultTab.value = resolveResultTab(payload)
if (payload.status === 'FAILED') {
ElMessage.error(payload.message || '映射生成失败')
return
}
if (payload.status === 'NEED_INDEX_SELECTION') {
ElMessage.warning(payload.message || '当前配置仍需补充索引信息')
return
}
ElMessage.success(payload.message || '映射生成完成')
} catch (error) {
responsePayload.value = null
ElMessage.error(getErrorMessage(error))
} finally {
isGenerating.value = false
}
}
const buildExportFileName = () => {
const baseFileName = selectedIcdFile.value?.name.replace(/\.[^.]+$/, '') || 'mapping-summary'
return `${baseFileName}-mapping-summary.json`
}
const handleExportMapping = () => {
if (!mappingJsonPreview.value) {
ElMessage.warning('当前没有可导出的映射摘要')
return
}
const blob = new Blob([mappingJsonPreview.value], { type: 'application/json;charset=utf-8' })
const objectUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = objectUrl
link.download = buildExportFileName()
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(objectUrl)
ElMessage.success('映射摘要已导出')
}
const resetPage = () => {
selectedIcdFile.value = null
resetParsedState()
}
</script>
<style scoped lang="scss">
.mms-mapping-view {
min-height: 100%;
padding: 24px;
background: #f5f7fa;
.mms-mapping-page {
gap: 12px;
overflow: hidden;
}
.mms-mapping-card {
padding: 32px;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
.mms-mapping-layout {
display: grid;
grid-template-columns: minmax(360px, 0.85fr) minmax(0, 1.15fr);
gap: 16px;
width: 100%;
height: 100%;
overflow: hidden;
}
.page-title {
margin: 0 0 12px;
font-size: 24px;
font-weight: 600;
color: #1f2937;
.left-panel-stack {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 16px;
min-height: 0;
}
.page-description {
margin: 0;
font-size: 14px;
line-height: 1.6;
color: #4b5563;
@media (max-width: 1280px) {
.mms-mapping-layout {
grid-template-columns: 1fr;
}
}
</style>