refactor(steady): 重构稳态校验功能的合同验证逻辑
- 优化测量点对话框路径解析格式化 - 添加稳态校验重启API端点验证功能 - 更新创建参数类型定义支持可选lineId和lineIds数组 - 修复任务表格组件模板缩进格式问题 - 扩展任务表格列配置验证数组格式 - 添加任务表格仅对失败行显示重启操作功能 - 集成共享台账树组件发送选中叶节点监测点功能 - 添加多监测点创建参数构建支持 - 实现指标选择为空时全指标校验功能 - 添加预期项目数量计算功能 - 优化创建对话框指标选择标签防止输入框重置 - 添加任务表格树形选择滚动下拉菜单样式 - 实现页面创建流程调用创建API并轮询任务状态 - 添加创建结果面板加载状态和进度显示 - 实现页面重启流程确认调用重启API功能 - 优化汇总表格异常字段持久化支持 - 添加汇总表格监测点名称显示功能 - 优化详情面板按需加载项目详情 - 更新接口类型支持分页字段定义 - 优化ICD路径检查对话框标准映射参数传递 - 添加ICD记录导出SQL和JSON功能 - 扩展ICD路径API端点和类型定义 - 添加ICD路径参考选项和映射详情功能 - 重构ICD表格列显示移除激活和创建时间列 - 添加ICD映射详情对话框三个标签页功能 - 优化ICD映射详情JSON树形视图显示 - 更新ICD类型选项覆盖手动和上游标准状态
This commit is contained in:
@@ -35,6 +35,16 @@ export const deleteSteadyChecksquareTasks = (taskIds: SteadyDataView.SteadyCheck
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const restartSteadyChecksquareTask = (taskId: string) => {
|
||||||
|
return http.post<SteadyDataView.SteadyChecksquareTask>(
|
||||||
|
`/steady/checksquare/restart?taskId=${encodeURIComponent(taskId)}`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const getSteadyChecksquareDetail = (taskId: string) => {
|
export const getSteadyChecksquareDetail = (taskId: string) => {
|
||||||
return http.get<SteadyDataView.SteadyChecksquareQueryResult>('/steady/checksquare/detail', { taskId }, { loading: false })
|
return http.get<SteadyDataView.SteadyChecksquareQueryResult>('/steady/checksquare/detail', { taskId }, { loading: false })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ export namespace SteadyDataView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SteadyChecksquareCreateParams {
|
export interface SteadyChecksquareCreateParams {
|
||||||
lineId: string
|
lineId?: string
|
||||||
|
lineIds?: string[]
|
||||||
indicatorCodes: string[]
|
indicatorCodes: string[]
|
||||||
timeStart: string
|
timeStart: string
|
||||||
timeEnd: string
|
timeEnd: string
|
||||||
@@ -149,6 +150,8 @@ export namespace SteadyDataView {
|
|||||||
export interface SteadyChecksquareItem {
|
export interface SteadyChecksquareItem {
|
||||||
itemId?: string
|
itemId?: string
|
||||||
itemKey: string
|
itemKey: string
|
||||||
|
lineId?: string
|
||||||
|
lineName?: string
|
||||||
indicatorCode: string
|
indicatorCode: string
|
||||||
indicatorName?: string
|
indicatorName?: string
|
||||||
harmonicOrder?: number | null
|
harmonicOrder?: number | null
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export const listIcdPathsApi = (params: MmsMapping.IcdPathListRequest) => {
|
|||||||
return http.post<MmsMapping.IcdPathRecord[]>('/api/mms-mapping/icd-paths/list', params)
|
return http.post<MmsMapping.IcdPathRecord[]>('/api/mms-mapping/icd-paths/list', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const listIcdPathReferencesApi = () => {
|
||||||
|
return http.post<MmsMapping.IcdPathReferenceOption[]>('/api/mms-mapping/icd-paths/reference-list')
|
||||||
|
}
|
||||||
|
|
||||||
export const createIcdPathApi = (params: MmsMapping.CreateIcdPathRequest) => {
|
export const createIcdPathApi = (params: MmsMapping.CreateIcdPathRequest) => {
|
||||||
return http.post<boolean>('/api/mms-mapping/icd-paths/add', params)
|
return http.post<boolean>('/api/mms-mapping/icd-paths/add', params)
|
||||||
}
|
}
|
||||||
@@ -73,6 +77,14 @@ export const saveIcdPathCheckResultApi = (id: string, params: MmsMapping.SaveIcd
|
|||||||
return http.post<boolean>(`/api/mms-mapping/icd-paths/${id}/icd-check-result`, params)
|
return http.post<boolean>(`/api/mms-mapping/icd-paths/${id}/icd-check-result`, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getIcdPathCheckMsgApi = (id: string) => {
|
||||||
|
return http.post<MmsMapping.IcdPathCheckMsgResponse>(`/api/mms-mapping/icd-paths/${id}/icd-check-msg`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getIcdPathMappingDetailApi = (id: string) => {
|
||||||
|
return http.post<MmsMapping.IcdPathMappingDetailResponse>(`/api/mms-mapping/icd-paths/${id}/mapping-detail`)
|
||||||
|
}
|
||||||
|
|
||||||
export const checkIcdJsonConsistencyApi = (params: MmsMapping.IcdJsonConsistencyCheckRequest) => {
|
export const checkIcdJsonConsistencyApi = (params: MmsMapping.IcdJsonConsistencyCheckRequest) => {
|
||||||
return http.post<MmsMapping.IcdJsonConsistencyCheckResponse>('/api/mms-mapping/check-icd-json-consistency', params)
|
return http.post<MmsMapping.IcdJsonConsistencyCheckResponse>('/api/mms-mapping/check-icd-json-consistency', params)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export namespace MmsMapping {
|
|||||||
|
|
||||||
export interface IcdCheckMsg {
|
export interface IcdCheckMsg {
|
||||||
summary?: string
|
summary?: string
|
||||||
details?: string[]
|
details?: unknown[]
|
||||||
issuesJson?: string
|
issuesJson?: string
|
||||||
correctedJson?: string
|
correctedJson?: string
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
@@ -183,7 +183,6 @@ export namespace MmsMapping {
|
|||||||
export interface IcdPathRecord {
|
export interface IcdPathRecord {
|
||||||
id?: string
|
id?: string
|
||||||
name?: string
|
name?: string
|
||||||
path?: string
|
|
||||||
angle?: number
|
angle?: number
|
||||||
usePhaseIndex?: number
|
usePhaseIndex?: number
|
||||||
state?: number
|
state?: number
|
||||||
@@ -199,6 +198,19 @@ export namespace MmsMapping {
|
|||||||
updateTime?: string
|
updateTime?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IcdPathReferenceOption {
|
||||||
|
id?: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IcdPathMappingDetailResponse {
|
||||||
|
id?: string
|
||||||
|
name?: string
|
||||||
|
jsonStr?: string
|
||||||
|
xmlStr?: string
|
||||||
|
icdText?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface IcdPathListRequest {
|
export interface IcdPathListRequest {
|
||||||
keyword?: string
|
keyword?: string
|
||||||
type?: number
|
type?: number
|
||||||
@@ -207,10 +219,10 @@ export namespace MmsMapping {
|
|||||||
|
|
||||||
export interface CreateIcdPathRequest {
|
export interface CreateIcdPathRequest {
|
||||||
name: string
|
name: string
|
||||||
path: string
|
|
||||||
angle?: number
|
angle?: number
|
||||||
usePhaseIndex?: number
|
usePhaseIndex?: number
|
||||||
type?: number
|
type?: number
|
||||||
|
referenceIcdId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateIcdPathWithFileRequest {
|
export interface CreateIcdPathWithFileRequest {
|
||||||
@@ -236,6 +248,8 @@ export namespace MmsMapping {
|
|||||||
msg?: IcdCheckMsg
|
msg?: IcdCheckMsg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IcdPathCheckMsgResponse = IcdCheckMsg | null
|
||||||
|
|
||||||
export interface IcdJsonConsistencyCheckRequest {
|
export interface IcdJsonConsistencyCheckRequest {
|
||||||
checkedJson: string
|
checkedJson: string
|
||||||
standardJson: string
|
standardJson: string
|
||||||
|
|||||||
13
frontend/src/api/tools/parsePqdif/index.ts
Normal file
13
frontend/src/api/tools/parsePqdif/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import http from '@/api'
|
||||||
|
import type { ParsePqdif } from './interface'
|
||||||
|
|
||||||
|
export const parsePqdifApi = (pqdifFile: File) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
// 关键业务节点:后端 @RequestPart 固定读取 pqdifFile,字段名不能与页面变量名脱钩。
|
||||||
|
formData.append('pqdifFile', pqdifFile)
|
||||||
|
|
||||||
|
return http.post<ParsePqdif.ParseResponse>('/api/parse-pqdif/parse', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
56
frontend/src/api/tools/parsePqdif/interface/index.ts
Normal file
56
frontend/src/api/tools/parsePqdif/interface/index.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
export namespace ParsePqdif {
|
||||||
|
export type ParseStatus = 'SUCCESS' | 'FAILED' | (string & {})
|
||||||
|
export type SeriesDataStatus = 'DATA_SUCCESS' | 'DATA_FAILED' | (string & {})
|
||||||
|
|
||||||
|
export interface RecordItem {
|
||||||
|
recordIndex?: number
|
||||||
|
typeGuid?: string
|
||||||
|
typeName?: string
|
||||||
|
observation?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SeriesItem {
|
||||||
|
seriesIndex?: number
|
||||||
|
quantityUnitsId?: number
|
||||||
|
quantityCharacteristicGuid?: string
|
||||||
|
valueTypeGuid?: string
|
||||||
|
seriesBaseType?: number
|
||||||
|
scale?: number
|
||||||
|
offset?: number
|
||||||
|
dataStatus?: SeriesDataStatus
|
||||||
|
dataMessage?: string | null
|
||||||
|
valueCount?: number
|
||||||
|
firstValues?: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelItem {
|
||||||
|
channelIndex?: number
|
||||||
|
name?: string
|
||||||
|
seriesCount?: number
|
||||||
|
phaseId?: number
|
||||||
|
quantityTypeGuid?: string
|
||||||
|
quantityMeasuredId?: number
|
||||||
|
series?: SeriesItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ObservationItem {
|
||||||
|
recordIndex?: number
|
||||||
|
name?: string
|
||||||
|
timeStartExcelDays?: number
|
||||||
|
timeStartText?: string
|
||||||
|
channelCount?: number
|
||||||
|
channels?: ChannelItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParseResponse {
|
||||||
|
status?: ParseStatus
|
||||||
|
message?: string
|
||||||
|
fileName?: string | null
|
||||||
|
nativeVersion?: string | null
|
||||||
|
recordCount?: number
|
||||||
|
observationCount?: number
|
||||||
|
sampleValueCount?: number
|
||||||
|
records?: RecordItem[]
|
||||||
|
observations?: ObservationItem[]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ const STATIC_ROUTE_NAMES = new Set([
|
|||||||
'tools',
|
'tools',
|
||||||
'toolWaveform',
|
'toolWaveform',
|
||||||
'toolMmsMapping',
|
'toolMmsMapping',
|
||||||
|
'toolParsePqdif',
|
||||||
'deviceTypes',
|
'deviceTypes',
|
||||||
'toolAddData',
|
'toolAddData',
|
||||||
'toolAddLedger',
|
'toolAddLedger',
|
||||||
|
|||||||
@@ -63,6 +63,15 @@ export const staticRouter: RouteRecordRaw[] = [
|
|||||||
title: '模型映射管理'
|
title: '模型映射管理'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/tools/parsePqdif',
|
||||||
|
name: 'toolParsePqdif',
|
||||||
|
component: () => import('@/views/tools/parsePqdif/index.vue'),
|
||||||
|
meta: {
|
||||||
|
cacheName: 'ParsePqdifView',
|
||||||
|
title: 'PQDIF解析'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/tools/deviceTypes',
|
path: '/tools/deviceTypes',
|
||||||
name: 'deviceTypes',
|
name: 'deviceTypes',
|
||||||
|
|||||||
@@ -1,417 +0,0 @@
|
|||||||
# 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,621 @@
|
|||||||
|
# check-square API 调试文档
|
||||||
|
|
||||||
|
## 1. 模块说明
|
||||||
|
|
||||||
|
- 模块路径:`steady/check-square`
|
||||||
|
- 接口基础路径:`/steady/checksquare`
|
||||||
|
- 返回包装:接口统一返回 `HttpResult<T>`,调试时重点查看响应体中的业务数据字段 `data`。
|
||||||
|
- 时间格式:`yyyy-MM-dd HH:mm:ss`
|
||||||
|
|
||||||
|
本模块用于按监测点、时间范围和指标执行稳态数据校验,并提供任务列表、任务详情、检测项明细查询、失败任务重启和任务删除能力。当前标准支持单监测点和多监测点任务:单监测点可继续传 `lineId`,多监测点推荐传 `lineIds`。
|
||||||
|
|
||||||
|
## 2. 通用约定
|
||||||
|
|
||||||
|
### 2.1 请求头
|
||||||
|
|
||||||
|
| 名称 | 示例 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Content-Type` | `application/json` | `POST` 请求使用 JSON 请求体 |
|
||||||
|
| `Authorization` | 登录态 Token | 如当前环境开启认证,需携带现有登录接口返回的认证信息 |
|
||||||
|
|
||||||
|
### 2.2 任务状态
|
||||||
|
|
||||||
|
| 值 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `RUNNING` | 执行中 |
|
||||||
|
| `SUCCESS` | 执行成功 |
|
||||||
|
| `FAIL` | 执行失败 |
|
||||||
|
|
||||||
|
### 2.3 明细类型
|
||||||
|
|
||||||
|
| 值 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `SEGMENT` | 连续性区间,包含正常区间和缺失区间 |
|
||||||
|
| `VALUE_ORDER` | 指标值大小关系异常明细 |
|
||||||
|
| `HARMONIC_PARITY` | 谐波奇偶关系异常明细 |
|
||||||
|
|
||||||
|
### 2.4 统计类型
|
||||||
|
|
||||||
|
| 值 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `AVG` | 平均值 |
|
||||||
|
| `MAX` | 最大值 |
|
||||||
|
| `MIN` | 最小值 |
|
||||||
|
| `CP95` | CP95 值 |
|
||||||
|
|
||||||
|
## 3. 调试顺序建议
|
||||||
|
|
||||||
|
1. 调用 `POST /steady/checksquare/create`:按监测点列表、时间范围和指标列表创建或复用任务。
|
||||||
|
2. 读取 `/create` 返回的 `data.taskId`:该返回值是任务列表中的行信息。
|
||||||
|
3. 如果 `taskStatus=RUNNING`,轮询 `POST /steady/checksquare/query`,或稍后调用 `GET /steady/checksquare/detail` 查看任务详情。
|
||||||
|
4. 调用 `GET /steady/checksquare/detail`:用 `taskId` 查询任务下的检测项。
|
||||||
|
5. 读取 `/detail` 返回的 `items[].itemId`:按检测项继续查询连续性区间或异常明细。
|
||||||
|
6. 调用 `GET /steady/checksquare/item-detail`:按 `itemId + detailType` 查询具体明细。
|
||||||
|
7. 任务失败后如需重新执行,调用 `POST /steady/checksquare/restart`。
|
||||||
|
8. 需要清理任务时调用 `POST /steady/checksquare/delete`。
|
||||||
|
|
||||||
|
注意:旧的获取或创建兼容接口已移除。创建或复用任务的逻辑已经合并到 `/create` 中。
|
||||||
|
|
||||||
|
## 4. 创建或复用任务
|
||||||
|
|
||||||
|
### 4.1 接口
|
||||||
|
|
||||||
|
`POST /steady/checksquare/create`
|
||||||
|
|
||||||
|
### 4.2 行为说明
|
||||||
|
|
||||||
|
接口开始执行前,会先按 `lineIds + timeStart + timeEnd` 查询是否存在未删除任务:
|
||||||
|
|
||||||
|
- 已存在:直接返回该任务的任务列表行信息。
|
||||||
|
- 不存在:创建 `RUNNING` 任务并立即返回任务列表行信息,后台继续执行校验;任务完成后状态更新为 `SUCCESS`,失败时更新为 `FAIL`。
|
||||||
|
|
||||||
|
`lineIds` 会去重并清理空字符串。若未传 `lineIds`,后端会兼容读取 `lineId` 并转换为单元素监测点列表。返回对象为 `SteadyChecksquareTaskVO`。
|
||||||
|
|
||||||
|
### 4.3 请求字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `lineId` | `String` | 条件必填 | 单监测点 ID;当 `lineIds` 为空时使用 |
|
||||||
|
| `lineIds` | `Array<String>` | 条件必填 | 监测点 ID 列表;多监测点推荐使用该字段 |
|
||||||
|
| `indicatorCodes` | `Array<String>` | 是 | 指标编码列表 |
|
||||||
|
| `timeStart` | `String` | 是 | 开始时间,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||||
|
| `timeEnd` | `String` | 是 | 结束时间,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||||
|
|
||||||
|
`lineId` 与 `lineIds` 至少传一个;`indicatorCodes` 是数组,不要传成单个字符串。
|
||||||
|
|
||||||
|
### 4.4 请求示例
|
||||||
|
|
||||||
|
单监测点兼容写法:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /steady/checksquare/create
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lineId": "LINE_001",
|
||||||
|
"indicatorCodes": ["VOLTAGE_A", "CURRENT_A"],
|
||||||
|
"timeStart": "2026-06-18 00:00:00",
|
||||||
|
"timeEnd": "2026-06-18 01:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
多监测点标准写法:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lineIds": ["LINE_001", "LINE_002"],
|
||||||
|
"indicatorCodes": ["VOLTAGE_A", "CURRENT_A"],
|
||||||
|
"timeStart": "2026-06-18 00:00:00",
|
||||||
|
"timeEnd": "2026-06-18 01:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 响应字段 `data`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `taskId` | `String` | 任务 ID |
|
||||||
|
| `taskNo` | `String` | 任务编号 |
|
||||||
|
| `lineId` | `String` | 任务首个监测点 ID;兼容旧页面字段 |
|
||||||
|
| `lineIds` | `Array<String>` | 本次任务的监测点 ID 列表 |
|
||||||
|
| `lineName` | `String` | 监测点名称;多监测点时为多个名称拼接结果 |
|
||||||
|
| `timeStart` | `String` | 开始时间 |
|
||||||
|
| `timeEnd` | `String` | 结束时间 |
|
||||||
|
| `intervalMinutes` | `Integer` | 统计间隔,单位分钟 |
|
||||||
|
| `taskStatus` | `String` | 任务状态:`RUNNING`、`SUCCESS`、`FAIL` |
|
||||||
|
| `itemCount` | `Integer` | 检测项数量 |
|
||||||
|
| `abnormalItemCount` | `Integer` | 异常检测项数量 |
|
||||||
|
| `minDataIntegrity` | `BigDecimal` | 最低数据完整性,0 到 1 的小数 |
|
||||||
|
| `createTime` | `String` | 创建时间 |
|
||||||
|
|
||||||
|
### 4.6 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"taskId": "1812345678901234567",
|
||||||
|
"taskNo": "CS202606180001",
|
||||||
|
"lineId": "LINE_001",
|
||||||
|
"lineIds": ["LINE_001", "LINE_002"],
|
||||||
|
"lineName": "1号监测点,2号监测点",
|
||||||
|
"timeStart": "2026-06-18 00:00:00",
|
||||||
|
"timeEnd": "2026-06-18 01:00:00",
|
||||||
|
"intervalMinutes": 1,
|
||||||
|
"taskStatus": "RUNNING",
|
||||||
|
"itemCount": 0,
|
||||||
|
"abnormalItemCount": 0,
|
||||||
|
"minDataIntegrity": 0.000000,
|
||||||
|
"createTime": "2026-06-18 09:30:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 查询任务列表
|
||||||
|
|
||||||
|
### 5.1 接口
|
||||||
|
|
||||||
|
`POST /steady/checksquare/query`
|
||||||
|
|
||||||
|
### 5.2 请求字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `lineId` | `String` | 否 | 监测点 ID;后端按任务的 `lineIds` 检索文本匹配 |
|
||||||
|
| `indicatorCode` | `String` | 否 | 指标编码;后端按任务的指标编码检索文本匹配 |
|
||||||
|
| `timeStart` | `String` | 否 | 检测开始时间,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||||
|
| `timeEnd` | `String` | 否 | 检测结束时间,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||||
|
| `hasAbnormal` | `Boolean` | 否 | 是否存在异常;传 `true` 时仅查询异常检测项数量大于 0 的任务 |
|
||||||
|
| `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-18 00:00:00",
|
||||||
|
"timeEnd": "2026-06-18 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": "CS202606180001",
|
||||||
|
"lineId": "LINE_001",
|
||||||
|
"lineIds": ["LINE_001", "LINE_002"],
|
||||||
|
"lineName": "1号监测点,2号监测点",
|
||||||
|
"timeStart": "2026-06-18 00:00:00",
|
||||||
|
"timeEnd": "2026-06-18 01:00:00",
|
||||||
|
"intervalMinutes": 1,
|
||||||
|
"taskStatus": "SUCCESS",
|
||||||
|
"itemCount": 4,
|
||||||
|
"abnormalItemCount": 1,
|
||||||
|
"minDataIntegrity": 0.985000,
|
||||||
|
"createTime": "2026-06-18 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;兼容旧页面字段 |
|
||||||
|
| `lineIds` | `Array<String>` | 本次任务的监测点 ID 列表 |
|
||||||
|
| `lineName` | `String` | 监测点名称 |
|
||||||
|
| `timeStart` | `String` | 开始时间 |
|
||||||
|
| `timeEnd` | `String` | 结束时间 |
|
||||||
|
| `intervalMinutes` | `Integer` | 统计间隔,单位分钟 |
|
||||||
|
| `items` | `Array<Object>` | 检测项列表 |
|
||||||
|
|
||||||
|
### 6.5 检测项字段 `items[]`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `itemId` | `String` | 检测项 ID,用于查询明细 |
|
||||||
|
| `itemKey` | `String` | 检测项唯一键 |
|
||||||
|
| `lineId` | `String` | 当前检测项所属监测点 ID |
|
||||||
|
| `lineName` | `String` | 当前检测项所属监测点名称 |
|
||||||
|
| `indicatorCode` | `String` | 指标编码 |
|
||||||
|
| `indicatorName` | `String` | 指标名称 |
|
||||||
|
| `harmonicOrder` | `Integer` | 谐波次数;非谐波或聚合项可为空 |
|
||||||
|
| `intervalMinutes` | `Integer` | 当前检测项统计间隔,单位分钟 |
|
||||||
|
| `hasData` | `Boolean` | 时间范围内是否存在任意数据 |
|
||||||
|
| `expectedPointCount` | `Integer` | 期望点数 |
|
||||||
|
| `actualPointCount` | `Integer` | 实际点数 |
|
||||||
|
| `missingPointCount` | `Integer` | 缺失点数 |
|
||||||
|
| `dataIntegrity` | `BigDecimal` | 数据完整性,0 到 1 的小数 |
|
||||||
|
| `dataIntegrityText` | `String` | 数据完整性文本 |
|
||||||
|
| `abnormal` | `Boolean` | 指标值大小关系是否异常 |
|
||||||
|
| `abnormalPointCount` | `Integer` | 指标值大小关系异常点数 |
|
||||||
|
| `abnormalDetails` | `Array<Object>` | 指标值大小关系异常明细摘要 |
|
||||||
|
| `harmonicParityAbnormal` | `Boolean` | 谐波奇偶关系是否异常 |
|
||||||
|
| `harmonicParityAbnormalPointCount` | `Integer` | 谐波奇偶关系异常点数 |
|
||||||
|
| `harmonicParityAbnormalDetails` | `Array<Object>` | 谐波奇偶关系异常明细摘要 |
|
||||||
|
| `statSummaries` | `Array<Object>` | 统计类型摘要 |
|
||||||
|
| `statDetails` | `Array<Object>` | 统计类型明细 |
|
||||||
|
|
||||||
|
### 6.6 统计摘要字段 `statSummaries[]`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `statType` | `String` | 统计类型:`AVG`、`MAX`、`MIN`、`CP95` |
|
||||||
|
| `supported` | `Boolean` | 是否支持该统计类型 |
|
||||||
|
| `hasData` | `Boolean` | 是否存在数据 |
|
||||||
|
| `expectedPointCount` | `Integer` | 期望点数 |
|
||||||
|
| `actualPointCount` | `Integer` | 实际点数 |
|
||||||
|
| `missingPointCount` | `Integer` | 缺失点数 |
|
||||||
|
| `dataIntegrity` | `BigDecimal` | 数据完整性 |
|
||||||
|
| `dataIntegrityText` | `String` | 数据完整性文本 |
|
||||||
|
|
||||||
|
### 6.7 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"taskId": "1812345678901234567",
|
||||||
|
"taskNo": "CS202606180001",
|
||||||
|
"lineId": "LINE_001",
|
||||||
|
"lineIds": ["LINE_001", "LINE_002"],
|
||||||
|
"lineName": "1号监测点,2号监测点",
|
||||||
|
"timeStart": "2026-06-18 00:00:00",
|
||||||
|
"timeEnd": "2026-06-18 01:00:00",
|
||||||
|
"intervalMinutes": 1,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"itemId": "1812345678901234568",
|
||||||
|
"itemKey": "LINE_001:VOLTAGE_A",
|
||||||
|
"lineId": "LINE_001",
|
||||||
|
"lineName": "1号监测点",
|
||||||
|
"indicatorCode": "VOLTAGE_A",
|
||||||
|
"indicatorName": "A相电压",
|
||||||
|
"harmonicOrder": null,
|
||||||
|
"intervalMinutes": 1,
|
||||||
|
"hasData": true,
|
||||||
|
"expectedPointCount": 60,
|
||||||
|
"actualPointCount": 59,
|
||||||
|
"missingPointCount": 1,
|
||||||
|
"dataIntegrity": 0.983333,
|
||||||
|
"dataIntegrityText": "98.33%",
|
||||||
|
"abnormal": true,
|
||||||
|
"abnormalPointCount": 1,
|
||||||
|
"abnormalDetails": [],
|
||||||
|
"harmonicParityAbnormal": false,
|
||||||
|
"harmonicParityAbnormalPointCount": 0,
|
||||||
|
"harmonicParityAbnormalDetails": [],
|
||||||
|
"statSummaries": [
|
||||||
|
{
|
||||||
|
"statType": "AVG",
|
||||||
|
"supported": true,
|
||||||
|
"hasData": true,
|
||||||
|
"expectedPointCount": 60,
|
||||||
|
"actualPointCount": 59,
|
||||||
|
"missingPointCount": 1,
|
||||||
|
"dataIntegrity": 0.983333,
|
||||||
|
"dataIntegrityText": "98.33%"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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` 同时为正数时启用分页 |
|
||||||
|
| `pageSize` | `Integer` | 否 | 每页条数;和 `pageNum` 同时为正数时启用分页 |
|
||||||
|
|
||||||
|
### 7.3 请求示例
|
||||||
|
|
||||||
|
查询连续性区间:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /steady/checksquare/item-detail?itemId=1812345678901234568&detailType=SEGMENT&pageNum=1&pageSize=10
|
||||||
|
```
|
||||||
|
|
||||||
|
查询大小关系异常:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /steady/checksquare/item-detail?itemId=1812345678901234568&detailType=VALUE_ORDER&pageNum=1&pageSize=10
|
||||||
|
```
|
||||||
|
|
||||||
|
查询谐波奇偶关系异常:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /steady/checksquare/item-detail?itemId=1812345678901234568&detailType=HARMONIC_PARITY&statType=AVG&pageNum=1&pageSize=10
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.4 响应字段 `data`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `itemId` | `String` | 检测项 ID |
|
||||||
|
| `lineId` | `String` | 当前检测项所属监测点 ID |
|
||||||
|
| `lineName` | `String` | 当前检测项所属监测点名称 |
|
||||||
|
| `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 连续性区间字段 `segments[]`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `startTime` | `String` | 区间开始时间 |
|
||||||
|
| `endTime` | `String` | 区间结束时间 |
|
||||||
|
| `status` | `String` | 区间状态:`NORMAL`、`MISSING` |
|
||||||
|
| `harmonicOrder` | `Integer` | 谐波次数 |
|
||||||
|
| `missingPointCount` | `Integer` | 缺失点数 |
|
||||||
|
| `durationMinutes` | `Integer` | 持续时长,单位分钟 |
|
||||||
|
|
||||||
|
### 7.6 大小关系异常字段 `valueOrderDetails[]`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `time` | `String` | 异常点时间 |
|
||||||
|
| `phase` | `String` | 相别 |
|
||||||
|
| `harmonicOrder` | `Integer` | 谐波次数 |
|
||||||
|
| `maxValue` | `BigDecimal` | 最大值 |
|
||||||
|
| `minValue` | `BigDecimal` | 最小值 |
|
||||||
|
| `avgValue` | `BigDecimal` | 平均值 |
|
||||||
|
| `cp95Value` | `BigDecimal` | CP95 值 |
|
||||||
|
|
||||||
|
### 7.7 谐波奇偶关系异常字段 `harmonicParityDetails[]`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `time` | `String` | 异常点时间 |
|
||||||
|
| `phase` | `String` | 相别 |
|
||||||
|
| `statType` | `String` | 统计类型 |
|
||||||
|
| `evenHarmonicOrder` | `Integer` | 偶次谐波次数 |
|
||||||
|
| `evenValue` | `BigDecimal` | 偶次谐波值 |
|
||||||
|
| `oddHarmonicOrders` | `Array<Integer>` | 参与比较的奇次谐波次数 |
|
||||||
|
| `oddValues` | `Array<BigDecimal>` | 参与比较的奇次谐波值 |
|
||||||
|
| `oddMedianValue` | `BigDecimal` | 奇次谐波中位数 |
|
||||||
|
| `thresholdMultiplier` | `BigDecimal` | 异常阈值倍数 |
|
||||||
|
|
||||||
|
### 7.8 响应示例
|
||||||
|
|
||||||
|
连续性区间:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"itemId": "1812345678901234568",
|
||||||
|
"lineId": "LINE_001",
|
||||||
|
"lineName": "1号监测点",
|
||||||
|
"detailType": "SEGMENT",
|
||||||
|
"statType": null,
|
||||||
|
"pageNum": 1,
|
||||||
|
"pageSize": 10,
|
||||||
|
"total": 1,
|
||||||
|
"segments": [
|
||||||
|
{
|
||||||
|
"startTime": "2026-06-18 00:10:00",
|
||||||
|
"endTime": "2026-06-18 00:10:00",
|
||||||
|
"status": "MISSING",
|
||||||
|
"harmonicOrder": null,
|
||||||
|
"missingPointCount": 1,
|
||||||
|
"durationMinutes": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"valueOrderDetails": [],
|
||||||
|
"harmonicParityDetails": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
大小关系异常:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"itemId": "1812345678901234568",
|
||||||
|
"lineId": "LINE_001",
|
||||||
|
"lineName": "1号监测点",
|
||||||
|
"detailType": "VALUE_ORDER",
|
||||||
|
"statType": null,
|
||||||
|
"pageNum": 1,
|
||||||
|
"pageSize": 10,
|
||||||
|
"total": 1,
|
||||||
|
"segments": [],
|
||||||
|
"valueOrderDetails": [
|
||||||
|
{
|
||||||
|
"time": "2026-06-18 00:20:00",
|
||||||
|
"phase": "A",
|
||||||
|
"harmonicOrder": null,
|
||||||
|
"maxValue": 231.12000000,
|
||||||
|
"minValue": 228.45000000,
|
||||||
|
"avgValue": 229.64000000,
|
||||||
|
"cp95Value": 230.98000000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"harmonicParityDetails": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 重启失败任务
|
||||||
|
|
||||||
|
### 8.1 接口
|
||||||
|
|
||||||
|
`POST /steady/checksquare/restart?taskId={taskId}`
|
||||||
|
|
||||||
|
### 8.2 行为说明
|
||||||
|
|
||||||
|
- 仅允许重启 `taskStatus=FAIL` 的任务。
|
||||||
|
- 重启复用原任务记录,`taskId` 不变。
|
||||||
|
- 重启前会清理该任务旧的检测项、统计摘要和明细数据,避免重复结果键。
|
||||||
|
- 接口返回时任务状态已更新为 `RUNNING`;后台执行结束后更新为 `SUCCESS` 或 `FAIL`。
|
||||||
|
|
||||||
|
### 8.3 请求参数
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `taskId` | `String` | 是 | 失败任务 ID |
|
||||||
|
|
||||||
|
### 8.4 请求示例
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /steady/checksquare/restart?taskId=1812345678901234567
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.5 响应说明
|
||||||
|
|
||||||
|
响应 `data` 与 `/create` 的 `SteadyChecksquareTaskVO` 字段一致。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"taskId": "1812345678901234567",
|
||||||
|
"taskNo": "CS202606180001",
|
||||||
|
"lineId": "LINE_001",
|
||||||
|
"lineIds": ["LINE_001", "LINE_002"],
|
||||||
|
"lineName": "1号监测点,2号监测点",
|
||||||
|
"timeStart": "2026-06-18 00:00:00",
|
||||||
|
"timeEnd": "2026-06-18 01:00:00",
|
||||||
|
"intervalMinutes": 1,
|
||||||
|
"taskStatus": "RUNNING",
|
||||||
|
"itemCount": 0,
|
||||||
|
"abnormalItemCount": 0,
|
||||||
|
"minDataIntegrity": 0.000000,
|
||||||
|
"createTime": "2026-06-18 09:30:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 删除任务
|
||||||
|
|
||||||
|
### 9.1 接口
|
||||||
|
|
||||||
|
`POST /steady/checksquare/delete`
|
||||||
|
|
||||||
|
### 9.2 请求字段
|
||||||
|
|
||||||
|
请求体直接传任务 ID 数组。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `taskIds` | `Array<String>` | 是 | 任务 ID 数组 |
|
||||||
|
|
||||||
|
### 9.3 请求示例
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /steady/checksquare/delete
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"1812345678901234567"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.4 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 常见调试注意事项
|
||||||
|
|
||||||
|
- `/create` 返回的是任务列表行信息,不是详情页完整数据。
|
||||||
|
- `/create` 如果命中已存在任务,会直接返回该任务,不会生成重复任务。
|
||||||
|
- 新标准推荐传 `lineIds`;`lineId` 仅作为单监测点兼容字段保留。
|
||||||
|
- `/query` 的 `lineId` 会匹配任务内的 `lineIds`,可用于筛选多监测点任务。
|
||||||
|
- `/detail` 需要使用 `/create`、`/restart` 或 `/query` 返回的 `taskId`。
|
||||||
|
- `/detail` 的任务级 `lineId` 是首个监测点,检测项级 `items[].lineId` 才是该检测项实际所属监测点。
|
||||||
|
- `/item-detail` 需要使用 `/detail` 返回的 `items[].itemId`。
|
||||||
|
- `/item-detail` 只有 `pageNum` 和 `pageSize` 同时为正数时才分页;否则返回全部匹配明细,分页字段为空。
|
||||||
|
- `/restart` 只允许重启失败任务,成功或执行中的任务调用会返回业务失败。
|
||||||
|
- `dataIntegrity` 是 0 到 1 的小数,展示百分比优先使用 `dataIntegrityText`。
|
||||||
@@ -6,9 +6,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-empty v-if="!task" class="empty-result" description="新增后在此查看任务摘要" />
|
<div v-if="loading" class="create-loading">
|
||||||
|
<el-icon class="create-loading__icon"><Loading /></el-icon>
|
||||||
|
<span class="create-loading__title">正在创建校验任务</span>
|
||||||
|
<span class="create-loading__desc">任务提交后将在此显示摘要</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty v-else-if="!task" class="empty-result" description="新增后在此查看任务摘要" />
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<div class="result-scroll">
|
||||||
<div class="result-body">
|
<div class="result-body">
|
||||||
<div class="result-overview">
|
<div class="result-overview">
|
||||||
<div class="task-card">
|
<div class="task-card">
|
||||||
@@ -31,7 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="metric-item">
|
<div class="metric-item">
|
||||||
<span class="metric-label">检测项</span>
|
<span class="metric-label">检测项</span>
|
||||||
<span class="metric-value">{{ task.itemCount ?? '-' }}</span>
|
<span class="metric-value">{{ displayItemCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-item">
|
<div class="metric-item">
|
||||||
<span class="metric-label">异常项</span>
|
<span class="metric-label">异常项</span>
|
||||||
@@ -39,7 +46,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="metric-item">
|
<div class="metric-item">
|
||||||
<span class="metric-label">最低完整率</span>
|
<span class="metric-label">最低完整率</span>
|
||||||
<span class="metric-value">{{ formatChecksquareIntegrity(task.minDataIntegrity) }}</span>
|
<span class="metric-value">
|
||||||
|
{{ formatChecksquareIntegrity(task.minDataIntegrity) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-item">
|
<div class="metric-item">
|
||||||
<span class="metric-label">统计间隔</span>
|
<span class="metric-label">统计间隔</span>
|
||||||
@@ -59,11 +68,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Loading } from '@element-plus/icons-vue'
|
||||||
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
||||||
import {
|
import {
|
||||||
formatChecksquareIntegrity,
|
formatChecksquareIntegrity,
|
||||||
@@ -75,9 +87,18 @@ defineOptions({
|
|||||||
name: 'ChecksquareCreateResultPanel'
|
name: 'ChecksquareCreateResultPanel'
|
||||||
})
|
})
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
task: SteadyDataView.SteadyChecksquareTask | null
|
task: SteadyDataView.SteadyChecksquareTask | null
|
||||||
|
expectedItemCount: number
|
||||||
|
loading?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const displayItemCount = computed(() => {
|
||||||
|
if (Number(props.task?.itemCount || 0) > 0) return props.task?.itemCount
|
||||||
|
if (props.expectedItemCount > 0) return props.expectedItemCount
|
||||||
|
|
||||||
|
return props.task?.itemCount ?? '-'
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -99,6 +120,43 @@ defineProps<{
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.create-loading {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-loading__icon {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 28px;
|
||||||
|
animation: rotate-loading 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-loading__title {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-loading__desc {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate-loading {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.result-body {
|
.result-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -107,6 +165,13 @@ defineProps<{
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.result-overview {
|
.result-overview {
|
||||||
display: grid;
|
display: grid;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
|||||||
@@ -31,6 +31,13 @@
|
|||||||
highlight-current-row
|
highlight-current-row
|
||||||
empty-text="暂无校验结果"
|
empty-text="暂无校验结果"
|
||||||
>
|
>
|
||||||
|
<el-table-column label="监测点名称" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="line-name" :title="resolveChecksquareLineName(row)">
|
||||||
|
{{ resolveChecksquareLineName(row) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="indicatorName" label="指标名称" width="160">
|
<el-table-column prop="indicatorName" label="指标名称" width="160">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="indicator-name" :title="resolveChecksquareRowName(row)">
|
<span class="indicator-name" :title="resolveChecksquareRowName(row)">
|
||||||
@@ -105,21 +112,25 @@ defineOptions({
|
|||||||
name: 'ChecksquareSummaryTable'
|
name: 'ChecksquareSummaryTable'
|
||||||
})
|
})
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
result: SteadyDataView.SteadyChecksquareQueryResult | null
|
|
||||||
items: SteadyDataView.SteadyChecksquareItem[]
|
|
||||||
loading: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
refresh: []
|
refresh: []
|
||||||
detail: [row: SteadyDataView.SteadyChecksquareItem]
|
detail: [row: SteadyDataView.SteadyChecksquareItem]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
result: SteadyDataView.SteadyChecksquareQueryResult | null
|
||||||
|
items: SteadyDataView.SteadyChecksquareItem[]
|
||||||
|
loading: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
const hasAbnormalCount = (value?: number | null) => Number(value || 0) > 0
|
const hasAbnormalCount = (value?: number | null) => Number(value || 0) > 0
|
||||||
|
|
||||||
const stripPercentUnit = (value: string) => value.replace(/%$/, '')
|
const stripPercentUnit = (value: string) => value.replace(/%$/, '')
|
||||||
|
|
||||||
|
const resolveChecksquareLineName = (row: SteadyDataView.SteadyChecksquareItem) => {
|
||||||
|
return row.lineName || row.lineId || props.result?.lineName || props.result?.lineId || '-'
|
||||||
|
}
|
||||||
|
|
||||||
const formatSummaryDataIntegrity = (row: SteadyDataView.SteadyChecksquareItem) => {
|
const formatSummaryDataIntegrity = (row: SteadyDataView.SteadyChecksquareItem) => {
|
||||||
return stripPercentUnit(formatDataIntegrity(row.dataIntegrity, row.dataIntegrityText))
|
return stripPercentUnit(formatDataIntegrity(row.dataIntegrity, row.dataIntegrityText))
|
||||||
}
|
}
|
||||||
@@ -157,6 +168,7 @@ const formatSummaryStatIntegrity = (
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.line-name,
|
||||||
.indicator-name {
|
.indicator-name {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|||||||
@@ -11,6 +11,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #operation="{ row }">
|
<template #operation="{ row }">
|
||||||
|
<el-button
|
||||||
|
v-if="row.taskStatus === 'FAIL'"
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
:icon="RefreshRight"
|
||||||
|
@click="emit('restart', row)"
|
||||||
|
>
|
||||||
|
重启
|
||||||
|
</el-button>
|
||||||
<el-button type="danger" link :icon="Delete" @click="emit('delete', row)">删除</el-button>
|
<el-button type="danger" link :icon="Delete" @click="emit('delete', row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</ProTable>
|
</ProTable>
|
||||||
@@ -19,7 +28,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h, reactive, ref } from 'vue'
|
import { computed, h, reactive, ref } from 'vue'
|
||||||
import { ElButton, ElDatePicker, ElTag, ElTreeSelect } from 'element-plus'
|
import { ElButton, ElDatePicker, ElTag, ElTreeSelect } from 'element-plus'
|
||||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
import { Delete, Plus, RefreshRight } from '@element-plus/icons-vue'
|
||||||
import ProTable from '@/components/ProTable/index.vue'
|
import ProTable from '@/components/ProTable/index.vue'
|
||||||
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
||||||
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
||||||
@@ -45,6 +54,7 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
createTask: []
|
createTask: []
|
||||||
detail: [row: SteadyDataView.SteadyChecksquareTask]
|
detail: [row: SteadyDataView.SteadyChecksquareTask]
|
||||||
|
restart: [row: SteadyDataView.SteadyChecksquareTask]
|
||||||
delete: [row: SteadyDataView.SteadyChecksquareTask]
|
delete: [row: SteadyDataView.SteadyChecksquareTask]
|
||||||
viewMeasurementPoint: [row: SteadyDataView.SteadyChecksquareTask]
|
viewMeasurementPoint: [row: SteadyDataView.SteadyChecksquareTask]
|
||||||
}>()
|
}>()
|
||||||
@@ -135,6 +145,7 @@ const renderLineSearch = ({ searchParam }: { searchParam: ChecksquareTaskSearchP
|
|||||||
collapseTags: true,
|
collapseTags: true,
|
||||||
collapseTagsTooltip: true,
|
collapseTagsTooltip: true,
|
||||||
maxCollapseTags: 1,
|
maxCollapseTags: 1,
|
||||||
|
popperClass: 'checksquare-search-tree-popper',
|
||||||
filterable: true,
|
filterable: true,
|
||||||
clearable: true,
|
clearable: true,
|
||||||
defaultExpandAll: true,
|
defaultExpandAll: true,
|
||||||
@@ -159,6 +170,7 @@ const renderIndicatorSearch = ({ searchParam }: { searchParam: ChecksquareTaskSe
|
|||||||
collapseTags: true,
|
collapseTags: true,
|
||||||
collapseTagsTooltip: true,
|
collapseTagsTooltip: true,
|
||||||
maxCollapseTags: 1,
|
maxCollapseTags: 1,
|
||||||
|
popperClass: 'checksquare-search-tree-popper',
|
||||||
filterable: true,
|
filterable: true,
|
||||||
clearable: true,
|
clearable: true,
|
||||||
defaultExpandAll: true,
|
defaultExpandAll: true,
|
||||||
@@ -292,7 +304,7 @@ const columns = reactive<ColumnProps<SteadyDataView.SteadyChecksquareTask>[]>([
|
|||||||
minWidth: 170,
|
minWidth: 170,
|
||||||
render: ({ row }) => resolveChecksquareText(row.createTime)
|
render: ({ row }) => resolveChecksquareText(row.createTime)
|
||||||
},
|
},
|
||||||
{ prop: 'operation', label: '操作', fixed: 'right', width: 150 }
|
{ prop: 'operation', label: '操作', fixed: 'right', width: 180 }
|
||||||
])
|
])
|
||||||
|
|
||||||
const getTableList = (params: ChecksquareTaskSearchParams) => {
|
const getTableList = (params: ChecksquareTaskSearchParams) => {
|
||||||
@@ -329,4 +341,8 @@ defineExpose({
|
|||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.checksquare-search-tree-popper .el-select-dropdown__wrap) {
|
||||||
|
max-height: 280px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -33,10 +33,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="query-actions">
|
<div class="query-actions">
|
||||||
<el-button type="primary" :icon="Plus" :loading="loading.query" @click="emit('create')">
|
<el-button type="primary" :icon="Plus" :loading="loading.query" :disabled="loading.query" @click="emit('create')">
|
||||||
新增
|
新增
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="primary" plain :icon="RefreshLeft" @click="emit('reset')">重置</el-button>
|
<el-button type="primary" plain :icon="RefreshLeft" :disabled="loading.query" @click="emit('reset')">重置</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-field indicator-form-item">
|
<div class="toolbar-field indicator-form-item">
|
||||||
<span class="toolbar-field__label">稳态指标:</span>
|
<span class="toolbar-field__label">稳态指标:</span>
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
filterable
|
filterable
|
||||||
clearable
|
clearable
|
||||||
default-expand-all
|
default-expand-all
|
||||||
|
:disabled="loading.query"
|
||||||
node-key="treeKey"
|
node-key="treeKey"
|
||||||
value-key="treeKey"
|
value-key="treeKey"
|
||||||
:props="{ label: 'name', children: 'children' }"
|
:props="{ label: 'name', children: 'children' }"
|
||||||
@@ -65,7 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-tree-select>
|
</el-tree-select>
|
||||||
<el-button type="primary" plain @click="handleSelectAllIndicators">全选</el-button>
|
<el-button type="primary" plain :disabled="loading.query" @click="handleSelectAllIndicators">全选</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -331,7 +332,65 @@ watch(
|
|||||||
|
|
||||||
.indicator-tree-select {
|
.indicator-tree-select {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-tree-select :deep(.el-select__wrapper) {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 32px;
|
||||||
|
height: 32px;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-tree-select :deep(.el-select__selection) {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 24px;
|
||||||
|
height: 24px;
|
||||||
|
max-height: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-tree-select :deep(.el-select__selected-item) {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-tree-select :deep(.el-select__tags) {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 24px;
|
||||||
|
height: 24px;
|
||||||
|
max-height: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-tree-select :deep(.el-tag) {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 22px;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-tree-select :deep(.el-select__tags-text) {
|
||||||
|
max-width: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator-select-node {
|
.indicator-select-node {
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ const files = {
|
|||||||
summaryTable: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareSummaryTable.vue'),
|
summaryTable: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareSummaryTable.vue'),
|
||||||
detailPanel: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareDetailPanel.vue'),
|
detailPanel: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareDetailPanel.vue'),
|
||||||
createResultPanel: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareCreateResultPanel.vue'),
|
createResultPanel: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareCreateResultPanel.vue'),
|
||||||
measurementPointDialog: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareMeasurementPointDialog.vue'),
|
measurementPointDialog: path.resolve(
|
||||||
|
rootDir,
|
||||||
|
'views/steady/checksquare/components/ChecksquareMeasurementPointDialog.vue'
|
||||||
|
),
|
||||||
payload: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquarePayload.ts'),
|
payload: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquarePayload.ts'),
|
||||||
ledgerUtils: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareLedger.ts'),
|
ledgerUtils: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareLedger.ts'),
|
||||||
taskTableUtils: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareTaskTable.ts'),
|
taskTableUtils: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareTaskTable.ts'),
|
||||||
@@ -39,11 +42,20 @@ const checks = [
|
|||||||
/\/steady\/checksquare\/create/.test(api) &&
|
/\/steady\/checksquare\/create/.test(api) &&
|
||||||
!/\/steady\/checksquare\/get-or-create/.test(api) &&
|
!/\/steady\/checksquare\/get-or-create/.test(api) &&
|
||||||
/\/steady\/checksquare\/delete/.test(api) &&
|
/\/steady\/checksquare\/delete/.test(api) &&
|
||||||
|
/\/steady\/checksquare\/restart\?taskId=/.test(api) &&
|
||||||
/\/steady\/checksquare\/detail/.test(api) &&
|
/\/steady\/checksquare\/detail/.test(api) &&
|
||||||
/\/steady\/checksquare\/item-detail/.test(api)
|
/\/steady\/checksquare\/item-detail/.test(api)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'checksquare restart api uses documented task id query endpoint',
|
||||||
|
() =>
|
||||||
|
/export const restartSteadyChecksquareTask = \(taskId: string\)/.test(read(files.api)) &&
|
||||||
|
/http\.post<SteadyDataView\.SteadyChecksquareTask>\([\s\S]*`\/steady\/checksquare\/restart\?taskId=\$\{encodeURIComponent\(taskId\)\}`/.test(
|
||||||
|
read(files.api)
|
||||||
|
)
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'checksquare delete api accepts documented task id array body',
|
'checksquare delete api accepts documented task id array body',
|
||||||
() =>
|
() =>
|
||||||
@@ -66,7 +78,8 @@ const checks = [
|
|||||||
const typeBlock =
|
const typeBlock =
|
||||||
read(files.apiTypes).match(/interface SteadyChecksquareCreateParams\s*\{[\s\S]*?\n {4}\}/)?.[0] || ''
|
read(files.apiTypes).match(/interface SteadyChecksquareCreateParams\s*\{[\s\S]*?\n {4}\}/)?.[0] || ''
|
||||||
return (
|
return (
|
||||||
/lineId: string/.test(typeBlock) &&
|
/lineId\?: string/.test(typeBlock) &&
|
||||||
|
/lineIds\?: string\[\]/.test(typeBlock) &&
|
||||||
/indicatorCodes: string\[\]/.test(typeBlock) &&
|
/indicatorCodes: string\[\]/.test(typeBlock) &&
|
||||||
/timeStart: string/.test(typeBlock) &&
|
/timeStart: string/.test(typeBlock) &&
|
||||||
/timeEnd: string/.test(typeBlock) &&
|
/timeEnd: string/.test(typeBlock) &&
|
||||||
@@ -75,7 +88,10 @@ const checks = [
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
['task table component exists', () => exists(files.taskTable)],
|
['task table component exists', () => exists(files.taskTable)],
|
||||||
['task table uses ProTable like event list', () => /<ProTable[\s\S]*row-key="taskId"[\s\S]*:columns="columns"/.test(read(files.taskTable))],
|
[
|
||||||
|
'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',
|
'task table exposes create task header action',
|
||||||
() =>
|
() =>
|
||||||
@@ -87,9 +103,17 @@ const checks = [
|
|||||||
'task table has documented task columns',
|
'task table has documented task columns',
|
||||||
() => {
|
() => {
|
||||||
const source = read(files.taskTable)
|
const source = read(files.taskTable)
|
||||||
return ['taskNo', 'lineName', 'timeStart', 'timeEnd', 'taskStatus', 'itemCount', 'abnormalItemCount', 'minDataIntegrity', 'createTime'].every(
|
return [
|
||||||
prop => new RegExp(`prop:\\s*'${prop}'`).test(source)
|
'taskNo',
|
||||||
)
|
'lineName',
|
||||||
|
'timeStart',
|
||||||
|
'timeEnd',
|
||||||
|
'taskStatus',
|
||||||
|
'itemCount',
|
||||||
|
'abnormalItemCount',
|
||||||
|
'minDataIntegrity',
|
||||||
|
'createTime'
|
||||||
|
].every(prop => new RegExp(`prop:\\s*'${prop}'`).test(source))
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -105,6 +129,19 @@ const checks = [
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'task table exposes restart only for failed rows',
|
||||||
|
() => {
|
||||||
|
const taskTable = read(files.taskTable)
|
||||||
|
const operationSlot = taskTable.match(/<template #operation="\{ row \}">[\s\S]*?<\/template>/)?.[0] || ''
|
||||||
|
return (
|
||||||
|
/v-if="row\.taskStatus === 'FAIL'"/.test(operationSlot) &&
|
||||||
|
/emit\('restart', row\)/.test(operationSlot) &&
|
||||||
|
/restart: \[row: SteadyDataView\.SteadyChecksquareTask\]/.test(taskTable) &&
|
||||||
|
/RefreshRight/.test(taskTable)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'task detail opens from abnormal item count value',
|
'task detail opens from abnormal item count value',
|
||||||
() => {
|
() => {
|
||||||
@@ -112,8 +149,7 @@ const checks = [
|
|||||||
return (
|
return (
|
||||||
/prop:\s*'abnormalItemCount'[\s\S]*ElButton[\s\S]*type:\s*'primary'[\s\S]*link:\s*true[\s\S]*emit\('detail', row\)/.test(
|
/prop:\s*'abnormalItemCount'[\s\S]*ElButton[\s\S]*type:\s*'primary'[\s\S]*link:\s*true[\s\S]*emit\('detail', row\)/.test(
|
||||||
taskTable
|
taskTable
|
||||||
) &&
|
) && /resolveChecksquareText\(row\.abnormalItemCount\)/.test(taskTable)
|
||||||
/resolveChecksquareText\(row\.abnormalItemCount\)/.test(taskTable)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -124,8 +160,18 @@ const checks = [
|
|||||||
/taskTimeRange/.test(read(files.taskTableUtils)) &&
|
/taskTimeRange/.test(read(files.taskTableUtils)) &&
|
||||||
/hasAbnormal/.test(read(files.taskTable))
|
/hasAbnormal/.test(read(files.taskTable))
|
||||||
],
|
],
|
||||||
['workbench remains create dialog selector body', () => /SteadyLedgerTree/.test(read(files.workbench)) && /TimePeriodSearch/.test(read(files.workbench))],
|
[
|
||||||
['workbench emits create instead of old query action', () => /create: \[\]/.test(read(files.workbench)) && !/query: \[\]/.test(read(files.workbench))],
|
'workbench remains create dialog selector body',
|
||||||
|
() => /SteadyLedgerTree/.test(read(files.workbench)) && /TimePeriodSearch/.test(read(files.workbench))
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'shared ledger tree emits checked leaf monitor points for checksquare create payload',
|
||||||
|
() => /getCheckedNodes\(\s*true\s*,\s*false\s*\)/.test(read(path.resolve(rootDir, 'views/steady/steadyDataView/components/SteadyLedgerTree.vue')))
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'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))],
|
['workbench no longer renders result table', () => !/ChecksquareSummaryTable/.test(read(files.workbench))],
|
||||||
[
|
[
|
||||||
'workbench create action uses short add label',
|
'workbench create action uses short add label',
|
||||||
@@ -148,7 +194,51 @@ const checks = [
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'payload builds create params without harmonic orders',
|
'payload builds create params without harmonic orders',
|
||||||
() => /buildSteadyChecksquareCreatePayload/.test(read(files.payload)) && !/harmonicOrder/.test(read(files.payload))
|
() =>
|
||||||
|
/buildSteadyChecksquareCreatePayload/.test(read(files.payload)) &&
|
||||||
|
!/harmonicOrder/.test(read(files.payload))
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'payload supports multi monitor point create params',
|
||||||
|
() => {
|
||||||
|
const payload = read(files.payload)
|
||||||
|
const page = read(files.page)
|
||||||
|
return (
|
||||||
|
/buildSteadyChecksquareCreatePayload\s*=\s*\(\s*lineIds:\s*string\[\]/.test(payload) &&
|
||||||
|
/lineIds,/.test(payload) &&
|
||||||
|
/lineId:\s*lineIds\[0\]/.test(payload) &&
|
||||||
|
!/lineIds\.length > 1/.test(payload) &&
|
||||||
|
/buildSteadyChecksquareCreatePayload\(lineIds\.value,\s*selectedIndicators\.value,\s*formState\.value\)/.test(
|
||||||
|
page
|
||||||
|
) &&
|
||||||
|
!/buildSteadyChecksquareCreatePayload\(lineIds\.value\[0\]/.test(page)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'payload allows empty indicator selection for full indicator checks',
|
||||||
|
() => {
|
||||||
|
const payload = read(files.payload)
|
||||||
|
const page = read(files.page)
|
||||||
|
return (
|
||||||
|
!/if\s*\(!indicators\.length\)\s*return\s*'请选择指标'/.test(payload) &&
|
||||||
|
/indicatorCodes:\s*collectChecksquareIndicatorCodes\(indicators\)/.test(payload) &&
|
||||||
|
/indicators:\s*selectedIndicators\.value/.test(page)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'payload exposes expected item count calculation for selected or full indicators',
|
||||||
|
() => {
|
||||||
|
const payload = read(files.payload)
|
||||||
|
return (
|
||||||
|
/calculateChecksquareExpectedItemCount/.test(payload) &&
|
||||||
|
/selectedIndicatorCount/.test(payload) &&
|
||||||
|
/totalIndicatorCount/.test(payload) &&
|
||||||
|
/lineCount/.test(payload) &&
|
||||||
|
/selectedIndicatorCount > 0 \? selectedIndicatorCount : totalIndicatorCount/.test(payload)
|
||||||
|
)
|
||||||
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'page renders task table as first screen',
|
'page renders task table as first screen',
|
||||||
@@ -211,6 +301,38 @@ const checks = [
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'create dialog indicator select keeps selected tags from resizing input',
|
||||||
|
() => {
|
||||||
|
const workbench = read(files.workbench)
|
||||||
|
return (
|
||||||
|
/class="indicator-tree-select"/.test(workbench) &&
|
||||||
|
/collapse-tags/.test(workbench) &&
|
||||||
|
/\.indicator-tree-select\s*:deep\(\.el-select__wrapper\)[\s\S]*height:\s*32px/.test(workbench) &&
|
||||||
|
/\.indicator-tree-select\s*:deep\(\.el-select__selection\)[\s\S]*width:\s*0[\s\S]*height:\s*24px[\s\S]*overflow:\s*hidden/.test(
|
||||||
|
workbench
|
||||||
|
) &&
|
||||||
|
/\.indicator-tree-select\s*:deep\(\.el-select__tags\)[\s\S]*height:\s*24px[\s\S]*overflow:\s*hidden[\s\S]*flex-wrap:\s*nowrap/.test(
|
||||||
|
workbench
|
||||||
|
) &&
|
||||||
|
/\.indicator-tree-select\s*:deep\(\.el-tag\)[\s\S]*height:\s*22px[\s\S]*overflow:\s*hidden/.test(
|
||||||
|
workbench
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'task table tree select filters use scrollable dropdowns',
|
||||||
|
() => {
|
||||||
|
const taskTable = read(files.taskTable)
|
||||||
|
return (
|
||||||
|
/popperClass:\s*'checksquare-search-tree-popper'/.test(taskTable) &&
|
||||||
|
/\.checksquare-search-tree-popper[\s\S]*\.el-select-dropdown__wrap[\s\S]*max-height:\s*280px/.test(
|
||||||
|
taskTable
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'task table monitor point name opens measurement point dialog like event list',
|
'task table monitor point name opens measurement point dialog like event list',
|
||||||
() => {
|
() => {
|
||||||
@@ -292,20 +414,23 @@ const checks = [
|
|||||||
[
|
[
|
||||||
'page wraps old workbench in create dialog',
|
'page wraps old workbench in create dialog',
|
||||||
() =>
|
() =>
|
||||||
/<el-dialog[\s\S]*新增校验任务[\s\S]*width="960px"[\s\S]*<ChecksquareWorkbench/.test(
|
/<el-dialog[\s\S]*新增校验任务[\s\S]*width="960px"[\s\S]*<ChecksquareWorkbench/.test(read(files.page)) &&
|
||||||
read(files.page)
|
|
||||||
) &&
|
|
||||||
/\.checksquare-create-dialog\s*\{[\s\S]*height:\s*560px/.test(read(files.page))
|
/\.checksquare-create-dialog\s*\{[\s\S]*height:\s*560px/.test(read(files.page))
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'page create flow calls create api, keeps summary only and refreshes task table',
|
'page create flow calls create api, then polls task list status and refreshes task table',
|
||||||
() =>
|
() => {
|
||||||
/createSteadyChecksquareTask/.test(read(files.page)) &&
|
const page = read(files.page)
|
||||||
!/getOrCreateSteadyChecksquareTask/.test(read(files.page)) &&
|
return (
|
||||||
!/refreshCreateTaskDetail/.test(read(files.page)) &&
|
/createSteadyChecksquareTask/.test(page) &&
|
||||||
!/startCreateTaskPolling/.test(read(files.page)) &&
|
/querySteadyChecksquareTasks\(buildCreateTaskStatusQuery\(activeCreateTask\.value\)\)/.test(page) &&
|
||||||
/taskTableRef\.value\?\.refresh\(\)/.test(read(files.page)) &&
|
!/getSteadyChecksquareDetail\(createdTask\.taskId\)/.test(page) &&
|
||||||
/activeCreateTask\.value/.test(read(files.page))
|
!/getOrCreateSteadyChecksquareTask/.test(page) &&
|
||||||
|
/startCreateTaskPolling\(createdTask\.taskId\)/.test(page) &&
|
||||||
|
/taskTableRef\.value\?\.refresh\(\)/.test(page) &&
|
||||||
|
/activeCreateTask\.value/.test(page)
|
||||||
|
)
|
||||||
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'create dialog shows create task summary without detail table',
|
'create dialog shows create task summary without detail table',
|
||||||
@@ -315,54 +440,75 @@ const checks = [
|
|||||||
const panel = read(files.createResultPanel)
|
const panel = read(files.createResultPanel)
|
||||||
return (
|
return (
|
||||||
exists(files.createResultPanel) &&
|
exists(files.createResultPanel) &&
|
||||||
/<template #result>[\s\S]*<ChecksquareCreateResultPanel[\s\S]*:task="activeCreateTask"[\s\S]*<\/template>/.test(
|
/<template #result>[\s\S]*<ChecksquareCreateResultPanel[\s\S]*:task="activeCreateTask"[\s\S]*:loading="loading\.query"[\s\S]*<\/template>/.test(
|
||||||
page
|
page
|
||||||
) &&
|
) &&
|
||||||
|
/activeCreateTask\.value\s*=\s*null[\s\S]*loading\.query\s*=\s*true/.test(page) &&
|
||||||
|
/:disabled="loading\.query"/.test(workbench) &&
|
||||||
/\.checksquare-create-dialog\s*\{[\s\S]*display:\s*block/.test(page) &&
|
/\.checksquare-create-dialog\s*\{[\s\S]*display:\s*block/.test(page) &&
|
||||||
/<slot name="result" \/>/.test(workbench) &&
|
/<slot name="result" \/>/.test(workbench) &&
|
||||||
/\.checksquare-main\s*\{[\s\S]*display:\s*flex[\s\S]*flex-direction:\s*column/.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(
|
/\.checksquare-layout\s*\{[\s\S]*grid-template-columns:\s*240px\s+minmax\(0,\s*1fr\)/.test(workbench) &&
|
||||||
workbench
|
|
||||||
) &&
|
|
||||||
/\.checksquare-result-slot\s*\{[\s\S]*width:\s*100%/.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*\{[\s\S]*min-width:\s*0/.test(workbench) &&
|
||||||
/\.checksquare-result-slot\s*:deep\(\.checksquare-result-panel\)\s*\{[\s\S]*height:\s*100%/.test(
|
/\.checksquare-result-slot\s*:deep\(\.checksquare-result-panel\)\s*\{[\s\S]*height:\s*100%/.test(
|
||||||
workbench
|
workbench
|
||||||
) &&
|
) &&
|
||||||
/name:\s*'ChecksquareCreateResultPanel'/.test(panel) &&
|
/name:\s*'ChecksquareCreateResultPanel'/.test(panel) &&
|
||||||
|
/loading\?:\s*boolean/.test(panel) &&
|
||||||
|
!/detail:\s*SteadyDataView\.SteadyChecksquareQueryResult \| null/.test(panel) &&
|
||||||
|
/expectedItemCount:\s*number/.test(panel) &&
|
||||||
|
/:expected-item-count="expectedCreateItemCount"/.test(page) &&
|
||||||
|
/v-if="loading"/.test(panel) &&
|
||||||
|
/正在创建校验任务/.test(panel) &&
|
||||||
|
/class="result-scroll"/.test(panel) &&
|
||||||
/class="result-overview"/.test(panel) &&
|
/class="result-overview"/.test(panel) &&
|
||||||
/class="result-body"/.test(panel) &&
|
/class="result-body"/.test(panel) &&
|
||||||
|
!/class="result-items"/.test(panel) &&
|
||||||
|
!/detailItems/.test(panel) &&
|
||||||
/class="result-detail-grid"/.test(panel) &&
|
/class="result-detail-grid"/.test(panel) &&
|
||||||
!/detail-block--wide/.test(panel) &&
|
!/detail-block--wide/.test(panel) &&
|
||||||
/\.result-overview\s*\{[\s\S]*grid-template-columns:\s*minmax\(0,\s*1fr\)/.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-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) &&
|
/\.result-detail-grid\s*\{[^}]*grid-template-columns:\s*repeat\(2,\s*minmax\(0,\s*1fr\)\)/.test(
|
||||||
|
panel
|
||||||
|
) &&
|
||||||
!/class="result-tips"/.test(panel) &&
|
!/class="result-tips"/.test(panel) &&
|
||||||
!/class="tips-title"/.test(panel) &&
|
!/class="tips-title"/.test(panel) &&
|
||||||
!/class="tips-list"/.test(panel) &&
|
!/class="tips-list"/.test(panel) &&
|
||||||
/class="result-metrics"[\s\S]*lineName[\s\S]*task\.itemCount/.test(panel) &&
|
/class="result-metrics"[\s\S]*lineName[\s\S]*displayItemCount/.test(panel) &&
|
||||||
/校验任务摘要/.test(panel) &&
|
/校验任务摘要/.test(panel) &&
|
||||||
/task\.itemCount/.test(panel) &&
|
/displayItemCount/.test(panel) &&
|
||||||
|
/props\.expectedItemCount/.test(panel) &&
|
||||||
/task\.abnormalItemCount/.test(panel) &&
|
/task\.abnormalItemCount/.test(panel) &&
|
||||||
/task\.minDataIntegrity/.test(panel) &&
|
/task\.minDataIntegrity/.test(panel) &&
|
||||||
!/class="detail-label">任务编号/.test(panel) &&
|
!/class="detail-label">任务编号/.test(panel) &&
|
||||||
/\.result-body\s*\{[\s\S]*flex:\s*1/.test(panel) &&
|
/\.result-body\s*\{[\s\S]*flex:\s*1/.test(panel) &&
|
||||||
|
/\.result-scroll\s*\{[\s\S]*overflow-y:\s*auto/.test(panel) &&
|
||||||
!/class="result-detail-table"/.test(panel) &&
|
!/class="result-detail-table"/.test(panel) &&
|
||||||
!/resultItems/.test(panel) &&
|
|
||||||
!/emit\('detail', row\)/.test(panel)
|
!/emit\('detail', row\)/.test(panel)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'create dialog does not poll create task detail',
|
'create dialog polls running task status and clears polling lifecycle',
|
||||||
() => {
|
() => {
|
||||||
const page = read(files.page)
|
const page = read(files.page)
|
||||||
return (
|
return (
|
||||||
!/createTaskPollingTimer/.test(page) &&
|
/createTaskPollingTimer/.test(page) &&
|
||||||
!/startCreateTaskPolling/.test(page) &&
|
/startCreateTaskPolling/.test(page) &&
|
||||||
!/stopCreateTaskPolling/.test(page) &&
|
/stopCreateTaskPolling/.test(page) &&
|
||||||
!/setInterval/.test(page) &&
|
/window\.setTimeout/.test(page) &&
|
||||||
!/onBeforeUnmount/.test(page)
|
/window\.clearTimeout/.test(page) &&
|
||||||
|
/CREATE_TASK_POLLING_INTERVALS/.test(page) &&
|
||||||
|
/getCreateTaskPollingDelay/.test(page) &&
|
||||||
|
/buildCreateTaskStatusQuery/.test(page) &&
|
||||||
|
/isChecksquareTaskFinished/.test(page) &&
|
||||||
|
/onBeforeUnmount\(stopCreateTaskPolling\)/.test(page) &&
|
||||||
|
/watch\(createDialogVisible/.test(page) &&
|
||||||
|
/refreshCreateTaskStatus/.test(page) &&
|
||||||
|
!/refreshCreateTaskDetail/.test(page) &&
|
||||||
|
/activeCreateTask\.value\s*=\s*\{\s*\.\.\.activeCreateTask\.value/.test(page)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -374,9 +520,20 @@ const checks = [
|
|||||||
/deleteSteadyChecksquareTasks\(\[row\.taskId\]\)/.test(read(files.page)) &&
|
/deleteSteadyChecksquareTasks\(\[row\.taskId\]\)/.test(read(files.page)) &&
|
||||||
/taskTableRef\.value\?\.refresh\(\)/.test(read(files.page))
|
/taskTableRef\.value\?\.refresh\(\)/.test(read(files.page))
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'page restart flow confirms, calls restart api and refreshes task table',
|
||||||
|
() =>
|
||||||
|
/@restart="handleRestartTask"/.test(read(files.page)) &&
|
||||||
|
/restartSteadyChecksquareTask/.test(read(files.page)) &&
|
||||||
|
/ElMessageBox\.confirm/.test(read(files.page)) &&
|
||||||
|
/restartSteadyChecksquareTask\(row\.taskId\)/.test(read(files.page)) &&
|
||||||
|
/taskTableRef\.value\?\.refresh\(\)/.test(read(files.page))
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'page detail flow calls detail api',
|
'page detail flow calls detail api',
|
||||||
() => /getSteadyChecksquareDetail/.test(read(files.page)) && /detailDialogVisible\.value = true/.test(read(files.page))
|
() =>
|
||||||
|
/getSteadyChecksquareDetail/.test(read(files.page)) &&
|
||||||
|
/detailDialogVisible\.value = true/.test(read(files.page))
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'task detail and item detail dialogs use the same size',
|
'task detail and item detail dialogs use the same size',
|
||||||
@@ -393,7 +550,9 @@ const checks = [
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'summary table supports persisted abnormal fields',
|
'summary table supports persisted abnormal fields',
|
||||||
() => /abnormalPointCount/.test(read(files.summaryTable)) && /harmonicParityAbnormalPointCount/.test(read(files.summaryTable))
|
() =>
|
||||||
|
/abnormalPointCount/.test(read(files.summaryTable)) &&
|
||||||
|
/harmonicParityAbnormalPointCount/.test(read(files.summaryTable))
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'summary table renders positive abnormal counts in danger color',
|
'summary table renders positive abnormal counts in danger color',
|
||||||
@@ -401,7 +560,9 @@ const checks = [
|
|||||||
const summaryTable = read(files.summaryTable)
|
const summaryTable = read(files.summaryTable)
|
||||||
return (
|
return (
|
||||||
/hasAbnormalCount/.test(summaryTable) &&
|
/hasAbnormalCount/.test(summaryTable) &&
|
||||||
/:class="\{\s*'is-abnormal-count': hasAbnormalCount\(row\.abnormalPointCount\)\s*\}"/.test(summaryTable) &&
|
/:class="\{\s*'is-abnormal-count': hasAbnormalCount\(row\.abnormalPointCount\)\s*\}"/.test(
|
||||||
|
summaryTable
|
||||||
|
) &&
|
||||||
/:class="\{\s*'is-abnormal-count': hasAbnormalCount\(row\.harmonicParityAbnormalPointCount\)\s*\}"/.test(
|
/:class="\{\s*'is-abnormal-count': hasAbnormalCount\(row\.harmonicParityAbnormalPointCount\)\s*\}"/.test(
|
||||||
summaryTable
|
summaryTable
|
||||||
) &&
|
) &&
|
||||||
@@ -414,8 +575,9 @@ const checks = [
|
|||||||
() => {
|
() => {
|
||||||
const summaryTable = read(files.summaryTable)
|
const summaryTable = read(files.summaryTable)
|
||||||
const dataIntegrityGroup =
|
const dataIntegrityGroup =
|
||||||
summaryTable.match(/<el-table-column label="数据完整性"[\s\S]*?<\/el-table-column>\s*<el-table-column label="操作"/)?.[0] ||
|
summaryTable.match(
|
||||||
''
|
/<el-table-column label="数据完整性"[\s\S]*?<\/el-table-column>\s*<el-table-column label="操作"/
|
||||||
|
)?.[0] || ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
/label="数据完整性"/.test(dataIntegrityGroup) &&
|
/label="数据完整性"/.test(dataIntegrityGroup) &&
|
||||||
@@ -459,6 +621,24 @@ const checks = [
|
|||||||
'summary table keeps indicator name column at configured width',
|
'summary table keeps indicator name column at configured width',
|
||||||
() => /prop="indicatorName" label="指标名称" width="160"/.test(read(files.summaryTable))
|
() => /prop="indicatorName" label="指标名称" width="160"/.test(read(files.summaryTable))
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'summary table displays monitor point name before indicator name',
|
||||||
|
() => {
|
||||||
|
const summaryTable = read(files.summaryTable)
|
||||||
|
const lineNameIndex = summaryTable.indexOf('label="监测点名称"')
|
||||||
|
const indicatorIndex = summaryTable.indexOf('prop="indicatorName"')
|
||||||
|
|
||||||
|
return (
|
||||||
|
/resolveChecksquareLineName/.test(summaryTable) &&
|
||||||
|
/:title="resolveChecksquareLineName\(row\)"/.test(summaryTable) &&
|
||||||
|
/row\.lineName\s*\|\|\s*row\.lineId\s*\|\|\s*props\.result\?\.lineName\s*\|\|\s*props\.result\?\.lineId/.test(
|
||||||
|
summaryTable
|
||||||
|
) &&
|
||||||
|
lineNameIndex >= 0 &&
|
||||||
|
indicatorIndex > lineNameIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'summary table places abnormal fields after indicator name and hides max continuous missing column',
|
'summary table places abnormal fields after indicator name and hides max continuous missing column',
|
||||||
() => {
|
() => {
|
||||||
@@ -534,14 +714,17 @@ const checks = [
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'detail panel loads item details on demand',
|
'detail panel loads item details on demand',
|
||||||
() => /getSteadyChecksquareItemDetail/.test(read(files.detailPanel)) && /detailType/.test(read(files.detailPanel))
|
() =>
|
||||||
|
/getSteadyChecksquareItemDetail/.test(read(files.detailPanel)) && /detailType/.test(read(files.detailPanel))
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'item detail api types support documented pagination fields',
|
'item detail api types support documented pagination fields',
|
||||||
() => {
|
() => {
|
||||||
const types = read(files.apiTypes)
|
const types = read(files.apiTypes)
|
||||||
return (
|
return (
|
||||||
/interface SteadyChecksquareItemDetailParams[\s\S]*pageNum\?: number[\s\S]*pageSize\?: number/.test(types) &&
|
/interface SteadyChecksquareItemDetailParams[\s\S]*pageNum\?: number[\s\S]*pageSize\?: number/.test(
|
||||||
|
types
|
||||||
|
) &&
|
||||||
/interface SteadyChecksquareItemDetail[\s\S]*pageNum\?: number \| null[\s\S]*pageSize\?: number \| null[\s\S]*total\?: number \| null/.test(
|
/interface SteadyChecksquareItemDetail[\s\S]*pageNum\?: number \| null[\s\S]*pageSize\?: number \| null[\s\S]*total\?: number \| null/.test(
|
||||||
types
|
types
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,22 +7,14 @@
|
|||||||
:request-api="querySteadyChecksquareTasks"
|
:request-api="querySteadyChecksquareTasks"
|
||||||
@create-task="openCreateDialog"
|
@create-task="openCreateDialog"
|
||||||
@detail="openTaskDetail"
|
@detail="openTaskDetail"
|
||||||
|
@restart="handleRestartTask"
|
||||||
@delete="handleDeleteTask"
|
@delete="handleDeleteTask"
|
||||||
@view-measurement-point="openMeasurementPointDialog"
|
@view-measurement-point="openMeasurementPointDialog"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ChecksquareMeasurementPointDialog
|
<ChecksquareMeasurementPointDialog v-model:visible="measurementPointDialogVisible" :data="measurementPointData" />
|
||||||
v-model:visible="measurementPointDialogVisible"
|
|
||||||
:data="measurementPointData"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog v-model="createDialogVisible" title="新增校验任务" width="960px" append-to-body destroy-on-close>
|
||||||
v-model="createDialogVisible"
|
|
||||||
title="新增校验任务"
|
|
||||||
width="960px"
|
|
||||||
append-to-body
|
|
||||||
destroy-on-close
|
|
||||||
>
|
|
||||||
<div class="checksquare-create-dialog">
|
<div class="checksquare-create-dialog">
|
||||||
<ChecksquareWorkbench
|
<ChecksquareWorkbench
|
||||||
v-model:form="formState"
|
v-model:form="formState"
|
||||||
@@ -44,6 +36,8 @@
|
|||||||
<template #result>
|
<template #result>
|
||||||
<ChecksquareCreateResultPanel
|
<ChecksquareCreateResultPanel
|
||||||
:task="activeCreateTask"
|
:task="activeCreateTask"
|
||||||
|
:loading="loading.query"
|
||||||
|
:expected-item-count="expectedCreateItemCount"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ChecksquareWorkbench>
|
</ChecksquareWorkbench>
|
||||||
@@ -71,7 +65,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
createSteadyChecksquareTask,
|
createSteadyChecksquareTask,
|
||||||
@@ -79,7 +73,8 @@ import {
|
|||||||
getSteadyChecksquareDetail,
|
getSteadyChecksquareDetail,
|
||||||
getSteadyTrendIndicatorTree,
|
getSteadyTrendIndicatorTree,
|
||||||
getSteadyTrendLedgerTree,
|
getSteadyTrendLedgerTree,
|
||||||
querySteadyChecksquareTasks
|
querySteadyChecksquareTasks,
|
||||||
|
restartSteadyChecksquareTask
|
||||||
} from '@/api/steady/steadyDataView'
|
} from '@/api/steady/steadyDataView'
|
||||||
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
||||||
import {
|
import {
|
||||||
@@ -102,6 +97,7 @@ import {
|
|||||||
} from './utils/checksquareLedger'
|
} from './utils/checksquareLedger'
|
||||||
import {
|
import {
|
||||||
buildSteadyChecksquareCreatePayload,
|
buildSteadyChecksquareCreatePayload,
|
||||||
|
calculateChecksquareExpectedItemCount,
|
||||||
defaultChecksquareFormState,
|
defaultChecksquareFormState,
|
||||||
validateChecksquareSelection
|
validateChecksquareSelection
|
||||||
} from './utils/checksquarePayload'
|
} from './utils/checksquarePayload'
|
||||||
@@ -137,8 +133,20 @@ const loading = reactive({
|
|||||||
detail: false
|
detail: false
|
||||||
})
|
})
|
||||||
let ledgerSearchTimer: ReturnType<typeof setTimeout> | null = null
|
let ledgerSearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let createTaskPollingTimer: number | null = null
|
||||||
|
let createTaskPollingRunning = false
|
||||||
|
let createTaskPollingCount = 0
|
||||||
|
const CREATE_TASK_POLLING_INTERVALS = [3000, 5000, 10000]
|
||||||
|
|
||||||
const lineIds = computed(() => collectSelectedLineIds(selectedLedgerNodes.value))
|
const lineIds = computed(() => collectSelectedLineIds(selectedLedgerNodes.value))
|
||||||
|
const totalIndicatorCount = computed(() => collectLeafIndicators(indicatorTree.value).length)
|
||||||
|
const expectedCreateItemCount = computed(() =>
|
||||||
|
calculateChecksquareExpectedItemCount({
|
||||||
|
lineCount: lineIds.value.length,
|
||||||
|
selectedIndicatorCount: selectedIndicators.value.length,
|
||||||
|
totalIndicatorCount: totalIndicatorCount.value
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const unwrapData = <T,>(response: { data: T } | T): T => {
|
const unwrapData = <T,>(response: { data: T } | T): T => {
|
||||||
if (response && typeof response === 'object' && 'data' in response) {
|
if (response && typeof response === 'object' && 'data' in response) {
|
||||||
@@ -148,6 +156,13 @@ const unwrapData = <T,>(response: { data: T } | T): T => {
|
|||||||
return response as T
|
return response as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isChecksquareTaskFinished = (status?: string) => status === 'SUCCESS' || status === 'FAIL'
|
||||||
|
|
||||||
|
const getCreateTaskPollingDelay = () => {
|
||||||
|
const index = Math.min(createTaskPollingCount, CREATE_TASK_POLLING_INTERVALS.length - 1)
|
||||||
|
return CREATE_TASK_POLLING_INTERVALS[index]
|
||||||
|
}
|
||||||
|
|
||||||
const loadLedgerTree = async (keyword = ledgerKeyword.value) => {
|
const loadLedgerTree = async (keyword = ledgerKeyword.value) => {
|
||||||
loading.ledger = true
|
loading.ledger = true
|
||||||
try {
|
try {
|
||||||
@@ -177,6 +192,7 @@ const loadIndicatorTree = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openCreateDialog = () => {
|
const openCreateDialog = () => {
|
||||||
|
stopCreateTaskPolling()
|
||||||
activeCreateTask.value = null
|
activeCreateTask.value = null
|
||||||
createDialogVisible.value = true
|
createDialogVisible.value = true
|
||||||
}
|
}
|
||||||
@@ -196,6 +212,7 @@ const handleIndicatorChange = (nodes: SteadyDataView.SteadyIndicatorNode[]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
|
stopCreateTaskPolling()
|
||||||
formState.value = defaultChecksquareFormState()
|
formState.value = defaultChecksquareFormState()
|
||||||
selectedLedgerNodes.value = []
|
selectedLedgerNodes.value = []
|
||||||
selectedIndicators.value = []
|
selectedIndicators.value = []
|
||||||
@@ -205,7 +222,82 @@ const handleReset = () => {
|
|||||||
selectorResetKey.value += 1
|
selectorResetKey.value += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeCreateTask = (result: SteadyDataView.SteadyChecksquareTask): SteadyDataView.SteadyChecksquareTask | null => {
|
const buildCreateTaskStatusQuery = (
|
||||||
|
task: SteadyDataView.SteadyChecksquareTask | null
|
||||||
|
): SteadyDataView.SteadyChecksquareTaskQueryParams => ({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
lineId: task?.lineId,
|
||||||
|
timeStart: task?.timeStart,
|
||||||
|
timeEnd: task?.timeEnd
|
||||||
|
})
|
||||||
|
|
||||||
|
const refreshCreateTaskStatus = async (taskId: string) => {
|
||||||
|
if (createTaskPollingRunning) return
|
||||||
|
|
||||||
|
createTaskPollingRunning = true
|
||||||
|
try {
|
||||||
|
const taskResponse = await querySteadyChecksquareTasks(buildCreateTaskStatusQuery(activeCreateTask.value))
|
||||||
|
const taskPage = unwrapData(taskResponse)
|
||||||
|
const latestTask = taskPage.records?.find(task => task.taskId === taskId)
|
||||||
|
|
||||||
|
if (activeCreateTask.value && latestTask) {
|
||||||
|
activeCreateTask.value = {
|
||||||
|
...activeCreateTask.value,
|
||||||
|
taskNo: latestTask.taskNo || activeCreateTask.value.taskNo,
|
||||||
|
lineId: latestTask.lineId || activeCreateTask.value.lineId,
|
||||||
|
lineName: latestTask.lineName || activeCreateTask.value.lineName,
|
||||||
|
timeStart: latestTask.timeStart || activeCreateTask.value.timeStart,
|
||||||
|
timeEnd: latestTask.timeEnd || activeCreateTask.value.timeEnd,
|
||||||
|
intervalMinutes: latestTask.intervalMinutes ?? activeCreateTask.value.intervalMinutes,
|
||||||
|
taskStatus: latestTask.taskStatus || activeCreateTask.value.taskStatus,
|
||||||
|
itemCount: latestTask.itemCount ?? activeCreateTask.value.itemCount,
|
||||||
|
abnormalItemCount: latestTask.abnormalItemCount ?? activeCreateTask.value.abnormalItemCount,
|
||||||
|
minDataIntegrity: latestTask.minDataIntegrity ?? activeCreateTask.value.minDataIntegrity,
|
||||||
|
createTime: latestTask.createTime || activeCreateTask.value.createTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isChecksquareTaskFinished(latestTask?.taskStatus || activeCreateTask.value?.taskStatus)) {
|
||||||
|
stopCreateTaskPolling()
|
||||||
|
taskTableRef.value?.refresh()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleCreateTaskPolling(taskId)
|
||||||
|
} finally {
|
||||||
|
createTaskPollingRunning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopCreateTaskPolling = () => {
|
||||||
|
if (createTaskPollingTimer !== null) window.clearTimeout(createTaskPollingTimer)
|
||||||
|
|
||||||
|
createTaskPollingTimer = null
|
||||||
|
createTaskPollingRunning = false
|
||||||
|
createTaskPollingCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleCreateTaskPolling = (taskId: string) => {
|
||||||
|
if (createTaskPollingTimer !== null) window.clearTimeout(createTaskPollingTimer)
|
||||||
|
|
||||||
|
createTaskPollingTimer = window.setTimeout(() => {
|
||||||
|
createTaskPollingTimer = null
|
||||||
|
createTaskPollingCount += 1
|
||||||
|
void refreshCreateTaskStatus(taskId).catch(() => scheduleCreateTaskPolling(taskId))
|
||||||
|
}, getCreateTaskPollingDelay())
|
||||||
|
}
|
||||||
|
|
||||||
|
const startCreateTaskPolling = (taskId: string) => {
|
||||||
|
stopCreateTaskPolling()
|
||||||
|
|
||||||
|
// 校验任务只需要监控执行状态,轮询任务列表行信息,避免反复拉取检测项明细。
|
||||||
|
scheduleCreateTaskPolling(taskId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeCreateTask = (
|
||||||
|
result: SteadyDataView.SteadyChecksquareTask
|
||||||
|
): SteadyDataView.SteadyChecksquareTask | null => {
|
||||||
const taskId = result.taskId || result.taskNo
|
const taskId = result.taskId || result.taskNo
|
||||||
if (!taskId) return null
|
if (!taskId) return null
|
||||||
|
|
||||||
@@ -236,14 +328,19 @@ const handleCreateTask = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activeCreateTask.value = null
|
||||||
loading.query = true
|
loading.query = true
|
||||||
try {
|
try {
|
||||||
// /create 只返回任务行信息,检测项明细统一通过 /detail 拉取,避免把列表行误当成详情数据。
|
// /create 返回任务行信息,新增弹窗只监控任务状态,不拉取检测项详情。
|
||||||
const response = await createSteadyChecksquareTask(
|
const response = await createSteadyChecksquareTask(
|
||||||
buildSteadyChecksquareCreatePayload(lineIds.value[0], selectedIndicators.value, formState.value)
|
buildSteadyChecksquareCreatePayload(lineIds.value, selectedIndicators.value, formState.value)
|
||||||
)
|
)
|
||||||
const result = unwrapData(response)
|
const result = unwrapData(response)
|
||||||
activeCreateTask.value = normalizeCreateTask(result)
|
const createdTask = normalizeCreateTask(result)
|
||||||
|
activeCreateTask.value = createdTask
|
||||||
|
if (createdTask?.taskId && !isChecksquareTaskFinished(createdTask.taskStatus)) {
|
||||||
|
startCreateTaskPolling(createdTask.taskId)
|
||||||
|
}
|
||||||
ElMessage.success('校验任务已获取')
|
ElMessage.success('校验任务已获取')
|
||||||
taskTableRef.value?.refresh()
|
taskTableRef.value?.refresh()
|
||||||
} finally {
|
} finally {
|
||||||
@@ -283,12 +380,31 @@ const handleDeleteTask = async (row: SteadyDataView.SteadyChecksquareTask) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除接口按任务 ID 数组批量处理;列表行操作只传当前行任务 ID。
|
// 删除接口按任务 ID 数组批量处理,行操作只传当前任务 ID。
|
||||||
await deleteSteadyChecksquareTasks([row.taskId])
|
await deleteSteadyChecksquareTasks([row.taskId])
|
||||||
ElMessage.success('删除校验任务成功')
|
ElMessage.success('删除校验任务成功')
|
||||||
taskTableRef.value?.refresh()
|
taskTableRef.value?.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRestartTask = async (row: SteadyDataView.SteadyChecksquareTask) => {
|
||||||
|
if (!row.taskId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确认重启该失败的校验任务吗?重启后会清理旧结果并重新执行。', '重启确认', {
|
||||||
|
confirmButtonText: '重启',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重启接口仅支持失败任务,成功后复用原任务 ID 并刷新列表展示最新运行状态。
|
||||||
|
await restartSteadyChecksquareTask(row.taskId)
|
||||||
|
ElMessage.success('校验任务已重启')
|
||||||
|
taskTableRef.value?.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
const openItemDetail = (item: SteadyDataView.SteadyChecksquareItem) => {
|
const openItemDetail = (item: SteadyDataView.SteadyChecksquareItem) => {
|
||||||
selectedItem.value = item
|
selectedItem.value = item
|
||||||
itemDetailDialogVisible.value = true
|
itemDetailDialogVisible.value = true
|
||||||
@@ -303,6 +419,12 @@ onMounted(() => {
|
|||||||
loadLedgerTree()
|
loadLedgerTree()
|
||||||
loadIndicatorTree()
|
loadIndicatorTree()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(createDialogVisible, visible => {
|
||||||
|
if (!visible) stopCreateTaskPolling()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(stopCreateTaskPolling)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -29,13 +29,25 @@ export const collectChecksquareIndicatorCodes = (indicators: SteadyDataView.Stea
|
|||||||
return Array.from(new Set(indicators.map(item => item.indicatorCode).filter(Boolean))) as string[]
|
return Array.from(new Set(indicators.map(item => item.indicatorCode).filter(Boolean))) as string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const calculateChecksquareExpectedItemCount = (params: {
|
||||||
|
lineCount: number
|
||||||
|
selectedIndicatorCount: number
|
||||||
|
totalIndicatorCount: number
|
||||||
|
}) => {
|
||||||
|
const { lineCount, selectedIndicatorCount, totalIndicatorCount } = params
|
||||||
|
const indicatorCount = selectedIndicatorCount > 0 ? selectedIndicatorCount : totalIndicatorCount
|
||||||
|
|
||||||
|
return lineCount * indicatorCount
|
||||||
|
}
|
||||||
|
|
||||||
export const buildSteadyChecksquareCreatePayload = (
|
export const buildSteadyChecksquareCreatePayload = (
|
||||||
lineId: string,
|
lineIds: string[],
|
||||||
indicators: SteadyDataView.SteadyIndicatorNode[],
|
indicators: SteadyDataView.SteadyIndicatorNode[],
|
||||||
formState: ChecksquareFormState
|
formState: ChecksquareFormState
|
||||||
): SteadyDataView.SteadyChecksquareCreateParams => {
|
): SteadyDataView.SteadyChecksquareCreateParams => {
|
||||||
return {
|
return {
|
||||||
lineId,
|
lineId: lineIds[0],
|
||||||
|
lineIds,
|
||||||
indicatorCodes: collectChecksquareIndicatorCodes(indicators),
|
indicatorCodes: collectChecksquareIndicatorCodes(indicators),
|
||||||
timeStart: (formState.timeRange[0] || '').replace(/\.[^.]+$/, ''),
|
timeStart: (formState.timeRange[0] || '').replace(/\.[^.]+$/, ''),
|
||||||
timeEnd: (formState.timeRange[1] || '').replace(/\.[^.]+$/, '')
|
timeEnd: (formState.timeRange[1] || '').replace(/\.[^.]+$/, '')
|
||||||
@@ -47,11 +59,9 @@ export const validateChecksquareSelection = (params: {
|
|||||||
indicators: SteadyDataView.SteadyIndicatorNode[]
|
indicators: SteadyDataView.SteadyIndicatorNode[]
|
||||||
timeRange: string[]
|
timeRange: string[]
|
||||||
}) => {
|
}) => {
|
||||||
const { lineIds, indicators, timeRange } = params
|
const { lineIds, timeRange } = params
|
||||||
|
|
||||||
if (!lineIds.length) return '请选择监测点'
|
if (!lineIds.length) return '请选择监测点'
|
||||||
if (lineIds.length > 1) return '数据校验一次只能选择一个监测点'
|
|
||||||
if (!indicators.length) return '请选择指标'
|
|
||||||
if (!timeRange[0]) return '请选择开始时间'
|
if (!timeRange[0]) return '请选择开始时间'
|
||||||
if (!timeRange[1]) return '请选择结束时间'
|
if (!timeRange[1]) return '请选择结束时间'
|
||||||
if (Date.parse(timeRange[0].replace(' ', 'T')) > Date.parse(timeRange[1].replace(' ', 'T'))) {
|
if (Date.parse(timeRange[0].replace(' ', 'T')) > Date.parse(timeRange[1].replace(' ', 'T'))) {
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ const handleKeywordChange = (value: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleCheck = () => {
|
const handleCheck = () => {
|
||||||
emit('change', (treeRef.value?.getCheckedNodes(false, false) || []) as SteadyDataView.SteadyLedgerNode[])
|
emit('change', (treeRef.value?.getCheckedNodes(true, false) || []) as SteadyDataView.SteadyLedgerNode[])
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyDefaultCheckedKeys = async () => {
|
const applyDefaultCheckedKeys = async () => {
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ const selectionRulesSource = fs.readFileSync(path.join(currentDir, '..', 'utils'
|
|||||||
|
|
||||||
const expectations = [
|
const expectations = [
|
||||||
[
|
[
|
||||||
'ledger tree excludes half-checked parents when collecting checked nodes',
|
'ledger tree emits leaf monitor points when collecting checked nodes',
|
||||||
/getCheckedNodes\(\s*false\s*,\s*false\s*\)/,
|
/getCheckedNodes\(\s*true\s*,\s*false\s*\)/,
|
||||||
read('SteadyLedgerTree.vue')
|
read('SteadyLedgerTree.vue')
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ const handleKeywordChange = (value: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleCheck = () => {
|
const handleCheck = () => {
|
||||||
emit('change', (treeRef.value?.getCheckedNodes(false, false) || []) as SteadyTrend.SteadyLedgerNode[])
|
emit('change', (treeRef.value?.getCheckedNodes(true, false) || []) as SteadyTrend.SteadyLedgerNode[])
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyDefaultCheckedKeys = async () => {
|
const applyDefaultCheckedKeys = async () => {
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ const selectionRulesSource = fs.readFileSync(path.join(currentDir, '..', 'utils'
|
|||||||
|
|
||||||
const expectations = [
|
const expectations = [
|
||||||
[
|
[
|
||||||
'ledger tree excludes half-checked parents when collecting checked nodes',
|
'ledger tree emits leaf monitor points when collecting checked nodes',
|
||||||
/getCheckedNodes\(\s*false\s*,\s*false\s*\)/,
|
/getCheckedNodes\(\s*true\s*,\s*false\s*\)/,
|
||||||
read('SteadyLedgerTree.vue')
|
read('SteadyLedgerTree.vue')
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
<div class="tool-text">进入 MMS 映射页面,后续可继续补充映射配置和预览能力。</div>
|
<div class="tool-text">进入 MMS 映射页面,后续可继续补充映射配置和预览能力。</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button class="tool-item" type="button" @click="handleNavigate('/tools/parsePqdif')">
|
||||||
|
<div class="tool-name">PQDIF解析</div>
|
||||||
|
<div class="tool-text">进入 PQDIF 解析页面,后续可接入文件上传、解析进度和结果展示能力。</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="tool-item" type="button" @click="handleNavigate('/tools/deviceTypes')">
|
<button class="tool-item" type="button" @click="handleNavigate('/tools/deviceTypes')">
|
||||||
<div class="tool-name">设备类型维护</div>
|
<div class="tool-name">设备类型维护</div>
|
||||||
<div class="tool-text">维护设备类型,并从列表进入 ICD 一致性校验和 PQDIF 预留校验。</div>
|
<div class="tool-text">维护设备类型,并从列表进入 ICD 一致性校验和 PQDIF 预留校验。</div>
|
||||||
|
|||||||
@@ -83,11 +83,14 @@
|
|||||||
:is-saving-icd-check-result="isSavingIcdCheckResult"
|
:is-saving-icd-check-result="isSavingIcdCheckResult"
|
||||||
:save-icd-check-result-text="saveIcdCheckResultText"
|
:save-icd-check-result-text="saveIcdCheckResultText"
|
||||||
:icd-consistency-status="icdConsistencyStatus"
|
:icd-consistency-status="icdConsistencyStatus"
|
||||||
|
:should-open-icd-consistency-problems="shouldOpenIcdConsistencyProblems"
|
||||||
:show-description="false"
|
:show-description="false"
|
||||||
@export-mapping="handleExportMapping"
|
@export-mapping="handleExportMappingEvent"
|
||||||
@generate-xml-mapping="handleGenerateXmlMapping"
|
@generate-xml-mapping="handleGenerateXmlMapping"
|
||||||
@icd-check="handleIcdConsistencyCheck"
|
@icd-check="handleIcdConsistencyCheck"
|
||||||
@update-mapping-json="handleUpdateMappingJson"
|
@confirm-icd-consistency-problems="handleConfirmIcdConsistencyProblemsEvent"
|
||||||
|
@remove-icd-consistency-problem="handleRemoveIcdConsistencyProblemEvent"
|
||||||
|
@update-mapping-json="handleUpdateMappingJsonEvent"
|
||||||
@update:sequence-dialog-visible="sequenceDialogVisible = $event"
|
@update:sequence-dialog-visible="sequenceDialogVisible = $event"
|
||||||
@sequence-config-complete="handleSequenceConfigComplete"
|
@sequence-config-complete="handleSequenceConfigComplete"
|
||||||
@save-icd-check-result="handleSaveIcdCheckResult"
|
@save-icd-check-result="handleSaveIcdCheckResult"
|
||||||
@@ -125,7 +128,8 @@ const props = defineProps<{
|
|||||||
icdPathId: string
|
icdPathId: string
|
||||||
icdPathName: string
|
icdPathName: string
|
||||||
icdPathType: number
|
icdPathType: number
|
||||||
activeIcdPathRecord: MmsMapping.IcdPathRecord | null
|
standardMappingJson: string
|
||||||
|
standardMappingName: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -134,7 +138,7 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const dialogTitle = computed(() => (props.icdPathName ? `ICD校验:${props.icdPathName}` : 'ICD校验'))
|
const dialogTitle = computed(() => (props.icdPathName ? `ICD校验:${props.icdPathName}` : 'ICD校验'))
|
||||||
const showConsistencyCheck = computed(() => props.icdPathType === 2 || props.icdPathType === 3)
|
const showConsistencyCheck = computed(() => props.icdPathType === 2 || props.icdPathType === 4)
|
||||||
|
|
||||||
const saveIcdCheckResult = (params: MmsMapping.SaveIcdCheckResultRequest): Promise<ResultData<boolean> | boolean> => {
|
const saveIcdCheckResult = (params: MmsMapping.SaveIcdCheckResultRequest): Promise<ResultData<boolean> | boolean> => {
|
||||||
return saveIcdPathCheckResultApi(props.icdPathId, params)
|
return saveIcdPathCheckResultApi(props.icdPathId, params)
|
||||||
@@ -184,6 +188,7 @@ const {
|
|||||||
saveIcdCheckResultText,
|
saveIcdCheckResultText,
|
||||||
hasIcdConsistencyCheckResult,
|
hasIcdConsistencyCheckResult,
|
||||||
icdConsistencyStatus,
|
icdConsistencyStatus,
|
||||||
|
shouldOpenIcdConsistencyProblems,
|
||||||
confirmDialogVisible,
|
confirmDialogVisible,
|
||||||
isConfirmingSelection,
|
isConfirmingSelection,
|
||||||
handleIcdFileChange,
|
handleIcdFileChange,
|
||||||
@@ -193,6 +198,8 @@ const {
|
|||||||
handleExportMapping,
|
handleExportMapping,
|
||||||
handleGenerateXmlMapping,
|
handleGenerateXmlMapping,
|
||||||
handleIcdConsistencyCheck,
|
handleIcdConsistencyCheck,
|
||||||
|
handleConfirmIcdConsistencyProblems,
|
||||||
|
handleRemoveIcdConsistencyProblem,
|
||||||
handleUpdateMappingJson,
|
handleUpdateMappingJson,
|
||||||
handleSequenceConfigComplete,
|
handleSequenceConfigComplete,
|
||||||
handleSaveIcdCheckResult,
|
handleSaveIcdCheckResult,
|
||||||
@@ -200,9 +207,9 @@ const {
|
|||||||
} = useMmsMappingFlow({
|
} = useMmsMappingFlow({
|
||||||
deviceTypeCheckId: toRef(props, 'icdPathId'),
|
deviceTypeCheckId: toRef(props, 'icdPathId'),
|
||||||
deviceTypeCheckName: toRef(props, 'icdPathName'),
|
deviceTypeCheckName: toRef(props, 'icdPathName'),
|
||||||
// 关键业务节点:只有需要一致性校验的 ICD 类型才注入标准 JSON,避免隐藏校验按钮后保存仍被校验前置条件卡住。
|
// 关键业务节点:列表接口不再返回大字段,标准 JSON 由宿主页通过 mapping-detail 详情接口加载后传入。
|
||||||
standardMappingJson: computed(() => (showConsistencyCheck.value ? props.activeIcdPathRecord?.jsonStr?.trim() || '' : '')),
|
standardMappingJson: computed(() => (showConsistencyCheck.value ? props.standardMappingJson?.trim() || '' : '')),
|
||||||
standardMappingName: computed(() => (showConsistencyCheck.value ? props.activeIcdPathRecord?.name?.trim() || '' : '')),
|
standardMappingName: computed(() => (showConsistencyCheck.value ? props.standardMappingName?.trim() || '' : '')),
|
||||||
saveIcdCheckResult,
|
saveIcdCheckResult,
|
||||||
onIcdCheckSaved: () => {
|
onIcdCheckSaved: () => {
|
||||||
emit('saved')
|
emit('saved')
|
||||||
@@ -210,6 +217,31 @@ const {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleExportMappingEvent = (...args: unknown[]) => {
|
||||||
|
const [type] = args
|
||||||
|
|
||||||
|
if (type !== 'json' && type !== 'xml') return
|
||||||
|
handleExportMapping(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirmIcdConsistencyProblemsEvent = () => {
|
||||||
|
handleConfirmIcdConsistencyProblems()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveIcdConsistencyProblemEvent = (...args: unknown[]) => {
|
||||||
|
const [index] = args
|
||||||
|
|
||||||
|
if (typeof index !== 'number') return
|
||||||
|
handleRemoveIcdConsistencyProblem(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdateMappingJsonEvent = (...args: unknown[]) => {
|
||||||
|
const [mappingJson] = args
|
||||||
|
|
||||||
|
if (typeof mappingJson !== 'string') return
|
||||||
|
handleUpdateMappingJson(mappingJson)
|
||||||
|
}
|
||||||
|
|
||||||
type StepStatus = 'success' | 'wait' | 'process' | 'finish' | 'error'
|
type StepStatus = 'success' | 'wait' | 'process' | 'finish' | 'error'
|
||||||
|
|
||||||
interface IcdCheckFlowStep {
|
interface IcdCheckFlowStep {
|
||||||
@@ -348,10 +380,16 @@ const handleClose = () => {
|
|||||||
|
|
||||||
:deep(.icd-check-flow .el-step__description) {
|
:deep(.icd-check-flow .el-step__description) {
|
||||||
max-width: 132px;
|
max-width: 132px;
|
||||||
|
min-height: 36px;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
text-overflow: ellipsis;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.icd-check-flow .el-step__head.is-process),
|
:deep(.icd-check-flow .el-step__head.is-process),
|
||||||
|
|||||||
@@ -10,8 +10,6 @@
|
|||||||
<div class="icd-check-dialog__body">
|
<div class="icd-check-dialog__body">
|
||||||
<div class="icd-path-form-dialog__body">
|
<div class="icd-path-form-dialog__body">
|
||||||
<el-form ref="formRef" :model="formModel" :rules="formRules" label-width="120px">
|
<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-form-item label="ICD名称" prop="name">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="formModel.name"
|
v-model="formModel.name"
|
||||||
@@ -21,8 +19,6 @@
|
|||||||
placeholder="请输入 ICD 名称"
|
placeholder="请输入 ICD 名称"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="ICD类型">
|
<el-form-item label="ICD类型">
|
||||||
<el-select v-model="formModel.type" clearable placeholder="请选择 ICD 类型">
|
<el-select v-model="formModel.type" clearable placeholder="请选择 ICD 类型">
|
||||||
<el-option
|
<el-option
|
||||||
@@ -33,33 +29,23 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
<el-form-item label="标准ICD引用" prop="referenceIcdId">
|
||||||
<el-col :span="24">
|
<el-select
|
||||||
<el-form-item label="ICD路径" prop="path">
|
v-model="formModel.referenceIcdId"
|
||||||
<el-input
|
|
||||||
v-model="formModel.path"
|
|
||||||
maxlength="260"
|
|
||||||
show-word-limit
|
|
||||||
clearable
|
clearable
|
||||||
placeholder="请输入 ICD 存储路径"
|
filterable
|
||||||
/>
|
:loading="referenceLoading"
|
||||||
</el-form-item>
|
placeholder="请选择标准 ICD 引用"
|
||||||
</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-option
|
||||||
</el-upload>
|
v-for="option in referenceOptions"
|
||||||
|
:key="option.id"
|
||||||
|
:label="option.name"
|
||||||
|
:value="option.id"
|
||||||
|
:disabled="!option.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="角度">
|
<el-form-item label="角度">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="formModel.angle"
|
v-model="formModel.angle"
|
||||||
@@ -68,8 +54,6 @@
|
|||||||
controls-position="right"
|
controls-position="right"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="相位索引">
|
<el-form-item label="相位索引">
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="formModel.usePhaseIndex"
|
v-model="formModel.usePhaseIndex"
|
||||||
@@ -79,8 +63,6 @@
|
|||||||
inactive-text="关闭"
|
inactive-text="关闭"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,9 +76,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref, watch } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
import { ElMessage, type FormInstance, type FormRules, type UploadFile } from 'element-plus'
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||||
import type { ResultData } from '@/api/interface'
|
import type { ResultData } from '@/api/interface'
|
||||||
import { createIcdPathApi, createIcdPathWithFileApi, updateIcdPathApi, updateIcdPathWithFileApi } from '@/api/tools/mmsmapping'
|
import { createIcdPathApi, listIcdPathReferencesApi, updateIcdPathApi } from '@/api/tools/mmsmapping'
|
||||||
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
|
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -117,32 +99,38 @@ const emit = defineEmits<{
|
|||||||
interface IcdPathFormModel {
|
interface IcdPathFormModel {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
path: string
|
|
||||||
angle?: number
|
angle?: number
|
||||||
usePhaseIndex: number
|
usePhaseIndex: number
|
||||||
type?: number
|
type?: number
|
||||||
|
referenceIcdId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IcdPathReferenceSelectOption {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const selectedIcdFile = ref<File | null>(null)
|
const referenceLoading = ref(false)
|
||||||
|
const referenceOptions = ref<IcdPathReferenceSelectOption[]>([])
|
||||||
const formModel = reactive<IcdPathFormModel>({
|
const formModel = reactive<IcdPathFormModel>({
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
path: '',
|
|
||||||
angle: 0,
|
angle: 0,
|
||||||
usePhaseIndex: 0,
|
usePhaseIndex: 0,
|
||||||
type: 3
|
type: 4,
|
||||||
|
referenceIcdId: ''
|
||||||
})
|
})
|
||||||
const icdTypeOptions = [
|
const icdTypeOptions = [
|
||||||
{ label: '手动录入的标准', value: 1 },
|
{ label: '手动录入的标准 ICD', value: 1 },
|
||||||
{ label: '手动录入的非标准', value: 2 },
|
{ label: '手动录入的非标准 ICD', value: 2 },
|
||||||
{ label: '上游解析传递', value: 3 }
|
{ label: '上游解析传递的标准 ICD', value: 3 },
|
||||||
|
{ label: '上游解析传递的非标准 ICD', value: 4 }
|
||||||
]
|
]
|
||||||
const pathRequired = computed(() => !selectedIcdFile.value)
|
|
||||||
const formRules = computed<FormRules<IcdPathFormModel>>(() => ({
|
const formRules = computed<FormRules<IcdPathFormModel>>(() => ({
|
||||||
name: [{ required: true, message: '请输入 ICD 名称', trigger: 'blur' }],
|
name: [{ required: true, message: '请输入 ICD 名称', trigger: 'blur' }],
|
||||||
path: pathRequired.value ? [{ required: true, message: '请输入 ICD 存储路径', trigger: 'blur' }] : []
|
referenceIcdId: [{ required: true, message: '请选择标准 ICD 引用', trigger: 'change' }]
|
||||||
}))
|
}))
|
||||||
const dialogTitle = computed(() => (props.mode === 'create' ? '新增ICD记录' : '编辑ICD记录'))
|
const dialogTitle = computed(() => (props.mode === 'create' ? '新增ICD记录' : '编辑ICD记录'))
|
||||||
|
|
||||||
@@ -162,11 +150,10 @@ const getErrorMessage = (error: unknown) => {
|
|||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
formModel.id = ''
|
formModel.id = ''
|
||||||
formModel.name = ''
|
formModel.name = ''
|
||||||
formModel.path = ''
|
|
||||||
formModel.angle = 0
|
formModel.angle = 0
|
||||||
formModel.usePhaseIndex = 0
|
formModel.usePhaseIndex = 0
|
||||||
formModel.type = 3
|
formModel.type = 4
|
||||||
selectedIcdFile.value = null
|
formModel.referenceIcdId = ''
|
||||||
formRef.value?.clearValidate()
|
formRef.value?.clearValidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,38 +163,57 @@ const fillForm = (record: MmsMapping.IcdPathRecord | null) => {
|
|||||||
|
|
||||||
formModel.id = record.id || ''
|
formModel.id = record.id || ''
|
||||||
formModel.name = record.name || ''
|
formModel.name = record.name || ''
|
||||||
formModel.path = record.path || ''
|
|
||||||
formModel.angle = record.angle ?? 0
|
formModel.angle = record.angle ?? 0
|
||||||
formModel.usePhaseIndex = record.usePhaseIndex ?? 0
|
formModel.usePhaseIndex = record.usePhaseIndex ?? 0
|
||||||
formModel.type = record.type
|
formModel.type = record.type
|
||||||
|
formModel.referenceIcdId = record.referenceIcdId || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
visible => {
|
visible => {
|
||||||
if (visible) fillForm(props.record)
|
if (!visible) return
|
||||||
|
|
||||||
|
fillForm(props.record)
|
||||||
|
loadReferenceOptions()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const buildSavePayload = (): MmsMapping.CreateIcdPathRequest => ({
|
const buildSavePayload = (): MmsMapping.CreateIcdPathRequest => ({
|
||||||
name: formModel.name.trim(),
|
name: formModel.name.trim(),
|
||||||
path: formModel.path.trim() || selectedIcdFile.value?.name || '',
|
|
||||||
angle: formModel.angle,
|
angle: formModel.angle,
|
||||||
usePhaseIndex: formModel.usePhaseIndex,
|
usePhaseIndex: formModel.usePhaseIndex,
|
||||||
type: formModel.type
|
type: formModel.type,
|
||||||
|
referenceIcdId: formModel.referenceIcdId
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
emit('update:visible', false)
|
emit('update:visible', false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleIcdFileChange = (uploadFile: UploadFile) => {
|
const loadReferenceOptions = async () => {
|
||||||
selectedIcdFile.value = uploadFile.raw || null
|
referenceLoading.value = true
|
||||||
formRef.value?.clearValidate('path')
|
|
||||||
|
try {
|
||||||
|
const response = await listIcdPathReferencesApi()
|
||||||
|
const records = unwrapApiPayload<MmsMapping.IcdPathReferenceOption[]>(response) || []
|
||||||
|
|
||||||
|
referenceOptions.value = records.reduce<IcdPathReferenceSelectOption[]>((result, option) => {
|
||||||
|
if (option.id && option.name) {
|
||||||
|
result.push({
|
||||||
|
id: option.id,
|
||||||
|
name: option.name
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleIcdFileRemove = () => {
|
return result
|
||||||
selectedIcdFile.value = null
|
}, [])
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getErrorMessage(error))
|
||||||
|
referenceOptions.value = []
|
||||||
|
} finally {
|
||||||
|
referenceLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@@ -225,22 +231,6 @@ const handleSave = async () => {
|
|||||||
const payload = buildSavePayload()
|
const payload = buildSavePayload()
|
||||||
let response: ResultData<boolean> | boolean
|
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 =
|
response =
|
||||||
props.mode === 'create'
|
props.mode === 'create'
|
||||||
? await createIcdPathApi(payload)
|
? await createIcdPathApi(payload)
|
||||||
@@ -248,7 +238,6 @@ const handleSave = async () => {
|
|||||||
...payload,
|
...payload,
|
||||||
id: formModel.id
|
id: formModel.id
|
||||||
})
|
})
|
||||||
}
|
|
||||||
const saved = unwrapApiPayload<boolean>(response)
|
const saved = unwrapApiPayload<boolean>(response)
|
||||||
|
|
||||||
if (saved === false) {
|
if (saved === false) {
|
||||||
|
|||||||
@@ -33,12 +33,17 @@ const checks = [
|
|||||||
[
|
[
|
||||||
'ICD path check dialog only requires standard mapping when consistency check is visible',
|
'ICD path check dialog only requires standard mapping when consistency check is visible',
|
||||||
() =>
|
() =>
|
||||||
/standardMappingJson:\s*computed\(\(\)\s*=>\s*\(?showConsistencyCheck\.value\s*\?\s*props\.activeIcdPathRecord\?\.jsonStr\?\.trim\(\)\s*\|\|\s*''\s*:\s*''\)?\)/.test(
|
/:standard-mapping-json="activeIcdPathMappingJson"/.test(pageSource) &&
|
||||||
|
/:standard-mapping-name="activeIcdPathMappingName"/.test(pageSource) &&
|
||||||
|
/standardMappingJson:\s*string/.test(checkDialogSource) &&
|
||||||
|
/standardMappingName:\s*string/.test(checkDialogSource) &&
|
||||||
|
/standardMappingJson:\s*computed\(\(\)\s*=>\s*\(?showConsistencyCheck\.value\s*\?\s*props\.standardMappingJson\?\.trim\(\)\s*\|\|\s*''\s*:\s*''\)?\)/.test(
|
||||||
checkDialogSource
|
checkDialogSource
|
||||||
) &&
|
) &&
|
||||||
/standardMappingName:\s*computed\(\(\)\s*=>\s*\(?showConsistencyCheck\.value\s*\?\s*props\.activeIcdPathRecord\?\.name\?\.trim\(\)\s*\|\|\s*''\s*:\s*''\)?\)/.test(
|
/standardMappingName:\s*computed\(\(\)\s*=>\s*\(?showConsistencyCheck\.value\s*\?\s*props\.standardMappingName\?\.trim\(\)\s*\|\|\s*''\s*:\s*''\)?\)/.test(
|
||||||
checkDialogSource
|
checkDialogSource
|
||||||
)
|
) &&
|
||||||
|
!/props\.activeIcdPathRecord\?\.jsonStr/.test(checkDialogSource)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
|
||||||
|
const rootDir = process.cwd()
|
||||||
|
const pageDir = path.resolve(rootDir, 'frontend/src/views/tools/mmsMapping')
|
||||||
|
const resultPanelPath = path.join(pageDir, 'components/MappingResultPanel.vue')
|
||||||
|
const checkDialogPath = path.join(pageDir, 'components/IcdPathCheckDialog.vue')
|
||||||
|
const flowPath = path.join(pageDir, 'utils/useMmsMappingFlow.ts')
|
||||||
|
|
||||||
|
const resultPanelSource = fs.readFileSync(resultPanelPath, 'utf8')
|
||||||
|
const checkDialogSource = fs.readFileSync(checkDialogPath, 'utf8')
|
||||||
|
const flowSource = fs.readFileSync(flowPath, 'utf8')
|
||||||
|
|
||||||
|
const checks = [
|
||||||
|
[
|
||||||
|
'ICD consistency issue dialog reviews the current issue list without clearing it on confirm',
|
||||||
|
() =>
|
||||||
|
!/reviewingIcdConsistencyProblemList/.test(resultPanelSource) &&
|
||||||
|
/v-for="\([^"]+in\s+icdConsistencyProblemList"/.test(resultPanelSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ICD consistency issue confirm emits an acknowledgement only',
|
||||||
|
() =>
|
||||||
|
/confirmIcdConsistencyProblems/.test(resultPanelSource) &&
|
||||||
|
/emit\('confirm-icd-consistency-problems'\)/.test(resultPanelSource) &&
|
||||||
|
!/emit\('confirm-icd-consistency-problems',/.test(resultPanelSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ICD consistency issue dialog has explicit confirm action',
|
||||||
|
() => /confirmIcdConsistencyProblems/.test(resultPanelSource) && /event: 'confirm-icd-consistency-problems'/.test(resultPanelSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ICD consistency indicator shows remaining issue count on the warning icon',
|
||||||
|
() => /el-badge[\s\S]*icdConsistencyProblemCount/.test(resultPanelSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ICD consistency issue dialog supports removing a single reviewed issue',
|
||||||
|
() =>
|
||||||
|
/@click="emit\('remove-icd-consistency-problem',\s*index\)"/.test(resultPanelSource) &&
|
||||||
|
/event:\s*'remove-icd-consistency-problem',\s*index:\s*number/.test(resultPanelSource) &&
|
||||||
|
/handleRemoveIcdConsistencyProblem/.test(flowSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ICD consistency issue removal is wired from dialog host to flow state',
|
||||||
|
() =>
|
||||||
|
/@remove-icd-consistency-problem="handleRemoveIcdConsistencyProblemEvent"/.test(checkDialogSource) &&
|
||||||
|
/handleRemoveIcdConsistencyProblemEvent/.test(checkDialogSource) &&
|
||||||
|
/handleRemoveIcdConsistencyProblem/.test(flowSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ICD consistency check opens the issue dialog when failed',
|
||||||
|
() =>
|
||||||
|
/shouldOpenIcdConsistencyProblems/.test(resultPanelSource) &&
|
||||||
|
/shouldOpenIcdConsistencyProblems\.value \+= 1/.test(flowSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'save is gated until failed ICD consistency issues are confirmed',
|
||||||
|
() => /hasConfirmedIcdConsistencyProblems/.test(flowSource) && /!hasConfirmedIcdConsistencyProblems\.value/.test(flowSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'reviewed ICD consistency result follows the remaining reviewed issue list',
|
||||||
|
() =>
|
||||||
|
/reviewedIcdConsistencyResult/.test(flowSource) &&
|
||||||
|
/if\s*\(!lastIcdConsistencyCheckResult\.value\)\s*return\s+1/.test(flowSource) &&
|
||||||
|
/return\s+reviewedIcdConsistencyProblemList\.value\.length\s*\?\s*0\s*:\s*1/.test(flowSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'save payload preserves the remaining reviewed issue list after manual removal',
|
||||||
|
() =>
|
||||||
|
/buildReviewedIcdCheckMsg/.test(flowSource) &&
|
||||||
|
/const\s+reviewedIssues\s*=\s*reviewedIcdConsistencyProblemList\.value/.test(flowSource) &&
|
||||||
|
/details:\s*reviewedIssues/.test(flowSource) &&
|
||||||
|
/issuesJson:\s*reviewedIssues\.length/.test(flowSource)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
const failedChecks = checks.filter(([, check]) => !check()).map(([name]) => name)
|
||||||
|
|
||||||
|
if (failedChecks.length) {
|
||||||
|
console.error('ICD consistency issue review contract failed:')
|
||||||
|
failedChecks.forEach(name => console.error(`- ${name}`))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('ICD consistency issue review contract passed.')
|
||||||
@@ -55,6 +55,33 @@ const checks = [
|
|||||||
['mmsMapping page uses ProTable request API', () => /<ProTable[\s\S]*ref="proTable"[\s\S]*:request-api="getTableList"/.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 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 table header exposes create and batch delete', () => /<template\s+#tableHeader="scope">[\s\S]*openCreateDialog[\s\S]*handleBatchDelete\(scope\.selectedListIds\)/.test(pageSource)],
|
||||||
|
['mmsMapping table header exports selected ICD records as SQL', () => /<template\s+#tableHeader="scope">[\s\S]*handleExportSelectedSql\(scope\.selectedList\)/.test(pageSource)],
|
||||||
|
['mmsMapping table header exports selected ICD records as JSON', () => /<template\s+#tableHeader="scope">[\s\S]*handleExportSelectedJson\(scope\.selectedList\)/.test(pageSource)],
|
||||||
|
['ICD SQL export targets icd_path table', () => /INSERT INTO\s+\\?`icd_path\\?`/.test(pageSource)],
|
||||||
|
['ICD SQL export escapes single quotes', () => /replace\(/.test(pageSource) && /''/.test(pageSource)],
|
||||||
|
['ICD SQL export downloads sql file', () => /downloadTextFile[\s\S]*\.sql[\s\S]*application\/sql;charset=utf-8/.test(pageSource)],
|
||||||
|
[
|
||||||
|
'ICD JSON export includes all database fields and detail payloads',
|
||||||
|
() =>
|
||||||
|
/buildIcdPathJsonRecord/.test(pageSource) &&
|
||||||
|
/id:\s*row\.id/.test(pageSource) &&
|
||||||
|
/name:\s*row\.name/.test(pageSource) &&
|
||||||
|
/angle:\s*row\.angle/.test(pageSource) &&
|
||||||
|
/usePhaseIndex:\s*row\.usePhaseIndex/.test(pageSource) &&
|
||||||
|
/state:\s*row\.state/.test(pageSource) &&
|
||||||
|
/jsonStr:\s*detail\?\.jsonStr/.test(pageSource) &&
|
||||||
|
/xmlStr:\s*detail\?\.xmlStr/.test(pageSource) &&
|
||||||
|
/result:\s*row\.result/.test(pageSource) &&
|
||||||
|
/msg:\s*row\.msg/.test(pageSource) &&
|
||||||
|
/type:\s*row\.type/.test(pageSource) &&
|
||||||
|
/referenceIcdId:\s*row\.referenceIcdId/.test(pageSource) &&
|
||||||
|
/createBy:\s*row\.createBy/.test(pageSource) &&
|
||||||
|
/createTime:\s*row\.createTime/.test(pageSource) &&
|
||||||
|
/updateBy:\s*row\.updateBy/.test(pageSource) &&
|
||||||
|
/updateTime:\s*row\.updateTime/.test(pageSource)
|
||||||
|
],
|
||||||
|
['ICD JSON export encodes ICD text as Base64', () => /encodeTextToBase64/.test(pageSource) && /TextEncoder/.test(pageSource) && /detail\?\.icdText/.test(pageSource)],
|
||||||
|
['ICD JSON export downloads json file', () => /handleExportSelectedJson[\s\S]*\.json[\s\S]*application\/json;charset=utf-8/.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 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 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)],
|
['mmsMapping uses ICD path check dialog', () => /import\s+IcdPathCheckDialog\s+from\s+'\.\/components\/IcdPathCheckDialog\.vue'/.test(pageSource) && /<IcdPathCheckDialog[\s\S]*@saved="refreshIcdPaths"/.test(pageSource)],
|
||||||
@@ -67,17 +94,47 @@ const checks = [
|
|||||||
/createIcdPathWithFileApi/.test(apiSource) &&
|
/createIcdPathWithFileApi/.test(apiSource) &&
|
||||||
/updateIcdPathWithFileApi/.test(apiSource) &&
|
/updateIcdPathWithFileApi/.test(apiSource) &&
|
||||||
/deleteIcdPathsApi/.test(apiSource) &&
|
/deleteIcdPathsApi/.test(apiSource) &&
|
||||||
/saveIcdPathCheckResultApi/.test(apiSource)
|
/saveIcdPathCheckResultApi/.test(apiSource) &&
|
||||||
|
/getIcdPathCheckMsgApi/.test(apiSource) &&
|
||||||
|
/getIcdPathMappingDetailApi/.test(apiSource) &&
|
||||||
|
/listIcdPathReferencesApi/.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 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) && /\/api\/mms-mapping\/icd-paths\/\$\{id\}\/icd-check-msg/.test(apiSource) && /\/api\/mms-mapping\/icd-paths\/\$\{id\}\/mapping-detail/.test(apiSource) && /\/api\/mms-mapping\/icd-paths\/reference-list/.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 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 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 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 reference option type is defined', () => /interface\s+IcdPathReferenceOption[\s\S]*id\?:\s*string[\s\S]*name\?:\s*string/.test(typeSource)],
|
||||||
|
['ICD path save requests include standard reference ID', () => /CreateIcdPathRequest[\s\S]*referenceIcdId\?:\s*string/.test(typeSource) && /buildSavePayload[\s\S]*referenceIcdId:\s*formModel\.referenceIcdId/.test(formDialogSource)],
|
||||||
|
['ICD path mapping detail response type is defined', () => /interface\s+IcdPathMappingDetailResponse[\s\S]*id\?:\s*string[\s\S]*name\?:\s*string[\s\S]*jsonStr\?:\s*string[\s\S]*xmlStr\?:\s*string[\s\S]*icdText\?:\s*string/.test(typeSource)],
|
||||||
|
[
|
||||||
|
'ICD path type options cover manual and upstream standard states',
|
||||||
|
() =>
|
||||||
|
/icdTypeOptions\s*=\s*\[[\s\S]*手动录入的标准 ICD[\s\S]*value:\s*1[\s\S]*手动录入的非标准 ICD[\s\S]*value:\s*2[\s\S]*上游解析传递的标准 ICD[\s\S]*value:\s*3[\s\S]*上游解析传递的非标准 ICD[\s\S]*value:\s*4/.test(
|
||||||
|
pageSource
|
||||||
|
) &&
|
||||||
|
/icdTypeOptions\s*=\s*\[[\s\S]*手动录入的标准 ICD[\s\S]*value:\s*1[\s\S]*手动录入的非标准 ICD[\s\S]*value:\s*2[\s\S]*上游解析传递的标准 ICD[\s\S]*value:\s*3[\s\S]*上游解析传递的非标准 ICD[\s\S]*value:\s*4/.test(
|
||||||
|
formDialogSource
|
||||||
|
)
|
||||||
|
],
|
||||||
['ICD path type renders readable text in table', () => /getIcdTypeText\(scope\.row\.type\)/.test(pageSource)],
|
['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 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)],
|
['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 path form requires standard reference select after ICD type', () => /<el-form-item label="ICD类型"[\s\S]*<\/el-form-item>\s*<el-form-item label="标准ICD引用" prop="referenceIcdId">[\s\S]*<el-select\s+v-model="formModel\.referenceIcdId"[\s\S]*v-for="option in referenceOptions"[\s\S]*:label="option\.name"[\s\S]*:value="option\.id"/.test(formDialogSource) && /referenceIcdId:\s*\[\{\s*required:\s*true,\s*message:\s*'请选择标准 ICD 引用'/.test(formDialogSource)],
|
||||||
|
['ICD path form loads and backfills standard reference ID', () => /listIcdPathReferencesApi/.test(formDialogSource) && /loadReferenceOptions/.test(formDialogSource) && /formModel\.referenceIcdId\s*=\s*record\.referenceIcdId\s*\|\|\s*''/.test(formDialogSource)],
|
||||||
|
['ICD path create and edit form uses single column layout', () => !/<el-col\s+:span="12"/.test(formDialogSource)],
|
||||||
|
['new ICD path defaults to upstream parsed non-standard type', () => /type:\s*4/.test(formDialogSource) && /formModel\.type\s*=\s*4/.test(formDialogSource)],
|
||||||
|
[
|
||||||
|
'ICD path field is removed from page display form and request payloads',
|
||||||
|
() =>
|
||||||
|
!/prop:\s*'path'/.test(pageSource) &&
|
||||||
|
!/label:\s*'ICD璺緞'/.test(pageSource) &&
|
||||||
|
!/ICD 瀛樺偍璺緞/.test(formDialogSource) &&
|
||||||
|
!/formModel\.path/.test(formDialogSource) &&
|
||||||
|
!/path:\s*formModel/.test(formDialogSource) &&
|
||||||
|
!/path:\s*row\.path/.test(pageSource) &&
|
||||||
|
!/IcdPathRecord[\s\S]*path\?:/.test(typeSource) &&
|
||||||
|
!/CreateIcdPathRequest[\s\S]*path:/.test(typeSource)
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'ICD dialog action buttons match parse ICD primary style',
|
'ICD dialog action buttons match parse ICD primary style',
|
||||||
() =>
|
() =>
|
||||||
@@ -86,25 +143,89 @@ const checks = [
|
|||||||
isPrimarySolidButton(resultPanelSource, 'saveIcdCheckResultText')
|
isPrimarySolidButton(resultPanelSource, 'saveIcdCheckResultText')
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'ICD table activation column calls activation handler before operation',
|
'ICD table removes activation and create time display columns',
|
||||||
() =>
|
() =>
|
||||||
/prop:\s*'activation'[\s\S]*label:\s*'激活'[\s\S]*render:\s*scope\s*=>\s*renderActivationStatus\(scope\.row\)[\s\S]*prop:\s*'operation'/.test(
|
!/prop:\s*'activation'[\s\S]*label:\s*'激活'/.test(pageSource) &&
|
||||||
pageSource
|
!/renderActivationStatus/.test(pageSource) &&
|
||||||
) && /renderActivationStatus[\s\S]*handleActivateIcdPath\(row\)/.test(pageSource)
|
!/prop:\s*'createTime'[\s\S]*label:\s*'创建时间'/.test(pageSource)
|
||||||
],
|
],
|
||||||
['ICD activation updates current record to standard type', () => /handleActivateIcdPath/.test(pageSource) && /type:\s*1/.test(pageSource) && /updateIcdPathApi/.test(pageSource)],
|
[
|
||||||
['ICD activation requires mapping JSON before setting standard type', () => /handleActivateIcdPath[\s\S]*row\.jsonStr\?\.trim\(\)[\s\S]*不能激活/.test(pageSource)],
|
'ICD table shows mapping detail, reference standard ICD, then check result',
|
||||||
['ICD page fetches active standard record independent of current table filters', () => /refreshActiveIcdPathRecord/.test(pageSource) && /listIcdPathsApi\(\{\s*type:\s*1\s*\}\)/.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)],
|
/listIcdPathReferencesApi/.test(pageSource) &&
|
||||||
['ICD path form allows uploaded file name to fill missing path', () => /pathRequired/.test(formDialogSource) && /selectedIcdFile\.value\?\.name/.test(formDialogSource)],
|
/referenceIcdNameMap/.test(pageSource) &&
|
||||||
|
/loadReferenceIcdNameMap/.test(pageSource) &&
|
||||||
|
/renderReferenceIcdName/.test(pageSource) &&
|
||||||
|
/prop:\s*'mappingDetail'[\s\S]*width:\s*150[\s\S]*prop:\s*'referenceIcdId'[\s\S]*width:\s*150[\s\S]*prop:\s*'result'[\s\S]*width:\s*150/.test(
|
||||||
|
pageSource
|
||||||
|
) &&
|
||||||
|
/renderReferenceIcdName[\s\S]*openReferenceIcdDetail\(row\)/.test(pageSource) &&
|
||||||
|
/openReferenceIcdDetail[\s\S]*openMappingDetail\(\{[\s\S]*id:\s*row\.referenceIcdId[\s\S]*name:\s*getReferenceIcdName\(row\.referenceIcdId\)/.test(
|
||||||
|
pageSource
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ICD mapping detail column appears before operation',
|
||||||
|
() =>
|
||||||
|
/prop:\s*'mappingDetail'[\s\S]*label:\s*'映射文件详情'[\s\S]*render:\s*scope\s*=>\s*renderMappingDetailAction\(scope\.row\)[\s\S]*prop:\s*'operation'/.test(
|
||||||
|
pageSource
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ICD mapping detail dialog uses three documented tabs',
|
||||||
|
() =>
|
||||||
|
(/v-model="mappingDetailDialogVisible"/.test(pageSource) &&
|
||||||
|
/<el-tab-pane\s+[^>]*name="json"/.test(pageSource) &&
|
||||||
|
/<el-tab-pane\s+[^>]*name="xml"/.test(pageSource) &&
|
||||||
|
/<el-tab-pane\s+[^>]*name="icd"/.test(pageSource)) ||
|
||||||
|
/v-model="mappingDetailDialogVisible"/.test(pageSource) &&
|
||||||
|
/<el-tab-pane\s+label="JSON映射"\s+name="json"/.test(pageSource) &&
|
||||||
|
/<el-tab-pane\s+label="XML映射"\s+name="xml"/.test(pageSource) &&
|
||||||
|
/<el-tab-pane\s+label="ICD源文件"\s+name="icd"/.test(pageSource) &&
|
||||||
|
/currentMappingDetail\?\.jsonStr/.test(pageSource) &&
|
||||||
|
/currentMappingDetail\?\.xmlStr/.test(pageSource) &&
|
||||||
|
/currentMappingDetail\?\.icdText/.test(pageSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ICD mapping detail JSON tab uses standard JSON viewer',
|
||||||
|
() =>
|
||||||
|
/import JsonMappingTree from '\.\/components\/JsonMappingTree\.vue'/.test(pageSource) &&
|
||||||
|
/formatMappingDetailJsonSource/.test(pageSource) &&
|
||||||
|
/mappingDetailJsonSource/.test(pageSource) &&
|
||||||
|
/<JsonMappingTree\s+:source="mappingDetailJsonSource"/.test(pageSource) &&
|
||||||
|
!/<pre class="mapping-detail-dialog__content">\{\{ currentMappingDetail\?\.jsonStr/.test(pageSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ICD mapping detail source tab uses raw text viewer without JSON tree parsing',
|
||||||
|
() =>
|
||||||
|
!/mappingDetailIcdSource/.test(pageSource) &&
|
||||||
|
!/<JsonMappingTree\s+:source="mappingDetailIcdSource"/.test(pageSource) &&
|
||||||
|
/<pre class="mapping-detail-dialog__content">\{\{ currentMappingDetail\?\.icdText/.test(pageSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ICD mapping detail tabs keep a single scroll owner',
|
||||||
|
() =>
|
||||||
|
!/<el-scrollbar\s+max-height="420px">[\s\S]*?<JsonMappingTree/.test(pageSource) &&
|
||||||
|
!/<el-scrollbar\s+max-height="420px">[\s\S]*?mapping-detail-dialog__content/.test(pageSource) &&
|
||||||
|
/class="mapping-detail-dialog__json"/.test(pageSource) &&
|
||||||
|
/\.mapping-detail-dialog__content\s*\{[\s\S]*max-height:\s*420px[\s\S]*overflow:\s*auto/.test(
|
||||||
|
pageSource
|
||||||
|
)
|
||||||
|
],
|
||||||
|
['ICD standard type helper treats type 1 and 3 as standard', () => /isStandardIcdType\s*=\s*\(type\?:\s*number\)\s*=>\s*type\s*===\s*1\s*\|\|\s*type\s*===\s*3/.test(pageSource)],
|
||||||
|
['ICD non-standard type helper treats type 2 and 4 as activatable', () => /isNonStandardIcdType\s*=\s*\(type\?:\s*number\)\s*=>\s*type\s*===\s*2\s*\|\|\s*type\s*===\s*4/.test(pageSource)],
|
||||||
|
['ICD page fetches active standard records independent of current table filters', () => /refreshActiveIcdPathRecord/.test(pageSource) && /listIcdPathsApi\(\{\s*type:\s*1\s*\}\)/.test(pageSource) && /listIcdPathsApi\(\{\s*type:\s*3\s*\}\)/.test(pageSource)],
|
||||||
|
['ICD path form does not expose file selection', () => !/<el-upload[\s\S]*ICD/.test(formDialogSource) && !/handleIcdFileChange/.test(formDialogSource) && !/selectedIcdFile/.test(formDialogSource)],
|
||||||
|
['ICD path form saves JSON request only', () => /createIcdPathApi/.test(formDialogSource) && /updateIcdPathApi/.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 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)],
|
['ICD check dialog receives detail-loaded standard mapping JSON', () => /:standard-mapping-json="activeIcdPathMappingJson"/.test(pageSource) && /standardMappingJson:\s*string/.test(checkDialogSource) && !/activeIcdPathRecord:\s*MmsMapping\.IcdPathRecord\s*\|\s*null/.test(checkDialogSource) && /standardMappingJson/.test(flowSource)],
|
||||||
[
|
[
|
||||||
'non-standard and upstream ICD types show consistency check action',
|
'non-standard and upstream ICD types show consistency check action',
|
||||||
() =>
|
() =>
|
||||||
/showConsistencyCheck/.test(checkDialogSource) &&
|
/showConsistencyCheck/.test(checkDialogSource) &&
|
||||||
/props\.icdPathType\s*===\s*2[\s\S]*props\.icdPathType\s*===\s*3/.test(checkDialogSource) &&
|
/props\.icdPathType\s*===\s*2[\s\S]*props\.icdPathType\s*===\s*4/.test(checkDialogSource) &&
|
||||||
!/props\.icdPathType\s*===\s*1/.test(checkDialogSource) &&
|
!/props\.icdPathType\s*===\s*1/.test(checkDialogSource) &&
|
||||||
|
!/props\.icdPathType\s*===\s*3/.test(checkDialogSource) &&
|
||||||
/:show-icd-check-action="showConsistencyCheck"/.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)],
|
['ICD check action calls backend consistency check before save', () => /@icd-check="handleIcdConsistencyCheck"/.test(checkDialogSource) && /handleIcdConsistencyCheck/.test(flowSource) && /checkIcdJsonConsistencyApi/.test(flowSource) && /lastIcdConsistencyCheckResult/.test(flowSource)],
|
||||||
@@ -114,13 +235,14 @@ const checks = [
|
|||||||
/icdConsistencyStatus/.test(flowSource) &&
|
/icdConsistencyStatus/.test(flowSource) &&
|
||||||
/:icd-consistency-status="icdConsistencyStatus"/.test(checkDialogSource) &&
|
/:icd-consistency-status="icdConsistencyStatus"/.test(checkDialogSource) &&
|
||||||
/icd-consistency-indicator/.test(resultPanelSource) &&
|
/icd-consistency-indicator/.test(resultPanelSource) &&
|
||||||
/icdConsistencyProblemDialogVisible\s*=\s*true/.test(resultPanelSource)
|
/openIcdConsistencyProblems/.test(resultPanelSource)
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'ICD check issues are separated from JSON mapping problems',
|
'ICD check issues are separated from JSON mapping problems',
|
||||||
() =>
|
() =>
|
||||||
/const\s+jsonMappingProblemList\s*=\s*computed\(\(\)\s*=>\s*\[/.test(flowSource) &&
|
/const\s+jsonMappingProblemList\s*=\s*computed\(\(\)\s*=>\s*\[/.test(flowSource) &&
|
||||||
/const\s+icdConsistencyProblemList\s*=\s*computed\(\(\)\s*=>\s*lastIcdConsistencyCheckResult\.value\?\.issues/.test(
|
/const\s+icdConsistencyProblemList\s*=\s*computed\(\(\)\s*=>/.test(flowSource) &&
|
||||||
|
/reviewedIcdConsistencyProblemList\.value/.test(
|
||||||
flowSource
|
flowSource
|
||||||
) &&
|
) &&
|
||||||
!/const\s+problemList\s*=\s*computed\(\(\)\s*=>\s*\[[\s\S]*lastIcdConsistencyCheckResult\.value\?\.issues/.test(
|
!/const\s+problemList\s*=\s*computed\(\(\)\s*=>\s*\[[\s\S]*lastIcdConsistencyCheckResult\.value\?\.issues/.test(
|
||||||
@@ -150,7 +272,7 @@ const checks = [
|
|||||||
[
|
[
|
||||||
'ICD path check msg uses JSON object payload and readable table render',
|
'ICD path check msg uses JSON object payload and readable table render',
|
||||||
() =>
|
() =>
|
||||||
/interface\s+IcdCheckMsg[\s\S]*summary\?:\s*string[\s\S]*details\?:\s*string\[\][\s\S]*\[key:\s*string\]:\s*unknown/.test(
|
/interface\s+IcdCheckMsg[\s\S]*summary\?:\s*string[\s\S]*details\?:\s*unknown\[\][\s\S]*\[key:\s*string\]:\s*unknown/.test(
|
||||||
typeSource
|
typeSource
|
||||||
) &&
|
) &&
|
||||||
/SaveIcdPathCheckResultRequest[\s\S]*msg\?:\s*IcdCheckMsg/.test(typeSource) &&
|
/SaveIcdPathCheckResultRequest[\s\S]*msg\?:\s*IcdCheckMsg/.test(typeSource) &&
|
||||||
@@ -163,6 +285,24 @@ const checks = [
|
|||||||
/renderIcdCheckMsg/.test(pageSource) &&
|
/renderIcdCheckMsg/.test(pageSource) &&
|
||||||
/JSON\.stringify\(value\)/.test(pageSource)
|
/JSON\.stringify\(value\)/.test(pageSource)
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'ICD inconsistent result opens saved check detail dialog',
|
||||||
|
() =>
|
||||||
|
/renderIcdResult[\s\S]*row\.result\s*===\s*0[\s\S]*handleOpenIcdCheckMsg\(row\)/.test(pageSource) &&
|
||||||
|
/getIcdPathCheckMsgApi\(row\.id\)/.test(pageSource) &&
|
||||||
|
/v-model="icdCheckMsgDialogVisible"/.test(pageSource) &&
|
||||||
|
/currentCheckMsgDetail/.test(pageSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ICD check msg dialog shows formatted raw JSON without diff list',
|
||||||
|
() =>
|
||||||
|
!/icdCheckMsgDetails/.test(pageSource) &&
|
||||||
|
!/String\(item\)/.test(pageSource) &&
|
||||||
|
/normalizeIcdCheckMsgJsonValue/.test(pageSource) &&
|
||||||
|
/formatIcdCheckMsgJson/.test(pageSource) &&
|
||||||
|
/JSON\.parse\(trimmedValue\)/.test(pageSource) &&
|
||||||
|
/JSON\.stringify\(normalizeIcdCheckMsgJsonValue\(detail\),\s*null,\s*4\)/.test(pageSource)
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'ICD path check save writes parsed ICD document back to path record',
|
'ICD path check save writes parsed ICD document back to path record',
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/* 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/mmsMapping/index.vue')
|
||||||
|
const source = fs.readFileSync(pageFile, 'utf8')
|
||||||
|
|
||||||
|
const columnsStart = source.indexOf('const columns = reactive')
|
||||||
|
const columnsEnd = source.indexOf('const buildListParams', columnsStart)
|
||||||
|
const columnsSource = columnsStart >= 0 && columnsEnd > columnsStart ? source.slice(columnsStart, columnsEnd) : ''
|
||||||
|
|
||||||
|
const getColumnIndex = prop => columnsSource.indexOf(`prop: '${prop}'`)
|
||||||
|
|
||||||
|
const checks = [
|
||||||
|
[
|
||||||
|
'ICD type column is widened to avoid text clipping',
|
||||||
|
() => /prop: 'type'[\s\S]*?label: 'ICD类型'[\s\S]*?width: 220/.test(columnsSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Mapping detail column uses width 150',
|
||||||
|
() => /prop: 'mappingDetail'[\s\S]*?label: '映射文件详情'[\s\S]*?width: 150/.test(columnsSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Reference ICD column uses width 150',
|
||||||
|
() => /prop: 'referenceIcdId'[\s\S]*?label: '参照标准ICD'[\s\S]*?width: 150/.test(columnsSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ICD result column uses width 150',
|
||||||
|
() => /prop: 'result'[\s\S]*?label: '校验结论'[\s\S]*?width: 150/.test(columnsSource)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Detail, reference ICD, and result columns keep requested order',
|
||||||
|
() => {
|
||||||
|
const mappingDetailIndex = getColumnIndex('mappingDetail')
|
||||||
|
const referenceIcdIndex = getColumnIndex('referenceIcdId')
|
||||||
|
const resultIndex = getColumnIndex('result')
|
||||||
|
|
||||||
|
return (
|
||||||
|
mappingDetailIndex >= 0 &&
|
||||||
|
referenceIcdIndex > mappingDetailIndex &&
|
||||||
|
resultIndex > referenceIcdIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
const failures = checks.filter(([, check]) => !check()).map(([name]) => name)
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error('mmsMapping ICD path table columns contract failed:')
|
||||||
|
for (const failure of failures) {
|
||||||
|
console.error(`- ${failure}`)
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('mmsMapping ICD path table columns contract passed')
|
||||||
@@ -20,6 +20,24 @@
|
|||||||
>
|
>
|
||||||
批量删除
|
批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
:icon="Download"
|
||||||
|
:disabled="!scope.isSelected"
|
||||||
|
@click="handleExportSelectedSql(scope.selectedList)"
|
||||||
|
>
|
||||||
|
导出SQL
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
:icon="Download"
|
||||||
|
:disabled="!scope.isSelected"
|
||||||
|
@click="handleExportSelectedJson(scope.selectedList)"
|
||||||
|
>
|
||||||
|
导出JSON
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
<template #operation="{ row }">
|
<template #operation="{ row }">
|
||||||
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
|
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
|
||||||
@@ -42,23 +60,91 @@
|
|||||||
:icd-path-id="currentIcdCheckRecord.id"
|
:icd-path-id="currentIcdCheckRecord.id"
|
||||||
:icd-path-name="currentIcdCheckRecord.name"
|
:icd-path-name="currentIcdCheckRecord.name"
|
||||||
:icd-path-type="currentIcdCheckRecord.type"
|
:icd-path-type="currentIcdCheckRecord.type"
|
||||||
:active-icd-path-record="activeIcdPathRecord"
|
:standard-mapping-json="activeIcdPathMappingJson"
|
||||||
|
:standard-mapping-name="activeIcdPathMappingName"
|
||||||
@saved="refreshIcdPaths"
|
@saved="refreshIcdPaths"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="mappingDetailDialogVisible"
|
||||||
|
title="映射文件详情"
|
||||||
|
width="860px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<div v-loading="mappingDetailLoading" class="mapping-detail-dialog">
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="ICD名称">
|
||||||
|
{{ currentMappingDetail?.name || currentMappingDetailRecordName || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<el-tabs v-model="mappingDetailActiveTab" class="mapping-detail-dialog__tabs">
|
||||||
|
<el-tab-pane label="JSON映射" name="json">
|
||||||
|
<div class="mapping-detail-dialog__json">
|
||||||
|
<JsonMappingTree :source="mappingDetailJsonSource" />
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="XML映射" name="xml">
|
||||||
|
<pre class="mapping-detail-dialog__content">{{ currentMappingDetail?.xmlStr || '暂无 XML 映射内容' }}</pre>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="ICD源文件" name="icd">
|
||||||
|
<pre class="mapping-detail-dialog__content">{{ currentMappingDetail?.icdText || '暂无 ICD 源文件内容' }}</pre>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="mappingDetailDialogVisible = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="icdCheckMsgDialogVisible"
|
||||||
|
title="校验结论详情"
|
||||||
|
width="720px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<div v-loading="icdCheckMsgLoading" class="icd-check-msg-detail">
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="ICD名称">{{ currentCheckMsgRecordName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="结论摘要">
|
||||||
|
{{ icdCheckMsgSummary || '暂无校验结论详情' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div v-if="icdCheckMsgExtraJson" class="icd-check-msg-detail__section">
|
||||||
|
<div class="icd-check-msg-detail__title">原始详情</div>
|
||||||
|
<el-scrollbar max-height="260px">
|
||||||
|
<pre class="icd-check-msg-detail__json">{{ icdCheckMsgExtraJson }}</pre>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="icdCheckMsgDialogVisible = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { Connection, Delete, Edit, Plus } from '@element-plus/icons-vue'
|
import { Connection, Delete, Document, Download, Edit, Plus } from '@element-plus/icons-vue'
|
||||||
import { ElMessage, ElMessageBox, type TagProps } from 'element-plus'
|
import { ElMessage, ElMessageBox, type TagProps } from 'element-plus'
|
||||||
import { nextTick, reactive, ref } from 'vue'
|
import { computed, nextTick, reactive, ref } from 'vue'
|
||||||
import type { ResultData } from '@/api/interface'
|
import type { ResultData } from '@/api/interface'
|
||||||
import ProTable from '@/components/ProTable/index.vue'
|
import ProTable from '@/components/ProTable/index.vue'
|
||||||
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
||||||
import { deleteIcdPathsApi, listIcdPathsApi, updateIcdPathApi } from '@/api/tools/mmsmapping'
|
import {
|
||||||
|
deleteIcdPathsApi,
|
||||||
|
getIcdPathCheckMsgApi,
|
||||||
|
getIcdPathMappingDetailApi,
|
||||||
|
listIcdPathReferencesApi,
|
||||||
|
listIcdPathsApi
|
||||||
|
} from '@/api/tools/mmsmapping'
|
||||||
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
|
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
|
||||||
import IcdPathFormDialog from './components/IcdPathFormDialog.vue'
|
import IcdPathFormDialog from './components/IcdPathFormDialog.vue'
|
||||||
import IcdPathCheckDialog from './components/IcdPathCheckDialog.vue'
|
import IcdPathCheckDialog from './components/IcdPathCheckDialog.vue'
|
||||||
|
import JsonMappingTree from './components/JsonMappingTree.vue'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MmsMappingView'
|
name: 'MmsMappingView'
|
||||||
@@ -77,15 +163,56 @@ interface IcdPathTableParams {
|
|||||||
const proTable = ref<ProTableInstance>()
|
const proTable = ref<ProTableInstance>()
|
||||||
const formDialogVisible = ref(false)
|
const formDialogVisible = ref(false)
|
||||||
const icdCheckDialogVisible = ref(false)
|
const icdCheckDialogVisible = ref(false)
|
||||||
|
const icdCheckMsgDialogVisible = ref(false)
|
||||||
|
const icdCheckMsgLoading = ref(false)
|
||||||
|
const mappingDetailDialogVisible = ref(false)
|
||||||
|
const mappingDetailLoading = ref(false)
|
||||||
|
const mappingDetailActiveTab = ref('json')
|
||||||
const formMode = ref<'create' | 'edit'>('create')
|
const formMode = ref<'create' | 'edit'>('create')
|
||||||
const currentFormRecord = ref<MmsMapping.IcdPathRecord | null>(null)
|
const currentFormRecord = ref<MmsMapping.IcdPathRecord | null>(null)
|
||||||
const currentIcdCheckRecord = reactive({
|
const currentIcdCheckRecord = reactive({
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
type: 3
|
type: 4
|
||||||
})
|
})
|
||||||
const activeIcdPathRecord = ref<MmsMapping.IcdPathRecord | null>(null)
|
const activeIcdPathRecord = ref<MmsMapping.IcdPathRecord | null>(null)
|
||||||
const activatingIcdPathId = ref('')
|
const activeIcdPathMappingJson = ref('')
|
||||||
|
const activeIcdPathMappingName = ref('')
|
||||||
|
const referenceIcdNameMap = ref<Record<string, string>>({})
|
||||||
|
const currentCheckMsgRecordName = ref('')
|
||||||
|
const currentCheckMsgDetail = ref<MmsMapping.IcdPathCheckMsgResponse>(null)
|
||||||
|
const currentMappingDetailRecordName = ref('')
|
||||||
|
const currentMappingDetail = ref<MmsMapping.IcdPathMappingDetailResponse | null>(null)
|
||||||
|
const formatMappingDetailJsonSource = (source?: string | null, emptyText = '') => {
|
||||||
|
const text = source?.trim()
|
||||||
|
if (!text) return emptyText
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(text), null, 4)
|
||||||
|
} catch {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mappingDetailJsonSource = computed(() =>
|
||||||
|
formatMappingDetailJsonSource(currentMappingDetail.value?.jsonStr, '暂无 JSON 映射内容')
|
||||||
|
)
|
||||||
|
const ICD_PATH_SQL_COLUMNS = [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'angle',
|
||||||
|
'use_phase_index',
|
||||||
|
'state',
|
||||||
|
'json_str',
|
||||||
|
'xml_str',
|
||||||
|
'result',
|
||||||
|
'msg',
|
||||||
|
'type',
|
||||||
|
'reference_icd_id',
|
||||||
|
'create_by',
|
||||||
|
'create_time',
|
||||||
|
'update_by',
|
||||||
|
'update_time'
|
||||||
|
] as const
|
||||||
|
|
||||||
function unwrapApiPayload<T>(response: ResultData<T> | T): T {
|
function unwrapApiPayload<T>(response: ResultData<T> | T): T {
|
||||||
if (response && typeof response === 'object' && 'data' in response) {
|
if (response && typeof response === 'object' && 'data' in response) {
|
||||||
@@ -129,12 +256,16 @@ const resultOptions = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const icdTypeOptions = [
|
const icdTypeOptions = [
|
||||||
{ label: '手动录入的标准', value: 1 },
|
{ label: '手动录入的标准 ICD', value: 1 },
|
||||||
{ label: '手动录入的非标准', value: 2 },
|
{ label: '手动录入的非标准 ICD', value: 2 },
|
||||||
{ label: '上游解析传递', value: 3 }
|
{ label: '上游解析传递的标准 ICD', value: 3 },
|
||||||
|
{ label: '上游解析传递的非标准 ICD', value: 4 }
|
||||||
]
|
]
|
||||||
|
|
||||||
const getIcdTypeText = (value?: number) => icdTypeOptions.find(option => option.value === value)?.label || '未知类型'
|
const getIcdTypeText = (value?: number) => icdTypeOptions.find(option => option.value === value)?.label || '未知类型'
|
||||||
|
const isStandardIcdType = (type?: number) => type === 1 || type === 3
|
||||||
|
const isNonStandardIcdType = (type?: number) => type === 2 || type === 4
|
||||||
|
const getReferenceIcdName = (id?: string) => (id ? referenceIcdNameMap.value[id] || '' : '')
|
||||||
|
|
||||||
const renderIcdCheckMsg = (value: MmsMapping.IcdPathRecord['msg']) => {
|
const renderIcdCheckMsg = (value: MmsMapping.IcdPathRecord['msg']) => {
|
||||||
if (!value) return ''
|
if (!value) return ''
|
||||||
@@ -148,34 +279,187 @@ const renderIcdCheckMsg = (value: MmsMapping.IcdPathRecord['msg']) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderActivationStatus = (row: MmsMapping.IcdPathRecord) => {
|
const icdCheckMsgSummary = computed(() => {
|
||||||
if (row.type === 1) {
|
const detail = currentCheckMsgDetail.value
|
||||||
|
|
||||||
|
if (!detail) return ''
|
||||||
|
if (typeof detail.summary === 'string' && detail.summary.trim()) return detail.summary.trim()
|
||||||
|
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizeIcdCheckMsgJsonValue = (value: unknown): unknown => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmedValue = value.trim()
|
||||||
|
|
||||||
|
if (!trimmedValue) return value
|
||||||
|
|
||||||
|
try {
|
||||||
|
return normalizeIcdCheckMsgJsonValue(JSON.parse(trimmedValue))
|
||||||
|
} catch {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) return value.map(item => normalizeIcdCheckMsgJsonValue(item))
|
||||||
|
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return Object.entries(value).reduce<Record<string, unknown>>((result, [key, item]) => {
|
||||||
|
result[key] = normalizeIcdCheckMsgJsonValue(item)
|
||||||
|
return result
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatIcdCheckMsgJson = (detail: MmsMapping.IcdPathCheckMsgResponse) => {
|
||||||
|
if (!detail) return ''
|
||||||
|
|
||||||
|
return JSON.stringify(normalizeIcdCheckMsgJsonValue(detail), null, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
const icdCheckMsgExtraJson = computed(() => {
|
||||||
|
const detail = currentCheckMsgDetail.value
|
||||||
|
|
||||||
|
if (!detail) return ''
|
||||||
|
|
||||||
|
return formatIcdCheckMsgJson(detail)
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderIcdResult = (row: MmsMapping.IcdPathRecord) => {
|
||||||
|
if (row.result === 0) {
|
||||||
return (
|
return (
|
||||||
<el-tag type="success" effect="light">
|
<el-tag
|
||||||
已激活
|
class="icd-result-tag--clickable"
|
||||||
|
type="danger"
|
||||||
|
effect="light"
|
||||||
|
onClick={() => handleOpenIcdCheckMsg(row)}
|
||||||
|
>
|
||||||
|
{getIcdResultText(row.result)}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.type === 2) {
|
|
||||||
return (
|
return (
|
||||||
<el-button
|
<el-tag type={getIcdResultTagType(row.result)} effect="light">
|
||||||
type="primary"
|
{getIcdResultText(row.result)}
|
||||||
size="small"
|
</el-tag>
|
||||||
loading={activatingIcdPathId.value === row.id}
|
)
|
||||||
disabled={!row.id}
|
}
|
||||||
onClick={() => handleActivateIcdPath(row)}
|
|
||||||
>
|
const renderReferenceIcdName = (row: MmsMapping.IcdPathRecord) => {
|
||||||
激活
|
const referenceName = getReferenceIcdName(row.referenceIcdId)
|
||||||
|
|
||||||
|
if (!row.referenceIcdId) return '-'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<el-button link type="primary" disabled={!referenceName} onClick={() => openReferenceIcdDetail(row)}>
|
||||||
|
{referenceName || row.referenceIcdId}
|
||||||
</el-button>
|
</el-button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const renderMappingDetailAction = (row: MmsMapping.IcdPathRecord) => (
|
||||||
<el-button type="primary" size="small" disabled>
|
<el-button link type="primary" icon={Document} disabled={!row.id} onClick={() => openMappingDetail(row)}>
|
||||||
激活
|
查看详情
|
||||||
</el-button>
|
</el-button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const stringifySqlValue = (value: unknown) => {
|
||||||
|
if (value === undefined || value === null || value === '') return 'NULL'
|
||||||
|
if (typeof value === 'number') return Number.isFinite(value) ? String(value) : 'NULL'
|
||||||
|
if (typeof value === 'boolean') return value ? '1' : '0'
|
||||||
|
|
||||||
|
const text = typeof value === 'string' ? value : JSON.stringify(value)
|
||||||
|
|
||||||
|
return `'${text.replace(/'/g, "''")}'`
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildIcdPathSqlValues = (
|
||||||
|
row: MmsMapping.IcdPathRecord,
|
||||||
|
detail: MmsMapping.IcdPathMappingDetailResponse | null = null
|
||||||
|
) => [
|
||||||
|
row.id,
|
||||||
|
row.name,
|
||||||
|
row.angle,
|
||||||
|
row.usePhaseIndex,
|
||||||
|
row.state,
|
||||||
|
detail?.jsonStr,
|
||||||
|
detail?.xmlStr,
|
||||||
|
row.result,
|
||||||
|
row.msg,
|
||||||
|
row.type,
|
||||||
|
row.referenceIcdId,
|
||||||
|
row.createBy,
|
||||||
|
row.createTime,
|
||||||
|
row.updateBy,
|
||||||
|
row.updateTime
|
||||||
|
]
|
||||||
|
|
||||||
|
const buildIcdPathInsertSql = (
|
||||||
|
records: Array<{
|
||||||
|
row: MmsMapping.IcdPathRecord
|
||||||
|
detail: MmsMapping.IcdPathMappingDetailResponse | null
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const columns = ICD_PATH_SQL_COLUMNS.map(column => `\`${column}\``).join(', ')
|
||||||
|
|
||||||
|
return records
|
||||||
|
.map(({ row, detail }) => {
|
||||||
|
const values = buildIcdPathSqlValues(row, detail).map(stringifySqlValue).join(', ')
|
||||||
|
|
||||||
|
return `INSERT INTO \`icd_path\` (${columns}) VALUES (${values});`
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodeTextToBase64 = (value?: string) => {
|
||||||
|
if (!value) return ''
|
||||||
|
|
||||||
|
const bytes = new TextEncoder().encode(value)
|
||||||
|
let binaryText = ''
|
||||||
|
|
||||||
|
bytes.forEach(byte => {
|
||||||
|
binaryText += String.fromCharCode(byte)
|
||||||
|
})
|
||||||
|
|
||||||
|
return window.btoa(binaryText)
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildIcdPathJsonRecord = (
|
||||||
|
row: MmsMapping.IcdPathRecord,
|
||||||
|
detail: MmsMapping.IcdPathMappingDetailResponse | null = null
|
||||||
|
) => ({
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
angle: row.angle,
|
||||||
|
usePhaseIndex: row.usePhaseIndex,
|
||||||
|
state: row.state,
|
||||||
|
icd: encodeTextToBase64(detail?.icdText),
|
||||||
|
jsonStr: detail?.jsonStr,
|
||||||
|
xmlStr: detail?.xmlStr,
|
||||||
|
result: row.result,
|
||||||
|
msg: row.msg,
|
||||||
|
type: row.type,
|
||||||
|
referenceIcdId: row.referenceIcdId,
|
||||||
|
createBy: row.createBy,
|
||||||
|
createTime: row.createTime,
|
||||||
|
updateBy: row.updateBy,
|
||||||
|
updateTime: row.updateTime
|
||||||
|
})
|
||||||
|
|
||||||
|
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 columns = reactive<ColumnProps<MmsMapping.IcdPathRecord>[]>([
|
const columns = reactive<ColumnProps<MmsMapping.IcdPathRecord>[]>([
|
||||||
@@ -192,15 +476,14 @@ const columns = reactive<ColumnProps<MmsMapping.IcdPathRecord>[]>([
|
|||||||
label: '关键字',
|
label: '关键字',
|
||||||
order: 1,
|
order: 1,
|
||||||
props: {
|
props: {
|
||||||
placeholder: '请输入 ICD 名称或路径'
|
placeholder: '请输入 ICD 名称'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ prop: 'path', label: 'ICD路径', minWidth: 260 },
|
|
||||||
{
|
{
|
||||||
prop: 'type',
|
prop: 'type',
|
||||||
label: 'ICD类型',
|
label: 'ICD类型',
|
||||||
width: 170,
|
width: 220,
|
||||||
render: scope => getIcdTypeText(scope.row.type),
|
render: scope => getIcdTypeText(scope.row.type),
|
||||||
enum: icdTypeOptions,
|
enum: icdTypeOptions,
|
||||||
isFilterEnum: false,
|
isFilterEnum: false,
|
||||||
@@ -220,15 +503,19 @@ const columns = reactive<ColumnProps<MmsMapping.IcdPathRecord>[]>([
|
|||||||
width: 110,
|
width: 110,
|
||||||
render: scope => getUsePhaseIndexText(scope.row.usePhaseIndex)
|
render: scope => getUsePhaseIndexText(scope.row.usePhaseIndex)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'mappingDetail',
|
||||||
|
label: '映射文件详情',
|
||||||
|
width: 150,
|
||||||
|
render: scope => renderMappingDetailAction(scope.row)
|
||||||
|
},
|
||||||
|
{ prop: 'msg', label: '结论描述', minWidth: 220, isShow: false, render: scope => renderIcdCheckMsg(scope.row.msg) },
|
||||||
|
{ prop: 'referenceIcdId', label: '参照标准ICD', width: 150, render: scope => renderReferenceIcdName(scope.row) },
|
||||||
{
|
{
|
||||||
prop: 'result',
|
prop: 'result',
|
||||||
label: '校验结论',
|
label: '校验结论',
|
||||||
minWidth: 140,
|
width: 150,
|
||||||
render: scope => (
|
render: scope => renderIcdResult(scope.row),
|
||||||
<el-tag type={getIcdResultTagType(scope.row.result)} effect="light">
|
|
||||||
{getIcdResultText(scope.row.result)}
|
|
||||||
</el-tag>
|
|
||||||
),
|
|
||||||
enum: resultOptions,
|
enum: resultOptions,
|
||||||
isFilterEnum: false,
|
isFilterEnum: false,
|
||||||
search: {
|
search: {
|
||||||
@@ -237,16 +524,7 @@ const columns = reactive<ColumnProps<MmsMapping.IcdPathRecord>[]>([
|
|||||||
order: 3
|
order: 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ prop: 'msg', label: '结论描述', minWidth: 220, isShow: false, render: scope => renderIcdCheckMsg(scope.row.msg) },
|
|
||||||
{ prop: 'referenceIcdId', label: '标准ICD引用', minWidth: 170, isShow: false },
|
|
||||||
{ prop: 'createTime', label: '创建时间', minWidth: 170 },
|
|
||||||
{ prop: 'updateTime', label: '更新时间', minWidth: 170, isShow: false },
|
{ prop: 'updateTime', label: '更新时间', minWidth: 170, isShow: false },
|
||||||
{
|
|
||||||
prop: 'activation',
|
|
||||||
label: '激活',
|
|
||||||
width: 110,
|
|
||||||
render: scope => renderActivationStatus(scope.row)
|
|
||||||
},
|
|
||||||
{ prop: 'operation', label: '操作', fixed: 'right', width: 240 }
|
{ prop: 'operation', label: '操作', fixed: 'right', width: 240 }
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -261,13 +539,15 @@ const buildListParams = (params: IcdPathTableParams): MmsMapping.IcdPathListRequ
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getTableList = async (params: IcdPathTableParams = {}) => {
|
const getTableList = async (params: IcdPathTableParams = {}) => {
|
||||||
|
await loadReferenceIcdNameMap()
|
||||||
|
|
||||||
const response = await listIcdPathsApi(buildListParams(params))
|
const response = await listIcdPathsApi(buildListParams(params))
|
||||||
const records = unwrapApiPayload<MmsMapping.IcdPathRecord[]>(response) || []
|
const records = unwrapApiPayload<MmsMapping.IcdPathRecord[]>(response) || []
|
||||||
const pageNum = params.pageNum || 1
|
const pageNum = params.pageNum || 1
|
||||||
const pageSize = params.pageSize || 10
|
const pageSize = params.pageSize || 10
|
||||||
const startIndex = (pageNum - 1) * pageSize
|
const startIndex = (pageNum - 1) * pageSize
|
||||||
|
|
||||||
activeIcdPathRecord.value = records.find(record => record.type === 1 && record.state !== 0) || activeIcdPathRecord.value
|
activeIcdPathRecord.value = records.find(record => isStandardIcdType(record.type) && record.state !== 0) || activeIcdPathRecord.value
|
||||||
|
|
||||||
// ICD 路径接口当前返回列表数据;这里统一包装为 ProTable 分页结构,保持和设备类型管理页一致。
|
// ICD 路径接口当前返回列表数据;这里统一包装为 ProTable 分页结构,保持和设备类型管理页一致。
|
||||||
return {
|
return {
|
||||||
@@ -281,11 +561,30 @@ const getTableList = async (params: IcdPathTableParams = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const refreshActiveIcdPathRecord = async () => {
|
const refreshActiveIcdPathRecord = async () => {
|
||||||
// 关键业务节点:标准 ICD 不能依赖当前表格筛选结果,否则校验弹窗可能拿不到已激活记录的 JSON。
|
// 关键业务节点:标准 ICD 不能依赖当前表格筛选结果,否则校验弹窗可能拿不到已激活记录。
|
||||||
const response = await listIcdPathsApi({ type: 1 })
|
const [manualResponse, upstreamResponse] = await Promise.all([listIcdPathsApi({ type: 1 }), listIcdPathsApi({ type: 3 })])
|
||||||
const records = unwrapApiPayload<MmsMapping.IcdPathRecord[]>(response) || []
|
const records = [
|
||||||
|
...(unwrapApiPayload<MmsMapping.IcdPathRecord[]>(manualResponse) || []),
|
||||||
|
...(unwrapApiPayload<MmsMapping.IcdPathRecord[]>(upstreamResponse) || [])
|
||||||
|
]
|
||||||
|
|
||||||
activeIcdPathRecord.value = records.find(record => record.type === 1 && record.state !== 0) || null
|
activeIcdPathRecord.value = records.find(record => isStandardIcdType(record.type) && record.state !== 0) || null
|
||||||
|
|
||||||
|
if (!activeIcdPathRecord.value?.id) {
|
||||||
|
activeIcdPathMappingJson.value = ''
|
||||||
|
activeIcdPathMappingName.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const detail = await getIcdPathMappingDetail(activeIcdPathRecord.value)
|
||||||
|
|
||||||
|
activeIcdPathMappingJson.value = detail.jsonStr?.trim() || ''
|
||||||
|
activeIcdPathMappingName.value = detail.name?.trim() || activeIcdPathRecord.value.name?.trim() || ''
|
||||||
|
} catch {
|
||||||
|
activeIcdPathMappingJson.value = ''
|
||||||
|
activeIcdPathMappingName.value = activeIcdPathRecord.value.name?.trim() || ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshIcdPaths = () => {
|
const refreshIcdPaths = () => {
|
||||||
@@ -294,6 +593,24 @@ const refreshIcdPaths = () => {
|
|||||||
refreshActiveIcdPathRecord()
|
refreshActiveIcdPathRecord()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadReferenceIcdNameMap = async () => {
|
||||||
|
try {
|
||||||
|
const response = await listIcdPathReferencesApi()
|
||||||
|
const records = unwrapApiPayload<MmsMapping.IcdPathReferenceOption[]>(response) || []
|
||||||
|
|
||||||
|
referenceIcdNameMap.value = records.reduce<Record<string, string>>((result, record) => {
|
||||||
|
if (record.id && record.name) {
|
||||||
|
result[record.id] = record.name
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}, {})
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getErrorMessage(error))
|
||||||
|
referenceIcdNameMap.value = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleTableRequestError = (error: unknown) => {
|
const handleTableRequestError = (error: unknown) => {
|
||||||
ElMessage.error(getErrorMessage(error))
|
ElMessage.error(getErrorMessage(error))
|
||||||
}
|
}
|
||||||
@@ -310,6 +627,50 @@ const openEditDialog = (row: MmsMapping.IcdPathRecord) => {
|
|||||||
formDialogVisible.value = true
|
formDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getIcdPathMappingDetail = async (row: MmsMapping.IcdPathRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
throw new Error('当前 ICD 记录缺少 ID,不能获取映射文件详情')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getIcdPathMappingDetailApi(row.id)
|
||||||
|
|
||||||
|
return unwrapApiPayload<MmsMapping.IcdPathMappingDetailResponse>(response) || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openMappingDetail = async (row: MmsMapping.IcdPathRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前 ICD 记录缺少 ID,不能查看映射文件详情')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentMappingDetailRecordName.value = row.name || ''
|
||||||
|
currentMappingDetail.value = null
|
||||||
|
mappingDetailActiveTab.value = 'json'
|
||||||
|
mappingDetailDialogVisible.value = true
|
||||||
|
mappingDetailLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 关键业务节点:列表接口不再返回映射大字段,详情弹窗必须按记录 ID 单独获取。
|
||||||
|
currentMappingDetail.value = await getIcdPathMappingDetail(row)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
mappingDetailLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openReferenceIcdDetail = async (row: MmsMapping.IcdPathRecord) => {
|
||||||
|
if (!row.referenceIcdId) {
|
||||||
|
ElMessage.warning('当前 ICD 记录缺少参照标准 ICD,不能查看详情')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await openMappingDetail({
|
||||||
|
id: row.referenceIcdId,
|
||||||
|
name: getReferenceIcdName(row.referenceIcdId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const deleteIcdPaths = async (ids: string[], successMessage: string) => {
|
const deleteIcdPaths = async (ids: string[], successMessage: string) => {
|
||||||
if (!ids.length) {
|
if (!ids.length) {
|
||||||
ElMessage.warning('请选择要删除的 ICD 记录')
|
ElMessage.warning('请选择要删除的 ICD 记录')
|
||||||
@@ -348,56 +709,57 @@ const handleBatchDelete = async (ids: string[]) => {
|
|||||||
await deleteIcdPaths(ids, 'ICD 记录批量删除成功')
|
await deleteIcdPaths(ids, 'ICD 记录批量删除成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleActivateIcdPath = async (row: MmsMapping.IcdPathRecord) => {
|
const handleExportSelectedSql = async (records: MmsMapping.IcdPathRecord[]) => {
|
||||||
if (!row.id) {
|
if (!records.length) {
|
||||||
ElMessage.warning('当前 ICD 记录缺少 ID,不能激活')
|
ElMessage.warning('请选择要导出的 ICD 记录')
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!row.jsonStr?.trim()) {
|
|
||||||
ElMessage.warning('当前 ICD 记录缺少 JSON 映射结果,不能激活,请先完成 ICD 校验并保存')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('激活后该 ICD 将作为标准 ICD 参与一致性校验,是否继续?', '激活ICD记录', {
|
const exportRecords = await Promise.all(
|
||||||
confirmButtonText: '确定',
|
records.map(async row => ({
|
||||||
cancelButtonText: '取消',
|
row,
|
||||||
type: 'warning'
|
detail: row.id ? await getIcdPathMappingDetail(row) : null
|
||||||
})
|
}))
|
||||||
} catch {
|
)
|
||||||
return
|
const sql = buildIcdPathInsertSql(exportRecords)
|
||||||
}
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||||||
|
|
||||||
activatingIcdPathId.value = row.id
|
downloadTextFile(sql, `icd-path-${timestamp}.sql`, 'application/sql;charset=utf-8')
|
||||||
|
ElMessage.success('SQL 已导出')
|
||||||
try {
|
|
||||||
// 关键业务节点:当前后端文档未提供独立激活接口,前端通过更新 ICD 类型为标准记录完成激活。
|
|
||||||
const response = await updateIcdPathApi({
|
|
||||||
id: row.id,
|
|
||||||
name: row.name || '',
|
|
||||||
path: row.path || '',
|
|
||||||
angle: row.angle,
|
|
||||||
usePhaseIndex: row.usePhaseIndex,
|
|
||||||
type: 1
|
|
||||||
})
|
|
||||||
const activated = unwrapApiPayload<boolean>(response)
|
|
||||||
|
|
||||||
if (activated === false) {
|
|
||||||
ElMessage.warning('ICD 记录激活未返回成功')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessage.success('ICD 记录激活成功')
|
|
||||||
refreshIcdPaths()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(getErrorMessage(error))
|
ElMessage.error(getErrorMessage(error))
|
||||||
} finally {
|
|
||||||
activatingIcdPathId.value = ''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleIcdCheck = (row: MmsMapping.IcdPathRecord) => {
|
const handleExportSelectedJson = async (records: MmsMapping.IcdPathRecord[]) => {
|
||||||
|
if (!records.length) {
|
||||||
|
ElMessage.warning('请选择要导出的 ICD 记录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exportRecords = await Promise.all(
|
||||||
|
records.map(async row => ({
|
||||||
|
row,
|
||||||
|
detail: row.id ? await getIcdPathMappingDetail(row) : null
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const json = JSON.stringify(
|
||||||
|
exportRecords.map(({ row, detail }) => buildIcdPathJsonRecord(row, detail)),
|
||||||
|
null,
|
||||||
|
4
|
||||||
|
)
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||||||
|
|
||||||
|
downloadTextFile(json, `icd-path-${timestamp}.json`, 'application/json;charset=utf-8')
|
||||||
|
ElMessage.success('JSON 已导出')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getErrorMessage(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleIcdCheck = async (row: MmsMapping.IcdPathRecord) => {
|
||||||
if (!row.id) {
|
if (!row.id) {
|
||||||
ElMessage.warning('当前 ICD 记录缺少 ID,不能执行 ICD 校验')
|
ElMessage.warning('当前 ICD 记录缺少 ID,不能执行 ICD 校验')
|
||||||
return
|
return
|
||||||
@@ -406,11 +768,38 @@ const handleIcdCheck = (row: MmsMapping.IcdPathRecord) => {
|
|||||||
// 关键业务节点:MMS 映射页承载 ICD 存储记录校验,生成的 JSON/XML 回写当前 ICD 记录。
|
// 关键业务节点:MMS 映射页承载 ICD 存储记录校验,生成的 JSON/XML 回写当前 ICD 记录。
|
||||||
currentIcdCheckRecord.id = row.id
|
currentIcdCheckRecord.id = row.id
|
||||||
currentIcdCheckRecord.name = row.name || ''
|
currentIcdCheckRecord.name = row.name || ''
|
||||||
currentIcdCheckRecord.type = row.type ?? 3
|
currentIcdCheckRecord.type = row.type ?? 4
|
||||||
refreshActiveIcdPathRecord()
|
|
||||||
|
if (isNonStandardIcdType(currentIcdCheckRecord.type)) {
|
||||||
|
await refreshActiveIcdPathRecord()
|
||||||
|
}
|
||||||
|
|
||||||
icdCheckDialogVisible.value = true
|
icdCheckDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOpenIcdCheckMsg = async (row: MmsMapping.IcdPathRecord) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前 ICD 记录缺少 ID,不能查看校验结论详情')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentCheckMsgRecordName.value = row.name || ''
|
||||||
|
currentCheckMsgDetail.value = null
|
||||||
|
icdCheckMsgDialogVisible.value = true
|
||||||
|
icdCheckMsgLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 关键业务节点:不一致详情以服务端保存的 Msg 为准,避免列表摘要和详情内容不同步。
|
||||||
|
const response = await getIcdPathCheckMsgApi(row.id)
|
||||||
|
|
||||||
|
currentCheckMsgDetail.value = unwrapApiPayload<MmsMapping.IcdPathCheckMsgResponse>(response)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
icdCheckMsgLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nextTick(refreshActiveIcdPathRecord)
|
nextTick(refreshActiveIcdPathRecord)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -420,4 +809,71 @@ nextTick(refreshActiveIcdPathRecord)
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icd-result-tag--clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icd-check-msg-detail {
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icd-check-msg-detail__section {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icd-check-msg-detail__title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icd-check-msg-detail__json {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-detail-dialog {
|
||||||
|
min-height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-detail-dialog__tabs {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-detail-dialog__json {
|
||||||
|
display: flex;
|
||||||
|
max-height: 420px;
|
||||||
|
min-height: 260px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-detail-dialog__json :deep(.json-tree-body),
|
||||||
|
.mapping-detail-dialog__json :deep(.mapping-json-text) {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-detail-dialog__content {
|
||||||
|
max-height: 420px;
|
||||||
|
min-height: 260px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
|||||||
const isGeneratingXml = ref(false)
|
const isGeneratingXml = ref(false)
|
||||||
const isSavingIcdCheckResult = ref(false)
|
const isSavingIcdCheckResult = ref(false)
|
||||||
const lastIcdConsistencyCheckResult = ref<MmsMapping.IcdJsonConsistencyCheckResponse | null>(null)
|
const lastIcdConsistencyCheckResult = ref<MmsMapping.IcdJsonConsistencyCheckResponse | null>(null)
|
||||||
|
const reviewedIcdConsistencyProblemList = ref<string[]>([])
|
||||||
|
const hasConfirmedIcdConsistencyProblems = ref(false)
|
||||||
|
const shouldOpenIcdConsistencyProblems = ref(0)
|
||||||
const parsedIcdDocument = ref<MmsMapping.IcdDocument | null>(null)
|
const parsedIcdDocument = ref<MmsMapping.IcdDocument | null>(null)
|
||||||
const hasSequenceConfigured = ref(false)
|
const hasSequenceConfigured = ref(false)
|
||||||
const sequenceDialogVisible = ref(false)
|
const sequenceDialogVisible = ref(false)
|
||||||
@@ -271,13 +274,20 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
|||||||
const canSaveIcdCheckResult = computed(() => {
|
const canSaveIcdCheckResult = computed(() => {
|
||||||
if (!deviceTypeCheckId.value || !mappingJsonPreview.value || !xmlContentForExport.value || isSubmitting.value) return false
|
if (!deviceTypeCheckId.value || !mappingJsonPreview.value || !xmlContentForExport.value || isSubmitting.value) return false
|
||||||
if (standardMappingJson.value && !lastIcdConsistencyCheckResult.value) return false
|
if (standardMappingJson.value && !lastIcdConsistencyCheckResult.value) return false
|
||||||
|
if (icdConsistencyStatus.value === 'failed' && !hasConfirmedIcdConsistencyProblems.value) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
const hasIcdConsistencyCheckResult = computed(() => Boolean(lastIcdConsistencyCheckResult.value))
|
const hasIcdConsistencyCheckResult = computed(() => Boolean(lastIcdConsistencyCheckResult.value))
|
||||||
|
const reviewedIcdConsistencyResult = computed(() => {
|
||||||
|
if (!lastIcdConsistencyCheckResult.value) return 1
|
||||||
|
if (lastIcdConsistencyCheckResult.value.result === 1) return 1
|
||||||
|
|
||||||
|
return reviewedIcdConsistencyProblemList.value.length ? 0 : 1
|
||||||
|
})
|
||||||
const icdConsistencyStatus = computed<IcdConsistencyStatus>(() => {
|
const icdConsistencyStatus = computed<IcdConsistencyStatus>(() => {
|
||||||
if (!lastIcdConsistencyCheckResult.value) return ''
|
if (!lastIcdConsistencyCheckResult.value) return ''
|
||||||
return lastIcdConsistencyCheckResult.value.result === 1 ? 'success' : 'failed'
|
return reviewedIcdConsistencyResult.value === 1 ? 'success' : 'failed'
|
||||||
})
|
})
|
||||||
const saveIcdCheckResultText = computed(() => '保存')
|
const saveIcdCheckResultText = computed(() => '保存')
|
||||||
|
|
||||||
@@ -324,7 +334,11 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
|||||||
...(xmlResponsePayload.value?.problems?.filter(Boolean) || [])
|
...(xmlResponsePayload.value?.problems?.filter(Boolean) || [])
|
||||||
])
|
])
|
||||||
const problemList = jsonMappingProblemList
|
const problemList = jsonMappingProblemList
|
||||||
const icdConsistencyProblemList = computed(() => lastIcdConsistencyCheckResult.value?.issues?.filter(Boolean) || [])
|
const icdConsistencyProblemList = computed(() => {
|
||||||
|
if (!lastIcdConsistencyCheckResult.value) return []
|
||||||
|
if (lastIcdConsistencyCheckResult.value.result === 1) return []
|
||||||
|
return reviewedIcdConsistencyProblemList.value
|
||||||
|
})
|
||||||
const icdConsistencyProblemEmptyText = '当前 ICD 校验未返回 issues'
|
const icdConsistencyProblemEmptyText = '当前 ICD 校验未返回 issues'
|
||||||
|
|
||||||
const methodDescribe = computed(() => xmlResponsePayload.value?.methodDescribe?.trim() || '')
|
const methodDescribe = computed(() => xmlResponsePayload.value?.methodDescribe?.trim() || '')
|
||||||
@@ -397,6 +411,9 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
|||||||
responsePayload.value = null
|
responsePayload.value = null
|
||||||
xmlResponsePayload.value = null
|
xmlResponsePayload.value = null
|
||||||
lastIcdConsistencyCheckResult.value = null
|
lastIcdConsistencyCheckResult.value = null
|
||||||
|
reviewedIcdConsistencyProblemList.value = []
|
||||||
|
hasConfirmedIcdConsistencyProblems.value = false
|
||||||
|
shouldOpenIcdConsistencyProblems.value = 0
|
||||||
parsedIcdDocument.value = null
|
parsedIcdDocument.value = null
|
||||||
hasSequenceConfigured.value = false
|
hasSequenceConfigured.value = false
|
||||||
sequenceDialogVisible.value = false
|
sequenceDialogVisible.value = false
|
||||||
@@ -646,6 +663,8 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
|||||||
const payload = unwrapApiPayload<MmsMapping.IcdJsonConsistencyCheckResponse>(response)
|
const payload = unwrapApiPayload<MmsMapping.IcdJsonConsistencyCheckResponse>(response)
|
||||||
|
|
||||||
lastIcdConsistencyCheckResult.value = payload
|
lastIcdConsistencyCheckResult.value = payload
|
||||||
|
reviewedIcdConsistencyProblemList.value = payload.issues?.filter(Boolean) || []
|
||||||
|
hasConfirmedIcdConsistencyProblems.value = payload.result === 1
|
||||||
|
|
||||||
if (payload.result === 1) {
|
if (payload.result === 1) {
|
||||||
ElMessage.success(payload.message || `与${standardMappingName.value}一致`)
|
ElMessage.success(payload.message || `与${standardMappingName.value}一致`)
|
||||||
@@ -653,6 +672,7 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
activeResultTab.value = 'problem'
|
activeResultTab.value = 'problem'
|
||||||
|
shouldOpenIcdConsistencyProblems.value += 1
|
||||||
ElMessage.warning(payload.message || `与${standardMappingName.value}不一致`)
|
ElMessage.warning(payload.message || `与${standardMappingName.value}不一致`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastIcdConsistencyCheckResult.value = {
|
lastIcdConsistencyCheckResult.value = {
|
||||||
@@ -660,6 +680,9 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
|||||||
message: getErrorMessage(error),
|
message: getErrorMessage(error),
|
||||||
issues: [getErrorMessage(error)]
|
issues: [getErrorMessage(error)]
|
||||||
}
|
}
|
||||||
|
reviewedIcdConsistencyProblemList.value = [getErrorMessage(error)]
|
||||||
|
hasConfirmedIcdConsistencyProblems.value = false
|
||||||
|
shouldOpenIcdConsistencyProblems.value += 1
|
||||||
activeResultTab.value = 'problem'
|
activeResultTab.value = 'problem'
|
||||||
ElMessage.error(getErrorMessage(error))
|
ElMessage.error(getErrorMessage(error))
|
||||||
} finally {
|
} finally {
|
||||||
@@ -677,6 +700,43 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
|||||||
correctedJson: checkResult?.correctedJson || undefined
|
correctedJson: checkResult?.correctedJson || undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleConfirmIcdConsistencyProblems = () => {
|
||||||
|
// 关键业务节点:确认只表示用户已审核当前问题列表,剩余问题需要继续带入保存结果,不能把确认动作当成清空问题。
|
||||||
|
hasConfirmedIcdConsistencyProblems.value = true
|
||||||
|
|
||||||
|
if (!reviewedIcdConsistencyProblemList.value.length) {
|
||||||
|
ElMessage.success('ICD校验问题已确认,当前没有剩余问题')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.warning(`ICD校验问题已确认,问题列表仍保留 ${reviewedIcdConsistencyProblemList.value.length} 条问题`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveIcdConsistencyProblem = (index: number) => {
|
||||||
|
if (index < 0 || index >= reviewedIcdConsistencyProblemList.value.length) return
|
||||||
|
|
||||||
|
reviewedIcdConsistencyProblemList.value = reviewedIcdConsistencyProblemList.value.filter(
|
||||||
|
(_, itemIndex) => itemIndex !== index
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!reviewedIcdConsistencyProblemList.value.length) {
|
||||||
|
hasConfirmedIcdConsistencyProblems.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildReviewedIcdCheckMsg = (
|
||||||
|
checkResult: MmsMapping.IcdJsonConsistencyCheckResponse | null,
|
||||||
|
summaryMessage: string
|
||||||
|
): MmsMapping.IcdCheckMsg => {
|
||||||
|
const reviewedIssues = reviewedIcdConsistencyProblemList.value
|
||||||
|
|
||||||
|
return {
|
||||||
|
...buildIcdCheckMsg(checkResult, summaryMessage),
|
||||||
|
details: reviewedIssues,
|
||||||
|
issuesJson: reviewedIssues.length ? checkResult?.issuesJson || undefined : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSaveIcdCheckResult = async () => {
|
const handleSaveIcdCheckResult = async () => {
|
||||||
if (!deviceTypeCheckId.value) {
|
if (!deviceTypeCheckId.value) {
|
||||||
ElMessage.warning('当前缺少设备类型 ID,不能入库校验结果')
|
ElMessage.warning('当前缺少设备类型 ID,不能入库校验结果')
|
||||||
@@ -700,20 +760,28 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (icdConsistencyStatus.value === 'failed' && !hasConfirmedIcdConsistencyProblems.value) {
|
||||||
|
ElMessage.warning('请先确认 ICD 校验问题列表')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
isSavingIcdCheckResult.value = true
|
isSavingIcdCheckResult.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const checkResult = lastIcdConsistencyCheckResult.value
|
const checkResult = lastIcdConsistencyCheckResult.value
|
||||||
const result = checkResult?.result ?? 1
|
const result = reviewedIcdConsistencyResult.value
|
||||||
const summaryMessage =
|
const summaryMessage =
|
||||||
checkResult?.message ||
|
result === 1
|
||||||
(xmlContentForExport.value ? 'ICD一致性校验通过,JSON/XML已生成' : 'ICD一致性校验通过,JSON已生成')
|
? xmlContentForExport.value
|
||||||
|
? 'ICD一致性校验通过,JSON/XML已生成'
|
||||||
|
: 'ICD一致性校验通过,JSON已生成'
|
||||||
|
: checkResult?.message || 'ICD一致性校验不通过'
|
||||||
const savePayload = {
|
const savePayload = {
|
||||||
icdDocument: parsedIcdDocument.value || responsePayload.value?.icdDocument,
|
icdDocument: parsedIcdDocument.value || responsePayload.value?.icdDocument,
|
||||||
mappingJson,
|
mappingJson,
|
||||||
xml: xmlContentForExport.value,
|
xml: xmlContentForExport.value,
|
||||||
result,
|
result,
|
||||||
msg: buildIcdCheckMsg(checkResult, summaryMessage)
|
msg: buildReviewedIcdCheckMsg(checkResult, summaryMessage)
|
||||||
}
|
}
|
||||||
// 关键业务节点:ICD 校验流程可由设备类型或 ICD 存储记录承载,保存接口由宿主页注入以避免串写业务表。
|
// 关键业务节点:ICD 校验流程可由设备类型或 ICD 存储记录承载,保存接口由宿主页注入以避免串写业务表。
|
||||||
const response = options.saveIcdCheckResult
|
const response = options.saveIcdCheckResult
|
||||||
@@ -745,6 +813,9 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
|||||||
}
|
}
|
||||||
xmlResponsePayload.value = null
|
xmlResponsePayload.value = null
|
||||||
lastIcdConsistencyCheckResult.value = null
|
lastIcdConsistencyCheckResult.value = null
|
||||||
|
reviewedIcdConsistencyProblemList.value = []
|
||||||
|
hasConfirmedIcdConsistencyProblems.value = false
|
||||||
|
shouldOpenIcdConsistencyProblems.value = 0
|
||||||
hasSequenceConfigured.value = false
|
hasSequenceConfigured.value = false
|
||||||
activeResultTab.value = 'json'
|
activeResultTab.value = 'json'
|
||||||
}
|
}
|
||||||
@@ -809,6 +880,7 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
|||||||
saveIcdCheckResultText,
|
saveIcdCheckResultText,
|
||||||
hasIcdConsistencyCheckResult,
|
hasIcdConsistencyCheckResult,
|
||||||
icdConsistencyStatus,
|
icdConsistencyStatus,
|
||||||
|
shouldOpenIcdConsistencyProblems,
|
||||||
confirmDialogVisible,
|
confirmDialogVisible,
|
||||||
isConfirmingSelection,
|
isConfirmingSelection,
|
||||||
handleIcdFileChange,
|
handleIcdFileChange,
|
||||||
@@ -818,6 +890,8 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
|||||||
handleExportMapping,
|
handleExportMapping,
|
||||||
handleGenerateXmlMapping,
|
handleGenerateXmlMapping,
|
||||||
handleIcdConsistencyCheck,
|
handleIcdConsistencyCheck,
|
||||||
|
handleConfirmIcdConsistencyProblems,
|
||||||
|
handleRemoveIcdConsistencyProblem,
|
||||||
handleUpdateMappingJson,
|
handleUpdateMappingJson,
|
||||||
handleSequenceConfigComplete,
|
handleSequenceConfigComplete,
|
||||||
handleSaveIcdCheckResult,
|
handleSaveIcdCheckResult,
|
||||||
|
|||||||
@@ -54,7 +54,11 @@
|
|||||||
<div class="panel-section result-card grow-card preview-tab-section">
|
<div class="panel-section result-card grow-card preview-tab-section">
|
||||||
<div v-if="icdConsistencyStatus" class="icd-consistency-status">
|
<div v-if="icdConsistencyStatus" class="icd-consistency-status">
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
:content="icdConsistencyStatus === 'success' ? 'ICD校验通过' : 'ICD校验不通过,点击查看JSON映射问题列表'"
|
:content="
|
||||||
|
icdConsistencyStatus === 'success'
|
||||||
|
? 'ICD校验通过'
|
||||||
|
: `ICD校验不通过,点击查看JSON映射问题列表(${icdConsistencyProblemCount}条)`
|
||||||
|
"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -62,8 +66,14 @@
|
|||||||
:class="['icd-consistency-indicator', `is-${icdConsistencyStatus}`]"
|
:class="['icd-consistency-indicator', `is-${icdConsistencyStatus}`]"
|
||||||
:disabled="icdConsistencyStatus !== 'failed'"
|
:disabled="icdConsistencyStatus !== 'failed'"
|
||||||
@click="openIcdConsistencyProblems"
|
@click="openIcdConsistencyProblems"
|
||||||
|
>
|
||||||
|
<el-badge
|
||||||
|
:value="icdConsistencyProblemCount"
|
||||||
|
:hidden="icdConsistencyProblemCount === 0"
|
||||||
|
class="icd-consistency-indicator__badge"
|
||||||
>
|
>
|
||||||
<el-icon><WarningFilled /></el-icon>
|
<el-icon><WarningFilled /></el-icon>
|
||||||
|
</el-badge>
|
||||||
</button>
|
</button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,7 +118,7 @@
|
|||||||
plain
|
plain
|
||||||
size="small"
|
size="small"
|
||||||
:icon="WarningFilled"
|
:icon="WarningFilled"
|
||||||
@click="icdConsistencyProblemDialogVisible = true"
|
@click="openIcdConsistencyProblems"
|
||||||
>
|
>
|
||||||
{{ icdConsistencyProblemButtonText }}
|
{{ icdConsistencyProblemButtonText }}
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -199,9 +209,22 @@
|
|||||||
>
|
>
|
||||||
<span class="problem-index">{{ index + 1 }}</span>
|
<span class="problem-index">{{ index + 1 }}</span>
|
||||||
<span class="problem-text">{{ problem }}</span>
|
<span class="problem-text">{{ problem }}</span>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
:icon="Delete"
|
||||||
|
class="problem-remove"
|
||||||
|
@click="emit('remove-icd-consistency-problem', index)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-else :description="icdConsistencyProblemEmptyText" />
|
<el-empty v-else :description="icdConsistencyProblemEmptyText" />
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="icdConsistencyProblemDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmIcdConsistencyProblems">确认</el-button>
|
||||||
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@@ -301,6 +324,7 @@
|
|||||||
import {
|
import {
|
||||||
CircleCheck,
|
CircleCheck,
|
||||||
Connection,
|
Connection,
|
||||||
|
Delete,
|
||||||
Document,
|
Document,
|
||||||
Download,
|
Download,
|
||||||
Finished,
|
Finished,
|
||||||
@@ -382,10 +406,12 @@ const props = withDefaults(defineProps<{
|
|||||||
isSavingIcdCheckResult: boolean
|
isSavingIcdCheckResult: boolean
|
||||||
saveIcdCheckResultText: string
|
saveIcdCheckResultText: string
|
||||||
icdConsistencyStatus?: IcdConsistencyStatus
|
icdConsistencyStatus?: IcdConsistencyStatus
|
||||||
|
shouldOpenIcdConsistencyProblems?: number
|
||||||
}>(), {
|
}>(), {
|
||||||
showDescription: true,
|
showDescription: true,
|
||||||
showIcdCheckAction: true,
|
showIcdCheckAction: true,
|
||||||
icdConsistencyStatus: ''
|
icdConsistencyStatus: '',
|
||||||
|
shouldOpenIcdConsistencyProblems: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -397,6 +423,8 @@ const emit = defineEmits<{
|
|||||||
(event: 'sequence-config-complete'): void
|
(event: 'sequence-config-complete'): void
|
||||||
(event: 'icd-check'): void
|
(event: 'icd-check'): void
|
||||||
(event: 'save-icd-check-result'): void
|
(event: 'save-icd-check-result'): void
|
||||||
|
(event: 'confirm-icd-consistency-problems'): void
|
||||||
|
(event: 'remove-icd-consistency-problem', index: number): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const activeTabProxy = computed({
|
const activeTabProxy = computed({
|
||||||
@@ -414,6 +442,7 @@ const icdConsistencyProblemButtonText = computed(() =>
|
|||||||
? `${ICD_CONSISTENCY_PROBLEM_LABEL} (${props.icdConsistencyProblemList.length})`
|
? `${ICD_CONSISTENCY_PROBLEM_LABEL} (${props.icdConsistencyProblemList.length})`
|
||||||
: ICD_CONSISTENCY_PROBLEM_LABEL
|
: ICD_CONSISTENCY_PROBLEM_LABEL
|
||||||
)
|
)
|
||||||
|
const icdConsistencyProblemCount = computed(() => props.icdConsistencyProblemList.length)
|
||||||
const problemDialogVisible = ref(false)
|
const problemDialogVisible = ref(false)
|
||||||
const icdConsistencyProblemDialogVisible = ref(false)
|
const icdConsistencyProblemDialogVisible = ref(false)
|
||||||
const matchResultDialogVisible = ref(false)
|
const matchResultDialogVisible = ref(false)
|
||||||
@@ -480,6 +509,19 @@ const openIcdConsistencyProblems = () => {
|
|||||||
icdConsistencyProblemDialogVisible.value = true
|
icdConsistencyProblemDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const confirmIcdConsistencyProblems = () => {
|
||||||
|
emit('confirm-icd-consistency-problems')
|
||||||
|
icdConsistencyProblemDialogVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.shouldOpenIcdConsistencyProblems,
|
||||||
|
value => {
|
||||||
|
if (!value) return
|
||||||
|
openIcdConsistencyProblems()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is JsonObject =>
|
const isRecord = (value: unknown): value is JsonObject =>
|
||||||
Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
||||||
|
|
||||||
@@ -940,6 +982,11 @@ const confirmSequenceConfig = () => {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.problem-remove {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.match-result-detail {
|
.match-result-detail {
|
||||||
max-height: 58vh;
|
max-height: 58vh;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
<template>
|
||||||
|
<section class="mapping-panel request-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div>
|
||||||
|
<div class="panel-title-tabs">
|
||||||
|
<span class="panel-title-tab">PQDIF解析</span>
|
||||||
|
</div>
|
||||||
|
<p class="panel-description">选择 .pqd 或 .pqdif 文件后,调用后端解析接口读取结构和采样摘要。</p>
|
||||||
|
</div>
|
||||||
|
<el-tag :type="requestStatusTagType" effect="light">{{ requestStatusText }}</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-content">
|
||||||
|
<div class="panel-section file-action-row">
|
||||||
|
<div class="file-select-row">
|
||||||
|
<el-input
|
||||||
|
:model-value="selectedFileName"
|
||||||
|
readonly
|
||||||
|
placeholder="请选择 .pqd 或 .pqdif 文件"
|
||||||
|
class="file-input"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" :icon="FolderOpened" :disabled="isParsing" @click="openFilePicker">
|
||||||
|
选择文件
|
||||||
|
</el-button>
|
||||||
|
<input
|
||||||
|
ref="fileInputRef"
|
||||||
|
class="hidden-file-input"
|
||||||
|
type="file"
|
||||||
|
accept=".pqd,.pqdif"
|
||||||
|
@change="event => emit('file-change', event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" :icon="Search" :loading="isParsing" :disabled="!canParse" @click="emit('parse')">
|
||||||
|
开始解析
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" plain :icon="Delete" :disabled="!canReset" @click="emit('reset')">清空</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedFileName" class="file-meta">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span class="file-meta__name">{{ selectedFileName }}</span>
|
||||||
|
<span v-if="selectedFileSizeText" class="file-meta__size">{{ selectedFileSizeText }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Delete, Document, FolderOpened, Search } from '@element-plus/icons-vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'PqdifRequestPanel'
|
||||||
|
})
|
||||||
|
|
||||||
|
type TagType = 'success' | 'warning' | 'info' | 'primary' | 'danger'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
selectedFileName: string
|
||||||
|
selectedFileSizeText: string
|
||||||
|
isParsing: boolean
|
||||||
|
canParse: boolean
|
||||||
|
canReset: boolean
|
||||||
|
requestStatusText: string
|
||||||
|
requestStatusTagType: TagType
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'file-change', value: Event): void
|
||||||
|
(event: 'parse'): void
|
||||||
|
(event: 'reset'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
|
const openFilePicker = () => {
|
||||||
|
fileInputRef.value?.click()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.mapping-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-section {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 36px;
|
||||||
|
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 {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #4b5563;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-action-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-select-row {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
width: 420px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-meta__name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-meta__size {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mapping-panel {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-select-row {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-action-row {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
<template>
|
||||||
|
<section class="mapping-panel result-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="result-view-tabs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="['panel-title-tab', { 'is-active': activeTabProxy === 'records' }]"
|
||||||
|
@click="activeTabProxy = 'records'"
|
||||||
|
>
|
||||||
|
Records
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="['panel-title-tab', { 'is-active': activeTabProxy === 'observations' }]"
|
||||||
|
@click="activeTabProxy = 'observations'"
|
||||||
|
>
|
||||||
|
Observations
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-actions">
|
||||||
|
<el-tag type="info" effect="light">Record {{ records.length }}</el-tag>
|
||||||
|
<el-tag type="info" effect="light">Observation {{ observations.length }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-content panel-content--fixed">
|
||||||
|
<div class="panel-section result-card">
|
||||||
|
<el-tabs v-model="activeTabProxy" class="result-tabs">
|
||||||
|
<el-tab-pane label="Records" name="records">
|
||||||
|
<el-table v-if="records.length" :data="records" border height="100%">
|
||||||
|
<el-table-column prop="recordIndex" label="下标" width="90" />
|
||||||
|
<el-table-column prop="typeName" label="类型名称" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="typeGuid" label="类型GUID" min-width="260" show-overflow-tooltip />
|
||||||
|
<el-table-column label="Observation" width="130">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.observation ? 'success' : 'info'" effect="light">
|
||||||
|
{{ row.observation ? '是' : '否' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-empty v-else description="暂无 Record 数据" />
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="Observations" name="observations">
|
||||||
|
<el-scrollbar v-if="observations.length" class="observation-scroll">
|
||||||
|
<el-collapse class="observation-collapse">
|
||||||
|
<el-collapse-item
|
||||||
|
v-for="(observation, observationIndex) in observations"
|
||||||
|
:key="`${observation.recordIndex}-${observationIndex}`"
|
||||||
|
:name="String(observationIndex)"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<span class="observation-title">
|
||||||
|
{{ observation.name || `Observation ${observationIndex + 1}` }}
|
||||||
|
</span>
|
||||||
|
<span class="observation-meta">
|
||||||
|
Record {{ observation.recordIndex ?? '-' }} / Channel {{ observation.channelCount ?? 0 }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-descriptions :column="2" border class="observation-desc">
|
||||||
|
<el-descriptions-item label="开始时间">
|
||||||
|
{{ observation.timeStartText || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Excel days">
|
||||||
|
{{ observation.timeStartExcelDays ?? '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="(channel, channelIndex) in observation.channels || []"
|
||||||
|
:key="`${channel.channelIndex}-${channelIndex}`"
|
||||||
|
class="channel-card"
|
||||||
|
>
|
||||||
|
<div class="channel-header">
|
||||||
|
<div class="channel-title">
|
||||||
|
{{ channel.name || `Channel ${channelIndex + 1}` }}
|
||||||
|
</div>
|
||||||
|
<div class="channel-meta">
|
||||||
|
Series {{ channel.seriesCount ?? 0 }} / Phase {{ channel.phaseId ?? '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="2" border class="channel-desc">
|
||||||
|
<el-descriptions-item label="量类型GUID">
|
||||||
|
{{ channel.quantityTypeGuid || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="测量量ID">
|
||||||
|
{{ channel.quantityMeasuredId ?? '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<el-table :data="channel.series || []" border row-key="seriesIndex">
|
||||||
|
<el-table-column prop="seriesIndex" label="Series" width="90" />
|
||||||
|
<el-table-column prop="dataStatus" label="数据状态" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag
|
||||||
|
:type="row.dataStatus === 'DATA_FAILED' ? 'danger' : 'success'"
|
||||||
|
effect="light"
|
||||||
|
>
|
||||||
|
{{ row.dataStatus || '-' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="valueCount" label="点数" width="100" />
|
||||||
|
<el-table-column prop="quantityUnitsId" label="单位ID" width="100" />
|
||||||
|
<el-table-column prop="seriesBaseType" label="基础类型" width="110" />
|
||||||
|
<el-table-column label="scale/offset" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.scale ?? '-' }} / {{ row.offset ?? '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="前5个采样值" min-width="220" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ formatFirstValues(row.firstValues) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="dataMessage" label="数据消息" min-width="180" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
|
</el-scrollbar>
|
||||||
|
<el-empty v-else description="暂无 Observation 数据" />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { ParsePqdif } from '@/api/tools/parsePqdif/interface'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'PqdifResultPanel'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
activeTab: 'records' | 'observations'
|
||||||
|
records: ParsePqdif.RecordItem[]
|
||||||
|
observations: ParsePqdif.ObservationItem[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:activeTab', value: 'records' | 'observations'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const activeTabProxy = computed({
|
||||||
|
get: () => props.activeTab,
|
||||||
|
set: value => emit('update:activeTab', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatFirstValues = (values?: number[]) => {
|
||||||
|
if (!values?.length) return '-'
|
||||||
|
return values.map(value => String(value)).join(', ')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.mapping-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-view-tabs {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title-tab {
|
||||||
|
position: relative;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 2px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 36px;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title-tab.is-active {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title-tab.is-active::after {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: -1px;
|
||||||
|
left: 0;
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--el-color-primary);
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-panel {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-content--fixed {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-section {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
:deep(.el-tabs__header) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__content) {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tab-pane) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.observation-collapse {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.observation-title {
|
||||||
|
margin-right: 12px;
|
||||||
|
color: #1f2937;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.observation-meta,
|
||||||
|
.channel-meta {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.observation-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.observation-desc,
|
||||||
|
.channel-desc {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-card {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-title {
|
||||||
|
color: #1f2937;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mapping-panel {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header,
|
||||||
|
.panel-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<template>
|
||||||
|
<section class="mapping-panel summary-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="panel-title-tabs">
|
||||||
|
<span class="panel-title-tab">解析摘要</span>
|
||||||
|
</div>
|
||||||
|
<el-tag v-if="result" :type="statusTagType" effect="light">{{ statusText }}</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="result">
|
||||||
|
<el-alert
|
||||||
|
v-if="result.message"
|
||||||
|
:title="result.message"
|
||||||
|
:type="isBusinessFailed ? 'error' : 'success'"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
/>
|
||||||
|
<div class="summary-metric-list">
|
||||||
|
<div v-for="metric in summaryMetrics" :key="metric.label" class="summary-metric-item">
|
||||||
|
<span class="summary-metric-item__label">{{ metric.label }}</span>
|
||||||
|
<span class="summary-metric-item__value">{{ metric.value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="文件名">{{ result.fileName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="native版本">{{ result.nativeVersion || '-' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
<el-empty v-else description="暂无解析摘要" />
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { ParsePqdif } from '@/api/tools/parsePqdif/interface'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'PqdifSummaryPanel'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
result: ParsePqdif.ParseResponse | null
|
||||||
|
isBusinessFailed: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
if (props.result?.status === 'SUCCESS') return '成功'
|
||||||
|
if (props.result?.status === 'FAILED') return '失败'
|
||||||
|
return props.result?.status || '-'
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusTagType = computed(() => {
|
||||||
|
if (props.result?.status === 'SUCCESS') return 'success'
|
||||||
|
if (props.result?.status === 'FAILED') return 'danger'
|
||||||
|
return 'info'
|
||||||
|
})
|
||||||
|
|
||||||
|
const summaryMetrics = computed(() => [
|
||||||
|
{ label: 'Record', value: props.result?.recordCount ?? 0 },
|
||||||
|
{ label: 'Observation', value: props.result?.observationCount ?? 0 },
|
||||||
|
{ label: '采样上限', value: props.result?.sampleValueCount ?? 0 },
|
||||||
|
{ label: '业务状态', value: props.result?.status || '-' }
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.mapping-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 36px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title-tab::after {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: -1px;
|
||||||
|
left: 0;
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--el-color-primary);
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-panel {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-metric-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-metric-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-metric-item__label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-metric-item__value {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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 = {
|
||||||
|
page: path.resolve(rootDir, 'views/tools/parsePqdif/index.vue'),
|
||||||
|
flow: path.resolve(rootDir, 'views/tools/parsePqdif/utils/useParsePqdifFlow.ts'),
|
||||||
|
requestPanel: path.resolve(rootDir, 'views/tools/parsePqdif/components/PqdifRequestPanel.vue'),
|
||||||
|
summaryPanel: path.resolve(rootDir, 'views/tools/parsePqdif/components/PqdifSummaryPanel.vue'),
|
||||||
|
resultPanel: path.resolve(rootDir, 'views/tools/parsePqdif/components/PqdifResultPanel.vue'),
|
||||||
|
api: path.resolve(rootDir, 'api/tools/parsePqdif/index.ts'),
|
||||||
|
apiTypes: path.resolve(rootDir, 'api/tools/parsePqdif/interface/index.ts')
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = file => fs.existsSync(file)
|
||||||
|
const read = file => (exists(file) ? fs.readFileSync(file, 'utf8') : '')
|
||||||
|
|
||||||
|
const pageSource = read(files.page)
|
||||||
|
const flowSource = read(files.flow)
|
||||||
|
const apiSource = read(files.api)
|
||||||
|
const apiTypesSource = read(files.apiTypes)
|
||||||
|
|
||||||
|
const checks = [
|
||||||
|
['parsePqdif API file exists', () => exists(files.api)],
|
||||||
|
['parsePqdif API type file exists', () => exists(files.apiTypes)],
|
||||||
|
['parsePqdif API uses documented endpoint', () => /\/api\/parse-pqdif\/parse/.test(apiSource)],
|
||||||
|
['parsePqdif API appends documented pqdifFile field', () => /append\('pqdifFile',\s*pqdifFile\)/.test(apiSource)],
|
||||||
|
['parsePqdif API sends multipart form data', () => /multipart\/form-data/.test(apiSource)],
|
||||||
|
['parsePqdif types include response status', () => /export\s+type\s+ParseStatus/.test(apiTypesSource)],
|
||||||
|
['parsePqdif types include observations', () => /interface\s+ObservationItem/.test(apiTypesSource)],
|
||||||
|
['parsePqdif flow exists', () => exists(files.flow)],
|
||||||
|
['parsePqdif flow imports API', () => /parsePqdifApi/.test(flowSource)],
|
||||||
|
['parsePqdif flow validates .pqd and .pqdif suffixes', () => /SUPPORTED_PQDIF_EXTENSIONS/.test(flowSource)],
|
||||||
|
['request panel exists', () => exists(files.requestPanel)],
|
||||||
|
['summary panel exists', () => exists(files.summaryPanel)],
|
||||||
|
['result panel exists', () => exists(files.resultPanel)],
|
||||||
|
['page uses mmsMapping-style parse layout', () => /parse-pqdif-grid/.test(pageSource)],
|
||||||
|
['request panel reuses mapping panel class', () => /class="mapping-panel/.test(read(files.requestPanel))],
|
||||||
|
['request panel keeps compact action section', () => /panel-section file-action-row/.test(read(files.requestPanel))],
|
||||||
|
['summary panel exposes metric cards', () => /summary-metric-list/.test(read(files.summaryPanel))],
|
||||||
|
['result panel exposes records and observations tabs', () => /result-view-tabs/.test(read(files.resultPanel))],
|
||||||
|
['result panel highlights failed series rows', () => /DATA_FAILED/.test(read(files.resultPanel))],
|
||||||
|
[
|
||||||
|
'page imports parsePqdif components',
|
||||||
|
() =>
|
||||||
|
/PqdifRequestPanel/.test(pageSource) &&
|
||||||
|
/PqdifSummaryPanel/.test(pageSource) &&
|
||||||
|
/PqdifResultPanel/.test(pageSource)
|
||||||
|
],
|
||||||
|
['page keeps route component name', () => /name:\s*'ParsePqdifView'/.test(pageSource)],
|
||||||
|
['page uses parsePqdif flow composable', () => /useParsePqdifFlow/.test(pageSource)]
|
||||||
|
]
|
||||||
|
|
||||||
|
const failures = checks.filter(([, check]) => !check()).map(([name]) => name)
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error('parsePqdif page contract failed:')
|
||||||
|
for (const failure of failures) {
|
||||||
|
console.error(`- ${failure}`)
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('parsePqdif page contract passed')
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/* 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/parsePqdif/index.vue'),
|
||||||
|
toolsHome: path.resolve(rootDir, 'views/tools/index.vue'),
|
||||||
|
staticRouter: path.resolve(rootDir, 'routers/modules/staticRouter.ts'),
|
||||||
|
dynamicRouter: path.resolve(rootDir, 'routers/modules/dynamicRouter.ts')
|
||||||
|
}
|
||||||
|
|
||||||
|
const read = file => (fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '')
|
||||||
|
const exists = file => fs.existsSync(file)
|
||||||
|
|
||||||
|
const checks = [
|
||||||
|
['parsePqdif page exists', () => exists(files.page)],
|
||||||
|
['parsePqdif page declares route component name', () => /name:\s*'ParsePqdifView'/.test(read(files.page))],
|
||||||
|
['static router registers /tools/parsePqdif', () => /path:\s*'\/tools\/parsePqdif'/.test(read(files.staticRouter))],
|
||||||
|
['static route name is toolParsePqdif', () => /name:\s*'toolParsePqdif'/.test(read(files.staticRouter))],
|
||||||
|
[
|
||||||
|
'static router imports parsePqdif page',
|
||||||
|
() => /@\/views\/tools\/parsePqdif\/index\.vue/.test(read(files.staticRouter))
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'dynamic router keeps parsePqdif static route from being overwritten',
|
||||||
|
() => /STATIC_ROUTE_NAMES[\s\S]*'toolParsePqdif'/.test(read(files.dynamicRouter))
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'tools center links to parsePqdif',
|
||||||
|
() => /handleNavigate\('\/tools\/parsePqdif'\)/.test(read(files.toolsHome)) && /PQDIF解析/.test(read(files.toolsHome))
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
const failures = checks.filter(([, check]) => !check()).map(([name]) => name)
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error('parsePqdif route contract failed:')
|
||||||
|
for (const failure of failures) {
|
||||||
|
console.error(`- ${failure}`)
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('parsePqdif route contract passed')
|
||||||
84
frontend/src/views/tools/parsePqdif/index.vue
Normal file
84
frontend/src/views/tools/parsePqdif/index.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table-box parse-pqdif-page">
|
||||||
|
<section class="parse-pqdif-grid">
|
||||||
|
<PqdifRequestPanel
|
||||||
|
:selected-file-name="selectedFileName"
|
||||||
|
:selected-file-size-text="selectedFileSizeText"
|
||||||
|
:is-parsing="isParsing"
|
||||||
|
:can-parse="canParse"
|
||||||
|
:can-reset="canReset"
|
||||||
|
:request-status-text="requestStatusText"
|
||||||
|
:request-status-tag-type="requestStatusTagType"
|
||||||
|
@file-change="handleFileChange"
|
||||||
|
@parse="parseSelectedFile"
|
||||||
|
@reset="resetPage"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PqdifSummaryPanel :result="parseResult" :is-business-failed="isBusinessFailed" />
|
||||||
|
|
||||||
|
<PqdifResultPanel
|
||||||
|
v-model:active-tab="activeResultTab"
|
||||||
|
:records="records"
|
||||||
|
:observations="observations"
|
||||||
|
class="parse-pqdif-result"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import PqdifRequestPanel from './components/PqdifRequestPanel.vue'
|
||||||
|
import PqdifResultPanel from './components/PqdifResultPanel.vue'
|
||||||
|
import PqdifSummaryPanel from './components/PqdifSummaryPanel.vue'
|
||||||
|
import { useParsePqdifFlow } from './utils/useParsePqdifFlow'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ParsePqdifView'
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedFileName,
|
||||||
|
selectedFileSizeText,
|
||||||
|
parseResult,
|
||||||
|
records,
|
||||||
|
observations,
|
||||||
|
isParsing,
|
||||||
|
isBusinessFailed,
|
||||||
|
activeResultTab,
|
||||||
|
requestStatusText,
|
||||||
|
requestStatusTagType,
|
||||||
|
canParse,
|
||||||
|
canReset,
|
||||||
|
handleFileChange,
|
||||||
|
parseSelectedFile,
|
||||||
|
resetPage
|
||||||
|
} = useParsePqdifFlow()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.parse-pqdif-page {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parse-pqdif-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 420px;
|
||||||
|
gap: 12px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parse-pqdif-result {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.parse-pqdif-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
142
frontend/src/views/tools/parsePqdif/utils/useParsePqdifFlow.ts
Normal file
142
frontend/src/views/tools/parsePqdif/utils/useParsePqdifFlow.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import type { ResultData } from '@/api/interface'
|
||||||
|
import { parsePqdifApi } from '@/api/tools/parsePqdif'
|
||||||
|
import type { ParsePqdif } from '@/api/tools/parsePqdif/interface'
|
||||||
|
|
||||||
|
type ResultTab = 'records' | 'observations'
|
||||||
|
type TagType = 'success' | 'warning' | 'info' | 'primary' | 'danger'
|
||||||
|
|
||||||
|
const SUPPORTED_PQDIF_EXTENSIONS = ['pqd', 'pqdif']
|
||||||
|
|
||||||
|
const 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 getFileExtension = (fileName: string) => fileName.split('.').pop()?.toLowerCase() || ''
|
||||||
|
|
||||||
|
const formatFileSize = (size: number) => {
|
||||||
|
if (!Number.isFinite(size) || size <= 0) return '0 B'
|
||||||
|
if (size < 1024) return `${size} B`
|
||||||
|
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
||||||
|
return `${(size / 1024 / 1024).toFixed(2)} MB`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getErrorMessage = (error: unknown) => {
|
||||||
|
if (error instanceof Error && error.message) return error.message
|
||||||
|
if (error && typeof error === 'object' && 'message' in error) return String((error as { message?: unknown }).message || '')
|
||||||
|
return 'PQDIF解析接口调用失败,请检查后端服务和文件内容'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useParsePqdifFlow = () => {
|
||||||
|
const selectedFile = ref<File | null>(null)
|
||||||
|
const parseResult = ref<ParsePqdif.ParseResponse | null>(null)
|
||||||
|
const isParsing = ref(false)
|
||||||
|
const activeResultTab = ref<ResultTab>('records')
|
||||||
|
|
||||||
|
const selectedFileName = computed(() => selectedFile.value?.name || '')
|
||||||
|
const selectedFileSizeText = computed(() => (selectedFile.value ? formatFileSize(selectedFile.value.size) : ''))
|
||||||
|
const hasResult = computed(() => Boolean(parseResult.value))
|
||||||
|
const isBusinessFailed = computed(() => parseResult.value?.status === 'FAILED')
|
||||||
|
const records = computed(() => parseResult.value?.records || [])
|
||||||
|
const observations = computed(() => parseResult.value?.observations || [])
|
||||||
|
const canParse = computed(() => Boolean(selectedFile.value && !isParsing.value))
|
||||||
|
const canReset = computed(() => Boolean(selectedFile.value || parseResult.value) && !isParsing.value)
|
||||||
|
|
||||||
|
const requestStatusText = computed(() => {
|
||||||
|
if (isParsing.value) return '解析中'
|
||||||
|
if (parseResult.value?.status === 'SUCCESS') return '解析成功'
|
||||||
|
if (parseResult.value?.status === 'FAILED') return '解析失败'
|
||||||
|
if (selectedFile.value) return '待解析'
|
||||||
|
return '未选择文件'
|
||||||
|
})
|
||||||
|
|
||||||
|
const requestStatusTagType = computed<TagType>(() => {
|
||||||
|
if (isParsing.value) return 'warning'
|
||||||
|
if (parseResult.value?.status === 'SUCCESS') return 'success'
|
||||||
|
if (parseResult.value?.status === 'FAILED') return 'danger'
|
||||||
|
if (selectedFile.value) return 'primary'
|
||||||
|
return 'info'
|
||||||
|
})
|
||||||
|
|
||||||
|
const isSupportedPqdifFile = (fileName: string) => SUPPORTED_PQDIF_EXTENSIONS.includes(getFileExtension(fileName))
|
||||||
|
|
||||||
|
const setSelectedFile = (file: File | null) => {
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
if (!isSupportedPqdifFile(file.name)) {
|
||||||
|
ElMessage.warning('仅支持 .pqd 或 .pqdif 格式文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFile.value = file
|
||||||
|
parseResult.value = null
|
||||||
|
activeResultTab.value = 'records'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileChange = (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0] || null
|
||||||
|
|
||||||
|
setSelectedFile(file)
|
||||||
|
input.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseSelectedFile = async () => {
|
||||||
|
if (!selectedFile.value) {
|
||||||
|
ElMessage.warning('请先选择 PQDIF 文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isParsing.value = true
|
||||||
|
try {
|
||||||
|
// 关键业务节点:HTTP code 成功不等于解析成功,业务失败需要读取 data.status 和 data.message。
|
||||||
|
const response = await parsePqdifApi(selectedFile.value)
|
||||||
|
const payload = unwrapApiPayload(response)
|
||||||
|
|
||||||
|
parseResult.value = payload
|
||||||
|
activeResultTab.value = payload.observations?.length ? 'observations' : 'records'
|
||||||
|
|
||||||
|
if (payload.status === 'FAILED') {
|
||||||
|
ElMessage.error(payload.message || 'PQDIF解析失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success(payload.message || 'PQDIF解析完成')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
isParsing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetPage = () => {
|
||||||
|
selectedFile.value = null
|
||||||
|
parseResult.value = null
|
||||||
|
activeResultTab.value = 'records'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedFile,
|
||||||
|
selectedFileName,
|
||||||
|
selectedFileSizeText,
|
||||||
|
parseResult,
|
||||||
|
records,
|
||||||
|
observations,
|
||||||
|
isParsing,
|
||||||
|
isBusinessFailed,
|
||||||
|
hasResult,
|
||||||
|
activeResultTab,
|
||||||
|
requestStatusText,
|
||||||
|
requestStatusTagType,
|
||||||
|
canParse,
|
||||||
|
canReset,
|
||||||
|
handleFileChange,
|
||||||
|
parseSelectedFile,
|
||||||
|
resetPage
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user