feat(steady): 重构稳态校验功能并优化界面布局
- 更新 API 接口路径从 /steady/data-view/checksquare/* 到 /steady/checksquare/* - 修改校验任务状态枚举值 FAILED 为 FAIL 并更新相关处理逻辑 - 移除缺失率和最大连续缺失分钟数字段,简化数据完整性计算 - 添加新的创建结果面板组件 ChecksquareCreateResultPanel.vue - 调整创建对话框布局,采用两行搜索控件设计 - 更新任务表头部按钮文字为"新增"并调整搜索列配置为5列 - 修改详情面板显示开始时间和结束时间字段 - 重构工作台界面布局,使用 flex 布局替代 grid 布局 - 更新设备类型相关 API 接口和数据结构定义 - 添加设备类型字典常量并更新路由配置 - 优化搜索表单展开收起逻辑的计算方式 - 调整创建流程不再轮询获取任务详情,改为直接显示摘要信息 - 更新数据完整性格式化函数参数和调用方式 - 修改创建对话框样式类名和尺寸配置 - 添加设备类型管理相关的接口定义和实现方法
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -43,6 +43,7 @@ const STATIC_ROUTE_NAMES = new Set([
|
||||
'tools',
|
||||
'toolWaveform',
|
||||
'toolMmsMapping',
|
||||
'deviceTypes',
|
||||
'toolAddData',
|
||||
'toolAddLedger',
|
||||
'eventList',
|
||||
|
||||
@@ -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: '数据验证'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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` 数组创建检测项。
|
||||
- 当前文档只覆盖现有有效接口,不包含旧的任务获取或创建兼容接口。
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
],
|
||||
[
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 '--'
|
||||
|
||||
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
605
frontend/src/views/tools/deviceTypes/index.vue
Normal file
605
frontend/src/views/tools/deviceTypes/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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
788
frontend/src/views/tools/mmsMapping/utils/useMmsMappingFlow.ts
Normal file
788
frontend/src/views/tools/mmsMapping/utils/useMmsMappingFlow.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user