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) => {
|
||||
return http.get<SteadyDataView.SteadyChecksquareQueryResult>('/steady/checksquare/detail', { taskId }, { loading: false })
|
||||
}
|
||||
|
||||
@@ -97,7 +97,8 @@ export namespace SteadyDataView {
|
||||
}
|
||||
|
||||
export interface SteadyChecksquareCreateParams {
|
||||
lineId: string
|
||||
lineId?: string
|
||||
lineIds?: string[]
|
||||
indicatorCodes: string[]
|
||||
timeStart: string
|
||||
timeEnd: string
|
||||
@@ -149,6 +150,8 @@ export namespace SteadyDataView {
|
||||
export interface SteadyChecksquareItem {
|
||||
itemId?: string
|
||||
itemKey: string
|
||||
lineId?: string
|
||||
lineName?: string
|
||||
indicatorCode: string
|
||||
indicatorName?: string
|
||||
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)
|
||||
}
|
||||
|
||||
export const listIcdPathReferencesApi = () => {
|
||||
return http.post<MmsMapping.IcdPathReferenceOption[]>('/api/mms-mapping/icd-paths/reference-list')
|
||||
}
|
||||
|
||||
export const createIcdPathApi = (params: MmsMapping.CreateIcdPathRequest) => {
|
||||
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)
|
||||
}
|
||||
|
||||
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) => {
|
||||
return http.post<MmsMapping.IcdJsonConsistencyCheckResponse>('/api/mms-mapping/check-icd-json-consistency', params)
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ export namespace MmsMapping {
|
||||
|
||||
export interface IcdCheckMsg {
|
||||
summary?: string
|
||||
details?: string[]
|
||||
details?: unknown[]
|
||||
issuesJson?: string
|
||||
correctedJson?: string
|
||||
[key: string]: unknown
|
||||
@@ -183,7 +183,6 @@ export namespace MmsMapping {
|
||||
export interface IcdPathRecord {
|
||||
id?: string
|
||||
name?: string
|
||||
path?: string
|
||||
angle?: number
|
||||
usePhaseIndex?: number
|
||||
state?: number
|
||||
@@ -199,6 +198,19 @@ export namespace MmsMapping {
|
||||
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 {
|
||||
keyword?: string
|
||||
type?: number
|
||||
@@ -207,10 +219,10 @@ export namespace MmsMapping {
|
||||
|
||||
export interface CreateIcdPathRequest {
|
||||
name: string
|
||||
path: string
|
||||
angle?: number
|
||||
usePhaseIndex?: number
|
||||
type?: number
|
||||
referenceIcdId?: string
|
||||
}
|
||||
|
||||
export interface CreateIcdPathWithFileRequest {
|
||||
@@ -236,6 +248,8 @@ export namespace MmsMapping {
|
||||
msg?: IcdCheckMsg
|
||||
}
|
||||
|
||||
export type IcdPathCheckMsgResponse = IcdCheckMsg | null
|
||||
|
||||
export interface IcdJsonConsistencyCheckRequest {
|
||||
checkedJson: 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',
|
||||
'toolWaveform',
|
||||
'toolMmsMapping',
|
||||
'toolParsePqdif',
|
||||
'deviceTypes',
|
||||
'toolAddData',
|
||||
'toolAddLedger',
|
||||
|
||||
@@ -63,6 +63,15 @@ export const staticRouter: RouteRecordRaw[] = [
|
||||
title: '模型映射管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/tools/parsePqdif',
|
||||
name: 'toolParsePqdif',
|
||||
component: () => import('@/views/tools/parsePqdif/index.vue'),
|
||||
meta: {
|
||||
cacheName: 'ParsePqdifView',
|
||||
title: 'PQDIF解析'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/tools/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,56 +6,66 @@
|
||||
</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>
|
||||
<div class="result-body">
|
||||
<div class="result-overview">
|
||||
<div class="task-card">
|
||||
<div class="task-title">
|
||||
<span>{{ task.taskNo || task.taskId || '-' }}</span>
|
||||
<el-tag :type="resolveChecksquareTaskStatusType(task.taskStatus)" effect="plain">
|
||||
{{ formatChecksquareTaskStatus(task.taskStatus) }}
|
||||
</el-tag>
|
||||
<div class="result-scroll">
|
||||
<div class="result-body">
|
||||
<div class="result-overview">
|
||||
<div class="task-card">
|
||||
<div class="task-title">
|
||||
<span>{{ task.taskNo || task.taskId || '-' }}</span>
|
||||
<el-tag :type="resolveChecksquareTaskStatusType(task.taskStatus)" effect="plain">
|
||||
{{ formatChecksquareTaskStatus(task.taskStatus) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span>{{ task.lineName || task.lineId || '-' }}</span>
|
||||
<span>{{ task.timeStart || '-' }} 至 {{ task.timeEnd || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span>{{ task.lineName || task.lineId || '-' }}</span>
|
||||
<span>{{ task.timeStart || '-' }} 至 {{ task.timeEnd || '-' }}</span>
|
||||
|
||||
<div class="result-metrics">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">进线/监测点</span>
|
||||
<span class="metric-value">{{ task.lineName || task.lineId || '-' }}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">检测项</span>
|
||||
<span class="metric-value">{{ displayItemCount }}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">异常项</span>
|
||||
<span class="metric-value is-danger">{{ task.abnormalItemCount ?? '-' }}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">最低完整率</span>
|
||||
<span class="metric-value">
|
||||
{{ formatChecksquareIntegrity(task.minDataIntegrity) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">统计间隔</span>
|
||||
<span class="metric-value">{{ task.intervalMinutes ?? '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-metrics">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">进线/监测点</span>
|
||||
<span class="metric-value">{{ task.lineName || task.lineId || '-' }}</span>
|
||||
<div class="result-detail-grid">
|
||||
<div class="detail-block">
|
||||
<span class="detail-label">校验时间</span>
|
||||
<span class="detail-value">{{ task.timeStart || '-' }} 至 {{ task.timeEnd || '-' }}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">检测项</span>
|
||||
<span class="metric-value">{{ task.itemCount ?? '-' }}</span>
|
||||
<div class="detail-block">
|
||||
<span class="detail-label">创建时间</span>
|
||||
<span class="detail-value">{{ task.createTime || '-' }}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">异常项</span>
|
||||
<span class="metric-value is-danger">{{ task.abnormalItemCount ?? '-' }}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">最低完整率</span>
|
||||
<span class="metric-value">{{ formatChecksquareIntegrity(task.minDataIntegrity) }}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">统计间隔</span>
|
||||
<span class="metric-value">{{ task.intervalMinutes ?? '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-detail-grid">
|
||||
<div class="detail-block">
|
||||
<span class="detail-label">校验时间</span>
|
||||
<span class="detail-value">{{ task.timeStart || '-' }} 至 {{ task.timeEnd || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-block">
|
||||
<span class="detail-label">创建时间</span>
|
||||
<span class="detail-value">{{ task.createTime || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,6 +74,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
||||
import {
|
||||
formatChecksquareIntegrity,
|
||||
@@ -75,9 +87,18 @@ defineOptions({
|
||||
name: 'ChecksquareCreateResultPanel'
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -99,6 +120,43 @@ defineProps<{
|
||||
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 {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@@ -107,6 +165,13 @@ defineProps<{
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.result-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.result-overview {
|
||||
display: grid;
|
||||
flex: none;
|
||||
|
||||
@@ -31,6 +31,13 @@
|
||||
highlight-current-row
|
||||
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">
|
||||
<template #default="{ row }">
|
||||
<span class="indicator-name" :title="resolveChecksquareRowName(row)">
|
||||
@@ -105,21 +112,25 @@ defineOptions({
|
||||
name: 'ChecksquareSummaryTable'
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
result: SteadyDataView.SteadyChecksquareQueryResult | null
|
||||
items: SteadyDataView.SteadyChecksquareItem[]
|
||||
loading: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: []
|
||||
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 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) => {
|
||||
return stripPercentUnit(formatDataIntegrity(row.dataIntegrity, row.dataIntegrityText))
|
||||
}
|
||||
@@ -157,6 +168,7 @@ const formatSummaryStatIntegrity = (
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.line-name,
|
||||
.indicator-name {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
|
||||
@@ -11,6 +11,15 @@
|
||||
</template>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
</ProTable>
|
||||
@@ -19,7 +28,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, h, reactive, ref } from 'vue'
|
||||
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 type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
||||
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
||||
@@ -45,6 +54,7 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
createTask: []
|
||||
detail: [row: SteadyDataView.SteadyChecksquareTask]
|
||||
restart: [row: SteadyDataView.SteadyChecksquareTask]
|
||||
delete: [row: SteadyDataView.SteadyChecksquareTask]
|
||||
viewMeasurementPoint: [row: SteadyDataView.SteadyChecksquareTask]
|
||||
}>()
|
||||
@@ -135,6 +145,7 @@ const renderLineSearch = ({ searchParam }: { searchParam: ChecksquareTaskSearchP
|
||||
collapseTags: true,
|
||||
collapseTagsTooltip: true,
|
||||
maxCollapseTags: 1,
|
||||
popperClass: 'checksquare-search-tree-popper',
|
||||
filterable: true,
|
||||
clearable: true,
|
||||
defaultExpandAll: true,
|
||||
@@ -159,6 +170,7 @@ const renderIndicatorSearch = ({ searchParam }: { searchParam: ChecksquareTaskSe
|
||||
collapseTags: true,
|
||||
collapseTagsTooltip: true,
|
||||
maxCollapseTags: 1,
|
||||
popperClass: 'checksquare-search-tree-popper',
|
||||
filterable: true,
|
||||
clearable: true,
|
||||
defaultExpandAll: true,
|
||||
@@ -292,7 +304,7 @@ const columns = reactive<ColumnProps<SteadyDataView.SteadyChecksquareTask>[]>([
|
||||
minWidth: 170,
|
||||
render: ({ row }) => resolveChecksquareText(row.createTime)
|
||||
},
|
||||
{ prop: 'operation', label: '操作', fixed: 'right', width: 150 }
|
||||
{ prop: 'operation', label: '操作', fixed: 'right', width: 180 }
|
||||
])
|
||||
|
||||
const getTableList = (params: ChecksquareTaskSearchParams) => {
|
||||
@@ -329,4 +341,8 @@ defineExpose({
|
||||
vertical-align: bottom;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:global(.checksquare-search-tree-popper .el-select-dropdown__wrap) {
|
||||
max-height: 280px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -33,10 +33,10 @@
|
||||
/>
|
||||
</div>
|
||||
<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 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 class="toolbar-field indicator-form-item">
|
||||
<span class="toolbar-field__label">稳态指标:</span>
|
||||
@@ -52,6 +52,7 @@
|
||||
filterable
|
||||
clearable
|
||||
default-expand-all
|
||||
:disabled="loading.query"
|
||||
node-key="treeKey"
|
||||
value-key="treeKey"
|
||||
:props="{ label: 'name', children: 'children' }"
|
||||
@@ -65,7 +66,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</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>
|
||||
</section>
|
||||
@@ -331,7 +332,65 @@ watch(
|
||||
|
||||
.indicator-tree-select {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
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 {
|
||||
|
||||
@@ -15,7 +15,10 @@ const files = {
|
||||
summaryTable: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareSummaryTable.vue'),
|
||||
detailPanel: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareDetailPanel.vue'),
|
||||
createResultPanel: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareCreateResultPanel.vue'),
|
||||
measurementPointDialog: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareMeasurementPointDialog.vue'),
|
||||
measurementPointDialog: path.resolve(
|
||||
rootDir,
|
||||
'views/steady/checksquare/components/ChecksquareMeasurementPointDialog.vue'
|
||||
),
|
||||
payload: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquarePayload.ts'),
|
||||
ledgerUtils: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareLedger.ts'),
|
||||
taskTableUtils: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareTaskTable.ts'),
|
||||
@@ -39,11 +42,20 @@ const checks = [
|
||||
/\/steady\/checksquare\/create/.test(api) &&
|
||||
!/\/steady\/checksquare\/get-or-create/.test(api) &&
|
||||
/\/steady\/checksquare\/delete/.test(api) &&
|
||||
/\/steady\/checksquare\/restart\?taskId=/.test(api) &&
|
||||
/\/steady\/checksquare\/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',
|
||||
() =>
|
||||
@@ -66,7 +78,8 @@ const checks = [
|
||||
const typeBlock =
|
||||
read(files.apiTypes).match(/interface SteadyChecksquareCreateParams\s*\{[\s\S]*?\n {4}\}/)?.[0] || ''
|
||||
return (
|
||||
/lineId: string/.test(typeBlock) &&
|
||||
/lineId\?: string/.test(typeBlock) &&
|
||||
/lineIds\?: string\[\]/.test(typeBlock) &&
|
||||
/indicatorCodes: string\[\]/.test(typeBlock) &&
|
||||
/timeStart: string/.test(typeBlock) &&
|
||||
/timeEnd: string/.test(typeBlock) &&
|
||||
@@ -75,7 +88,10 @@ const checks = [
|
||||
}
|
||||
],
|
||||
['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',
|
||||
() =>
|
||||
@@ -87,9 +103,17 @@ const checks = [
|
||||
'task table has documented task columns',
|
||||
() => {
|
||||
const source = read(files.taskTable)
|
||||
return ['taskNo', 'lineName', 'timeStart', 'timeEnd', 'taskStatus', 'itemCount', 'abnormalItemCount', 'minDataIntegrity', 'createTime'].every(
|
||||
prop => new RegExp(`prop:\\s*'${prop}'`).test(source)
|
||||
)
|
||||
return [
|
||||
'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',
|
||||
() => {
|
||||
@@ -112,8 +149,7 @@ const checks = [
|
||||
return (
|
||||
/prop:\s*'abnormalItemCount'[\s\S]*ElButton[\s\S]*type:\s*'primary'[\s\S]*link:\s*true[\s\S]*emit\('detail', row\)/.test(
|
||||
taskTable
|
||||
) &&
|
||||
/resolveChecksquareText\(row\.abnormalItemCount\)/.test(taskTable)
|
||||
) && /resolveChecksquareText\(row\.abnormalItemCount\)/.test(taskTable)
|
||||
)
|
||||
}
|
||||
],
|
||||
@@ -124,8 +160,18 @@ const checks = [
|
||||
/taskTimeRange/.test(read(files.taskTableUtils)) &&
|
||||
/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 create action uses short add label',
|
||||
@@ -148,7 +194,51 @@ const checks = [
|
||||
],
|
||||
[
|
||||
'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',
|
||||
@@ -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',
|
||||
() => {
|
||||
@@ -292,20 +414,23 @@ const checks = [
|
||||
[
|
||||
'page wraps old workbench in create dialog',
|
||||
() =>
|
||||
/<el-dialog[\s\S]*新增校验任务[\s\S]*width="960px"[\s\S]*<ChecksquareWorkbench/.test(
|
||||
read(files.page)
|
||||
) &&
|
||||
/<el-dialog[\s\S]*新增校验任务[\s\S]*width="960px"[\s\S]*<ChecksquareWorkbench/.test(read(files.page)) &&
|
||||
/\.checksquare-create-dialog\s*\{[\s\S]*height:\s*560px/.test(read(files.page))
|
||||
],
|
||||
[
|
||||
'page create flow calls create api, keeps summary only and refreshes task table',
|
||||
() =>
|
||||
/createSteadyChecksquareTask/.test(read(files.page)) &&
|
||||
!/getOrCreateSteadyChecksquareTask/.test(read(files.page)) &&
|
||||
!/refreshCreateTaskDetail/.test(read(files.page)) &&
|
||||
!/startCreateTaskPolling/.test(read(files.page)) &&
|
||||
/taskTableRef\.value\?\.refresh\(\)/.test(read(files.page)) &&
|
||||
/activeCreateTask\.value/.test(read(files.page))
|
||||
'page create flow calls create api, then polls task list status and refreshes task table',
|
||||
() => {
|
||||
const page = read(files.page)
|
||||
return (
|
||||
/createSteadyChecksquareTask/.test(page) &&
|
||||
/querySteadyChecksquareTasks\(buildCreateTaskStatusQuery\(activeCreateTask\.value\)\)/.test(page) &&
|
||||
!/getSteadyChecksquareDetail\(createdTask\.taskId\)/.test(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',
|
||||
@@ -315,54 +440,75 @@ const checks = [
|
||||
const panel = read(files.createResultPanel)
|
||||
return (
|
||||
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
|
||||
) &&
|
||||
/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) &&
|
||||
/<slot name="result" \/>/.test(workbench) &&
|
||||
/\.checksquare-main\s*\{[\s\S]*display:\s*flex[\s\S]*flex-direction:\s*column/.test(workbench) &&
|
||||
/\.checksquare-layout\s*\{[\s\S]*grid-template-columns:\s*240px\s+minmax\(0,\s*1fr\)/.test(
|
||||
workbench
|
||||
) &&
|
||||
/\.checksquare-layout\s*\{[\s\S]*grid-template-columns:\s*240px\s+minmax\(0,\s*1fr\)/.test(workbench) &&
|
||||
/\.checksquare-result-slot\s*\{[\s\S]*width:\s*100%/.test(workbench) &&
|
||||
/\.checksquare-result-slot\s*\{[\s\S]*min-width:\s*0/.test(workbench) &&
|
||||
/\.checksquare-result-slot\s*:deep\(\.checksquare-result-panel\)\s*\{[\s\S]*height:\s*100%/.test(
|
||||
workbench
|
||||
) &&
|
||||
/name:\s*'ChecksquareCreateResultPanel'/.test(panel) &&
|
||||
/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-body"/.test(panel) &&
|
||||
!/class="result-items"/.test(panel) &&
|
||||
!/detailItems/.test(panel) &&
|
||||
/class="result-detail-grid"/.test(panel) &&
|
||||
!/detail-block--wide/.test(panel) &&
|
||||
/\.result-overview\s*\{[\s\S]*grid-template-columns:\s*minmax\(0,\s*1fr\)/.test(panel) &&
|
||||
/\.result-metrics\s*\{[\s\S]*grid-template-columns:\s*repeat\(2,\s*minmax\(0,\s*1fr\)\)/.test(panel) &&
|
||||
/\.result-detail-grid\s*\{[^}]*grid-template-columns:\s*repeat\(2,\s*minmax\(0,\s*1fr\)\)/.test(panel) &&
|
||||
/\.result-detail-grid\s*\{[^}]*grid-template-columns:\s*repeat\(2,\s*minmax\(0,\s*1fr\)\)/.test(
|
||||
panel
|
||||
) &&
|
||||
!/class="result-tips"/.test(panel) &&
|
||||
!/class="tips-title"/.test(panel) &&
|
||||
!/class="tips-list"/.test(panel) &&
|
||||
/class="result-metrics"[\s\S]*lineName[\s\S]*task\.itemCount/.test(panel) &&
|
||||
/class="result-metrics"[\s\S]*lineName[\s\S]*displayItemCount/.test(panel) &&
|
||||
/校验任务摘要/.test(panel) &&
|
||||
/task\.itemCount/.test(panel) &&
|
||||
/displayItemCount/.test(panel) &&
|
||||
/props\.expectedItemCount/.test(panel) &&
|
||||
/task\.abnormalItemCount/.test(panel) &&
|
||||
/task\.minDataIntegrity/.test(panel) &&
|
||||
!/class="detail-label">任务编号/.test(panel) &&
|
||||
/\.result-body\s*\{[\s\S]*flex:\s*1/.test(panel) &&
|
||||
/\.result-scroll\s*\{[\s\S]*overflow-y:\s*auto/.test(panel) &&
|
||||
!/class="result-detail-table"/.test(panel) &&
|
||||
!/resultItems/.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)
|
||||
return (
|
||||
!/createTaskPollingTimer/.test(page) &&
|
||||
!/startCreateTaskPolling/.test(page) &&
|
||||
!/stopCreateTaskPolling/.test(page) &&
|
||||
!/setInterval/.test(page) &&
|
||||
!/onBeforeUnmount/.test(page)
|
||||
/createTaskPollingTimer/.test(page) &&
|
||||
/startCreateTaskPolling/.test(page) &&
|
||||
/stopCreateTaskPolling/.test(page) &&
|
||||
/window\.setTimeout/.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)) &&
|
||||
/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',
|
||||
() => /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',
|
||||
@@ -393,7 +550,9 @@ const checks = [
|
||||
],
|
||||
[
|
||||
'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',
|
||||
@@ -401,7 +560,9 @@ const checks = [
|
||||
const summaryTable = read(files.summaryTable)
|
||||
return (
|
||||
/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(
|
||||
summaryTable
|
||||
) &&
|
||||
@@ -414,8 +575,9 @@ const checks = [
|
||||
() => {
|
||||
const summaryTable = read(files.summaryTable)
|
||||
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 (
|
||||
/label="数据完整性"/.test(dataIntegrityGroup) &&
|
||||
@@ -459,6 +621,24 @@ const checks = [
|
||||
'summary table keeps indicator name column at configured width',
|
||||
() => /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',
|
||||
() => {
|
||||
@@ -534,14 +714,17 @@ const checks = [
|
||||
],
|
||||
[
|
||||
'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',
|
||||
() => {
|
||||
const types = read(files.apiTypes)
|
||||
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(
|
||||
types
|
||||
)
|
||||
|
||||
@@ -7,22 +7,14 @@
|
||||
:request-api="querySteadyChecksquareTasks"
|
||||
@create-task="openCreateDialog"
|
||||
@detail="openTaskDetail"
|
||||
@restart="handleRestartTask"
|
||||
@delete="handleDeleteTask"
|
||||
@view-measurement-point="openMeasurementPointDialog"
|
||||
/>
|
||||
|
||||
<ChecksquareMeasurementPointDialog
|
||||
v-model:visible="measurementPointDialogVisible"
|
||||
:data="measurementPointData"
|
||||
/>
|
||||
<ChecksquareMeasurementPointDialog v-model:visible="measurementPointDialogVisible" :data="measurementPointData" />
|
||||
|
||||
<el-dialog
|
||||
v-model="createDialogVisible"
|
||||
title="新增校验任务"
|
||||
width="960px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<el-dialog v-model="createDialogVisible" title="新增校验任务" width="960px" append-to-body destroy-on-close>
|
||||
<div class="checksquare-create-dialog">
|
||||
<ChecksquareWorkbench
|
||||
v-model:form="formState"
|
||||
@@ -44,6 +36,8 @@
|
||||
<template #result>
|
||||
<ChecksquareCreateResultPanel
|
||||
:task="activeCreateTask"
|
||||
:loading="loading.query"
|
||||
:expected-item-count="expectedCreateItemCount"
|
||||
/>
|
||||
</template>
|
||||
</ChecksquareWorkbench>
|
||||
@@ -71,7 +65,7 @@
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
createSteadyChecksquareTask,
|
||||
@@ -79,7 +73,8 @@ import {
|
||||
getSteadyChecksquareDetail,
|
||||
getSteadyTrendIndicatorTree,
|
||||
getSteadyTrendLedgerTree,
|
||||
querySteadyChecksquareTasks
|
||||
querySteadyChecksquareTasks,
|
||||
restartSteadyChecksquareTask
|
||||
} from '@/api/steady/steadyDataView'
|
||||
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
||||
import {
|
||||
@@ -102,6 +97,7 @@ import {
|
||||
} from './utils/checksquareLedger'
|
||||
import {
|
||||
buildSteadyChecksquareCreatePayload,
|
||||
calculateChecksquareExpectedItemCount,
|
||||
defaultChecksquareFormState,
|
||||
validateChecksquareSelection
|
||||
} from './utils/checksquarePayload'
|
||||
@@ -137,8 +133,20 @@ const loading = reactive({
|
||||
detail: false
|
||||
})
|
||||
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 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 => {
|
||||
if (response && typeof response === 'object' && 'data' in response) {
|
||||
@@ -148,6 +156,13 @@ const unwrapData = <T,>(response: { data: T } | T): 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) => {
|
||||
loading.ledger = true
|
||||
try {
|
||||
@@ -177,6 +192,7 @@ const loadIndicatorTree = async () => {
|
||||
}
|
||||
|
||||
const openCreateDialog = () => {
|
||||
stopCreateTaskPolling()
|
||||
activeCreateTask.value = null
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
@@ -196,6 +212,7 @@ const handleIndicatorChange = (nodes: SteadyDataView.SteadyIndicatorNode[]) => {
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
stopCreateTaskPolling()
|
||||
formState.value = defaultChecksquareFormState()
|
||||
selectedLedgerNodes.value = []
|
||||
selectedIndicators.value = []
|
||||
@@ -205,7 +222,82 @@ const handleReset = () => {
|
||||
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
|
||||
if (!taskId) return null
|
||||
|
||||
@@ -236,14 +328,19 @@ const handleCreateTask = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
activeCreateTask.value = null
|
||||
loading.query = true
|
||||
try {
|
||||
// /create 只返回任务行信息,检测项明细统一通过 /detail 拉取,避免把列表行误当成详情数据。
|
||||
// /create 返回任务行信息,新增弹窗只监控任务状态,不拉取检测项详情。
|
||||
const response = await createSteadyChecksquareTask(
|
||||
buildSteadyChecksquareCreatePayload(lineIds.value[0], selectedIndicators.value, formState.value)
|
||||
buildSteadyChecksquareCreatePayload(lineIds.value, selectedIndicators.value, formState.value)
|
||||
)
|
||||
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('校验任务已获取')
|
||||
taskTableRef.value?.refresh()
|
||||
} finally {
|
||||
@@ -283,12 +380,31 @@ const handleDeleteTask = async (row: SteadyDataView.SteadyChecksquareTask) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 删除接口按任务 ID 数组批量处理;列表行操作只传当前行任务 ID。
|
||||
// 删除接口按任务 ID 数组批量处理,行操作只传当前任务 ID。
|
||||
await deleteSteadyChecksquareTasks([row.taskId])
|
||||
ElMessage.success('删除校验任务成功')
|
||||
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) => {
|
||||
selectedItem.value = item
|
||||
itemDetailDialogVisible.value = true
|
||||
@@ -303,6 +419,12 @@ onMounted(() => {
|
||||
loadLedgerTree()
|
||||
loadIndicatorTree()
|
||||
})
|
||||
|
||||
watch(createDialogVisible, visible => {
|
||||
if (!visible) stopCreateTaskPolling()
|
||||
})
|
||||
|
||||
onBeforeUnmount(stopCreateTaskPolling)
|
||||
</script>
|
||||
|
||||
<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[]
|
||||
}
|
||||
|
||||
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 = (
|
||||
lineId: string,
|
||||
lineIds: string[],
|
||||
indicators: SteadyDataView.SteadyIndicatorNode[],
|
||||
formState: ChecksquareFormState
|
||||
): SteadyDataView.SteadyChecksquareCreateParams => {
|
||||
return {
|
||||
lineId,
|
||||
lineId: lineIds[0],
|
||||
lineIds,
|
||||
indicatorCodes: collectChecksquareIndicatorCodes(indicators),
|
||||
timeStart: (formState.timeRange[0] || '').replace(/\.[^.]+$/, ''),
|
||||
timeEnd: (formState.timeRange[1] || '').replace(/\.[^.]+$/, '')
|
||||
@@ -47,11 +59,9 @@ export const validateChecksquareSelection = (params: {
|
||||
indicators: SteadyDataView.SteadyIndicatorNode[]
|
||||
timeRange: string[]
|
||||
}) => {
|
||||
const { lineIds, indicators, timeRange } = params
|
||||
const { lineIds, timeRange } = params
|
||||
|
||||
if (!lineIds.length) return '请选择监测点'
|
||||
if (lineIds.length > 1) return '数据校验一次只能选择一个监测点'
|
||||
if (!indicators.length) return '请选择指标'
|
||||
if (!timeRange[0]) return '请选择开始时间'
|
||||
if (!timeRange[1]) return '请选择结束时间'
|
||||
if (Date.parse(timeRange[0].replace(' ', 'T')) > Date.parse(timeRange[1].replace(' ', 'T'))) {
|
||||
|
||||
@@ -123,7 +123,7 @@ const handleKeywordChange = (value: string) => {
|
||||
}
|
||||
|
||||
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 () => {
|
||||
|
||||
@@ -12,8 +12,8 @@ const selectionRulesSource = fs.readFileSync(path.join(currentDir, '..', 'utils'
|
||||
|
||||
const expectations = [
|
||||
[
|
||||
'ledger tree excludes half-checked parents when collecting checked nodes',
|
||||
/getCheckedNodes\(\s*false\s*,\s*false\s*\)/,
|
||||
'ledger tree emits leaf monitor points when collecting checked nodes',
|
||||
/getCheckedNodes\(\s*true\s*,\s*false\s*\)/,
|
||||
read('SteadyLedgerTree.vue')
|
||||
],
|
||||
[
|
||||
|
||||
@@ -123,7 +123,7 @@ const handleKeywordChange = (value: string) => {
|
||||
}
|
||||
|
||||
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 () => {
|
||||
|
||||
@@ -12,8 +12,8 @@ const selectionRulesSource = fs.readFileSync(path.join(currentDir, '..', 'utils'
|
||||
|
||||
const expectations = [
|
||||
[
|
||||
'ledger tree excludes half-checked parents when collecting checked nodes',
|
||||
/getCheckedNodes\(\s*false\s*,\s*false\s*\)/,
|
||||
'ledger tree emits leaf monitor points when collecting checked nodes',
|
||||
/getCheckedNodes\(\s*true\s*,\s*false\s*\)/,
|
||||
read('SteadyLedgerTree.vue')
|
||||
],
|
||||
[
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
<div class="tool-text">进入 MMS 映射页面,后续可继续补充映射配置和预览能力。</div>
|
||||
</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')">
|
||||
<div class="tool-name">设备类型维护</div>
|
||||
<div class="tool-text">维护设备类型,并从列表进入 ICD 一致性校验和 PQDIF 预留校验。</div>
|
||||
|
||||
@@ -83,11 +83,14 @@
|
||||
:is-saving-icd-check-result="isSavingIcdCheckResult"
|
||||
:save-icd-check-result-text="saveIcdCheckResultText"
|
||||
:icd-consistency-status="icdConsistencyStatus"
|
||||
:should-open-icd-consistency-problems="shouldOpenIcdConsistencyProblems"
|
||||
:show-description="false"
|
||||
@export-mapping="handleExportMapping"
|
||||
@export-mapping="handleExportMappingEvent"
|
||||
@generate-xml-mapping="handleGenerateXmlMapping"
|
||||
@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"
|
||||
@sequence-config-complete="handleSequenceConfigComplete"
|
||||
@save-icd-check-result="handleSaveIcdCheckResult"
|
||||
@@ -125,7 +128,8 @@ const props = defineProps<{
|
||||
icdPathId: string
|
||||
icdPathName: string
|
||||
icdPathType: number
|
||||
activeIcdPathRecord: MmsMapping.IcdPathRecord | null
|
||||
standardMappingJson: string
|
||||
standardMappingName: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -134,7 +138,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
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> => {
|
||||
return saveIcdPathCheckResultApi(props.icdPathId, params)
|
||||
@@ -184,6 +188,7 @@ const {
|
||||
saveIcdCheckResultText,
|
||||
hasIcdConsistencyCheckResult,
|
||||
icdConsistencyStatus,
|
||||
shouldOpenIcdConsistencyProblems,
|
||||
confirmDialogVisible,
|
||||
isConfirmingSelection,
|
||||
handleIcdFileChange,
|
||||
@@ -193,6 +198,8 @@ const {
|
||||
handleExportMapping,
|
||||
handleGenerateXmlMapping,
|
||||
handleIcdConsistencyCheck,
|
||||
handleConfirmIcdConsistencyProblems,
|
||||
handleRemoveIcdConsistencyProblem,
|
||||
handleUpdateMappingJson,
|
||||
handleSequenceConfigComplete,
|
||||
handleSaveIcdCheckResult,
|
||||
@@ -200,9 +207,9 @@ const {
|
||||
} = useMmsMappingFlow({
|
||||
deviceTypeCheckId: toRef(props, 'icdPathId'),
|
||||
deviceTypeCheckName: toRef(props, 'icdPathName'),
|
||||
// 关键业务节点:只有需要一致性校验的 ICD 类型才注入标准 JSON,避免隐藏校验按钮后保存仍被校验前置条件卡住。
|
||||
standardMappingJson: computed(() => (showConsistencyCheck.value ? props.activeIcdPathRecord?.jsonStr?.trim() || '' : '')),
|
||||
standardMappingName: computed(() => (showConsistencyCheck.value ? props.activeIcdPathRecord?.name?.trim() || '' : '')),
|
||||
// 关键业务节点:列表接口不再返回大字段,标准 JSON 由宿主页通过 mapping-detail 详情接口加载后传入。
|
||||
standardMappingJson: computed(() => (showConsistencyCheck.value ? props.standardMappingJson?.trim() || '' : '')),
|
||||
standardMappingName: computed(() => (showConsistencyCheck.value ? props.standardMappingName?.trim() || '' : '')),
|
||||
saveIcdCheckResult,
|
||||
onIcdCheckSaved: () => {
|
||||
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'
|
||||
|
||||
interface IcdCheckFlowStep {
|
||||
@@ -348,10 +380,16 @@ const handleClose = () => {
|
||||
|
||||
:deep(.icd-check-flow .el-step__description) {
|
||||
max-width: 132px;
|
||||
min-height: 36px;
|
||||
margin-top: 6px;
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
:deep(.icd-check-flow .el-step__head.is-process),
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
<div class="icd-check-dialog__body">
|
||||
<div class="icd-path-form-dialog__body">
|
||||
<el-form ref="formRef" :model="formModel" :rules="formRules" label-width="120px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="ICD名称" prop="name">
|
||||
<el-input
|
||||
v-model="formModel.name"
|
||||
@@ -21,8 +19,6 @@
|
||||
placeholder="请输入 ICD 名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="ICD类型">
|
||||
<el-select v-model="formModel.type" clearable placeholder="请选择 ICD 类型">
|
||||
<el-option
|
||||
@@ -33,33 +29,23 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="ICD路径" prop="path">
|
||||
<el-input
|
||||
v-model="formModel.path"
|
||||
maxlength="260"
|
||||
show-word-limit
|
||||
<el-form-item label="标准ICD引用" prop="referenceIcdId">
|
||||
<el-select
|
||||
v-model="formModel.referenceIcdId"
|
||||
clearable
|
||||
placeholder="请输入 ICD 存储路径"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="ICD文件">
|
||||
<el-upload
|
||||
accept=".icd,.cid,.scd,.xml"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:show-file-list="true"
|
||||
:on-change="handleIcdFileChange"
|
||||
:on-remove="handleIcdFileRemove"
|
||||
filterable
|
||||
:loading="referenceLoading"
|
||||
placeholder="请选择标准 ICD 引用"
|
||||
>
|
||||
<el-button type="primary" plain>选择文件</el-button>
|
||||
</el-upload>
|
||||
<el-option
|
||||
v-for="option in referenceOptions"
|
||||
:key="option.id"
|
||||
:label="option.name"
|
||||
:value="option.id"
|
||||
:disabled="!option.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="角度">
|
||||
<el-input-number
|
||||
v-model="formModel.angle"
|
||||
@@ -68,8 +54,6 @@
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="相位索引">
|
||||
<el-switch
|
||||
v-model="formModel.usePhaseIndex"
|
||||
@@ -79,8 +63,6 @@
|
||||
inactive-text="关闭"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,9 +76,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
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 { createIcdPathApi, createIcdPathWithFileApi, updateIcdPathApi, updateIcdPathWithFileApi } from '@/api/tools/mmsmapping'
|
||||
import { createIcdPathApi, listIcdPathReferencesApi, updateIcdPathApi } from '@/api/tools/mmsmapping'
|
||||
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
|
||||
|
||||
defineOptions({
|
||||
@@ -117,32 +99,38 @@ const emit = defineEmits<{
|
||||
interface IcdPathFormModel {
|
||||
id: string
|
||||
name: string
|
||||
path: string
|
||||
angle?: number
|
||||
usePhaseIndex: number
|
||||
type?: number
|
||||
referenceIcdId: string
|
||||
}
|
||||
|
||||
interface IcdPathReferenceSelectOption {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const saving = ref(false)
|
||||
const selectedIcdFile = ref<File | null>(null)
|
||||
const referenceLoading = ref(false)
|
||||
const referenceOptions = ref<IcdPathReferenceSelectOption[]>([])
|
||||
const formModel = reactive<IcdPathFormModel>({
|
||||
id: '',
|
||||
name: '',
|
||||
path: '',
|
||||
angle: 0,
|
||||
usePhaseIndex: 0,
|
||||
type: 3
|
||||
type: 4,
|
||||
referenceIcdId: ''
|
||||
})
|
||||
const icdTypeOptions = [
|
||||
{ label: '手动录入的标准', value: 1 },
|
||||
{ label: '手动录入的非标准', value: 2 },
|
||||
{ label: '上游解析传递', value: 3 }
|
||||
{ label: '手动录入的标准 ICD', value: 1 },
|
||||
{ label: '手动录入的非标准 ICD', value: 2 },
|
||||
{ label: '上游解析传递的标准 ICD', value: 3 },
|
||||
{ label: '上游解析传递的非标准 ICD', value: 4 }
|
||||
]
|
||||
const pathRequired = computed(() => !selectedIcdFile.value)
|
||||
const formRules = computed<FormRules<IcdPathFormModel>>(() => ({
|
||||
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记录'))
|
||||
|
||||
@@ -162,11 +150,10 @@ const getErrorMessage = (error: unknown) => {
|
||||
const resetForm = () => {
|
||||
formModel.id = ''
|
||||
formModel.name = ''
|
||||
formModel.path = ''
|
||||
formModel.angle = 0
|
||||
formModel.usePhaseIndex = 0
|
||||
formModel.type = 3
|
||||
selectedIcdFile.value = null
|
||||
formModel.type = 4
|
||||
formModel.referenceIcdId = ''
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
@@ -176,38 +163,57 @@ const fillForm = (record: MmsMapping.IcdPathRecord | null) => {
|
||||
|
||||
formModel.id = record.id || ''
|
||||
formModel.name = record.name || ''
|
||||
formModel.path = record.path || ''
|
||||
formModel.angle = record.angle ?? 0
|
||||
formModel.usePhaseIndex = record.usePhaseIndex ?? 0
|
||||
formModel.type = record.type
|
||||
formModel.referenceIcdId = record.referenceIcdId || ''
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
visible => {
|
||||
if (visible) fillForm(props.record)
|
||||
if (!visible) return
|
||||
|
||||
fillForm(props.record)
|
||||
loadReferenceOptions()
|
||||
}
|
||||
)
|
||||
|
||||
const buildSavePayload = (): MmsMapping.CreateIcdPathRequest => ({
|
||||
name: formModel.name.trim(),
|
||||
path: formModel.path.trim() || selectedIcdFile.value?.name || '',
|
||||
angle: formModel.angle,
|
||||
usePhaseIndex: formModel.usePhaseIndex,
|
||||
type: formModel.type
|
||||
type: formModel.type,
|
||||
referenceIcdId: formModel.referenceIcdId
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const handleIcdFileChange = (uploadFile: UploadFile) => {
|
||||
selectedIcdFile.value = uploadFile.raw || null
|
||||
formRef.value?.clearValidate('path')
|
||||
}
|
||||
const loadReferenceOptions = async () => {
|
||||
referenceLoading.value = true
|
||||
|
||||
const handleIcdFileRemove = () => {
|
||||
selectedIcdFile.value = null
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}, [])
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error))
|
||||
referenceOptions.value = []
|
||||
} finally {
|
||||
referenceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -225,30 +231,13 @@ const handleSave = async () => {
|
||||
const payload = buildSavePayload()
|
||||
let response: ResultData<boolean> | boolean
|
||||
|
||||
if (selectedIcdFile.value) {
|
||||
// 关键业务节点:选择 ICD 文件时按接口文档提交 icdFile + request,后端会同步保存原始文件内容。
|
||||
response =
|
||||
props.mode === 'create'
|
||||
? await createIcdPathWithFileApi({
|
||||
icdFile: selectedIcdFile.value,
|
||||
request: payload
|
||||
})
|
||||
: await updateIcdPathWithFileApi({
|
||||
icdFile: selectedIcdFile.value,
|
||||
request: {
|
||||
...payload,
|
||||
id: formModel.id
|
||||
}
|
||||
})
|
||||
} else {
|
||||
response =
|
||||
props.mode === 'create'
|
||||
? await createIcdPathApi(payload)
|
||||
: await updateIcdPathApi({
|
||||
...payload,
|
||||
id: formModel.id
|
||||
})
|
||||
}
|
||||
response =
|
||||
props.mode === 'create'
|
||||
? await createIcdPathApi(payload)
|
||||
: await updateIcdPathApi({
|
||||
...payload,
|
||||
id: formModel.id
|
||||
})
|
||||
const saved = unwrapApiPayload<boolean>(response)
|
||||
|
||||
if (saved === false) {
|
||||
|
||||
@@ -33,12 +33,17 @@ const checks = [
|
||||
[
|
||||
'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
|
||||
) &&
|
||||
/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
|
||||
)
|
||||
) &&
|
||||
!/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 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 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 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)],
|
||||
@@ -67,17 +94,47 @@ const checks = [
|
||||
/createIcdPathWithFileApi/.test(apiSource) &&
|
||||
/updateIcdPathWithFileApi/.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 JSON consistency API uses backend comparison endpoint', () => /checkIcdJsonConsistencyApi/.test(apiSource) && /\/api\/mms-mapping\/check-icd-json-consistency/.test(apiSource) && /interface\s+IcdJsonConsistencyCheckRequest/.test(typeSource) && /interface\s+IcdJsonConsistencyCheckResponse/.test(typeSource)],
|
||||
['ICD path types are defined', () => /interface\s+IcdPathRecord/.test(typeSource) && /interface\s+CreateIcdPathRequest/.test(typeSource) && /interface\s+UpdateIcdPathRequest/.test(typeSource)],
|
||||
['ICD path type options are fixed business enum values', () => /icdTypeOptions\s*=\s*\[[\s\S]*手动录入的标准[\s\S]*value:\s*1[\s\S]*手动录入的非标准[\s\S]*value:\s*2[\s\S]*上游解析传递[\s\S]*value:\s*3/.test(pageSource) && /icdTypeOptions\s*=\s*\[[\s\S]*手动录入的标准[\s\S]*value:\s*1[\s\S]*手动录入的非标准[\s\S]*value:\s*2[\s\S]*上游解析传递[\s\S]*value:\s*3/.test(formDialogSource)],
|
||||
['ICD path 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 search uses select options', () => /prop:\s*'type'[\s\S]*enum:\s*icdTypeOptions[\s\S]*search:\s*\{[\s\S]*el:\s*'select'/.test(pageSource)],
|
||||
['ICD path form uses select for type', () => /<el-select\s+v-model="formModel\.type"[\s\S]*<el-option[\s\S]*v-for="option in icdTypeOptions"/.test(formDialogSource)],
|
||||
['new ICD path defaults to upstream parsed type', () => /type:\s*3/.test(formDialogSource) && /formModel\.type\s*=\s*3/.test(formDialogSource)],
|
||||
['ICD 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',
|
||||
() =>
|
||||
@@ -86,25 +143,89 @@ const checks = [
|
||||
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(
|
||||
pageSource
|
||||
) && /renderActivationStatus[\s\S]*handleActivateIcdPath\(row\)/.test(pageSource)
|
||||
!/prop:\s*'activation'[\s\S]*label:\s*'激活'/.test(pageSource) &&
|
||||
!/renderActivationStatus/.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 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)],
|
||||
['ICD path form allows uploaded file name to fill missing path', () => /pathRequired/.test(formDialogSource) && /selectedIcdFile\.value\?\.name/.test(formDialogSource)],
|
||||
[
|
||||
'ICD table shows mapping detail, reference standard ICD, then check result',
|
||||
() =>
|
||||
/listIcdPathReferencesApi/.test(pageSource) &&
|
||||
/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 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',
|
||||
() =>
|
||||
/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*3/.test(checkDialogSource) &&
|
||||
/:show-icd-check-action="showConsistencyCheck"/.test(checkDialogSource)
|
||||
],
|
||||
['ICD check action calls backend consistency check before save', () => /@icd-check="handleIcdConsistencyCheck"/.test(checkDialogSource) && /handleIcdConsistencyCheck/.test(flowSource) && /checkIcdJsonConsistencyApi/.test(flowSource) && /lastIcdConsistencyCheckResult/.test(flowSource)],
|
||||
@@ -114,13 +235,14 @@ const checks = [
|
||||
/icdConsistencyStatus/.test(flowSource) &&
|
||||
/:icd-consistency-status="icdConsistencyStatus"/.test(checkDialogSource) &&
|
||||
/icd-consistency-indicator/.test(resultPanelSource) &&
|
||||
/icdConsistencyProblemDialogVisible\s*=\s*true/.test(resultPanelSource)
|
||||
/openIcdConsistencyProblems/.test(resultPanelSource)
|
||||
],
|
||||
[
|
||||
'ICD check issues are separated from JSON mapping problems',
|
||||
() =>
|
||||
/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
|
||||
) &&
|
||||
!/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',
|
||||
() =>
|
||||
/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
|
||||
) &&
|
||||
/SaveIcdPathCheckResultRequest[\s\S]*msg\?:\s*IcdCheckMsg/.test(typeSource) &&
|
||||
@@ -163,6 +285,24 @@ const checks = [
|
||||
/renderIcdCheckMsg/.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',
|
||||
() =>
|
||||
|
||||
@@ -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
|
||||
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 #operation="{ row }">
|
||||
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
|
||||
@@ -42,23 +60,91 @@
|
||||
:icd-path-id="currentIcdCheckRecord.id"
|
||||
:icd-path-name="currentIcdCheckRecord.name"
|
||||
:icd-path-type="currentIcdCheckRecord.type"
|
||||
:active-icd-path-record="activeIcdPathRecord"
|
||||
:standard-mapping-json="activeIcdPathMappingJson"
|
||||
:standard-mapping-name="activeIcdPathMappingName"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
<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 { nextTick, reactive, ref } from 'vue'
|
||||
import { computed, nextTick, reactive, ref } from 'vue'
|
||||
import type { ResultData } from '@/api/interface'
|
||||
import ProTable from '@/components/ProTable/index.vue'
|
||||
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
||||
import { 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 IcdPathFormDialog from './components/IcdPathFormDialog.vue'
|
||||
import IcdPathCheckDialog from './components/IcdPathCheckDialog.vue'
|
||||
import JsonMappingTree from './components/JsonMappingTree.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'MmsMappingView'
|
||||
@@ -77,15 +163,56 @@ interface IcdPathTableParams {
|
||||
const proTable = ref<ProTableInstance>()
|
||||
const formDialogVisible = 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 currentFormRecord = ref<MmsMapping.IcdPathRecord | null>(null)
|
||||
const currentIcdCheckRecord = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
type: 3
|
||||
type: 4
|
||||
})
|
||||
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 {
|
||||
if (response && typeof response === 'object' && 'data' in response) {
|
||||
@@ -129,12 +256,16 @@ const resultOptions = [
|
||||
]
|
||||
|
||||
const icdTypeOptions = [
|
||||
{ label: '手动录入的标准', value: 1 },
|
||||
{ label: '手动录入的非标准', value: 2 },
|
||||
{ label: '上游解析传递', value: 3 }
|
||||
{ label: '手动录入的标准 ICD', value: 1 },
|
||||
{ label: '手动录入的非标准 ICD', value: 2 },
|
||||
{ label: '上游解析传递的标准 ICD', value: 3 },
|
||||
{ label: '上游解析传递的非标准 ICD', value: 4 }
|
||||
]
|
||||
|
||||
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']) => {
|
||||
if (!value) return ''
|
||||
@@ -148,36 +279,189 @@ const renderIcdCheckMsg = (value: MmsMapping.IcdPathRecord['msg']) => {
|
||||
}
|
||||
}
|
||||
|
||||
const renderActivationStatus = (row: MmsMapping.IcdPathRecord) => {
|
||||
if (row.type === 1) {
|
||||
const icdCheckMsgSummary = computed(() => {
|
||||
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 (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
if (row.type === 2) {
|
||||
return (
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
loading={activatingIcdPathId.value === row.id}
|
||||
disabled={!row.id}
|
||||
onClick={() => handleActivateIcdPath(row)}
|
||||
>
|
||||
激活
|
||||
</el-button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<el-tag type={getIcdResultTagType(row.result)} effect="light">
|
||||
{getIcdResultText(row.result)}
|
||||
</el-tag>
|
||||
)
|
||||
}
|
||||
|
||||
const renderReferenceIcdName = (row: MmsMapping.IcdPathRecord) => {
|
||||
const referenceName = getReferenceIcdName(row.referenceIcdId)
|
||||
|
||||
if (!row.referenceIcdId) return '-'
|
||||
|
||||
return (
|
||||
<el-button type="primary" size="small" disabled>
|
||||
激活
|
||||
<el-button link type="primary" disabled={!referenceName} onClick={() => openReferenceIcdDetail(row)}>
|
||||
{referenceName || row.referenceIcdId}
|
||||
</el-button>
|
||||
)
|
||||
}
|
||||
|
||||
const renderMappingDetailAction = (row: MmsMapping.IcdPathRecord) => (
|
||||
<el-button link type="primary" icon={Document} disabled={!row.id} onClick={() => openMappingDetail(row)}>
|
||||
查看详情
|
||||
</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>[]>([
|
||||
{ type: 'selection', fixed: 'left', width: 70 },
|
||||
{ type: 'index', fixed: 'left', width: 70, label: '序号' },
|
||||
@@ -192,15 +476,14 @@ const columns = reactive<ColumnProps<MmsMapping.IcdPathRecord>[]>([
|
||||
label: '关键字',
|
||||
order: 1,
|
||||
props: {
|
||||
placeholder: '请输入 ICD 名称或路径'
|
||||
placeholder: '请输入 ICD 名称'
|
||||
}
|
||||
}
|
||||
},
|
||||
{ prop: 'path', label: 'ICD路径', minWidth: 260 },
|
||||
{
|
||||
prop: 'type',
|
||||
label: 'ICD类型',
|
||||
width: 170,
|
||||
width: 220,
|
||||
render: scope => getIcdTypeText(scope.row.type),
|
||||
enum: icdTypeOptions,
|
||||
isFilterEnum: false,
|
||||
@@ -220,15 +503,19 @@ const columns = reactive<ColumnProps<MmsMapping.IcdPathRecord>[]>([
|
||||
width: 110,
|
||||
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',
|
||||
label: '校验结论',
|
||||
minWidth: 140,
|
||||
render: scope => (
|
||||
<el-tag type={getIcdResultTagType(scope.row.result)} effect="light">
|
||||
{getIcdResultText(scope.row.result)}
|
||||
</el-tag>
|
||||
),
|
||||
width: 150,
|
||||
render: scope => renderIcdResult(scope.row),
|
||||
enum: resultOptions,
|
||||
isFilterEnum: false,
|
||||
search: {
|
||||
@@ -237,16 +524,7 @@ const columns = reactive<ColumnProps<MmsMapping.IcdPathRecord>[]>([
|
||||
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: 'activation',
|
||||
label: '激活',
|
||||
width: 110,
|
||||
render: scope => renderActivationStatus(scope.row)
|
||||
},
|
||||
{ prop: 'operation', label: '操作', fixed: 'right', width: 240 }
|
||||
])
|
||||
|
||||
@@ -261,13 +539,15 @@ const buildListParams = (params: IcdPathTableParams): MmsMapping.IcdPathListRequ
|
||||
}
|
||||
|
||||
const getTableList = async (params: IcdPathTableParams = {}) => {
|
||||
await loadReferenceIcdNameMap()
|
||||
|
||||
const response = await listIcdPathsApi(buildListParams(params))
|
||||
const records = unwrapApiPayload<MmsMapping.IcdPathRecord[]>(response) || []
|
||||
const pageNum = params.pageNum || 1
|
||||
const pageSize = params.pageSize || 10
|
||||
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 分页结构,保持和设备类型管理页一致。
|
||||
return {
|
||||
@@ -281,11 +561,30 @@ const getTableList = async (params: IcdPathTableParams = {}) => {
|
||||
}
|
||||
|
||||
const refreshActiveIcdPathRecord = async () => {
|
||||
// 关键业务节点:标准 ICD 不能依赖当前表格筛选结果,否则校验弹窗可能拿不到已激活记录的 JSON。
|
||||
const response = await listIcdPathsApi({ type: 1 })
|
||||
const records = unwrapApiPayload<MmsMapping.IcdPathRecord[]>(response) || []
|
||||
// 关键业务节点:标准 ICD 不能依赖当前表格筛选结果,否则校验弹窗可能拿不到已激活记录。
|
||||
const [manualResponse, upstreamResponse] = await Promise.all([listIcdPathsApi({ type: 1 }), listIcdPathsApi({ type: 3 })])
|
||||
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 = () => {
|
||||
@@ -294,6 +593,24 @@ const refreshIcdPaths = () => {
|
||||
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) => {
|
||||
ElMessage.error(getErrorMessage(error))
|
||||
}
|
||||
@@ -310,6 +627,50 @@ const openEditDialog = (row: MmsMapping.IcdPathRecord) => {
|
||||
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) => {
|
||||
if (!ids.length) {
|
||||
ElMessage.warning('请选择要删除的 ICD 记录')
|
||||
@@ -348,56 +709,57 @@ const handleBatchDelete = async (ids: string[]) => {
|
||||
await deleteIcdPaths(ids, 'ICD 记录批量删除成功')
|
||||
}
|
||||
|
||||
const handleActivateIcdPath = async (row: MmsMapping.IcdPathRecord) => {
|
||||
if (!row.id) {
|
||||
ElMessage.warning('当前 ICD 记录缺少 ID,不能激活')
|
||||
return
|
||||
}
|
||||
|
||||
if (!row.jsonStr?.trim()) {
|
||||
ElMessage.warning('当前 ICD 记录缺少 JSON 映射结果,不能激活,请先完成 ICD 校验并保存')
|
||||
const handleExportSelectedSql = async (records: MmsMapping.IcdPathRecord[]) => {
|
||||
if (!records.length) {
|
||||
ElMessage.warning('请选择要导出的 ICD 记录')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm('激活后该 ICD 将作为标准 ICD 参与一致性校验,是否继续?', '激活ICD记录', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const exportRecords = await Promise.all(
|
||||
records.map(async row => ({
|
||||
row,
|
||||
detail: row.id ? await getIcdPathMappingDetail(row) : null
|
||||
}))
|
||||
)
|
||||
const sql = buildIcdPathInsertSql(exportRecords)
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||||
|
||||
activatingIcdPathId.value = row.id
|
||||
|
||||
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()
|
||||
downloadTextFile(sql, `icd-path-${timestamp}.sql`, 'application/sql;charset=utf-8')
|
||||
ElMessage.success('SQL 已导出')
|
||||
} catch (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) {
|
||||
ElMessage.warning('当前 ICD 记录缺少 ID,不能执行 ICD 校验')
|
||||
return
|
||||
@@ -406,11 +768,38 @@ const handleIcdCheck = (row: MmsMapping.IcdPathRecord) => {
|
||||
// 关键业务节点:MMS 映射页承载 ICD 存储记录校验,生成的 JSON/XML 回写当前 ICD 记录。
|
||||
currentIcdCheckRecord.id = row.id
|
||||
currentIcdCheckRecord.name = row.name || ''
|
||||
currentIcdCheckRecord.type = row.type ?? 3
|
||||
refreshActiveIcdPathRecord()
|
||||
currentIcdCheckRecord.type = row.type ?? 4
|
||||
|
||||
if (isNonStandardIcdType(currentIcdCheckRecord.type)) {
|
||||
await refreshActiveIcdPathRecord()
|
||||
}
|
||||
|
||||
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)
|
||||
</script>
|
||||
|
||||
@@ -420,4 +809,71 @@ nextTick(refreshActiveIcdPathRecord)
|
||||
padding: 0;
|
||||
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>
|
||||
|
||||
@@ -48,6 +48,9 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
const isGeneratingXml = ref(false)
|
||||
const isSavingIcdCheckResult = ref(false)
|
||||
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 hasSequenceConfigured = ref(false)
|
||||
const sequenceDialogVisible = ref(false)
|
||||
@@ -271,13 +274,20 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
const canSaveIcdCheckResult = computed(() => {
|
||||
if (!deviceTypeCheckId.value || !mappingJsonPreview.value || !xmlContentForExport.value || isSubmitting.value) return false
|
||||
if (standardMappingJson.value && !lastIcdConsistencyCheckResult.value) return false
|
||||
if (icdConsistencyStatus.value === 'failed' && !hasConfirmedIcdConsistencyProblems.value) return false
|
||||
|
||||
return true
|
||||
})
|
||||
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>(() => {
|
||||
if (!lastIcdConsistencyCheckResult.value) return ''
|
||||
return lastIcdConsistencyCheckResult.value.result === 1 ? 'success' : 'failed'
|
||||
return reviewedIcdConsistencyResult.value === 1 ? 'success' : 'failed'
|
||||
})
|
||||
const saveIcdCheckResultText = computed(() => '保存')
|
||||
|
||||
@@ -324,7 +334,11 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
...(xmlResponsePayload.value?.problems?.filter(Boolean) || [])
|
||||
])
|
||||
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 methodDescribe = computed(() => xmlResponsePayload.value?.methodDescribe?.trim() || '')
|
||||
@@ -397,6 +411,9 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
responsePayload.value = null
|
||||
xmlResponsePayload.value = null
|
||||
lastIcdConsistencyCheckResult.value = null
|
||||
reviewedIcdConsistencyProblemList.value = []
|
||||
hasConfirmedIcdConsistencyProblems.value = false
|
||||
shouldOpenIcdConsistencyProblems.value = 0
|
||||
parsedIcdDocument.value = null
|
||||
hasSequenceConfigured.value = false
|
||||
sequenceDialogVisible.value = false
|
||||
@@ -646,6 +663,8 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
const payload = unwrapApiPayload<MmsMapping.IcdJsonConsistencyCheckResponse>(response)
|
||||
|
||||
lastIcdConsistencyCheckResult.value = payload
|
||||
reviewedIcdConsistencyProblemList.value = payload.issues?.filter(Boolean) || []
|
||||
hasConfirmedIcdConsistencyProblems.value = payload.result === 1
|
||||
|
||||
if (payload.result === 1) {
|
||||
ElMessage.success(payload.message || `与${standardMappingName.value}一致`)
|
||||
@@ -653,6 +672,7 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
}
|
||||
|
||||
activeResultTab.value = 'problem'
|
||||
shouldOpenIcdConsistencyProblems.value += 1
|
||||
ElMessage.warning(payload.message || `与${standardMappingName.value}不一致`)
|
||||
} catch (error) {
|
||||
lastIcdConsistencyCheckResult.value = {
|
||||
@@ -660,6 +680,9 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
message: getErrorMessage(error),
|
||||
issues: [getErrorMessage(error)]
|
||||
}
|
||||
reviewedIcdConsistencyProblemList.value = [getErrorMessage(error)]
|
||||
hasConfirmedIcdConsistencyProblems.value = false
|
||||
shouldOpenIcdConsistencyProblems.value += 1
|
||||
activeResultTab.value = 'problem'
|
||||
ElMessage.error(getErrorMessage(error))
|
||||
} finally {
|
||||
@@ -677,6 +700,43 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
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 () => {
|
||||
if (!deviceTypeCheckId.value) {
|
||||
ElMessage.warning('当前缺少设备类型 ID,不能入库校验结果')
|
||||
@@ -700,20 +760,28 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (icdConsistencyStatus.value === 'failed' && !hasConfirmedIcdConsistencyProblems.value) {
|
||||
ElMessage.warning('请先确认 ICD 校验问题列表')
|
||||
return
|
||||
}
|
||||
|
||||
isSavingIcdCheckResult.value = true
|
||||
|
||||
try {
|
||||
const checkResult = lastIcdConsistencyCheckResult.value
|
||||
const result = checkResult?.result ?? 1
|
||||
const result = reviewedIcdConsistencyResult.value
|
||||
const summaryMessage =
|
||||
checkResult?.message ||
|
||||
(xmlContentForExport.value ? 'ICD一致性校验通过,JSON/XML已生成' : 'ICD一致性校验通过,JSON已生成')
|
||||
result === 1
|
||||
? xmlContentForExport.value
|
||||
? 'ICD一致性校验通过,JSON/XML已生成'
|
||||
: 'ICD一致性校验通过,JSON已生成'
|
||||
: checkResult?.message || 'ICD一致性校验不通过'
|
||||
const savePayload = {
|
||||
icdDocument: parsedIcdDocument.value || responsePayload.value?.icdDocument,
|
||||
mappingJson,
|
||||
xml: xmlContentForExport.value,
|
||||
result,
|
||||
msg: buildIcdCheckMsg(checkResult, summaryMessage)
|
||||
msg: buildReviewedIcdCheckMsg(checkResult, summaryMessage)
|
||||
}
|
||||
// 关键业务节点:ICD 校验流程可由设备类型或 ICD 存储记录承载,保存接口由宿主页注入以避免串写业务表。
|
||||
const response = options.saveIcdCheckResult
|
||||
@@ -745,6 +813,9 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
}
|
||||
xmlResponsePayload.value = null
|
||||
lastIcdConsistencyCheckResult.value = null
|
||||
reviewedIcdConsistencyProblemList.value = []
|
||||
hasConfirmedIcdConsistencyProblems.value = false
|
||||
shouldOpenIcdConsistencyProblems.value = 0
|
||||
hasSequenceConfigured.value = false
|
||||
activeResultTab.value = 'json'
|
||||
}
|
||||
@@ -809,6 +880,7 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
saveIcdCheckResultText,
|
||||
hasIcdConsistencyCheckResult,
|
||||
icdConsistencyStatus,
|
||||
shouldOpenIcdConsistencyProblems,
|
||||
confirmDialogVisible,
|
||||
isConfirmingSelection,
|
||||
handleIcdFileChange,
|
||||
@@ -818,6 +890,8 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
handleExportMapping,
|
||||
handleGenerateXmlMapping,
|
||||
handleIcdConsistencyCheck,
|
||||
handleConfirmIcdConsistencyProblems,
|
||||
handleRemoveIcdConsistencyProblem,
|
||||
handleUpdateMappingJson,
|
||||
handleSequenceConfigComplete,
|
||||
handleSaveIcdCheckResult,
|
||||
|
||||
@@ -54,7 +54,11 @@
|
||||
<div class="panel-section result-card grow-card preview-tab-section">
|
||||
<div v-if="icdConsistencyStatus" class="icd-consistency-status">
|
||||
<el-tooltip
|
||||
:content="icdConsistencyStatus === 'success' ? 'ICD校验通过' : 'ICD校验不通过,点击查看JSON映射问题列表'"
|
||||
:content="
|
||||
icdConsistencyStatus === 'success'
|
||||
? 'ICD校验通过'
|
||||
: `ICD校验不通过,点击查看JSON映射问题列表(${icdConsistencyProblemCount}条)`
|
||||
"
|
||||
placement="top"
|
||||
>
|
||||
<button
|
||||
@@ -63,7 +67,13 @@
|
||||
:disabled="icdConsistencyStatus !== 'failed'"
|
||||
@click="openIcdConsistencyProblems"
|
||||
>
|
||||
<el-icon><WarningFilled /></el-icon>
|
||||
<el-badge
|
||||
:value="icdConsistencyProblemCount"
|
||||
:hidden="icdConsistencyProblemCount === 0"
|
||||
class="icd-consistency-indicator__badge"
|
||||
>
|
||||
<el-icon><WarningFilled /></el-icon>
|
||||
</el-badge>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@@ -108,7 +118,7 @@
|
||||
plain
|
||||
size="small"
|
||||
:icon="WarningFilled"
|
||||
@click="icdConsistencyProblemDialogVisible = true"
|
||||
@click="openIcdConsistencyProblems"
|
||||
>
|
||||
{{ icdConsistencyProblemButtonText }}
|
||||
</el-button>
|
||||
@@ -199,9 +209,22 @@
|
||||
>
|
||||
<span class="problem-index">{{ index + 1 }}</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>
|
||||
<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
|
||||
@@ -301,6 +324,7 @@
|
||||
import {
|
||||
CircleCheck,
|
||||
Connection,
|
||||
Delete,
|
||||
Document,
|
||||
Download,
|
||||
Finished,
|
||||
@@ -382,10 +406,12 @@ const props = withDefaults(defineProps<{
|
||||
isSavingIcdCheckResult: boolean
|
||||
saveIcdCheckResultText: string
|
||||
icdConsistencyStatus?: IcdConsistencyStatus
|
||||
shouldOpenIcdConsistencyProblems?: number
|
||||
}>(), {
|
||||
showDescription: true,
|
||||
showIcdCheckAction: true,
|
||||
icdConsistencyStatus: ''
|
||||
icdConsistencyStatus: '',
|
||||
shouldOpenIcdConsistencyProblems: 0
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -397,6 +423,8 @@ const emit = defineEmits<{
|
||||
(event: 'sequence-config-complete'): void
|
||||
(event: 'icd-check'): void
|
||||
(event: 'save-icd-check-result'): void
|
||||
(event: 'confirm-icd-consistency-problems'): void
|
||||
(event: 'remove-icd-consistency-problem', index: number): void
|
||||
}>()
|
||||
|
||||
const activeTabProxy = computed({
|
||||
@@ -414,6 +442,7 @@ const icdConsistencyProblemButtonText = computed(() =>
|
||||
? `${ICD_CONSISTENCY_PROBLEM_LABEL} (${props.icdConsistencyProblemList.length})`
|
||||
: ICD_CONSISTENCY_PROBLEM_LABEL
|
||||
)
|
||||
const icdConsistencyProblemCount = computed(() => props.icdConsistencyProblemList.length)
|
||||
const problemDialogVisible = ref(false)
|
||||
const icdConsistencyProblemDialogVisible = ref(false)
|
||||
const matchResultDialogVisible = ref(false)
|
||||
@@ -480,6 +509,19 @@ const openIcdConsistencyProblems = () => {
|
||||
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 =>
|
||||
Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
||||
|
||||
@@ -940,6 +982,11 @@ const confirmSequenceConfig = () => {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.problem-remove {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.match-result-detail {
|
||||
max-height: 58vh;
|
||||
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