feat(steady): 重构稳态校验功能并优化界面布局

- 更新 API 接口路径从 /steady/data-view/checksquare/* 到 /steady/checksquare/*
- 修改校验任务状态枚举值 FAILED 为 FAIL 并更新相关处理逻辑
- 移除缺失率和最大连续缺失分钟数字段,简化数据完整性计算
- 添加新的创建结果面板组件 ChecksquareCreateResultPanel.vue
- 调整创建对话框布局,采用两行搜索控件设计
- 更新任务表头部按钮文字为"新增"并调整搜索列配置为5列
- 修改详情面板显示开始时间和结束时间字段
- 重构工作台界面布局,使用 flex 布局替代 grid 布局
- 更新设备类型相关 API 接口和数据结构定义
- 添加设备类型字典常量并更新路由配置
- 优化搜索表单展开收起逻辑的计算方式
- 调整创建流程不再轮询获取任务详情,改为直接显示摘要信息
- 更新数据完整性格式化函数参数和调用方式
- 修改创建对话框样式类名和尺寸配置
- 添加设备类型管理相关的接口定义和实现方法
This commit is contained in:
2026-06-15 08:40:44 +08:00
parent 81f90ce0f2
commit ef80aff151
38 changed files with 4165 additions and 1254 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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