13 Commits
2026-06 ... dev

Author SHA1 Message Date
caozehui
8c7c34cda2 温度湿度弹窗中新增守时勾选框 2026-07-13 10:35:04 +08:00
caozehui
b0ee58a1b5 docs: add time check design spec 2026-07-10 16:02:28 +08:00
caozehui
c3915dc8f5 检测计划-开启守时检测功能 2026-07-10 10:19:47 +08:00
caozehui
ff4a197436 Merge branch 'dev' 2026-07-10 08:43:26 +08:00
caozehui
ad2d94a893 切换程控源页面保持页面缓存 2026-07-10 08:42:38 +08:00
caozehui
030e84ae16 被检设备上传图片功能 2026-07-09 16:35:53 +08:00
caozehui
60112e84e3 归档 2026-07-09 09:15:40 +08:00
caozehui
8f12d0e03e 暂态写入写出时间 2026-07-08 15:51:11 +08:00
caozehui
49ded8c568 微调 2026-07-08 08:33:38 +08:00
caozehui
a39ad579f9 微调 2026-07-06 20:06:31 +08:00
caozehui
657d357159 微调 2026-07-06 16:24:23 +08:00
caozehui
0d7d5722c1 微调 2026-07-06 10:44:38 +08:00
caozehui
36496e5825 脚本检测项单位调整 2026-07-06 08:46:48 +08:00
99 changed files with 757 additions and 148 deletions

View File

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

View File

@@ -1 +1 @@
55936
51248

Binary file not shown.

View File

@@ -17,3 +17,4 @@
.\binlog.000039
.\binlog.000040
.\binlog.000041
.\binlog.000042

View 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`:验证只提示,不改变表格结构。

View 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}`
}

View File

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

View File

@@ -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 {
@@ -116,4 +119,4 @@ export namespace Device {
* 被检设备表格查询分页返回的对象;
*/
export interface ResPqDevPage extends ResPage<ResPqDev> {}
}
}

View File

@@ -36,6 +36,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 {
@@ -68,4 +69,4 @@ export const useCheckStore = defineStore(CHECK_STORE_KEY, {
this.nodesConnectable = nodesConnectable
}
}
})
})

View File

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

View File

@@ -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;
}
</style>
.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>

View File

@@ -679,6 +679,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':

View File

@@ -28,18 +28,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 +66,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"
/>
@@ -255,20 +255,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,6 +354,7 @@ const handleSubmitFast = () => {
testItemList: [
checkStore.selectTestItems.preTest,
checkStore.selectTestItems.channelsTest,
checkStore.selectTestItems.timeTest,
checkStore.selectTestItems.test
]
}).then(res => {
@@ -369,10 +370,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,6 +385,7 @@ const handleSubmitFast = () => {
testItemList: [
checkStore.selectTestItems.preTest,
checkStore.selectTestItems.channelsTest,
checkStore.selectTestItems.timeTest,
checkStore.selectTestItems.test
]
}).then(res => {
@@ -402,12 +401,47 @@ 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.timeTest
) {
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
]
}).then(res => {
if (res.code !== 'A0000') {
ElMessageBox.alert('守时检测失败', '检测失败', {
confirmButtonText: '确定',
type: 'error'
})
timeTestStatus.value = 'error'
}
})
}
timeTestStatus.value = 'start'
}
break
case 4: // 正式检测步骤
if (TestStatus.value == 'waiting') {
// 如果没有预检测、系数校准和守时检测,直接进行正式检测需要先初始化
if (
!checkStore.selectTestItems.preTest &&
!checkStore.selectTestItems.channelsTest &&
!checkStore.selectTestItems.timeTest &&
checkStore.selectTestItems.test
) {
startPreTest({
@@ -421,6 +455,7 @@ const handleSubmitFast = () => {
testItemList: [
checkStore.selectTestItems.preTest,
checkStore.selectTestItems.channelsTest,
checkStore.selectTestItems.timeTest,
checkStore.selectTestItems.test
]
}).then(res => {
@@ -629,27 +664,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++
}
}
}

View File

@@ -8,6 +8,9 @@
<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='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 +36,30 @@ const dialogFormRef = ref()
const dialogVisible = ref(false)
const formContent = reactive<Device.ResTH>({ temperature: 0, humidity: 0 })
const timeTestChecked = ref(false)
const checkStore = useCheckStore()
const open = async () => {
resetFormContent()
syncTimeTestSelection(false)
dialogVisible.value = true
}
// 清空表单内容
const resetFormContent = () => {
Object.assign(formContent, { temperature: '22', humidity: '50' })
timeTestChecked.value = false
}
const syncTimeTestSelection = (selected: boolean) => {
timeTestChecked.value = selected
checkStore.setSelectTestItems({
...checkStore.selectTestItems,
timeTest: selected,
})
}
const handleTimeTestChange = (value: string | number | boolean) => {
syncTimeTestSelection(Boolean(value))
}
@@ -95,5 +113,4 @@ defineExpose({ open })
<style scoped lang='scss'>
</style>
</style>

View File

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

View File

@@ -109,6 +109,10 @@ import {
shouldHandleConnectionTerminalMessage,
} from "./connectionState";
defineOptions({
name: "controlSource",
});
const show = ref(false);
const router = useRouter();
const modeId = ref();

View File

@@ -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('-', '')
@@ -694,4 +809,54 @@ defineExpose({ open })
const props = defineProps<{
refreshTable: (() => Promise<void>) | undefined
}>()
</script>
</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>

View File

@@ -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 //写出时间
} //暂态
}
]

View File

@@ -1,13 +0,0 @@
<template>
<div>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

View File

@@ -321,13 +321,18 @@ const setUnit = (row: any) => {
row.name == '电流有效值' ? (text = o) : ''
row.name == '电流相角' ? (text = '°') : ''
} else if (row.name == '电压序分量') {
text = '%Un V'
text = props.valueCode == 'Absolute'? 'V':'%Un V'
} else if (row.name == '电流序分量') {
text = '%In A'
text = props.valueCode == 'Absolute'? 'A':'%In A'
} else if (row.name.includes('视在功率')) {
text = props.valueCode == 'Absolute' ? 'VA' : '%Un*In VA'
} else if (row.name.includes('无功功率')) {
text = props.valueCode == 'Absolute' ? 'Var' : '%Un*In Var'
} else if (row.name.includes('功率因数')) {
text = ''
} else {
text = unit.filter(item => item.label == row.pname)[0]?.unit
}
return text || ''
}
const save = () => {

View File

@@ -8,14 +8,14 @@
<!-- 全局设置菜单内容 -->
<div class="test-pane-scroll">
<el-radio-group v-model="form.subType" class="test-radio-group">
<el-radio
v-for="item in tabChildren"
:key="item"
:label="item.label"
:value="item.code"
border
class="test-radio-item"
/>
<el-radio
v-for="item in tabChildren"
:key="item"
:label="item.label"
:value="item.code"
border
class="test-radio-item"
/>
<!-- <el-radio v-for="item in tabChildren" :value="item.value" border>{{ item.label }}</el-radio> -->
</el-radio-group>
</div>
@@ -162,7 +162,7 @@
<TestScriptFlickerTab :childForm="childForm" />
</el-tab-pane>
<el-tab-pane label="暂态编辑" v-if="childForm[0].dipFlag || childForm[1].dipFlag">
<TestScriptDipTab :childForm="childForm" @setRetainTime="setRetainTime" @setFPreTime="setFPreTime" @setFAfterTime="setFAfterTime" />
<TestScriptDipTab :childForm="childForm" @setRetainTime="setRetainTime" @setFPreTime="setFPreTime" @setFAfterTime="setFAfterTime" @setFRampIn="setFRampIn" @setFRampOut="setFRampOut"/>
</el-tab-pane>
</el-tabs>
</div>
@@ -314,6 +314,16 @@ const setFAfterTime = (val: any) => {
item.dipData.fafterTime = val
})
}
const setFRampIn = (val: any) => {
form.value.channelList.forEach((item: any) => {
item.dipData.frampIn = val
})
}
const setFRampOut = (val: any) => {
form.value.channelList.forEach((item: any) => {
item.dipData.frampOut = val
})
}
// 保存数据
const save = () => {
@@ -745,37 +755,37 @@ defineExpose({ open })
flex: 0 0 auto;
}
.test-pane-scroll {
height: 295px;
display: flex;
justify-content: flex-start;
align-items: flex-start;
overflow-x: auto;
overflow-y: auto;
}
.test-radio-group {
display: flex;
align-items: flex-start;
min-width: 100%;
flex-direction: column;
}
.test-pane-scroll {
height: 295px;
display: flex;
justify-content: flex-start;
align-items: flex-start;
overflow-x: auto;
overflow-y: auto;
}
.test-radio-group {
display: flex;
align-items: flex-start;
min-width: 100%;
flex-direction: column;
}
.left-tabs {
flex: 1 1 auto;
min-width: 0;
}
:deep(.test-radio-group .el-radio) {
width: max-content;
max-width: none;
}
:deep(.test-radio-group .test-radio-item) {
margin: 0 0 10px 0;
}
:deep(.test-radio-group .el-radio__label) {
white-space: nowrap;
}
:deep(.test-radio-group .el-radio) {
width: max-content;
max-width: none;
}
:deep(.test-radio-group .test-radio-item) {
margin: 0 0 10px 0;
}
:deep(.test-radio-group .el-radio__label) {
white-space: nowrap;
}
</style>

View File

@@ -31,7 +31,7 @@
<el-input
type="number"
@input="handleInput"
v-model="form[0].dipData.ftransValue"
v-model.number="form[0].dipData.ftransValue"
:disabled="!form[0].dipFlag"
onkeypress="return (/[\d]/.test(String.fromCharCode(event.keyCode)))"
/>
@@ -39,7 +39,7 @@
<el-form-item label="持续时间(周波)">
<el-input
type="number"
v-model="form[0].dipData.retainTime"
v-model.number="form[0].dipData.retainTime"
@input="handleInputRetainTime"
:disabled="!form[0].dipFlag"
onkeypress="return (/[\d.]/.test(String.fromCharCode(event.keyCode)))"
@@ -48,7 +48,7 @@
<el-form-item label="前延时间(S)">
<el-input
type="number"
v-model="form[0].dipData.fpreTime"
v-model.number="form[0].dipData.fpreTime"
@input="validateFPreTime"
:disabled="!form[0].dipFlag"
onkeypress="return (/[\d.]/.test(String.fromCharCode(event.keyCode)))"
@@ -57,12 +57,30 @@
<el-form-item label="后延时间(S)">
<el-input
type="number"
v-model="form[0].dipData.fafterTime"
v-model.number="form[0].dipData.fafterTime"
@input="validateFAfterTime"
:disabled="!form[0].dipFlag"
onkeypress="return (/[\d.]/.test(String.fromCharCode(event.keyCode)))"
/>
</el-form-item>
<el-form-item label="写入时间(S)">
<el-input
type="number"
v-model.number="form[0].dipData.frampIn"
@input="validateFRampIn"
:disabled="!form[0].dipFlag"
onkeypress="return (/[\d.]/.test(String.fromCharCode(event.keyCode)))"
/>
</el-form-item>
<el-form-item label="写出时间(S)">
<el-input
type="number"
v-model.number="form[0].dipData.frampOut"
@input="validateFRampOut"
:disabled="!form[0].dipFlag"
onkeypress="return (/[\d.]/.test(String.fromCharCode(event.keyCode)))"
/>
</el-form-item>
</el-form>
</div>
</div>
@@ -135,6 +153,20 @@ const validateFAfterTime = value => {
emit('setFAfterTime', value )
}
}
const validateFRampIn = value => {
if (value <= 0) {
ElMessage.warning('写入时间不能小于等于0S')
props.childForm[0].dipData.frampIn = 0.001
emit('setFRampIn', 0.001)
}
}
const validateFRampOut = value => {
if (value <= 0) {
ElMessage.warning('写出时间不能小于等于0S')
props.childForm[0].dipData.frampOut = 0.001
emit('setFRampOut', 0.001)
}
}
</script>

View File

@@ -112,8 +112,8 @@
:key="index"
:label="option.label"
:value="option.value"
:disabled="option.fluke!=1&&pqSourceArray?.find(item=>formContent.sourceIds==item.value)?.label.includes('FLUKE')"
/>
<!-- :disabled="option.fluke!=1&&pqSourceArray?.find(item=>formContent.sourceIds==item.value)?.label.includes('FLUKE')"-->
</el-select>
</el-form-item>
<el-form-item :label-width="110" label="误差体系" prop="errorSysId">
@@ -172,21 +172,6 @@
/>
</el-select>
</el-form-item>
<!-- <el-form-item :label-width="110" label="守时检测" prop="timeCheck">
<el-radio-group
v-model="formContent.timeCheck"
:disabled="planType != 0 || allDisabled || !canEdited"
>
<el-radio :value="1"></el-radio>
<el-radio :value="0"></el-radio>
</el-radio-group>
</el-form-item>-->
<!-- <el-form-item label="关联报告模版" :label-width="100" prop='associateReport'>
<el-radio-group v-model="formContent.associateReport">
<el-radio :value="1"></el-radio>
<el-radio :value="0"></el-radio>
</el-radio-group>
</el-form-item> -->
<el-form-item
v-if="formContent.associateReport === 1"
:label-width="110"
@@ -207,6 +192,9 @@
/>
</el-select>
</el-form-item>
<el-form-item v-if="!selectByMode" :label-width="110" label="守时检测" prop="timeCheck">
<el-switch v-model="formContent.timeCheck" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item v-if="selectByMode" :label-width="110" label="项目负责人" prop="leader">
<el-select
v-model="formContent.leader"