fix(iot): 修复场景联动动态列表 key 与校验问题
- 新增 getStableObjectKey,统一处理对象列表 v-for 稳定 key - 场景联动触发器、执行器、条件组、条件项改用稳定对象 key - 保持场景规则 API 类型不包含 UI 专用 _key 字段 - 修复场景联动触发器/执行器校验与地图详情跳转pull/348/head
parent
aeff25209d
commit
6fb45f1ded
|
|
@ -24,7 +24,7 @@ export namespace RuleSceneApi {
|
|||
operator?: string;
|
||||
value?: any;
|
||||
cronExpression?: string;
|
||||
conditionGroups?: TriggerCondition[][];
|
||||
conditionGroups?: TriggerCondition[][]; // 后端结构:List<List<TriggerCondition>>;外层「或」、组内「且」
|
||||
}
|
||||
|
||||
/** 场景联动规则的触发条件 */
|
||||
|
|
|
|||
|
|
@ -19,21 +19,29 @@ export namespace MesDvMaintenRecordLineApi {
|
|||
|
||||
/** 查询设备保养记录明细分页 */
|
||||
export function getMaintenRecordLinePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<MesDvMaintenRecordLineApi.MaintenRecordLine>>('/mes/dv/mainten-record-line/page', { params });
|
||||
return requestClient.get<
|
||||
PageResult<MesDvMaintenRecordLineApi.MaintenRecordLine>
|
||||
>('/mes/dv/mainten-record-line/page', { params });
|
||||
}
|
||||
|
||||
/** 查询设备保养记录明细详情 */
|
||||
export function getMaintenRecordLine(id: number) {
|
||||
return requestClient.get<MesDvMaintenRecordLineApi.MaintenRecordLine>(`/mes/dv/mainten-record-line/get?id=${id}`);
|
||||
return requestClient.get<MesDvMaintenRecordLineApi.MaintenRecordLine>(
|
||||
`/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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<div class="relative">
|
||||
<div
|
||||
v-for="(subGroup, subGroupIndex) in trigger.conditionGroups"
|
||||
:key="`sub-group-${subGroupIndex}`"
|
||||
:key="getStableObjectKey(subGroup)"
|
||||
class="relative"
|
||||
>
|
||||
<!-- 子条件组容器(橙色主题) -->
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
<div v-else class="space-y-4">
|
||||
<div
|
||||
v-for="(condition, conditionIndex) in subGroup"
|
||||
:key="`condition-${conditionIndex}`"
|
||||
:key="getStableObjectKey(condition)"
|
||||
class="relative"
|
||||
>
|
||||
<!-- 条件配置 -->
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
>
|
||||
<div
|
||||
v-for="(group, groupIndex) in conditionGroups"
|
||||
:key="`group-${groupIndex}`"
|
||||
:key="getStableObjectKey(group)"
|
||||
class="relative"
|
||||
>
|
||||
<!-- 条件组容器(橙色主题) -->
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<div v-else class="space-y-[24px]">
|
||||
<div
|
||||
v-for="(action, index) in actions"
|
||||
:key="`action-${index}`"
|
||||
:key="getStableObjectKey(action)"
|
||||
class="rounded-lg border border-blue-200 bg-blue-50/40 shadow-sm transition-shadow hover:shadow-md dark:border-blue-900/40 dark:bg-blue-950/20"
|
||||
>
|
||||
<!-- 执行器头部(蓝色主题) -->
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
|||
<div v-if="triggers.length > 0" class="space-y-[24px]">
|
||||
<div
|
||||
v-for="(triggerItem, index) in triggers"
|
||||
:key="`trigger-${index}`"
|
||||
:key="getStableObjectKey(triggerItem)"
|
||||
class="rounded-[8px] border border-green-200 bg-green-50/40 shadow-sm transition-shadow hover:shadow-md dark:border-green-900/40 dark:bg-green-950/20"
|
||||
>
|
||||
<!-- 触发器头部(绿色主题) -->
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,7 @@ export namespace RuleSceneApi {
|
|||
operator?: string;
|
||||
value?: any;
|
||||
cronExpression?: string;
|
||||
// 后端结构:List<List<TriggerCondition>>;外层「或」、组内「且」
|
||||
conditionGroups?: TriggerCondition[][];
|
||||
conditionGroups?: TriggerCondition[][]; // 后端结构:List<List<TriggerCondition>>;外层「或」、组内「且」
|
||||
}
|
||||
|
||||
/** 场景联动规则的触发条件 */
|
||||
|
|
|
|||
|
|
@ -19,21 +19,29 @@ export namespace MesDvMaintenRecordLineApi {
|
|||
|
||||
/** 查询设备保养记录明细分页 */
|
||||
export function getMaintenRecordLinePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<MesDvMaintenRecordLineApi.MaintenRecordLine>>('/mes/dv/mainten-record-line/page', { params });
|
||||
return requestClient.get<
|
||||
PageResult<MesDvMaintenRecordLineApi.MaintenRecordLine>
|
||||
>('/mes/dv/mainten-record-line/page', { params });
|
||||
}
|
||||
|
||||
/** 查询设备保养记录明细详情 */
|
||||
export function getMaintenRecordLine(id: number) {
|
||||
return requestClient.get<MesDvMaintenRecordLineApi.MaintenRecordLine>(`/mes/dv/mainten-record-line/get?id=${id}`);
|
||||
return requestClient.get<MesDvMaintenRecordLineApi.MaintenRecordLine>(
|
||||
`/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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<div class="relative">
|
||||
<div
|
||||
v-for="(subGroup, subGroupIndex) in trigger.conditionGroups"
|
||||
:key="`sub-group-${subGroupIndex}`"
|
||||
:key="getStableObjectKey(subGroup)"
|
||||
class="relative"
|
||||
>
|
||||
<!-- 子条件组容器 -->
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
<div v-else class="space-y-4">
|
||||
<div
|
||||
v-for="(condition, conditionIndex) in subGroup"
|
||||
:key="`condition-${conditionIndex}`"
|
||||
:key="getStableObjectKey(condition)"
|
||||
class="relative"
|
||||
>
|
||||
<!-- 条件配置 -->
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
>
|
||||
<div
|
||||
v-for="(group, groupIndex) in conditionGroups"
|
||||
:key="`group-${groupIndex}`"
|
||||
:key="getStableObjectKey(group)"
|
||||
class="relative"
|
||||
>
|
||||
<!-- 条件组容器(橙色主题) -->
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<div v-else class="space-y-[24px]">
|
||||
<div
|
||||
v-for="(action, index) in actions"
|
||||
:key="`action-${index}`"
|
||||
:key="getStableObjectKey(action)"
|
||||
class="rounded-lg border-2 border-blue-200 bg-blue-50 shadow-sm transition-shadow hover:shadow-md"
|
||||
>
|
||||
<!-- 执行器头部 - 蓝色主题 -->
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
|||
<div v-if="triggers.length > 0" class="space-y-[24px]">
|
||||
<div
|
||||
v-for="(triggerItem, index) in triggers"
|
||||
:key="`trigger-${index}`"
|
||||
:key="getStableObjectKey(triggerItem)"
|
||||
class="rounded-[8px] border-2 border-green-200 bg-green-50 shadow-sm transition-shadow hover:shadow-md"
|
||||
>
|
||||
<!-- 触发器头部 - 绿色主题 -->
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { buildShortUUID } from './uuid';
|
||||
|
||||
export function bindMethods<T extends object>(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<object, string>();
|
||||
|
||||
/**
|
||||
* 为对象引用生成稳定 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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue