Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ed318530a | ||
|
|
8c7c34cda2 | ||
|
|
b0ee58a1b5 | ||
|
|
c3915dc8f5 | ||
|
|
ff4a197436 | ||
|
|
ad2d94a893 | ||
|
|
030e84ae16 | ||
|
|
60112e84e3 | ||
|
|
8f12d0e03e | ||
|
|
49ded8c568 | ||
|
|
a39ad579f9 | ||
|
|
657d357159 | ||
|
|
0d7d5722c1 | ||
|
|
36496e5825 |
@@ -54,13 +54,13 @@ webSocket:
|
||||
port: 7778
|
||||
|
||||
#源参数下发,暂态数据默认值
|
||||
Dip:
|
||||
# 暂态前时间(s)
|
||||
# fPreTime: 2f
|
||||
#Dip:
|
||||
# 暂态前时间(s)
|
||||
# fPreTime: 2f
|
||||
#写入时间(s)
|
||||
fRampIn: 0.001f
|
||||
# fRampIn: 0.001f
|
||||
#写出时间(s)
|
||||
fRampOut: 0.001f
|
||||
# fRampOut: 0.001f
|
||||
# 暂态后时间(s)
|
||||
# fAfterTime: 3f
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo52
Normal file
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo52
Normal file
Binary file not shown.
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo53
Normal file
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo53
Normal file
Binary file not shown.
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo54
Normal file
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo54
Normal file
Binary file not shown.
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo55
Normal file
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo55
Normal file
Binary file not shown.
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo56
Normal file
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo56
Normal file
Binary file not shown.
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo57
Normal file
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo57
Normal file
Binary file not shown.
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo82_tmp
Normal file
BIN
build/extraResources/mysql/data/#innodb_redo/#ib_redo82_tmp
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
||||
55936
|
||||
51248
|
||||
|
||||
Binary file not shown.
BIN
build/extraResources/mysql/data/binlog.000042
Normal file
BIN
build/extraResources/mysql/data/binlog.000042
Normal file
Binary file not shown.
@@ -17,3 +17,4 @@
|
||||
.\binlog.000039
|
||||
.\binlog.000040
|
||||
.\binlog.000041
|
||||
.\binlog.000042
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
211
docs/superpowers/specs/2026-07-10-time-check-design.md
Normal file
211
docs/superpowers/specs/2026-07-10-time-check-design.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# 守时检测设计
|
||||
|
||||
## 背景
|
||||
|
||||
现有守时页面 `frontend/src/views/home/components/timeTest.vue` 是演示式页面,包含假数据、步骤条和旧的“标准时间/上送时刻”展示方式,不符合新的业务流程。新的守时检测需要同时支持多台装置,动态展示每台装置的暂态触发时刻、守时误差和结果,并与现有检测弹窗、后端 `startPreTest` 流程联动。
|
||||
|
||||
后端项目位于 `D:\njcn\test\CN_Gather`。本设计覆盖前端与后端协同修改。
|
||||
|
||||
## 目标
|
||||
|
||||
- 守时页面只展示守时检测用时和设备结果表,不展示源连接、设备连接、协议校验、相序校验等前置阶段。
|
||||
- 表格列固定为:装置名称、暂态1触发时刻、暂态2触发时刻、守时误差、结果。
|
||||
- 未收到对应数据时,单元格显示 loading 转圈状态。
|
||||
- 守时检测计时格式为 `HH:mm:ss`。
|
||||
- 支持多台设备同时检测。
|
||||
- 守时检测过程要像 `factorTest.vue` 一样监听并拦截 WebSocket 错误,错误时终止流程。
|
||||
- 守时完成后,如果后续还有正式检测,前后端自动流向正式检测且保持组件状态;如果没有正式检测,则清理连接和 WebSocket 资源。
|
||||
|
||||
## 检测项契约
|
||||
|
||||
`PreDetectionParam.testItemList` 从 3 位改为 4 位:
|
||||
|
||||
1. 第 1 位:预检测
|
||||
2. 第 2 位:系数校准
|
||||
3. 第 3 位:守时校验
|
||||
4. 第 4 位:正式检测
|
||||
|
||||
前端所有调用 `startPreTest` 的位置都必须发送 4 位数组,例如:
|
||||
|
||||
- 只做守时检测:`[false, false, true, false]`
|
||||
- 预检测 + 守时检测:`[true, false, true, false]`
|
||||
- 守时检测 + 正式检测:`[false, false, true, true]`
|
||||
- 预检测 + 守时检测 + 正式检测:`[true, false, true, true]`
|
||||
|
||||
后端所有读取 `testItemList` 的逻辑同步使用新索引,不能继续把第三位解释为正式检测。
|
||||
|
||||
## 前端流程
|
||||
|
||||
### 启动规则
|
||||
|
||||
`testPopup.vue` 继续作为检测弹窗流程编排入口。
|
||||
|
||||
只勾选守时检测时,前端调用现有 `startPreTest`。接口成功返回后,认为守时检测开始,`timeTest.vue` 开始计时。后端随后执行源连接、设备连接、协议校验、相序校验,再进入守时暂态流程。
|
||||
|
||||
勾选预检测 + 守时检测时,守时检测不在 `startPreTest` 成功后立刻开始计时。预检测包含源连接、设备连接、协议校验、相序校验四个阶段;这四个阶段完成后,后端推送守时开始/暂态数据相关消息,前端开始守时计时并展示表格更新。
|
||||
|
||||
### 页面结构
|
||||
|
||||
`timeTest.vue` 改为轻量结果页:
|
||||
|
||||
- 顶部为紧凑计时区域,显示 `守时检测用时:HH:mm:ss`。
|
||||
- 下方为结果表格,列为 `装置名称 / 暂态1触发时刻 / 暂态2触发时刻 / 守时误差 / 结果`。
|
||||
- 行数据初始化自 `checkStore.devices`。
|
||||
- 未收到数据的字段显示 Element Plus loading 转圈。
|
||||
- `time_waiting_second` 消息只触发轻量提示,例如“已收到第一组暂态数据,等待第二组暂态数据”,不增加页面状态区和统计区。
|
||||
|
||||
### Step 切换与状态保活
|
||||
|
||||
守时检测完成且后续进入正式检测后,用户可以点击弹窗顶部 step 切回守时检测页。此时守时页只读展示已完成结果,不重启监听、不重置 loading、不重新计时。
|
||||
|
||||
用户再切回正式检测页时,正式检测组件的表格信息、计时信息、日志进度和当前检测步骤不能初始化。实现上应保持组件实例存活,只切换 `stepsActiveView` 的显示状态,避免因 step 切换触发正式检测组件重建或初始化。
|
||||
|
||||
### 错误处理
|
||||
|
||||
`timeTest.vue` 需要接收并监听 `webMsgSend`,与 `factorTest.vue` 保持一致的错误拦截思路:
|
||||
|
||||
- 通用错误码如 `10520`、`10521`、`10522`、`10523`、`10524`、`10525`、`10527`、`10528`、`10529`、`10550`、`10551`、`10552`、`-1`。
|
||||
- WebSocket 连接类错误,如 `socket_timeout`、`server_error`、`device_error`、`connect/Source`、`connect/Dev`。
|
||||
- 源连接、设备连接、协议校验、相序校验阶段虽然不展示在页面中,但这些阶段的错误必须被守时页捕获,弹窗提示后 `emit('update:testStatus', 'error')`,父流程终止。
|
||||
|
||||
## 后端流程
|
||||
|
||||
### 总体接入
|
||||
|
||||
后端采用复用现有检测链路的方案。`startPreTest` 仍作为统一入口,守时校验作为现有检测链路中的独立业务阶段插入。
|
||||
|
||||
单独勾选守时检测时,后端需要执行完整前置阶段:
|
||||
|
||||
1. 源连接
|
||||
2. 设备连接
|
||||
3. 协议校验
|
||||
4. 相序校验
|
||||
5. 守时检测暂态流程
|
||||
|
||||
勾选预检测 + 守时检测时,预检测已经包含前四个阶段。后端不重复执行、不关闭源连接和设备连接,直接进入守时检测暂态流程。
|
||||
|
||||
### 守时业务规则
|
||||
|
||||
守时检测使用后端内置的两段守时专用暂态脚本,不由前端传脚本编号。
|
||||
|
||||
业务过程:
|
||||
|
||||
1. 下发暂态脚本 1。
|
||||
2. 收取每台装置的一组暂态数据。
|
||||
3. 两次暂态触发时刻均从 `DevData.time` 读取。
|
||||
4. 同一设备存在多个监测点数据时,以第一条数据为准。
|
||||
5. 第一组数据收齐后,后端推送 `time_waiting_second` 事件。
|
||||
6. 后端等待 `waitTime - 暂态持续时间1 - fRampOut - fPreTime - fRampIn`。
|
||||
7. 下发暂态脚本 2。
|
||||
8. 收取每台装置第二组暂态数据。
|
||||
9. 计算 `dtMs = 暂态1触发时刻 - 暂态2触发时刻 - waitTime`。
|
||||
10. `dtMs <= 10ms` 为合格,否则不合格。
|
||||
|
||||
结果判定完全由后端负责。前端只展示后端推送的守时误差和结果,不自行计算合格性。
|
||||
|
||||
### 守时结束后的资源处理
|
||||
|
||||
如果 `testItemList[3]` 为 `true`,守时检测完成后自动流向正式检测:
|
||||
|
||||
- 后端不关闭源连接。
|
||||
- 后端不关闭设备连接。
|
||||
- 后端不清理正式检测需要的流程上下文。
|
||||
- 前端不关闭 WebSocket,不重建正式检测组件。
|
||||
- 前端收到守时成功后自动切到正式检测步骤。
|
||||
|
||||
如果 `testItemList[3]` 为 `false`,守时检测后没有正式检测:
|
||||
|
||||
- 后端在守时结束后断开设备连接和源连接。
|
||||
- 后端清理检测流程上下文。
|
||||
- 前端停止计时,展示最终结果,状态置为 `success` 或 `error`。
|
||||
- 弹窗关闭时清理 WebSocket 回调或连接。
|
||||
|
||||
如果守时检测失败,无论是否勾选正式检测,都终止流程,不进入正式检测,并按错误流程释放资源。
|
||||
|
||||
## WebSocket 消息
|
||||
|
||||
守时阶段使用独立的守时消息语义,避免前端混在正式检测或系数校准消息里猜状态。推荐 `requestId` 使用 `Time_Check`。
|
||||
|
||||
### `time_first_data`
|
||||
|
||||
收到某台设备第一次暂态数据。
|
||||
|
||||
`data` 建议字段:
|
||||
|
||||
- `deviceId`
|
||||
- `deviceName`
|
||||
- `monitorId` 或监测点标识
|
||||
- `firstTriggerTime`
|
||||
|
||||
### `time_waiting_second`
|
||||
|
||||
第一组暂态数据已收齐,后端已进入等待第二次暂态数据阶段。
|
||||
|
||||
该消息只作为事件触发标志,前端只做提示,不依赖业务字段。
|
||||
|
||||
### `time_second_data`
|
||||
|
||||
收到某台设备第二次暂态数据。
|
||||
|
||||
`data` 建议字段:
|
||||
|
||||
- `deviceId`
|
||||
- `deviceName`
|
||||
- `monitorId` 或监测点标识
|
||||
- `secondTriggerTime`
|
||||
|
||||
### `time_result`
|
||||
|
||||
某台设备守时结果计算完成。
|
||||
|
||||
`data` 建议字段:
|
||||
|
||||
- `deviceId`
|
||||
- `deviceName`
|
||||
- `firstTriggerTime`
|
||||
- `secondTriggerTime`
|
||||
- `waitTimeMs`
|
||||
- `dtMs`
|
||||
- `result`
|
||||
- `message`
|
||||
|
||||
`result` 可以使用后端现有结果约定,也可以明确为 `passed` / `failed`。前端需统一映射为“合格 / 不合格”。
|
||||
|
||||
### `time_finish`
|
||||
|
||||
全部守时检测完成。
|
||||
|
||||
`data` 建议字段:
|
||||
|
||||
- `allPassed`
|
||||
- `results`
|
||||
- `hasFormalTest`
|
||||
|
||||
前端收到后停止守时计时。如果 `hasFormalTest` 为 `true`,自动流向正式检测;否则结束当前检测流程。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
前端:
|
||||
|
||||
- `frontend/src/views/home/components/timeTest.vue`
|
||||
- `frontend/src/views/home/components/testPopup.vue`
|
||||
- `frontend/src/api/socket/socket.ts`
|
||||
- 必要时检查 `frontend/src/views/home/components/compareTestPopup.vue` 是否存在同类 `testItemList` 构造逻辑。
|
||||
|
||||
后端:
|
||||
|
||||
- `D:\njcn\test\CN_Gather\detection\src\main\java\com\njcn\gather\detection\pojo\param\PreDetectionParam.java`
|
||||
- `D:\njcn\test\CN_Gather\detection\src\main\java\com\njcn\gather\detection\controller\PreDetectionController.java`
|
||||
- `D:\njcn\test\CN_Gather\detection\src\main\java\com\njcn\gather\detection\service\impl\PreDetectionServiceImpl.java`
|
||||
- `D:\njcn\test\CN_Gather\detection\src\main\java\com\njcn\gather\detection\handler\SocketDevResponseService.java`
|
||||
- `D:\njcn\test\CN_Gather\detection\src\main\java\com\njcn\gather\detection\pojo\enums\SourceOperateCodeEnum.java`
|
||||
|
||||
## 测试建议
|
||||
|
||||
- 单独勾选守时检测:验证 `startPreTest` 成功后计时立即开始,前置四阶段错误可被前端拦截,最终无正式检测时资源被清理。
|
||||
- 勾选预检测 + 守时检测:验证前置四阶段不重复执行、不关闭连接,守时计时从预检测四阶段完成后开始。
|
||||
- 勾选守时检测 + 正式检测:验证守时成功后自动进入正式检测,连接和 WebSocket 不被清理。
|
||||
- 正式检测中点击 step 切回守时页,再切回正式检测页:验证正式检测表格、计时、日志和进度不初始化。
|
||||
- 多设备且多监测点:验证每台设备只取第一条监测点数据参与守时计算。
|
||||
- 守时失败:验证不进入正式检测,并按错误流程释放资源。
|
||||
- `time_waiting_second`:验证只提示,不改变表格结构。
|
||||
@@ -18,4 +18,4 @@ VITE_COMPANY_NAME=南京灿能电力自动化股份有限公司
|
||||
|
||||
VITE_IS_SHOW_RAW_DATA=true
|
||||
# 开启激活验证
|
||||
VITE_ACTIVATE_OPEN=true
|
||||
VITE_ACTIVATE_OPEN=false
|
||||
|
||||
@@ -134,6 +134,8 @@ export namespace CheckData {
|
||||
devCurr: number //设备电流
|
||||
factorFlag: number //是否支持系数校准
|
||||
checkResult: number //检测结果
|
||||
checkState?: number | null //检测状态
|
||||
formalCheckTime?: number | null //正式检测耗时
|
||||
chnNumList: string[] //连线存储数据
|
||||
}
|
||||
|
||||
@@ -179,10 +181,27 @@ export namespace CheckData {
|
||||
scriptType: string
|
||||
scriptName?: string //可以不要该属性,有点多余
|
||||
code?: string
|
||||
scriptCode?: string
|
||||
// 设备
|
||||
devices: Array<DeviceCheckResult>
|
||||
}
|
||||
|
||||
export interface FormalProgressParam {
|
||||
planId: string
|
||||
devIds: string[]
|
||||
userPageId: string
|
||||
userId?: string
|
||||
formalCheckTime?: number
|
||||
}
|
||||
|
||||
export interface FormalProgressVO {
|
||||
formalCheckTime: number
|
||||
minMaxSort: number | null
|
||||
nextSort: number | null
|
||||
process: number
|
||||
tableRows: ScriptChnItem[]
|
||||
}
|
||||
|
||||
export enum ButtonColorEnum {
|
||||
INFO = '#909399',
|
||||
LOADING = 'var(--el-color-primary)',
|
||||
|
||||
91
frontend/src/api/device/device/formData.ts
Normal file
91
frontend/src/api/device/device/formData.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { Device } from '@/api/device/interface/device'
|
||||
import type { Monitor } from '@/api/device/interface/monitor'
|
||||
|
||||
const DEVICE_FIELD_NAMES = [
|
||||
'id',
|
||||
'name',
|
||||
'pattern',
|
||||
'devType',
|
||||
'manufacturer',
|
||||
'createDate',
|
||||
'createId',
|
||||
'hardwareVersion',
|
||||
'softwareVersion',
|
||||
'protocol',
|
||||
'ip',
|
||||
'port',
|
||||
'encryptionFlag',
|
||||
'series',
|
||||
'devKey',
|
||||
'sampleId',
|
||||
'arrivedDate',
|
||||
'cityName',
|
||||
'gdName',
|
||||
'subName',
|
||||
'reportPath',
|
||||
'icdId',
|
||||
'planId',
|
||||
'factorFlag',
|
||||
'preinvestmentPlan',
|
||||
'delegate',
|
||||
'inspectChannel',
|
||||
'inspectDate',
|
||||
'harmSysId',
|
||||
'importFlag'
|
||||
] as const
|
||||
|
||||
const MONITOR_FIELD_NAMES = [
|
||||
'id',
|
||||
'devId',
|
||||
'busbar',
|
||||
'name',
|
||||
'num',
|
||||
'pt',
|
||||
'ct',
|
||||
'connection',
|
||||
'statInterval',
|
||||
'harmSysId',
|
||||
'checkFlag'
|
||||
] as const
|
||||
|
||||
function appendIfPresent(formData: FormData, key: string, value: unknown) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return
|
||||
}
|
||||
formData.append(key, String(value))
|
||||
}
|
||||
|
||||
function appendMonitorFields(formData: FormData, monitorList: Monitor.ResPqMon[]) {
|
||||
monitorList.forEach((monitor, index) => {
|
||||
MONITOR_FIELD_NAMES.forEach(fieldName => {
|
||||
appendIfPresent(formData, `monitorList[${index}].${fieldName}`, monitor[fieldName])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function buildPqDevFormData(device: Device.ResPqDev, imageFile?: File | null) {
|
||||
const formData = new FormData()
|
||||
|
||||
DEVICE_FIELD_NAMES.forEach(fieldName => {
|
||||
appendIfPresent(formData, fieldName, device[fieldName])
|
||||
})
|
||||
|
||||
appendMonitorFields(formData, device.monitorList ?? [])
|
||||
|
||||
if (imageFile) {
|
||||
formData.append('image', imageFile)
|
||||
}
|
||||
|
||||
return formData
|
||||
}
|
||||
|
||||
export function buildDeviceImagePreviewSrc(
|
||||
hasImage?: boolean,
|
||||
imageBase64?: string | null,
|
||||
imageContentType?: string | null
|
||||
) {
|
||||
if (!hasImage || !imageBase64 || !imageContentType) {
|
||||
return null
|
||||
}
|
||||
return `data:${imageContentType};base64,${imageBase64}`
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {Device} from '@/api/device/interface/device'
|
||||
import http from '@/api'
|
||||
import { buildPqDevFormData } from './formData'
|
||||
|
||||
/**
|
||||
* @name 被检设备管理模块
|
||||
@@ -11,13 +12,13 @@ export const getPqDevList = (params: Device.ReqPqDevParams) => {
|
||||
}
|
||||
|
||||
//添加被检设备
|
||||
export const addPqDev = (params: Device.ResPqDev) => {
|
||||
return http.post(`/pqDev/add`, params)
|
||||
export const addPqDev = (params: Device.ResPqDev, imageFile?: File | null) => {
|
||||
return http.upload(`/pqDev/add`, buildPqDevFormData(params, imageFile))
|
||||
}
|
||||
|
||||
//编辑被检设备
|
||||
export const updatePqDev = (params: Device.ResPqDev) => {
|
||||
return http.post(`/pqDev/update`, params)
|
||||
export const updatePqDev = (params: Device.ResPqDev, imageFile?: File | null) => {
|
||||
return http.upload(`/pqDev/update`, buildPqDevFormData(params, imageFile))
|
||||
}
|
||||
|
||||
//删除被检设备
|
||||
@@ -71,3 +72,7 @@ export const getPqDev = () => {
|
||||
export const getSelectOptions = (params:{ pattern: string }) => {
|
||||
return http.get(`/pqDev/getSelectOptions`, params)
|
||||
}
|
||||
|
||||
export const downloadDeviceImage = (id: string) => {
|
||||
return http.downloadGetWithHeaders(`/pqDev/image/download?id=${id}`)
|
||||
}
|
||||
|
||||
@@ -91,6 +91,9 @@ export namespace Device {
|
||||
monitorList: Monitor.ResPqMon[]
|
||||
checked?: boolean // 是否已选择
|
||||
disabled?: boolean // 是否禁用
|
||||
hasImage?: boolean // 是否存在设备图片
|
||||
imageBase64?: string | null // 设备图片 Base64,用于详情回显
|
||||
imageContentType?: string | null // 设备图片 MIME 类型
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
|
||||
@@ -86,6 +86,7 @@ export namespace Plan {
|
||||
totalCheckCount: number;
|
||||
checkedDeviceCount: number;
|
||||
uncheckedDeviceCount: number;
|
||||
checkingDeviceCount: number;
|
||||
firstQualifiedDeviceCount: number;
|
||||
secondQualifiedDeviceCount: number;
|
||||
thirdOrMoreQualifiedDeviceCount: number;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import http from '@/api'
|
||||
import { useDetectionLockStore } from '@/stores/modules/detectionLock'
|
||||
import type { CheckData } from '@/api/check/interface'
|
||||
|
||||
|
||||
export const startPreTest = async (params) => {
|
||||
@@ -37,6 +38,14 @@ export const resumeTest = (params) => {
|
||||
return http.post(`/prepare/restartTemTest/`, params, {loading: false})
|
||||
}
|
||||
|
||||
export const saveFormalProgress = (params: CheckData.FormalProgressParam & { formalCheckTime: number }) => {
|
||||
return http.post(`/prepare/saveFormalProgress`, params, { loading: false })
|
||||
}
|
||||
|
||||
export const getFormalProgress = (params: CheckData.FormalProgressParam) => {
|
||||
return http.post<CheckData.FormalProgressVO>(`/prepare/formalProgress`, params, { loading: false })
|
||||
}
|
||||
|
||||
/**
|
||||
* 比对式通道配对
|
||||
* @param params
|
||||
|
||||
@@ -20,7 +20,10 @@ export const useCheckStore = defineStore(CHECK_STORE_KEY, {
|
||||
temperature: 0,
|
||||
humidity: 0,
|
||||
chnNumList: [] as string[], //连线数据
|
||||
nodesConnectable: true //设置是能可以连线
|
||||
nodesConnectable: true, //设置是能可以连线
|
||||
resumeFormal: false,
|
||||
restoringFormalProgress: false,
|
||||
formalProgressSnapshot: null as CheckData.FormalProgressVO | null
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
@@ -36,6 +39,7 @@ export const useCheckStore = defineStore(CHECK_STORE_KEY, {
|
||||
initSelectTestItems() {
|
||||
const appSceneStore = useAppSceneStore()
|
||||
this.selectTestItems.preTest = true
|
||||
this.selectTestItems.timeTest = false
|
||||
if (appSceneStore.currentScene === '1') {
|
||||
this.selectTestItems.channelsTest = true
|
||||
} else {
|
||||
@@ -66,6 +70,15 @@ export const useCheckStore = defineStore(CHECK_STORE_KEY, {
|
||||
},
|
||||
setNodesConnectable(nodesConnectable: boolean) {
|
||||
this.nodesConnectable = nodesConnectable
|
||||
},
|
||||
setResumeFormal(resumeFormal: boolean) {
|
||||
this.resumeFormal = resumeFormal
|
||||
},
|
||||
setRestoringFormalProgress(restoring: boolean) {
|
||||
this.restoringFormalProgress = restoring
|
||||
},
|
||||
setFormalProgressSnapshot(snapshot: CheckData.FormalProgressVO | null) {
|
||||
this.formalProgressSnapshot = snapshot
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -355,7 +355,7 @@ const handleSubmitAgain = async () => {
|
||||
devIds: devIds.value,
|
||||
standardDevIds: standardDevIds.value,
|
||||
pairs: pairs.value,
|
||||
testItemList: [checkStore.selectTestItems.preTest, false, checkStore.selectTestItems.test],
|
||||
testItemList: [checkStore.selectTestItems.preTest, false, false, checkStore.selectTestItems.test],
|
||||
userId: userStore.userInfo.id,
|
||||
phaseCheck: xiangXuIsShow.value ? 1 : 0,
|
||||
})
|
||||
@@ -423,7 +423,7 @@ const handleSubmitFast = async () => {
|
||||
devIds: devIds.value,
|
||||
standardDevIds: standardDevIds.value,
|
||||
pairs: pairs.value,
|
||||
testItemList: [checkStore.selectTestItems.preTest, false, checkStore.selectTestItems.test],
|
||||
testItemList: [checkStore.selectTestItems.preTest, false, false, checkStore.selectTestItems.test],
|
||||
|
||||
userId: userStore.userInfo.id,
|
||||
phaseCheck: xiangXuIsShow.value ? 1 : 0,
|
||||
@@ -448,7 +448,7 @@ const handleSubmitFast = async () => {
|
||||
devIds: devIds.value,
|
||||
standardDevIds: standardDevIds.value,
|
||||
pairs: pairs.value,
|
||||
testItemList: [checkStore.selectTestItems.preTest, false, checkStore.selectTestItems.test],
|
||||
testItemList: [checkStore.selectTestItems.preTest, false, false, checkStore.selectTestItems.test],
|
||||
|
||||
userId: userStore.userInfo.id,
|
||||
phaseCheck: xiangXuIsShow.value ? 1 : 0,
|
||||
@@ -571,7 +571,8 @@ const sendReCheck = () => {
|
||||
humidity: checkStore.humidity,
|
||||
testItemList: [
|
||||
checkStore.selectTestItems.preTest,
|
||||
checkStore.selectTestItems.channelsTest,
|
||||
false,
|
||||
false,
|
||||
checkStore.selectTestItems.test
|
||||
]
|
||||
}).then(res => {
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
<template>
|
||||
<el-dialog title="选择测试内容" v-model='dialogVisible' @close="handleClose" v-bind="dialogSmall" >
|
||||
<div>
|
||||
<el-form ref="dialogFormRef" :model="formContent" class="form-three">
|
||||
<el-form-item prop="preTest" :label-width="100">
|
||||
<el-checkbox v-model="formContent.preTest" label="预检测"/>
|
||||
<el-form ref="dialogFormRef" :model="formContent" class="form-three select-test-form">
|
||||
<el-form-item prop="preTest">
|
||||
<div class="test-item-checkbox">
|
||||
<span>预检测</span>
|
||||
<el-checkbox v-model="formContent.preTest"/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="checkStore.plan.timeCheck===1" prop="timeTest" :label-width="100">
|
||||
<el-checkbox v-model="formContent.timeTest" label="守时检测"/>
|
||||
<el-form-item v-if="checkStore.plan.timeCheck===1" prop="timeTest">
|
||||
<div class="test-item-checkbox">
|
||||
<span>守时检测</span>
|
||||
<el-checkbox v-model="formContent.timeTest"/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="channelsTestShow" prop="channelsTest" :label-width="100">
|
||||
<el-checkbox v-model="formContent.channelsTest" label="系数校准"/>
|
||||
<el-form-item v-if="channelsTestShow" prop="channelsTest">
|
||||
<div class="test-item-checkbox">
|
||||
<span>系数校准</span>
|
||||
<el-checkbox v-model="formContent.channelsTest"/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item prop="test" :label-width="100">
|
||||
<el-checkbox v-model="formContent.test" label="正式检测"/>
|
||||
<el-form-item prop="test">
|
||||
<div class="test-item-checkbox">
|
||||
<span>正式检测</span>
|
||||
<el-checkbox v-model="formContent.test"/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -123,5 +135,30 @@ defineExpose({open})
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.test-item-checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 92px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.select-test-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 120px);
|
||||
justify-content: center;
|
||||
column-gap: 70px;
|
||||
row-gap: 22px;
|
||||
padding: 12px 0 18px;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
width: auto !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
flex: none;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -483,7 +483,11 @@ const columns = reactive<ColumnProps<Device.ResPqDev>[]>([
|
||||
sortable: true,
|
||||
isShow: checkStateShow,
|
||||
render: scope => {
|
||||
return scope.row.checkState === 0 ? '未检' : scope.row.checkState === 1 ? '检测中' : scope.row.checkState === 2 ? '检测完成':'归档'
|
||||
return scope.row.checkState === 0 ? '未检'
|
||||
: scope.row.checkState === 1 ? '检测中'
|
||||
: scope.row.checkState === 2 ? '检测完成'
|
||||
: scope.row.checkState === 3 ? '归档'
|
||||
: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -586,7 +590,9 @@ const handleSelectionChange = (selection: any[]) => {
|
||||
devVolt: item.devVolt,
|
||||
devCurr: item.devCurr,
|
||||
factorFlag: item.factorFlag,
|
||||
checkResult: item.checkResult
|
||||
checkResult: item.checkResult,
|
||||
checkState: item.checkState,
|
||||
formalCheckTime: item.formalCheckTime
|
||||
}
|
||||
})
|
||||
|
||||
@@ -600,14 +606,16 @@ const handleSelectionChange = (selection: any[]) => {
|
||||
|
||||
const isUncheckedDevice = (device: Device.ResPqDev) => Number(device.checkState) === 0 || Number(device.checkResult) === 2
|
||||
|
||||
const hasCheckedSelectedDevice = () => channelsSelection.value.some(device => !isUncheckedDevice(device))
|
||||
|
||||
const hasUncheckedSelectedDevice = () => channelsSelection.value.some(device => isUncheckedDevice(device))
|
||||
|
||||
const hasCheckedUnqualifiedSelectedDevice = () =>
|
||||
channelsSelection.value.some(device => !isUncheckedDevice(device) && Number(device.checkResult) === 0)
|
||||
|
||||
const shouldShowRecheckModeDialog = () => hasCheckedSelectedDevice()
|
||||
const isCheckedUnqualifiedDevice = (device: Device.ResPqDev) =>
|
||||
Number(device.checkState) === 2 && Number(device.checkResult) === 0
|
||||
|
||||
const shouldShowRecheckModeDialog = () =>
|
||||
channelsSelection.value.length > 0 && channelsSelection.value.every(isCheckedUnqualifiedDevice)
|
||||
|
||||
const canUseUnqualifiedItemRecheck = () => hasCheckedUnqualifiedSelectedDevice() && !hasUncheckedSelectedDevice()
|
||||
|
||||
@@ -653,7 +661,7 @@ function tableHeaderInit(val: number) {
|
||||
switch (val) {
|
||||
case 0: // 设备检测模式
|
||||
case 1: // 手动检测模式
|
||||
checkStateTable.value = [0, 1, 2] // 显示所有检测状态
|
||||
checkStateTable.value = [0, 1, 2] // 显示未检、检测中、检测完成状态
|
||||
tableKey.value++
|
||||
operationShow.value = false // 隐藏操作列
|
||||
documentStateShow.value = false // 隐藏文档状态
|
||||
@@ -665,7 +673,7 @@ function tableHeaderInit(val: number) {
|
||||
break
|
||||
case 3: // 报告生成模式
|
||||
if (modeStore.currentMode === '比对式') {
|
||||
checkStateTable.value = [2, 3] // 显示检测中,检测完成和归档状态
|
||||
checkStateTable.value = [2, 3] // 显示检测完成和归档状态
|
||||
} else {
|
||||
checkStateTable.value = [2, 3] // 显示检测完成和归档状态
|
||||
}
|
||||
@@ -689,7 +697,7 @@ function tableHeaderInit(val: number) {
|
||||
break
|
||||
case 5: // 数据查询模式
|
||||
if (modeStore.currentMode === '比对式') {
|
||||
checkStateTable.value = [1, 2, 3] // 显示检测中,检测完成和归档状态
|
||||
checkStateTable.value = [1, 2, 3] // 显示检测中、检测完成和归档状态
|
||||
} else {
|
||||
checkStateTable.value = [2, 3] // 显示检测完成和归档状态
|
||||
}
|
||||
@@ -914,6 +922,14 @@ const handleTest = async (val: string) => {
|
||||
|
||||
// 处理检测相关操作
|
||||
if (val === '手动检测' || val === '一键检测' || val === '系数校准') {
|
||||
const hasCheckingDevice = channelsSelection.value.some(item => Number(item.checkState) === 1)
|
||||
const hasNonCheckingDevice = channelsSelection.value.some(item => Number(item.checkState) !== 1)
|
||||
if (hasCheckingDevice && hasNonCheckingDevice) {
|
||||
ElMessage.warning('检测中的设备不能和未检、检测完成的设备同时检测')
|
||||
return
|
||||
}
|
||||
checkStore.setResumeFormal(false)
|
||||
|
||||
// 检查选中设备的检测状态是否一致
|
||||
const checkStates = channelsSelection.value.map(item => item.checkState)
|
||||
const allCheckStatesEqual = new Set(checkStates).size <= 1
|
||||
|
||||
@@ -105,7 +105,7 @@ import { InfoFilled, Loading } from '@element-plus/icons-vue'
|
||||
// 单通道单测试项详情弹窗组件
|
||||
import dataCheckSingleChannelSingleTestPopup from './dataCheckSingleChannelSingleTestPopup.vue'
|
||||
// Vue 3 Composition API
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, toRef, watch } from 'vue'
|
||||
import { computed, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref, toRef, watch } from 'vue'
|
||||
// 对话框大小绑定工具
|
||||
import { dialogBig } from '@/utils/elementBind'
|
||||
// 检测数据类型定义
|
||||
@@ -151,7 +151,14 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
// 定义向父组件发送的事件
|
||||
const emit = defineEmits(['update:testStatus', 'update:webMsgSend', 'sendPause', 'sendResume', 'closeWebSocket'])
|
||||
const emit = defineEmits([
|
||||
'update:testStatus',
|
||||
'update:webMsgSend',
|
||||
'sendPause',
|
||||
'sendResume',
|
||||
'closeWebSocket',
|
||||
'restoreFormalProgressRequired'
|
||||
])
|
||||
|
||||
// ========== 界面状态相关 ==========
|
||||
// 测试项进度抽屉是否打开
|
||||
@@ -284,13 +291,6 @@ watch(testStatus, function(newValue, oldValue) {
|
||||
// 直接进入检测流程
|
||||
emit('update:testStatus', 'process')
|
||||
}
|
||||
// 开始计时
|
||||
startTimeCount()
|
||||
// 显示测试日志抽屉
|
||||
showTestLog()
|
||||
// 重置时间记录
|
||||
startData.value = new Date()
|
||||
timeDifference.value = 0
|
||||
}
|
||||
|
||||
// 需要停止计时的状态
|
||||
@@ -552,14 +552,8 @@ watch(webMsgSend, function(newValue, oldValue) {
|
||||
switch (newValue.operateCode) {
|
||||
case 'VERIFY_MAPPING$01':
|
||||
if (newValue.code == 25001) {
|
||||
ElMessage.success('初始化成功!')
|
||||
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}:协议校验成功!` })
|
||||
timeDifference.value = +new Date().getTime() - startData.value.getTime()
|
||||
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}:初始化成功!` })
|
||||
percentage.value = 3
|
||||
// 初始化完成,准备开始正式检测
|
||||
activeIndex = getNextActiveIndex() + 2
|
||||
emit('update:testStatus', 'process')
|
||||
} else if (newValue.code == 25002) {
|
||||
let data = JSON.parse(newValue.data)
|
||||
ElMessageBox.alert(`脚本与icd校验失败!icd名称:${data['icdType']} -> 校验项:${data['dataType']}`, '初始化失败', {
|
||||
@@ -585,6 +579,62 @@ watch(webMsgSend, function(newValue, oldValue) {
|
||||
break
|
||||
}
|
||||
break
|
||||
case 'YJC_xujy':
|
||||
switch (newValue.operateCode) {
|
||||
case 'OPER_GATHER':
|
||||
if (newValue.code == 10200) {
|
||||
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}:相序校验源参数下发成功!` })
|
||||
} else if (newValue.code == 10201) {
|
||||
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}:相序校验源参数下发中...` })
|
||||
} else if (newValue.code == 10520) {
|
||||
ElMessageBox.alert('相序校验报文解析异常!', '初始化失败', {
|
||||
confirmButtonText: '确定',
|
||||
type: 'error',
|
||||
})
|
||||
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}:相序校验报文解析异常!` })
|
||||
emit('update:testStatus', 'test_init_fail')
|
||||
} else if (newValue.code == -1) {
|
||||
ElMessageBox.alert('相序校验源未知异常!', '初始化失败', {
|
||||
confirmButtonText: '确定',
|
||||
type: 'error',
|
||||
})
|
||||
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}:相序校验源未知异常!` })
|
||||
emit('update:testStatus', 'test_init_fail')
|
||||
}
|
||||
break
|
||||
case 'DATA_REQUEST$02':
|
||||
if (newValue.code == 10200) {
|
||||
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}:${newValue.data}` })
|
||||
} else if (newValue.code == 10201) {
|
||||
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}:获取相序校验数据中...` })
|
||||
} else if (newValue.code == 25003) {
|
||||
ElMessageBox.alert('相序校验未通过!', '初始化失败', {
|
||||
confirmButtonText: '确定',
|
||||
type: 'error',
|
||||
})
|
||||
testLogList.push({ type: 'error', log: `${new Date().toLocaleString()}:相序校验未通过!` })
|
||||
emit('update:testStatus', 'test_init_fail')
|
||||
} else if (newValue.code == 25001) {
|
||||
ElMessage.success('初始化成功!')
|
||||
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}:相序校验成功!` })
|
||||
timeDifference.value = +new Date().getTime() - startData.value.getTime()
|
||||
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}:初始化成功!` })
|
||||
percentage.value = 4
|
||||
// 初始化完成,准备开始正式检测
|
||||
activeIndex = getNextActiveIndex() + 2
|
||||
emit('update:testStatus', 'process')
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
case 'formal_progress_required':
|
||||
testLogList.push({ type: 'info', log: `${new Date().toLocaleString()}:恢复进度中` })
|
||||
emit('restoreFormalProgressRequired')
|
||||
break
|
||||
case 'formal_progress_no_next':
|
||||
ElMessage.warning('当前暂存进度没有可续检脚本,请重新开始检测')
|
||||
emit('update:testStatus', 'error')
|
||||
break
|
||||
// 暂停测试成功
|
||||
case 'preStopTest':
|
||||
if (newValue.operateCode == 'stop') {
|
||||
@@ -679,6 +729,12 @@ watch(webMsgSend, function(newValue, oldValue) {
|
||||
case 'F_End':
|
||||
handleEndItem('F', newValue.desc, newValue.data)
|
||||
break
|
||||
case 'Angle_Start':
|
||||
handleStartItem('Angle', newValue.desc)
|
||||
break
|
||||
case 'Angle_End':
|
||||
handleEndItem('Angle', newValue.desc, newValue.data)
|
||||
break
|
||||
// 检测结束
|
||||
case 'Quit':
|
||||
|
||||
@@ -692,6 +748,9 @@ watch(webMsgSend, function(newValue, oldValue) {
|
||||
// ========== 检测项处理函数 ==========
|
||||
// 处理检测项开始消息
|
||||
const handleStartItem = (code: string, desc: string | undefined) => {
|
||||
if (!timer) {
|
||||
startTimeCount()
|
||||
}
|
||||
if (desc === undefined) {
|
||||
// 大测试项开始
|
||||
activeIndex = getActiveIndex(code)
|
||||
@@ -956,7 +1015,7 @@ const updateLog = (isStart: boolean) => {
|
||||
}
|
||||
|
||||
// 更新总时间显示
|
||||
timeView.value = secondToTime(timeDifference.value / 1000)
|
||||
// timeView is driven by timeCount so resumed formal tests keep base + current elapsed time.
|
||||
// 如果是最后一个测试项,记录总结日志
|
||||
if (activeIndex === checkTotal) {
|
||||
testLogList.push({
|
||||
@@ -1202,6 +1261,13 @@ const startTimeCount = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const startFormalTimer = () => {
|
||||
startTimeCount()
|
||||
showTestLog()
|
||||
startData.value = new Date()
|
||||
timeDifference.value = 0
|
||||
}
|
||||
|
||||
// 停止计时
|
||||
const stopTimeCount = () => {
|
||||
if (timer) {
|
||||
@@ -1237,6 +1303,36 @@ const secondToTime = (second: number) => {
|
||||
return h + ':' + m + ':' + s
|
||||
}
|
||||
|
||||
const getElapsedSeconds = () => {
|
||||
return timeCount.value
|
||||
}
|
||||
|
||||
const applyFormalProgress = (snapshot: CheckData.FormalProgressVO) => {
|
||||
stopTimeCount()
|
||||
timeCount.value = snapshot.formalCheckTime ?? 0
|
||||
timeView.value = secondToTime(timeCount.value)
|
||||
timeDifference.value = timeCount.value * 1000
|
||||
percentage.value = Math.trunc(snapshot.process ?? 0)
|
||||
|
||||
checkResult.splice(0)
|
||||
snapshot.tableRows.forEach(row => {
|
||||
const scriptItem = scriptData.find(item => item.code === row.code || item.code === row.scriptCode || item.id === row.scriptType)
|
||||
checkResult.push({
|
||||
scriptType: scriptItem?.id ?? row.scriptType,
|
||||
scriptName: scriptItem?.scriptName ?? row.scriptName,
|
||||
code: row.code ?? row.scriptCode ?? scriptItem?.code,
|
||||
devices: row.devices.map(device => ({
|
||||
deviceId: device.deviceId,
|
||||
deviceName: device.deviceName,
|
||||
chnResult: [...device.chnResult]
|
||||
}))
|
||||
})
|
||||
})
|
||||
activeIndex = snapshot.tableRows.filter(row =>
|
||||
row.devices.every(device => device.chnResult.every(result => result !== CheckData.ChnCheckResultEnum.UNKNOWN))
|
||||
).length
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
mittBus.on(STOP_DETECTION_TIMER_EVENT, handleStopDetectionTimer)
|
||||
})
|
||||
@@ -1267,6 +1363,9 @@ onBeforeUnmount(() => {
|
||||
// 暴露给父组件的方法
|
||||
defineExpose({
|
||||
handlePause, // 暂停方法
|
||||
getElapsedSeconds,
|
||||
applyFormalProgress,
|
||||
startFormalTimer
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
width="1550px"
|
||||
:model-value="dialogVisible"
|
||||
:before-close="beforeClose"
|
||||
:close-on-click-modal="!isRestoringFormalProgress"
|
||||
:close-on-press-escape="!isRestoringFormalProgress"
|
||||
:show-close="!isRestoringFormalProgress"
|
||||
@close="handleClose"
|
||||
height="1000px"
|
||||
draggable
|
||||
@@ -28,18 +31,18 @@
|
||||
:icon="stepsActive > 1 || stepsActiveIndex > stepsTotalNum - 1 ? SuccessFilled : Edit"
|
||||
@click="handleStepClick(1)"
|
||||
/>
|
||||
<!-- 守时检测步骤 -->
|
||||
<el-step
|
||||
v-if="timeTestSelected"
|
||||
title="守时检测"
|
||||
:icon="stepsActive > 2 || stepsActiveIndex > stepsTotalNum - 1 ? SuccessFilled : UploadFilled"
|
||||
@click="handleStepClick(2)"
|
||||
/>
|
||||
<!-- 系数校准步骤 -->
|
||||
<el-step
|
||||
v-if="channelsTestSelected"
|
||||
title="系数校准"
|
||||
:icon="stepsActive > 3 || stepsActiveIndex > stepsTotalNum - 1 ? SuccessFilled : Odometer"
|
||||
:icon="stepsActive > 2 || stepsActiveIndex > stepsTotalNum - 1 ? SuccessFilled : Odometer"
|
||||
@click="handleStepClick(2)"
|
||||
/>
|
||||
<!-- 守时检测步骤 -->
|
||||
<el-step
|
||||
v-if="timeTestSelected"
|
||||
title="守时检测"
|
||||
:icon="stepsActive > 3 || stepsActiveIndex > stepsTotalNum - 1 ? SuccessFilled : UploadFilled"
|
||||
@click="handleStepClick(3)"
|
||||
/>
|
||||
<!-- 正式检测步骤 -->
|
||||
@@ -66,13 +69,13 @@
|
||||
<!-- 守时检测组件 -->
|
||||
<timeTest
|
||||
v-if="showComponent && timeTestSelected"
|
||||
v-show="timeTestSelected && stepsActiveView == 2"
|
||||
v-show="timeTestSelected && stepsActiveView == 3"
|
||||
v-model:testStatus="timeTestStatus"
|
||||
/>
|
||||
<!-- 系数校准检测组件 -->
|
||||
<factorTest
|
||||
v-if="showComponent && channelsTestSelected"
|
||||
v-show="channelsTestSelected && stepsActiveView == 3"
|
||||
v-show="channelsTestSelected && stepsActiveView == 2"
|
||||
v-model:testStatus="channelsTestStatus"
|
||||
:webMsgSend="webMsgSend"
|
||||
/>
|
||||
@@ -87,6 +90,7 @@
|
||||
@sendPause="sendPause"
|
||||
@sendResume="sendResume"
|
||||
@closeWebSocket="closeWebSocket"
|
||||
@restoreFormalProgressRequired="handleRestoreFormalProgressRequired"
|
||||
:stepsActive="stepsActive"
|
||||
/>
|
||||
|
||||
@@ -94,10 +98,17 @@
|
||||
<template #footer>
|
||||
<div>
|
||||
<!-- 开始检测按钮 - 当前状态为等待时显示 -->
|
||||
<el-button type="primary" :icon="VideoPlay" v-if="ActiveStatue === 'waiting'" @click="handleSubmitFast">
|
||||
<el-button type="primary" :icon="VideoPlay" v-if="ActiveStatue === 'waiting'" :disabled="isRestoringFormalProgress" @click="handleSubmitFast">
|
||||
开始检测
|
||||
</el-button>
|
||||
|
||||
<el-button type="primary" v-if="isRestoringFormalProgress" disabled>
|
||||
<el-icon class="loading-box" style="color: #fff; margin-right: 8px">
|
||||
<component :is="Refresh" />
|
||||
</el-icon>
|
||||
恢复进度中
|
||||
</el-button>
|
||||
|
||||
<!-- 初始化中按钮 - 禁用状态,显示加载动画 -->
|
||||
<el-button type="primary" v-if="TestStatus === 'test_init'" disabled>
|
||||
<el-icon class="loading-box" style="color: #fff; margin-right: 8px">
|
||||
@@ -107,7 +118,7 @@
|
||||
</el-button>
|
||||
|
||||
<!-- 停止检测按钮 - 检测进行中时显示 -->
|
||||
<el-button type="primary" v-if="TestStatus == 'process'" :icon="VideoPause" @click="handlePause()">
|
||||
<el-button type="primary" v-if="TestStatus == 'process'" :icon="VideoPause" :disabled="isRestoringFormalProgress" @click="handlePause()">
|
||||
停止检测
|
||||
</el-button>
|
||||
|
||||
@@ -120,10 +131,20 @@
|
||||
</el-button>
|
||||
|
||||
<!-- 继续检测按钮 - 检测已暂停时显示 -->
|
||||
<el-button type="warning" v-if="TestStatus == 'paused'" :icon="VideoPlay" @click="sendResume">
|
||||
<el-button type="warning" v-if="TestStatus == 'paused'" :icon="VideoPlay" :disabled="isRestoringFormalProgress" @click="sendResume">
|
||||
继续检测
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
v-if="TestStatus == 'paused'"
|
||||
:icon="DocumentChecked"
|
||||
:disabled="isRestoringFormalProgress"
|
||||
@click="handleSaveFormalProgress"
|
||||
>
|
||||
暂存并退出
|
||||
</el-button>
|
||||
|
||||
<!-- 下一步/完成/错误状态按钮 -->
|
||||
<el-button
|
||||
:type="ActiveStatue === 'success' ? 'primary' : 'danger'"
|
||||
@@ -136,13 +157,14 @@
|
||||
ActiveStatue === 'pause_timeout'||
|
||||
ActiveStatue === 'test_init_fail')
|
||||
"
|
||||
:disabled="isRestoringFormalProgress"
|
||||
@click="nextStep"
|
||||
>
|
||||
{{ nextStepText }}
|
||||
</el-button>
|
||||
|
||||
<!-- 退出检测按钮 - 默认显示 -->
|
||||
<el-button type="primary" @click="handleQuit" v-else>退出检测</el-button>
|
||||
<el-button type="primary" :disabled="isRestoringFormalProgress" @click="handleQuit" v-else>退出检测</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -154,6 +176,7 @@ import { onBeforeUnmount, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Coin,
|
||||
DocumentChecked,
|
||||
Edit,
|
||||
Key,
|
||||
Odometer,
|
||||
@@ -170,14 +193,17 @@ import factorTest from './factorTest.vue'
|
||||
import test from './test.vue'
|
||||
import socketClient from '@/utils/webSocketClient'
|
||||
import { useCheckStore } from '@/stores/modules/check'
|
||||
import { pauseTest, resumeTest, startPreTest } from '@/api/socket/socket'
|
||||
import { useDetectionLockStore } from '@/stores/modules/detectionLock'
|
||||
import { getFormalProgress, pauseTest, resumeTest, saveFormalProgress, startPreTest } from '@/api/socket/socket'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import { JwtUtil } from '@/utils/jwtUtil'
|
||||
import mittBus, { STOP_DETECTION_TIMER_EVENT } from '@/utils/mittBus'
|
||||
import type { CheckData } from '@/api/check/interface'
|
||||
|
||||
// ====================== 状态管理 ======================
|
||||
const userStore = useUserStore()
|
||||
const checkStore = useCheckStore()
|
||||
const detectionLockStore = useDetectionLockStore()
|
||||
|
||||
// ====================== 基础状态变量 ======================
|
||||
const nextStepText = ref('下一步') // 下一步按钮文本
|
||||
@@ -185,7 +211,13 @@ const dialogVisible = ref(false) // 弹窗显示状态
|
||||
const dialogTitle = ref('') // 弹窗标题
|
||||
const showComponent = ref(true) // 是否显示检测组件
|
||||
const preTestRef = ref<{ initializeParameters: () => void } | null>(null) // 预检测组件引用
|
||||
const testRef = ref<{ handlePause: () => void } | null>(null) // 正式检测组件引用
|
||||
const testRef = ref<{
|
||||
handlePause: () => void
|
||||
getElapsedSeconds: () => number
|
||||
applyFormalProgress: (snapshot: CheckData.FormalProgressVO) => void
|
||||
startFormalTimer: () => void
|
||||
} | null>(null) // 正式检测组件引用
|
||||
const isRestoringFormalProgress = ref(false)
|
||||
|
||||
// ====================== 步骤控制相关变量 ======================
|
||||
const showSteps = ref(false) // 是否显示步骤条
|
||||
@@ -255,20 +287,20 @@ const initOperate = () => {
|
||||
}
|
||||
stepsTotalNum.value = count + 1
|
||||
|
||||
// 根据选中的检测项,设置初始显示的步骤(按优先级:预检测 > 守时检测 > 系数校准 > 正式检测)
|
||||
// 根据选中的检测项,设置初始显示的步骤(按优先级:预检测 > 系数校准 > 守时检测 > 正式检测)
|
||||
if (preTestSelected.value) {
|
||||
stepsActiveView.value = 1 // 显示预检测组件
|
||||
stepsActive.value = 1 // 业务逻辑设为预检测
|
||||
return
|
||||
}
|
||||
if (timeTestSelected.value) {
|
||||
stepsActiveView.value = 2 // 显示守时检测组件
|
||||
stepsActive.value = 2 // 业务逻辑设为守时检测
|
||||
if (channelsTestSelected.value) {
|
||||
stepsActiveView.value = 2 // 显示系数校准组件
|
||||
stepsActive.value = 2 // 业务逻辑设为系数校准
|
||||
return
|
||||
}
|
||||
if (channelsTestSelected.value) {
|
||||
stepsActiveView.value = 3 // 显示系数校准组件
|
||||
stepsActive.value = 3 // 业务逻辑设为系数校准
|
||||
if (timeTestSelected.value) {
|
||||
stepsActiveView.value = 3 // 显示守时检测组件
|
||||
stepsActive.value = 3 // 业务逻辑设为守时检测
|
||||
return
|
||||
}
|
||||
if (testSelected.value) {
|
||||
@@ -354,8 +386,10 @@ const handleSubmitFast = () => {
|
||||
testItemList: [
|
||||
checkStore.selectTestItems.preTest,
|
||||
checkStore.selectTestItems.channelsTest,
|
||||
checkStore.selectTestItems.timeTest,
|
||||
checkStore.selectTestItems.test
|
||||
]
|
||||
],
|
||||
resumeFormal: checkStore.resumeFormal
|
||||
}).then(res => {
|
||||
if (res.code !== 'A0000') {
|
||||
ElMessageBox.alert('预检测失败', '检测失败', {
|
||||
@@ -369,10 +403,7 @@ const handleSubmitFast = () => {
|
||||
}
|
||||
}
|
||||
break
|
||||
case 2: // 守时检测步骤
|
||||
timeTestStatus.value = 'start'
|
||||
break
|
||||
case 3: // 系数校准步骤
|
||||
case 2: // 系数校准步骤
|
||||
if (channelsTestStatus.value == 'waiting') {
|
||||
// 如果没有预检测且有系数校准,需要先进行初始化
|
||||
if (!checkStore.selectTestItems.preTest && checkStore.selectTestItems.channelsTest) {
|
||||
@@ -387,8 +418,10 @@ const handleSubmitFast = () => {
|
||||
testItemList: [
|
||||
checkStore.selectTestItems.preTest,
|
||||
checkStore.selectTestItems.channelsTest,
|
||||
checkStore.selectTestItems.timeTest,
|
||||
checkStore.selectTestItems.test
|
||||
]
|
||||
],
|
||||
resumeFormal: checkStore.resumeFormal
|
||||
}).then(res => {
|
||||
if (res.code !== 'A0000') {
|
||||
ElMessageBox.alert('系数校准失败', '检测失败', {
|
||||
@@ -402,13 +435,12 @@ const handleSubmitFast = () => {
|
||||
channelsTestStatus.value = 'start'
|
||||
}
|
||||
break
|
||||
case 4: // 正式检测步骤
|
||||
if (TestStatus.value == 'waiting') {
|
||||
// 如果没有预检测和系数校准,直接进行正式检测需要先初始化
|
||||
case 3: // 守时检测步骤
|
||||
if (timeTestStatus.value == 'waiting') {
|
||||
if (
|
||||
!checkStore.selectTestItems.preTest &&
|
||||
!checkStore.selectTestItems.channelsTest &&
|
||||
checkStore.selectTestItems.test
|
||||
checkStore.selectTestItems.timeTest
|
||||
) {
|
||||
startPreTest({
|
||||
userPageId: JwtUtil.getLoginName(),
|
||||
@@ -421,19 +453,64 @@ const handleSubmitFast = () => {
|
||||
testItemList: [
|
||||
checkStore.selectTestItems.preTest,
|
||||
checkStore.selectTestItems.channelsTest,
|
||||
checkStore.selectTestItems.timeTest,
|
||||
checkStore.selectTestItems.test
|
||||
]
|
||||
],
|
||||
resumeFormal: checkStore.resumeFormal
|
||||
}).then(res => {
|
||||
if (res.code !== 'A0000') {
|
||||
ElMessageBox.alert('正式检测失败', '检测失败', {
|
||||
ElMessageBox.alert('守时检测失败', '检测失败', {
|
||||
confirmButtonText: '确定',
|
||||
type: 'error'
|
||||
})
|
||||
TestStatus.value = 'error'
|
||||
timeTestStatus.value = 'error'
|
||||
}
|
||||
})
|
||||
}
|
||||
timeTestStatus.value = 'start'
|
||||
}
|
||||
break
|
||||
case 4: // 正式检测步骤
|
||||
if (TestStatus.value == 'waiting') {
|
||||
// 如果没有预检测、系数校准和守时检测,直接进行正式检测需要先初始化
|
||||
const needStartPreTest =
|
||||
!checkStore.selectTestItems.preTest &&
|
||||
!checkStore.selectTestItems.channelsTest &&
|
||||
!checkStore.selectTestItems.timeTest &&
|
||||
checkStore.selectTestItems.test
|
||||
TestStatus.value = 'start'
|
||||
if (needStartPreTest) {
|
||||
startPreTest({
|
||||
userPageId: JwtUtil.getLoginName(),
|
||||
devIds: deviceIds,
|
||||
planId: planId,
|
||||
reCheckType: checkStore.reCheckType == 1 ? '1' : '2',
|
||||
userId: userStore.userInfo.id,
|
||||
temperature: checkStore.temperature,
|
||||
humidity: checkStore.humidity,
|
||||
testItemList: [
|
||||
checkStore.selectTestItems.preTest,
|
||||
checkStore.selectTestItems.channelsTest,
|
||||
checkStore.selectTestItems.timeTest,
|
||||
checkStore.selectTestItems.test
|
||||
],
|
||||
resumeFormal: checkStore.resumeFormal
|
||||
}).then(res => {
|
||||
if (res.code !== 'A0000') {
|
||||
ElMessageBox.alert(res.message || '正式检测失败', '检测失败', {
|
||||
confirmButtonText: '确定',
|
||||
type: 'error'
|
||||
})
|
||||
TestStatus.value = 'test_init_fail'
|
||||
return
|
||||
}
|
||||
testRef.value?.startFormalTimer()
|
||||
}).catch(() => {
|
||||
TestStatus.value = 'test_init_fail'
|
||||
})
|
||||
} else {
|
||||
testRef.value?.startFormalTimer()
|
||||
}
|
||||
} else if (TestStatus.value == 'paused') {
|
||||
// 如果是暂停状态,发送继续指令
|
||||
sendResume()
|
||||
@@ -566,13 +643,14 @@ const sendPause = () => {
|
||||
/**
|
||||
* 发送继续检测指令
|
||||
*/
|
||||
const sendResume = () => {
|
||||
const sendResume = async () => {
|
||||
|
||||
// 调用继续检测API
|
||||
resumeTest({
|
||||
const res: any = await resumeTest({
|
||||
userPageId: JwtUtil.getLoginName(),
|
||||
devIds: checkStore.devices.map(item => item.deviceId),
|
||||
planId: checkStore.plan.id,
|
||||
sourceId: getCurrentSourceId(),
|
||||
reCheckType: '2', // 操作类型:0-系数校验,1-预检测,2-正式检测,8-不合格项复检
|
||||
userId: userStore.userInfo.id,
|
||||
temperature: checkStore.temperature,
|
||||
@@ -580,11 +658,81 @@ const sendResume = () => {
|
||||
})
|
||||
|
||||
// 发送继续成功消息给子组件
|
||||
if (res.code !== 'A0000') {
|
||||
ElMessage.error(res.message || '继续检测失败')
|
||||
TestStatus.value = 'paused'
|
||||
return
|
||||
}
|
||||
|
||||
Object.assign(webMsgSend.value, {
|
||||
requestId: 'Resume_Success'
|
||||
})
|
||||
}
|
||||
|
||||
const getCurrentSourceId = () => {
|
||||
const plan = checkStore.plan as any
|
||||
if (plan.sourceId) {
|
||||
return plan.sourceId
|
||||
}
|
||||
if (Array.isArray(plan.sourceIds)) {
|
||||
return plan.sourceIds[0]
|
||||
}
|
||||
if (typeof plan.sourceIds === 'string') {
|
||||
return plan.sourceIds.split(',').filter(Boolean)[0]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const buildFormalProgressParam = () => ({
|
||||
userPageId: JwtUtil.getLoginName(),
|
||||
devIds: checkStore.devices.map(item => item.deviceId),
|
||||
planId: checkStore.plan.id,
|
||||
userId: userStore.userInfo.id,
|
||||
formalCheckTime: testRef.value?.getElapsedSeconds() ?? 0
|
||||
})
|
||||
|
||||
const sleep = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms))
|
||||
|
||||
const handleSaveFormalProgress = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('是否保存当前正式检测进度并退出?', '提示', {
|
||||
confirmButtonText: '是',
|
||||
cancelButtonText: '否',
|
||||
type: 'warning'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const res: any = await saveFormalProgress(buildFormalProgressParam())
|
||||
if (res.code !== 'A0000') {
|
||||
ElMessage.error('暂存进度失败')
|
||||
return
|
||||
}
|
||||
ElMessage.success('暂存进度成功')
|
||||
detectionLockStore.clearHolder()
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const handleRestoreFormalProgressRequired = async () => {
|
||||
isRestoringFormalProgress.value = true
|
||||
checkStore.setRestoringFormalProgress(true)
|
||||
try {
|
||||
const res: any = await getFormalProgress(buildFormalProgressParam())
|
||||
if (res.code !== 'A0000') {
|
||||
ElMessage.error('恢复进度失败')
|
||||
TestStatus.value = 'error'
|
||||
return
|
||||
}
|
||||
testRef.value?.applyFormalProgress(res.data)
|
||||
checkStore.setFormalProgressSnapshot(res.data)
|
||||
await sleep(350)
|
||||
} finally {
|
||||
isRestoringFormalProgress.value = false
|
||||
checkStore.setRestoringFormalProgress(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭WebSocket连接
|
||||
*/
|
||||
@@ -629,27 +777,25 @@ const nextStep = () => {
|
||||
if (ActiveStatue.value != 'error') {
|
||||
ActiveStatue.value = 'waiting' // 重置为等待状态
|
||||
let tempStep = stepsActiveIndex.value
|
||||
let idx = 1
|
||||
const stepOrder = [
|
||||
{ key: 'preTest', step: 1 },
|
||||
{ key: 'channelsTest', step: 2 },
|
||||
{ key: 'timeTest', step: 3 },
|
||||
{ key: 'test', step: 4 }
|
||||
] as const
|
||||
stepsActiveIndex.value++ // 步骤索引递增
|
||||
|
||||
// 遍历检测项,找到下一个需要执行的步骤
|
||||
for (let selectTestItemsKey in checkStore.selectTestItems) {
|
||||
if (
|
||||
tempStep == 0 &&
|
||||
checkStore.selectTestItems[selectTestItemsKey as keyof typeof checkStore.selectTestItems]
|
||||
) {
|
||||
for (let item of stepOrder) {
|
||||
if (tempStep == 0 && checkStore.selectTestItems[item.key]) {
|
||||
// 找到下一个要执行的检测项
|
||||
stepsActiveView.value = idx // 设置显示的组件
|
||||
stepsActive.value = idx // 设置业务逻辑步骤
|
||||
stepsActiveView.value = item.step // 设置显示的组件
|
||||
stepsActive.value = item.step // 设置业务逻辑步骤
|
||||
return
|
||||
}
|
||||
if (
|
||||
checkStore.selectTestItems[selectTestItemsKey as keyof typeof checkStore.selectTestItems] &&
|
||||
tempStep != 0
|
||||
) {
|
||||
if (checkStore.selectTestItems[item.key] && tempStep != 0) {
|
||||
tempStep-- // 跳过已选择的检测项
|
||||
}
|
||||
idx++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -680,12 +826,18 @@ function clearData() {
|
||||
TestStatus.value = 'waiting'
|
||||
ActiveStatue.value = 'waiting'
|
||||
nextStepText.value = '下一步'
|
||||
isRestoringFormalProgress.value = false
|
||||
checkStore.setRestoringFormalProgress(false)
|
||||
checkStore.setFormalProgressSnapshot(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭前确认处理
|
||||
*/
|
||||
const beforeClose = () => {
|
||||
if (isRestoringFormalProgress.value) {
|
||||
return
|
||||
}
|
||||
// 如果检测未完成且不是错误状态,需要用户确认
|
||||
if (stepsActiveIndex.value < stepsTotalNum.value && ActiveStatue.value != 'error') {
|
||||
ElMessageBox.confirm('检测未完成,是否退出当前检测流程?', '提示', {
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
<el-form-item label='相对湿度(%)' prop='humidity' :label-width='110'>
|
||||
<el-input v-model='formContent.humidity' placeholder='请输入湿度' maxlength='32' show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item v-if='hasCheckingDevice' label='是否续检' :label-width='110'>
|
||||
<el-checkbox v-model='resumeFormalChecked' @change='handleResumeFormalChange' />
|
||||
</el-form-item>
|
||||
<el-form-item v-if='checkStore.plan.timeCheck === 1' label='守时检测' prop='timeTest' :label-width='110'>
|
||||
<el-checkbox v-model='timeTestChecked' @change='handleTimeTestChange' />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
@@ -33,15 +39,47 @@ const dialogFormRef = ref()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const formContent = reactive<Device.ResTH>({ temperature: 0, humidity: 0 })
|
||||
const hasCheckingDevice = ref(false)
|
||||
const resumeFormalChecked = ref(false)
|
||||
const timeTestChecked = ref(false)
|
||||
const checkStore = useCheckStore()
|
||||
const open = async () => {
|
||||
resetFormContent()
|
||||
syncResumeFormalSelection()
|
||||
timeTestChecked.value = Boolean(checkStore.selectTestItems.timeTest)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 清空表单内容
|
||||
const resetFormContent = () => {
|
||||
Object.assign(formContent, { temperature: '22', humidity: '50' })
|
||||
hasCheckingDevice.value = false
|
||||
resumeFormalChecked.value = false
|
||||
timeTestChecked.value = false
|
||||
}
|
||||
|
||||
const syncTimeTestSelection = (selected: boolean) => {
|
||||
timeTestChecked.value = selected
|
||||
checkStore.setSelectTestItems({
|
||||
...checkStore.selectTestItems,
|
||||
timeTest: selected,
|
||||
})
|
||||
}
|
||||
|
||||
const syncResumeFormalSelection = () => {
|
||||
hasCheckingDevice.value = checkStore.devices.some(device => Number(device.checkState) === 1)
|
||||
resumeFormalChecked.value = hasCheckingDevice.value
|
||||
checkStore.setResumeFormal(hasCheckingDevice.value)
|
||||
}
|
||||
|
||||
const handleResumeFormalChange = (value: string | number | boolean) => {
|
||||
const selected = hasCheckingDevice.value && Boolean(value)
|
||||
resumeFormalChecked.value = selected
|
||||
checkStore.setResumeFormal(selected)
|
||||
}
|
||||
|
||||
const handleTimeTestChange = (value: string | number | boolean) => {
|
||||
syncTimeTestSelection(Boolean(value))
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +109,7 @@ const handleStart = () => {
|
||||
if (valid) {
|
||||
checkStore.setTemperature(formContent.temperature)
|
||||
checkStore.setHumidity(formContent.humidity)
|
||||
checkStore.setResumeFormal(hasCheckingDevice.value && resumeFormalChecked.value)
|
||||
emit('openTestDialog2')
|
||||
handleClose()
|
||||
}
|
||||
@@ -95,5 +134,4 @@ defineExpose({ open })
|
||||
|
||||
|
||||
<style scoped lang='scss'>
|
||||
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@
|
||||
default-expand-all
|
||||
:data="props.treeData"
|
||||
:props="defaultProps"
|
||||
style="width: 100%"
|
||||
style="width: 100%;margin-bottom: 80px;"
|
||||
:expand-on-click-node="false"
|
||||
:highlight-current="true"
|
||||
@node-click="handleNodeClick"
|
||||
|
||||
@@ -109,6 +109,10 @@ import {
|
||||
shouldHandleConnectionTerminalMessage,
|
||||
} from "./connectionState";
|
||||
|
||||
defineOptions({
|
||||
name: "controlSource",
|
||||
});
|
||||
|
||||
const show = ref(false);
|
||||
const router = useRouter();
|
||||
const modeId = ref();
|
||||
|
||||
@@ -95,6 +95,33 @@
|
||||
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="设备图片" class="device-image-form-item">
|
||||
<div class="device-image-field">
|
||||
<input
|
||||
ref="imageInputRef"
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png,image/jpeg,image/png"
|
||||
style="display: none"
|
||||
@change="handleImageChange"
|
||||
/>
|
||||
<div v-if="imagePreviewUrl" class="device-image-preview">
|
||||
<img :src="imagePreviewUrl" alt="device image preview" />
|
||||
</div>
|
||||
<div v-else class="device-image-empty">暂无图片</div>
|
||||
<div class="device-image-actions">
|
||||
<el-button size="small" @click="triggerImageSelect">
|
||||
{{ hasPersistedImage || selectedImageFile ? '更换图片' : '上传图片' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="formContent.id && hasPersistedImage"
|
||||
size="small"
|
||||
@click="handleImageDownload"
|
||||
>
|
||||
下载图片
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-divider>参数信息</el-divider>
|
||||
<el-form-item label="通道数" prop="devChns" v-if="DevIsShow">
|
||||
<el-input v-model="formContent.devChns" :disabled="true" />
|
||||
@@ -259,7 +286,8 @@
|
||||
import { dialogBig } from '@/utils/elementBind'
|
||||
import { type Device } from '@/api/device/interface/device'
|
||||
import { ElMessage, type FormItemRule } from 'element-plus'
|
||||
import { addPqDev, getSelectOptions, updatePqDev } from '@/api/device/device'
|
||||
import { addPqDev, downloadDeviceImage, getSelectOptions, updatePqDev } from '@/api/device/device'
|
||||
import { buildDeviceImagePreviewSrc } from '@/api/device/device/formData'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useDictStore } from '@/stores/modules/dict'
|
||||
// 使用 dayjs 库格式化
|
||||
@@ -286,6 +314,11 @@ const activeTab = ref('0') // '0' 对应第一个 tab
|
||||
const monitorTableHeight = ref(375) // 默认高度
|
||||
|
||||
const selectOptions = ref<Record<string, Device.SelectOption[]>>({})
|
||||
const imageInputRef = ref<HTMLInputElement>()
|
||||
const selectedImageFile = ref<File | null>(null)
|
||||
const imagePreviewUrl = ref<string | null>(null)
|
||||
const imagePreviewObjectUrl = ref<string | null>(null)
|
||||
const hasPersistedImage = ref(false)
|
||||
|
||||
const pqChannelArray = ref([
|
||||
{
|
||||
@@ -305,6 +338,85 @@ const onTabChange = (tabName: string | number) => {
|
||||
}
|
||||
}
|
||||
|
||||
const revokeImageObjectUrl = () => {
|
||||
if (imagePreviewObjectUrl.value) {
|
||||
URL.revokeObjectURL(imagePreviewObjectUrl.value)
|
||||
imagePreviewObjectUrl.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const resetImageState = () => {
|
||||
revokeImageObjectUrl()
|
||||
selectedImageFile.value = null
|
||||
imagePreviewUrl.value = null
|
||||
hasPersistedImage.value = false
|
||||
if (imageInputRef.value) {
|
||||
imageInputRef.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const validateImageFile = (file: File) => {
|
||||
const allowedTypes = ['image/jpeg', 'image/png']
|
||||
const isAllowedType = allowedTypes.includes(file.type) || /\.(jpe?g|png)$/i.test(file.name)
|
||||
if (!isAllowedType) {
|
||||
ElMessage.warning({ message: '仅支持 JPG、JPEG、PNG 图片' })
|
||||
return false
|
||||
}
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
ElMessage.warning({ message: '图片大小不能超过 10MB' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const triggerImageSelect = () => {
|
||||
imageInputRef.value?.click()
|
||||
}
|
||||
|
||||
const handleImageChange = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
if (!validateImageFile(file)) {
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
revokeImageObjectUrl()
|
||||
selectedImageFile.value = file
|
||||
imagePreviewObjectUrl.value = URL.createObjectURL(file)
|
||||
imagePreviewUrl.value = imagePreviewObjectUrl.value
|
||||
}
|
||||
|
||||
const updateImageEchoState = (device: Partial<Device.ResPqDev>) => {
|
||||
revokeImageObjectUrl()
|
||||
selectedImageFile.value = null
|
||||
hasPersistedImage.value = Boolean(device.hasImage)
|
||||
imagePreviewUrl.value = buildDeviceImagePreviewSrc(device.hasImage, device.imageBase64, device.imageContentType)
|
||||
if (imageInputRef.value) {
|
||||
imageInputRef.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const getDownloadFileName = (contentDisposition?: string | null) => {
|
||||
const match = contentDisposition?.match(/filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i)
|
||||
return decodeURIComponent(match?.[1] ?? match?.[2] ?? 'device-image')
|
||||
}
|
||||
|
||||
const handleImageDownload = async () => {
|
||||
if (!formContent.id || !hasPersistedImage.value) {
|
||||
return
|
||||
}
|
||||
const response = await downloadDeviceImage(formContent.id)
|
||||
const blobUrl = URL.createObjectURL(response.data)
|
||||
const link = document.createElement('a')
|
||||
link.href = blobUrl
|
||||
link.download = getDownloadFileName(response.headers['content-disposition'])
|
||||
link.click()
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
}
|
||||
|
||||
function useMetaInfo() {
|
||||
const dialogVisible = ref(false)
|
||||
const titleType = ref('add')
|
||||
@@ -473,6 +585,7 @@ const close = () => {
|
||||
dialogVisible.value = false
|
||||
// 清空dialogForm中的值
|
||||
resetFormContent()
|
||||
resetImageState()
|
||||
// 重置表单
|
||||
dialogFormRef.value?.resetFields()
|
||||
}
|
||||
@@ -535,7 +648,7 @@ const save = () => {
|
||||
formContent.monitorList = monitor.value
|
||||
|
||||
if (titleType.value != 'add') {
|
||||
await updatePqDev(formContent)
|
||||
await updatePqDev(formContent, selectedImageFile.value)
|
||||
ElMessage.success({ message: `${dialogTitle.value}成功!` })
|
||||
} else {
|
||||
// 新增需要把设备模式转成字典ID
|
||||
@@ -550,7 +663,7 @@ const save = () => {
|
||||
if (protocolItem) {
|
||||
formContent.protocol = protocolItem.id
|
||||
}
|
||||
await addPqDev(formContent)
|
||||
await addPqDev(formContent, selectedImageFile.value)
|
||||
ElMessage.success({ message: `${dialogTitle.value}成功!` })
|
||||
}
|
||||
|
||||
@@ -617,9 +730,11 @@ const open = async (
|
||||
formContent.inspectChannel = formContent.inspectChannel.split(',').filter(Boolean)
|
||||
}
|
||||
monitor.value = data.monitorList
|
||||
updateImageEchoState(data)
|
||||
} else {
|
||||
monitor.value = []
|
||||
resetFormContent()
|
||||
resetImageState()
|
||||
//只有比对式设备ID由前端传
|
||||
if (currentMode === '比对式') formContent.id = generateUUID().replaceAll('-', '')
|
||||
|
||||
@@ -695,3 +810,53 @@ const props = defineProps<{
|
||||
refreshTable: (() => Promise<void>) | undefined
|
||||
}>()
|
||||
</script>
|
||||
<style scoped>
|
||||
.form-three {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.device-image-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.device-image-preview {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.device-image-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.device-image-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
color: #909399;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.device-image-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.device-image-actions :deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,7 +39,9 @@ let scriptForm: any = {
|
||||
ftransValue: '', // 暂态幅值
|
||||
retainTime: '', // 暂态持续时间
|
||||
fpreTime:2.0,
|
||||
fafterTime:3.0
|
||||
fafterTime:3.0,
|
||||
frampIn:0.001, //写入时间
|
||||
frampOut:0.001 //写出时间
|
||||
} //暂态
|
||||
},
|
||||
{
|
||||
@@ -71,7 +73,9 @@ let scriptForm: any = {
|
||||
ftransValue: '', // 暂态幅值
|
||||
retainTime: '', // 暂态持续时间
|
||||
fpreTime:2.0,
|
||||
fafterTime:3.0
|
||||
fafterTime:3.0,
|
||||
frampIn:0.001, //写入时间
|
||||
frampOut:0.001 //写出时间
|
||||
} //暂态
|
||||
},
|
||||
{
|
||||
@@ -103,7 +107,9 @@ let scriptForm: any = {
|
||||
ftransValue: '', // 暂态幅值
|
||||
retainTime: '', // 暂态持续时间
|
||||
fpreTime:2.0,
|
||||
fafterTime:3.0
|
||||
fafterTime:3.0,
|
||||
frampIn:0.001, //写入时间
|
||||
frampOut:0.001 //写出时间
|
||||
} //暂态
|
||||
},
|
||||
{
|
||||
@@ -135,7 +141,9 @@ let scriptForm: any = {
|
||||
ftransValue: '', // 暂态幅值
|
||||
retainTime: '', // 暂态持续时间
|
||||
fpreTime:2.0,
|
||||
fafterTime:3.0
|
||||
fafterTime:3.0,
|
||||
frampIn:0.001, //写入时间
|
||||
frampOut:0.001 //写出时间
|
||||
} //暂态
|
||||
},
|
||||
{
|
||||
@@ -167,7 +175,9 @@ let scriptForm: any = {
|
||||
ftransValue: '', // 暂态幅值
|
||||
retainTime: '', // 暂态持续时间
|
||||
fpreTime:2.0,
|
||||
fafterTime:3.0
|
||||
fafterTime:3.0,
|
||||
frampIn:0.001, //写入时间
|
||||
frampOut:0.001 //写出时间
|
||||
} //暂态
|
||||
},
|
||||
{
|
||||
@@ -199,7 +209,9 @@ let scriptForm: any = {
|
||||
ftransValue: '', // 暂态幅值
|
||||
retainTime: '', // 暂态持续时间
|
||||
fpreTime:2.0,
|
||||
fafterTime:3.0
|
||||
fafterTime:3.0,
|
||||
frampIn:0.001, //写入时间
|
||||
frampOut:0.001 //写出时间
|
||||
} //暂态
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user