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

@@ -18,29 +18,29 @@ export const querySteadyTrendDay = (params: SteadyDataView.SteadyTrendQueryParam
}
export const querySteadyChecksquareTasks = (params: SteadyDataView.SteadyChecksquareTaskQueryParams) => {
return http.post<SteadyDataView.PageResult<SteadyDataView.SteadyChecksquareTask>>('/steady/data-view/checksquare/query', params, {
return http.post<SteadyDataView.PageResult<SteadyDataView.SteadyChecksquareTask>>('/steady/checksquare/query', params, {
loading: false
})
}
export const createSteadyChecksquareTask = (params: SteadyDataView.SteadyChecksquareCreateParams) => {
return http.post<SteadyDataView.SteadyChecksquareCreateResult>('/steady/data-view/checksquare/create', params, {
return http.post<SteadyDataView.SteadyChecksquareTask>('/steady/checksquare/create', params, {
loading: false
})
}
export const deleteSteadyChecksquareTasks = (taskIds: SteadyDataView.SteadyChecksquareDeleteParams) => {
return http.post<boolean>('/steady/data-view/checksquare/delete', taskIds, {
return http.post<boolean>('/steady/checksquare/delete', taskIds, {
loading: false
})
}
export const getSteadyChecksquareDetail = (taskId: string) => {
return http.get<SteadyDataView.SteadyChecksquareQueryResult>('/steady/data-view/checksquare/detail', { taskId }, { loading: false })
return http.get<SteadyDataView.SteadyChecksquareQueryResult>('/steady/checksquare/detail', { taskId }, { loading: false })
}
export const getSteadyChecksquareItemDetail = (params: SteadyDataView.SteadyChecksquareItemDetailParams) => {
return http.get<SteadyDataView.SteadyChecksquareItemDetail>('/steady/data-view/checksquare/item-detail', params, {
return http.get<SteadyDataView.SteadyChecksquareItemDetail>('/steady/checksquare/item-detail', params, {
loading: false
})
}

View File

@@ -113,26 +113,13 @@ export namespace SteadyDataView {
timeStart?: string
timeEnd?: string
intervalMinutes?: number
taskStatus?: 'SUCCESS' | string
taskStatus?: 'RUNNING' | 'SUCCESS' | 'FAIL' | string
itemCount?: number
abnormalItemCount?: number
minDataIntegrity?: number | null
maxMissingRate?: number | null
createTime?: string
}
export interface SteadyChecksquareCreateResult {
taskId: string
taskNo?: string
lineId?: string
lineName?: string
timeStart?: string
timeEnd?: string
intervalMinutes?: number
itemCount?: number
abnormalItemCount?: number
}
export interface SteadyChecksquareSegment {
startTime: string
endTime: string
@@ -151,9 +138,6 @@ export namespace SteadyDataView {
missingPointCount?: number
dataIntegrity?: number | null
dataIntegrityText?: string | null
missingRate?: number | null
missingRateText?: string | null
maxContinuousMissingMinutes?: number
}
export interface SteadyChecksquareStatDetail {
@@ -174,9 +158,6 @@ export namespace SteadyDataView {
missingPointCount?: number
dataIntegrity?: number | null
dataIntegrityText?: string | null
missingRate?: number | null
missingRateText?: string | null
maxContinuousMissingMinutes?: number
abnormal?: boolean
abnormalPointCount?: number
harmonicParityAbnormal?: boolean
@@ -194,6 +175,11 @@ export namespace SteadyDataView {
timeStart: string
timeEnd: string
intervalMinutes?: number
taskStatus?: 'RUNNING' | 'SUCCESS' | 'FAIL' | string
itemCount?: number
abnormalItemCount?: number
minDataIntegrity?: number | null
createTime?: string
items: SteadyChecksquareItem[]
}

View File

@@ -9,20 +9,72 @@ const buildIcdFormData = (icdFile: File) => {
return formData
}
const buildIcdPathFormData = (icdFile: File, request: MmsMapping.CreateIcdPathRequest | MmsMapping.UpdateIcdPathRequest) => {
const formData = buildIcdFormData(icdFile)
formData.append('request', new Blob([JSON.stringify(request)], { type: 'application/json' }))
return formData
}
export const listDeviceTypesApi = () => {
return http.get<MmsMapping.DeviceType[]>('/api/mms-mapping/dev-types')
return http.get<MmsMapping.DeviceType[]>('/api/device-types')
}
export const createDeviceTypeApi = (params: MmsMapping.CreateDeviceTypeRequest) => {
return http.post<MmsMapping.DeviceType>('/api/mms-mapping/dev-types', params)
return http.post<boolean>('/api/device-types/add', params)
}
export const updateDeviceTypeApi = (params: MmsMapping.UpdateDeviceTypeRequest) => {
return http.post<boolean>('/api/device-types/update', params)
}
export const deleteDeviceTypesApi = (ids: string[]) => {
return http.post<boolean>('/api/device-types/delete', ids)
}
export const saveIcdCheckResultApi = (id: string, params: MmsMapping.SaveIcdCheckResultRequest) => {
return http.post<boolean>(`/api/mms-mapping/dev-types/${id}/icd-check-result`, params)
return http.post<boolean>(`/api/device-types/${id}/icd-check-result`, params)
}
export const pqdifCheckApi = (id: string) => {
return http.post<MmsMapping.PqdifCheckPlaceholder>(`/api/mms-mapping/dev-types/${id}/pqdif-check`)
return http.post<MmsMapping.PqdifCheckPlaceholder>(`/api/device-types/${id}/pqdif-check`)
}
export const listIcdPathsApi = (params: MmsMapping.IcdPathListRequest) => {
return http.post<MmsMapping.IcdPathRecord[]>('/api/mms-mapping/icd-paths/list', params)
}
export const createIcdPathApi = (params: MmsMapping.CreateIcdPathRequest) => {
return http.post<boolean>('/api/mms-mapping/icd-paths/add', params)
}
export const createIcdPathWithFileApi = (params: MmsMapping.CreateIcdPathWithFileRequest) => {
return http.post<boolean>('/api/mms-mapping/icd-paths/add', buildIcdPathFormData(params.icdFile, params.request), {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
export const updateIcdPathApi = (params: MmsMapping.UpdateIcdPathRequest) => {
return http.post<boolean>('/api/mms-mapping/icd-paths/update', params)
}
export const updateIcdPathWithFileApi = (params: MmsMapping.UpdateIcdPathWithFileRequest) => {
return http.post<boolean>('/api/mms-mapping/icd-paths/update', buildIcdPathFormData(params.icdFile, params.request), {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
export const deleteIcdPathsApi = (ids: string[]) => {
return http.post<boolean>('/api/mms-mapping/icd-paths/delete', ids)
}
export const saveIcdPathCheckResultApi = (id: string, params: MmsMapping.SaveIcdPathCheckResultRequest) => {
return http.post<boolean>(`/api/mms-mapping/icd-paths/${id}/icd-check-result`, params)
}
export const checkIcdJsonConsistencyApi = (params: MmsMapping.IcdJsonConsistencyCheckRequest) => {
return http.post<MmsMapping.IcdJsonConsistencyCheckResponse>('/api/mms-mapping/check-icd-json-consistency', params)
}
export const getIcdApi = (params: MmsMapping.GetIcdParams) => {

View File

@@ -65,6 +65,7 @@ export namespace MmsMapping {
export interface GetXmlFromJsonRequestPayload {
mappingJson: string
configType?: number
}
export interface GetXmlFromJsonParams {
@@ -137,6 +138,11 @@ export namespace MmsMapping {
icdPath?: string
icdResult?: number
icdMsg?: string
power?: string
devVolt?: number
devCurr?: number
devChns?: number
waveCmd?: string
reportName?: string
canCheckIcd?: boolean
canCheckPqdif?: boolean
@@ -144,12 +150,19 @@ export namespace MmsMapping {
export interface CreateDeviceTypeRequest {
name: string
icdId?: string
icdName?: string
icdPath?: string
icd?: string
power?: string
devVolt?: number
devCurr?: number
devChns?: number
waveCmd?: string
reportName?: string
}
export interface UpdateDeviceTypeRequest extends CreateDeviceTypeRequest {
id: string
}
export interface SaveIcdCheckResultRequest {
mappingJson?: string
xml?: string
@@ -157,6 +170,75 @@ export namespace MmsMapping {
msg?: string
}
export interface IcdPathRecord {
id?: string
name?: string
path?: string
angle?: number
usePhaseIndex?: number
state?: number
jsonStr?: string
xmlStr?: string
result?: number
msg?: string
type?: number
referenceIcdId?: string
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
export interface IcdPathListRequest {
keyword?: string
type?: number
result?: number
}
export interface CreateIcdPathRequest {
name: string
path: string
angle?: number
usePhaseIndex?: number
type?: number
}
export interface CreateIcdPathWithFileRequest {
icdFile: File
request: CreateIcdPathRequest
}
export interface UpdateIcdPathRequest extends CreateIcdPathRequest {
id: string
}
export interface UpdateIcdPathWithFileRequest {
icdFile: File
request: UpdateIcdPathRequest
}
export interface SaveIcdPathCheckResultRequest {
mappingJson?: string
xml?: string
result?: number
msg?: string
}
export interface IcdJsonConsistencyCheckRequest {
checkedJson: string
standardJson: string
saveToDisk?: boolean
outputDir?: string
}
export interface IcdJsonConsistencyCheckResponse {
result?: number
message?: string
issues?: string[]
issuesJson?: string
correctedJson?: string
}
export interface PqdifCheckPlaceholder {
status?: string
message?: string

View File

@@ -80,15 +80,15 @@ const breakPoint = computed<BreakPoint>(() => gridRef.value?.breakPoint)
// 判断是否显示 展开/合并 按钮
const showCollapse = computed(() => {
let show = false
const searchColCount =
typeof props.searchCol !== 'number' ? props.searchCol[breakPoint.value] : props.searchCol
const firstRowSearchCols = Math.max(searchColCount - 1, 1)
props.columns.reduce((prev, current) => {
prev +=
(current.search![breakPoint.value]?.span ?? current.search?.span ?? 1) +
(current.search![breakPoint.value]?.offset ?? current.search?.offset ?? 0)
if (typeof props.searchCol !== 'number') {
if (prev > props.searchCol[breakPoint.value]) show = true
} else {
if (prev > props.searchCol) show = true
}
if (prev > firstRowSearchCols) show = true
return prev
}, 0)
return show

View File

@@ -3,7 +3,11 @@ export const DICT_CODES = {
EVENT_TYPE: 'event_type',
LEDGER_DEVICE_TYPE: 'ledger_device_type',
LEDGER_DEVICE_MODEL: 'Ex-factory_Dev_Type',
LEDGER_TERMINAL_MODEL: 'Dev_Type'
LEDGER_TERMINAL_MODEL: 'Dev_Type',
DEVICE_TYPE_WORK_POWER: 'Dev_Power',
DEVICE_TYPE_CHANNEL_COUNT: 'Dev_Chns',
DEVICE_TYPE_RATED_VOLTAGE: 'Dev_Volt',
DEVICE_TYPE_RATED_CURRENT: 'Dev_Curr'
} as const
export type DictCode = (typeof DICT_CODES)[keyof typeof DICT_CODES]

View File

@@ -43,6 +43,7 @@ const STATIC_ROUTE_NAMES = new Set([
'tools',
'toolWaveform',
'toolMmsMapping',
'deviceTypes',
'toolAddData',
'toolAddLedger',
'eventList',

View File

@@ -60,17 +60,16 @@ export const staticRouter: RouteRecordRaw[] = [
component: () => import('@/views/tools/mmsMapping/index.vue'),
meta: {
cacheName: 'MmsMappingView',
title: 'MMS 映射'
title: '模型映射管理'
}
},
{
path: '/tools/mmsMapping/deviceTypes',
name: 'toolMmsMappingDeviceTypes',
alias: ['/tools/mmsmapping/deviceTypes', '/tools/mms-mapping/device-types'],
component: () => import('@/views/tools/mmsMapping/deviceTypes/index.vue'),
path: '/tools/deviceTypes',
name: 'deviceTypes',
component: () => import('@/views/tools/deviceTypes/index.vue'),
meta: {
cacheName: 'MmsDeviceTypesView',
title: '设备类型校验'
title: '设备类型管理'
}
},
{
@@ -159,7 +158,7 @@ export const staticRouter: RouteRecordRaw[] = [
component: () => import('@/views/steady/checksquare/index.vue'),
meta: {
cacheName: 'ChecksquareView',
title: '数据验证入库'
title: '数据验证'
}
},
{

View File

@@ -0,0 +1,417 @@
# check-square API 调试文档
## 1. 模块说明
- 模块路径:`steady/check-square`
- 接口基础路径:`/steady/checksquare`
- 返回包装:接口统一返回 `HttpResult<T>`,调试时重点查看响应体中的业务数据字段 `data`
- 时间格式:`yyyy-MM-dd HH:mm:ss`
本模块用于按监测点、时间范围和指标执行稳态数据校验,并提供任务列表、任务详情、检测项明细查询和任务删除能力。
## 2. 通用约定
### 2.1 请求头
| 名称 | 示例 | 说明 |
| --- | --- | --- |
| `Content-Type` | `application/json` | `POST` 请求使用 JSON 请求体 |
| `Authorization` | 登录态 Token | 如当前环境开启认证,需携带现有登录接口返回的认证信息 |
### 2.2 任务状态
| 值 | 说明 |
| --- | --- |
| `RUNNING` | 执行中 |
| `SUCCESS` | 执行成功 |
| `FAIL` | 执行失败 |
### 2.3 明细类型
| 值 | 说明 |
| --- | --- |
| `SEGMENT` | 缺失区间 |
| `VALUE_ORDER` | 指标值大小关系异常明细 |
| `HARMONIC_PARITY` | 谐波奇偶关系异常明细 |
## 3. 调试顺序建议
1. 调用 `POST /steady/checksquare/create`:按监测点和时间范围创建或获取任务。
2. 读取 `/create` 返回的 `data.taskId`:该返回值是任务列表中的行信息。
3. 调用 `GET /steady/checksquare/detail`:用 `taskId` 查询任务下的检测项。
4. 读取 `/detail` 返回的 `items[].itemId`:按检测项继续查询缺失区间或异常明细。
5. 调用 `GET /steady/checksquare/item-detail`:按 `itemId + detailType` 查询具体明细。
6. 调用 `POST /steady/checksquare/query`:按条件分页查询历史任务列表。
7. 需要清理任务时调用 `POST /steady/checksquare/delete`
注意:旧的获取或创建兼容接口已移除。创建或复用任务的逻辑已经合并到 `/create` 中。
## 4. 创建或获取任务
### 4.1 接口
`POST /steady/checksquare/create`
### 4.2 行为说明
接口开始执行前,会先按 `lineId + timeStart + timeEnd` 查询是否存在未删除的任务:
- 已存在:直接返回该任务的任务列表行信息。
- 不存在:创建任务,执行校验,任务执行完成后返回任务列表行信息。
返回对象为 `SteadyChecksquareTaskVO`,用于页面任务列表展示。若要查看检测项明细,需要再调用 `GET /steady/checksquare/detail`
### 4.3 请求字段
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `lineId` | `String` | 是 | 监测点 ID |
| `indicatorCodes` | `Array<String>` | 是 | 指标编码列表 |
| `timeStart` | `String` | 是 | 开始时间,格式 `yyyy-MM-dd HH:mm:ss` |
| `timeEnd` | `String` | 是 | 结束时间,格式 `yyyy-MM-dd HH:mm:ss` |
`indicatorCodes` 是数组;不要传成单个字符串。
### 4.4 请求示例
```http
POST /steady/checksquare/create
Content-Type: application/json
```
```json
{
"lineId": "LINE_001",
"indicatorCodes": ["VOLTAGE_A", "CURRENT_A"],
"timeStart": "2026-06-13 00:00:00",
"timeEnd": "2026-06-13 01:00:00"
}
```
### 4.5 响应字段 `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `taskId` | `String` | 任务 ID |
| `taskNo` | `String` | 任务编号 |
| `lineId` | `String` | 监测点 ID |
| `lineName` | `String` | 监测点名称 |
| `timeStart` | `String` | 开始时间 |
| `timeEnd` | `String` | 结束时间 |
| `intervalMinutes` | `Integer` | 统计间隔,单位分钟 |
| `taskStatus` | `String` | 任务状态:`RUNNING``SUCCESS``FAIL` |
| `itemCount` | `Integer` | 检测项数量 |
| `abnormalItemCount` | `Integer` | 异常检测项数量 |
| `minDataIntegrity` | `BigDecimal` | 最低数据完整率 |
| `createTime` | `String` | 创建时间 |
### 4.6 响应示例
```json
{
"code": 200,
"msg": "success",
"data": {
"taskId": "1812345678901234567",
"taskNo": "CS202606130001",
"lineId": "LINE_001",
"lineName": "1号监测点",
"timeStart": "2026-06-13 00:00:00",
"timeEnd": "2026-06-13 01:00:00",
"intervalMinutes": 1,
"taskStatus": "SUCCESS",
"itemCount": 2,
"abnormalItemCount": 1,
"minDataIntegrity": 98.50,
"createTime": "2026-06-13 09:30:00"
}
}
```
## 5. 查询任务列表
### 5.1 接口
`POST /steady/checksquare/query`
### 5.2 请求字段
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `lineId` | `String` | 否 | 监测点 ID |
| `indicatorCode` | `String` | 否 | 指标编码 |
| `timeStart` | `String` | 否 | 检测开始时间,格式 `yyyy-MM-dd HH:mm:ss` |
| `timeEnd` | `String` | 否 | 检测结束时间,格式 `yyyy-MM-dd HH:mm:ss` |
| `hasAbnormal` | `Boolean` | 否 | 是否存在异常 |
| `pageNum` | `Integer` | 否 | 页码 |
| `pageSize` | `Integer` | 否 | 每页条数 |
`indicatorCode` 是单个字符串,用于历史任务筛选;和 `/create``indicatorCodes` 数组不同。
### 5.3 请求示例
```http
POST /steady/checksquare/query
Content-Type: application/json
```
```json
{
"lineId": "LINE_001",
"indicatorCode": "VOLTAGE_A",
"timeStart": "2026-06-13 00:00:00",
"timeEnd": "2026-06-13 23:59:59",
"hasAbnormal": true,
"pageNum": 1,
"pageSize": 10
}
```
### 5.4 响应说明
`data` 为 MyBatis-Plus `Page<SteadyChecksquareTaskVO>` 分页对象,常用字段如下:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `records` | `Array<Object>` | 任务列表,每项字段同 `/create` 返回的 `SteadyChecksquareTaskVO` |
| `total` | `Long` | 总记录数 |
| `size` | `Long` | 每页条数 |
| `current` | `Long` | 当前页码 |
| `pages` | `Long` | 总页数 |
### 5.5 响应示例
```json
{
"code": 200,
"msg": "success",
"data": {
"records": [
{
"taskId": "1812345678901234567",
"taskNo": "CS202606130001",
"lineId": "LINE_001",
"lineName": "1号监测点",
"timeStart": "2026-06-13 00:00:00",
"timeEnd": "2026-06-13 01:00:00",
"intervalMinutes": 1,
"taskStatus": "SUCCESS",
"itemCount": 2,
"abnormalItemCount": 1,
"minDataIntegrity": 98.50,
"createTime": "2026-06-13 09:30:00"
}
],
"total": 1,
"size": 10,
"current": 1,
"pages": 1
}
}
```
## 6. 查询任务详情
### 6.1 接口
`GET /steady/checksquare/detail?taskId={taskId}`
### 6.2 请求参数
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `taskId` | `String` | 是 | 任务 ID |
### 6.3 请求示例
```http
GET /steady/checksquare/detail?taskId=1812345678901234567
```
### 6.4 响应字段 `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `taskId` | `String` | 任务 ID |
| `taskNo` | `String` | 任务编号 |
| `lineId` | `String` | 监测点 ID |
| `lineName` | `String` | 监测点名称 |
| `timeStart` | `String` | 开始时间 |
| `timeEnd` | `String` | 结束时间 |
| `intervalMinutes` | `Integer` | 统计间隔,单位分钟 |
| `items` | `Array<Object>` | 检测项列表 |
### 6.5 检测项字段 `items[]`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `itemId` | `String` | 检测项 ID用于查询明细 |
| `itemKey` | `String` | 检测项唯一键 |
| `indicatorCode` | `String` | 指标编码 |
| `indicatorName` | `String` | 指标名称 |
| `harmonicOrder` | `Integer` | 谐波次数 |
| `intervalMinutes` | `Integer` | 当前检测项统计间隔,单位分钟 |
| `hasData` | `Boolean` | 时间范围内是否存在任意数据 |
| `expectedPointCount` | `Integer` | 期望点数 |
| `actualPointCount` | `Integer` | 实际点数 |
| `missingPointCount` | `Integer` | 缺失点数 |
| `dataIntegrity` | `BigDecimal` | 数据完整率 |
| `dataIntegrityText` | `String` | 数据完整率文本 |
| `abnormal` | `Boolean` | 指标值大小关系是否异常 |
| `abnormalPointCount` | `Integer` | 指标值大小关系异常点数 |
| `abnormalDetails` | `Array<Object>` | 指标值大小关系异常明细摘要 |
| `harmonicParityAbnormal` | `Boolean` | 谐波奇偶关系是否异常 |
| `harmonicParityAbnormalPointCount` | `Integer` | 谐波奇偶关系异常点数 |
| `harmonicParityAbnormalDetails` | `Array<Object>` | 谐波奇偶关系异常明细摘要 |
| `statSummaries` | `Array<Object>` | 统计类型摘要 |
| `statDetails` | `Array<Object>` | 统计类型明细 |
### 6.6 响应示例
```json
{
"code": 200,
"msg": "success",
"data": {
"taskId": "1812345678901234567",
"taskNo": "CS202606130001",
"lineId": "LINE_001",
"lineName": "1号监测点",
"timeStart": "2026-06-13 00:00:00",
"timeEnd": "2026-06-13 01:00:00",
"intervalMinutes": 1,
"items": [
{
"itemId": "1812345678901234568",
"itemKey": "LINE_001:VOLTAGE_A",
"indicatorCode": "VOLTAGE_A",
"indicatorName": "A相电压",
"harmonicOrder": null,
"intervalMinutes": 1,
"hasData": true,
"expectedPointCount": 60,
"actualPointCount": 59,
"missingPointCount": 1,
"dataIntegrity": 98.33,
"dataIntegrityText": "98.33%",
"abnormal": true,
"abnormalPointCount": 1,
"abnormalDetails": [],
"harmonicParityAbnormal": false,
"harmonicParityAbnormalPointCount": 0,
"harmonicParityAbnormalDetails": [],
"statSummaries": [],
"statDetails": []
}
]
}
}
```
## 7. 查询检测项明细
### 7.1 接口
`GET /steady/checksquare/item-detail`
### 7.2 请求参数
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `itemId` | `String` | 是 | 检测项 ID |
| `detailType` | `String` | 是 | 明细类型:`SEGMENT``VALUE_ORDER``HARMONIC_PARITY` |
| `statType` | `String` | 否 | 统计类型;查询统计明细时使用 |
| `pageNum` | `Integer` | 否 | 页码 |
| `pageSize` | `Integer` | 否 | 每页条数 |
### 7.3 请求示例
```http
GET /steady/checksquare/item-detail?itemId=1812345678901234568&detailType=SEGMENT&pageNum=1&pageSize=10
```
### 7.4 响应字段 `data`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `itemId` | `String` | 检测项 ID |
| `detailType` | `String` | 明细类型 |
| `statType` | `String` | 统计类型 |
| `pageNum` | `Integer` | 当前页码;未分页查询时为空 |
| `pageSize` | `Integer` | 每页条数;未分页查询时为空 |
| `total` | `Long` | 总记录数;未分页查询时为空 |
| `segments` | `Array<Object>` | 缺失区间,`detailType=SEGMENT` 时查看 |
| `valueOrderDetails` | `Array<Object>` | 指标值大小关系异常明细,`detailType=VALUE_ORDER` 时查看 |
| `harmonicParityDetails` | `Array<Object>` | 谐波奇偶关系异常明细,`detailType=HARMONIC_PARITY` 时查看 |
### 7.5 响应示例
```json
{
"code": 200,
"msg": "success",
"data": {
"itemId": "1812345678901234568",
"detailType": "SEGMENT",
"statType": null,
"pageNum": 1,
"pageSize": 10,
"total": 1,
"segments": [
{
"segmentStart": "2026-06-13 00:10:00",
"segmentEnd": "2026-06-13 00:10:00",
"pointCount": 1
}
],
"valueOrderDetails": [],
"harmonicParityDetails": []
}
}
```
## 8. 删除任务
### 8.1 接口
`POST /steady/checksquare/delete`
### 8.2 请求字段
请求体直接传任务 ID 数组。
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `taskIds` | `Array<String>` | 是 | 任务 ID 数组 |
### 8.3 请求示例
```http
POST /steady/checksquare/delete
Content-Type: application/json
```
```json
[
"1812345678901234567"
]
```
### 8.4 响应示例
```json
{
"code": 200,
"msg": "success",
"data": true
}
```
## 9. 常见调试注意事项
- `/create` 返回的是任务列表行信息,不是详情页完整数据。
- `/create` 如果命中已存在任务,会直接返回该任务;不会生成重复任务。
- `/detail` 需要使用 `/create``/query` 返回的 `taskId`
- `/item-detail` 需要使用 `/detail` 返回的 `items[].itemId`
- `/query` 使用 `indicatorCode` 单值筛选;`/create` 使用 `indicatorCodes` 数组创建检测项。
- 当前文档只覆盖现有有效接口,不包含旧的任务获取或创建兼容接口。

View File

@@ -0,0 +1,215 @@
<template>
<aside class="table-main card checksquare-result-panel">
<div class="table-header">
<div class="header-button-lf">
<span class="section-title">校验任务摘要</span>
</div>
</div>
<el-empty v-if="!task" class="empty-result" description="新增后在此查看任务摘要" />
<template v-else>
<div class="result-body">
<div class="result-overview">
<div class="task-card">
<div class="task-title">
<span>{{ task.taskNo || task.taskId || '-' }}</span>
<el-tag :type="resolveChecksquareTaskStatusType(task.taskStatus)" effect="plain">
{{ formatChecksquareTaskStatus(task.taskStatus) }}
</el-tag>
</div>
<div class="task-meta">
<span>{{ task.lineName || task.lineId || '-' }}</span>
<span>{{ task.timeStart || '-' }} {{ task.timeEnd || '-' }}</span>
</div>
</div>
<div class="result-metrics">
<div class="metric-item">
<span class="metric-label">进线/监测点</span>
<span class="metric-value">{{ task.lineName || task.lineId || '-' }}</span>
</div>
<div class="metric-item">
<span class="metric-label">检测项</span>
<span class="metric-value">{{ task.itemCount ?? '-' }}</span>
</div>
<div class="metric-item">
<span class="metric-label">异常项</span>
<span class="metric-value is-danger">{{ task.abnormalItemCount ?? '-' }}</span>
</div>
<div class="metric-item">
<span class="metric-label">最低完整率</span>
<span class="metric-value">{{ formatChecksquareIntegrity(task.minDataIntegrity) }}</span>
</div>
<div class="metric-item">
<span class="metric-label">统计间隔</span>
<span class="metric-value">{{ task.intervalMinutes ?? '-' }}</span>
</div>
</div>
</div>
<div class="result-detail-grid">
<div class="detail-block">
<span class="detail-label">校验时间</span>
<span class="detail-value">{{ task.timeStart || '-' }} {{ task.timeEnd || '-' }}</span>
</div>
<div class="detail-block">
<span class="detail-label">创建时间</span>
<span class="detail-value">{{ task.createTime || '-' }}</span>
</div>
</div>
</div>
</template>
</aside>
</template>
<script setup lang="ts">
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
import {
formatChecksquareIntegrity,
formatChecksquareTaskStatus,
resolveChecksquareTaskStatusType
} from '../utils/checksquareTaskTable'
defineOptions({
name: 'ChecksquareCreateResultPanel'
})
defineProps<{
task: SteadyDataView.SteadyChecksquareTask | null
}>()
</script>
<style scoped lang="scss">
.checksquare-result-panel {
display: flex;
height: 100%;
min-width: 0;
min-height: 0;
flex-direction: column;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.empty-result {
flex: 1;
}
.result-body {
display: flex;
flex: 1;
min-height: 0;
flex-direction: column;
gap: 12px;
}
.result-overview {
display: grid;
flex: none;
grid-template-columns: minmax(0, 1fr);
gap: 12px;
align-items: stretch;
}
.task-card {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
}
.task-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-weight: 600;
}
.task-title span:first-child,
.task-meta span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-meta {
display: flex;
min-width: 0;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.result-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.metric-item {
display: flex;
min-width: 0;
flex-direction: column;
gap: 4px;
padding: 8px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
}
.metric-label,
.detail-label {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.metric-value,
.detail-value {
overflow: hidden;
font-weight: 600;
color: var(--el-text-color-primary);
text-overflow: ellipsis;
white-space: nowrap;
}
.metric-value.is-danger {
color: var(--el-color-danger);
}
.result-detail-grid {
display: grid;
flex: none;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.detail-block {
display: flex;
min-width: 0;
flex-direction: column;
gap: 6px;
padding: 12px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
background: var(--el-fill-color-blank);
}
.detail-value {
font-size: 13px;
white-space: normal;
word-break: break-all;
}
@media (max-width: 1280px) {
.result-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>

View File

@@ -8,7 +8,7 @@
<div class="overview-item">
<span class="overview-label">数据完整性</span>
<span class="overview-value">
{{ formatDataIntegrity(selectedItem.dataIntegrity, selectedItem.dataIntegrityText, selectedItem.missingRate) }}
{{ formatDataIntegrity(selectedItem.dataIntegrity, selectedItem.dataIntegrityText) }}
</span>
</div>
<div class="overview-item">
@@ -45,7 +45,8 @@
<el-table-column prop="status" label="状态" width="90">
<template #default="{ row }">{{ formatSegmentStatus(row.status) }}</template>
</el-table-column>
<el-table-column prop="startTime" label="统计时间" min-width="220" />
<el-table-column prop="startTime" label="开始时间" min-width="170" />
<el-table-column prop="endTime" label="结束时间" min-width="170" />
<el-table-column prop="harmonicOrder" label="谐波次数" width="100" align="right">
<template #default="{ row }">{{ row.harmonicOrder ?? '-' }}</template>
</el-table-column>

View File

@@ -121,7 +121,7 @@ const hasAbnormalCount = (value?: number | null) => Number(value || 0) > 0
const stripPercentUnit = (value: string) => value.replace(/%$/, '')
const formatSummaryDataIntegrity = (row: SteadyDataView.SteadyChecksquareItem) => {
return stripPercentUnit(formatDataIntegrity(row.dataIntegrity, row.dataIntegrityText, row.missingRate))
return stripPercentUnit(formatDataIntegrity(row.dataIntegrity, row.dataIntegrityText))
}
const formatSummaryStatIntegrity = (

View File

@@ -4,10 +4,10 @@
row-key="taskId"
:columns="columns"
:request-api="getTableList"
:search-col="{ xs: 1, sm: 2, md: 2, lg: 4, xl: 4 }"
:search-col="{ xs: 1, sm: 2, md: 2, lg: 5, xl: 5 }"
>
<template #tableHeader>
<el-button type="primary" :icon="Plus" @click="emit('createTask')">新增校验任务</el-button>
<el-button type="primary" :icon="Plus" @click="emit('createTask')">新增</el-button>
</template>
<template #operation="{ row }">
@@ -284,7 +284,7 @@ const columns = reactive<ColumnProps<SteadyDataView.SteadyChecksquareTask>[]>([
label: '最低完整性',
minWidth: 120,
align: 'center',
render: ({ row }) => formatChecksquareIntegrity(row.minDataIntegrity, row.maxMissingRate)
render: ({ row }) => formatChecksquareIntegrity(row.minDataIntegrity)
},
{
prop: 'createTime',

View File

@@ -32,6 +32,12 @@
@update:range-value="handleTimeRangeChange"
/>
</div>
<div class="query-actions">
<el-button type="primary" :icon="Plus" :loading="loading.query" @click="emit('create')">
新增
</el-button>
<el-button type="primary" plain :icon="RefreshLeft" @click="emit('reset')">重置</el-button>
</div>
<div class="toolbar-field indicator-form-item">
<span class="toolbar-field__label">稳态指标</span>
<div class="indicator-select-row">
@@ -62,13 +68,11 @@
<el-button type="primary" plain @click="handleSelectAllIndicators">全选</el-button>
</div>
</div>
<div class="query-actions">
<el-button type="primary" :icon="Plus" :loading="loading.query" @click="emit('create')">
新增校验任务
</el-button>
<el-button type="primary" plain :icon="RefreshLeft" @click="emit('reset')">重置</el-button>
</div>
</section>
<div class="checksquare-result-slot">
<slot name="result" />
</div>
</main>
</div>
</template>
@@ -210,7 +214,7 @@ watch(
<style scoped lang="scss">
.checksquare-layout {
display: grid;
grid-template-columns: 320px minmax(0, 1fr);
grid-template-columns: 240px minmax(0, 1fr);
gap: 12px;
width: 100%;
height: 100%;
@@ -249,17 +253,31 @@ watch(
}
.checksquare-main {
display: block;
display: flex;
flex-direction: column;
gap: 12px;
align-items: flex-end;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.checksquare-result-slot {
width: 100%;
min-width: 0;
min-height: 0;
flex: 1;
}
.checksquare-result-slot :deep(.checksquare-result-panel) {
height: 100%;
}
.query-card {
display: grid;
grid-template-columns: minmax(360px, 1.2fr) minmax(280px, 1fr);
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: stretch;
align-items: center;
padding: 12px;
}
@@ -271,6 +289,7 @@ watch(
}
.toolbar-field--time {
flex: 1 1 100%;
min-width: 0;
}
@@ -297,6 +316,8 @@ watch(
}
.indicator-form-item {
order: 2;
flex: 1 1 auto;
min-width: 0;
}
@@ -330,20 +351,27 @@ watch(
.query-actions {
display: flex;
grid-column: 1 / -1;
order: 3;
flex: 0 0 auto;
justify-content: flex-start;
gap: 8px;
}
@media (max-width: 1360px) {
.checksquare-layout:not(.is-ledger-collapsed) {
grid-template-columns: 280px minmax(0, 1fr);
grid-template-columns: 220px minmax(0, 1fr);
}
}
@media (max-width: 1280px) {
.query-card {
grid-template-columns: minmax(0, 1fr);
.toolbar-field--time,
.indicator-form-item {
flex: 1 1 100%;
}
.checksquare-result-slot {
width: 100%;
min-width: 0;
}
}
</style>

View File

@@ -14,6 +14,7 @@ const files = {
taskTable: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareTaskTable.vue'),
summaryTable: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareSummaryTable.vue'),
detailPanel: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareDetailPanel.vue'),
createResultPanel: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareCreateResultPanel.vue'),
measurementPointDialog: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareMeasurementPointDialog.vue'),
payload: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquarePayload.ts'),
ledgerUtils: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareLedger.ts'),
@@ -34,11 +35,12 @@ const checks = [
() => {
const api = read(files.api)
return (
/\/steady\/data-view\/checksquare\/query/.test(api) &&
/\/steady\/data-view\/checksquare\/create/.test(api) &&
/\/steady\/data-view\/checksquare\/delete/.test(api) &&
/\/steady\/data-view\/checksquare\/detail/.test(api) &&
/\/steady\/data-view\/checksquare\/item-detail/.test(api)
/\/steady\/checksquare\/query/.test(api) &&
/\/steady\/checksquare\/create/.test(api) &&
!/\/steady\/checksquare\/get-or-create/.test(api) &&
/\/steady\/checksquare\/delete/.test(api) &&
/\/steady\/checksquare\/detail/.test(api) &&
/\/steady\/checksquare\/item-detail/.test(api)
)
}
],
@@ -48,7 +50,7 @@ const checks = [
/export const deleteSteadyChecksquareTasks = \(taskIds: SteadyDataView\.SteadyChecksquareDeleteParams\)/.test(
read(files.api)
) &&
/http\.post<boolean>\('\/steady\/data-view\/checksquare\/delete', taskIds/.test(read(files.api)) &&
/http\.post<boolean>\('\/steady\/checksquare\/delete', taskIds/.test(read(files.api)) &&
/export type SteadyChecksquareDeleteParams = string\[\]/.test(read(files.apiTypes))
],
[
@@ -76,7 +78,10 @@ const checks = [
['task table uses ProTable like event list', () => /<ProTable[\s\S]*row-key="taskId"[\s\S]*:columns="columns"/.test(read(files.taskTable))],
[
'task table exposes create task header action',
() => /<template #tableHeader>/.test(read(files.taskTable)) && //.test(read(files.taskTable)) && /emit\('createTask'\)/.test(read(files.taskTable))
() =>
/<template #tableHeader>/.test(read(files.taskTable)) &&
/>新增<\/el-button>/.test(read(files.taskTable)) &&
/emit\('createTask'\)/.test(read(files.taskTable))
],
[
'task table has documented task columns',
@@ -123,14 +128,23 @@ const checks = [
['workbench emits create instead of old query action', () => /create: \[\]/.test(read(files.workbench)) && !/query: \[\]/.test(read(files.workbench))],
['workbench no longer renders result table', () => !/ChecksquareSummaryTable/.test(read(files.workbench))],
[
'create dialog workbench keeps time and indicator on one row without compressing actions',
() =>
/\.query-card\s*\{[\s\S]*grid-template-columns:\s*minmax\(360px,\s*1\.2fr\)\s+minmax\(280px,\s*1fr\)/.test(
read(files.workbench)
) &&
/\.query-actions\s*\{[\s\S]*grid-column:\s*1\s*\/\s*-1/.test(read(files.workbench)) &&
/\.indicator-select-row\s*\{[\s\S]*align-items:\s*center/.test(read(files.workbench)) &&
/\.query-actions\s*\{[\s\S]*justify-content:\s*flex-start/.test(read(files.workbench))
'workbench create action uses short add label',
() => /@click="emit\('create'\)"[\s\S]*>\s*新增\s*<\/el-button>/.test(read(files.workbench))
],
[
'create dialog workbench places search controls in two rows with actions after indicator',
() => {
const workbench = read(files.workbench)
return (
/\.query-card\s*\{[\s\S]*display:\s*flex[\s\S]*flex-wrap:\s*wrap/.test(workbench) &&
/\.toolbar-field--time\s*\{[\s\S]*flex:\s*1\s+1\s+100%/.test(workbench) &&
/\.indicator-form-item\s*\{[\s\S]*order:\s*2[\s\S]*flex:\s*1\s+1\s+auto/.test(workbench) &&
/\.query-actions\s*\{[\s\S]*order:\s*3[\s\S]*flex:\s*0\s+0\s+auto/.test(workbench) &&
/<div class="query-actions">[\s\S]*emit\('create'\)[\s\S]*emit\('reset'\)[\s\S]*<\/div>\s*<div class="toolbar-field indicator-form-item">/.test(
workbench
)
)
}
],
[
'payload builds create params without harmonic orders',
@@ -260,7 +274,14 @@ const checks = [
],
[
'search collapse toggle only appears when filters exceed available first row columns',
() => /prev\s*>\s*props\.searchCol\[breakPoint\.value\]/.test(read(files.searchForm))
() =>
/const searchColCount[\s\S]*typeof props\.searchCol !== 'number'[\s\S]*props\.searchCol\[breakPoint\.value\][\s\S]*const firstRowSearchCols[\s\S]*Math\.max\(searchColCount - 1,\s*1\)[\s\S]*prev\s*>\s*firstRowSearchCols/.test(
read(files.searchForm)
)
],
[
'checksquare task search grid follows event list five-column layout',
() => /:search-col="\{\s*xs:\s*1,\s*sm:\s*2,\s*md:\s*2,\s*lg:\s*5,\s*xl:\s*5\s*\}"/.test(read(files.taskTable))
],
[
'custom search render does not receive generic form item v-model',
@@ -270,14 +291,80 @@ const checks = [
],
[
'page wraps old workbench in create dialog',
() => /<el-dialog[\s\S]*新增校验任务[\s\S]*<ChecksquareWorkbench/.test(read(files.page))
() =>
/<el-dialog[\s\S]*新增校验任务[\s\S]*width="960px"[\s\S]*<ChecksquareWorkbench/.test(
read(files.page)
) &&
/\.checksquare-create-dialog\s*\{[\s\S]*height:\s*560px/.test(read(files.page))
],
[
'page create flow calls create api and refreshes task table',
'page create flow calls create api, keeps summary only and refreshes task table',
() =>
/createSteadyChecksquareTask/.test(read(files.page)) &&
!/getOrCreateSteadyChecksquareTask/.test(read(files.page)) &&
!/refreshCreateTaskDetail/.test(read(files.page)) &&
!/startCreateTaskPolling/.test(read(files.page)) &&
/taskTableRef\.value\?\.refresh\(\)/.test(read(files.page)) &&
/createDialogVisible\.value = false/.test(read(files.page))
/activeCreateTask\.value/.test(read(files.page))
],
[
'create dialog shows create task summary without detail table',
() => {
const page = read(files.page)
const workbench = read(files.workbench)
const panel = read(files.createResultPanel)
return (
exists(files.createResultPanel) &&
/<template #result>[\s\S]*<ChecksquareCreateResultPanel[\s\S]*:task="activeCreateTask"[\s\S]*<\/template>/.test(
page
) &&
/\.checksquare-create-dialog\s*\{[\s\S]*display:\s*block/.test(page) &&
/<slot name="result" \/>/.test(workbench) &&
/\.checksquare-main\s*\{[\s\S]*display:\s*flex[\s\S]*flex-direction:\s*column/.test(workbench) &&
/\.checksquare-layout\s*\{[\s\S]*grid-template-columns:\s*240px\s+minmax\(0,\s*1fr\)/.test(
workbench
) &&
/\.checksquare-result-slot\s*\{[\s\S]*width:\s*100%/.test(workbench) &&
/\.checksquare-result-slot\s*\{[\s\S]*min-width:\s*0/.test(workbench) &&
/\.checksquare-result-slot\s*:deep\(\.checksquare-result-panel\)\s*\{[\s\S]*height:\s*100%/.test(
workbench
) &&
/name:\s*'ChecksquareCreateResultPanel'/.test(panel) &&
/class="result-overview"/.test(panel) &&
/class="result-body"/.test(panel) &&
/class="result-detail-grid"/.test(panel) &&
!/detail-block--wide/.test(panel) &&
/\.result-overview\s*\{[\s\S]*grid-template-columns:\s*minmax\(0,\s*1fr\)/.test(panel) &&
/\.result-metrics\s*\{[\s\S]*grid-template-columns:\s*repeat\(2,\s*minmax\(0,\s*1fr\)\)/.test(panel) &&
/\.result-detail-grid\s*\{[^}]*grid-template-columns:\s*repeat\(2,\s*minmax\(0,\s*1fr\)\)/.test(panel) &&
!/class="result-tips"/.test(panel) &&
!/class="tips-title"/.test(panel) &&
!/class="tips-list"/.test(panel) &&
/class="result-metrics"[\s\S]*lineName[\s\S]*task\.itemCount/.test(panel) &&
/校验任务摘要/.test(panel) &&
/task\.itemCount/.test(panel) &&
/task\.abnormalItemCount/.test(panel) &&
/task\.minDataIntegrity/.test(panel) &&
!/class="detail-label">任务编号/.test(panel) &&
/\.result-body\s*\{[\s\S]*flex:\s*1/.test(panel) &&
!/class="result-detail-table"/.test(panel) &&
!/resultItems/.test(panel) &&
!/emit\('detail', row\)/.test(panel)
)
}
],
[
'create dialog does not poll create task detail',
() => {
const page = read(files.page)
return (
!/createTaskPollingTimer/.test(page) &&
!/startCreateTaskPolling/.test(page) &&
!/stopCreateTaskPolling/.test(page) &&
!/setInterval/.test(page) &&
!/onBeforeUnmount/.test(page)
)
}
],
[
'page delete flow confirms, calls delete api and refreshes task table',
@@ -407,8 +494,26 @@ const checks = [
/formatChecksquareIntegrity/.test(taskTableUtils) &&
/prop="dataIntegrity"/.test(summaryTable) &&
/formatDataIntegrity/.test(tableUtils) &&
!/prop:\s*'maxMissingRate'/.test(taskTable) &&
!/prop="missingRate"/.test(summaryTable)
!/maxMissingRate/.test(types) &&
!/missingRate/.test(types) &&
!/maxContinuousMissingMinutes/.test(types) &&
!/maxMissingRate/.test(taskTable) &&
!/missingRate/.test(summaryTable) &&
!/missingRate/.test(tableUtils) &&
!/missingRate/.test(taskTableUtils)
)
}
],
[
'checksquare task status follows documented FAIL value',
() => {
const source = read(files.taskTableUtils)
const types = read(files.apiTypes)
return (
/taskStatus\?: 'RUNNING' \| 'SUCCESS' \| 'FAIL' \| string/.test(types) &&
/status === 'FAIL'/.test(source) &&
!/FAILED/.test(source)
)
}
],
@@ -467,7 +572,13 @@ const checks = [
'detail panel renders documented detail fields',
() => {
const detailPanel = read(files.detailPanel)
return /prop="status"[\s\S]*状态/.test(detailPanel) && /oddHarmonicOrders/.test(detailPanel) && /oddValues/.test(detailPanel)
return (
/prop="status"[\s\S]*状态/.test(detailPanel) &&
/prop="startTime" label="开始时间"/.test(detailPanel) &&
/prop="endTime" label="结束时间"/.test(detailPanel) &&
/oddHarmonicOrders/.test(detailPanel) &&
/oddValues/.test(detailPanel)
)
}
],
[

View File

@@ -16,7 +16,13 @@
:data="measurementPointData"
/>
<el-dialog v-model="createDialogVisible" title="新增校验任务" width="1120px" append-to-body destroy-on-close>
<el-dialog
v-model="createDialogVisible"
title="新增校验任务"
width="960px"
append-to-body
destroy-on-close
>
<div class="checksquare-create-dialog">
<ChecksquareWorkbench
v-model:form="formState"
@@ -34,7 +40,13 @@
@indicator-change="handleIndicatorChange"
@create="handleCreateTask"
@reset="handleReset"
/>
>
<template #result>
<ChecksquareCreateResultPanel
:task="activeCreateTask"
/>
</template>
</ChecksquareWorkbench>
</div>
</el-dialog>
@@ -78,6 +90,7 @@ import {
sortSteadyIndicatorTree
} from '@/views/steady/steadyDataView/utils/selectionRules'
import { normalizeSteadyLedgerTree } from '@/views/steady/steadyDataView/utils/ledgerTree'
import ChecksquareCreateResultPanel from './components/ChecksquareCreateResultPanel.vue'
import ChecksquareDetailPanel from './components/ChecksquareDetailPanel.vue'
import ChecksquareMeasurementPointDialog from './components/ChecksquareMeasurementPointDialog.vue'
import ChecksquareSummaryTable from './components/ChecksquareSummaryTable.vue'
@@ -104,6 +117,7 @@ const selectedIndicators = ref<SteadyDataView.SteadyIndicatorNode[]>([])
const taskDetail = ref<SteadyDataView.SteadyChecksquareQueryResult | null>(null)
const selectedTask = ref<SteadyDataView.SteadyChecksquareTask | null>(null)
const selectedItem = ref<SteadyDataView.SteadyChecksquareItem | null>(null)
const activeCreateTask = ref<SteadyDataView.SteadyChecksquareTask | null>(null)
const measurementPointData = ref<ChecksquareMeasurementPointDetail | null>(null)
const formState = ref(defaultChecksquareFormState())
const ledgerKeyword = ref('')
@@ -163,6 +177,7 @@ const loadIndicatorTree = async () => {
}
const openCreateDialog = () => {
activeCreateTask.value = null
createDialogVisible.value = true
}
@@ -186,9 +201,30 @@ const handleReset = () => {
selectedIndicators.value = []
defaultLedgerCheckedKeys.value = []
defaultIndicatorCheckedKeys.value = []
activeCreateTask.value = null
selectorResetKey.value += 1
}
const normalizeCreateTask = (result: SteadyDataView.SteadyChecksquareTask): SteadyDataView.SteadyChecksquareTask | null => {
const taskId = result.taskId || result.taskNo
if (!taskId) return null
return {
taskId,
taskNo: result.taskNo,
lineId: result.lineId,
lineName: result.lineName,
timeStart: result.timeStart,
timeEnd: result.timeEnd,
intervalMinutes: result.intervalMinutes,
taskStatus: result.taskStatus,
itemCount: result.itemCount,
abnormalItemCount: result.abnormalItemCount,
minDataIntegrity: result.minDataIntegrity,
createTime: result.createTime
}
}
const handleCreateTask = async () => {
const selectionError = validateChecksquareSelection({
lineIds: lineIds.value,
@@ -202,12 +238,13 @@ const handleCreateTask = async () => {
loading.query = true
try {
// 新增校验任务会写入结果表,成功后刷新历史任务列表展示落库记录
await createSteadyChecksquareTask(
// /create 只返回任务行信息,检测项明细统一通过 /detail 拉取,避免把列表行误当成详情数据
const response = await createSteadyChecksquareTask(
buildSteadyChecksquareCreatePayload(lineIds.value[0], selectedIndicators.value, formState.value)
)
ElMessage.success('新增校验任务成功')
createDialogVisible.value = false
const result = unwrapData(response)
activeCreateTask.value = normalizeCreateTask(result)
ElMessage.success('校验任务已获取')
taskTableRef.value?.refresh()
} finally {
loading.query = false
@@ -276,6 +313,7 @@ onMounted(() => {
}
.checksquare-create-dialog {
display: block;
height: 560px;
min-height: 0;
}

View File

@@ -24,14 +24,9 @@ export const formatBooleanText = (value?: boolean | null) => {
return value ? '是' : '否'
}
export const formatDataIntegrity = (value?: number | null, text?: string | null, fallbackMissingRate?: number | null) => {
export const formatDataIntegrity = (value?: number | null, text?: string | null) => {
if (text) return text
const integrityValue =
value === null || value === undefined || !Number.isFinite(Number(value))
? fallbackMissingRate === null || fallbackMissingRate === undefined || !Number.isFinite(Number(fallbackMissingRate))
? null
: 1 - Number(fallbackMissingRate)
: Number(value)
const integrityValue = value === null || value === undefined || !Number.isFinite(Number(value)) ? null : Number(value)
if (integrityValue === null) return '-'
@@ -52,7 +47,7 @@ export const formatStatMissingRate = (
const summary = findStatSummary(item, statType)
if (!summary || summary.supported === false) return '-'
return formatDataIntegrity(summary.dataIntegrity, summary.dataIntegrityText, summary.missingRate)
return formatDataIntegrity(summary.dataIntegrity, summary.dataIntegrityText)
}
export const resolveChecksquareRowName = (item: SteadyDataView.SteadyChecksquareItem) => {
@@ -216,10 +211,6 @@ const summarizeStatType = (
const expectedPointCount = supportedSummaries.reduce((total, summary) => total + (summary.expectedPointCount || 0), 0)
const actualPointCount = supportedSummaries.reduce((total, summary) => total + (summary.actualPointCount || 0), 0)
const missingPointCount = supportedSummaries.reduce((total, summary) => total + (summary.missingPointCount || 0), 0)
const maxContinuousMissingMinutes = Math.max(
0,
...supportedSummaries.map(summary => summary.maxContinuousMissingMinutes || 0)
)
return {
statType,
@@ -229,8 +220,7 @@ const summarizeStatType = (
actualPointCount,
missingPointCount,
dataIntegrity: expectedPointCount ? actualPointCount / expectedPointCount : null,
missingRate: expectedPointCount ? missingPointCount / expectedPointCount : null,
maxContinuousMissingMinutes
dataIntegrityText: expectedPointCount ? undefined : '-'
}
}
@@ -252,7 +242,6 @@ export const buildHarmonicParentSummary = (
const expectedPointCount = sumNumber(children, item => item.expectedPointCount)
const actualPointCount = sumNumber(children, item => item.actualPointCount)
const missingPointCount = sumNumber(children, item => item.missingPointCount)
const maxContinuousMissingMinutes = Math.max(0, ...children.map(item => item.maxContinuousMissingMinutes || 0))
const statSummaries = CHECKSQUARE_STAT_TYPES.map(statType => summarizeStatType(children, statType))
return {
@@ -263,9 +252,6 @@ export const buildHarmonicParentSummary = (
missingPointCount,
dataIntegrity: expectedPointCount ? actualPointCount / expectedPointCount : null,
dataIntegrityText: expectedPointCount ? undefined : '-',
missingRate: expectedPointCount ? missingPointCount / expectedPointCount : null,
missingRateText: expectedPointCount ? undefined : '-',
maxContinuousMissingMinutes,
statSummaries,
statDetails: [],
children

View File

@@ -19,7 +19,7 @@ export const buildChecksquareTaskQueryParams = (
export const formatChecksquareTaskStatus = (status?: string) => {
if (!status) return '--'
if (status === 'SUCCESS') return '成功'
if (status === 'FAILED') return '失败'
if (status === 'FAIL') return '失败'
if (status === 'RUNNING') return '执行中'
return status
@@ -27,19 +27,14 @@ export const formatChecksquareTaskStatus = (status?: string) => {
export const resolveChecksquareTaskStatusType = (status?: string) => {
if (status === 'SUCCESS') return 'success'
if (status === 'FAILED') return 'danger'
if (status === 'FAIL') return 'danger'
if (status === 'RUNNING') return 'warning'
return 'info'
}
export const formatChecksquareIntegrity = (value?: number | null, fallbackMissingRate?: number | null) => {
const integrityValue =
value === null || value === undefined || !Number.isFinite(Number(value))
? fallbackMissingRate === null || fallbackMissingRate === undefined || !Number.isFinite(Number(fallbackMissingRate))
? null
: 1 - Number(fallbackMissingRate)
: Number(value)
export const formatChecksquareIntegrity = (value?: number | null) => {
const integrityValue = value === null || value === undefined || !Number.isFinite(Number(value)) ? null : Number(value)
if (integrityValue === null) return '--'

View File

@@ -0,0 +1,67 @@
/* eslint-env node */
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(currentDir, '../../../..')
const files = {
api: path.resolve(rootDir, 'api/tools/mmsmapping/index.ts'),
types: path.resolve(rootDir, 'api/tools/mmsmapping/interface/index.ts'),
page: path.resolve(rootDir, 'views/tools/deviceTypes/index.vue'),
mmsMappingFlow: path.resolve(rootDir, 'views/tools/mmsMapping/utils/useMmsMappingFlow.ts')
}
const read = file => fs.readFileSync(file, 'utf8')
const apiSource = read(files.api)
const typeSource = read(files.types)
const pageSource = read(files.page)
const mmsMappingSource = read(files.mmsMappingFlow)
const checks = [
['list API uses /api/device-types', () => /http\.get<[^>]+>\('\/api\/device-types'\)/.test(apiSource)],
['create API uses /api/device-types/add', () => /http\.post<[^>]+>\('\/api\/device-types\/add'/.test(apiSource)],
[
'update API uses /api/device-types/update',
() => /http\.post<[^>]+>\('\/api\/device-types\/update'/.test(apiSource)
],
[
'delete API uses /api/device-types/delete',
() => /http\.post<[^>]+>\('\/api\/device-types\/delete'/.test(apiSource)
],
[
'ICD result API uses /api/device-types/{id}/icd-check-result',
() => /`\/api\/device-types\/\$\{id\}\/icd-check-result`/.test(apiSource)
],
[
'PQDIF API uses /api/device-types/{id}/pqdif-check',
() => /`\/api\/device-types\/\$\{id\}\/pqdif-check`/.test(apiSource)
],
['old mms device type API path is removed', () => !/\/api\/mms-mapping\/dev-types/.test(apiSource)],
['DeviceType type includes power', () => /power\?:\s*string/.test(typeSource)],
['DeviceType type includes devVolt', () => /devVolt\?:\s*number/.test(typeSource)],
['DeviceType type includes devCurr', () => /devCurr\?:\s*number/.test(typeSource)],
['DeviceType type includes devChns', () => /devChns\?:\s*number/.test(typeSource)],
['DeviceType type includes waveCmd', () => /waveCmd\?:\s*string/.test(typeSource)],
['create request uses icd field', () => /CreateDeviceTypeRequest[\s\S]*icd\?:\s*string/.test(typeSource)],
['page form uses icd instead of icdId', () => /formModel\.icd/.test(pageSource) && !/formModel\.icdId/.test(pageSource)],
['page exposes edit flow', () => /openEditDialog/.test(pageSource) && /handleSaveDeviceType/.test(pageSource)],
['page exposes delete flow', () => /handleDeleteDeviceType/.test(pageSource) && /handleBatchDelete/.test(pageSource)],
[
'mms mapping save result imports updated shared API',
() => /saveIcdCheckResultApi/.test(mmsMappingSource) && /deviceTypeCheckId/.test(mmsMappingSource)
]
]
const failures = checks.filter(([, check]) => !check()).map(([name]) => name)
if (failures.length) {
console.error('deviceTypes API contract failed:')
for (const failure of failures) {
console.error(`- ${failure}`)
}
process.exit(1)
}
console.log('deviceTypes API contract passed')

View File

@@ -0,0 +1,30 @@
/* eslint-env node */
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(currentDir, '../../../..')
const pageFile = path.resolve(rootDir, 'views/tools/deviceTypes/index.vue')
const dialogFile = path.resolve(rootDir, 'views/tools/deviceTypes/components/IcdCheckDialog.vue')
const source = fs.readFileSync(pageFile, 'utf8')
const checks = [
['deviceTypes page no longer imports IcdCheckDialog', () => !/IcdCheckDialog/.test(source)],
['deviceTypes page no longer renders ICD check dialog', () => !/<IcdCheckDialog/.test(source)],
['deviceTypes page no longer exposes ICD check action', () => !/handleIcdCheck/.test(source) && !/ICD校验/.test(source)],
['deviceTypes page no longer holds ICD check dialog state', () => !/icdCheckDialogVisible/.test(source) && !/currentIcdCheckDevice/.test(source)],
['deviceTypes ICD check dialog file is removed', () => !fs.existsSync(dialogFile)]
]
const failures = checks.filter(([, check]) => !check()).map(([name]) => name)
if (failures.length) {
console.error('deviceTypes no ICD dialog contract failed:')
for (const failure of failures) {
console.error(`- ${failure}`)
}
process.exit(1)
}
console.log('deviceTypes no ICD dialog contract passed')

View File

@@ -0,0 +1,43 @@
/* eslint-env node */
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(currentDir, '../../../..')
const files = {
page: path.resolve(rootDir, 'views/tools/deviceTypes/index.vue'),
staticRouter: path.resolve(rootDir, 'routers/modules/staticRouter.ts'),
dynamicRouter: path.resolve(rootDir, 'routers/modules/dynamicRouter.ts')
}
const read = file => fs.readFileSync(file, 'utf8')
const exists = file => fs.existsSync(file)
const checks = [
['deviceTypes page exists', () => exists(files.page)],
['static router registers /tools/deviceTypes', () => /path:\s*'\/tools\/deviceTypes'/.test(read(files.staticRouter))],
['static route name is deviceTypes', () => /name:\s*'deviceTypes'/.test(read(files.staticRouter))],
['static route title is device type management', () => /title:\s*'设备类型管理'/.test(read(files.staticRouter))],
[
'static router imports deviceTypes page',
() => /@\/views\/tools\/deviceTypes\/index\.vue/.test(read(files.staticRouter))
],
[
'dynamic router keeps deviceTypes static route from being overwritten',
() => /STATIC_ROUTE_NAMES[\s\S]*'deviceTypes'/.test(read(files.dynamicRouter))
]
]
const failures = checks.filter(([, check]) => !check()).map(([name]) => name)
if (failures.length) {
console.error('deviceTypes route contract failed:')
for (const failure of failures) {
console.error(`- ${failure}`)
}
process.exit(1)
}
console.log('deviceTypes route contract passed')

View File

@@ -0,0 +1,119 @@
/* eslint-env node */
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(currentDir, '../../../..')
const pageFile = path.resolve(rootDir, 'views/tools/deviceTypes/index.vue')
const source = fs.readFileSync(pageFile, 'utf8')
const checks = [
['device type ICD check dialog is removed', () => !/IcdCheckDialog/.test(source) && !/handleIcdCheck/.test(source)],
['device type ICD check no longer routes to mms mapping page', () => !/router\.push\([\s\S]*\/tools\/mmsMapping/.test(source) && !/useRouter/.test(source)],
['page imports shared ProTable', () => /import\s+ProTable\s+from\s+'@\/components\/ProTable\/index\.vue'/.test(source)],
['page uses ProTable for device type table', () => /<ProTable[\s\S]*ref="proTable"[\s\S]*:columns="columns"/.test(source)],
['device type table uses ProTable request API', () => /<ProTable[\s\S]*:request-api="getTableList"/.test(source)],
[
'device type search grid follows event list five-field layout on wide screens',
() => /:search-col="\{\s*xs:\s*1,\s*sm:\s*2,\s*md:\s*2,\s*lg:\s*5,\s*xl:\s*5\s*\}"/.test(source)
],
['device type table keeps ProTable pagination enabled', () => !/:pagination="false"/.test(source)],
['device type table no longer passes static table data', () => !/:data="deviceTypes"/.test(source)],
['table keeps default ProTable refresh and column setting tools', () => /:tool-button="\['refresh', 'setting'\]"/.test(source)],
['table does not override ProTable toolButton slot', () => !/<template\s+#toolButton/.test(source)],
['table header actions use ProTable tableHeader slot', () => /<template\s+#tableHeader="scope">[\s\S]*handleBatchDelete\(scope\.selectedListIds\)/.test(source)],
['row actions use ProTable operation slot', () => /<template\s+#operation="\{\s*row\s*\}">[\s\S]*openEditDialog\(row\)[\s\S]*handleDeleteDeviceType\(row\)/.test(source)],
['old hand-written el-table body is removed', () => !/class="device-type-table-body"/.test(source)],
['device voltage column uses requested fixed width', () => /prop:\s*'devVolt'[\s\S]*width:\s*120/.test(source)],
['device current column uses requested fixed width', () => /prop:\s*'devCurr'[\s\S]*width:\s*120/.test(source)],
['channel count column uses compact fixed width', () => /prop:\s*'devChns'[\s\S]*width:\s*88/.test(source)],
['device type name column shares remaining table width', () => /prop:\s*'name'[\s\S]*minWidth:\s*170/.test(source)],
['ICD name column shares remaining table width', () => /prop:\s*'icdName'[\s\S]*minWidth:\s*170/.test(source)],
['power column uses fixed width 170', () => /prop:\s*'power'[\s\S]*width:\s*170/.test(source)],
[
'device type dialog uses dict selects for power, channels, voltage and current',
() =>
/<el-select[\s\S]*v-model="formModel\.power"[\s\S]*powerOptions/.test(source) &&
/<el-select[\s\S]*v-model="formModel\.devChns"[\s\S]*channelOptions/.test(source) &&
/<el-select[\s\S]*v-model="formModel\.devVolt"[\s\S]*voltageOptions/.test(source) &&
/<el-select[\s\S]*v-model="formModel\.devCurr"[\s\S]*currentOptions/.test(source)
],
[
'device type page reads option lists from dict code constants',
() =>
/DICT_CODES\.DEVICE_TYPE_WORK_POWER/.test(source) &&
/DICT_CODES\.DEVICE_TYPE_CHANNEL_COUNT/.test(source) &&
/DICT_CODES\.DEVICE_TYPE_RATED_VOLTAGE/.test(source) &&
/DICT_CODES\.DEVICE_TYPE_RATED_CURRENT/.test(source)
],
[
'device type key fields are searchable',
() =>
/prop:\s*'name'[\s\S]*search:\s*\{[\s\S]*el:\s*'input'[\s\S]*order:\s*1/.test(source) &&
/prop:\s*'icdName'[\s\S]*search:\s*\{[\s\S]*el:\s*'input'[\s\S]*order:\s*2/.test(source) &&
/prop:\s*'power'[\s\S]*search:\s*\{[\s\S]*el:\s*'select'[\s\S]*order:\s*3/.test(source) &&
/prop:\s*'devVolt'[\s\S]*search:\s*\{[\s\S]*el:\s*'select'[\s\S]*order:\s*4/.test(source) &&
/prop:\s*'devCurr'[\s\S]*search:\s*\{[\s\S]*el:\s*'select'[\s\S]*order:\s*5/.test(source) &&
/prop:\s*'devChns'[\s\S]*search:\s*\{[\s\S]*el:\s*'select'[\s\S]*order:\s*6/.test(source) &&
/prop:\s*'icdResult'[\s\S]*search:\s*\{[\s\S]*el:\s*'select'[\s\S]*order:\s*7/.test(source)
],
[
'device type list filters local full-list records before pagination',
() =>
/const\s+filterDeviceTypes\s*=\s*\(records:\s*MmsMapping\.DeviceType\[\],\s*params:\s*DeviceTypeTableParams\)/.test(source) &&
/const\s+filteredRecords\s*=\s*filterDeviceTypes\(records,\s*params\)/.test(source) &&
/records:\s*filteredRecords\.slice\(startIndex,\s*startIndex\s*\+\s*pageSize\)/.test(source) &&
/total:\s*filteredRecords\.length/.test(source)
],
['ICD check action is not shown on device type rows', () => !/ICD校验/.test(source) && !/ICD一致性校验/.test(source)],
['ICD result column shares remaining table width', () => /prop:\s*'icdResult'[\s\S]*minWidth:\s*170/.test(source)],
['operation column keeps action buttons visible', () => /prop:\s*'operation'[\s\S]*width:\s*360/.test(source)],
[
'page keeps ProTable pagination above layout footer',
() => {
const pageStyle = source.match(/\.mms-device-type-page\s*\{(?<style>[^}]*)\}/)?.groups?.style || ''
return (
/min-height:\s*0/.test(pageStyle) &&
/padding:\s*0/.test(pageStyle) &&
/overflow:\s*hidden/.test(pageStyle) &&
!/mms-device-type-card/.test(source)
)
}
],
[
'device type page follows event list direct ProTable layout',
() =>
/<div\s+class="table-box mms-device-type-page">\s*<ProTable/.test(source) &&
!/<section[\s\S]*<ProTable[\s\S]*<\/section>/.test(source)
],
[
'list API result is wrapped as a local ProTable page',
() =>
/const\s+getTableList\s*=\s*async\s*\(\s*params[\s\S]*pageNum[\s\S]*pageSize[\s\S]*records[\s\S]*total[\s\S]*current[\s\S]*size/.test(
source
)
],
[
'mutations refresh the ProTable request data',
() =>
/const\s+refreshDeviceTypes\s*=\s*\(\s*\)\s*=>\s*\{[\s\S]*proTable\.value\?\.clearSelection\(\)[\s\S]*proTable\.value\?\.getTableList\(\)[\s\S]*\}/.test(
source
)
],
['secondary long text columns are controlled by column setting', () => /prop:\s*'icdPath'[\s\S]*isShow:\s*false/.test(source) && /prop:\s*'waveCmd'[\s\S]*isShow:\s*false/.test(source) && /prop:\s*'reportName'[\s\S]*isShow:\s*false/.test(source) && /prop:\s*'icdMsg'[\s\S]*isShow:\s*false/.test(source)],
['numeric columns use ProTable default centered alignment', () => !/prop:\s*'devVolt'[\s\S]*align:\s*'right'/.test(source) && !/prop:\s*'devCurr'[\s\S]*align:\s*'right'/.test(source) && !/prop:\s*'devChns'[\s\S]*align:\s*'right'/.test(source)]
]
const failures = checks.filter(([, check]) => !check()).map(([name]) => name)
if (failures.length) {
console.error('deviceTypes table contract failed:')
for (const failure of failures) {
console.error(`- ${failure}`)
}
process.exit(1)
}
console.log('deviceTypes table contract passed')

View File

@@ -0,0 +1,605 @@
<template>
<div class="table-box mms-device-type-page">
<ProTable
ref="proTable"
:columns="columns"
:request-api="getTableList"
:request-error="handleTableRequestError"
:search-col="{ xs: 1, sm: 2, md: 2, lg: 5, xl: 5 }"
:tool-button="['refresh', 'setting']"
row-key="id"
>
<template #tableHeader="scope">
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增</el-button>
<el-button
type="danger"
plain
:icon="Delete"
:disabled="!scope.isSelected"
@click="handleBatchDelete(scope.selectedListIds)"
>
批量删除
</el-button>
</template>
<template #operation="{ row }">
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
<el-button
link
type="primary"
:icon="DocumentChecked"
:loading="pqdifCheckingId === row.id"
:disabled="!row.canCheckPqdif"
@click="handlePqdifCheck(row)"
>
PQDIF校验
</el-button>
<el-button link type="danger" :icon="Delete" @click="handleDeleteDeviceType(row)">删除</el-button>
</template>
</ProTable>
<el-dialog
v-model="formDialogVisible"
class="device-type-form-dialog"
:title="formDialogTitle"
width="800px"
destroy-on-close
>
<el-form ref="formRef" :model="formModel" :rules="formRules" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="名称" prop="name">
<el-input
v-model="formModel.name"
maxlength="32"
show-word-limit
clearable
placeholder="请输入设备类型名称"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工作电源">
<el-select v-model="formModel.power" clearable filterable placeholder="请选择工作电源">
<el-option
v-for="option in powerOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="通道数">
<el-select v-model="formModel.devChns" clearable filterable placeholder="请选择通道数">
<el-option
v-for="option in channelOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="额定电压(V)">
<el-select v-model="formModel.devVolt" clearable filterable placeholder="请选择额定电压">
<el-option
v-for="option in voltageOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="额定电流(A)">
<el-select v-model="formModel.devCurr" clearable filterable placeholder="请选择额定电流">
<el-option
v-for="option in currentOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备关联ICD">
<el-input v-model="formModel.icd" maxlength="80" clearable placeholder="可选,填写关联 ICD ID" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="录波命令">
<el-input v-model="formModel.waveCmd" maxlength="120" clearable placeholder="可选,录波命令" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="报告模板">
<el-input v-model="formModel.reportName" maxlength="120" clearable placeholder="可选,报告模板名称" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="formDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSaveDeviceType">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="tsx">
import { Delete, DocumentChecked, Edit, Plus } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type TagProps } from 'element-plus'
import { computed, reactive, ref } from 'vue'
import type { ResultData } from '@/api/interface'
import ProTable from '@/components/ProTable/index.vue'
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
import { DICT_CODES } from '@/constants/dictCodes'
import { useDictStore } from '@/stores/modules/dict'
import {
createDeviceTypeApi,
deleteDeviceTypesApi,
listDeviceTypesApi,
pqdifCheckApi,
updateDeviceTypeApi
} from '@/api/tools/mmsmapping'
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
defineOptions({
name: 'MmsDeviceTypesView'
})
type TagType = TagProps['type']
interface DeviceTypeFormModel {
id: string
name: string
icd: string
power: string
devVolt: string
devCurr: string
devChns: string
waveCmd: string
reportName: string
}
interface DeviceTypeTableParams {
pageNum?: number
pageSize?: number
name?: string
icdName?: string
power?: string
devVolt?: string | number
devCurr?: string | number
devChns?: string | number
icdResult?: string | number
}
const dictStore = useDictStore()
const saving = ref(false)
const pqdifCheckingId = ref('')
const formDialogVisible = ref(false)
const formMode = ref<'create' | 'edit'>('create')
const formRef = ref<FormInstance>()
const proTable = ref<ProTableInstance>()
const formModel = reactive<DeviceTypeFormModel>({
id: '',
name: '',
icd: '',
power: '',
devVolt: '',
devCurr: '',
devChns: '',
waveCmd: '',
reportName: ''
})
const formRules: FormRules<DeviceTypeFormModel> = {
name: [{ required: true, message: '请输入设备类型名称', trigger: 'blur' }]
}
const formDialogTitle = computed(() => (formMode.value === 'create' ? '新增设备类型' : '编辑设备类型'))
const powerOptions = computed(() => resolveDictOptions(DICT_CODES.DEVICE_TYPE_WORK_POWER, formModel.power))
const channelOptions = computed(() => resolveDictOptions(DICT_CODES.DEVICE_TYPE_CHANNEL_COUNT, formModel.devChns))
const voltageOptions = computed(() => resolveDictOptions(DICT_CODES.DEVICE_TYPE_RATED_VOLTAGE, formModel.devVolt))
const currentOptions = computed(() => resolveDictOptions(DICT_CODES.DEVICE_TYPE_RATED_CURRENT, formModel.devCurr))
const icdResultOptions = [
{ label: '一致', value: 1 },
{ label: '不一致', value: 0 },
{ label: '未校验', value: -1 }
]
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 && typeof error === 'object' && 'message' in error && typeof error.message === 'string') {
return error.message
}
if (error instanceof Error && error.message) return error.message
return '接口调用失败,请检查后端服务和请求参数'
}
const trimOptionalText = (value?: string) => {
const text = value?.trim()
return text || undefined
}
const toOptionalNumber = (value?: string) => {
const text = value?.trim()
if (!text) return undefined
const parsedValue = Number(text)
return Number.isNaN(parsedValue) ? undefined : parsedValue
}
const stringifyOptionalNumber = (value?: number) => {
if (value === undefined || value === null) return ''
return String(value)
}
const normalizeSearchText = (value: unknown) => String(value ?? '').trim().toLowerCase()
const includesSearchText = (value: unknown, keyword: unknown) => {
const searchText = normalizeSearchText(keyword)
if (!searchText) return true
return normalizeSearchText(value).includes(searchText)
}
const equalsSearchValue = (value: unknown, keyword: unknown) => {
const searchText = normalizeSearchText(keyword)
if (!searchText) return true
return normalizeSearchText(value) === searchText
}
const resolveIcdResultSearchValue = (value?: number) => {
if (value === 1 || value === 0) return String(value)
return '-1'
}
const filterDeviceTypes = (records: MmsMapping.DeviceType[], params: DeviceTypeTableParams) => {
return records.filter(record => {
return (
includesSearchText(record.name, params.name) &&
includesSearchText(record.icdName, params.icdName) &&
equalsSearchValue(record.power, params.power) &&
equalsSearchValue(record.devVolt, params.devVolt) &&
equalsSearchValue(record.devCurr, params.devCurr) &&
equalsSearchValue(record.devChns, params.devChns) &&
equalsSearchValue(resolveIcdResultSearchValue(record.icdResult), params.icdResult)
)
})
}
const resolveDictOptions = (code: string, currentValue = '') => {
const options = dictStore.getDictData(code).map(item => ({
label: item.name,
value: String(item.value || item.code || item.id)
}))
if (!currentValue || options.some(item => item.value === currentValue)) return options
return [
...options,
{
label: currentValue,
value: currentValue
}
]
}
const resetForm = () => {
formModel.id = ''
formModel.name = ''
formModel.icd = ''
formModel.power = ''
formModel.devVolt = ''
formModel.devCurr = ''
formModel.devChns = ''
formModel.waveCmd = ''
formModel.reportName = ''
formRef.value?.clearValidate()
}
const openCreateDialog = () => {
formMode.value = 'create'
resetForm()
formDialogVisible.value = true
}
const openEditDialog = (row: MmsMapping.DeviceType) => {
formMode.value = 'edit'
resetForm()
formModel.id = row.id || ''
formModel.name = row.name || ''
formModel.icd = row.icdId || ''
formModel.power = row.power || ''
formModel.devVolt = stringifyOptionalNumber(row.devVolt)
formModel.devCurr = stringifyOptionalNumber(row.devCurr)
formModel.devChns = stringifyOptionalNumber(row.devChns)
formModel.waveCmd = row.waveCmd || ''
formModel.reportName = row.reportName || ''
formDialogVisible.value = true
}
const getIcdResultText = (value?: number) => {
if (value === 1) return '一致'
if (value === 0) return '不一致'
return '未校验'
}
const getIcdResultTagType = (value?: number): TagType => {
if (value === 1) return 'success'
if (value === 0) return 'danger'
return 'info'
}
const columns = reactive<ColumnProps<MmsMapping.DeviceType>[]>([
{ type: 'selection', fixed: 'left', width: 70 },
{ type: 'index', fixed: 'left', width: 70, label: '序号' },
{
prop: 'name',
label: '设备类型名称',
minWidth: 170,
fixed: 'left',
search: {
el: 'input',
label: '设备类型名称',
order: 1,
props: {
placeholder: '请输入设备类型名称'
}
}
},
{
prop: 'icdName',
label: 'ICD 名称',
minWidth: 170,
search: {
el: 'input',
label: 'ICD 名称',
order: 2,
props: {
placeholder: '请输入 ICD 名称'
}
}
},
{ prop: 'icdPath', label: 'ICD 路径', minWidth: 220, isShow: false },
{
prop: 'power',
label: '工作电源',
width: 170,
enum: powerOptions,
isFilterEnum: false,
search: {
el: 'select',
label: '工作电源',
order: 3
}
},
{
prop: 'devVolt',
label: '额定电压(V)',
width: 120,
enum: voltageOptions,
isFilterEnum: false,
search: {
el: 'select',
label: '额定电压(V)',
order: 4
}
},
{
prop: 'devCurr',
label: '额定电流(A)',
width: 120,
enum: currentOptions,
isFilterEnum: false,
search: {
el: 'select',
label: '额定电流(A)',
order: 5
}
},
{
prop: 'devChns',
label: '通道数',
width: 88,
enum: channelOptions,
isFilterEnum: false,
search: {
el: 'select',
label: '通道数',
order: 6
}
},
{ prop: 'waveCmd', label: '录波命令', minWidth: 140, isShow: false },
{ prop: 'reportName', label: '报告模板', minWidth: 160, isShow: false },
{
prop: 'icdResult',
label: 'ICD 校验结论',
minWidth: 170,
render: scope => (
<el-tag type={getIcdResultTagType(scope.row.icdResult)} effect="light">
{getIcdResultText(scope.row.icdResult)}
</el-tag>
),
enum: icdResultOptions,
isFilterEnum: false,
search: {
el: 'select',
label: 'ICD 校验结论',
order: 7
}
},
{ prop: 'icdMsg', label: '结论描述', minWidth: 220, isShow: false },
{ prop: 'operation', label: '操作', fixed: 'right', width: 360 }
])
const buildSavePayload = (): MmsMapping.CreateDeviceTypeRequest => ({
name: formModel.name.trim(),
icd: trimOptionalText(formModel.icd),
power: trimOptionalText(formModel.power),
devVolt: toOptionalNumber(formModel.devVolt),
devCurr: toOptionalNumber(formModel.devCurr),
devChns: toOptionalNumber(formModel.devChns),
waveCmd: trimOptionalText(formModel.waveCmd),
reportName: trimOptionalText(formModel.reportName)
})
const getTableList = async (params: DeviceTypeTableParams = {}) => {
const response = await listDeviceTypesApi()
const records = unwrapApiPayload<MmsMapping.DeviceType[]>(response) || []
const filteredRecords = filterDeviceTypes(records, params)
const pageNum = params.pageNum || 1
const pageSize = params.pageSize || 10
const startIndex = (pageNum - 1) * pageSize
// 设备类型后端当前提供全量列表接口;这里适配 ProTable 分页结构,使表格布局与暂降事件列表保持一致。
return {
data: {
records: filteredRecords.slice(startIndex, startIndex + pageSize),
total: filteredRecords.length,
current: pageNum,
size: pageSize
}
}
}
const refreshDeviceTypes = () => {
proTable.value?.clearSelection()
proTable.value?.getTableList()
}
const handleTableRequestError = (error: unknown) => {
ElMessage.error(getErrorMessage(error))
}
const handleSaveDeviceType = async () => {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
if (formMode.value === 'edit' && !formModel.id) {
ElMessage.warning('当前设备类型缺少 ID不能保存修改')
return
}
saving.value = true
try {
const payload = buildSavePayload()
// 关键业务节点:设备类型维护接口按后端文档提交 cs_dev_type 业务字段ICD 展示字段由列表接口回填。
if (formMode.value === 'create') {
await createDeviceTypeApi(payload)
ElMessage.success('设备类型新增成功')
} else {
await updateDeviceTypeApi({
...payload,
id: formModel.id
})
ElMessage.success('设备类型编辑成功')
}
formDialogVisible.value = false
refreshDeviceTypes()
} catch (error) {
ElMessage.error(getErrorMessage(error))
} finally {
saving.value = false
}
}
const deleteDeviceTypes = async (ids: string[], successMessage: string) => {
if (!ids.length) {
ElMessage.warning('请选择要删除的设备类型')
return
}
try {
await ElMessageBox.confirm('删除后设备类型将被置为无效状态,是否继续?', '删除设备类型', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch {
return
}
try {
await deleteDeviceTypesApi(ids)
ElMessage.success(successMessage)
refreshDeviceTypes()
} catch (error) {
ElMessage.error(getErrorMessage(error))
}
}
const handleDeleteDeviceType = async (row: MmsMapping.DeviceType) => {
if (!row.id) {
ElMessage.warning('当前设备类型缺少 ID不能删除')
return
}
await deleteDeviceTypes([row.id], '设备类型删除成功')
}
const handleBatchDelete = async (ids: string[]) => {
await deleteDeviceTypes(ids, '设备类型批量删除成功')
}
const handlePqdifCheck = async (row: MmsMapping.DeviceType) => {
if (!row.id) {
ElMessage.warning('当前设备类型缺少 ID不能执行 PQDIF 校验')
return
}
pqdifCheckingId.value = row.id
try {
const response = await pqdifCheckApi(row.id)
const payload = unwrapApiPayload<MmsMapping.PqdifCheckPlaceholder>(response)
ElMessage.info(payload?.message || 'PQDIF校验功能待实现')
} catch (error) {
ElMessage.error(getErrorMessage(error))
} finally {
pqdifCheckingId.value = ''
}
}
</script>
<style scoped lang="scss">
.mms-device-type-page {
min-height: 0;
padding: 0;
overflow: hidden;
}
:deep(.device-type-form-dialog .el-dialog__header) {
margin-right: 0;
padding: 12px 16px;
background: #536fe5;
}
:deep(.device-type-form-dialog .el-dialog__title),
:deep(.device-type-form-dialog .el-dialog__headerbtn .el-dialog__close) {
color: #ffffff;
}
:deep(.device-type-form-dialog .el-select) {
width: 100%;
}
</style>

View File

@@ -19,8 +19,8 @@
<div class="tool-text">进入 MMS 映射页面后续可继续补充映射配置和预览能力</div>
</button>
<button class="tool-item" type="button" @click="handleNavigate('/tools/mmsMapping/deviceTypes')">
<div class="tool-name">设备类型校验</div>
<button class="tool-item" type="button" @click="handleNavigate('/tools/deviceTypes')">
<div class="tool-name">设备类型维护</div>
<div class="tool-text">维护设备类型并从列表进入 ICD 一致性校验和 PQDIF 预留校验</div>
</button>

View File

@@ -0,0 +1,376 @@
<template>
<el-dialog
:model-value="visible"
class="icd-check-dialog"
:title="dialogTitle"
width="72vw"
top="4vh"
destroy-on-close
@close="handleClose"
>
<div class="icd-check-dialog__body">
<div class="icd-check-flow-wrap">
<el-steps class="icd-check-flow" :active="activeStepIndex" finish-status="success" align-center>
<el-step
v-for="step in icdCheckFlowSteps"
:key="step.title"
:title="step.title"
:description="step.description"
:status="step.status"
/>
</el-steps>
</div>
<div class="icd-check-dialog__layout">
<div class="left-panel-stack">
<MappingRequestPanel
:selected-icd-file-name="selectedIcdFileName"
:is-submitting="isSubmitting"
:is-parsing="isParsing"
:icd-file-accept="icdFileAccept"
:show-description="false"
:show-reset-button="false"
panel-title="选取及解析ICD"
:can-parse="canParseIcd"
:can-reset="canResetPage"
@file-change="handleIcdFileChange"
@parse="handleParseIcd"
@reset="resetPage"
/>
<MappingConfigPanel
v-model:index-selection-json="indexSelectionJsonText"
:is-submitting="isSubmitting"
:is-generating="isGenerating"
:can-generate="canGenerate"
:json-error="indexSelectionError"
:show-generate-button="true"
:show-confirm-button="true"
confirm-button-text="索引配置"
:can-confirm="Boolean(confirmData.length) && !isSubmitting"
:has-default-json="Boolean(indexSelectionJsonText.trim())"
:empty-description="configEmptyDescription"
:show-description="false"
@confirm-config="confirmDialogVisible = true"
@generate="handleGenerateMapping"
/>
</div>
<MappingResultPanel
v-model:active-result-tab="activeResultTab"
:mapping-meta-text="mappingMetaText"
:mapping-json-preview="mappingJsonPreview"
:xml-meta-text="xmlMetaText"
:xml-mapping-preview="xmlMappingPreview"
:xml-empty-text="xmlEmptyText"
:problem-tab-label="problemTabLabel"
:problem-list="problemList"
:problem-empty-text="problemEmptyText"
:method-describe="methodDescribe"
:can-export-json-mapping="canExportJsonMapping"
:can-export-xml-mapping="canExportXmlMapping"
:can-configure-sequence="canConfigureSequence"
:can-generate-xml-mapping="canGenerateXmlMapping"
:is-generating-xml="isGeneratingXml"
:show-xml-mapping-tab="showXmlMappingTab"
:sequence-dialog-visible="sequenceDialogVisible"
:show-save-icd-check-result="showSaveIcdCheckResult"
:show-icd-check-action="showConsistencyCheck"
:can-save-icd-check-result="canSaveIcdCheckResult"
:is-saving-icd-check-result="isSavingIcdCheckResult"
:save-icd-check-result-text="saveIcdCheckResultText"
:show-description="false"
@export-mapping="handleExportMapping"
@generate-xml-mapping="handleGenerateXmlMapping"
@icd-check="handleIcdConsistencyCheck"
@update-mapping-json="handleUpdateMappingJson"
@update:sequence-dialog-visible="sequenceDialogVisible = $event"
@sequence-config-complete="handleSequenceConfigComplete"
@save-icd-check-result="handleSaveIcdCheckResult"
/>
</div>
</div>
<MappingConfirmDialog
:visible="confirmDialogVisible"
:submitting="isConfirmingSelection"
:confirm-data="confirmData"
@update:visible="confirmDialogVisible = $event"
@confirm="handleConfirmIndexSelection"
/>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, toRef } from 'vue'
import type { ResultData } from '@/api/interface'
import { saveIcdPathCheckResultApi } from '@/api/tools/mmsmapping'
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
import MappingRequestPanel from './MappingRequestPanel.vue'
import MappingConfigPanel from './MappingConfigPanel.vue'
import MappingResultPanel from './MappingResultPanel.vue'
import MappingConfirmDialog from './MappingConfirmDialog.vue'
import { useMmsMappingFlow } from '../utils/useMmsMappingFlow'
defineOptions({
name: 'IcdPathCheckDialog'
})
const props = defineProps<{
visible: boolean
icdPathId: string
icdPathName: string
icdPathType: number
activeIcdPathRecord: MmsMapping.IcdPathRecord | null
}>()
const emit = defineEmits<{
(event: 'update:visible', value: boolean): void
(event: 'saved'): void
}>()
const dialogTitle = computed(() => (props.icdPathName ? `ICD校验${props.icdPathName}` : 'ICD校验'))
const showConsistencyCheck = computed(() => props.icdPathType === 1 || props.icdPathType === 3)
const saveIcdCheckResult = (params: MmsMapping.SaveIcdCheckResultRequest): Promise<ResultData<boolean> | boolean> => {
return saveIcdPathCheckResultApi(props.icdPathId, params)
}
const {
selectedIcdFileName,
isSubmitting,
isParsing,
icdFileAccept,
canParseIcd,
canResetPage,
indexSelectionJsonText,
isGenerating,
canGenerate,
indexSelectionError,
confirmData,
configEmptyDescription,
activeResultTab,
hasParsedIcd,
hasIndexSelection,
hasJsonMapping,
hasSequenceConfigured,
hasXmlMapping,
mappingMetaText,
mappingJsonPreview,
xmlMetaText,
xmlMappingPreview,
xmlEmptyText,
problemTabLabel,
problemList,
problemEmptyText,
methodDescribe,
canExportJsonMapping,
canExportXmlMapping,
canConfigureSequence,
canGenerateXmlMapping,
isGeneratingXml,
showXmlMappingTab,
sequenceDialogVisible,
showSaveIcdCheckResult,
canSaveIcdCheckResult,
isSavingIcdCheckResult,
saveIcdCheckResultText,
hasIcdConsistencyCheckResult,
confirmDialogVisible,
isConfirmingSelection,
handleIcdFileChange,
handleParseIcd,
resetPage,
handleGenerateMapping,
handleExportMapping,
handleGenerateXmlMapping,
handleIcdConsistencyCheck,
handleUpdateMappingJson,
handleSequenceConfigComplete,
handleSaveIcdCheckResult,
handleConfirmIndexSelection
} = useMmsMappingFlow({
deviceTypeCheckId: toRef(props, 'icdPathId'),
deviceTypeCheckName: toRef(props, 'icdPathName'),
standardMappingJson: computed(() => props.activeIcdPathRecord?.jsonStr?.trim() || ''),
standardMappingName: computed(() => props.activeIcdPathRecord?.name?.trim() || ''),
saveIcdCheckResult,
onIcdCheckSaved: () => {
emit('saved')
emit('update:visible', false)
}
})
type StepStatus = 'success' | 'wait' | 'process' | 'finish' | 'error'
interface IcdCheckFlowStep {
title: string
description: string
status: StepStatus
}
const createFlowStep = (title: string, description: string, status: StepStatus): IcdCheckFlowStep => ({
title,
description,
status
})
const activeStepIndex = computed(() => {
const saveStepIndex = showConsistencyCheck.value ? 7 : 6
if (isSavingIcdCheckResult.value) return saveStepIndex
if (hasIcdConsistencyCheckResult.value) return 6
if (hasXmlMapping.value) return showConsistencyCheck.value ? 6 : saveStepIndex
if (isGeneratingXml.value) return 5
if (hasSequenceConfigured.value) return 5
if (hasJsonMapping.value) return 4
if (isGenerating.value) return 3
if (hasIndexSelection.value) return 3
if (confirmDialogVisible.value || confirmData.value.length) return 2
if (hasParsedIcd.value) return 2
if (isParsing.value) return 1
if (selectedIcdFileName.value) return 1
return 0
})
const icdCheckFlowSteps = computed<IcdCheckFlowStep[]>(() => {
const steps = [
createFlowStep(
'选取ICD',
selectedIcdFileName.value ? selectedIcdFileName.value : '等待选择ICD文件',
selectedIcdFileName.value ? 'success' : 'process'
),
createFlowStep(
'解析ICD',
hasParsedIcd.value ? '已完成ICD解析' : isParsing.value ? '正在解析ICD' : '等待解析ICD',
hasParsedIcd.value ? 'success' : isParsing.value || selectedIcdFileName.value ? 'process' : 'wait'
),
createFlowStep(
'索引配置',
hasIndexSelection.value ? '已生成索引配置' : confirmData.value.length ? '等待确认索引' : '等待解析结果',
hasIndexSelection.value ? 'success' : confirmDialogVisible.value || confirmData.value.length ? 'process' : 'wait'
),
createFlowStep(
'JSON映射',
hasJsonMapping.value ? '已生成JSON映射' : isGenerating.value ? '正在生成JSON映射' : '等待索引配置',
hasJsonMapping.value ? 'success' : isGenerating.value ? 'process' : 'wait'
),
createFlowStep(
'序列配置',
hasSequenceConfigured.value ? '已完成序列配置' : hasJsonMapping.value ? '等待序列配置' : '等待JSON映射',
hasSequenceConfigured.value ? 'success' : hasJsonMapping.value ? 'process' : 'wait'
),
createFlowStep(
'XML映射',
hasXmlMapping.value ? '已生成XML映射' : isGeneratingXml.value ? '正在生成XML映射' : '等待序列配置',
hasXmlMapping.value ? 'success' : isGeneratingXml.value ? 'process' : hasSequenceConfigured.value ? 'process' : 'wait'
)
]
if (showConsistencyCheck.value) {
steps.push(
createFlowStep(
'ICD一致性校验',
hasIcdConsistencyCheckResult.value
? '已完成JSON一致性校验'
: hasXmlMapping.value
? '等待执行JSON一致性校验'
: '等待XML映射',
hasIcdConsistencyCheckResult.value ? 'success' : hasXmlMapping.value ? 'process' : 'wait'
)
)
}
steps.push(
createFlowStep(
'保存',
isSavingIcdCheckResult.value ? '正在保存校验结果' : hasXmlMapping.value ? '可保存校验结果' : '等待XML映射',
isSavingIcdCheckResult.value ? 'process' : 'wait'
)
)
return steps
})
const handleClose = () => {
resetPage()
emit('update:visible', false)
}
</script>
<style scoped lang="scss">
.icd-check-dialog__body {
display: flex;
flex-direction: column;
gap: 16px;
height: 78vh;
min-height: 0;
overflow: hidden;
}
.icd-check-flow-wrap {
flex: 0 0 auto;
padding: 22px 16px 19px;
border: 1px solid #dcebe2;
border-radius: 8px;
background: #fbfefc;
overflow-x: auto;
}
.icd-check-flow {
min-width: 1040px;
}
:deep(.icd-check-flow .el-step__title) {
font-size: 14px;
font-weight: 600;
line-height: 1.5;
}
:deep(.icd-check-flow .el-step__description) {
max-width: 132px;
margin-top: 6px;
font-size: 12px;
line-height: 1.5;
word-break: break-word;
}
:deep(.icd-check-flow .el-step__head.is-process),
:deep(.icd-check-flow .el-step__title.is-process),
:deep(.icd-check-flow .el-step__description.is-process) {
color: #67c23a;
border-color: #67c23a;
}
.icd-check-dialog__layout {
display: grid;
grid-template-columns: minmax(360px, 1fr) minmax(0, 1fr);
gap: 16px;
height: 100%;
min-height: 0;
overflow: hidden;
}
.left-panel-stack {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 16px;
min-height: 0;
}
:deep(.el-dialog__body) {
padding: 12px 16px 16px;
}
@media (max-width: 1280px) {
.icd-check-dialog__body {
height: 76vh;
overflow: auto;
}
.icd-check-dialog__layout {
grid-template-columns: 1fr;
height: auto;
}
}
</style>

View File

@@ -0,0 +1,282 @@
<template>
<el-dialog
:model-value="visible"
class="icd-check-dialog"
:title="dialogTitle"
width="760px"
destroy-on-close
@close="handleClose"
>
<div class="icd-check-dialog__body">
<div class="icd-path-form-dialog__body">
<el-form ref="formRef" :model="formModel" :rules="formRules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="ICD名称" prop="name">
<el-input
v-model="formModel.name"
maxlength="80"
show-word-limit
clearable
placeholder="请输入 ICD 名称"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="ICD类型">
<el-select v-model="formModel.type" clearable placeholder="请选择 ICD 类型">
<el-option
v-for="option in icdTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="ICD路径" prop="path">
<el-input
v-model="formModel.path"
maxlength="260"
show-word-limit
clearable
placeholder="请输入 ICD 存储路径"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="ICD文件">
<el-upload
accept=".icd,.cid,.scd,.xml"
:auto-upload="false"
:limit="1"
:show-file-list="true"
:on-change="handleIcdFileChange"
:on-remove="handleIcdFileRemove"
>
<el-button type="primary" plain>选择文件</el-button>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="角度">
<el-input-number
v-model="formModel.angle"
:min="-360"
:max="360"
controls-position="right"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="相位索引">
<el-switch
v-model="formModel.usePhaseIndex"
:active-value="1"
:inactive-value="0"
active-text="启用"
inactive-text="关闭"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</div>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage, type FormInstance, type FormRules, type UploadFile } from 'element-plus'
import type { ResultData } from '@/api/interface'
import { createIcdPathApi, createIcdPathWithFileApi, updateIcdPathApi, updateIcdPathWithFileApi } from '@/api/tools/mmsmapping'
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
defineOptions({
name: 'IcdPathFormDialog'
})
const props = defineProps<{
visible: boolean
mode: 'create' | 'edit'
record: MmsMapping.IcdPathRecord | null
}>()
const emit = defineEmits<{
(event: 'update:visible', value: boolean): void
(event: 'saved'): void
}>()
interface IcdPathFormModel {
id: string
name: string
path: string
angle?: number
usePhaseIndex: number
type?: number
}
const formRef = ref<FormInstance>()
const saving = ref(false)
const selectedIcdFile = ref<File | null>(null)
const formModel = reactive<IcdPathFormModel>({
id: '',
name: '',
path: '',
angle: 0,
usePhaseIndex: 0,
type: 3
})
const icdTypeOptions = [
{ label: '手动录入的标准', value: 1 },
{ label: '手动录入的非标准', value: 2 },
{ label: '上游解析传递', value: 3 }
]
const formRules: FormRules<IcdPathFormModel> = {
name: [{ required: true, message: '请输入 ICD 名称', trigger: 'blur' }],
path: [{ required: true, message: '请输入 ICD 存储路径', trigger: 'blur' }]
}
const dialogTitle = computed(() => (props.mode === 'create' ? '新增ICD记录' : '编辑ICD记录'))
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 && error.message) return error.message
return '接口调用失败,请检查后端服务和请求参数'
}
const resetForm = () => {
formModel.id = ''
formModel.name = ''
formModel.path = ''
formModel.angle = 0
formModel.usePhaseIndex = 0
formModel.type = 3
selectedIcdFile.value = null
formRef.value?.clearValidate()
}
const fillForm = (record: MmsMapping.IcdPathRecord | null) => {
resetForm()
if (!record) return
formModel.id = record.id || ''
formModel.name = record.name || ''
formModel.path = record.path || ''
formModel.angle = record.angle ?? 0
formModel.usePhaseIndex = record.usePhaseIndex ?? 0
formModel.type = record.type
}
watch(
() => props.visible,
visible => {
if (visible) fillForm(props.record)
}
)
const buildSavePayload = (): MmsMapping.CreateIcdPathRequest => ({
name: formModel.name.trim(),
path: formModel.path.trim(),
angle: formModel.angle,
usePhaseIndex: formModel.usePhaseIndex,
type: formModel.type
})
const handleClose = () => {
emit('update:visible', false)
}
const handleIcdFileChange = (uploadFile: UploadFile) => {
selectedIcdFile.value = uploadFile.raw || null
}
const handleIcdFileRemove = () => {
selectedIcdFile.value = null
}
const handleSave = async () => {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
if (props.mode === 'edit' && !formModel.id) {
ElMessage.warning('当前 ICD 记录缺少 ID不能保存修改')
return
}
saving.value = true
try {
const payload = buildSavePayload()
let response: ResultData<boolean> | boolean
if (selectedIcdFile.value) {
// 关键业务节点:选择 ICD 文件时按接口文档提交 icdFile + request后端会同步保存原始文件内容。
response =
props.mode === 'create'
? await createIcdPathWithFileApi({
icdFile: selectedIcdFile.value,
request: payload
})
: await updateIcdPathWithFileApi({
icdFile: selectedIcdFile.value,
request: {
...payload,
id: formModel.id
}
})
} else {
response =
props.mode === 'create'
? await createIcdPathApi(payload)
: await updateIcdPathApi({
...payload,
id: formModel.id
})
}
const saved = unwrapApiPayload<boolean>(response)
if (saved === false) {
ElMessage.warning('ICD 记录保存未返回成功')
return
}
ElMessage.success(props.mode === 'create' ? 'ICD 记录新增成功' : 'ICD 记录编辑成功')
emit('saved')
handleClose()
} catch (error) {
ElMessage.error(getErrorMessage(error))
} finally {
saving.value = false
}
}
</script>
<style scoped lang="scss">
.icd-path-form-dialog__body {
min-height: 0;
padding: 4px 0 0;
}
:deep(.icd-check-dialog .el-dialog__body) {
padding: 16px 20px 4px;
}
:deep(.el-input-number),
:deep(.el-select) {
width: 100%;
}
</style>

View File

@@ -6,6 +6,7 @@
<slot name="actions" />
<el-button type="primary" plain size="small" :disabled="!rootNode" @click="expandAll">全部展开</el-button>
<el-button plain size="small" :disabled="!rootNode" @click="collapseAll">全部收起</el-button>
<slot name="trailing-actions" />
</div>
</div>
@@ -169,7 +170,11 @@ function collapseAll() {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: 8px;
gap: 12px;
}
.json-tree-actions :deep(.el-button + .el-button) {
margin-left: 0;
}
.json-tree-body,

View File

@@ -1,7 +1,7 @@
<template>
<el-dialog
:model-value="visible"
title="人工索引配置"
title="索引配置"
width="960px"
destroy-on-close
top="6vh"
@@ -421,7 +421,7 @@ const handleConfirm = () => {
display: flex;
flex-direction: column;
gap: 16px;
max-height: 68vh;
max-height: 58vh;
padding-right: 4px;
overflow: auto;
}

View File

@@ -0,0 +1,45 @@
/* eslint-env node */
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(currentDir, '../../../..')
const files = {
page: path.resolve(rootDir, 'views/tools/mmsMapping/index.vue'),
checkDialog: path.resolve(rootDir, 'views/tools/mmsMapping/components/IcdPathCheckDialog.vue'),
flow: path.resolve(rootDir, 'views/tools/mmsMapping/utils/useMmsMappingFlow.ts')
}
const read = file => fs.readFileSync(file, 'utf8')
const pageSource = read(files.page)
const checkDialogSource = read(files.checkDialog)
const flowSource = read(files.flow)
const checks = [
['flow composable exports useMmsMappingFlow', () => /export\s+const\s+useMmsMappingFlow/.test(flowSource)],
['mapping page hosts ICD path check dialog', () => /<IcdPathCheckDialog/.test(pageSource)],
['ICD path check dialog imports useMmsMappingFlow', () => /import\s+\{\s*useMmsMappingFlow\s*\}\s+from\s+'\.{2}\/utils\/useMmsMappingFlow'/.test(checkDialogSource)],
[
'ICD path check dialog renders core panels',
() =>
/<MappingRequestPanel/.test(checkDialogSource) &&
/<MappingConfigPanel/.test(checkDialogSource) &&
/<MappingResultPanel/.test(checkDialogSource) &&
/<MappingConfirmDialog/.test(checkDialogSource)
],
['flow still saves ICD check result through shared API', () => /saveIcdCheckResultApi/.test(flowSource)],
['flow supports saved callback for dialog host', () => /onIcdCheckSaved/.test(flowSource) && /options\.onIcdCheckSaved\?\.\(\)/.test(flowSource)]
]
const failures = checks.filter(([, check]) => !check()).map(([name]) => name)
if (failures.length) {
console.error('mmsMapping flow contract failed:')
for (const failure of failures) {
console.error(`- ${failure}`)
}
process.exit(1)
}
console.log('mmsMapping flow contract passed')

View File

@@ -0,0 +1,116 @@
/* eslint-env node */
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(currentDir, '../../../..')
const files = {
page: path.resolve(rootDir, 'views/tools/mmsMapping/index.vue'),
formDialog: path.resolve(rootDir, 'views/tools/mmsMapping/components/IcdPathFormDialog.vue'),
checkDialog: path.resolve(rootDir, 'views/tools/mmsMapping/components/IcdPathCheckDialog.vue'),
requestPanel: path.resolve(rootDir, 'views/tools/mmsMapping/components/MappingRequestPanel.vue'),
resultPanel: path.resolve(rootDir, 'views/tools/mmsMapping/components/MappingResultPanel.vue'),
api: path.resolve(rootDir, 'api/tools/mmsmapping/index.ts'),
types: path.resolve(rootDir, 'api/tools/mmsmapping/interface/index.ts'),
flow: path.resolve(rootDir, 'views/tools/mmsMapping/utils/useMmsMappingFlow.ts')
}
const read = file => fs.readFileSync(file, 'utf8')
const exists = file => fs.existsSync(file)
const pageSource = read(files.page)
const apiSource = read(files.api)
const typeSource = read(files.types)
const flowSource = read(files.flow)
const formDialogSource = exists(files.formDialog) ? read(files.formDialog) : ''
const checkDialogSource = exists(files.checkDialog) ? read(files.checkDialog) : ''
const requestPanelSource = exists(files.requestPanel) ? read(files.requestPanel) : ''
const resultPanelSource = exists(files.resultPanel) ? read(files.resultPanel) : ''
const findButtonBlock = (source, label) => {
let labelIndex = source.indexOf(label)
while (labelIndex >= 0) {
const startIndex = source.lastIndexOf('<el-button', labelIndex)
const endIndex = source.indexOf('</el-button>', labelIndex)
if (startIndex >= 0 && endIndex >= 0) {
return source.slice(startIndex, endIndex + '</el-button>'.length)
}
labelIndex = source.indexOf(label, labelIndex + label.length)
}
return ''
}
const isPrimarySolidButton = (source, label) => {
const block = findButtonBlock(source, label)
return block.includes('type="primary"') && !/\splain(\s|>|$)/.test(block)
}
const checks = [
['mmsMapping page imports shared ProTable', () => /import\s+ProTable\s+from\s+'@\/components\/ProTable\/index\.vue'/.test(pageSource)],
['mmsMapping page uses ProTable request API', () => /<ProTable[\s\S]*ref="proTable"[\s\S]*:request-api="getTableList"/.test(pageSource)],
['mmsMapping table keeps default refresh and column setting tools', () => /:tool-button="\['refresh', 'setting'\]"/.test(pageSource)],
['mmsMapping table header exposes create and batch delete', () => /<template\s+#tableHeader="scope">[\s\S]*openCreateDialog[\s\S]*handleBatchDelete\(scope\.selectedListIds\)/.test(pageSource)],
['mmsMapping row actions expose edit check and delete', () => /<template\s+#operation="\{\s*row\s*\}">[\s\S]*openEditDialog\(row\)[\s\S]*handleIcdCheck\(row\)[\s\S]*handleDeleteIcdPath\(row\)/.test(pageSource)],
['mmsMapping uses ICD path form dialog', () => /import\s+IcdPathFormDialog\s+from\s+'\.\/components\/IcdPathFormDialog\.vue'/.test(pageSource) && /<IcdPathFormDialog[\s\S]*@saved="refreshIcdPaths"/.test(pageSource)],
['mmsMapping uses ICD path check dialog', () => /import\s+IcdPathCheckDialog\s+from\s+'\.\/components\/IcdPathCheckDialog\.vue'/.test(pageSource) && /<IcdPathCheckDialog[\s\S]*@saved="refreshIcdPaths"/.test(pageSource)],
[
'ICD path APIs are registered',
() =>
/listIcdPathsApi/.test(apiSource) &&
/createIcdPathApi/.test(apiSource) &&
/updateIcdPathApi/.test(apiSource) &&
/createIcdPathWithFileApi/.test(apiSource) &&
/updateIcdPathWithFileApi/.test(apiSource) &&
/deleteIcdPathsApi/.test(apiSource) &&
/saveIcdPathCheckResultApi/.test(apiSource)
],
['ICD path APIs use documented mms-mapping endpoints', () => /\/api\/mms-mapping\/icd-paths\/list/.test(apiSource) && /\/api\/mms-mapping\/icd-paths\/add/.test(apiSource) && /\/api\/mms-mapping\/icd-paths\/update/.test(apiSource) && /\/api\/mms-mapping\/icd-paths\/delete/.test(apiSource) && /\/api\/mms-mapping\/icd-paths\/\$\{id\}\/icd-check-result/.test(apiSource)],
['ICD path file save APIs submit icdFile and JSON request parts', () => /formData\.append\('icdFile',\s*icdFile\)/.test(apiSource) && /formData\.append\('request',\s*new Blob\(\[JSON\.stringify\(request\)\],\s*\{\s*type:\s*'application\/json'\s*\}\)\)/.test(apiSource)],
['ICD JSON consistency API uses backend comparison endpoint', () => /checkIcdJsonConsistencyApi/.test(apiSource) && /\/api\/mms-mapping\/check-icd-json-consistency/.test(apiSource) && /interface\s+IcdJsonConsistencyCheckRequest/.test(typeSource) && /interface\s+IcdJsonConsistencyCheckResponse/.test(typeSource)],
['ICD path types are defined', () => /interface\s+IcdPathRecord/.test(typeSource) && /interface\s+CreateIcdPathRequest/.test(typeSource) && /interface\s+UpdateIcdPathRequest/.test(typeSource)],
['ICD path type options are fixed business enum values', () => /icdTypeOptions\s*=\s*\[[\s\S]*手动录入的标准[\s\S]*value:\s*1[\s\S]*手动录入的非标准[\s\S]*value:\s*2[\s\S]*上游解析传递[\s\S]*value:\s*3/.test(pageSource) && /icdTypeOptions\s*=\s*\[[\s\S]*手动录入的标准[\s\S]*value:\s*1[\s\S]*手动录入的非标准[\s\S]*value:\s*2[\s\S]*上游解析传递[\s\S]*value:\s*3/.test(formDialogSource)],
['ICD path type renders readable text in table', () => /getIcdTypeText\(scope\.row\.type\)/.test(pageSource)],
['ICD path type search uses select options', () => /prop:\s*'type'[\s\S]*enum:\s*icdTypeOptions[\s\S]*search:\s*\{[\s\S]*el:\s*'select'/.test(pageSource)],
['ICD path form uses select for type', () => /<el-select\s+v-model="formModel\.type"[\s\S]*<el-option[\s\S]*v-for="option in icdTypeOptions"/.test(formDialogSource)],
['new ICD path defaults to upstream parsed type', () => /type:\s*3/.test(formDialogSource) && /formModel\.type\s*=\s*3/.test(formDialogSource)],
['ICD dialog action buttons match parse ICD primary style', () => isPrimarySolidButton(requestPanelSource, '选择 ICD') && isPrimarySolidButton(resultPanelSource, 'ICD校验') && isPrimarySolidButton(resultPanelSource, 'saveIcdCheckResultText')],
[
'ICD table activation column calls activation handler before operation',
() =>
/prop:\s*'activation'[\s\S]*label:\s*'激活'[\s\S]*render:\s*scope\s*=>\s*renderActivationStatus\(scope\.row\)[\s\S]*prop:\s*'operation'/.test(
pageSource
) && /renderActivationStatus[\s\S]*handleActivateIcdPath\(row\)/.test(pageSource)
],
['ICD activation updates current record to standard type', () => /handleActivateIcdPath/.test(pageSource) && /type:\s*1/.test(pageSource) && /updateIcdPathApi/.test(pageSource)],
['ICD path form supports multipart file save', () => /<el-upload[\s\S]*:auto-upload="false"[\s\S]*handleIcdFileChange/.test(formDialogSource) && /createIcdPathWithFileApi/.test(formDialogSource) && /updateIcdPathWithFileApi/.test(formDialogSource)],
['ICD check dialog receives ICD type', () => /:icd-path-type="currentIcdCheckRecord\.type"/.test(pageSource) && /icdPathType:\s*number/.test(checkDialogSource)],
['ICD check dialog receives active record JSON as standard mapping', () => /:active-icd-path-record="activeIcdPathRecord"/.test(pageSource) && /activeIcdPathRecord:\s*MmsMapping\.IcdPathRecord\s*\|\s*null/.test(checkDialogSource) && /standardMappingJson/.test(flowSource)],
[
'standard and upstream ICD types show consistency check action',
() =>
/showConsistencyCheck/.test(checkDialogSource) &&
/props\.icdPathType\s*===\s*1[\s\S]*props\.icdPathType\s*===\s*3/.test(checkDialogSource) &&
/:show-icd-check-action="showConsistencyCheck"/.test(checkDialogSource)
],
['ICD check action calls backend consistency check before save', () => /@icd-check="handleIcdConsistencyCheck"/.test(checkDialogSource) && /handleIcdConsistencyCheck/.test(flowSource) && /checkIcdJsonConsistencyApi/.test(flowSource) && /lastIcdConsistencyCheckResult/.test(flowSource)],
['flow supports injected ICD check save handler', () => /saveIcdCheckResult\?:/.test(flowSource) && /options\.saveIcdCheckResult/.test(flowSource)],
['form dialog follows ICD check dialog visual shell', () => /class="icd-check-dialog"/.test(formDialogSource) && /class="icd-check-dialog__body"/.test(formDialogSource)],
['check dialog reuses shared MMS mapping flow panels', () => /<MappingRequestPanel/.test(checkDialogSource) && /<MappingConfigPanel/.test(checkDialogSource) && /<MappingResultPanel/.test(checkDialogSource) && /<MappingConfirmDialog/.test(checkDialogSource)]
]
const failures = checks.filter(([, check]) => !check()).map(([name]) => name)
if (failures.length) {
console.error('mmsMapping ICD path page contract failed:')
for (const failure of failures) {
console.error(`- ${failure}`)
}
process.exit(1)
}
console.log('mmsMapping ICD path page contract passed')

View File

@@ -0,0 +1,35 @@
/* eslint-env node */
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(currentDir, '../../../..')
const panelFiles = [
path.resolve(rootDir, 'views/tools/mmsMapping/components/MappingRequestPanel.vue'),
path.resolve(rootDir, 'views/tools/mmsMapping/components/MappingConfigPanel.vue'),
path.resolve(rootDir, 'views/tools/mmsMapping/components/MappingResultPanel.vue')
]
const checks = panelFiles.flatMap(file => {
const source = fs.readFileSync(file, 'utf8')
const name = path.basename(file)
return [
[`${name} uses tab-style panel title`, () => /class="panel-title-tabs"[\s\S]*class="panel-title-tab"/.test(source)],
[`${name} does not keep the old h2 panel title`, () => !/<h2\s+class="panel-title">/.test(source)],
[`${name} keeps title underline styling local`, () => /\.panel-title-tabs[\s\S]*\.panel-title-tab/.test(source)]
]
})
const failures = checks.filter(([, check]) => !check()).map(([name]) => name)
if (failures.length) {
console.error('mmsMapping panel title tabs contract failed:')
for (const failure of failures) {
console.error(`- ${failure}`)
}
process.exit(1)
}
console.log('mmsMapping panel title tabs contract passed')

View File

@@ -1,287 +0,0 @@
<template>
<div class="table-box mms-device-type-page">
<section class="table-main card mms-device-type-card">
<div class="table-header">
<div class="header-button-lf">
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增设备类型</el-button>
</div>
<div class="header-button-ri">
<el-button circle :icon="Refresh" :loading="loading" @click="loadDeviceTypes" />
</div>
</div>
<div class="device-type-table-body">
<el-table v-loading="loading" :data="deviceTypes" border stripe height="100%">
<el-table-column
prop="name"
label="设备类型名称"
min-width="180"
fixed="left"
show-overflow-tooltip
/>
<el-table-column prop="icdName" label="ICD 名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="icdPath" label="ICD 路径" min-width="220" show-overflow-tooltip />
<el-table-column prop="reportName" label="报告模板" min-width="160" show-overflow-tooltip />
<el-table-column label="ICD 校验结论" min-width="150" align="center">
<template #default="{ row }">
<el-tag :type="getIcdResultTagType(row.icdResult)" effect="light">
{{ getIcdResultText(row.icdResult) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="icdMsg" label="结论描述" min-width="220" show-overflow-tooltip />
<el-table-column label="操作" width="230" fixed="right" align="center">
<template #default="{ row }">
<el-button
link
type="primary"
:icon="Connection"
:disabled="!row.canCheckIcd"
@click="handleIcdCheck(row)"
>
ICD一致性校验
</el-button>
<el-button
link
type="primary"
:icon="DocumentChecked"
:loading="pqdifCheckingId === row.id"
:disabled="!row.canCheckPqdif"
@click="handlePqdifCheck(row)"
>
PQDIF校验
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</section>
<el-dialog v-model="createDialogVisible" title="新增设备类型" width="520px" destroy-on-close>
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="110px">
<el-form-item label="设备类型名称" prop="name">
<el-input v-model="createForm.name" maxlength="80" clearable placeholder="请输入设备类型名称" />
</el-form-item>
<el-form-item label="ICD ID">
<el-input v-model="createForm.icdId" maxlength="80" clearable placeholder="可选,关联 ICD ID" />
</el-form-item>
<el-form-item label="ICD 名称">
<el-input v-model="createForm.icdName" maxlength="120" clearable placeholder="可选ICD 名称" />
</el-form-item>
<el-form-item label="ICD 路径">
<el-input v-model="createForm.icdPath" maxlength="260" clearable placeholder="可选ICD 存储路径" />
</el-form-item>
<el-form-item label="报告模板">
<el-input
v-model="createForm.reportName"
maxlength="120"
clearable
placeholder="可选,报告模板名称"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="createDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="creating" @click="handleCreateDeviceType">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { Connection, DocumentChecked, Plus, Refresh } from '@element-plus/icons-vue'
import { ElMessage, type FormInstance, type FormRules, type TagProps } from 'element-plus'
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import type { ResultData } from '@/api/interface'
import { createDeviceTypeApi, listDeviceTypesApi, pqdifCheckApi } from '@/api/tools/mmsmapping'
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
defineOptions({
name: 'MmsDeviceTypesView'
})
type TagType = TagProps['type']
const router = useRouter()
const loading = ref(false)
const creating = ref(false)
const pqdifCheckingId = ref('')
const createDialogVisible = ref(false)
const createFormRef = ref<FormInstance>()
const deviceTypes = ref<MmsMapping.DeviceType[]>([])
const createForm = reactive<MmsMapping.CreateDeviceTypeRequest>({
name: '',
icdId: '',
icdName: '',
icdPath: '',
reportName: ''
})
const createRules: FormRules<MmsMapping.CreateDeviceTypeRequest> = {
name: [{ required: true, message: '请输入设备类型名称', trigger: 'blur' }]
}
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 && error.message) return error.message
return '接口调用失败,请检查后端服务和请求参数'
}
const trimOptionalText = (value?: string) => {
const text = value?.trim()
return text || undefined
}
const resetCreateForm = () => {
createForm.name = ''
createForm.icdId = ''
createForm.icdName = ''
createForm.icdPath = ''
createForm.reportName = ''
createFormRef.value?.clearValidate()
}
const openCreateDialog = () => {
resetCreateForm()
createDialogVisible.value = true
}
const getIcdResultText = (value?: number) => {
if (value === 1) return '一致'
if (value === 0) return '不一致'
return '未校验'
}
const getIcdResultTagType = (value?: number): TagType => {
if (value === 1) return 'success'
if (value === 0) return 'danger'
return 'info'
}
const loadDeviceTypes = async () => {
loading.value = true
try {
const response = await listDeviceTypesApi()
deviceTypes.value = unwrapApiPayload<MmsMapping.DeviceType[]>(response) || []
} catch (error) {
deviceTypes.value = []
ElMessage.error(getErrorMessage(error))
} finally {
loading.value = false
}
}
const handleCreateDeviceType = async () => {
const valid = await createFormRef.value?.validate().catch(() => false)
if (!valid) return
creating.value = true
try {
await createDeviceTypeApi({
name: createForm.name.trim(),
icdId: trimOptionalText(createForm.icdId),
icdName: trimOptionalText(createForm.icdName),
icdPath: trimOptionalText(createForm.icdPath),
reportName: trimOptionalText(createForm.reportName)
})
createDialogVisible.value = false
ElMessage.success('设备类型新增成功')
await loadDeviceTypes()
} catch (error) {
ElMessage.error(getErrorMessage(error))
} finally {
creating.value = false
}
}
const handleIcdCheck = async (row: MmsMapping.DeviceType) => {
if (!row.id) {
ElMessage.warning('当前设备类型缺少 ID不能执行 ICD 一致性校验')
return
}
// 关键业务节点设备类型页只负责传递校验上下文ICD 文件选择、索引确认和映射生成复用现有 MMS 映射页流程。
await router.push({
path: '/tools/mmsMapping',
query: {
deviceTypeId: row.id,
deviceTypeName: row.name || '',
fromDeviceTypeCheck: '1'
}
})
}
const handlePqdifCheck = async (row: MmsMapping.DeviceType) => {
if (!row.id) {
ElMessage.warning('当前设备类型缺少 ID不能执行 PQDIF 校验')
return
}
pqdifCheckingId.value = row.id
try {
const response = await pqdifCheckApi(row.id)
const payload = unwrapApiPayload<MmsMapping.PqdifCheckPlaceholder>(response)
ElMessage.info(payload?.message || 'PQDIF校验功能待实现')
} catch (error) {
ElMessage.error(getErrorMessage(error))
} finally {
pqdifCheckingId.value = ''
}
}
onMounted(() => {
loadDeviceTypes()
})
</script>
<style scoped lang="scss">
.mms-device-type-page {
overflow: hidden;
}
.mms-device-type-card {
padding: 16px;
overflow: hidden;
}
.table-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
flex: none;
}
.header-button-lf,
.header-button-ri {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.device-type-table-body {
display: flex;
flex: 1;
min-height: 0;
margin-top: 12px;
overflow: hidden;
}
.device-type-table-body :deep(.el-table__inner-wrapper) {
height: 100%;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,788 @@
import { computed, ref, toValue, type MaybeRefOrGetter } from 'vue'
import { ElMessage } from 'element-plus'
import type { ResultData } from '@/api/interface'
import {
buildIndexConfirmDataApi,
buildIndexSelectionApi,
checkIcdJsonConsistencyApi,
getIcdApi,
getIcdMmsJsonApi,
getXmlFromJsonApi,
saveIcdCheckResultApi
} from '@/api/tools/mmsmapping'
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
import { formatIndexSelectionJson, parseIndexSelectionJson } from './indexSelection'
import { createBaseRequestPayload } from './requestPayload'
type ResultTab = 'json' | 'xml' | 'problem'
type TagType = 'success' | 'warning' | 'info' | 'primary' | 'danger'
type ExportMappingType = 'json' | 'xml'
const DEFAULT_REQUEST_FORM: MmsMapping.BaseRequestForm = {
version: '1.0',
author: 'system'
}
export interface UseMmsMappingFlowOptions {
deviceTypeCheckId?: MaybeRefOrGetter<string>
deviceTypeCheckName?: MaybeRefOrGetter<string>
standardMappingJson?: MaybeRefOrGetter<string>
standardMappingName?: MaybeRefOrGetter<string>
saveIcdCheckResult?: (params: MmsMapping.SaveIcdCheckResultRequest) => Promise<ResultData<boolean> | boolean>
onIcdCheckSaved?: () => void
}
export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
const selectedIcdFile = ref<File | null>(null)
const responsePayload = ref<MmsMapping.MappingTaskResponse | null>(null)
const xmlResponsePayload = ref<MmsMapping.MappingTaskResponse | null>(null)
const activeResultTab = ref<ResultTab>('json')
const parsedCandidates = ref<MmsMapping.IndexCandidateGroup[]>([])
const confirmData = ref<MmsMapping.IndexConfirmGroup[]>([])
const indexSelectionJsonText = ref('')
const confirmDialogVisible = ref(false)
const isParsing = ref(false)
const isConfirmingSelection = ref(false)
const isGenerating = ref(false)
const isGeneratingXml = ref(false)
const isSavingIcdCheckResult = ref(false)
const lastIcdConsistencyCheckResult = ref<MmsMapping.IcdJsonConsistencyCheckResponse | null>(null)
const hasSequenceConfigured = ref(false)
const sequenceDialogVisible = 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 && error.message) return error.message
return '接口调用失败,请检查后端服务和请求参数'
}
const logConfirmDataDiagnostics = (groups: MmsMapping.IndexConfirmGroup[]) => {
const interharmonicGroups = groups
.map(group => ({
groupKey: group.groupKey?.trim() || '',
groupDesc: group.groupDesc?.trim() || '',
labelItems: (group.labelItems || [])
.map(item => ({
label: item.label?.trim() || '',
defaultLnInst: item.defaultLnInst?.trim() || '',
commonLnInstValues: item.commonLnInstValues || [],
targets: (item.targets || []).map(target => ({
reportName: target.reportName?.trim() || '',
dataSetName: target.dataSetName?.trim() || '',
availableLnInstValues: target.availableLnInstValues || []
}))
}))
.filter(item =>
[item.label, ...item.commonLnInstValues, ...item.targets.map(target => target.dataSetName)].some(
value => String(value || '').includes('间谐波')
)
)
}))
.filter(group => group.labelItems.length)
// 关键业务节点:人工确认弹窗是否缺少某个 lnInst首先取决于 build-index-confirm-data 的返回内容,这里保留诊断日志便于核对接口是否漏数。
console.info('[mmsMapping] build-index-confirm-data result', {
groupCount: groups.length,
interharmonicGroups
})
const missingFiveItems = interharmonicGroups.flatMap(group =>
group.labelItems
.filter(item => item.commonLnInstValues.length && !item.commonLnInstValues.includes('5'))
.map(item => ({
groupKey: group.groupKey,
label: item.label,
defaultLnInst: item.defaultLnInst,
commonLnInstValues: item.commonLnInstValues
}))
)
if (missingFiveItems.length) {
console.warn('[mmsMapping] interharmonic lnInst missing "5"', missingFiveItems)
}
}
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: ''
}
}
try {
return {
value: parseIndexSelectionJson(source),
error: ''
}
} catch (error) {
return {
value: [] as MmsMapping.IndexSelectionGroup[],
error: getErrorMessage(error)
}
}
})
const isSubmitting = computed(
() =>
isParsing.value ||
isConfirmingSelection.value ||
isGenerating.value ||
isGeneratingXml.value ||
isSavingIcdCheckResult.value
)
const canResetPage = computed(() =>
Boolean(
selectedIcdFile.value ||
responsePayload.value ||
xmlResponsePayload.value ||
confirmData.value.length ||
indexSelectionJsonText.value.trim()
)
)
const indexSelectionError = computed(() => parsedIndexSelectionState.value.error)
const canParseIcd = computed(() => Boolean(selectedIcdFile.value && !isSubmitting.value))
const canGenerate = computed(() =>
Boolean(
selectedIcdFile.value &&
indexSelectionJsonText.value.trim() &&
!indexSelectionError.value &&
!isSubmitting.value
)
)
// 关键业务节点:请求配置区只在用户已经选择 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 候选数据并准备人工确认,请稍候。'
if (isConfirmingSelection.value) return '正在根据索引配置生成索引配置,请稍候。'
if (confirmDialogVisible.value || confirmData.value.length) {
return '请先在弹窗中完成索引配置,确认后会自动回填索引配置。'
}
if (selectedIcdFile.value) return '已选择 ICD 文件,请先点击“解析 ICD”进入人工确认流程。'
return '当前 ICD 暂未生成可编辑的索引配置。'
})
const requestStatusText = computed(() => {
if (isParsing.value) return '解析中'
if (isConfirmingSelection.value) return '确认中'
if (isGeneratingXml.value) return 'XML转换中'
if (isGenerating.value) 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<TagType>(() => {
if (isParsing.value || isConfirmingSelection.value || isGenerating.value || isGeneratingXml.value) return 'warning'
if (confirmDialogVisible.value) return 'primary'
if (selectedIcdFile.value && indexSelectionJsonText.value.trim()) return 'success'
if (selectedIcdFile.value) return 'primary'
return 'info'
})
const responseStatusText = computed(() => {
if (isGenerating.value) return 'JSON生成中'
if (isGeneratingXml.value) return 'XML生成中'
if (isParsing.value || isConfirmingSelection.value) return '处理中'
if (xmlResponsePayload.value?.status === 'FAILED') return 'XML失败'
if (responsePayload.value?.status === 'FAILED') return '失败'
if (responsePayload.value?.status === 'NEED_INDEX_SELECTION') return '待配置'
if (xmlResponsePayload.value) return 'XML已生成'
if (mappingJsonPreview.value) return 'JSON已生成'
if (responsePayload.value) return '已解析'
return '未生成'
})
const responseStatusTagType = computed<TagType>(() => {
if (isSubmitting.value) return 'warning'
if (xmlResponsePayload.value?.status === 'FAILED') return 'danger'
if (responsePayload.value?.status === 'FAILED') return 'danger'
if (responsePayload.value?.status === 'NEED_INDEX_SELECTION') return 'warning'
if (xmlResponsePayload.value || responsePayload.value) return 'success'
return 'info'
})
const hasParsedIcd = computed(() => Boolean(responsePayload.value && responsePayload.value.status !== 'FAILED'))
const hasIndexSelection = computed(() => Boolean(indexSelectionJsonText.value.trim() && !indexSelectionError.value))
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} 字符`
})
// 关键业务节点getXmlFromJson 标准响应把 XML 文本放在 xmlFile.content旧字段仅保留为兼容兜底。
const hasJsonMapping = computed(() => Boolean(mappingJsonPreview.value))
const xmlContentForExport = computed(
() =>
xmlResponsePayload.value?.xmlFile?.content?.trim() ||
xmlResponsePayload.value?.mappingXml?.trim() ||
xmlResponsePayload.value?.xmlContent?.trim() ||
xmlResponsePayload.value?.xmlText?.trim() ||
''
)
const canConfigureSequence = computed(() => Boolean(mappingJsonPreview.value && !isSubmitting.value))
const canExportJsonMapping = computed(() => Boolean(xmlContentForExport.value && !isSubmitting.value))
const canExportXmlMapping = computed(() => Boolean(xmlContentForExport.value && !isSubmitting.value))
const canGenerateXmlMapping = computed(() => Boolean(mappingJsonPreview.value && hasSequenceConfigured.value && !isSubmitting.value))
const showXmlMappingTab = computed(() =>
Boolean(xmlResponsePayload.value && xmlResponsePayload.value.status !== 'FAILED')
)
const deviceTypeCheckId = computed(() => toValue(options.deviceTypeCheckId)?.trim() || '')
const standardMappingJson = computed(() => toValue(options.standardMappingJson)?.trim() || '')
const standardMappingName = computed(() => toValue(options.standardMappingName)?.trim() || '已激活ICD记录')
const showSaveIcdCheckResult = computed(() => Boolean(deviceTypeCheckId.value))
const canSaveIcdCheckResult = computed(() =>
Boolean(deviceTypeCheckId.value && xmlContentForExport.value && !isSubmitting.value)
)
const hasIcdConsistencyCheckResult = computed(() => Boolean(lastIcdConsistencyCheckResult.value))
const saveIcdCheckResultText = computed(() => '保存')
const xmlMappingPreview = computed(() => {
const source = xmlContentForExport.value
if (source) return source
const savedPath = xmlResponsePayload.value?.savedPath?.trim()
if (savedPath) return `XML 文件已生成:\n${savedPath}`
return ''
})
const hasXmlMapping = computed(() => Boolean(xmlMappingPreview.value))
const xmlMetaText = computed(() => {
const xmlFile = xmlResponsePayload.value?.xmlFile
if (xmlFile?.fileName) {
const encoding = xmlFile.encoding?.trim()
const contentType = xmlFile.contentType?.trim()
const suffixParts = [encoding, contentType].filter(Boolean)
return suffixParts.length ? `${xmlFile.fileName}${suffixParts.join('')}` : xmlFile.fileName
}
if (xmlResponsePayload.value?.savedPath) return `XML 文件路径:${xmlResponsePayload.value.savedPath}`
if (xmlMappingPreview.value) return `XML映射 ${xmlMappingPreview.value.length} 字符`
return '当前未生成 XML 映射'
})
const xmlEmptyText = computed(() => {
if (isGeneratingXml.value) return '正在根据 JSON 映射生成 XML 映射'
if (xmlResponsePayload.value && xmlResponsePayload.value.status !== 'FAILED') {
return '当前接口返回未包含 xmlFile.content'
}
if (mappingJsonPreview.value) return '当前接口返回未包含 XML 内容或文件路径'
return '请先生成 JSON 映射'
})
const problemList = computed(() => [
...(responsePayload.value?.problems?.filter(Boolean) || []),
...(xmlResponsePayload.value?.problems?.filter(Boolean) || []),
...(lastIcdConsistencyCheckResult.value?.issues?.filter(Boolean) || [])
])
const methodDescribe = computed(() => xmlResponsePayload.value?.methodDescribe?.trim() || '')
const problemTabLabel = computed(() => {
if (!problemList.value.length) return '问题列表'
return `问题列表(${problemList.value.length}`
})
const resolveResultTab = (payload: MmsMapping.MappingTaskResponse | null): ResultTab => {
if (payload?.mappingJson?.trim()) return 'json'
if (payload?.problems?.filter(Boolean).length) return 'problem'
return 'json'
}
const handleGenerateXmlMapping = async () => {
const mappingJson = responsePayload.value?.mappingJson?.trim()
if (!mappingJson) {
ElMessage.warning('请先生成 JSON 映射')
return
}
isGeneratingXml.value = true
activeResultTab.value = 'json'
xmlResponsePayload.value = null
try {
// 关键业务节点XML 映射依赖本次接口返回的完整 mappingJson避免使用旧结果生成不一致的 XML 文件。
const response = await getXmlFromJsonApi({
request: {
mappingJson
}
})
const payload = unwrapApiPayload<MmsMapping.MappingTaskResponse>(response)
xmlResponsePayload.value = payload
if (payload.status === 'FAILED') {
ElMessage.warning(payload.message || 'XML 映射生成失败')
activeResultTab.value = payload.problems?.filter(Boolean).length ? 'problem' : 'json'
return
}
activeResultTab.value = 'xml'
ElMessage.success(payload.message || 'XML 映射生成完成')
} catch (error) {
xmlResponsePayload.value = {
status: 'FAILED',
message: getErrorMessage(error),
problems: [getErrorMessage(error)]
}
activeResultTab.value = 'problem'
ElMessage.warning(getErrorMessage(error))
} finally {
isGeneratingXml.value = false
}
}
const stripProblemsFromIcdPayload = (payload: MmsMapping.MappingTaskResponse): MmsMapping.MappingTaskResponse => {
// 关键业务节点:解析 ICD 阶段只消费候选数据和文档结构,不把后端问题列表直接绑定到结果区。
const sanitizedPayload = { ...payload }
delete sanitizedPayload.problems
return sanitizedPayload
}
const resetParsedState = () => {
responsePayload.value = null
xmlResponsePayload.value = null
lastIcdConsistencyCheckResult.value = null
hasSequenceConfigured.value = false
sequenceDialogVisible.value = false
parsedCandidates.value = []
confirmData.value = []
indexSelectionJsonText.value = ''
confirmDialogVisible.value = false
activeResultTab.value = 'json'
}
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 文件后立即清空旧确认结果和旧请求配置,避免不同文件的索引配置串用。
selectedIcdFile.value = file
resetParsedState()
input.value = ''
}
const handleParseIcd = async () => {
if (!selectedIcdFile.value) {
ElMessage.warning('请先选择 ICD 文件')
return
}
isParsing.value = true
responsePayload.value = null
xmlResponsePayload.value = null
hasSequenceConfigured.value = false
sequenceDialogVisible.value = false
confirmDialogVisible.value = false
confirmData.value = []
indexSelectionJsonText.value = ''
try {
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 = []
ElMessage.error(payload.message || 'ICD 解析失败')
return
}
parsedCandidates.value = candidateGroups
// 关键业务节点:拿到 ICD 候选结果后必须先走 buildIndexConfirmData生成人工确认弹窗所需模型。
const confirmResponse = await buildIndexConfirmDataApi(candidateGroups)
const confirmGroups = unwrapApiPayload<MmsMapping.IndexConfirmGroup[]>(confirmResponse) || []
confirmData.value = confirmGroups
logConfirmDataDiagnostics(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))
} finally {
isParsing.value = false
}
}
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<MmsMapping.IndexSelectionGroup[]>(response) || []
// 关键业务节点:只有 buildIndexSelection 返回的正式结果才能进入请求配置区,避免前端自行拼装绑定关系。
indexSelectionJsonText.value = formatIndexSelectionJson(indexSelection)
confirmDialogVisible.value = false
ElMessage.success('索引配置完成,已回填索引配置')
} catch (error) {
ElMessage.error(getErrorMessage(error))
} finally {
isConfirmingSelection.value = false
}
}
const handleGenerateMapping = async () => {
if (!selectedIcdFile.value) {
ElMessage.warning('请先选择 ICD 文件')
return
}
if (!indexSelectionJsonText.value.trim()) {
if (confirmData.value.length) {
ElMessage.warning('请先完成索引配置并生成索引配置')
return
}
ElMessage.warning('请先解析 ICD并在弹窗中完成人工确认')
return
}
const { error, value } = parsedIndexSelectionState.value
if (error) {
responsePayload.value = {
status: 'NEED_INDEX_SELECTION',
message: '索引配置格式有误,请继续修正',
problems: [error]
}
activeResultTab.value = 'problem'
ElMessage.warning(error)
return
}
isGenerating.value = true
responsePayload.value = null
xmlResponsePayload.value = null
try {
// 关键业务节点:正式生成阶段只消费当前请求配置区里的索引配置,确保导出的映射与最终确认结果一致。
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 || '映射生成完成')
sequenceDialogVisible.value = true
} catch (error) {
responsePayload.value = null
xmlResponsePayload.value = null
lastIcdConsistencyCheckResult.value = null
ElMessage.error(getErrorMessage(error))
} finally {
isGenerating.value = false
}
}
const buildExportFileName = (type: ExportMappingType) => {
const baseFileName = selectedIcdFile.value?.name.replace(/\.[^.]+$/, '')
const suffix = `${type}-mapping.${type}`
return baseFileName ? `${baseFileName}-${suffix}` : suffix
}
const downloadTextFile = (content: string, fileName: string, mimeType: string) => {
const blob = new Blob([content], { type: mimeType })
const objectUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = objectUrl
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(objectUrl)
}
const handleExportMapping = (type: ExportMappingType) => {
if (type === 'json' && !mappingJsonPreview.value) {
ElMessage.warning('当前没有可导出的 JSON 映射')
return
}
if (type === 'xml' && !xmlContentForExport.value) {
ElMessage.warning('当前没有可导出的 XML 映射')
return
}
if (type === 'json') {
downloadTextFile(mappingJsonPreview.value, buildExportFileName('json'), 'application/json;charset=utf-8')
ElMessage.success('JSON 映射已导出')
return
}
downloadTextFile(xmlContentForExport.value, buildExportFileName('xml'), 'application/xml;charset=utf-8')
ElMessage.success('XML 映射已导出')
}
const handleIcdConsistencyCheck = async () => {
const checkedJson = responsePayload.value?.mappingJson?.trim() || mappingJsonPreview.value
if (!checkedJson) {
ElMessage.warning('请先生成 JSON 映射')
return
}
if (!standardMappingJson.value) {
ElMessage.warning('当前缺少已激活记录的 JSON 映射,不能执行 ICD 一致性校验')
return
}
isSavingIcdCheckResult.value = true
try {
// 关键业务节点ICD 一致性由后端按当前 JSON 与已激活标准 JSON 比对,前端只负责传参与展示结论。
const response = await checkIcdJsonConsistencyApi({
checkedJson,
standardJson: standardMappingJson.value,
saveToDisk: false
})
const payload = unwrapApiPayload<MmsMapping.IcdJsonConsistencyCheckResponse>(response)
lastIcdConsistencyCheckResult.value = payload
if (payload.result === 1) {
ElMessage.success(payload.message || `${standardMappingName.value}一致`)
return
}
activeResultTab.value = 'problem'
ElMessage.warning(payload.message || `${standardMappingName.value}不一致`)
} catch (error) {
lastIcdConsistencyCheckResult.value = {
result: 0,
message: getErrorMessage(error),
issues: [getErrorMessage(error)]
}
activeResultTab.value = 'problem'
ElMessage.error(getErrorMessage(error))
} finally {
isSavingIcdCheckResult.value = false
}
}
const handleSaveIcdCheckResult = async () => {
if (!deviceTypeCheckId.value) {
ElMessage.warning('当前缺少设备类型 ID不能入库校验结果')
return
}
const mappingJson = responsePayload.value?.mappingJson?.trim() || mappingJsonPreview.value
if (!mappingJson || !xmlContentForExport.value) {
ElMessage.warning('请先完成 XML 映射后再保存')
return
}
if (standardMappingJson.value && !lastIcdConsistencyCheckResult.value) {
ElMessage.warning('请先执行 ICD 一致性校验')
return
}
isSavingIcdCheckResult.value = true
try {
const checkResult = lastIcdConsistencyCheckResult.value
const result = checkResult?.result ?? 1
const msg =
checkResult?.message ||
(xmlContentForExport.value ? 'ICD一致性校验通过JSON/XML已生成' : 'ICD一致性校验通过JSON已生成')
const savePayload = {
mappingJson,
xml: xmlContentForExport.value || undefined,
result,
msg
}
// 关键业务节点ICD 校验流程可由设备类型或 ICD 存储记录承载,保存接口由宿主页注入以避免串写业务表。
const response = options.saveIcdCheckResult
? await options.saveIcdCheckResult(savePayload)
: await saveIcdCheckResultApi(deviceTypeCheckId.value, savePayload)
const saved = unwrapApiPayload<boolean>(response)
if (!saved) {
ElMessage.warning('校验结果入库未返回成功')
return
}
ElMessage.success('校验结果已入库')
options.onIcdCheckSaved?.()
} catch (error) {
ElMessage.error(getErrorMessage(error))
} finally {
isSavingIcdCheckResult.value = false
}
}
const handleUpdateMappingJson = (mappingJson: string) => {
if (!responsePayload.value) return
// 关键业务节点:序列配置修改的是当前 JSON 映射结果XML 结果需要清空,避免继续展示旧 JSON 转换出的 XML。
responsePayload.value = {
...responsePayload.value,
mappingJson
}
xmlResponsePayload.value = null
lastIcdConsistencyCheckResult.value = null
hasSequenceConfigured.value = false
activeResultTab.value = 'json'
}
const handleSequenceConfigComplete = () => {
hasSequenceConfigured.value = true
}
const resetPage = () => {
selectedIcdFile.value = null
resetParsedState()
}
return {
selectedIcdFileName,
isSubmitting,
isParsing,
icdFileAccept,
requestStatusText,
requestStatusTagType,
canParseIcd,
canResetPage,
showConfigPanel,
indexSelectionJsonText,
isGenerating,
canGenerate,
indexSelectionError,
showGenerateButton,
confirmData,
configEmptyDescription,
activeResultTab,
responseStatusText,
responseStatusTagType,
hasParsedIcd,
hasIndexSelection,
hasJsonMapping,
hasSequenceConfigured,
hasXmlMapping,
mappingMetaText,
mappingJsonPreview,
xmlMetaText,
xmlMappingPreview,
xmlEmptyText,
problemTabLabel,
problemList,
problemEmptyText,
methodDescribe,
canExportJsonMapping,
canExportXmlMapping,
canConfigureSequence,
canGenerateXmlMapping,
isGeneratingXml,
showXmlMappingTab,
sequenceDialogVisible,
showSaveIcdCheckResult,
canSaveIcdCheckResult,
isSavingIcdCheckResult,
saveIcdCheckResultText,
hasIcdConsistencyCheckResult,
confirmDialogVisible,
isConfirmingSelection,
handleIcdFileChange,
handleParseIcd,
resetPage,
handleGenerateMapping,
handleExportMapping,
handleGenerateXmlMapping,
handleIcdConsistencyCheck,
handleUpdateMappingJson,
handleSequenceConfigComplete,
handleSaveIcdCheckResult,
handleConfirmIndexSelection
}
}

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;
}