From 48547bc53bdf830ee4571e572ea863416e41a498 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 24 May 2026 19:41:15 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20IoT=20=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E9=A1=B5=E9=9D=A2=E5=A4=9A=E5=A4=84=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E4=B8=8E=E5=A5=91=E7=BA=A6=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复告警记录产品/设备筛选联动 - 清理设备详情延迟刷新 timer,避免卸载后触发查询 - 优化 OTA 固件编辑态只读展示与任务列表分页重置 - 修复场景联动值输入回显和布尔值类型 - 修复设备模拟器输入串台、数值类型提交和服务参数校验 - 更新 IoT bug 归档与迁移说明 --- .../src/views/iot/alert/record/data.ts | 16 +- .../device/device/detail/modules/message.vue | 12 +- .../device/detail/modules/simulator.vue | 133 ++++++++++++++++- .../detail/modules/thing-model-event.vue | 20 ++- .../detail/modules/thing-model-service.vue | 20 ++- .../src/views/iot/ota/firmware/data.ts | 24 ++- .../src/views/iot/ota/task/modules/list.vue | 2 +- .../iot/ota/task/record/modules/list.vue | 4 +- .../rule/scene/form/inputs/value-input.vue | 48 +++++- .../mes/tm/tool/components/tm-tool-select.vue | 5 +- .../src/views/iot/alert/record/data.ts | 16 +- .../device/device/detail/modules/message.vue | 12 +- .../device/detail/modules/simulator.vue | 140 +++++++++++++++++- .../detail/modules/thing-model-event.vue | 20 ++- .../detail/modules/thing-model-service.vue | 20 ++- .../src/views/iot/ota/firmware/data.ts | 18 ++- .../src/views/iot/ota/task/modules/list.vue | 2 +- .../iot/ota/task/record/modules/list.vue | 4 +- .../rule/scene/form/inputs/value-input.vue | 48 +++++- .../mes/tm/tool/components/tm-tool-select.vue | 5 +- 20 files changed, 508 insertions(+), 61 deletions(-) diff --git a/apps/web-antd/src/views/iot/alert/record/data.ts b/apps/web-antd/src/views/iot/alert/record/data.ts index 3123c0188..34154ee9e 100644 --- a/apps/web-antd/src/views/iot/alert/record/data.ts +++ b/apps/web-antd/src/views/iot/alert/record/data.ts @@ -62,13 +62,27 @@ export function useGridFormSchema(): VbenFormSchema[] { label: '设备', component: 'ApiSelect', componentProps: { - api: getSimpleDeviceList, + api: (params?: { productId?: number }) => + getSimpleDeviceList(undefined, params?.productId), labelField: 'deviceName', valueField: 'id', placeholder: '请选择设备', allowClear: true, showSearch: true, }, + dependencies: { + triggerFields: ['productId'], + componentProps: (values) => { + return { + params: { productId: values.productId }, + }; + }, + trigger: (values, formApi) => { + if (values.deviceId !== undefined) { + void formApi.setFieldValue('deviceId', undefined); + } + }, + }, }, { fieldName: 'processStatus', diff --git a/apps/web-antd/src/views/iot/device/device/detail/modules/message.vue b/apps/web-antd/src/views/iot/device/device/detail/modules/message.vue index 965d5eb24..16efec5cf 100644 --- a/apps/web-antd/src/views/iot/device/device/detail/modules/message.vue +++ b/apps/web-antd/src/views/iot/device/device/detail/modules/message.vue @@ -31,6 +31,7 @@ const queryParams = reactive({ const autoRefresh = ref(false); // 自动刷新开关 let autoRefreshTimer: any = null; // 自动刷新定时器 +let refreshTimer: ReturnType | undefined; // 延迟刷新定时器 /** 消息方法选项 */ const methodOptions = computed(() => { @@ -150,6 +151,10 @@ onBeforeUnmount(() => { clearInterval(autoRefreshTimer); autoRefreshTimer = null; } + if (refreshTimer) { + clearTimeout(refreshTimer); + refreshTimer = undefined; + } }); /** 初始化 */ @@ -161,9 +166,14 @@ onMounted(() => { /** 刷新消息列表 */ function refresh(delay = 0) { + if (refreshTimer) { + clearTimeout(refreshTimer); + refreshTimer = undefined; + } if (delay > 0) { - setTimeout(() => { + refreshTimer = setTimeout(() => { gridApi.query(); + refreshTimer = undefined; }, delay); } else { gridApi.query(); diff --git a/apps/web-antd/src/views/iot/device/device/detail/modules/simulator.vue b/apps/web-antd/src/views/iot/device/device/detail/modules/simulator.vue index 3af6c6216..eefd303f8 100644 --- a/apps/web-antd/src/views/iot/device/device/detail/modules/simulator.vue +++ b/apps/web-antd/src/views/iot/device/device/detail/modules/simulator.vue @@ -6,11 +6,12 @@ import type { IotDeviceApi } from '#/api/iot/device/device'; import type { IotProductApi } from '#/api/iot/product/product'; import type { ThingModelApi } from '#/api/iot/thingmodel'; -import { computed, ref } from 'vue'; +import { computed, ref, watch } from 'vue'; import { ContentWrap } from '@vben/common-ui'; import { DeviceStateEnum, + IoTDataSpecsDataTypeEnum, IotDeviceMessageMethodEnum, IoTThingModelTypeEnum, } from '@vben/constants'; @@ -21,8 +22,10 @@ import { Card, Col, Input, + InputNumber, message, Row, + Select, Table, Tabs, Textarea, @@ -51,7 +54,7 @@ const debugCollapsed = ref(false); // 指令调试区域折叠状态 const messageCollapsed = ref(false); // 设备消息区域折叠状态 // 表单数据:存储用户输入的模拟值 -const formData = ref>({}); +const formData = ref>({}); // 根据类型过滤物模型数据 const getFilteredThingModelList = (type: number) => { @@ -184,21 +187,75 @@ const serviceColumns = [ // 获取表单值 function getFormValue(identifier: string) { - return formData.value[identifier] || ''; + return formData.value[identifier] ?? ''; } // 设置表单值 -function setFormValue(identifier: string, value: string) { +function setFormValue(identifier: string, value: any) { formData.value[identifier] = value; } +/** 获取属性数据类型 */ +function getPropertyDataType(row: ThingModelApi.ThingModel) { + return row.property?.dataType; +} + +/** 判断属性是否为数值类型 */ +function isNumberProperty(row: ThingModelApi.ThingModel) { + return [ + IoTDataSpecsDataTypeEnum.DOUBLE, + IoTDataSpecsDataTypeEnum.FLOAT, + IoTDataSpecsDataTypeEnum.INT, + ].includes(getPropertyDataType(row) as any); +} + +/** 判断属性是否使用下拉选项 */ +function isSelectProperty(row: ThingModelApi.ThingModel) { + return [ + IoTDataSpecsDataTypeEnum.BOOL, + IoTDataSpecsDataTypeEnum.ENUM, + ].includes(getPropertyDataType(row) as any); +} + +/** 获取属性选项 */ +function getPropertyOptions(row: ThingModelApi.ThingModel) { + const list = row.property?.dataSpecsList || []; + if (list.length > 0) { + return list.map((item: any) => ({ + label: item.name || item.label || String(item.value), + value: String(item.value), + })); + } + if (getPropertyDataType(row) === IoTDataSpecsDataTypeEnum.BOOL) { + return [ + { label: '真 (true)', value: 'true' }, + { label: '假 (false)', value: 'false' }, + ]; + } + return []; +} + +/** 按物模型数据类型转换属性值 */ +function normalizePropertyValue(row: ThingModelApi.ThingModel, value: any) { + if (value === undefined || value === null || value === '') { + return undefined; + } + if (isNumberProperty(row)) { + return Number(value); + } + return value; +} + // 属性上报 async function handlePropertyPost() { try { const params: Record = {}; propertyList.value.forEach((item) => { - const value = formData.value[item.identifier!]; - if (value) { + const value = normalizePropertyValue( + item, + formData.value[item.identifier!], + ); + if (value !== undefined) { params[item.identifier!] = value; } }); @@ -281,8 +338,11 @@ async function handlePropertySet() { try { const params: Record = {}; propertyList.value.forEach((item) => { - const value = formData.value[item.identifier!]; - if (value) { + const value = normalizePropertyValue( + item, + formData.value[item.identifier!], + ); + if (value !== undefined) { params[item.identifier!] = value; } }); @@ -320,6 +380,14 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) { message.error('服务参数格式错误,请输入有效的JSON格式'); return; } + if ( + typeof inputParams !== 'object' || + inputParams === null || + Array.isArray(inputParams) + ) { + message.error('服务参数必须是 JSON 对象'); + return; + } } // 与后端 IotDeviceServiceInvokeReqDTO 对齐 :{ identifier, inputParams } @@ -340,6 +408,11 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) { console.error(error); } } + +/** 切换调试方法时清空输入,避免不同方法之间串台提交 */ +watch([activeTab, upstreamTab, downstreamTab], () => { + formData.value = {}; +});