refactor(steady): 重构稳态校验功能的合同验证逻辑

- 优化测量点对话框路径解析格式化
- 添加稳态校验重启API端点验证功能
- 更新创建参数类型定义支持可选lineId和lineIds数组
- 修复任务表格组件模板缩进格式问题
- 扩展任务表格列配置验证数组格式
- 添加任务表格仅对失败行显示重启操作功能
- 集成共享台账树组件发送选中叶节点监测点功能
- 添加多监测点创建参数构建支持
- 实现指标选择为空时全指标校验功能
- 添加预期项目数量计算功能
- 优化创建对话框指标选择标签防止输入框重置
- 添加任务表格树形选择滚动下拉菜单样式
- 实现页面创建流程调用创建API并轮询任务状态
- 添加创建结果面板加载状态和进度显示
- 实现页面重启流程确认调用重启API功能
- 优化汇总表格异常字段持久化支持
- 添加汇总表格监测点名称显示功能
- 优化详情面板按需加载项目详情
- 更新接口类型支持分页字段定义
- 优化ICD路径检查对话框标准映射参数传递
- 添加ICD记录导出SQL和JSON功能
- 扩展ICD路径API端点和类型定义
- 添加ICD路径参考选项和映射详情功能
- 重构ICD表格列显示移除激活和创建时间列
- 添加ICD映射详情对话框三个标签页功能
- 优化ICD映射详情JSON树形视图显示
- 更新ICD类型选项覆盖手动和上游标准状态
This commit is contained in:
2026-06-18 16:35:06 +08:00
parent e7519e5524
commit 92b7d3cd73
38 changed files with 3486 additions and 758 deletions

View File

@@ -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` 数组创建检测项。
- 当前文档只覆盖现有有效接口,不包含旧的任务获取或创建兼容接口。

View File

@@ -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`

View File

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

View File

@@ -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%;

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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
)

View File

@@ -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">

View File

@@ -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'))) {