Przeglądaj źródła

【功能完善】IoT: 规则场景监听器相关组件

puhui999 1 rok temu
rodzic
commit
73d2c2005c

+ 4 - 3
src/api/iot/rule/scene/index.ts

@@ -1,12 +1,13 @@
 import request from '@/config/axios'
+import { IotRuleSceneTriggerConfig } from '@/api/iot/rule/scene/scene.types'
 
 // IoT 规则场景(场景联动) VO
 export interface RuleSceneVO {
   id?: number // 场景编号
-  name?: string // 场景名称
+  name: string // 场景名称
   description?: string // 场景描述
-  status?: number // 场景状态
-  triggers?: any[] // 触发器数组
+  status: number // 场景状态
+  triggers: IotRuleSceneTriggerConfig[] // 触发器数组
   actions?: any[] // 执行器数组
 }
 

+ 4 - 4
src/api/iot/rule/scene/scene.types.ts

@@ -9,11 +9,11 @@ export interface IotRuleSceneTriggerConfig {
    */
   type: number
   /** 产品标识 */
-  productKey?: string
+  productKey: string
   /** 设备名称数组 */
-  deviceNames?: string[]
+  deviceNames: string[]
   /** 触发条件数组。条件之间是"或"的关系 */
-  conditions?: IotRuleSceneTriggerCondition[]
+  conditions: IotRuleSceneTriggerCondition[]
   /** CRON 表达式。当 type = 2 时必填 */
   cronExpression?: string
 }
@@ -29,7 +29,7 @@ export interface IotRuleSceneTriggerCondition {
    */
   type: string
   /** 消息标识符 */
-  identifier: string
+  identifier?: string
   /** 参数数组。参数之间是"或"的关系 */
   parameters: IotRuleSceneTriggerConditionParameter[]
 }

+ 1 - 1
src/store/modules/app.ts

@@ -61,7 +61,7 @@ export const useAppStore = defineStore('app', {
       tagsView: true, // 标签页
       tagsViewImmerse: false, // 标签页沉浸
       tagsViewIcon: true, // 是否显示标签图标
-      logo: true, // logo
+      logo: false, // logo
       fixedHeader: true, // 固定toolheader
       footer: true, // 显示页脚
       greyMode: false, // 是否开始灰色模式,用于特殊悼念日

+ 33 - 6
src/views/iot/rule/scene/IoTRuleSceneForm.vue

@@ -35,10 +35,19 @@
           <el-divider content-position="left">触发器配置</el-divider>
           <device-listener
             v-for="(trigger, index) in formData.triggers"
-            :model-value="trigger"
             :key="index"
+            :model-value="trigger"
+            @update:model-value="(val) => (formData.triggers[index] = val)"
             class="mb-10px"
-          />
+          >
+            <el-button
+              type="danger"
+              round
+              :icon="Delete"
+              size="small"
+              @click="removeTrigger(index)"
+            />
+          </device-listener>
           <el-text class="ml-10px!" type="primary" @click="addTrigger">添加触发器</el-text>
         </el-col>
         <el-col :span="24">
@@ -59,6 +68,8 @@
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { RuleSceneApi, RuleSceneVO } from '@/api/iot/rule/scene'
 import DeviceListener from './components/DeviceListener.vue'
+import { Delete } from '@element-plus/icons-vue'
+import { IotRuleSceneTriggerConfig } from '@/api/iot/rule/scene/scene.types'
 
 /** IoT 规则场景(场景联动) 表单 */
 defineOptions({ name: 'RuleSceneForm' })
@@ -72,7 +83,7 @@ const formLoading = ref(false) // 表单的加载中:1)修改时的数据加
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref<RuleSceneVO>({
   status: 0,
-  triggers: []
+  triggers: [] as IotRuleSceneTriggerConfig[]
 } as RuleSceneVO)
 const formRules = reactive({
   name: [{ required: true, message: '场景名称不能为空', trigger: 'blur' }],
@@ -82,10 +93,26 @@ const formRules = reactive({
 })
 const formRef = ref() // 表单 Ref
 
+/** 添加触发器 */
 const addTrigger = () => {
-  formData.value.triggers?.push({})
+  formData.value.triggers.push({
+    type: 1,
+    productKey: '',
+    deviceNames: [],
+    conditions: [
+      {
+        type: 'property',
+        parameters: []
+      }
+    ]
+  })
+}
+/** 移除触发器 */
+const removeTrigger = (index: number) => {
+  const newTriggers = [...formData.value.triggers]
+  newTriggers.splice(index, 1)
+  formData.value.triggers = newTriggers
 }
-
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
   dialogVisible.value = true
@@ -132,7 +159,7 @@ const submitForm = async () => {
 const resetForm = () => {
   formData.value = {
     status: 0,
-    triggers: []
+    triggers: [] as IotRuleSceneTriggerConfig[]
   } as RuleSceneVO
   formRef.value?.resetFields()
 }

+ 55 - 31
src/views/iot/rule/scene/components/DeviceListener.vue

@@ -3,7 +3,12 @@
     <div class="device-listener-header h-50px flex items-center px-10px">
       <div class="flex items-center mr-60px">
         <span class="mr-10px">触发条件</span>
-        <el-select v-model="triggerType" class="!w-240px" clearable placeholder="请选择触发条件">
+        <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"
@@ -20,12 +25,19 @@
         <span class="mr-10px">设备</span>
         <el-button type="primary">选择设备</el-button>
       </div>
-      <!-- 添加规则 -->
-      <el-button class="device-listener-delete" type="danger" round :icon="Delete" size="small" />
+      <!-- 删除触发器 -->
+      <div class="device-listener-delete">
+        <slot></slot>
+      </div>
     </div>
-    <div class="device-listener-condition flex p-10px">
+    <!-- 触发器条件 -->
+    <div
+      class="device-listener-condition 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="messageType" class="!w-160px" clearable placeholder="">
+        <el-select v-model="condition.type" class="!w-160px" clearable placeholder="">
           <el-option
             v-for="dict in getStrDictOptions(DICT_TYPE.IOT_DEVICE_MESSAGE_TYPE_ENUM)"
             :key="dict.value"
@@ -36,62 +48,74 @@
       </div>
       <div class="">
         <DeviceListenerCondition
-          v-for="(conditionParameter, index) in conditionParameters"
-          :key="index"
-          :model-value="conditionParameter"
-          @update:model-value="(val) => (conditionParameters[index] = val)"
+          v-for="(parameter, index2) in condition.parameters"
+          :key="index2"
+          :model-value="parameter"
+          @update:model-value="(val) => (condition.parameters[index2] = val)"
           class="mb-10px last:mb-0"
         >
-          <!-- 添加规则 -->
+          <!-- 删除规则 -->
           <el-button
             class="device-listener-delete"
             type="danger"
             circle
             :icon="Delete"
             size="small"
-            @click="removeConditionParameter(index)"
+            @click="removeConditionParameter(condition.parameters, index2)"
           />
         </DeviceListenerCondition>
       </div>
       <div class="flex flex-1 flex-col items-center justify-center w-a h-a">
         <!-- 添加规则 -->
-        <el-button type="primary" circle :icon="Plus" size="small" @click="addConditionParameter" />
+        <el-button
+          type="primary"
+          circle
+          :icon="Plus"
+          size="small"
+          @click="addConditionParameter(condition.parameters)"
+        />
       </div>
     </div>
+    <el-text class="ml-10px!" type="primary" @click="addCondition">添加触发条件</el-text>
   </div>
 </template>
 
 <script setup lang="ts">
 import { Delete, Plus } from '@element-plus/icons-vue'
 import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
-import { ref } from 'vue'
 import DeviceListenerCondition from './DeviceListenerCondition.vue'
-import { IotRuleSceneTriggerConditionParameter } from '@/api/iot/rule/scene/scene.types'
+import {
+  IotRuleSceneTriggerConditionParameter,
+  IotRuleSceneTriggerConfig
+} from '@/api/iot/rule/scene/scene.types'
+import { useVModel } from '@vueuse/core'
 
 /** 场景联动之监听器组件 */
 defineOptions({ name: 'DeviceListener' })
 
-defineProps<{
-  modelValue: any
-}>()
-
-const emit = defineEmits(['update:modelValue'])
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const triggerConfig = useVModel(props, 'modelValue', emits) as Ref<IotRuleSceneTriggerConfig>
 
-// 添加响应式变量
-const triggerType = ref()
-const messageType = ref('property')
-const conditionParameters = ref<IotRuleSceneTriggerConditionParameter[]>([])
 /** 添加触发条件 */
-const addConditionParameter = () => {
-  conditionParameters.value?.push({} as IotRuleSceneTriggerConditionParameter)
+const addCondition = () => {
+  triggerConfig.value.conditions.push({
+    type: 'property',
+    parameters: []
+  })
+}
+
+/** 添加参数 */
+const addConditionParameter = (conditionParameters: IotRuleSceneTriggerConditionParameter[]) => {
+  conditionParameters.push({} as IotRuleSceneTriggerConditionParameter)
 }
-/** 移除触发条件 */
-const removeConditionParameter = (index: number) => {
-  conditionParameters.value?.splice(index, 1)
+/** 移除参数 */
+const removeConditionParameter = (
+  conditionParameters: IotRuleSceneTriggerConditionParameter[],
+  index: number
+) => {
+  conditionParameters.splice(index, 1)
 }
-onMounted(() => {
-  addConditionParameter()
-})
 </script>
 
 <style lang="scss" scoped>

+ 0 - 379
src/views/iot/rule/scene/components/TriggerConditions.vue

@@ -1,379 +0,0 @@
-<template>
-  <div class="trigger-conditions">
-    <div class="conditions-header mb-3">
-      <el-button type="primary" @click="addCondition" :disabled="!productKey">
-        <Icon icon="ep:plus" class="mr-5px" /> 添加条件
-      </el-button>
-      <div class="conditions-tips" v-if="modelValue && modelValue.length > 0">
-        注:多个条件之间为"或"关系
-      </div>
-    </div>
-
-    <el-empty v-if="!modelValue || modelValue.length === 0" description="暂无触发条件" />
-
-    <div class="conditions-list" v-else>
-      <div v-for="(condition, index) in modelValue" :key="index" class="condition-item mb-3">
-        <el-card class="box-card">
-          <template #header>
-            <div class="card-header">
-              <span>条件 {{ index + 1 }}</span>
-              <el-button type="danger" link @click="removeCondition(index)"> 删除 </el-button>
-            </div>
-          </template>
-          <div class="condition-content">
-            <el-form label-width="100px" :model="condition">
-              <el-form-item label="消息类型">
-                <el-select
-                  v-model="condition.type"
-                  placeholder="请选择消息类型"
-                  @change="handleMessageTypeChange(index)"
-                >
-                  <el-option label="属性上报" :value="IotDeviceMessageTypeEnum.PROPERTY" />
-                  <el-option label="事件上报" :value="IotDeviceMessageTypeEnum.EVENT" />
-                </el-select>
-              </el-form-item>
-              <el-form-item label="消息标识符">
-                <el-select
-                  v-model="condition.identifier"
-                  placeholder="请选择消息标识符"
-                  filterable
-                  :loading="thingModelLoading"
-                  @change="handleIdentifierChange(index)"
-                >
-                  <el-option
-                    v-for="item in getThingModelOptions(condition.type)"
-                    :key="item.identifier"
-                    :label="item.name"
-                    :value="item.identifier"
-                  >
-                    <div class="thing-model-option">
-                      <span>{{ item.name }}</span>
-                      <span class="thing-model-identifier">{{ item.identifier }}</span>
-                    </div>
-                    <div class="thing-model-desc" v-if="item.description">{{
-                      item.description
-                    }}</div>
-                  </el-option>
-                </el-select>
-              </el-form-item>
-
-              <div class="parameters-area mt-3 mb-2">
-                <div class="parameters-header">
-                  <div>参数列表(多个参数之间为"或"关系)</div>
-                  <el-button type="primary" link @click="addParameter(index)"> 添加参数 </el-button>
-                </div>
-
-                <el-empty
-                  v-if="!condition.parameters || condition.parameters.length === 0"
-                  description="暂无参数"
-                />
-
-                <div class="parameters-list mt-2" v-else>
-                  <div
-                    v-for="(param, pIndex) in condition.parameters"
-                    :key="pIndex"
-                    class="parameter-item mb-2"
-                  >
-                    <el-card shadow="hover">
-                      <div class="parameter-item-header">
-                        <span>参数 {{ pIndex + 1 }}</span>
-                        <el-button type="danger" link @click="removeParameter(index, pIndex)">
-                          删除
-                        </el-button>
-                      </div>
-
-                      <el-form label-width="90px" :model="param" class="mt-2">
-                        <el-form-item label="标识符">
-                          <el-select
-                            v-model="param.identifier"
-                            placeholder="请选择参数标识符"
-                            filterable
-                          >
-                            <el-option
-                              v-for="item in getParameterOptions(condition)"
-                              :key="item.identifier"
-                              :label="item.name"
-                              :value="item.identifier"
-                            >
-                              <div class="thing-model-option">
-                                <span>{{ item.name }}</span>
-                                <span class="thing-model-identifier">{{ item.identifier }}</span>
-                              </div>
-                              <div class="thing-model-desc" v-if="item.description">{{
-                                item.description
-                              }}</div>
-                            </el-option>
-                          </el-select>
-                        </el-form-item>
-                        <el-form-item label="条件">
-                          <condition-selector
-                            v-model="param.condition"
-                            :placeholder="'请选择条件'"
-                            :value-placeholder="'请输入比较值'"
-                          />
-                        </el-form-item>
-                      </el-form>
-                    </el-card>
-                  </div>
-                </div>
-              </div>
-            </el-form>
-          </div>
-        </el-card>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { defineEmits, defineProps, onMounted, ref, watch } from 'vue'
-import {
-  IotDeviceMessageIdentifierEnum,
-  IotDeviceMessageTypeEnum,
-  IotRuleSceneTriggerCondition,
-  IotRuleSceneTriggerConditionParameter
-} from '@/api/iot/rule/scene/scene.types'
-import { ThingModelApi, ThingModelData } from '@/api/iot/thingmodel'
-import ConditionSelector from './ConditionSelector.vue'
-
-const props = defineProps({
-  modelValue: {
-    type: Array as () => IotRuleSceneTriggerCondition[],
-    required: true
-  },
-  productKey: {
-    type: String,
-    required: true
-  }
-})
-
-const emit = defineEmits(['update:modelValue'])
-
-// 物模型数据
-const thingModelList = ref<ThingModelData[]>([])
-const thingModelLoading = ref(false)
-
-// 加载物模型数据
-const loadThingModelData = async () => {
-  if (!props.productKey) return
-
-  try {
-    thingModelLoading.value = true
-    const result = await ThingModelApi.getThingModelListByProductId({
-      productKey: props.productKey
-    })
-    thingModelList.value = result || []
-  } catch (error) {
-    console.error('获取物模型数据失败', error)
-  } finally {
-    thingModelLoading.value = false
-  }
-}
-
-// 获取物模型选项
-const getThingModelOptions = (type: string) => {
-  if (!thingModelList.value) return []
-
-  return thingModelList.value.filter((item) => {
-    if (type === IotDeviceMessageTypeEnum.PROPERTY) {
-      return item.property
-    } else if (type === IotDeviceMessageTypeEnum.EVENT) {
-      return item.event
-    }
-    return false
-  })
-}
-
-// 获取参数选项
-const getParameterOptions = (condition: IotRuleSceneTriggerCondition) => {
-  if (!condition || !condition.identifier) return []
-
-  const model = thingModelList.value?.find((item) => item.identifier === condition.identifier)
-  if (!model) return []
-
-  if (condition.type === IotDeviceMessageTypeEnum.PROPERTY) {
-    return [model] // 属性本身就是参数
-  } else if (condition.type === IotDeviceMessageTypeEnum.EVENT) {
-    // TODO: 获取事件的输出参数列表
-    return []
-  }
-  return []
-}
-
-// 添加条件
-const addCondition = () => {
-  const newCondition: IotRuleSceneTriggerCondition = {
-    type: IotDeviceMessageTypeEnum.PROPERTY,
-    identifier: IotDeviceMessageIdentifierEnum.PROPERTY_REPORT,
-    parameters: []
-  }
-
-  const newValue = [...(props.modelValue || []), newCondition]
-  emit('update:modelValue', newValue)
-}
-
-// 移除条件
-const removeCondition = (index: number) => {
-  const newValue = [...props.modelValue]
-  newValue.splice(index, 1)
-  emit('update:modelValue', newValue)
-}
-
-// 消息类型变更
-const handleMessageTypeChange = (index: number) => {
-  const newValue = [...props.modelValue]
-  // 更新标识符
-  if (newValue[index].type === IotDeviceMessageTypeEnum.PROPERTY) {
-    newValue[index].identifier = IotDeviceMessageIdentifierEnum.PROPERTY_REPORT
-  } else if (newValue[index].type === IotDeviceMessageTypeEnum.EVENT) {
-    newValue[index].identifier = IotDeviceMessageIdentifierEnum.EVENT_REPORT
-  }
-  // 清空参数
-  newValue[index].parameters = []
-  emit('update:modelValue', newValue)
-}
-
-// 标识符变更
-const handleIdentifierChange = (index: number) => {
-  const newValue = [...props.modelValue]
-  // 清空参数
-  newValue[index].parameters = []
-  emit('update:modelValue', newValue)
-}
-
-// 添加参数
-const addParameter = (conditionIndex: number) => {
-  const newValue = [...props.modelValue]
-  if (!newValue[conditionIndex].parameters) {
-    newValue[conditionIndex].parameters = []
-  }
-
-  const newParameter: IotRuleSceneTriggerConditionParameter = {
-    identifier: '',
-    condition: {
-      operator: 'eq',
-      value: ''
-    }
-  }
-
-  newValue[conditionIndex].parameters.push(newParameter)
-  emit('update:modelValue', newValue)
-}
-
-// 移除参数
-const removeParameter = (conditionIndex: number, paramIndex: number) => {
-  const newValue = [...props.modelValue]
-  newValue[conditionIndex].parameters.splice(paramIndex, 1)
-  emit('update:modelValue', newValue)
-}
-
-// 监听 productKey 变化
-watch(
-  () => props.productKey,
-  (newVal) => {
-    if (!newVal) {
-      // 清空条件
-      if (props.modelValue?.length > 0) {
-        emit('update:modelValue', [])
-      }
-      // 清空物模型数据
-      thingModelList.value = []
-    } else {
-      // 加载物模型数据
-      loadThingModelData()
-    }
-  }
-)
-
-// 初始化
-onMounted(() => {
-  if (props.productKey) {
-    loadThingModelData()
-  }
-})
-</script>
-
-<style scoped>
-.trigger-conditions {
-  width: 100%;
-}
-
-.conditions-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-
-.conditions-tips {
-  font-size: 12px;
-  color: #999;
-}
-
-.condition-item {
-  border-radius: 4px;
-}
-
-.card-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-
-.parameters-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 10px;
-  font-weight: bold;
-}
-
-.parameter-item-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 10px;
-}
-
-.value-tips {
-  margin-top: 5px;
-  font-size: 12px;
-  color: #999;
-}
-
-.thing-model-option {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-
-.thing-model-identifier {
-  font-size: 12px;
-  color: #999;
-}
-
-.thing-model-desc {
-  margin-top: 4px;
-  font-size: 12px;
-  color: #666;
-}
-
-.mb-3 {
-  margin-bottom: 12px;
-}
-
-.mb-2 {
-  margin-bottom: 8px;
-}
-
-.mt-3 {
-  margin-top: 12px;
-}
-
-.mt-2 {
-  margin-top: 8px;
-}
-
-.mr-5px {
-  margin-right: 5px;
-}
-</style>