Browse Source

!790 perf:【IoT 物联网】场景联动优化
Merge pull request !790 from puhui999/feature/iot

芋道源码 1 năm trước cách đây
mục cha
commit
c427031e5a

+ 10 - 14
src/api/iot/rule/scene/scene.types.ts

@@ -4,13 +4,18 @@
 
 // 枚举定义
 const IotRuleSceneTriggerTypeEnum = {
-  DEVICE: 1, // 设备触发
-  TIMER: 2 // 定时触发
+  DEVICE_STATE_UPDATE: 1, // 设备上下线变更
+  DEVICE_PROPERTY_POST: 2, // 物模型属性上报
+  DEVICE_EVENT_POST: 3, // 设备事件上报
+  DEVICE_SERVICE_INVOKE: 4, // 设备服务调用
+  TIMER: 100 // 定时触发
 } as const
 
 const IotRuleSceneActionTypeEnum = {
-  DEVICE_CONTROL: 1, // 设备执行
-  ALERT: 2 // 告警执行
+  DEVICE_PROPERTY_SET: 1, // 设备属性设置,
+  DEVICE_SERVICE_INVOKE: 2, // 设备服务调用
+  ALERT_TRIGGER: 100, // 告警触发
+  ALERT_RECOVER: 101 // 告警恢复
 } as const
 
 const IotDeviceMessageTypeEnum = {
@@ -89,20 +94,12 @@ interface ActionDeviceControl {
   data: Record<string, any> // 具体数据
 }
 
-// 告警执行配置
-interface ActionAlert {
-  receiveType: number // 接收方式
-  phoneNumbers?: string[] // 手机号列表
-  emails?: string[] // 邮箱列表
-  content: string // 通知内容
-}
-
 // 执行器配置
 interface ActionConfig {
   key: any // 解决组件索引重用 TODO @puhui999:看看有没更好的解决方案呢。
   type: number // 执行类型
   deviceControl?: ActionDeviceControl // 设备控制
-  alert?: ActionAlert // 告警执行
+  alertConfigId?: number // 告警配置ID(告警恢复时需要)
 }
 
 // 主接口
@@ -122,7 +119,6 @@ export {
   TriggerConditionParameter,
   ActionConfig,
   ActionDeviceControl,
-  ActionAlert,
   IotRuleSceneTriggerTypeEnum,
   IotRuleSceneActionTypeEnum,
   IotDeviceMessageTypeEnum,

+ 33 - 13
src/views/iot/rule/scene/RuleSceneForm.vue

@@ -33,17 +33,36 @@
         </el-col>
         <el-col :span="24">
           <el-divider content-position="left">触发器配置</el-divider>
-          <device-listener
-            v-for="(trigger, index) in formData.triggers"
-            :key="trigger.key"
-            :model-value="trigger"
-            @update:model-value="(val) => (formData.triggers[index] = val)"
-            class="mb-10px"
-          >
-            <el-button type="danger" round size="small" @click="removeTrigger(index)">
-              <Icon icon="ep:delete" />
-            </el-button>
-          </device-listener>
+          <!-- 根据触发类型选择不同的监听器组件 -->
+          <template v-for="(trigger, index) in formData.triggers" :key="trigger.key">
+            <!-- 设备状态变更和定时触发使用简化的监听器 -->
+            <device-state-listener
+              v-if="
+                trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE ||
+                trigger.type === IotRuleSceneTriggerTypeEnum.TIMER
+              "
+              :model-value="trigger"
+              @update:model-value="(val) => (formData.triggers[index] = val)"
+              class="mb-10px"
+            >
+              <el-button type="danger" round size="small" @click="removeTrigger(index)">
+                <Icon icon="ep:delete" />
+              </el-button>
+            </device-state-listener>
+
+            <!-- 其他设备触发类型使用完整的监听器 -->
+            <device-listener
+              v-else
+              :model-value="trigger"
+              @update:model-value="(val) => (formData.triggers[index] = val)"
+              class="mb-10px"
+            >
+              <el-button type="danger" round size="small" @click="removeTrigger(index)">
+                <Icon icon="ep:delete" />
+              </el-button>
+            </device-listener>
+          </template>
+
           <el-button class="ml-10px!" type="primary" size="small" @click="addTrigger">
             添加触发器
           </el-button>
@@ -77,6 +96,7 @@
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { RuleSceneApi } from '@/api/iot/rule/scene'
 import DeviceListener from './components/listener/DeviceListener.vue'
+import DeviceStateListener from './components/listener/DeviceStateListener.vue'
 import { CommonStatusEnum } from '@/utils/constants'
 import {
   ActionConfig,
@@ -117,7 +137,7 @@ const formRef = ref() // 表单 Ref
 const addTrigger = () => {
   formData.value.triggers.push({
     key: generateUUID(), // 解决组件索引重用
-    type: IotRuleSceneTriggerTypeEnum.DEVICE,
+    type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST, // 默认为物模型属性上报
     productKey: '',
     deviceNames: [],
     conditions: [
@@ -138,7 +158,7 @@ const removeTrigger = (index: number) => {
 const addAction = () => {
   formData.value.actions.push({
     key: generateUUID(), // 解决组件索引重用
-    type: IotRuleSceneActionTypeEnum.DEVICE_CONTROL
+    type: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
   } as ActionConfig)
 }
 /** 移除执行器 */

+ 98 - 26
src/views/iot/rule/scene/components/action/ActionExecutor.vue

@@ -19,19 +19,13 @@
             />
           </el-select>
         </div>
-        <div
-          v-if="actionConfig.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL"
-          class="flex items-center mr-60px"
-        >
+        <div v-if="isDeviceAction" class="flex items-center mr-60px">
           <span class="mr-10px">产品</span>
           <el-button type="primary" @click="handleSelectProduct" size="small" plain>
             {{ product ? product.name : '选择产品' }}
           </el-button>
         </div>
-        <div
-          v-if="actionConfig.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL"
-          class="flex items-center mr-60px"
-        >
+        <div v-if="isDeviceAction" class="flex items-center mr-60px">
           <span class="mr-10px">设备</span>
           <el-button type="primary" @click="handleSelectDevice" size="small" plain>
             {{ isEmpty(deviceList) ? '选择设备' : deviceList.map((d) => d.deviceName).join(',') }}
@@ -47,7 +41,8 @@
 
       <!-- 设备控制执行器 -->
       <DeviceControlAction
-        v-if="actionConfig.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL"
+        v-if="isDeviceAction"
+        :action-type="actionConfig.type"
         :model-value="actionConfig.deviceControl"
         :product-id="product?.id"
         :product-key="product?.productKey"
@@ -55,11 +50,42 @@
       />
 
       <!-- 告警执行器 -->
-      <AlertAction
-        v-else-if="actionConfig.type === IotRuleSceneActionTypeEnum.ALERT"
-        :model-value="actionConfig.alert"
-        @update:model-value="(val) => (actionConfig.alert = val)"
-      />
+      <div v-else-if="isAlertAction">
+        <!-- 告警触发 - 无需额外配置 -->
+        <div
+          v-if="actionConfig.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER"
+          class="bg-[#dbe5f6] flex items-center justify-center p-10px"
+        >
+          <el-icon class="mr-5px text-blue-500"><Icon icon="ep:info-filled" /></el-icon>
+          <span class="text-gray-600">触发告警通知,系统将自动发送告警信息</span>
+        </div>
+
+        <!-- 告警恢复 - 需要选择告警配置 -->
+        <div v-else-if="actionConfig.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER">
+          <div class="bg-[#dbe5f6] flex items-center justify-center p-10px mb-10px">
+            <el-icon class="mr-5px text-blue-500"><Icon icon="ep:info-filled" /></el-icon>
+            <span class="text-gray-600">恢复指定的告警配置状态</span>
+          </div>
+          <div class="p-10px">
+            <el-form-item label="选择告警配置" required>
+              <el-select
+                v-model="actionConfig.alertConfigId"
+                class="!w-240px"
+                clearable
+                placeholder="请选择要恢复的告警配置"
+                :loading="alertConfigLoading"
+              >
+                <el-option
+                  v-for="config in alertConfigList"
+                  :key="config.id"
+                  :label="config.name"
+                  :value="config.id"
+                />
+              </el-select>
+            </el-form-item>
+          </div>
+        </div>
+      </div>
     </div>
 
     <!-- 产品、设备的选择 -->
@@ -80,11 +106,10 @@ import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import ProductTableSelect from '@/views/iot/product/product/components/ProductTableSelect.vue'
 import DeviceTableSelect from '@/views/iot/device/device/components/DeviceTableSelect.vue'
 import DeviceControlAction from './DeviceControlAction.vue'
-import AlertAction from './AlertAction.vue'
 import { ProductApi, ProductVO } from '@/api/iot/product/product'
 import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
+import { AlertConfigApi, AlertConfig } from '@/api/iot/alert/config'
 import {
-  ActionAlert,
   ActionConfig,
   ActionDeviceControl,
   IotDeviceMessageIdentifierEnum,
@@ -101,29 +126,56 @@ const actionConfig = useVModel(props, 'modelValue', emits) as Ref<ActionConfig>
 
 const message = useMessage()
 
+/** 计算属性:判断是否为设备相关执行类型 */
+const isDeviceAction = computed(() => {
+  return [
+    IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
+    IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
+  ].includes(actionConfig.value.type as any)
+})
+
+/** 计算属性:判断是否为告警相关执行类型 */
+const isAlertAction = computed(() => {
+  return [
+    IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
+    IotRuleSceneActionTypeEnum.ALERT_RECOVER
+  ].includes(actionConfig.value.type as any)
+})
+
 /** 初始化执行器结构 */
 const initActionConfig = () => {
   if (!actionConfig.value) {
-    actionConfig.value = { type: IotRuleSceneActionTypeEnum.DEVICE_CONTROL } as ActionConfig
+    actionConfig.value = { type: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET } as ActionConfig
   }
 
   // 设备控制执行器初始化
-  if (
-    actionConfig.value.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL &&
-    !actionConfig.value.deviceControl
-  ) {
+  if (isDeviceAction.value && !actionConfig.value.deviceControl) {
     actionConfig.value.deviceControl = {
       productKey: '',
       deviceNames: [],
-      type: IotDeviceMessageTypeEnum.PROPERTY,
-      identifier: IotDeviceMessageIdentifierEnum.PROPERTY_SET,
+      type:
+        actionConfig.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
+          ? IotDeviceMessageTypeEnum.PROPERTY
+          : IotDeviceMessageTypeEnum.SERVICE,
+      identifier:
+        actionConfig.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
+          ? IotDeviceMessageIdentifierEnum.PROPERTY_SET
+          : IotDeviceMessageIdentifierEnum.SERVICE_INVOKE,
       data: {}
     } as ActionDeviceControl
   }
 
   // 告警执行器初始化
-  if (actionConfig.value.type === IotRuleSceneActionTypeEnum.ALERT && !actionConfig.value.alert) {
-    actionConfig.value.alert = {} as ActionAlert
+  if (isAlertAction.value) {
+    if (actionConfig.value.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER) {
+      // 告警触发 - 无需额外配置
+      actionConfig.value.alertConfigId = undefined
+    } else if (actionConfig.value.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER) {
+      // 告警恢复 - 需要选择告警配置
+      if (!actionConfig.value.alertConfigId) {
+        actionConfig.value.alertConfigId = undefined
+      }
+    }
   }
 }
 
@@ -133,6 +185,10 @@ const deviceTableSelectRef = ref<InstanceType<typeof DeviceTableSelect>>()
 const product = ref<ProductVO>()
 const deviceList = ref<DeviceVO[]>([])
 
+/** 告警配置相关 */
+const alertConfigList = ref<AlertConfig[]>([])
+const alertConfigLoading = ref(false)
+
 /** 处理选择产品 */
 const handleSelectProduct = () => {
   productTableSelectRef.value?.open()
@@ -168,11 +224,27 @@ const handleDeviceSelect = (val: DeviceVO[]) => {
   }
 }
 
+/** 获取告警配置列表 */
+const getAlertConfigList = async () => {
+  try {
+    alertConfigLoading.value = true
+    alertConfigList.value = await AlertConfigApi.getSimpleAlertConfigList()
+  } catch (error) {
+    console.error('获取告警配置列表失败:', error)
+  } finally {
+    alertConfigLoading.value = false
+  }
+}
+
 /** 监听执行类型变化,初始化对应配置 */
 watch(
   () => actionConfig.value.type,
-  () => {
+  (newType) => {
     initActionConfig()
+    // 如果是告警恢复类型,需要加载告警配置列表
+    if (newType === IotRuleSceneActionTypeEnum.ALERT_RECOVER) {
+      getAlertConfigList()
+    }
   },
   { immediate: true }
 )

+ 0 - 91
src/views/iot/rule/scene/components/action/AlertAction.vue

@@ -1,91 +0,0 @@
-<template>
-  <div class="bg-[#dbe5f6] p-10px">
-    <div class="flex items-center mb-10px">
-      <span class="mr-10px w-80px">接收方式</span>
-      <el-select
-        v-model="alertConfig.receiveType"
-        class="!w-160px"
-        clearable
-        placeholder="选择接收方式"
-      >
-        <!-- TODO @芋艿:后续搞成字典 -->
-        <!-- TODO @puhui999:这里好像是 1、/2、/3 哈 -->
-        <el-option
-          v-for="(value, key) in IotAlertConfigReceiveTypeEnum"
-          :key="value"
-          :label="key === 'SMS' ? '短信' : key === 'MAIL' ? '邮箱' : '通知'"
-          :value="value"
-        />
-      </el-select>
-    </div>
-    <div
-      v-if="alertConfig.receiveType === IotAlertConfigReceiveTypeEnum.SMS"
-      class="flex items-center mb-10px"
-    >
-      <span class="mr-10px w-80px">手机号码</span>
-      <el-select
-        v-model="alertConfig.phoneNumbers"
-        class="!w-360px"
-        multiple
-        filterable
-        allow-create
-        default-first-option
-        placeholder="请输入手机号码"
-      />
-    </div>
-    <div
-      v-if="alertConfig.receiveType === IotAlertConfigReceiveTypeEnum.MAIL"
-      class="flex items-center mb-10px"
-    >
-      <span class="mr-10px w-80px">邮箱地址</span>
-      <el-select
-        v-model="alertConfig.emails"
-        class="!w-360px"
-        multiple
-        filterable
-        allow-create
-        default-first-option
-        placeholder="请输入邮箱地址"
-      />
-    </div>
-    <div class="flex items-center">
-      <span class="mr-10px w-80px align-self-start">通知内容</span>
-      <el-input
-        v-model="alertConfig.content"
-        type="textarea"
-        :rows="4"
-        class="!w-360px"
-        placeholder="请输入通知内容"
-      />
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { useVModel } from '@vueuse/core'
-import { ActionAlert, IotAlertConfigReceiveTypeEnum } from '@/api/iot/rule/scene/scene.types'
-
-/** 告警执行器组件 */
-defineOptions({ name: 'AlertAction' })
-
-const props = defineProps<{ modelValue: any }>()
-const emits = defineEmits(['update:modelValue'])
-const alertConfig = useVModel(props, 'modelValue', emits) as Ref<ActionAlert>
-
-/** 初始化告警执行器结构 */
-const initAlertConfig = () => {
-  if (!alertConfig.value) {
-    alertConfig.value = {
-      receiveType: IotAlertConfigReceiveTypeEnum.NOTIFY,
-      phoneNumbers: [],
-      emails: [],
-      content: ''
-    } as ActionAlert
-  }
-}
-
-/** 初始化 */
-onMounted(() => {
-  initAlertConfig()
-})
-</script>

+ 31 - 13
src/views/iot/rule/scene/components/action/DeviceControlAction.vue

@@ -1,11 +1,5 @@
 <template>
   <div class="bg-[#dbe5f6] flex p-10px">
-    <div class="flex flex-col items-center justify-center mr-10px h-a">
-      <el-select v-model="deviceControlConfig.type" class="!w-160px" clearable placeholder="">
-        <el-option label="属性" :value="IotDeviceMessageTypeEnum.PROPERTY" />
-        <el-option label="服务" :value="IotDeviceMessageTypeEnum.SERVICE" />
-      </el-select>
-    </div>
     <div class="">
       <div
         class="flex items-center justify-around mb-10px last:mb-0"
@@ -74,7 +68,8 @@ import { ThingModelApi } from '@/api/iot/thingmodel'
 import {
   ActionDeviceControl,
   IotDeviceMessageIdentifierEnum,
-  IotDeviceMessageTypeEnum
+  IotDeviceMessageTypeEnum,
+  IotRuleSceneActionTypeEnum
 } from '@/api/iot/rule/scene/scene.types'
 import ThingModelParamInput from '../ThingModelParamInput.vue'
 
@@ -83,6 +78,7 @@ defineOptions({ name: 'DeviceControlAction' })
 
 const props = defineProps<{
   modelValue: any
+  actionType: number
   productId?: number
   productKey?: string
 }>()
@@ -98,10 +94,6 @@ const addParameter = () => {
     message.warning('请先选择一个产品')
     return
   }
-  if (parameters.value.length >= thingModels.value().length) {
-    message.warning(`该产品只有${thingModels.value().length}个物模型!!!`)
-    return
-  }
   parameters.value.push({ identifier: '', value: undefined })
 }
 const removeParameter = (index: number) => {
@@ -140,8 +132,14 @@ const initDeviceControlConfig = () => {
     deviceControlConfig.value = {
       productKey: '',
       deviceNames: [],
-      type: IotDeviceMessageTypeEnum.PROPERTY,
-      identifier: IotDeviceMessageIdentifierEnum.PROPERTY_SET,
+      type:
+        props.actionType === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
+          ? IotDeviceMessageTypeEnum.PROPERTY
+          : IotDeviceMessageTypeEnum.SERVICE,
+      identifier:
+        props.actionType === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
+          ? IotDeviceMessageIdentifierEnum.PROPERTY_SET
+          : IotDeviceMessageIdentifierEnum.SERVICE_INVOKE,
       data: {}
     } as ActionDeviceControl
   } else {
@@ -208,6 +206,26 @@ watch(
   }
 )
 
+/** 监听执行类型变化 */
+watch(
+  () => props.actionType,
+  (val: any) => {
+    if (!val) {
+      return
+    }
+    // 切换执行类型时清空参数
+    deviceControlConfig.value.data = {}
+    parameters.value = []
+    if (val === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET) {
+      deviceControlConfig.value.type = IotDeviceMessageTypeEnum.PROPERTY
+      deviceControlConfig.value.identifier = IotDeviceMessageIdentifierEnum.PROPERTY_SET
+    } else if (val === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE) {
+      deviceControlConfig.value.type = IotDeviceMessageTypeEnum.SERVICE
+      deviceControlConfig.value.identifier = IotDeviceMessageIdentifierEnum.SERVICE_INVOKE
+    }
+  }
+)
+
 /** 监听消息类型变化 */
 watch(
   () => deviceControlConfig.value.type,

+ 86 - 27
src/views/iot/rule/scene/components/listener/DeviceListener.vue

@@ -18,19 +18,13 @@
             />
           </el-select>
         </div>
-        <div
-          v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.DEVICE"
-          class="flex items-center mr-60px"
-        >
+        <div v-if="isDeviceTrigger" class="flex items-center mr-60px">
           <span class="mr-10px">产品</span>
           <el-button type="primary" @click="productTableSelectRef?.open()" size="small" plain>
             {{ product ? product.name : '选择产品' }}
           </el-button>
         </div>
-        <div
-          v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.DEVICE"
-          class="flex items-center mr-60px"
-        >
+        <div v-if="isDeviceTrigger" class="flex items-center mr-60px">
           <span class="mr-10px">设备</span>
           <el-button type="primary" @click="openDeviceSelect" size="small" plain>
             {{ isEmpty(deviceList) ? '选择设备' : triggerConfig.deviceNames.join(',') }}
@@ -44,25 +38,22 @@
         </div>
       </div>
       <!-- 设备触发器条件 -->
-      <template v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.DEVICE">
+      <template v-if="isDeviceTrigger">
+        <!-- 设备上下线变更 - 无需额外配置 -->
         <div
+          v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE"
+          class="bg-[#dbe5f6] flex items-center justify-center p-10px"
+        >
+          <span class="text-gray-600">设备上下线状态变更时触发,无需额外配置</span>
+        </div>
+
+        <!-- 物模型属性上报、设备事件上报、设备服务调用 - 需要配置条件 -->
+        <div
+          v-else
           class="bg-[#dbe5f6] flex p-10px"
           v-for="(condition, index) in triggerConfig.conditions"
           :key="index"
         >
-          <div class="flex flex-col items-center justify-center mr-10px h-a">
-            <el-select
-              v-model="condition.type"
-              @change="condition.parameters = []"
-              class="!w-160px"
-              clearable
-              placeholder=""
-            >
-              <el-option label="属性" :value="IotDeviceMessageTypeEnum.PROPERTY" />
-              <el-option label="服务" :value="IotDeviceMessageTypeEnum.SERVICE" />
-              <el-option label="事件" :value="IotDeviceMessageTypeEnum.EVENT" />
-            </el-select>
-          </div>
           <div class="w-70%">
             <DeviceListenerCondition
               v-for="(parameter, index2) in condition.parameters"
@@ -118,9 +109,11 @@
         <span class="w-120px">CRON 表达式</span>
         <crontab v-model="triggerConfig.cronExpression" />
       </div>
-      <!-- 设备触发才可以设置多个触发条件 -->
+      <!-- 除了设备上下线变更,其他设备触发类型都可以设置多个触发条件 -->
       <el-text
-        v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.DEVICE"
+        v-if="
+          isDeviceTrigger && triggerConfig.type !== IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE
+        "
         class="ml-10px!"
         type="primary"
         @click="addCondition"
@@ -158,6 +151,7 @@ import {
   TriggerConditionParameter,
   TriggerConfig
 } from '@/api/iot/rule/scene/scene.types'
+import { Crontab } from '@/components/Crontab'
 
 /** 场景联动之监听器组件 */
 defineOptions({ name: 'DeviceListener' })
@@ -168,10 +162,35 @@ const triggerConfig = useVModel(props, 'modelValue', emits) as Ref<TriggerConfig
 
 const message = useMessage()
 
+/** 计算属性:判断是否为设备触发类型 */
+const isDeviceTrigger = computed(() => {
+  return [
+    IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
+    IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+    IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
+    IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+  ].includes(triggerConfig.value.type as any)
+})
+
 /** 添加触发条件 */
 const addCondition = () => {
+  // 根据触发类型设置默认的条件类型
+  let defaultConditionType: string = IotDeviceMessageTypeEnum.PROPERTY
+
+  switch (triggerConfig.value.type) {
+    case IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST:
+      defaultConditionType = IotDeviceMessageTypeEnum.PROPERTY
+      break
+    case IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST:
+      defaultConditionType = IotDeviceMessageTypeEnum.EVENT
+      break
+    case IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE:
+      defaultConditionType = IotDeviceMessageTypeEnum.SERVICE
+      break
+  }
+
   triggerConfig.value.conditions?.push({
-    type: IotDeviceMessageTypeEnum.PROPERTY,
+    type: defaultConditionType,
     identifier: IotDeviceMessageIdentifierEnum.PROPERTY_SET,
     parameters: []
   })
@@ -187,6 +206,10 @@ const addConditionParameter = (conditionParameters: TriggerConditionParameter[])
     message.warning('请先选择一个产品')
     return
   }
+  if (conditionParameters.length >= 1) {
+    message.warning('只允许添加一个参数')
+    return
+  }
   conditionParameters.push({} as TriggerConditionParameter)
 }
 /** 移除参数 */
@@ -290,12 +313,48 @@ const getThingModelTSL = async () => {
   thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(product.value.id)
 }
 
+/** 监听触发类型变化,自动设置条件类型 */
+watch(
+  () => triggerConfig.value.type,
+  (newType) => {
+    if (!newType || newType === IotRuleSceneTriggerTypeEnum.TIMER) {
+      return
+    }
+
+    // 设备上下线变更不需要条件配置
+    if (newType === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
+      triggerConfig.value.conditions = []
+      return
+    }
+
+    // 为其他设备触发类型设置默认条件
+    if (triggerConfig.value.conditions && triggerConfig.value.conditions.length > 0) {
+      triggerConfig.value.conditions.forEach((condition) => {
+        switch (newType) {
+          case IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST:
+            condition.type = IotDeviceMessageTypeEnum.PROPERTY
+            break
+          case IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST:
+            condition.type = IotDeviceMessageTypeEnum.EVENT
+            break
+          case IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE:
+            condition.type = IotDeviceMessageTypeEnum.SERVICE
+            break
+        }
+      })
+    }
+  }
+)
+
 /** 初始化 */
 onMounted(async () => {
   // 初始化产品和设备回显
   if (triggerConfig.value) {
-    // 初始化conditions数组,如果不存在
-    if (!triggerConfig.value.conditions) {
+    // 初始化conditions数组,如果不存在且不是设备上下线变更类型
+    if (
+      !triggerConfig.value.conditions &&
+      triggerConfig.value.type !== IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE
+    ) {
       triggerConfig.value.conditions = []
     }
 

+ 166 - 0
src/views/iot/rule/scene/components/listener/DeviceStateListener.vue

@@ -0,0 +1,166 @@
+<template>
+  <div>
+    <div class="m-10px">
+      <div class="relative bg-[#eff3f7] h-50px flex items-center px-10px">
+        <div class="flex items-center mr-60px">
+          <span class="mr-10px">触发条件</span>
+          <el-select
+            v-model="triggerConfig.type"
+            class="!w-240px"
+            clearable
+            placeholder="请选择触发条件"
+          >
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.IOT_RULE_SCENE_TRIGGER_TYPE_ENUM)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </div>
+        <div class="flex items-center mr-60px">
+          <span class="mr-10px">产品</span>
+          <el-button type="primary" @click="productTableSelectRef?.open()" size="small" plain>
+            {{ product ? product.name : '选择产品' }}
+          </el-button>
+        </div>
+        <div class="flex items-center mr-60px">
+          <span class="mr-10px">设备</span>
+          <el-button type="primary" @click="openDeviceSelect" size="small" plain>
+            {{ isEmpty(deviceList) ? '选择设备' : triggerConfig.deviceNames.join(',') }}
+          </el-button>
+        </div>
+        <!-- 删除触发器 -->
+        <div class="absolute top-auto right-16px bottom-auto">
+          <el-tooltip content="删除触发器" placement="top">
+            <slot></slot>
+          </el-tooltip>
+        </div>
+      </div>
+
+      <!-- 设备状态变更说明 -->
+      <div
+        v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE"
+        class="bg-[#dbe5f6] flex items-center justify-center p-10px"
+      >
+        <el-icon class="mr-5px text-blue-500"><Icon icon="ep:info-filled" /></el-icon>
+        <span class="text-gray-600">当选中的设备上线或下线时触发场景联动</span>
+      </div>
+
+      <!-- 定时触发 -->
+      <div
+        v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.TIMER"
+        class="bg-[#dbe5f6] flex items-center justify-between p-10px"
+      >
+        <span class="w-120px">CRON 表达式</span>
+        <crontab v-model="triggerConfig.cronExpression" />
+      </div>
+    </div>
+
+    <!-- 产品、设备的选择 -->
+    <ProductTableSelect ref="productTableSelectRef" @success="handleProductSelect" />
+    <DeviceTableSelect
+      ref="deviceTableSelectRef"
+      multiple
+      :product-id="product?.id"
+      @success="handleDeviceSelect"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { isEmpty } from '@/utils/is'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import ProductTableSelect from '@/views/iot/product/product/components/ProductTableSelect.vue'
+import DeviceTableSelect from '@/views/iot/device/device/components/DeviceTableSelect.vue'
+import { ProductApi, ProductVO } from '@/api/iot/product/product'
+import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
+import { IotRuleSceneTriggerTypeEnum, TriggerConfig } from '@/api/iot/rule/scene/scene.types'
+import { Crontab } from '@/components/Crontab'
+
+/** 设备状态监听器组件 */
+defineOptions({ name: 'DeviceStateListener' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const triggerConfig = useVModel(props, 'modelValue', emits) as Ref<TriggerConfig>
+
+const message = useMessage()
+
+/** 产品和设备选择引用 */
+const productTableSelectRef = ref<InstanceType<typeof ProductTableSelect>>()
+const deviceTableSelectRef = ref<InstanceType<typeof DeviceTableSelect>>()
+const product = ref<ProductVO>()
+const deviceList = ref<DeviceVO[]>([])
+
+/** 处理产品选择 */
+const handleProductSelect = (val: ProductVO) => {
+  product.value = val
+  triggerConfig.value.productKey = val.productKey
+  deviceList.value = []
+}
+
+/** 处理设备选择 */
+const handleDeviceSelect = (val: DeviceVO[]) => {
+  deviceList.value = val
+  triggerConfig.value.deviceNames = val.map((item) => item.deviceName)
+}
+
+/** 打开设备选择器 */
+const openDeviceSelect = () => {
+  if (!product.value) {
+    message.warning('请先选择一个产品')
+    return
+  }
+  deviceTableSelectRef.value?.open()
+}
+
+/**
+ * 初始化产品回显信息
+ */
+const initProductInfo = async () => {
+  if (!triggerConfig.value.productKey) {
+    return
+  }
+
+  try {
+    const productData = await ProductApi.getProductByKey(triggerConfig.value.productKey)
+    if (productData) {
+      product.value = productData
+    }
+  } catch (error) {
+    console.error('获取产品信息失败:', error)
+  }
+}
+
+/**
+ * 初始化设备回显信息
+ */
+const initDeviceInfo = async () => {
+  if (!triggerConfig.value.productKey || !triggerConfig.value.deviceNames?.length) {
+    return
+  }
+
+  try {
+    const deviceData = await DeviceApi.getDevicesByProductKeyAndNames(
+      triggerConfig.value.productKey,
+      triggerConfig.value.deviceNames
+    )
+
+    if (deviceData && deviceData.length > 0) {
+      deviceList.value = deviceData
+    }
+  } catch (error) {
+    console.error('获取设备信息失败:', error)
+  }
+}
+
+/** 初始化 */
+onMounted(async () => {
+  if (triggerConfig.value) {
+    await initProductInfo()
+    await initDeviceInfo()
+  }
+})
+</script>