Przeglądaj źródła

【功能完善】IoT: 场景联动执行器配置

puhui999 1 rok temu
rodzic
commit
eadf26dc3b

+ 2 - 1
src/utils/dict.ts

@@ -247,5 +247,6 @@ export enum DICT_TYPE {
   IOT_DATA_BRIDGE_DIRECTION_ENUM = 'iot_data_bridge_direction_enum', // 桥梁方向
   IOT_DATA_BRIDGE_TYPE_ENUM = 'iot_data_bridge_type_enum', // 桥梁类型
   IOT_DEVICE_MESSAGE_TYPE_ENUM = 'iot_device_message_type_enum', // IoT 设备消息类型枚举
-  IOT_RULE_SCENE_TRIGGER_TYPE_ENUM = 'iot_rule_scene_trigger_type_enum' // IoT 场景流转的触发类型枚举
+  IOT_RULE_SCENE_TRIGGER_TYPE_ENUM = 'iot_rule_scene_trigger_type_enum', // IoT 场景流转的触发类型枚举
+  IOT_RULE_SCENE_ACTION_TYPE_ENUM = 'iot_rule_scene_action_type_enum' // IoT 规则场景的触发类型枚举
 }

+ 36 - 7
src/views/iot/rule/scene/RuleSceneForm.vue

@@ -49,10 +49,21 @@
           </el-button>
         </el-col>
         <el-col :span="24">
-          <el-divider content-position="left">执行动作配置</el-divider>
-          <el-form-item label="执行器数组" prop="actionConfigs">
-            <!--            <el-input v-model="formData.actions" placeholder="请输入执行器数组" />-->
-          </el-form-item>
+          <el-divider content-position="left">执行器配置</el-divider>
+          <action-executor
+            v-for="(action, index) in formData.actions"
+            :key="index"
+            :model-value="action"
+            @update:model-value="(val) => (formData.actions[index] = val)"
+            class="mb-10px"
+          >
+            <el-button type="danger" round size="small" @click="removeAction(index)">
+              <Icon icon="ep:delete" />
+            </el-button>
+          </action-executor>
+          <el-button class="ml-10px!" type="primary" size="small" @click="addAction">
+            添加执行器
+          </el-button>
         </el-col>
       </el-row>
     </el-form>
@@ -65,15 +76,18 @@
 <script setup lang="ts">
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { RuleSceneApi } from '@/api/iot/rule/scene'
-import DeviceListener from './components/DeviceListener.vue'
+import DeviceListener from './components/listener/DeviceListener.vue'
 import { CommonStatusEnum } from '@/utils/constants'
 import {
+  ActionConfig,
   IotDeviceMessageIdentifierEnum,
   IotDeviceMessageTypeEnum,
   IotRuleScene,
+  IotRuleSceneActionTypeEnum,
   IotRuleSceneTriggerTypeEnum,
   TriggerConfig
 } from '@/api/iot/rule/scene/scene.types'
+import ActionExecutor from './components/action/ActionExecutor.vue'
 
 /** IoT 场景联动表单 */
 defineOptions({ name: 'IotRuleSceneForm' })
@@ -87,7 +101,8 @@ const formLoading = ref(false) // 表单的加载中:1)修改时的数据加
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref<IotRuleScene>({
   status: CommonStatusEnum.ENABLE,
-  triggers: [] as TriggerConfig[]
+  triggers: [] as TriggerConfig[],
+  actions: [] as ActionConfig[]
 } as IotRuleScene)
 const formRules = reactive({
   name: [{ required: true, message: '场景名称不能为空', trigger: 'blur' }],
@@ -119,6 +134,19 @@ const removeTrigger = (index: number) => {
   formData.value.triggers = newTriggers
 }
 
+/** 添加执行器 */
+const addAction = () => {
+  formData.value.actions.push({
+    type: IotRuleSceneActionTypeEnum.DEVICE_CONTROL
+  } as ActionConfig)
+}
+/** 移除执行器 */
+const removeAction = (index: number) => {
+  const newActions = [...formData.value.actions]
+  newActions.splice(index, 1)
+  formData.value.actions = newActions
+}
+
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
   dialogVisible.value = true
@@ -165,7 +193,8 @@ const submitForm = async () => {
 const resetForm = () => {
   formData.value = {
     status: CommonStatusEnum.ENABLE,
-    triggers: [] as TriggerConfig[]
+    triggers: [] as TriggerConfig[],
+    actions: [] as ActionConfig[]
   } as IotRuleScene
   formRef.value?.resetFields()
 }

+ 180 - 0
src/views/iot/rule/scene/components/action/ActionExecutor.vue

@@ -0,0 +1,180 @@
+<template>
+  <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="actionConfig.type"
+          class="!w-240px"
+          clearable
+          placeholder="请选择执行类型"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_RULE_SCENE_ACTION_TYPE_ENUM)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </div>
+      <div
+        v-if="actionConfig.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL"
+        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="actionConfig.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL"
+        class="flex items-center mr-60px"
+      >
+        <span class="mr-10px">设备</span>
+        <el-button type="primary" @click="openDeviceSelect" size="small" plain>
+          {{ isEmpty(deviceList) ? '选择设备' : actionConfig.deviceControl?.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>
+
+    <!-- 设备控制执行器 -->
+    <DeviceControlAction
+      v-if="actionConfig.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL"
+      :model-value="actionConfig.deviceControl"
+      @update:model-value="(val) => (actionConfig.deviceControl = val)"
+    />
+
+    <!-- 告警执行器 -->
+    <AlertAction
+      v-else-if="actionConfig.type === IotRuleSceneActionTypeEnum.ALERT"
+      :model-value="actionConfig.alert"
+      @update:model-value="(val) => (actionConfig.alert = val)"
+    />
+
+    <!-- 数据桥接执行器 -->
+    <DataBridgeAction
+      v-else-if="actionConfig.type === IotRuleSceneActionTypeEnum.DATA_BRIDGE"
+      :model-value="actionConfig.dataBridgeId"
+      @update:model-value="(val) => (actionConfig.dataBridgeId = val)"
+    />
+  </div>
+
+  <!-- 产品、设备的选择 -->
+  <ProductTableSelect ref="productTableSelectRef" @success="handleProductSelect" />
+  <DeviceTableSelect
+    ref="deviceTableSelectRef"
+    multiple
+    :product-id="product?.id"
+    @success="handleDeviceSelect"
+  />
+</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 DeviceControlAction from './DeviceControlAction.vue'
+import AlertAction from './AlertAction.vue'
+import DataBridgeAction from './DataBridgeAction.vue'
+import { ProductVO } from '@/api/iot/product/product'
+import { DeviceVO } from '@/api/iot/device/device'
+import { ThingModelApi } from '@/api/iot/thingmodel'
+import {
+  ActionAlert,
+  ActionConfig,
+  ActionDeviceControl,
+  IotRuleSceneActionTypeEnum
+} from '@/api/iot/rule/scene/scene.types'
+
+/** 场景联动之执行器组件 */
+defineOptions({ name: 'ActionExecutor' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const actionConfig = useVModel(props, 'modelValue', emits) as Ref<ActionConfig>
+
+const message = useMessage()
+
+// 初始化执行器结构
+const initActionConfig = () => {
+  if (!actionConfig.value) {
+    actionConfig.value = { type: IotRuleSceneActionTypeEnum.DEVICE_CONTROL } as ActionConfig
+  }
+
+  // 设备控制执行器初始化
+  if (
+    actionConfig.value.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL &&
+    !actionConfig.value.deviceControl
+  ) {
+    actionConfig.value.deviceControl = {} as ActionDeviceControl
+  }
+
+  // 告警执行器初始化
+  if (actionConfig.value.type === IotRuleSceneActionTypeEnum.ALERT && !actionConfig.value.alert) {
+    actionConfig.value.alert = {} as ActionAlert
+  }
+
+  // 数据桥接执行器初始化
+  if (
+    actionConfig.value.type === IotRuleSceneActionTypeEnum.DATA_BRIDGE &&
+    !actionConfig.value.dataBridgeId
+  ) {
+    actionConfig.value.dataBridgeId = undefined
+  }
+}
+
+// 监听执行类型变化,初始化对应配置
+watch(
+  () => actionConfig.value.type,
+  () => {
+    initActionConfig()
+  },
+  { immediate: true }
+)
+
+// 产品和设备选择
+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
+  actionConfig.value.deviceControl!.productKey = val.productKey
+  deviceList.value = []
+  getThingModelTSL()
+}
+
+/** 处理设备选择 */
+const handleDeviceSelect = (val: DeviceVO[]) => {
+  deviceList.value = val
+  actionConfig.value.deviceControl!.deviceNames = val.map((item) => item.deviceName)
+}
+
+/** 打开设备选择器 */
+const openDeviceSelect = () => {
+  if (!product.value) {
+    message.warning('请先选择一个产品')
+    return
+  }
+  deviceTableSelectRef.value?.open()
+}
+
+/** 获取产品物模型 */
+const thingModelTSL = ref<any>()
+const getThingModelTSL = async () => {
+  if (!product.value) {
+    return
+  }
+  thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(product.value.id)
+}
+</script>

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

@@ -0,0 +1,83 @@
+<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="选择接收方式"
+      >
+        <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> 

+ 51 - 0
src/views/iot/rule/scene/components/action/DataBridgeAction.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="bg-[#dbe5f6] p-10px">
+    <div class="flex items-center">
+      <span class="mr-10px w-80px">数据桥接</span>
+      <el-select
+        v-model="dataBridgeId"
+        class="!w-240px"
+        clearable
+        placeholder="选择数据桥接"
+      >
+        <el-option
+          v-for="bridge in dataBridgeList"
+          :key="bridge.id"
+          :label="bridge.name"
+          :value="bridge.id"
+        />
+      </el-select>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+
+/** 数据桥接执行器组件 */
+defineOptions({ name: 'DataBridgeAction' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataBridgeId = useVModel(props, 'modelValue', emits)
+
+// 模拟数据桥接列表,实际项目中应该从API获取
+const dataBridgeList = ref([
+  { id: 1, name: '数据桥接1' },
+  { id: 2, name: '数据桥接2' },
+  { id: 3, name: '数据桥接3' }
+])
+
+// 实际项目中,应该从API获取数据桥接列表
+// const getDataBridgeList = async () => {
+//   try {
+//     dataBridgeList.value = await DataBridgeApi.getSimpleList()
+//   } catch (error) {
+//     console.error('获取数据桥接列表失败', error)
+//   }
+// }
+
+// onMounted(() => {
+//   getDataBridgeList()
+// })
+</script> 

+ 95 - 0
src/views/iot/rule/scene/components/action/DeviceActionControl.vue

@@ -0,0 +1,95 @@
+<template>
+  <div class="device-action-control">
+    <!-- 属性设置 -->
+    <template v-if="messageType === IotDeviceMessageTypeEnum.PROPERTY">
+      <div class="flex flex-wrap gap-10px">
+        <div v-for="model in thingModels" :key="model.identifier" class="flex items-center mb-10px">
+          <span class="mr-10px inline-block min-w-80px">{{ model.name }}</span>
+          <!-- 根据属性类型渲染不同的输入控件 -->
+          <PropertyValueInput
+            :model-value="propertyData[model.identifier]"
+            :data-specs="model.dataSpecs"
+            @update:model-value="(val) => updatePropertyValue(model.identifier, val)"
+          />
+        </div>
+      </div>
+    </template>
+
+    <!-- 服务调用 -->
+    <template v-else-if="messageType === IotDeviceMessageTypeEnum.SERVICE">
+      <div class="flex flex-wrap gap-10px">
+        <template v-if="thingModels && thingModels.inputParams">
+          <div 
+            v-for="param in thingModels.inputParams" 
+            :key="param.identifier" 
+            class="flex items-center mb-10px"
+          >
+            <span class="mr-10px inline-block min-w-80px">{{ param.name }}</span>
+            <!-- 根据服务输入参数类型渲染不同的输入控件 -->
+            <PropertyValueInput
+              :model-value="serviceData[param.identifier]"
+              :data-specs="param.dataSpecs"
+              @update:model-value="(val) => updateServiceValue(param.identifier, val)"
+            />
+          </div>
+        </template>
+        <div v-else class="text-gray-500">该服务没有输入参数</div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import PropertyValueInput from './PropertyValueInput.vue'
+import { IotDeviceMessageTypeEnum } from '@/api/iot/rule/scene/scene.types'
+
+/** 设备控制执行器的参数配置组件 */
+defineOptions({ name: 'DeviceActionControl' })
+
+const props = defineProps<{
+  modelValue: any
+  thingModels: any
+  messageType: string
+  identifier: string
+}>()
+
+const emits = defineEmits(['update:modelValue'])
+const actionData = useVModel(props, 'modelValue', emits)
+
+// 属性数据
+const propertyData = computed({
+  get: () => {
+    return actionData.value || {}
+  },
+  set: (value) => {
+    actionData.value = value
+  }
+})
+
+// 服务数据
+const serviceData = computed({
+  get: () => {
+    return actionData.value || {}
+  },
+  set: (value) => {
+    actionData.value = value
+  }
+})
+
+// 更新属性值
+const updatePropertyValue = (identifier: string, value: any) => {
+  const newData = { ...propertyData.value }
+  newData[identifier] = value
+  propertyData.value = newData
+}
+
+// 更新服务参数值
+const updateServiceValue = (identifier: string, value: any) => {
+  const newData = { ...serviceData.value }
+  newData[identifier] = value
+  serviceData.value = newData
+}
+</script>
+
+<style scoped lang="scss"></style> 

+ 162 - 0
src/views/iot/rule/scene/components/action/DeviceControlAction.vue

@@ -0,0 +1,162 @@
+<template>
+  <div class="bg-[#dbe5f6] p-10px">
+    <div class="flex items-center mb-10px">
+      <span class="mr-10px w-60px">产品</span>
+      <el-button type="primary" @click="productTableSelectRef?.open()" size="small" plain>
+        {{ product ? product.name : '选择产品' }}
+      </el-button>
+    </div>
+    <div class="flex items-center mb-10px">
+      <span class="mr-10px w-60px">设备</span>
+      <el-button type="primary" @click="openDeviceSelect" size="small" plain>
+        {{ isEmpty(deviceList) ? '选择设备' : deviceControlConfig.deviceNames.join(',') }}
+      </el-button>
+    </div>
+    <div class="flex items-center mb-10px">
+      <span class="mr-10px w-60px">消息类型</span>
+      <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 v-if="deviceControlConfig.type" class="flex items-center mb-10px">
+      <span class="mr-10px w-60px">具体操作</span>
+      <el-select
+        v-model="deviceControlConfig.identifier"
+        class="!w-240px"
+        clearable
+        placeholder="选择操作项"
+      >
+        <template v-if="deviceControlConfig.type === IotDeviceMessageTypeEnum.PROPERTY">
+          <el-option :value="IotDeviceMessageIdentifierEnum.PROPERTY_SET" label="属性设置" />
+        </template>
+        <template v-else-if="deviceControlConfig.type === IotDeviceMessageTypeEnum.SERVICE">
+          <el-option
+            v-for="model in thingModelTSL?.services"
+            :key="model.identifier"
+            :label="model.name"
+            :value="model.identifier"
+          />
+        </template>
+      </el-select>
+    </div>
+    <DeviceActionControl
+      v-if="deviceControlConfig.identifier && deviceControlConfig.type"
+      :model-value="deviceControlConfig.data"
+      :thing-models="getThingModels()"
+      :message-type="deviceControlConfig.type"
+      :identifier="deviceControlConfig.identifier"
+      @update:model-value="(val) => (deviceControlConfig.data = val)"
+    />
+  </div>
+
+  <!-- 产品、设备的选择 -->
+  <ProductTableSelect ref="productTableSelectRef" @success="handleProductSelect" />
+  <DeviceTableSelect
+    ref="deviceTableSelectRef"
+    multiple
+    :product-id="product?.id"
+    @success="handleDeviceSelect"
+  />
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { isEmpty } from '@/utils/is'
+import ProductTableSelect from '@/views/iot/product/product/components/ProductTableSelect.vue'
+import DeviceTableSelect from '@/views/iot/device/device/components/DeviceTableSelect.vue'
+import DeviceActionControl from './DeviceActionControl.vue'
+import { ProductVO } from '@/api/iot/product/product'
+import { DeviceVO } from '@/api/iot/device/device'
+import { ThingModelApi } from '@/api/iot/thingmodel'
+import {
+  ActionDeviceControl,
+  IotDeviceMessageIdentifierEnum,
+  IotDeviceMessageTypeEnum
+} from '@/api/iot/rule/scene/scene.types'
+
+/** 设备控制执行器组件 */
+defineOptions({ name: 'DeviceControlAction' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const deviceControlConfig = useVModel(props, 'modelValue', emits) as Ref<ActionDeviceControl>
+
+const message = useMessage()
+
+// 初始化设备控制执行器结构
+const initDeviceControlConfig = () => {
+  if (!deviceControlConfig.value) {
+    deviceControlConfig.value = {
+      productKey: '',
+      deviceNames: [],
+      type: IotDeviceMessageTypeEnum.PROPERTY,
+      identifier: IotDeviceMessageIdentifierEnum.PROPERTY_SET,
+      data: {}
+    } as ActionDeviceControl
+  }
+}
+
+// 初始化
+onMounted(() => {
+  initDeviceControlConfig()
+})
+
+// 产品和设备选择
+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
+  deviceControlConfig.value.productKey = val.productKey
+  deviceList.value = []
+  getThingModelTSL()
+}
+
+/** 处理设备选择 */
+const handleDeviceSelect = (val: DeviceVO[]) => {
+  deviceList.value = val
+  deviceControlConfig.value.deviceNames = val.map((item) => item.deviceName)
+}
+
+/** 打开设备选择器 */
+const openDeviceSelect = () => {
+  if (!product.value) {
+    message.warning('请先选择一个产品')
+    return
+  }
+  deviceTableSelectRef.value?.open()
+}
+
+/** 获取产品物模型 */
+const thingModelTSL = ref<any>()
+const getThingModelTSL = async () => {
+  if (!product.value) {
+    return
+  }
+  thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(product.value.id)
+}
+
+/** 根据消息类型和标识符获取对应的物模型 */
+const getThingModels = () => {
+  if (!deviceControlConfig.value) return []
+  
+  switch (deviceControlConfig.value.type) {
+    case IotDeviceMessageTypeEnum.PROPERTY:
+      return thingModelTSL.value?.properties || []
+    case IotDeviceMessageTypeEnum.SERVICE:
+      return thingModelTSL.value?.services.find(
+        (s: any) => s.identifier === deviceControlConfig.value.identifier
+      ) || {}
+  }
+  return []
+}
+</script> 

+ 238 - 0
src/views/iot/rule/scene/components/action/PropertyValueInput.vue

@@ -0,0 +1,238 @@
+<template>
+  <div class="property-value-input">
+    <!-- 文本类型 -->
+    <template v-if="dataType === 'text'">
+      <el-input
+        v-model="inputValue"
+        class="!w-240px"
+        :placeholder="`请输入${dataSpecs?.name || '值'}`"
+      >
+        <template v-if="dataSpecs?.unitName" #append>{{ dataSpecs.unitName }}</template>
+      </el-input>
+    </template>
+
+    <!-- 数值类型 -->
+    <template v-else-if="['int', 'float', 'double'].includes(dataType)">
+      <el-input-number
+        v-model="inputValue"
+        class="!w-240px"
+        :min="dataSpecs?.min"
+        :max="dataSpecs?.max"
+        :precision="dataType === 'int' ? 0 : 2"
+        :step="dataType === 'int' ? 1 : 0.1"
+        :placeholder="`请输入${dataSpecs?.name || '值'}`"
+      />
+      <span v-if="dataSpecs?.unitName" class="ml-5px">{{ dataSpecs.unitName }}</span>
+    </template>
+
+    <!-- 枚举类型 -->
+    <template v-else-if="dataType === 'enum'">
+      <el-select
+        v-model="inputValue"
+        class="!w-240px"
+        clearable
+        :placeholder="`请选择${dataSpecs?.name || '值'}`"
+      >
+        <el-option
+          v-for="item in dataSpecs?.enumList"
+          :key="item.value"
+          :label="item.text"
+          :value="item.value"
+        />
+      </el-select>
+    </template>
+
+    <!-- 布尔类型 -->
+    <template v-else-if="dataType === 'bool'">
+      <el-radio-group v-model="inputValue">
+        <el-radio
+          v-for="item in [
+            { value: true, label: dataSpecs?.trueText || '是' },
+            { value: false, label: dataSpecs?.falseText || '否' }
+          ]"
+          :key="String(item.value)"
+          :label="item.value"
+        >
+          {{ item.label }}
+        </el-radio>
+      </el-radio-group>
+    </template>
+
+    <!-- 日期类型 -->
+    <template v-else-if="dataType === 'date'">
+      <el-date-picker
+        v-model="inputValue"
+        class="!w-240px"
+        type="datetime"
+        :placeholder="`请选择${dataSpecs?.name || '日期'}`"
+      />
+    </template>
+
+    <!-- 结构体类型 -->
+    <template v-else-if="dataType === 'struct'">
+      <el-collapse class="w-full">
+        <el-collapse-item title="结构体参数">
+          <div
+            v-for="field in dataSpecs?.specs"
+            :key="field.identifier"
+            class="flex items-center mb-10px"
+          >
+            <span class="mr-10px inline-block min-w-80px">{{ field.name }}</span>
+            <PropertyValueInput
+              :model-value="getStructFieldValue(field.identifier)"
+              :data-specs="field"
+              @update:model-value="(val) => updateStructField(field.identifier, val)"
+            />
+          </div>
+        </el-collapse-item>
+      </el-collapse>
+    </template>
+
+    <!-- 数组类型 -->
+    <template v-else-if="dataType === 'array'">
+      <div class="flex flex-col w-full">
+        <div v-for="(item, index) in arrayValue" :key="index" class="flex items-center mb-10px">
+          <PropertyValueInput
+            :model-value="item"
+            :data-specs="dataSpecs?.arrayInfo"
+            @update:model-value="(val) => updateArrayItem(index, val)"
+          />
+          <el-button
+            type="danger"
+            size="small"
+            circle
+            class="ml-10px"
+            @click="removeArrayItem(index)"
+          >
+            <Icon icon="ep:delete" />
+          </el-button>
+        </div>
+        <el-button type="primary" size="small" plain @click="addArrayItem">添加项</el-button>
+      </div>
+    </template>
+
+    <!-- 默认文本输入 -->
+    <template v-else>
+      <el-input
+        v-model="inputValue"
+        class="!w-240px"
+        :placeholder="`请输入${dataSpecs?.name || '值'}`"
+      />
+    </template>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+
+/** 属性值输入组件 */
+defineOptions({ name: 'PropertyValueInput' })
+
+const props = defineProps<{
+  modelValue: any
+  dataSpecs: any
+}>()
+
+const emits = defineEmits(['update:modelValue'])
+
+// 获取数据类型
+const dataType = computed(() => {
+  return props.dataSpecs?.dataType
+})
+
+// 基本类型的输入值
+const inputValue = computed({
+  get: () => {
+    // 对于日期类型,如果是字符串需要转换为日期对象
+    if (dataType.value === 'date' && typeof props.modelValue === 'string') {
+      return new Date(props.modelValue)
+    }
+    return props.modelValue
+  },
+  set: (value) => {
+    // 对于日期类型,需要转换为ISO字符串
+    if (dataType.value === 'date' && value instanceof Date) {
+      emits('update:modelValue', value.toISOString())
+      return
+    }
+    emits('update:modelValue', value)
+  }
+})
+
+// 结构体类型处理
+const structValue = computed({
+  get: () => {
+    return props.modelValue || {}
+  },
+  set: (value) => {
+    emits('update:modelValue', value)
+  }
+})
+
+// 获取结构体字段值
+const getStructFieldValue = (identifier: string) => {
+  return structValue.value[identifier]
+}
+
+// 更新结构体字段
+const updateStructField = (identifier: string, value: any) => {
+  const newStruct = { ...structValue.value }
+  newStruct[identifier] = value
+  structValue.value = newStruct
+}
+
+// 数组类型处理
+const arrayValue = computed({
+  get: () => {
+    return Array.isArray(props.modelValue) ? props.modelValue : []
+  },
+  set: (value) => {
+    emits('update:modelValue', value)
+  }
+})
+
+// 添加数组项
+const addArrayItem = () => {
+  const newArray = [...arrayValue.value]
+  // 根据数组元素类型创建默认值
+  const arrayItemType = props.dataSpecs?.arrayInfo?.dataType
+  let defaultValue = null
+  switch (arrayItemType) {
+    case 'int':
+    case 'float':
+    case 'double':
+      defaultValue = 0
+      break
+    case 'bool':
+      defaultValue = false
+      break
+    case 'text':
+      defaultValue = ''
+      break
+    case 'struct':
+      defaultValue = {}
+      break
+    case 'array':
+      defaultValue = []
+      break
+  }
+  newArray.push(defaultValue)
+  arrayValue.value = newArray
+}
+
+// 更新数组项
+const updateArrayItem = (index: number, value: any) => {
+  const newArray = [...arrayValue.value]
+  newArray[index] = value
+  arrayValue.value = newArray
+}
+
+// 移除数组项
+const removeArrayItem = (index: number) => {
+  const newArray = [...arrayValue.value]
+  newArray.splice(index, 1)
+  arrayValue.value = newArray
+}
+</script>
+
+<style scoped lang="scss"></style>

+ 0 - 0
src/views/iot/rule/scene/components/ConditionSelector.vue → src/views/iot/rule/scene/components/listener/ConditionSelector.vue


+ 0 - 0
src/views/iot/rule/scene/components/DeviceListener.vue → src/views/iot/rule/scene/components/listener/DeviceListener.vue


+ 0 - 0
src/views/iot/rule/scene/components/DeviceListenerCondition.vue → src/views/iot/rule/scene/components/listener/DeviceListenerCondition.vue