Просмотр исходного кода

feat:【IoT 物联网】场景联动触发器优化对齐后端

puhui999 10 месяцев назад
Родитель
Сommit
508de312b3

+ 9 - 4
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 = {

+ 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)
 }
 /** 移除执行器 */

+ 103 - 17
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,8 +38,18 @@
         </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"
@@ -57,10 +61,23 @@
               class="!w-160px"
               clearable
               placeholder=""
+              :disabled="isConditionTypeFixed"
             >
-              <el-option label="属性" :value="IotDeviceMessageTypeEnum.PROPERTY" />
-              <el-option label="服务" :value="IotDeviceMessageTypeEnum.SERVICE" />
-              <el-option label="事件" :value="IotDeviceMessageTypeEnum.EVENT" />
+              <el-option
+                v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST"
+                label="属性"
+                :value="IotDeviceMessageTypeEnum.PROPERTY"
+              />
+              <el-option
+                v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST"
+                label="事件"
+                :value="IotDeviceMessageTypeEnum.EVENT"
+              />
+              <el-option
+                v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE"
+                label="服务"
+                :value="IotDeviceMessageTypeEnum.SERVICE"
+              />
             </el-select>
           </div>
           <div class="w-70%">
@@ -118,9 +135,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 +177,7 @@ import {
   TriggerConditionParameter,
   TriggerConfig
 } from '@/api/iot/rule/scene/scene.types'
+import { Crontab } from '@/components/Crontab'
 
 /** 场景联动之监听器组件 */
 defineOptions({ name: 'DeviceListener' })
@@ -168,10 +188,40 @@ 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 isConditionTypeFixed = computed(() => {
+  return triggerConfig.value.type !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+})
+
 /** 添加触发条件 */
 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: []
   })
@@ -290,12 +340,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>