Przeglądaj źródła

perf:【IoT 物联网】场景联动触发器优化

puhui999 10 miesięcy temu
rodzic
commit
a554bc5309

+ 7 - 44
src/api/iot/rule/scene/scene.types.ts

@@ -2,15 +2,7 @@
  * IoT 场景联动接口定义
  */
 
-// TODO @puhui999:枚举挪到 views/iot/utils/constants.ts 里
-// 枚举定义
-const IotRuleSceneTriggerTypeEnum = {
-  DEVICE_STATE_UPDATE: 1, // 设备上下线变更
-  DEVICE_PROPERTY_POST: 2, // 物模型属性上报
-  DEVICE_EVENT_POST: 3, // 设备事件上报
-  DEVICE_SERVICE_INVOKE: 4, // 设备服务调用
-  TIMER: 100 // 定时触发
-} as const
+// 枚举定义已迁移到 constants.ts,这里不再重复导出
 
 const IotRuleSceneActionTypeEnum = {
   DEVICE_PROPERTY_SET: 1, // 设备属性设置,
@@ -25,11 +17,7 @@ const IotDeviceMessageTypeEnum = {
   EVENT: 'event' // 事件
 } as const
 
-// TODO @puhui999:这个貌似可以不要?
-const IotDeviceMessageIdentifierEnum = {
-  PROPERTY_SET: 'set', // 属性设置
-  SERVICE_INVOKE: '${identifier}' // 服务调用
-} as const
+// 已删除不需要的 IotDeviceMessageIdentifierEnum
 
 const IotRuleSceneTriggerConditionParameterOperatorEnum = {
   EQUALS: { name: '等于', value: '=' }, // 等于
@@ -64,29 +52,10 @@ const IotRuleSceneTriggerTimeOperatorEnum = {
   TODAY: { name: '在今日之间', value: 'today' } // 在今日之间
 } as const
 
-// TODO @puhui999:下面 IotAlertConfigReceiveTypeEnum、DeviceStateEnum 没用到,貌似可以删除下?
-const IotAlertConfigReceiveTypeEnum = {
-  SMS: 1, // 短信
-  MAIL: 2, // 邮箱
-  NOTIFY: 3 // 通知
-} as const
-
-// 设备状态枚举
-const DeviceStateEnum = {
-  INACTIVE: 0, // 未激活
-  ONLINE: 1, // 在线
-  OFFLINE: 2 // 离线
-} as const
-
-// TODO @puhui999:这个全局已经有啦
-// 通用状态枚举
-const CommonStatusEnum = {
-  ENABLE: 0, // 开启
-  DISABLE: 1 // 关闭
-} as const
+// 已删除未使用的枚举:IotAlertConfigReceiveTypeEnum、DeviceStateEnum
+// CommonStatusEnum 已在全局定义,这里不再重复定义
 
-// 基础接口
-// TODO @puhui999:这个貌似可以不要?
+// 基础接口(如果项目中有全局的 BaseDO,可以使用全局的)
 interface TenantBaseDO {
   createTime?: Date // 创建时间
   updateTime?: Date // 更新时间
@@ -144,7 +113,7 @@ interface RuleSceneFormData {
   name: string
   description?: string
   status: number
-  trigger: TriggerFormData
+  triggers: TriggerFormData[] // 支持多个触发器
   actions: ActionFormData[]
 }
 
@@ -209,8 +178,7 @@ interface IotRuleScene extends TenantBaseDO {
 }
 
 // 工具类型 - 从枚举中提取类型
-export type TriggerType =
-  (typeof IotRuleSceneTriggerTypeEnum)[keyof typeof IotRuleSceneTriggerTypeEnum]
+// TriggerType 现在从 constants.ts 中的枚举提取
 export type ActionType =
   (typeof IotRuleSceneActionTypeEnum)[keyof typeof IotRuleSceneActionTypeEnum]
 export type MessageType = (typeof IotDeviceMessageTypeEnum)[keyof typeof IotDeviceMessageTypeEnum]
@@ -246,16 +214,11 @@ export {
   ConditionGroupContainerFormData,
   SubConditionGroupFormData,
   ConditionFormData,
-  IotRuleSceneTriggerTypeEnum,
   IotRuleSceneActionTypeEnum,
   IotDeviceMessageTypeEnum,
-  IotDeviceMessageIdentifierEnum,
   IotRuleSceneTriggerConditionParameterOperatorEnum,
   IotRuleSceneTriggerConditionTypeEnum,
   IotRuleSceneTriggerTimeOperatorEnum,
-  IotAlertConfigReceiveTypeEnum,
-  DeviceStateEnum,
-  CommonStatusEnum,
   ValidationRule,
   FormValidationRules
 }

+ 55 - 54
src/views/iot/rule/scene/form/RuleSceneForm.vue

@@ -9,12 +9,12 @@
     :close-on-press-escape="false"
     @close="handleClose"
   >
-    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
+    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="110px">
       <!-- 基础信息配置 -->
       <BasicInfoSection v-model="formData" :rules="formRules" />
 
       <!-- 触发器配置 -->
-      <TriggerSection v-model:trigger="formData.trigger" @validate="handleTriggerValidate" />
+      <TriggerSection v-model:triggers="formData.triggers" @validate="handleTriggerValidate" />
 
       <!-- 执行器配置 -->
       <ActionSection v-model:actions="formData.actions" @validate="handleActionValidate" />
@@ -40,15 +40,21 @@ import BasicInfoSection from './sections/BasicInfoSection.vue'
 import TriggerSection from './sections/TriggerSection.vue'
 import ActionSection from './sections/ActionSection.vue'
 import {
-  CommonStatusEnum,
   IotRuleScene,
   IotRuleSceneActionTypeEnum,
-  IotRuleSceneTriggerTypeEnum,
-  RuleSceneFormData
+  RuleSceneFormData,
+  TriggerFormData
 } from '@/api/iot/rule/scene/scene.types'
+import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
 import { ElMessage } from 'element-plus'
 import { generateUUID } from '@/utils'
 
+// 导入全局的 CommonStatusEnum
+const CommonStatusEnum = {
+  ENABLE: 0, // 开启
+  DISABLE: 1 // 关闭
+} as const
+
 /** IoT 场景联动规则表单 - 主表单组件 */
 defineOptions({ name: 'RuleSceneForm' })
 
@@ -76,17 +82,19 @@ const createDefaultFormData = (): RuleSceneFormData => {
     name: '',
     description: '',
     status: CommonStatusEnum.ENABLE, // 默认启用状态
-    trigger: {
-      type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
-      productId: undefined,
-      deviceId: undefined,
-      identifier: undefined,
-      operator: undefined,
-      value: undefined,
-      cronExpression: undefined,
-      mainCondition: undefined,
-      conditionGroup: undefined
-    },
+    triggers: [
+      {
+        type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+        productId: undefined,
+        deviceId: undefined,
+        identifier: undefined,
+        operator: undefined,
+        value: undefined,
+        cronExpression: undefined,
+        mainCondition: undefined,
+        conditionGroup: undefined
+      }
+    ],
     actions: []
   }
 }
@@ -95,13 +103,13 @@ const createDefaultFormData = (): RuleSceneFormData => {
  * 将表单数据转换为 API 请求格式
  */
 const convertFormToVO = (formData: RuleSceneFormData): IotRuleScene => {
-  // 构建触发器条件
-  const buildTriggerConditions = () => {
+  // 构建单个触发器条件
+  const buildTriggerConditions = (trigger: TriggerFormData) => {
     const conditions: any[] = []
 
     // 处理主条件
-    if (formData.trigger.mainCondition) {
-      const mainCondition = formData.trigger.mainCondition
+    if (trigger.mainCondition) {
+      const mainCondition = trigger.mainCondition
       conditions.push({
         type: mainCondition.type === 2 ? 'property' : 'event',
         identifier: mainCondition.identifier || '',
@@ -115,8 +123,8 @@ const convertFormToVO = (formData: RuleSceneFormData): IotRuleScene => {
     }
 
     // 处理条件组
-    if (formData.trigger.conditionGroup?.subGroups) {
-      formData.trigger.conditionGroup.subGroups.forEach((subGroup) => {
+    if (trigger.conditionGroup?.subGroups) {
+      trigger.conditionGroup.subGroups.forEach((subGroup) => {
         subGroup.conditions.forEach((condition) => {
           conditions.push({
             type: condition.type === 2 ? 'property' : 'event',
@@ -140,19 +148,13 @@ const convertFormToVO = (formData: RuleSceneFormData): IotRuleScene => {
     name: formData.name,
     description: formData.description,
     status: Number(formData.status),
-    triggers: [
-      {
-        type: formData.trigger.type,
-        productKey: formData.trigger.productId
-          ? `product_${formData.trigger.productId}`
-          : undefined,
-        deviceNames: formData.trigger.deviceId
-          ? [`device_${formData.trigger.deviceId}`]
-          : undefined,
-        cronExpression: formData.trigger.cronExpression,
-        conditions: buildTriggerConditions()
-      }
-    ],
+    triggers: formData.triggers.map((trigger) => ({
+      type: trigger.type,
+      productKey: trigger.productId ? `product_${trigger.productId}` : undefined,
+      deviceNames: trigger.deviceId ? [`device_${trigger.deviceId}`] : undefined,
+      cronExpression: trigger.cronExpression,
+      conditions: buildTriggerConditions(trigger)
+    })),
     actions:
       formData.actions?.map((action) => ({
         type: action.type,
@@ -180,9 +182,7 @@ const convertFormToVO = (formData: RuleSceneFormData): IotRuleScene => {
  * 将 API 响应数据转换为表单格式
  */
 const convertVOToForm = (apiData: IotRuleScene): RuleSceneFormData => {
-  const firstTrigger = apiData.triggers?.[0]
-
-  // 解析触发器条件
+  // 解析单个触发器的条件
   const parseConditions = (trigger: any) => {
     if (!trigger?.conditions?.length) {
       return {
@@ -208,28 +208,23 @@ const convertVOToForm = (apiData: IotRuleScene): RuleSceneFormData => {
     }
   }
 
-  const conditionData = firstTrigger
-    ? parseConditions(firstTrigger)
-    : {
-        mainCondition: undefined,
-        conditionGroup: undefined
-      }
-
-  return {
-    ...apiData,
-    status: Number(apiData.status),
-    trigger: firstTrigger
-      ? {
-          type: Number(firstTrigger.type),
+  // 转换所有触发器
+  const triggers = apiData.triggers?.length
+    ? apiData.triggers.map((trigger) => {
+        const conditionData = parseConditions(trigger)
+        return {
+          type: Number(trigger.type),
           productId: undefined, // 需要从 productKey 解析
           deviceId: undefined, // 需要从 deviceNames 解析
           identifier: undefined,
           operator: undefined,
           value: undefined,
-          cronExpression: firstTrigger.cronExpression,
+          cronExpression: trigger.cronExpression,
           ...conditionData
         }
-      : {
+      })
+    : [
+        {
           type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
           productId: undefined,
           deviceId: undefined,
@@ -239,7 +234,13 @@ const convertVOToForm = (apiData: IotRuleScene): RuleSceneFormData => {
           cronExpression: undefined,
           mainCondition: undefined,
           conditionGroup: undefined
-        },
+        }
+      ]
+
+  return {
+    ...apiData,
+    status: Number(apiData.status),
+    triggers,
     actions:
       apiData.actions?.map((action) => ({
         ...action,

+ 3 - 5
src/views/iot/rule/scene/form/configs/ConditionGroupConfig.vue

@@ -114,11 +114,8 @@
 <script setup lang="ts">
 import { useVModel } from '@vueuse/core'
 import ConditionConfig from './ConditionConfig.vue'
-import {
-  ConditionGroupFormData,
-  ConditionFormData,
-  IotRuleSceneTriggerTypeEnum
-} from '@/api/iot/rule/scene/scene.types'
+import { ConditionFormData, ConditionGroupFormData } from '@/api/iot/rule/scene/scene.types'
+import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
 
 /** 条件组配置组件 */
 defineOptions({ name: 'ConditionGroupConfig' })
@@ -133,6 +130,7 @@ interface Props {
 
 interface Emits {
   (e: 'update:modelValue', value: ConditionGroupFormData): void
+
   (e: 'validate', result: { valid: boolean; message: string }): void
 }
 

+ 9 - 14
src/views/iot/rule/scene/form/configs/DeviceTriggerConfig.vue

@@ -62,26 +62,21 @@ import { useVModel } from '@vueuse/core'
 
 import MainConditionConfig from './MainConditionConfig.vue'
 import ConditionGroupContainerConfig from './ConditionGroupContainerConfig.vue'
-import {
-  TriggerFormData,
-  IotRuleSceneTriggerTypeEnum as TriggerTypeEnum
-} from '@/api/iot/rule/scene/scene.types'
+import { TriggerFormData } from '@/api/iot/rule/scene/scene.types'
+import { IotRuleSceneTriggerTypeEnum as TriggerTypeEnum } from '@/views/iot/utils/constants'
 
 /** 设备触发配置组件 */
 defineOptions({ name: 'DeviceTriggerConfig' })
 
-// TODO @puhui999:下面的 Props、Emits 可以合并到变量那;
-interface Props {
+// Props 和 Emits 定义
+const props = defineProps<{
   modelValue: TriggerFormData
-}
-
-interface Emits {
-  (e: 'update:modelValue', value: TriggerFormData): void
-  (e: 'validate', result: { valid: boolean; message: string }): void
-}
+}>()
 
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+const emit = defineEmits<{
+  'update:modelValue': [value: TriggerFormData]
+  validate: [result: { valid: boolean; message: string }]
+}>()
 
 const trigger = useVModel(props, 'modelValue', emit)
 

+ 8 - 18
src/views/iot/rule/scene/form/configs/MainConditionConfig.vue

@@ -20,19 +20,13 @@
     </div>
 
     <!-- 主条件配置 -->
-    <!-- TODO @puhui999:这里可以简化下,主条件是不能删除的。。。 -->
     <div v-else class="space-y-16px">
       <div class="flex items-center justify-between">
         <div class="flex items-center gap-8px">
           <span class="text-14px font-500 text-[var(--el-text-color-primary)]">主条件</span>
           <el-tag size="small" type="primary">必须满足</el-tag>
         </div>
-        <el-button type="danger" size="small" text @click="removeMainCondition">
-          <Icon icon="ep:delete" />
-          删除
-        </el-button>
       </div>
-
       <MainConditionInnerConfig
         :model-value="modelValue"
         @update:model-value="updateCondition"
@@ -53,18 +47,14 @@ import {
 /** 主条件配置组件 */
 defineOptions({ name: 'MainConditionConfig' })
 
-interface Props {
+defineProps<{
   modelValue?: ConditionFormData
   triggerType: number
-}
-
-interface Emits {
+}>()
+const emit = defineEmits<{
   (e: 'update:modelValue', value?: ConditionFormData): void
   (e: 'validate', result: { valid: boolean; message: string }): void
-}
-
-defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 // 事件处理
 const addMainCondition = () => {
@@ -79,10 +69,6 @@ const addMainCondition = () => {
   emit('update:modelValue', newCondition)
 }
 
-const removeMainCondition = () => {
-  emit('update:modelValue', undefined)
-}
-
 const updateCondition = (condition: ConditionFormData) => {
   emit('update:modelValue', condition)
 }
@@ -90,4 +76,8 @@ const updateCondition = (condition: ConditionFormData) => {
 const handleValidate = (result: { valid: boolean; message: string }) => {
   emit('validate', result)
 }
+
+onMounted(() => {
+  addMainCondition()
+})
 </script>

+ 2 - 7
src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue

@@ -1,11 +1,5 @@
 <template>
   <div class="space-y-16px">
-    <!-- 触发事件类型显示 -->
-    <div class="flex items-center gap-8px mb-16px">
-      <span class="text-14px text-[var(--el-text-color-regular)]">触发事件类型:</span>
-      <el-tag size="small" type="primary">{{ getTriggerTypeText(triggerType) }}</el-tag>
-    </div>
-
     <!-- 设备属性条件配置 -->
     <div v-if="isDevicePropertyTrigger" class="space-y-16px">
       <!-- 产品设备选择 -->
@@ -112,7 +106,7 @@ import OperatorSelector from '../selectors/OperatorSelector.vue'
 import ValueInput from '../inputs/ValueInput.vue'
 import DeviceStatusConditionConfig from './DeviceStatusConditionConfig.vue'
 import { ConditionFormData } from '@/api/iot/rule/scene/scene.types'
-import { IotRuleSceneTriggerTypeEnum } from '@/api/iot/rule/scene/scene.types'
+import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
 import { useVModel } from '@vueuse/core'
 
 /** 主条件内部配置组件 */
@@ -125,6 +119,7 @@ interface Props {
 
 interface Emits {
   (e: 'update:modelValue', value: ConditionFormData): void
+
   (e: 'validate', result: { valid: boolean; message: string }): void
 }
 

+ 7 - 16
src/views/iot/rule/scene/form/sections/BasicInfoSection.vue

@@ -1,6 +1,6 @@
 <!-- 基础信息配置组件 -->
 <template>
-  <el-card class="border border-[var(--el-border-color-light)] rounded-8px" shadow="never">
+  <el-card class="border border-[var(--el-border-color-light)] rounded-8px mb-10px" shadow="never">
     <template #header>
       <div class="flex items-center justify-between">
         <div class="flex items-center gap-8px">
@@ -8,10 +8,7 @@
           <span class="text-16px font-600 text-[var(--el-text-color-primary)]">基础信息</span>
         </div>
         <div class="flex items-center gap-8px">
-          <!-- TODO @puhui999:dict-tag 可以哇? -->
-          <el-tag :type="formData.status === 0 ? 'success' : 'danger'" size="small">
-            {{ formData.status === 0 ? '启用' : '禁用' }}
-          </el-tag>
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData.status" />
         </div>
       </div>
     </template>
@@ -60,25 +57,19 @@
 
 <script setup lang="ts">
 import { useVModel } from '@vueuse/core'
-import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
 
 /** 基础信息配置组件 */
 defineOptions({ name: 'BasicInfoSection' })
 
-// TODO @puhui999:下面的 Props、Emits 可以合并到变量那;
-
-interface Props {
+const props = defineProps<{
   modelValue: RuleSceneFormData
   rules?: any
-}
-
-interface Emits {
+}>()
+const emit = defineEmits<{
   (e: 'update:modelValue', value: RuleSceneFormData): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 const formData = useVModel(props, 'modelValue', emit)
 </script>

+ 167 - 110
src/views/iot/rule/scene/form/sections/TriggerSection.vue

@@ -1,47 +1,97 @@
 <template>
-  <el-card class="border border-[var(--el-border-color-light)] rounded-8px" shadow="never">
-    <!-- TODO @puhui999:触发器还是多个。。。每个触发器里面有事件类型 + 附加条件组(最好文案上,和阿里 iot 保持相对一致) -->
+  <el-card class="border border-[var(--el-border-color-light)] rounded-8px mb-10px" shadow="never">
     <template #header>
-      <div class="flex items-center gap-8px">
-        <Icon icon="ep:lightning" class="text-[var(--el-color-primary)] text-18px" />
-        <span class="text-16px font-600 text-[var(--el-text-color-primary)]">触发器配置</span>
-        <el-tag size="small" type="info">场景触发器</el-tag>
+      <div class="flex items-center justify-between">
+        <div class="flex items-center gap-8px">
+          <Icon icon="ep:lightning" class="text-[var(--el-color-primary)] text-18px" />
+          <span class="text-16px font-600 text-[var(--el-text-color-primary)]">触发器配置</span>
+          <el-tag size="small" type="info">{{ triggers.length }} 个触发器</el-tag>
+        </div>
+        <el-button type="primary" size="small" @click="addTrigger">
+          <Icon icon="ep:plus" />
+          添加触发器
+        </el-button>
       </div>
     </template>
 
-    <div class="p-16px space-y-16px">
-      <!-- 触发事件类型选择 -->
-      <el-form-item label="触发事件类型" required>
-        <el-select
-          :model-value="trigger.type"
-          @update:model-value="(value) => updateTriggerType(value)"
-          @change="onTriggerTypeChange"
-          placeholder="请选择触发事件类型"
-          class="w-full"
+    <div class="p-16px space-y-24px">
+      <!-- 触发器列表 -->
+      <div v-if="triggers.length > 0" class="space-y-24px">
+        <div
+          v-for="(triggerItem, index) in triggers"
+          :key="`trigger-${index}`"
+          class="border border-[var(--el-border-color-light)] rounded-8px p-16px relative"
         >
-          <el-option
-            v-for="option in triggerTypeOptions"
-            :key="option.value"
-            :label="option.label"
-            :value="option.value"
+          <!-- 触发器头部 -->
+          <div class="flex items-center justify-between mb-16px">
+            <div class="flex items-center gap-8px">
+              <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
+                触发器 {{ index + 1 }}
+              </span>
+              <el-tag size="small" :type="getTriggerTagType(triggerItem.type)">
+                {{ getTriggerTypeLabel(triggerItem.type) }}
+              </el-tag>
+            </div>
+            <div class="flex items-center gap-8px">
+              <el-button
+                v-if="triggers.length > 1"
+                type="danger"
+                size="small"
+                text
+                @click="removeTrigger(index)"
+              >
+                <Icon icon="ep:delete" />
+                删除
+              </el-button>
+            </div>
+          </div>
+
+          <!-- 触发事件类型选择 -->
+          <el-form-item label="触发事件类型" required>
+            <el-select
+              :model-value="triggerItem.type"
+              @update:model-value="(value) => updateTriggerType(index, value)"
+              placeholder="请选择触发事件类型"
+              class="w-full"
+            >
+              <el-option
+                v-for="option in triggerTypeOptions"
+                :key="option.value"
+                :label="option.label"
+                :value="option.value"
+              />
+            </el-select>
+          </el-form-item>
+
+          <!-- 设备触发配置 -->
+          <DeviceTriggerConfig
+            v-if="isDeviceTrigger(triggerItem.type)"
+            :model-value="triggerItem"
+            @update:model-value="(value) => updateTriggerDeviceConfig(index, value)"
           />
-        </el-select>
-      </el-form-item>
-
-      <!-- 设备触发配置 -->
-      <DeviceTriggerConfig
-        v-if="isDeviceTrigger(trigger.type)"
-        :model-value="trigger"
-        @update:model-value="updateTrigger"
-      />
-
-      <!-- 定时触发配置 -->
-      <!-- TODO @puhui999:这里要不 v-else 好了? -->
-      <TimerTriggerConfig
-        v-if="trigger.type === TriggerTypeEnum.TIMER"
-        :model-value="trigger.cronExpression"
-        @update:model-value="updateTriggerCronExpression"
-      />
+
+          <!-- 定时触发配置 -->
+          <TimerTriggerConfig
+            v-else-if="triggerItem.type === TriggerTypeEnum.TIMER"
+            :model-value="triggerItem.cronExpression"
+            @update:model-value="(value) => updateTriggerCronConfig(index, value)"
+          />
+        </div>
+      </div>
+
+      <!-- 空状态 -->
+      <div v-else class="py-40px text-center">
+        <el-empty description="暂无触发器">
+          <template #description>
+            <div class="space-y-8px">
+              <p class="text-[var(--el-text-color-secondary)]">暂无触发器配置</p>
+              <p class="text-12px text-[var(--el-text-color-placeholder)]">
+                请使用上方的"添加触发器"按钮来设置触发规则
+              </p>
+            </div>
+          </template>
+        </el-empty>
+      </div>
     </div>
   </el-card>
 </template>
@@ -50,108 +100,115 @@
 import { useVModel } from '@vueuse/core'
 import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue'
 import TimerTriggerConfig from '../configs/TimerTriggerConfig.vue'
+import { TriggerFormData } from '@/api/iot/rule/scene/scene.types'
 import {
-  TriggerFormData,
-  IotRuleSceneTriggerTypeEnum as TriggerTypeEnum
-} from '@/api/iot/rule/scene/scene.types'
+  getTriggerTypeOptions,
+  IotRuleSceneTriggerTypeEnum as TriggerTypeEnum,
+  IotRuleSceneTriggerTypeEnum,
+  isDeviceTrigger
+} from '@/views/iot/utils/constants'
 
 /** 触发器配置组件 */
 defineOptions({ name: 'TriggerSection' })
 
-// TODO @puhui999:下面的 Props、Emits 可以合并到变量那;
-interface Props {
-  trigger: TriggerFormData
+// Props 和 Emits 定义
+const props = defineProps<{
+  triggers: TriggerFormData[]
+}>()
+
+const emit = defineEmits<{
+  'update:triggers': [value: TriggerFormData[]]
+}>()
+
+const triggers = useVModel(props, 'triggers', emit)
+
+// 触发器类型选项(从 constants 中获取)
+const triggerTypeOptions = getTriggerTypeOptions()
+
+// 工具函数
+const getTriggerTypeLabel = (type: number): string => {
+  const option = triggerTypeOptions.find((opt) => opt.value === type)
+  return option?.label || '未知类型'
 }
 
-interface Emits {
-  (e: 'update:trigger', value: TriggerFormData): void
+const getTriggerTagType = (type: number): string => {
+  if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
+    return 'warning'
+  }
+  return isDeviceTrigger(type) ? 'success' : 'info'
 }
 
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
-
-const trigger = useVModel(props, 'trigger', emit)
-
-// 触发器类型选项
-// TODO @puhui999:/Users/yunai/Java/yudao-ui-admin-vue3/src/views/iot/utils/constants.ts
-const triggerTypeOptions = [
-  {
-    value: TriggerTypeEnum.DEVICE_STATE_UPDATE,
-    label: '设备状态变更'
-  },
-  {
-    value: TriggerTypeEnum.DEVICE_PROPERTY_POST,
-    label: '设备属性上报'
-  },
-  {
-    value: TriggerTypeEnum.DEVICE_EVENT_POST,
-    label: '设备事件上报'
-  },
-  {
-    value: TriggerTypeEnum.DEVICE_SERVICE_INVOKE,
-    label: '设备服务调用'
-  },
-  {
-    value: TriggerTypeEnum.TIMER,
-    label: '定时触发'
+// 事件处理函数
+const addTrigger = () => {
+  const newTrigger: TriggerFormData = {
+    type: TriggerTypeEnum.DEVICE_STATE_UPDATE,
+    productId: undefined,
+    deviceId: undefined,
+    identifier: undefined,
+    operator: undefined,
+    value: undefined,
+    cronExpression: undefined,
+    mainCondition: undefined,
+    conditionGroup: undefined
   }
-]
+  triggers.value.push(newTrigger)
+}
 
-// 工具函数
-// TODO @puhui999:/Users/yunai/Java/yudao-ui-admin-vue3/src/views/iot/utils/constants.ts
-const isDeviceTrigger = (type: number) => {
-  const deviceTriggerTypes = [
-    TriggerTypeEnum.DEVICE_STATE_UPDATE,
-    TriggerTypeEnum.DEVICE_PROPERTY_POST,
-    TriggerTypeEnum.DEVICE_EVENT_POST,
-    TriggerTypeEnum.DEVICE_SERVICE_INVOKE
-  ] as number[]
-  return deviceTriggerTypes.includes(type)
+const removeTrigger = (index: number) => {
+  if (triggers.value.length > 1) {
+    triggers.value.splice(index, 1)
+  }
 }
 
-// 事件处理
-const updateTriggerType = (type: number) => {
-  trigger.value.type = type
-  onTriggerTypeChange(type)
+const updateTriggerType = (index: number, type: number) => {
+  triggers.value[index].type = type
+  onTriggerTypeChange(index, type)
 }
 
-// TODO @puhui999:updateTriggerDeviceConfig
-const updateTrigger = (newTrigger: TriggerFormData) => {
-  trigger.value = newTrigger
+const updateTriggerDeviceConfig = (index: number, newTrigger: TriggerFormData) => {
+  triggers.value[index] = newTrigger
 }
 
-// TODO @puhui999:updateTriggerCronConfig
-const updateTriggerCronExpression = (cronExpression?: string) => {
-  trigger.value.cronExpression = cronExpression
+const updateTriggerCronConfig = (index: number, cronExpression?: string) => {
+  triggers.value[index].cronExpression = cronExpression
 }
 
-const onTriggerTypeChange = (type: number) => {
+const onTriggerTypeChange = (index: number, type: number) => {
+  const triggerItem = triggers.value[index]
+
   // 清理不相关的配置
   if (type === TriggerTypeEnum.TIMER) {
-    trigger.value.productId = undefined
-    trigger.value.deviceId = undefined
-    trigger.value.identifier = undefined
-    trigger.value.operator = undefined
-    trigger.value.value = undefined
-    trigger.value.mainCondition = undefined
-    trigger.value.conditionGroup = undefined
-    if (!trigger.value.cronExpression) {
-      trigger.value.cronExpression = '0 0 12 * * ?'
+    triggerItem.productId = undefined
+    triggerItem.deviceId = undefined
+    triggerItem.identifier = undefined
+    triggerItem.operator = undefined
+    triggerItem.value = undefined
+    triggerItem.mainCondition = undefined
+    triggerItem.conditionGroup = undefined
+    if (!triggerItem.cronExpression) {
+      triggerItem.cronExpression = '0 0 12 * * ?'
     }
   } else {
-    trigger.value.cronExpression = undefined
+    triggerItem.cronExpression = undefined
     if (type === TriggerTypeEnum.DEVICE_STATE_UPDATE) {
-      trigger.value.mainCondition = undefined
-      trigger.value.conditionGroup = undefined
+      triggerItem.mainCondition = undefined
+      triggerItem.conditionGroup = undefined
     } else {
       // 设备属性、事件、服务触发需要条件配置
-      if (!trigger.value.mainCondition) {
-        trigger.value.mainCondition = undefined // 等待用户配置
+      if (!triggerItem.mainCondition) {
+        triggerItem.mainCondition = undefined // 等待用户配置
       }
-      if (!trigger.value.conditionGroup) {
-        trigger.value.conditionGroup = undefined // 可选的条件组
+      if (!triggerItem.conditionGroup) {
+        triggerItem.conditionGroup = undefined // 可选的条件组
       }
     }
   }
 }
+
+// 初始化:确保至少有一个触发器
+onMounted(() => {
+  if (triggers.value.length === 0) {
+    addTrigger()
+  }
+})
 </script>

+ 12 - 16
src/views/iot/rule/scene/form/selectors/OperatorSelector.vue

@@ -15,26 +15,21 @@
       >
         <div class="flex items-center justify-between w-full py-4px">
           <div class="flex items-center gap-8px">
-            <div class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ operator.label }}</div>
-            <div class="text-12px text-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)] px-6px py-2px rounded-4px font-mono">{{ operator.symbol }}</div>
+            <div class="text-14px font-500 text-[var(--el-text-color-primary)]">
+              {{ operator.label }}
+            </div>
+            <div
+              class="text-12px text-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)] px-6px py-2px rounded-4px font-mono"
+            >
+              {{ operator.symbol }}
+            </div>
+          </div>
+          <div class="text-12px text-[var(--el-text-color-secondary)]">
+            {{ operator.description }}
           </div>
-          <div class="text-12px text-[var(--el-text-color-secondary)]">{{ operator.description }}</div>
         </div>
       </el-option>
     </el-select>
-
-    <!-- 操作符说明 -->
-    <!-- TODO @puhui999:这个去掉 -->
-    <div v-if="selectedOperator" class="mt-8px p-8px bg-[var(--el-fill-color-light)] rounded-4px border border-[var(--el-border-color-lighter)]">
-      <div class="flex items-center gap-6px">
-        <Icon icon="ep:info-filled" class="text-12px text-[var(--el-color-info)]" />
-        <span class="text-12px text-[var(--el-text-color-secondary)]">{{ selectedOperator.description }}</span>
-      </div>
-      <div v-if="selectedOperator.example" class="flex items-center gap-6px mt-4px">
-        <span class="text-12px text-[var(--el-text-color-secondary)]">示例:</span>
-        <code class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] px-4px py-2px rounded-2px font-mono">{{ selectedOperator.example }}</code>
-      </div>
-    </div>
   </div>
 </template>
 
@@ -51,6 +46,7 @@ interface Props {
 
 interface Emits {
   (e: 'update:modelValue', value: string): void
+
   (e: 'change', value: string): void
 }
 

+ 236 - 32
src/views/iot/rule/scene/form/selectors/PropertySelector.vue

@@ -1,14 +1,14 @@
 <!-- 属性选择器组件 -->
 <!-- TODO @yunai:可能要在 review 下 -->
 <template>
-  <div class="w-full">
+  <div class="flex items-center gap-8px">
     <el-select
       v-model="localValue"
       placeholder="请选择监控项"
       filterable
       clearable
       @change="handleChange"
-      class="w-full"
+      class="!w-150px"
       :loading="loading"
     >
       <el-option-group v-for="group in propertyGroups" :key="group.label" :label="group.label">
@@ -20,8 +20,12 @@
         >
           <div class="flex items-center justify-between w-full py-4px">
             <div class="flex-1">
-              <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ property.name }}</div>
-              <div class="text-12px text-[var(--el-text-color-secondary)]">{{ property.identifier }}</div>
+              <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">
+                {{ property.name }}
+              </div>
+              <div class="text-12px text-[var(--el-text-color-secondary)]">
+                {{ property.identifier }}
+              </div>
             </div>
             <div class="flex-shrink-0">
               <el-tag :type="getPropertyTypeTag(property.dataType)" size="small">
@@ -33,42 +37,98 @@
       </el-option-group>
     </el-select>
 
-    <!-- 属性详情 -->
-    <div v-if="selectedProperty" class="mt-16px p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]">
-      <div class="flex items-center gap-8px mb-12px">
-        <Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-16px" />
-        <span class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ selectedProperty.name }}</span>
-        <el-tag :type="getPropertyTypeTag(selectedProperty.dataType)" size="small">
-          {{ getPropertyTypeName(selectedProperty.dataType) }}
-        </el-tag>
-      </div>
-      <div class="space-y-8px ml-24px">
-        <div class="flex items-start gap-8px">
-          <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">标识符:</span>
-          <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.identifier }}</span>
-        </div>
-        <div v-if="selectedProperty.description" class="flex items-start gap-8px">
-          <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">描述:</span>
-          <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.description }}</span>
-        </div>
-        <div v-if="selectedProperty.unit" class="flex items-start gap-8px">
-          <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">单位:</span>
-          <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.unit }}</span>
-        </div>
-        <div v-if="selectedProperty.range" class="flex items-start gap-8px">
-          <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">取值范围:</span>
-          <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.range }}</span>
+    <!-- 属性详情触发按钮 -->
+    <div class="relative">
+      <el-button
+        v-if="selectedProperty"
+        ref="detailTriggerRef"
+        type="info"
+        :icon="InfoFilled"
+        circle
+        size="small"
+        @click="togglePropertyDetail"
+        class="flex-shrink-0"
+        title="查看属性详情"
+      />
+
+      <!-- 属性详情弹出层 -->
+      <Teleport to="body">
+        <div
+          v-if="showPropertyDetail && selectedProperty"
+          ref="propertyDetailRef"
+          class="property-detail-popover"
+          :style="popoverStyle"
+        >
+          <div
+            class="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-300px max-w-400px"
+          >
+            <div class="flex items-center gap-8px mb-12px">
+              <Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-4px" />
+              <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
+                {{ selectedProperty.name }}
+              </span>
+              <el-tag :type="getPropertyTypeTag(selectedProperty.dataType)" size="small">
+                {{ getPropertyTypeName(selectedProperty.dataType) }}
+              </el-tag>
+            </div>
+            <div class="space-y-8px ml-24px">
+              <div class="flex items-start gap-8px">
+                <span
+                  class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
+                >
+                  标识符:
+                </span>
+                <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+                  {{ selectedProperty.identifier }}
+                </span>
+              </div>
+              <div v-if="selectedProperty.description" class="flex items-start gap-8px">
+                <span
+                  class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
+                >
+                  描述:
+                </span>
+                <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+                  {{ selectedProperty.description }}
+                </span>
+              </div>
+              <div v-if="selectedProperty.unit" class="flex items-start gap-8px">
+                <span
+                  class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
+                >
+                  单位:
+                </span>
+                <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+                  {{ selectedProperty.unit }}
+                </span>
+              </div>
+              <div v-if="selectedProperty.range" class="flex items-start gap-8px">
+                <span
+                  class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
+                >
+                  取值范围:
+                </span>
+                <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+                  {{ selectedProperty.range }}
+                </span>
+              </div>
+            </div>
+            <!-- 关闭按钮 -->
+            <div class="flex justify-end mt-12px">
+              <el-button size="small" @click="hidePropertyDetail">关闭</el-button>
+            </div>
+          </div>
         </div>
-      </div>
+      </Teleport>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
 import { useVModel } from '@vueuse/core'
-import { IotRuleSceneTriggerTypeEnum } from '@/api/iot/rule/scene/scene.types'
+import { InfoFilled } from '@element-plus/icons-vue'
+import { IotRuleSceneTriggerTypeEnum, IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
 import { ThingModelApi } from '@/api/iot/thingmodel'
-import { IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
 import type { IotThingModelTSLRespVO, PropertySelectorItem } from './types'
 
 /** 属性选择器组件 */
@@ -83,6 +143,7 @@ interface Props {
 
 interface Emits {
   (e: 'update:modelValue', value: string): void
+
   (e: 'change', value: { type: string; config: any }): void
 }
 
@@ -96,6 +157,25 @@ const loading = ref(false)
 const propertyList = ref<PropertySelectorItem[]>([])
 const thingModelTSL = ref<IotThingModelTSLRespVO | null>(null)
 
+// 属性详情弹出层相关状态
+const showPropertyDetail = ref(false)
+const detailTriggerRef = ref()
+const propertyDetailRef = ref()
+const popoverStyle = ref({})
+
+// 点击外部关闭弹出层
+const handleClickOutside = (event: MouseEvent) => {
+  if (
+    showPropertyDetail.value &&
+    propertyDetailRef.value &&
+    detailTriggerRef.value &&
+    !propertyDetailRef.value.contains(event.target as Node) &&
+    !detailTriggerRef.value.$el.contains(event.target as Node)
+  ) {
+    hidePropertyDetail()
+  }
+}
+
 // 计算属性
 const propertyGroups = computed(() => {
   const groups: { label: string; options: any[] }[] = []
@@ -159,6 +239,67 @@ const getPropertyTypeTag = (dataType: string) => {
   return tagMap[dataType] || 'info'
 }
 
+// 弹出层控制方法
+const togglePropertyDetail = () => {
+  if (showPropertyDetail.value) {
+    hidePropertyDetail()
+  } else {
+    showPropertyDetailPopover()
+  }
+}
+
+const showPropertyDetailPopover = () => {
+  if (!selectedProperty.value || !detailTriggerRef.value) return
+
+  showPropertyDetail.value = true
+
+  nextTick(() => {
+    updatePopoverPosition()
+  })
+}
+
+const hidePropertyDetail = () => {
+  showPropertyDetail.value = false
+}
+
+const updatePopoverPosition = () => {
+  if (!detailTriggerRef.value || !propertyDetailRef.value) return
+
+  const triggerEl = detailTriggerRef.value.$el
+  const triggerRect = triggerEl.getBoundingClientRect()
+  const popoverEl = propertyDetailRef.value
+
+  // 计算弹出层位置
+  const left = triggerRect.left + triggerRect.width + 8
+  const top = triggerRect.top
+
+  // 检查是否超出视窗右边界
+  const popoverWidth = 400 // 最大宽度
+  const viewportWidth = window.innerWidth
+
+  let finalLeft = left
+  if (left + popoverWidth > viewportWidth - 16) {
+    // 如果超出右边界,显示在左侧
+    finalLeft = triggerRect.left - popoverWidth - 8
+  }
+
+  // 检查是否超出视窗下边界
+  let finalTop = top
+  const popoverHeight = popoverEl.offsetHeight || 200
+  const viewportHeight = window.innerHeight
+
+  if (top + popoverHeight > viewportHeight - 16) {
+    finalTop = Math.max(16, viewportHeight - popoverHeight - 16)
+  }
+
+  popoverStyle.value = {
+    position: 'fixed',
+    left: `${finalLeft}px`,
+    top: `${finalTop}px`,
+    zIndex: 9999
+  }
+}
+
 // 事件处理
 const handleChange = (value: string) => {
   const property = propertyList.value.find((p) => p.identifier === value)
@@ -168,6 +309,8 @@ const handleChange = (value: string) => {
       config: property
     })
   }
+  // 选择变化时隐藏详情弹出层
+  hidePropertyDetail()
 }
 
 // 获取物模型TSL数据
@@ -331,13 +474,74 @@ watch(
   () => props.triggerType,
   () => {
     localValue.value = ''
+    hidePropertyDetail()
   }
 )
+
+// 监听窗口大小变化,重新计算弹出层位置
+const handleResize = () => {
+  if (showPropertyDetail.value) {
+    updatePopoverPosition()
+  }
+}
+
+// 生命周期
+onMounted(() => {
+  document.addEventListener('click', handleClickOutside)
+  window.addEventListener('resize', handleResize)
+})
+
+onUnmounted(() => {
+  document.removeEventListener('click', handleClickOutside)
+  window.removeEventListener('resize', handleResize)
+})
 </script>
 
 <style scoped>
+@keyframes fadeInScale {
+  from {
+    opacity: 0;
+    transform: scale(0.9) translateY(-4px);
+  }
+
+  to {
+    opacity: 1;
+    transform: scale(1) translateY(0);
+  }
+}
+
 :deep(.el-select-dropdown__item) {
   height: auto;
   padding: 8px 20px;
 }
+
+.property-detail-popover {
+  animation: fadeInScale 0.2s ease-out;
+  transform-origin: top left;
+}
+
+/* 弹出层箭头效果(可选) */
+.property-detail-popover::before {
+  position: absolute;
+  top: 20px;
+  left: -8px;
+  width: 0;
+  height: 0;
+  border-top: 8px solid transparent;
+  border-right: 8px solid var(--el-border-color);
+  border-bottom: 8px solid transparent;
+  content: '';
+}
+
+.property-detail-popover::after {
+  position: absolute;
+  top: 20px;
+  left: -7px;
+  width: 0;
+  height: 0;
+  border-top: 8px solid transparent;
+  border-right: 8px solid white;
+  border-bottom: 8px solid transparent;
+  content: '';
+}
 </style>

+ 46 - 0
src/views/iot/utils/constants.ts

@@ -203,3 +203,49 @@ export const IoTOtaTaskRecordStatusEnum = {
     value: 50
   }
 } as const
+
+// ========== 场景联动规则相关常量 ==========
+
+/** IoT 场景联动触发器类型枚举 */
+export const IotRuleSceneTriggerTypeEnum = {
+  DEVICE_STATE_UPDATE: 1, // 设备上下线变更
+  DEVICE_PROPERTY_POST: 2, // 物模型属性上报
+  DEVICE_EVENT_POST: 3, // 设备事件上报
+  DEVICE_SERVICE_INVOKE: 4, // 设备服务调用
+  TIMER: 100 // 定时触发
+} as const
+
+/** 触发器类型选项配置 */
+export const getTriggerTypeOptions = () => [
+  {
+    value: IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
+    label: '设备状态变更'
+  },
+  {
+    value: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+    label: '设备属性上报'
+  },
+  {
+    value: IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
+    label: '设备事件上报'
+  },
+  {
+    value: IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
+    label: '设备服务调用'
+  },
+  {
+    value: IotRuleSceneTriggerTypeEnum.TIMER,
+    label: '定时触发'
+  }
+]
+
+/** 判断是否为设备触发器类型 */
+export const isDeviceTrigger = (type: number): boolean => {
+  const deviceTriggerTypes = [
+    IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
+    IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+    IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
+    IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+  ] as number[]
+  return deviceTriggerTypes.includes(type)
+}