feat(steady): 重构稳态校验功能并优化界面布局

- 更新 API 接口路径从 /steady/data-view/checksquare/* 到 /steady/checksquare/*
- 修改校验任务状态枚举值 FAILED 为 FAIL 并更新相关处理逻辑
- 移除缺失率和最大连续缺失分钟数字段,简化数据完整性计算
- 添加新的创建结果面板组件 ChecksquareCreateResultPanel.vue
- 调整创建对话框布局,采用两行搜索控件设计
- 更新任务表头部按钮文字为"新增"并调整搜索列配置为5列
- 修改详情面板显示开始时间和结束时间字段
- 重构工作台界面布局,使用 flex 布局替代 grid 布局
- 更新设备类型相关 API 接口和数据结构定义
- 添加设备类型字典常量并更新路由配置
- 优化搜索表单展开收起逻辑的计算方式
- 调整创建流程不再轮询获取任务详情,改为直接显示摘要信息
- 更新数据完整性格式化函数参数和调用方式
- 修改创建对话框样式类名和尺寸配置
- 添加设备类型管理相关的接口定义和实现方法
This commit is contained in:
2026-06-15 08:40:44 +08:00
parent 81f90ce0f2
commit ef80aff151
38 changed files with 4165 additions and 1254 deletions

View File

@@ -2,8 +2,10 @@
<section class="mapping-panel config-panel">
<div class="panel-header">
<div>
<h2 class="panel-title">人工索引配置</h2>
<p class="panel-description">展示现有的人工索引配置并允许继续编辑</p>
<div class="panel-title-tabs">
<span class="panel-title-tab">索引配置</span>
</div>
<p v-if="showDescription" class="panel-description">展示现有的索引配置并允许继续编辑</p>
</div>
<div class="panel-actions">
<el-button
@@ -23,7 +25,7 @@
:disabled="!canGenerate"
@click="emit('generate')"
>
生成JSON映射
JSON映射
</el-button>
</div>
</div>
@@ -39,7 +41,7 @@
:disabled="isSubmitting"
:rows="18"
resize="none"
placeholder="人工索引配置完成后,这里会自动回填索引配置,仍可继续直接编辑。"
placeholder="索引配置完成后,这里会自动回填索引配置,仍可继续直接编辑。"
@update:model-value="value => emit('update:indexSelectionJson', String(value || ''))"
/>
</div>
@@ -56,7 +58,7 @@ defineOptions({
name: 'MappingConfigPanel'
})
defineProps<{
withDefaults(defineProps<{
indexSelectionJson: string
isSubmitting: boolean
isGenerating: boolean
@@ -68,7 +70,10 @@ defineProps<{
canConfirm: boolean
hasDefaultJson: boolean
emptyDescription: string
}>()
showDescription?: boolean
}>(), {
showDescription: true
})
const emit = defineEmits<{
(event: 'update:indexSelectionJson', value: string): void
@@ -110,12 +115,34 @@ const emit = defineEmits<{
white-space: nowrap;
}
.panel-title {
margin: 0;
font-size: 22px;
font-weight: 600;
line-height: 1.4;
color: #1f2937;
.panel-actions :deep(.el-button + .el-button) {
margin-left: 0;
}
.panel-title-tabs {
display: inline-flex;
align-items: center;
border-bottom: 1px solid var(--el-border-color-light);
}
.panel-title-tab {
position: relative;
height: 36px;
padding: 0 2px;
font-size: 13px;
line-height: 36px;
color: var(--el-color-primary);
white-space: nowrap;
}
.panel-title-tab::after {
position: absolute;
right: 0;
bottom: -1px;
left: 0;
height: 2px;
background-color: var(--el-color-primary);
content: '';
}
.panel-description {

View File

@@ -2,10 +2,12 @@
<section class="mapping-panel">
<div class="panel-header">
<div>
<h2 class="panel-title">ICD 解析</h2>
<p class="panel-description">选择 ICD 文件后仅保存当前文件点击解析 ICD后才会向后台请求候选数据</p>
<div class="panel-title-tabs">
<span class="panel-title-tab">{{ panelTitle }}</span>
</div>
<p v-if="showDescription" class="panel-description">选择 ICD 文件后仅保存当前文件点击解析 ICD后才会向后台请求候选数据</p>
</div>
<el-tag :type="requestStatusTagType" effect="light">{{ requestStatusText }}</el-tag>
<el-tag v-if="requestStatusText" :type="requestStatusTagType" effect="light">{{ requestStatusText }}</el-tag>
</div>
<div class="panel-content">
@@ -19,7 +21,6 @@
/>
<el-button
type="primary"
plain
:icon="FolderOpened"
:disabled="isSubmitting"
@click="openIcdFilePicker"
@@ -43,7 +44,14 @@
>
解析 ICD
</el-button>
<el-button type="danger" plain :icon="Delete" :disabled="!canReset || isSubmitting" @click="emit('reset')">
<el-button
v-if="showResetButton"
type="danger"
plain
:icon="Delete"
:disabled="!canReset || isSubmitting"
@click="emit('reset')"
>
清空
</el-button>
</div>
@@ -61,16 +69,23 @@ defineOptions({
type TagType = 'success' | 'warning' | 'info' | 'primary' | 'danger'
defineProps<{
withDefaults(defineProps<{
selectedIcdFileName: string
isSubmitting: boolean
isParsing: boolean
icdFileAccept: string
requestStatusText: string
requestStatusTagType: TagType
requestStatusText?: string
requestStatusTagType?: TagType
showDescription?: boolean
showResetButton?: boolean
panelTitle?: string
canParse: boolean
canReset: boolean
}>()
}>(), {
showDescription: true,
showResetButton: true,
panelTitle: 'ICD 解析'
})
const emit = defineEmits<{
(event: 'file-change', value: Event): void
@@ -106,12 +121,30 @@ const openIcdFilePicker = () => {
gap: 16px;
}
.panel-title {
margin: 0;
font-size: 22px;
font-weight: 600;
line-height: 1.4;
color: #1f2937;
.panel-title-tabs {
display: inline-flex;
align-items: center;
border-bottom: 1px solid var(--el-border-color-light);
}
.panel-title-tab {
position: relative;
height: 36px;
padding: 0 2px;
font-size: 13px;
line-height: 36px;
color: var(--el-color-primary);
white-space: nowrap;
}
.panel-title-tab::after {
position: absolute;
right: 0;
bottom: -1px;
left: 0;
height: 2px;
background-color: var(--el-color-primary);
content: '';
}
.panel-description {

View File

@@ -2,10 +2,20 @@
<section class="mapping-panel">
<div class="panel-header">
<div>
<h2 class="panel-title">映射结果</h2>
<p class="panel-description">展示和导出JSON与XML的映射结果以及JSON的映射序列配置</p>
<div class="panel-title-tabs">
<span class="panel-title-tab">映射结果</span>
</div>
<p v-if="showDescription" class="panel-description">展示和导出JSON与XML的映射结果以及JSON的映射序列配置</p>
</div>
<div class="panel-actions">
<el-button
type="primary"
:icon="Setting"
:disabled="!canConfigureSequence"
@click="openSequenceDialog"
>
序列配置
</el-button>
<el-button
type="primary"
:icon="Connection"
@@ -13,12 +23,20 @@
:disabled="!canGenerateXmlMapping"
@click="emit('generate-xml-mapping')"
>
生成XML映射
XML映射
</el-button>
<el-button
v-if="showIcdCheckAction"
type="primary"
:icon="CircleCheck"
:disabled="!canSaveIcdCheckResult"
@click="emit('icd-check')"
>
ICD校验
</el-button>
<el-button
v-if="showSaveIcdCheckResult"
type="primary"
plain
:icon="Finished"
:loading="isSavingIcdCheckResult"
:disabled="!canSaveIcdCheckResult"
@@ -26,42 +44,31 @@
>
{{ saveIcdCheckResultText }}
</el-button>
<div class="export-actions">
<el-button
type="primary"
plain
:icon="Download"
:disabled="!canExportActiveMapping"
@click="emit('export-mapping', activeExportType)"
>
{{ exportButtonText }}
</el-button>
<el-dropdown trigger="click" :disabled="!canExportAnyMapping" @command="handleExportCommand">
<el-button
type="primary"
plain
class="export-menu-button"
:icon="ArrowDown"
:disabled="!canExportAnyMapping"
/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="json" :disabled="!canExportJsonMapping">
导出JSON映射
</el-dropdown-item>
<el-dropdown-item command="xml" :disabled="!canExportXmlMapping">
导出XML映射
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<el-tag :type="responseStatusTagType" effect="light">{{ responseStatusText }}</el-tag>
<el-tag v-if="responseStatusText" :type="responseStatusTagType" effect="light">
{{ responseStatusText }}
</el-tag>
</div>
</div>
<div class="panel-content panel-content--fixed">
<div class="panel-section result-card grow-card preview-tab-section">
<div class="mapping-preview-tabs">
<button
type="button"
:class="['panel-title-tab', { 'is-active': activeTabProxy === 'json' }]"
@click="activeTabProxy = 'json'"
>
JSON映射
</button>
<button
v-if="showXmlMappingTab"
type="button"
:class="['panel-title-tab', { 'is-active': activeTabProxy === 'xml' }]"
@click="activeTabProxy = 'xml'"
>
XML映射
</button>
</div>
<el-tabs v-model="activeTabProxy" class="preview-tabs">
<el-tab-pane label="JSON映射" name="json">
<div class="mapping-json-scroll">
@@ -71,16 +78,6 @@
:meta-text="mappingMetaText"
>
<template #actions>
<el-button
type="primary"
plain
size="small"
:icon="Setting"
:disabled="!mappingJsonPreview"
@click="openSequenceDialog"
>
序列配置
</el-button>
<el-button
type="primary"
plain
@@ -91,27 +88,49 @@
{{ problemButtonText }}
</el-button>
</template>
<template #trailing-actions>
<el-button
type="primary"
plain
size="small"
:icon="Download"
:disabled="!canExportJsonMapping"
@click="emit('export-mapping', 'json')"
>
下载JSON映射
</el-button>
</template>
</JsonMappingTree>
<el-empty v-else description="当前返回未包含 mappingJson" />
</div>
</el-tab-pane>
<el-tab-pane v-if="showXmlMappingTab" label="XML映射" name="xml">
<div class="mapping-json-scroll">
<div class="match-result-actions">
<el-button
type="primary"
plain
:icon="Document"
:disabled="!methodDescribe"
@click="matchResultDialogVisible = true"
>
匹配结果展示
</el-button>
</div>
<div v-if="xmlMappingPreview" class="xml-file-viewer">
<div class="xml-file-header">
<span class="xml-file-name">XML 文件</span>
<span class="xml-file-meta">{{ xmlMetaText }}</span>
<div v-if="xmlMappingPreview" class="xml-preview-viewer">
<div class="xml-preview-toolbar">
<div class="xml-preview-meta">{{ xmlMetaText }}</div>
<div class="xml-preview-actions">
<el-button
type="primary"
plain
size="small"
:icon="Document"
:disabled="!methodDescribe"
@click="matchResultDialogVisible = true"
>
匹配结果展示
</el-button>
<el-button
type="primary"
plain
size="small"
:icon="Download"
:disabled="!canExportXmlMapping"
@click="emit('export-mapping', 'xml')"
>
下载XML映射
</el-button>
</div>
</div>
<pre class="xml-file-content">{{ xmlMappingPreview }}</pre>
</div>
@@ -148,9 +167,9 @@
<el-dialog
v-model="sequenceDialogVisible"
title="序列配置"
width="920px"
width="960px"
destroy-on-close
top="8vh"
top="6vh"
class="sequence-config-dialog"
>
<template v-if="sequenceConfigRows.length">
@@ -217,7 +236,7 @@
</template>
<el-empty v-else description="当前 JSON 映射中未分析到包含 start 和 end 的序列。" />
<template #footer>
<el-button @click="sequenceDialogVisible = false">取消</el-button>
<el-button @click="closeSequenceDialog">取消</el-button>
<el-button type="primary" :disabled="!sequenceConfigRows.length" @click="confirmSequenceConfig">
确定
</el-button>
@@ -227,9 +246,8 @@
</template>
<script setup lang="ts">
import { ArrowDown, Connection, Document, Download, Finished, Search, Setting, Warning } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { computed, ref } from 'vue'
import { CircleCheck, Connection, Document, Download, Finished, Search, Setting, Warning } from '@element-plus/icons-vue'
import { computed, ref, watch } from 'vue'
import JsonMappingTree from './JsonMappingTree.vue'
defineOptions({
@@ -271,9 +289,10 @@ interface SequenceConfigGroup {
typeGroups: SequenceConfigTypeGroup[]
}
const props = defineProps<{
responseStatusText: string
responseStatusTagType: TagType
const props = withDefaults(defineProps<{
responseStatusText?: string
responseStatusTagType?: TagType
showDescription?: boolean
activeResultTab: ResultTab
mappingMetaText: string
mappingJsonPreview: string
@@ -286,20 +305,29 @@ const props = defineProps<{
methodDescribe: string
canExportJsonMapping: boolean
canExportXmlMapping: boolean
canConfigureSequence: boolean
canGenerateXmlMapping: boolean
isGeneratingXml: boolean
showXmlMappingTab: boolean
sequenceDialogVisible: boolean
showIcdCheckAction?: boolean
showSaveIcdCheckResult: boolean
canSaveIcdCheckResult: boolean
isSavingIcdCheckResult: boolean
saveIcdCheckResultText: string
}>()
}>(), {
showDescription: true,
showIcdCheckAction: true
})
const emit = defineEmits<{
(event: 'update:activeResultTab', value: ResultTab): void
(event: 'export-mapping', value: ExportMappingType): void
(event: 'generate-xml-mapping'): void
(event: 'update-mapping-json', value: string): void
(event: 'update:sequenceDialogVisible', value: boolean): void
(event: 'sequence-config-complete'): void
(event: 'icd-check'): void
(event: 'save-icd-check-result'): void
}>()
@@ -308,25 +336,17 @@ const activeTabProxy = computed({
set: value => emit('update:activeResultTab', value)
})
const canExportAnyMapping = computed(() => props.canExportJsonMapping || props.canExportXmlMapping)
const activeExportType = computed<ExportMappingType>(() => {
if (props.activeResultTab === 'xml') return 'xml'
if (props.canExportJsonMapping) return 'json'
if (props.canExportXmlMapping) return 'xml'
return 'json'
})
const canExportActiveMapping = computed(() =>
activeExportType.value === 'json' ? props.canExportJsonMapping : props.canExportXmlMapping
)
const exportButtonText = computed(() => (activeExportType.value === 'xml' ? '导出XML映射' : '导出JSON映射'))
const problemButtonText = computed(() =>
props.problemList.length ? `问题列表(${props.problemList.length}` : '问题列表'
)
const problemDialogVisible = ref(false)
const matchResultDialogVisible = ref(false)
const sequenceDialogVisible = ref(false)
const sequenceConfigRows = ref<SequenceConfigRow[]>([])
const sequenceSearchKeyword = ref('')
const sequenceDialogVisible = computed({
get: () => props.sequenceDialogVisible,
set: value => emit('update:sequenceDialogVisible', value)
})
const normalizedSequenceSearchKeyword = computed(() => sequenceSearchKeyword.value.trim().toLowerCase())
const filteredSequenceRows = computed(() => {
const keyword = normalizedSequenceSearchKeyword.value
@@ -379,12 +399,6 @@ const sequenceConfigGroups = computed<SequenceConfigGroup[]>(() => {
})
})
const handleExportCommand = (command: string | number | object) => {
if (command === 'json' || command === 'xml') {
emit('export-mapping', command)
}
}
const isRecord = (value: unknown): value is JsonObject =>
Boolean(value && typeof value === 'object' && !Array.isArray(value))
@@ -475,18 +489,36 @@ const normalizeSequenceValue = (value: string, valueType: string) => {
return numericValue
}
const openSequenceDialog = () => {
const prepareSequenceDialog = () => {
try {
const parsed = JSON.parse(props.mappingJsonPreview) as unknown
sequenceConfigRows.value = collectSequenceRows(parsed)
sequenceSearchKeyword.value = ''
sequenceDialogVisible.value = true
return true
} catch {
ElMessage.warning('当前 JSON 映射内容无法解析,不能配置序列')
return false
}
}
const openSequenceDialog = () => {
if (!prepareSequenceDialog()) return
sequenceDialogVisible.value = true
}
const closeSequenceDialog = () => {
sequenceDialogVisible.value = false
}
watch(
() => props.sequenceDialogVisible,
visible => {
if (!visible) return
prepareSequenceDialog()
}
)
const confirmSequenceConfig = () => {
try {
const nextJson = JSON.parse(props.mappingJsonPreview) as unknown
@@ -502,6 +534,7 @@ const confirmSequenceConfig = () => {
})
emit('update-mapping-json', JSON.stringify(nextJson, null, 4))
emit('sequence-config-complete')
sequenceDialogVisible.value = false
ElMessage.success('序列配置已同步到 JSON 映射')
} catch (error) {
@@ -539,25 +572,58 @@ const confirmSequenceConfig = () => {
gap: 12px;
}
.export-actions {
.panel-actions :deep(.el-button + .el-button) {
margin-left: 0;
}
.panel-title-tabs {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: 8px;
border-bottom: 1px solid var(--el-border-color-light);
}
.panel-title-tab {
position: relative;
height: 36px;
padding: 0 2px;
border: 0;
background: transparent;
font-size: 13px;
line-height: 36px;
color: var(--el-color-primary);
cursor: pointer;
white-space: nowrap;
}
.export-menu-button {
width: 32px;
padding: 8px;
.panel-title-tab::after {
position: absolute;
right: 0;
bottom: -1px;
left: 0;
height: 2px;
background-color: var(--el-color-primary);
content: '';
}
.panel-title {
margin: 0;
font-size: 22px;
font-weight: 600;
line-height: 1.4;
color: #1f2937;
.mapping-preview-tabs {
display: inline-flex;
align-items: center;
gap: 12px;
align-self: flex-start;
margin-bottom: 12px;
border-bottom: 1px solid var(--el-border-color-light);
}
.mapping-preview-tabs .panel-title-tab {
color: #4b5563;
}
.mapping-preview-tabs .panel-title-tab:not(.is-active)::after {
background-color: transparent;
}
.mapping-preview-tabs .panel-title-tab.is-active {
color: var(--el-color-primary);
}
.panel-description {
@@ -609,7 +675,7 @@ const confirmSequenceConfig = () => {
}
.preview-tabs :deep(.el-tabs__header) {
margin-bottom: 16px;
display: none;
}
.preview-tabs :deep(.el-tabs__nav-wrap::after) {
@@ -661,49 +727,14 @@ const confirmSequenceConfig = () => {
word-break: break-word;
}
.xml-file-viewer {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
border: 1px solid #dbe3f0;
border-radius: 10px;
background: #ffffff;
overflow: hidden;
}
.xml-file-header {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: space-between;
gap: 16px;
min-height: 40px;
padding: 0 14px;
border-bottom: 1px solid #e5e7eb;
background: #f8fafc;
white-space: nowrap;
}
.xml-file-name {
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
.xml-file-meta {
min-width: 0;
overflow: hidden;
font-size: 13px;
color: #64748b;
text-overflow: ellipsis;
}
.xml-file-content {
flex: 1;
min-height: 0;
margin: 0;
padding: 16px;
border: 1px solid #dbe3f0;
border-radius: 10px;
background: #ffffff;
overflow: auto;
font-family: Consolas, 'Courier New', monospace;
font-size: 13px;
@@ -712,11 +743,48 @@ const confirmSequenceConfig = () => {
white-space: pre;
}
.xml-preview-viewer {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
.xml-preview-toolbar {
display: flex;
align-items: center;
justify-content: flex-end;
min-height: 28px;
gap: 8px;
margin-bottom: 8px;
white-space: nowrap;
}
.xml-preview-meta {
flex: 1;
min-width: 0;
overflow: hidden;
color: #64748b;
font-size: 13px;
text-overflow: ellipsis;
}
.xml-preview-actions {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: 12px;
}
.xml-preview-actions :deep(.el-button + .el-button) {
margin-left: 0;
}
.problem-dialog-list {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 62vh;
max-height: 58vh;
padding-right: 4px;
overflow: auto;
}
@@ -753,13 +821,6 @@ const confirmSequenceConfig = () => {
word-break: break-word;
}
.match-result-actions {
display: flex;
flex: 0 0 auto;
justify-content: flex-end;
margin-bottom: 12px;
}
.match-result-detail {
max-height: 58vh;
padding: 14px 16px;
@@ -793,7 +854,7 @@ const confirmSequenceConfig = () => {
display: flex;
flex-direction: column;
gap: 16px;
max-height: 62vh;
max-height: 58vh;
padding-right: 4px;
overflow: auto;
}