From 6fb45f1ded89f3292b9854e0d8681e53104d83cc Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 24 May 2026 16:43:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(iot):=20=E4=BF=AE=E5=A4=8D=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E8=81=94=E5=8A=A8=E5=8A=A8=E6=80=81=E5=88=97=E8=A1=A8?= =?UTF-8?q?=20key=20=E4=B8=8E=E6=A0=A1=E9=AA=8C=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 getStableObjectKey,统一处理对象列表 v-for 稳定 key - 场景联动触发器、执行器、条件组、条件项改用稳定对象 key - 保持场景规则 API 类型不包含 UI 专用 _key 字段 - 修复场景联动触发器/执行器校验与地图详情跳转 --- apps/web-antd/src/api/iot/rule/scene/index.ts | 2 +- .../api/mes/dv/maintenrecord/line/index.ts | 16 ++++++++--- .../iot/home/modules/device-map-card.vue | 27 ++++++++++--------- .../form/configs/device-trigger-config.vue | 3 ++- .../configs/sub-condition-group-config.vue | 3 ++- .../configs/timer-condition-group-config.vue | 3 ++- .../scene/form/sections/action-section.vue | 3 ++- .../scene/form/sections/trigger-section.vue | 3 ++- .../src/views/iot/rule/scene/modules/form.vue | 10 ++++--- apps/web-ele/src/api/iot/rule/scene/index.ts | 3 +-- .../api/mes/dv/maintenrecord/line/index.ts | 16 ++++++++--- .../iot/home/modules/device-map-card.vue | 12 ++++----- .../form/configs/device-trigger-config.vue | 3 ++- .../configs/sub-condition-group-config.vue | 3 ++- .../configs/timer-condition-group-config.vue | 3 ++- .../scene/form/sections/action-section.vue | 3 ++- .../scene/form/sections/trigger-section.vue | 3 ++- .../src/views/iot/rule/scene/modules/form.vue | 10 ++++--- packages/@core/base/shared/src/utils/util.ts | 21 +++++++++++++++ 19 files changed, 100 insertions(+), 47 deletions(-) diff --git a/apps/web-antd/src/api/iot/rule/scene/index.ts b/apps/web-antd/src/api/iot/rule/scene/index.ts index fd522a324..23549f1ad 100644 --- a/apps/web-antd/src/api/iot/rule/scene/index.ts +++ b/apps/web-antd/src/api/iot/rule/scene/index.ts @@ -24,7 +24,7 @@ export namespace RuleSceneApi { operator?: string; value?: any; cronExpression?: string; - conditionGroups?: TriggerCondition[][]; + conditionGroups?: TriggerCondition[][]; // 后端结构:List>;外层「或」、组内「且」 } /** 场景联动规则的触发条件 */ diff --git a/apps/web-antd/src/api/mes/dv/maintenrecord/line/index.ts b/apps/web-antd/src/api/mes/dv/maintenrecord/line/index.ts index 36b45e57d..fa67f638a 100644 --- a/apps/web-antd/src/api/mes/dv/maintenrecord/line/index.ts +++ b/apps/web-antd/src/api/mes/dv/maintenrecord/line/index.ts @@ -19,21 +19,29 @@ export namespace MesDvMaintenRecordLineApi { /** 查询设备保养记录明细分页 */ export function getMaintenRecordLinePage(params: PageParam) { - return requestClient.get>('/mes/dv/mainten-record-line/page', { params }); + return requestClient.get< + PageResult + >('/mes/dv/mainten-record-line/page', { params }); } /** 查询设备保养记录明细详情 */ export function getMaintenRecordLine(id: number) { - return requestClient.get(`/mes/dv/mainten-record-line/get?id=${id}`); + return requestClient.get( + `/mes/dv/mainten-record-line/get?id=${id}`, + ); } /** 新增设备保养记录明细 */ -export function createMaintenRecordLine(data: MesDvMaintenRecordLineApi.MaintenRecordLine) { +export function createMaintenRecordLine( + data: MesDvMaintenRecordLineApi.MaintenRecordLine, +) { return requestClient.post('/mes/dv/mainten-record-line/create', data); } /** 修改设备保养记录明细 */ -export function updateMaintenRecordLine(data: MesDvMaintenRecordLineApi.MaintenRecordLine) { +export function updateMaintenRecordLine( + data: MesDvMaintenRecordLineApi.MaintenRecordLine, +) { return requestClient.put('/mes/dv/mainten-record-line/update', data); } diff --git a/apps/web-antd/src/views/iot/home/modules/device-map-card.vue b/apps/web-antd/src/views/iot/home/modules/device-map-card.vue index 6f82c94da..61e47f5e2 100644 --- a/apps/web-antd/src/views/iot/home/modules/device-map-card.vue +++ b/apps/web-antd/src/views/iot/home/modules/device-map-card.vue @@ -111,19 +111,22 @@ function initMap() { // 信息窗口打开后绑定链接点击事件 infoWindow.addEventListener('open', () => { setTimeout(() => { - const link = document.querySelector('.device-link'); - if (link) { - link.addEventListener('click', (e) => { - e.preventDefault(); - const deviceId = (e.target as HTMLElement).dataset.id; - if (deviceId) { - router.push({ - name: 'IoTDeviceDetail', - params: { id: deviceId }, - }); - } - }); + const link = document.querySelector( + '.BMap_bubble_content .device-link', + ); + if (!link) { + return; } + link.addEventListener('click', (e) => { + e.preventDefault(); + if (device.id === undefined || device.id === null) { + return; + } + router.push({ + name: 'IoTDeviceDetail', + params: { id: device.id }, + }); + }); }, 100); }); diff --git a/apps/web-antd/src/views/iot/rule/scene/form/configs/device-trigger-config.vue b/apps/web-antd/src/views/iot/rule/scene/form/configs/device-trigger-config.vue index 7ccd55651..9193d5861 100644 --- a/apps/web-antd/src/views/iot/rule/scene/form/configs/device-trigger-config.vue +++ b/apps/web-antd/src/views/iot/rule/scene/form/configs/device-trigger-config.vue @@ -5,6 +5,7 @@ import type { RuleSceneApi } from '#/api/iot/rule/scene'; import { nextTick } from 'vue'; import { IconifyIcon } from '@vben/icons'; +import { getStableObjectKey } from '@vben/utils'; import { useVModel } from '@vueuse/core'; import { Button, Tag } from 'ant-design-vue'; @@ -183,7 +184,7 @@ function removeConditionGroup() {
diff --git a/apps/web-antd/src/views/iot/rule/scene/form/configs/sub-condition-group-config.vue b/apps/web-antd/src/views/iot/rule/scene/form/configs/sub-condition-group-config.vue index 0c9ca2ae5..a23770c8a 100644 --- a/apps/web-antd/src/views/iot/rule/scene/form/configs/sub-condition-group-config.vue +++ b/apps/web-antd/src/views/iot/rule/scene/form/configs/sub-condition-group-config.vue @@ -8,6 +8,7 @@ import { IotRuleSceneTriggerConditionTypeEnum, } from '@vben/constants'; import { IconifyIcon } from '@vben/icons'; +import { getStableObjectKey } from '@vben/utils'; import { useVModel } from '@vueuse/core'; import { Button } from 'ant-design-vue'; @@ -105,7 +106,7 @@ function updateCondition(
diff --git a/apps/web-antd/src/views/iot/rule/scene/form/configs/timer-condition-group-config.vue b/apps/web-antd/src/views/iot/rule/scene/form/configs/timer-condition-group-config.vue index c2e5e6745..6f10000b5 100644 --- a/apps/web-antd/src/views/iot/rule/scene/form/configs/timer-condition-group-config.vue +++ b/apps/web-antd/src/views/iot/rule/scene/form/configs/timer-condition-group-config.vue @@ -5,6 +5,7 @@ import { nextTick } from 'vue'; import { IotRuleSceneTriggerTypeEnum } from '@vben/constants'; import { IconifyIcon } from '@vben/icons'; +import { getStableObjectKey } from '@vben/utils'; import { useVModel } from '@vueuse/core'; import { Button, Tag } from 'ant-design-vue'; @@ -96,7 +97,7 @@ function updateConditionGroup( >
diff --git a/apps/web-antd/src/views/iot/rule/scene/form/sections/action-section.vue b/apps/web-antd/src/views/iot/rule/scene/form/sections/action-section.vue index a9d149abf..4225d93ef 100644 --- a/apps/web-antd/src/views/iot/rule/scene/form/sections/action-section.vue +++ b/apps/web-antd/src/views/iot/rule/scene/form/sections/action-section.vue @@ -8,6 +8,7 @@ import { IotRuleSceneActionTypeEnum, } from '@vben/constants'; import { IconifyIcon } from '@vben/icons'; +import { getStableObjectKey } from '@vben/utils'; import { useVModel } from '@vueuse/core'; import { Button, Card, Empty, Form, Select, Tag } from 'ant-design-vue'; @@ -183,7 +184,7 @@ function onActionTypeChange(action: RuleSceneApi.Action, type: number) {
diff --git a/apps/web-antd/src/views/iot/rule/scene/form/sections/trigger-section.vue b/apps/web-antd/src/views/iot/rule/scene/form/sections/trigger-section.vue index bcd6d372b..8536d844e 100644 --- a/apps/web-antd/src/views/iot/rule/scene/form/sections/trigger-section.vue +++ b/apps/web-antd/src/views/iot/rule/scene/form/sections/trigger-section.vue @@ -9,6 +9,7 @@ import { isDeviceTrigger, } from '@vben/constants'; import { IconifyIcon } from '@vben/icons'; +import { getStableObjectKey } from '@vben/utils'; import { useVModel } from '@vueuse/core'; import { Button, Card, Empty, Form, Tag } from 'ant-design-vue'; @@ -131,7 +132,7 @@ onMounted(() => {
diff --git a/apps/web-antd/src/views/iot/rule/scene/modules/form.vue b/apps/web-antd/src/views/iot/rule/scene/modules/form.vue index 6e186491f..e34113d8c 100644 --- a/apps/web-antd/src/views/iot/rule/scene/modules/form.vue +++ b/apps/web-antd/src/views/iot/rule/scene/modules/form.vue @@ -103,12 +103,14 @@ function buildEmptyFormData(): RuleSceneApi.SceneRule { /** 回显时兜底,保证触发器/执行器数组不为空 */ function normalizeFormData(result: any): RuleSceneApi.SceneRule { + const triggers: RuleSceneApi.Trigger[] = result.triggers?.length + ? result.triggers + : buildEmptyFormData().triggers!; + const actions: RuleSceneApi.Action[] = result.actions || []; return { ...result, - triggers: result.triggers?.length - ? result.triggers - : buildEmptyFormData().triggers, - actions: result.actions || [], + triggers, + actions, }; } diff --git a/apps/web-ele/src/api/iot/rule/scene/index.ts b/apps/web-ele/src/api/iot/rule/scene/index.ts index b89b72b4a..23549f1ad 100644 --- a/apps/web-ele/src/api/iot/rule/scene/index.ts +++ b/apps/web-ele/src/api/iot/rule/scene/index.ts @@ -24,8 +24,7 @@ export namespace RuleSceneApi { operator?: string; value?: any; cronExpression?: string; - // 后端结构:List>;外层「或」、组内「且」 - conditionGroups?: TriggerCondition[][]; + conditionGroups?: TriggerCondition[][]; // 后端结构:List>;外层「或」、组内「且」 } /** 场景联动规则的触发条件 */ diff --git a/apps/web-ele/src/api/mes/dv/maintenrecord/line/index.ts b/apps/web-ele/src/api/mes/dv/maintenrecord/line/index.ts index 36b45e57d..fa67f638a 100644 --- a/apps/web-ele/src/api/mes/dv/maintenrecord/line/index.ts +++ b/apps/web-ele/src/api/mes/dv/maintenrecord/line/index.ts @@ -19,21 +19,29 @@ export namespace MesDvMaintenRecordLineApi { /** 查询设备保养记录明细分页 */ export function getMaintenRecordLinePage(params: PageParam) { - return requestClient.get>('/mes/dv/mainten-record-line/page', { params }); + return requestClient.get< + PageResult + >('/mes/dv/mainten-record-line/page', { params }); } /** 查询设备保养记录明细详情 */ export function getMaintenRecordLine(id: number) { - return requestClient.get(`/mes/dv/mainten-record-line/get?id=${id}`); + return requestClient.get( + `/mes/dv/mainten-record-line/get?id=${id}`, + ); } /** 新增设备保养记录明细 */ -export function createMaintenRecordLine(data: MesDvMaintenRecordLineApi.MaintenRecordLine) { +export function createMaintenRecordLine( + data: MesDvMaintenRecordLineApi.MaintenRecordLine, +) { return requestClient.post('/mes/dv/mainten-record-line/create', data); } /** 修改设备保养记录明细 */ -export function updateMaintenRecordLine(data: MesDvMaintenRecordLineApi.MaintenRecordLine) { +export function updateMaintenRecordLine( + data: MesDvMaintenRecordLineApi.MaintenRecordLine, +) { return requestClient.put('/mes/dv/mainten-record-line/update', data); } diff --git a/apps/web-ele/src/views/iot/home/modules/device-map-card.vue b/apps/web-ele/src/views/iot/home/modules/device-map-card.vue index 46bbc5759..c1bf9dded 100644 --- a/apps/web-ele/src/views/iot/home/modules/device-map-card.vue +++ b/apps/web-ele/src/views/iot/home/modules/device-map-card.vue @@ -117,13 +117,13 @@ function initMap() { } link.addEventListener('click', (e) => { e.preventDefault(); - const deviceId = (e.target as HTMLElement).dataset.id; - if (deviceId) { - router.push({ - name: 'IoTDeviceDetail', - params: { id: deviceId }, - }); + if (device.id === undefined || device.id === null) { + return; } + router.push({ + name: 'IoTDeviceDetail', + params: { id: device.id }, + }); }); }, 100); }); diff --git a/apps/web-ele/src/views/iot/rule/scene/form/configs/device-trigger-config.vue b/apps/web-ele/src/views/iot/rule/scene/form/configs/device-trigger-config.vue index b9a774f3b..f1563b9e0 100644 --- a/apps/web-ele/src/views/iot/rule/scene/form/configs/device-trigger-config.vue +++ b/apps/web-ele/src/views/iot/rule/scene/form/configs/device-trigger-config.vue @@ -5,6 +5,7 @@ import type { RuleSceneApi } from '#/api/iot/rule/scene'; import { nextTick } from 'vue'; import { IconifyIcon } from '@vben/icons'; +import { getStableObjectKey } from '@vben/utils'; import { useVModel } from '@vueuse/core'; import { ElButton, ElTag } from 'element-plus'; @@ -180,7 +181,7 @@ function removeConditionGroup() {
diff --git a/apps/web-ele/src/views/iot/rule/scene/form/configs/sub-condition-group-config.vue b/apps/web-ele/src/views/iot/rule/scene/form/configs/sub-condition-group-config.vue index 40c1bb942..bd93a2625 100644 --- a/apps/web-ele/src/views/iot/rule/scene/form/configs/sub-condition-group-config.vue +++ b/apps/web-ele/src/views/iot/rule/scene/form/configs/sub-condition-group-config.vue @@ -8,6 +8,7 @@ import { IotRuleSceneTriggerConditionTypeEnum, } from '@vben/constants'; import { IconifyIcon } from '@vben/icons'; +import { getStableObjectKey } from '@vben/utils'; import { useVModel } from '@vueuse/core'; import { ElButton } from 'element-plus'; @@ -105,7 +106,7 @@ function updateCondition(
diff --git a/apps/web-ele/src/views/iot/rule/scene/form/configs/timer-condition-group-config.vue b/apps/web-ele/src/views/iot/rule/scene/form/configs/timer-condition-group-config.vue index 25665f795..6e4c46602 100644 --- a/apps/web-ele/src/views/iot/rule/scene/form/configs/timer-condition-group-config.vue +++ b/apps/web-ele/src/views/iot/rule/scene/form/configs/timer-condition-group-config.vue @@ -5,6 +5,7 @@ import { nextTick } from 'vue'; import { IotRuleSceneTriggerTypeEnum } from '@vben/constants'; import { IconifyIcon } from '@vben/icons'; +import { getStableObjectKey } from '@vben/utils'; import { useVModel } from '@vueuse/core'; import { ElButton, ElTag } from 'element-plus'; @@ -96,7 +97,7 @@ function updateConditionGroup( >
diff --git a/apps/web-ele/src/views/iot/rule/scene/form/sections/action-section.vue b/apps/web-ele/src/views/iot/rule/scene/form/sections/action-section.vue index b100b0211..fc6baeed4 100644 --- a/apps/web-ele/src/views/iot/rule/scene/form/sections/action-section.vue +++ b/apps/web-ele/src/views/iot/rule/scene/form/sections/action-section.vue @@ -8,6 +8,7 @@ import { IotRuleSceneActionTypeEnum, } from '@vben/constants'; import { IconifyIcon } from '@vben/icons'; +import { getStableObjectKey } from '@vben/utils'; import { useVModel } from '@vueuse/core'; import { @@ -199,7 +200,7 @@ function onActionTypeChange(action: RuleSceneApi.Action, type: number) {
diff --git a/apps/web-ele/src/views/iot/rule/scene/form/sections/trigger-section.vue b/apps/web-ele/src/views/iot/rule/scene/form/sections/trigger-section.vue index 3fb6058cb..ccb0a9fbf 100644 --- a/apps/web-ele/src/views/iot/rule/scene/form/sections/trigger-section.vue +++ b/apps/web-ele/src/views/iot/rule/scene/form/sections/trigger-section.vue @@ -9,6 +9,7 @@ import { isDeviceTrigger, } from '@vben/constants'; import { IconifyIcon } from '@vben/icons'; +import { getStableObjectKey } from '@vben/utils'; import { useVModel } from '@vueuse/core'; import { ElButton, ElCard, ElEmpty, ElFormItem, ElTag } from 'element-plus'; @@ -164,7 +165,7 @@ onMounted(() => {
diff --git a/apps/web-ele/src/views/iot/rule/scene/modules/form.vue b/apps/web-ele/src/views/iot/rule/scene/modules/form.vue index f3d552b65..6c9324d12 100644 --- a/apps/web-ele/src/views/iot/rule/scene/modules/form.vue +++ b/apps/web-ele/src/views/iot/rule/scene/modules/form.vue @@ -103,12 +103,14 @@ function buildEmptyFormData(): RuleSceneApi.SceneRule { /** 回显时兜底,保证触发器/执行器数组不为空 */ function normalizeFormData(result: any): RuleSceneApi.SceneRule { + const triggers: RuleSceneApi.Trigger[] = result.triggers?.length + ? result.triggers + : buildEmptyFormData().triggers!; + const actions: RuleSceneApi.Action[] = result.actions || []; return { ...result, - triggers: result.triggers?.length - ? result.triggers - : buildEmptyFormData().triggers, - actions: result.actions || [], + triggers, + actions, }; } diff --git a/packages/@core/base/shared/src/utils/util.ts b/packages/@core/base/shared/src/utils/util.ts index 4fbae5c7d..97d88b0d7 100644 --- a/packages/@core/base/shared/src/utils/util.ts +++ b/packages/@core/base/shared/src/utils/util.ts @@ -1,3 +1,5 @@ +import { buildShortUUID } from './uuid'; + export function bindMethods(instance: T): void { const prototype = Object.getPrototypeOf(instance); const propertyNames = Object.getOwnPropertyNames(prototype); @@ -114,3 +116,22 @@ export function jsonParse(str: string) { return str; } } + +const stableObjectKeyMap = new WeakMap(); + +/** + * 为对象引用生成稳定 key,不写入对象本身。 + * + * 适用于 v-for 使用对象或数组项作为渲染单位,但不希望把 UI 字段混入业务数据的场景。 + */ +export function getStableObjectKey( + item: object, + generator: () => string = buildShortUUID, +): string { + let key = stableObjectKeyMap.get(item); + if (!key) { + key = generator(); + stableObjectKeyMap.set(item, key); + } + return key; +}