Jelajahi Sumber

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

芋道源码 7 bulan lalu
induk
melakukan
e53a676bf6

+ 10 - 7
src/api/iot/rule/scene/index.ts

@@ -1,5 +1,5 @@
 import request from '@/config/axios'
-import { IotRuleScene } from './scene.types'
+import { IotSceneRule } from './scene.types'
 
 // IoT 场景联动 API
 export const RuleSceneApi = {
@@ -14,21 +14,24 @@ export const RuleSceneApi = {
   },
 
   // 新增场景联动
-  createRuleScene: async (data: IotRuleScene) => {
+  createRuleScene: async (data: IotSceneRule) => {
     return await request.post({ url: `/iot/rule-scene/create`, data })
   },
 
   // 修改场景联动
-  updateRuleScene: async (data: IotRuleScene) => {
+  updateRuleScene: async (data: IotSceneRule) => {
     return await request.put({ url: `/iot/rule-scene/update`, data })
   },
 
   // 修改场景联动
   updateRuleSceneStatus: async (id: number, status: number) => {
-    return await request.put({ url: `/iot/rule-scene/update-status`, data: {
-      id,
-      status
-    }})
+    return await request.put({
+      url: `/iot/rule-scene/update-status`,
+      data: {
+        id,
+        status
+      }
+    })
   },
 
   // 删除场景联动

+ 13 - 131
src/api/iot/rule/scene/scene.types.ts

@@ -137,122 +137,18 @@ export interface PropertySelectorItem {
 
 // ========== 场景联动规则相关接口定义 ==========
 
-// 基础接口(如果项目中有全局的 BaseDO,可以使用全局的)
-interface TenantBaseDO {
-  createTime?: Date // 创建时间
-  updateTime?: Date // 更新时间
-  creator?: string // 创建者
-  updater?: string // 更新者
-  deleted?: boolean // 是否删除
-  tenantId?: number // 租户编号
-}
-
-// 触发条件参数
-interface TriggerConditionParameter {
-  identifier0?: string // 标识符(事件、服务)
-  identifier?: string // 标识符(属性)
-  operator: string // 操作符(必填)
-  value: string // 比较值(必填,多值用逗号分隔)
-}
-
-// 触发条件
-interface TriggerCondition {
-  type: string // 消息类型
-  identifier: string // 消息标识符
-  parameters: TriggerConditionParameter[] // 参数数组
-}
-
-// 触发器配置
-interface TriggerConfig {
-  key?: string // 组件唯一标识符,用于解决索引重用问题
-  type: number // 触发类型(必填)
-  productKey?: string // 产品标识(设备触发时必填)
-  deviceNames?: string[] // 设备名称数组(设备触发时必填)
-  conditions?: TriggerCondition[] // 触发条件数组(设备触发时必填)
-  cronExpression?: string // CRON表达式(定时触发时必填)
-}
-
-// 执行设备控制
-interface ActionDeviceControl {
-  productKey: string // 产品标识(必填)
-  deviceNames: string[] // 设备名称数组(必填)
-  type: string // 消息类型(必填)
-  identifier: string // 消息标识符(必填)
-  params: Record<string, any> // 参数对象(必填)- 统一使用 params 字段
-}
-
-// 执行器配置
-interface ActionConfig {
-  key?: string // 组件唯一标识符,用于解决索引重用问题
-  type: number // 执行类型(必填)
-  deviceControl?: ActionDeviceControl // 设备控制(设备控制时必填)
-  alertConfigId?: number // 告警配置ID(告警恢复时必填)
-}
-
-// 表单数据接口 - 直接对应后端 DO 结构
-interface RuleSceneFormData {
-  id?: number
-  name: string
-  description?: string
-  status: number
-  triggers: TriggerFormData[] // 支持多个触发器
-  actions: ActionFormData[]
-}
-
-// 触发器表单数据 - 直接对应 TriggerDO
-interface TriggerFormData {
-  type: number // 触发类型
-  productId?: number // 产品编号
-  deviceId?: number // 设备编号
-  identifier?: string // 物模型标识符
-  operator?: string // 操作符
-  value?: string // 参数值
-  cronExpression?: string // CRON 表达式
-  conditionGroups?: TriggerConditionFormData[][] // 条件组(二维数组)
-}
-
-// 触发条件表单数据 - 直接对应 TriggerConditionDO
-interface TriggerConditionFormData {
-  type: number // 条件类型:1-设备状态,2-设备属性,3-当前时间
-  productId?: number // 产品编号
-  deviceId?: number // 设备编号
-  identifier?: string // 标识符
-  operator: string // 操作符
-  param: string // 参数值
-}
-
-// 执行器表单数据 - 直接对应 ActionDO
-interface ActionFormData {
-  type: number // 执行类型
-  productId?: number // 产品编号
-  deviceId?: number // 设备编号
-  identifier?: string // 物模型标识符(服务调用时使用)
-  params?: Record<string, any> // 请求参数
-  alertConfigId?: number // 告警配置编号
-}
-
-// 主接口 - 原有的 API 接口格式(保持兼容性)
-interface IotRuleScene extends TenantBaseDO {
-  id?: number // 场景编号(新增时为空)
-  name: string // 场景名称(必填)
-  description?: string // 场景描述(可选)
-  status: number // 场景状态:0-开启,1-关闭
-  triggers: TriggerConfig[] // 触发器数组(必填,至少一个)
-  actions: ActionConfig[] // 执行器数组(必填,至少一个)
-}
-
 // 后端 DO 接口 - 匹配后端数据结构
-interface IotRuleSceneDO {
+interface IotSceneRule {
   id?: number // 场景编号
   name: string // 场景名称
   description?: string // 场景描述
   status: number // 场景状态:0-开启,1-关闭
-  triggers: TriggerDO[] // 触发器数组
-  actions: ActionDO[] // 执行器数组
+  triggers: Trigger[] // 触发器数组
+  actions: Action[] // 执行器数组
 }
 
 // 触发器 DO 结构
-interface TriggerDO {
+interface Trigger {
   type: number // 触发类型
   productId?: number // 产品编号
   deviceId?: number // 设备编号
@@ -260,12 +156,12 @@ interface TriggerDO {
   operator?: string // 操作符
   value?: string // 参数值
   cronExpression?: string // CRON 表达式
-  conditionGroups?: TriggerConditionDO[][] // 条件组(二维数组)
+  conditionGroups?: TriggerCondition[][] // 条件组(二维数组)
 }
 
 // 触发条件 DO 结构
-interface TriggerConditionDO {
-  type: number // 条件类型
+interface TriggerCondition {
+  type: number // 条件类型:1-设备状态,2-设备属性,3-当前时间
   productId?: number // 产品编号
   deviceId?: number // 设备编号
   identifier?: string // 标识符
@@ -274,12 +170,12 @@ interface TriggerConditionDO {
 }
 
 // 执行器 DO 结构
-interface ActionDO {
+interface Action {
   type: number // 执行类型
   productId?: number // 产品编号
   deviceId?: number // 设备编号
   identifier?: string // 物模型标识符(服务调用时使用)
-  params?: Record<string, any> // 请求参数
+  params?: string // 请求参数
   alertConfigId?: number // 告警配置编号
 }
 
@@ -298,23 +194,9 @@ interface FormValidationRules {
   [key: string]: ValidationRule[]
 }
 
+// 表单数据类型别名
+export type TriggerFormData = Trigger
+
 // TODO @puhui999:这个文件,目标最终没有哈,和别的模块一致;
 
-export {
-  IotRuleScene,
-  IotRuleSceneDO,
-  TriggerDO,
-  TriggerConditionDO,
-  ActionDO,
-  TriggerConfig,
-  TriggerCondition,
-  TriggerConditionParameter,
-  ActionConfig,
-  ActionDeviceControl,
-  RuleSceneFormData,
-  TriggerFormData,
-  TriggerConditionFormData,
-  ActionFormData,
-  ValidationRule,
-  FormValidationRules
-}
+export { IotSceneRule, Trigger, TriggerCondition, Action, ValidationRule, FormValidationRules }

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

@@ -36,7 +36,7 @@ import { useVModel } from '@vueuse/core'
 import BasicInfoSection from './sections/BasicInfoSection.vue'
 import TriggerSection from './sections/TriggerSection.vue'
 import ActionSection from './sections/ActionSection.vue'
-import { IotRuleSceneDO, RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
+import { IotSceneRule } from '@/api/iot/rule/scene/scene.types'
 import { RuleSceneApi } from '@/api/iot/rule/scene'
 import {
   IotRuleSceneTriggerTypeEnum,
@@ -54,7 +54,7 @@ const props = defineProps<{
   /** 抽屉显示状态 */
   modelValue: boolean
   /** 编辑的场景联动规则数据 */
-  ruleScene?: IotRuleSceneDO
+  ruleScene?: IotSceneRule
 }>()
 
 /** 组件事件定义 */
@@ -66,7 +66,7 @@ const emit = defineEmits<{
 const drawerVisible = useVModel(props, 'modelValue', emit) // 是否可见
 
 /** 创建默认的表单数据 */
-const createDefaultFormData = (): RuleSceneFormData => {
+const createDefaultFormData = (): IotSceneRule => {
   return {
     name: '',
     description: '',
@@ -89,7 +89,7 @@ const createDefaultFormData = (): RuleSceneFormData => {
 
 // 表单数据和状态
 const formRef = ref()
-const formData = ref<RuleSceneFormData>(createDefaultFormData())
+const formData = ref<IotSceneRule>(createDefaultFormData())
 // 自定义校验器
 const validateTriggers = (_rule: any, value: any, callback: any) => {
   if (!value || !Array.isArray(value) || value.length === 0) {

+ 47 - 228
src/views/iot/rule/scene/form/configs/AlertConfig.vue

@@ -1,262 +1,81 @@
 <!-- 告警配置组件 -->
 <template>
   <div class="w-full">
-    <!-- 告警配置选择区域 -->
-    <div
-      class="border border-[var(--el-border-color-light)] rounded-6px p-16px bg-[var(--el-fill-color-blank)]"
-    >
-      <div class="flex items-center gap-8px mb-12px">
-        <Icon icon="ep:bell" class="text-[var(--el-color-warning)] text-16px" />
-        <span class="text-14px font-600 text-[var(--el-text-color-primary)]">告警配置选择</span>
-        <el-tag size="small" type="warning">必选</el-tag>
-      </div>
-
-      <el-form-item label="告警配置" required>
-        <el-select
-          v-model="localValue"
-          placeholder="请选择告警配置"
-          filterable
-          clearable
-          @change="handleChange"
-          class="w-full"
-          :loading="loading"
+    <el-form-item label="告警配置" required>
+      <el-select
+        v-model="localValue"
+        placeholder="请选择告警配置"
+        filterable
+        clearable
+        @change="handleChange"
+        class="w-full"
+        :loading="loading"
+      >
+        <el-option
+          v-for="config in alertConfigs"
+          :key="config.id"
+          :label="config.name"
+          :value="config.id"
         >
-          <template #empty>
-            <div class="text-center py-20px">
-              <Icon
-                icon="ep:warning"
-                class="text-24px text-[var(--el-text-color-placeholder)] mb-8px"
-              />
-              <p class="text-12px text-[var(--el-text-color-secondary)]">暂无可用的告警配置</p>
-            </div>
-          </template>
-          <el-option
-            v-for="config in alertConfigs"
-            :key="config.id"
-            :label="config.name"
-            :value="config.id"
-            :disabled="!config.enabled"
-          >
-            <div class="flex items-center justify-between w-full py-6px">
-              <div class="flex items-center gap-12px flex-1">
-                <Icon
-                  :icon="config.enabled ? 'ep:circle-check' : 'ep:circle-close'"
-                  :class="
-                    config.enabled
-                      ? 'text-[var(--el-color-success)]'
-                      : 'text-[var(--el-color-danger)]'
-                  "
-                  class="text-16px flex-shrink-0"
-                />
-                <div class="flex-1">
-                  <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{
-                    config.name
-                  }}</div>
-                  <div class="text-12px text-[var(--el-text-color-secondary)] line-clamp-1">{{
-                    config.description
-                  }}</div>
-                </div>
-              </div>
-              <div class="flex items-center gap-8px">
-                <el-tag :type="getNotifyTypeTag(config.notifyType)" size="small">
-                  {{ getNotifyTypeName(config.notifyType) }}
-                </el-tag>
-                <el-tag :type="config.enabled ? 'success' : 'danger'" size="small">
-                  {{ config.enabled ? '启用' : '禁用' }}
-                </el-tag>
-              </div>
-            </div>
-          </el-option>
-        </el-select>
-      </el-form-item>
-    </div>
-
-    <!-- 告警配置详情 -->
-    <div
-      v-if="selectedConfig"
-      class="mt-16px border border-[var(--el-border-color-light)] rounded-6px p-16px bg-gradient-to-r from-orange-50 to-yellow-50"
-    >
-      <div class="flex items-center gap-8px mb-16px">
-        <Icon icon="ep:info-filled" class="text-[var(--el-color-warning)] text-18px" />
-        <span class="text-16px font-600 text-[var(--el-text-color-primary)]">配置详情</span>
-      </div>
-
-      <div class="grid grid-cols-1 md:grid-cols-2 gap-16px">
-        <!-- 基本信息 -->
-        <div class="space-y-12px">
-          <div class="flex items-center gap-8px">
-            <Icon icon="ep:document" class="text-[var(--el-color-primary)] text-14px" />
-            <span class="text-14px font-500 text-[var(--el-text-color-primary)]">基本信息</span>
-          </div>
-          <div class="pl-22px space-y-8px">
-            <div class="flex items-start gap-8px">
-              <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">名称:</span>
-              <span class="text-12px text-[var(--el-text-color-primary)] flex-1 font-500">{{
-                selectedConfig.name
-              }}</span>
-            </div>
-            <div class="flex items-start gap-8px">
-              <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">描述:</span>
-              <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{
-                selectedConfig.description
-              }}</span>
-            </div>
-            <div class="flex items-start gap-8px">
-              <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">状态:</span>
-              <el-tag :type="selectedConfig.enabled ? 'success' : 'danger'" size="small">
-                {{ selectedConfig.enabled ? '启用' : '禁用' }}
-              </el-tag>
-            </div>
-          </div>
-        </div>
-
-        <!-- 通知配置 -->
-        <div class="space-y-12px">
-          <div class="flex items-center gap-8px">
-            <Icon icon="ep:message" class="text-[var(--el-color-success)] text-14px" />
-            <span class="text-14px font-500 text-[var(--el-text-color-primary)]">通知配置</span>
-          </div>
-          <div class="pl-22px space-y-8px">
-            <div class="flex items-start gap-8px">
-              <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">方式:</span>
-              <el-tag :type="getNotifyTypeTag(selectedConfig.notifyType)" size="small">
-                {{ getNotifyTypeName(selectedConfig.notifyType) }}
-              </el-tag>
-            </div>
-            <div
-              v-if="selectedConfig.receivers && selectedConfig.receivers.length > 0"
-              class="flex items-start gap-8px"
-            >
-              <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px"
-                >接收人:</span
-              >
-              <div class="flex-1">
-                <div class="flex flex-wrap gap-4px">
-                  <el-tag
-                    v-for="receiver in selectedConfig.receivers.slice(0, 3)"
-                    :key="receiver"
-                    size="small"
-                    type="info"
-                  >
-                    {{ receiver }}
-                  </el-tag>
-                  <el-tag v-if="selectedConfig.receivers.length > 3" size="small" type="info">
-                    +{{ selectedConfig.receivers.length - 3 }}
-                  </el-tag>
-                </div>
-              </div>
-            </div>
+          <div class="flex items-center justify-between">
+            <span>{{ config.name }}</span>
+            <el-tag :type="config.enabled ? 'success' : 'danger'" size="small">
+              {{ config.enabled ? '启用' : '禁用' }}
+            </el-tag>
           </div>
-        </div>
-      </div>
-    </div>
+        </el-option>
+      </el-select>
+    </el-form-item>
   </div>
 </template>
 
 <script setup lang="ts">
 import { useVModel } from '@vueuse/core'
+import { AlertConfigApi } from '@/api/iot/alert/config'
 
 /** 告警配置组件 */
 defineOptions({ name: 'AlertConfig' })
 
-interface Props {
+const props = defineProps<{
   modelValue?: number
-}
+}>()
 
-interface Emits {
+const emit = defineEmits<{
   (e: 'update:modelValue', value?: number): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 const localValue = useVModel(props, 'modelValue', emit)
 
-// 状态
-const loading = ref(false)
-const alertConfigs = ref<any[]>([])
+const loading = ref(false) // 加载状态
+const alertConfigs = ref<any[]>([]) // 告警配置列表
 
-// 计算属性
-const selectedConfig = computed(() => {
-  return alertConfigs.value.find((config) => config.id === localValue.value)
-})
-
-// 工具函数
-const getNotifyTypeName = (type: number) => {
-  const typeMap = {
-    1: '邮件通知',
-    2: '短信通知',
-    3: '微信通知',
-    4: '钉钉通知'
-  }
-  return typeMap[type] || '未知'
-}
-
-const getNotifyTypeTag = (type: number) => {
-  const tagMap = {
-    1: 'primary', // 邮件
-    2: 'success', // 短信
-    3: 'warning', // 微信
-    4: 'info' // 钉钉
-  }
-  return tagMap[type] || 'info'
-}
-
-// 事件处理
+/**
+ * 处理选择变化事件
+ * @param value 选中的值
+ */
 const handleChange = (value?: number) => {
-  // 可以在这里添加额外的处理逻辑
-  console.log('告警配置选择变化:', value)
+  emit('update:modelValue', value)
 }
 
-// API 调用
-const getAlertConfigs = async () => {
+/**
+ * 加载告警配置列表
+ */
+const loadAlertConfigs = async () => {
   loading.value = true
   try {
-    // 这里应该调用真实的API获取告警配置
-    // 暂时使用模拟数据
-    // TODO @puhui999:这里是模拟数据
-    alertConfigs.value = [
-      {
-        id: 1,
-        name: '设备异常告警',
-        description: '设备状态异常时发送告警',
-        enabled: true,
-        notifyType: 1,
-        receivers: ['admin@example.com', 'operator@example.com']
-      },
-      {
-        id: 2,
-        name: '温度超限告警',
-        description: '温度超过阈值时发送告警',
-        enabled: true,
-        notifyType: 2,
-        receivers: ['13800138000', '13900139000']
-      },
-      {
-        id: 3,
-        name: '系统故障告警',
-        description: '系统发生故障时发送告警',
-        enabled: false,
-        notifyType: 3,
-        receivers: ['技术支持群']
-      }
-    ]
-  } catch (error) {
-    console.error('获取告警配置失败:', error)
+    const data = await AlertConfigApi.getAlertConfigPage({
+      pageNo: 1,
+      pageSize: 100,
+      enabled: true // 只加载启用的配置
+    })
+    alertConfigs.value = data.list || []
   } finally {
     loading.value = false
   }
 }
 
-// 初始化
+// 组件挂载时加载数据
 onMounted(() => {
-  getAlertConfigs()
+  loadAlertConfigs()
 })
 </script>
-
-<style scoped>
-:deep(.el-select-dropdown__item) {
-  height: auto;
-  padding: 8px 20px;
-}
-</style>

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

@@ -123,7 +123,7 @@ import DeviceSelector from '../selectors/DeviceSelector.vue'
 import PropertySelector from '../selectors/PropertySelector.vue'
 import OperatorSelector from '../selectors/OperatorSelector.vue'
 import ValueInput from '../inputs/ValueInput.vue'
-import { TriggerConditionFormData } from '@/api/iot/rule/scene/scene.types'
+import { TriggerCondition } from '@/api/iot/rule/scene/scene.types'
 import {
   IotRuleSceneTriggerConditionTypeEnum,
   IotRuleSceneTriggerConditionParameterOperatorEnum
@@ -133,12 +133,12 @@ import {
 defineOptions({ name: 'ConditionConfig' })
 
 const props = defineProps<{
-  modelValue: TriggerConditionFormData
+  modelValue: TriggerCondition
   triggerType: number
 }>()
 
 const emit = defineEmits<{
-  (e: 'update:modelValue', value: TriggerConditionFormData): void
+  (e: 'update:modelValue', value: TriggerCondition): void
   (e: 'validate', result: { valid: boolean; message: string }): void
 }>()
 
@@ -155,12 +155,12 @@ const isValid = ref(true)
 const valueValidation = ref({ valid: true, message: '' })
 
 // 事件处理
-const updateConditionField = (field: keyof TriggerConditionFormData, value: any) => {
+const updateConditionField = (field: keyof TriggerCondition, value: any) => {
   ;(condition.value as any)[field] = value
   emit('update:modelValue', condition.value)
 }
 
-const updateCondition = (newCondition: TriggerConditionFormData) => {
+const updateCondition = (newCondition: TriggerCondition) => {
   condition.value = newCondition
   emit('update:modelValue', condition.value)
 }

File diff ditekan karena terlalu besar
+ 213 - 763
src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue


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

@@ -84,17 +84,17 @@
 import { useVModel } from '@vueuse/core'
 import ProductSelector from '../selectors/ProductSelector.vue'
 import DeviceSelector from '../selectors/DeviceSelector.vue'
-import { TriggerConditionFormData } from '@/api/iot/rule/scene/scene.types'
+import { TriggerCondition } from '@/api/iot/rule/scene/scene.types'
 
 /** 设备状态条件配置组件 */
 defineOptions({ name: 'DeviceStatusConditionConfig' })
 
 const props = defineProps<{
-  modelValue: TriggerConditionFormData
+  modelValue: TriggerCondition
 }>()
 
 const emit = defineEmits<{
-  (e: 'update:modelValue', value: TriggerConditionFormData): void
+  (e: 'update:modelValue', value: TriggerCondition): void
   (e: 'validate', result: { valid: boolean; message: string }): void
 }>()
 

+ 115 - 27
src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue

@@ -59,8 +59,14 @@
           </el-form-item>
         </el-col>
 
-        <!-- 操作符选择 - 服务调用不需要操作符 -->
-        <el-col v-if="triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE" :span="6">
+        <!-- 操作符选择 - 服务调用和事件上报不需要操作符 -->
+        <el-col
+          v-if="
+            triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE &&
+            triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
+          "
+          :span="6"
+        >
           <el-form-item label="操作符" required>
             <OperatorSelector
               :model-value="condition.operator"
@@ -72,7 +78,14 @@
         </el-col>
 
         <!-- 值输入 -->
-        <el-col :span="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE ? 18 : 12">
+        <el-col
+          :span="
+            triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE ||
+            triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
+              ? 18
+              : 12
+          "
+        >
           <el-form-item
             :label="
               triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
@@ -82,11 +95,21 @@
             required
           >
             <!-- 服务调用参数配置 -->
-            <ServiceParamsInput
+            <JsonParamsInput
               v-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE"
-              :model-value="condition.value"
-              @update:model-value="(value) => updateConditionField('value', value)"
-              :service-config="propertyConfig"
+              v-model="condition.value"
+              type="service"
+              :config="serviceConfig"
+              placeholder="请输入JSON格式的服务参数"
+              @validate="handleValueValidate"
+            />
+            <!-- 事件上报参数配置 -->
+            <JsonParamsInput
+              v-else-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST"
+              v-model="condition.value"
+              type="event"
+              :config="eventConfig"
+              placeholder="请输入JSON格式的事件参数"
               @validate="handleValueValidate"
             />
             <!-- 普通值输入 -->
@@ -106,11 +129,44 @@
 
     <!-- 设备状态条件配置 -->
     <div v-else-if="isDeviceStatusTrigger" class="space-y-16px">
-      <DeviceStatusConditionConfig
-        :model-value="condition"
-        @update:model-value="updateCondition"
-        @validate="handleValidate"
-      />
+      <!-- 设备状态触发器使用简化的配置 -->
+      <el-row :gutter="16">
+        <el-col :span="12">
+          <el-form-item label="产品" required>
+            <ProductSelector
+              :model-value="condition.productId"
+              @update:model-value="(value) => updateConditionField('productId', value)"
+              @change="handleProductChange"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="设备" required>
+            <DeviceSelector
+              :model-value="condition.deviceId"
+              @update:model-value="(value) => updateConditionField('deviceId', value)"
+              :product-id="condition.productId"
+              @change="handleDeviceChange"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="16">
+        <el-col :span="12">
+          <el-form-item label="操作符" required>
+            <el-select
+              :model-value="condition.operator"
+              @update:model-value="(value) => updateConditionField('operator', value)"
+              placeholder="请选择操作符"
+              class="w-full"
+            >
+              <el-option label="变为在线" value="online" />
+              <el-option label="变为离线" value="offline" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
     </div>
 
     <!-- 其他触发类型的提示 -->
@@ -131,8 +187,8 @@ import DeviceSelector from '../selectors/DeviceSelector.vue'
 import PropertySelector from '../selectors/PropertySelector.vue'
 import OperatorSelector from '../selectors/OperatorSelector.vue'
 import ValueInput from '../inputs/ValueInput.vue'
-import ServiceParamsInput from '../inputs/ServiceParamsInput.vue'
-import DeviceStatusConditionConfig from './DeviceStatusConditionConfig.vue'
+import JsonParamsInput from '../inputs/JsonParamsInput.vue'
+
 import { TriggerFormData } from '@/api/iot/rule/scene/scene.types'
 import { IotRuleSceneTriggerTypeEnum, getTriggerTypeOptions } from '@/views/iot/utils/constants'
 import { useVModel } from '@vueuse/core'
@@ -172,6 +228,35 @@ const isDeviceStatusTrigger = computed(() => {
   return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE
 })
 
+// 服务配置 - 用于 JsonParamsInput
+const serviceConfig = computed(() => {
+  if (
+    propertyConfig.value &&
+    props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+  ) {
+    return {
+      service: {
+        name: propertyConfig.value.name || '服务',
+        inputParams: propertyConfig.value.inputParams || []
+      }
+    }
+  }
+  return undefined
+})
+
+// 事件配置 - 用于 JsonParamsInput
+const eventConfig = computed(() => {
+  if (propertyConfig.value && props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
+    return {
+      event: {
+        name: propertyConfig.value.name || '事件',
+        outputParams: propertyConfig.value.outputParams || []
+      }
+    }
+  }
+  return undefined
+})
+
 // 获取触发类型文本
 // TODO @puhui999:是不是有枚举可以服用哈;
 const getTriggerTypeText = (type: number) => {
@@ -198,11 +283,6 @@ const updateConditionField = (field: keyof TriggerFormData, value: any) => {
   updateValidationResult()
 }
 
-const updateCondition = (value: TriggerFormData) => {
-  emit('update:modelValue', value)
-  updateValidationResult()
-}
-
 const handleTriggerTypeChange = (type: number) => {
   emit('trigger-type-change', type)
 }
@@ -224,6 +304,14 @@ const handlePropertyChange = (propertyInfo: any) => {
   if (propertyInfo) {
     propertyType.value = propertyInfo.type
     propertyConfig.value = propertyInfo.config
+
+    // 对于事件上报和服务调用,自动设置操作符为 '='
+    if (
+      props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
+      props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+    ) {
+      condition.value.operator = '='
+    }
   }
   updateValidationResult()
 }
@@ -232,14 +320,12 @@ const handleOperatorChange = () => {
   updateValidationResult()
 }
 
-const handleValueValidate = (_result: { valid: boolean; message: string }) => {
-  updateValidationResult()
-}
-
-const handleValidate = (result: { valid: boolean; message: string }) => {
+// 处理参数验证结果
+const handleValueValidate = (result: { valid: boolean; message: string }) => {
   isValid.value = result.valid
   validationMessage.value = result.message
   emit('validate', result)
+  updateValidationResult()
 }
 
 // 验证逻辑
@@ -268,9 +354,10 @@ const updateValidationResult = () => {
       return
     }
 
-    // 服务调用不需要操作符
+    // 服务调用和事件上报不需要操作符
     if (
       props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE &&
+      props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST &&
       !condition.value.operator
     ) {
       isValid.value = false
@@ -298,8 +385,9 @@ watch(
     condition.value.productId,
     condition.value.deviceId,
     condition.value.identifier,
-    // 服务调用不需要监听操作符
-    props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+    // 服务调用和事件上报不需要监听操作符
+    props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE &&
+    props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
       ? condition.value.operator
       : null,
     condition.value.value

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

@@ -83,7 +83,7 @@
 import { nextTick } from 'vue'
 import { useVModel } from '@vueuse/core'
 import ConditionConfig from './ConditionConfig.vue'
-import { TriggerConditionFormData } from '@/api/iot/rule/scene/scene.types'
+import { TriggerCondition } from '@/api/iot/rule/scene/scene.types'
 import {
   IotRuleSceneTriggerConditionTypeEnum,
   IotRuleSceneTriggerConditionParameterOperatorEnum
@@ -93,13 +93,13 @@ import {
 defineOptions({ name: 'SubConditionGroupConfig' })
 
 const props = defineProps<{
-  modelValue: TriggerConditionFormData[]
+  modelValue: TriggerCondition[]
   triggerType: number
   maxConditions?: number
 }>()
 
 const emit = defineEmits<{
-  (e: 'update:modelValue', value: TriggerConditionFormData[]): void
+  (e: 'update:modelValue', value: TriggerCondition[]): void
   (e: 'validate', result: { valid: boolean; message: string }): void
 }>()
 
@@ -123,7 +123,7 @@ const addCondition = () => {
     return
   }
 
-  const newCondition: TriggerConditionFormData = {
+  const newCondition: TriggerCondition = {
     type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
     productId: undefined,
     deviceId: undefined,
@@ -161,7 +161,7 @@ const removeCondition = (index: number) => {
   }
 }
 
-const updateCondition = (index: number, condition: TriggerConditionFormData) => {
+const updateCondition = (index: number, condition: TriggerCondition) => {
   if (subGroup.value) {
     subGroup.value[index] = condition
   }

+ 526 - 0
src/views/iot/rule/scene/form/inputs/JsonParamsInput.vue

@@ -0,0 +1,526 @@
+<!-- JSON参数输入组件 - 通用版本 -->
+<template>
+  <div class="w-full min-w-0">
+    <!-- 参数配置 -->
+    <div v-if="hasConfig" class="space-y-12px">
+      <!-- JSON 输入框 -->
+      <div class="relative">
+        <el-input
+          v-model="paramsJson"
+          type="textarea"
+          :rows="4"
+          :placeholder="placeholder"
+          @input="handleParamsChange"
+          :class="{ 'is-error': jsonError }"
+        />
+        <!-- 查看详细示例弹出层 -->
+        <div class="absolute top-8px right-8px">
+          <el-popover
+            placement="left-start"
+            :width="450"
+            trigger="click"
+            :show-arrow="true"
+            :offset="8"
+            popper-class="json-params-detail-popover"
+          >
+            <template #reference>
+              <el-button type="info" :icon="InfoFilled" circle size="small" title="查看参数示例" />
+            </template>
+
+            <!-- 弹出层内容 -->
+            <div class="json-params-detail-content">
+              <div class="flex items-center gap-8px mb-16px">
+                <Icon :icon="titleIcon" class="text-[var(--el-color-primary)] text-18px" />
+                <span class="text-16px font-600 text-[var(--el-text-color-primary)]">
+                  {{ title }}
+                </span>
+              </div>
+
+              <div class="space-y-16px">
+                <!-- 参数列表 -->
+                <div v-if="paramsList.length > 0">
+                  <div class="flex items-center gap-8px mb-8px">
+                    <Icon :icon="paramsIcon" class="text-[var(--el-color-primary)] text-14px" />
+                    <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
+                      {{ paramsLabel }}
+                    </span>
+                  </div>
+                  <div class="ml-22px space-y-8px">
+                    <div
+                      v-for="param in paramsList"
+                      :key="param.identifier"
+                      class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
+                    >
+                      <div class="flex-1">
+                        <div class="text-12px font-500 text-[var(--el-text-color-primary)]">
+                          {{ param.name }}
+                          <el-tag v-if="param.required" size="small" type="danger" class="ml-4px">
+                            必填
+                          </el-tag>
+                        </div>
+                        <div class="text-11px text-[var(--el-text-color-secondary)]">
+                          {{ param.identifier }}
+                        </div>
+                      </div>
+                      <div class="flex items-center gap-8px">
+                        <el-tag :type="getParamTypeTag(param.dataType)" size="small">
+                          {{ getParamTypeName(param.dataType) }}
+                        </el-tag>
+                        <span class="text-11px text-[var(--el-text-color-secondary)]">
+                          {{ getExampleValue(param) }}
+                        </span>
+                      </div>
+                    </div>
+                  </div>
+
+                  <div class="mt-12px ml-22px">
+                    <div class="text-12px text-[var(--el-text-color-secondary)] mb-6px">
+                      完整 JSON 格式:
+                    </div>
+                    <pre
+                      class="p-12px bg-[var(--el-fill-color-light)] rounded-4px text-11px text-[var(--el-text-color-primary)] overflow-x-auto border-l-3px border-[var(--el-color-primary)]"
+                    >
+                      <code>{{ generateExampleJson() }}</code>
+                    </pre>
+                  </div>
+                </div>
+
+                <!-- 无参数提示 -->
+                <div v-else>
+                  <div class="text-center py-16px">
+                    <p class="text-14px text-[var(--el-text-color-secondary)]">{{
+                      emptyMessage
+                    }}</p>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </el-popover>
+        </div>
+      </div>
+
+      <!-- 验证状态和错误提示 -->
+      <div class="flex items-center justify-between">
+        <div class="flex items-center gap-8px">
+          <Icon
+            :icon="jsonError ? 'ep:warning' : 'ep:circle-check'"
+            :class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
+            class="text-14px"
+          />
+          <span
+            :class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
+            class="text-12px"
+          >
+            {{ jsonError || 'JSON 格式正确' }}
+          </span>
+        </div>
+
+        <!-- 快速填充按钮 -->
+        <div v-if="paramsList.length > 0" class="flex items-center gap-8px">
+          <span class="text-12px text-[var(--el-text-color-secondary)]">快速填充:</span>
+          <el-button size="small" type="primary" plain @click="fillExampleJson">
+            示例数据
+          </el-button>
+          <el-button size="small" type="danger" plain @click="clearParams"> 清空</el-button>
+        </div>
+      </div>
+    </div>
+
+    <!-- 无配置提示 -->
+    <div v-else class="text-center py-20px">
+      <p class="text-14px text-[var(--el-text-color-secondary)]">{{ noConfigMessage }}</p>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { InfoFilled } from '@element-plus/icons-vue'
+
+/** JSON参数输入组件 - 通用版本 */
+defineOptions({ name: 'JsonParamsInput' })
+
+export interface JsonParamsConfig {
+  // 服务配置
+  service?: {
+    name: string
+    inputParams?: any[]
+  }
+  // 事件配置
+  event?: {
+    name: string
+    outputParams?: any[]
+  }
+  // 属性配置
+  properties?: any[]
+  // 自定义配置
+  custom?: {
+    name: string
+    params: any[]
+  }
+}
+
+interface Props {
+  modelValue?: string
+  config?: JsonParamsConfig
+  type?: 'service' | 'event' | 'property' | 'custom'
+  placeholder?: string
+}
+
+interface Emits {
+  (e: 'update:modelValue', value: string): void
+  (e: 'validate', result: { valid: boolean; message: string }): void
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  type: 'service',
+  placeholder: '请输入JSON格式的参数'
+})
+
+const emit = defineEmits<Emits>()
+
+const localValue = useVModel(props, 'modelValue', emit, {
+  defaultValue: ''
+})
+
+// 状态
+const paramsJson = ref('')
+const jsonError = ref('')
+
+// 计算属性
+const hasConfig = computed(() => {
+  // TODO @puhui999: 后续统一处理
+  console.log(props.config)
+  // return !!(
+  //   props.config?.service ||
+  //   props.config?.event ||
+  //   props.config?.properties ||
+  //   props.config?.custom
+  // )
+  return true
+})
+
+const paramsList = computed(() => {
+  switch (props.type) {
+    case 'service':
+      return props.config?.service?.inputParams || []
+    case 'event':
+      return props.config?.event?.outputParams || []
+    case 'property':
+      return props.config?.properties || []
+    case 'custom':
+      return props.config?.custom?.params || []
+    default:
+      return []
+  }
+})
+
+const title = computed(() => {
+  switch (props.type) {
+    case 'service':
+      return `${props.config?.service?.name || '服务'} - 输入参数示例`
+    case 'event':
+      return `${props.config?.event?.name || '事件'} - 输出参数示例`
+    case 'property':
+      return '属性设置 - 参数示例'
+    case 'custom':
+      return `${props.config?.custom?.name || '自定义'} - 参数示例`
+    default:
+      return '参数示例'
+  }
+})
+
+const titleIcon = computed(() => {
+  switch (props.type) {
+    case 'service':
+      return 'ep:service'
+    case 'event':
+      return 'ep:bell'
+    case 'property':
+      return 'ep:edit'
+    case 'custom':
+      return 'ep:document'
+    default:
+      return 'ep:document'
+  }
+})
+
+const paramsIcon = computed(() => {
+  switch (props.type) {
+    case 'service':
+      return 'ep:edit'
+    case 'event':
+      return 'ep:upload'
+    case 'property':
+      return 'ep:setting'
+    case 'custom':
+      return 'ep:list'
+    default:
+      return 'ep:edit'
+  }
+})
+
+const paramsLabel = computed(() => {
+  switch (props.type) {
+    case 'service':
+      return '输入参数'
+    case 'event':
+      return '输出参数'
+    case 'property':
+      return '属性参数'
+    case 'custom':
+      return '参数列表'
+    default:
+      return '参数'
+  }
+})
+
+const emptyMessage = computed(() => {
+  switch (props.type) {
+    case 'service':
+      return '此服务无需输入参数'
+    case 'event':
+      return '此事件无输出参数'
+    case 'property':
+      return '无可设置的属性'
+    case 'custom':
+      return '无参数配置'
+    default:
+      return '无参数'
+  }
+})
+
+const noConfigMessage = computed(() => {
+  switch (props.type) {
+    case 'service':
+      return '请先选择服务'
+    case 'event':
+      return '请先选择事件'
+    case 'property':
+      return '请先选择产品'
+    case 'custom':
+      return '请先进行配置'
+    default:
+      return '请先进行配置'
+  }
+})
+
+// 事件处理
+const handleParamsChange = () => {
+  try {
+    jsonError.value = '' // 清除之前的错误
+
+    if (paramsJson.value.trim()) {
+      const parsed = JSON.parse(paramsJson.value)
+      localValue.value = paramsJson.value
+
+      // 额外的参数验证
+      if (typeof parsed !== 'object' || parsed === null) {
+        jsonError.value = '参数必须是一个有效的 JSON 对象'
+        emit('validate', { valid: false, message: jsonError.value })
+        return
+      }
+
+      // 验证必填参数
+      for (const param of paramsList.value) {
+        if (param.required && (!parsed[param.identifier] || parsed[param.identifier] === '')) {
+          jsonError.value = `参数 ${param.name} 为必填项`
+          emit('validate', { valid: false, message: jsonError.value })
+          return
+        }
+      }
+    } else {
+      localValue.value = ''
+    }
+
+    // 验证通过
+    emit('validate', { valid: true, message: 'JSON格式正确' })
+  } catch (error) {
+    jsonError.value = `JSON格式错误: ${error instanceof Error ? error.message : '未知错误'}`
+    emit('validate', { valid: false, message: jsonError.value })
+  }
+}
+
+// 快速填充示例数据
+const fillExampleJson = () => {
+  paramsJson.value = generateExampleJson()
+  handleParamsChange()
+}
+
+// 清空参数
+const clearParams = () => {
+  paramsJson.value = ''
+  localValue.value = ''
+  jsonError.value = ''
+  emit('validate', { valid: true, message: '' })
+}
+
+// 工具函数
+const getParamTypeName = (dataType: string) => {
+  const typeMap = {
+    int: '整数',
+    float: '浮点数',
+    double: '双精度',
+    text: '字符串',
+    bool: '布尔值',
+    enum: '枚举',
+    date: '日期',
+    struct: '结构体',
+    array: '数组'
+  }
+  return typeMap[dataType] || dataType
+}
+
+const getParamTypeTag = (dataType: string) => {
+  const tagMap = {
+    int: 'primary',
+    float: 'success',
+    double: 'success',
+    text: 'info',
+    bool: 'warning',
+    enum: 'danger',
+    date: 'primary',
+    struct: 'info',
+    array: 'warning'
+  }
+  return tagMap[dataType] || 'info'
+}
+
+const getExampleValue = (param: any) => {
+  switch (param.dataType) {
+    case 'int':
+      return '25'
+    case 'float':
+    case 'double':
+      return '25.5'
+    case 'bool':
+      return 'false'
+    case 'text':
+      return '"auto"'
+    case 'enum':
+      return '"option1"'
+    case 'struct':
+      return '{}'
+    case 'array':
+      return '[]'
+    default:
+      return '""'
+  }
+}
+
+const generateExampleJson = () => {
+  if (paramsList.value.length === 0) {
+    return '{}'
+  }
+
+  const example = {}
+  paramsList.value.forEach((param) => {
+    switch (param.dataType) {
+      case 'int':
+        example[param.identifier] = 25
+        break
+      case 'float':
+      case 'double':
+        example[param.identifier] = 25.5
+        break
+      case 'bool':
+        example[param.identifier] = false
+        break
+      case 'text':
+        example[param.identifier] = 'auto'
+        break
+      case 'struct':
+        example[param.identifier] = {}
+        break
+      case 'array':
+        example[param.identifier] = []
+        break
+      default:
+        example[param.identifier] = ''
+    }
+  })
+
+  return JSON.stringify(example, null, 2)
+}
+
+// 处理数据回显的函数
+const handleDataDisplay = (value: string) => {
+  if (!value || !value.trim()) {
+    paramsJson.value = ''
+    jsonError.value = ''
+    return
+  }
+
+  try {
+    // 尝试解析JSON,如果成功则格式化
+    const parsed = JSON.parse(value)
+    paramsJson.value = JSON.stringify(parsed, null, 2)
+    jsonError.value = ''
+  } catch {
+    // 如果不是有效的JSON,直接使用原字符串
+    paramsJson.value = value
+    jsonError.value = ''
+  }
+}
+
+// 监听外部值变化(编辑模式数据回显)
+watch(
+  () => localValue.value,
+  (newValue, oldValue) => {
+    // 避免循环更新
+    if (newValue === oldValue) return
+
+    // 使用 nextTick 确保在下一个 tick 中处理数据
+    nextTick(() => {
+      handleDataDisplay(newValue || '')
+    })
+  },
+  { immediate: true }
+)
+
+// 组件挂载后也尝试处理一次数据回显
+onMounted(() => {
+  nextTick(() => {
+    if (localValue.value) {
+      handleDataDisplay(localValue.value)
+    }
+  })
+})
+
+// 监听配置变化
+watch(
+  () => props.config,
+  (newConfig, oldConfig) => {
+    // 只有在配置真正变化时才清空数据
+    if (JSON.stringify(newConfig) !== JSON.stringify(oldConfig)) {
+      // 如果没有外部传入的值,才清空数据
+      if (!localValue.value) {
+        paramsJson.value = ''
+        jsonError.value = ''
+      }
+    }
+  }
+)
+</script>
+
+<style scoped>
+/* 弹出层内容样式 */
+.json-params-detail-content {
+  padding: 4px 0;
+}
+
+/* 弹出层自定义样式 */
+:global(.json-params-detail-popover) {
+  max-width: 500px !important;
+}
+
+:global(.json-params-detail-popover .el-popover__content) {
+  padding: 16px !important;
+}
+
+/* JSON 代码块样式 */
+.json-params-detail-content pre {
+  max-height: 200px;
+  overflow-y: auto;
+}
+</style>

+ 0 - 495
src/views/iot/rule/scene/form/inputs/ServiceParamsInput.vue

@@ -1,495 +0,0 @@
-<!-- 服务参数输入组件 -->
-<template>
-  <div class="w-full min-w-0">
-    <!-- 服务参数配置 -->
-    <div v-if="serviceConfig && serviceConfig.service" class="space-y-12px">
-      <!-- JSON 输入框 -->
-      <div class="relative">
-        <el-input
-          v-model="paramsJson"
-          type="textarea"
-          :rows="4"
-          placeholder="请输入JSON格式的服务参数"
-          @input="handleParamsChange"
-          :class="{ 'is-error': jsonError }"
-        />
-        <!-- 查看详细示例按钮 -->
-        <div class="absolute top-8px right-8px">
-          <el-button
-            ref="exampleTriggerRef"
-            type="info"
-            :icon="InfoFilled"
-            circle
-            size="small"
-            @click="toggleExampleDetail"
-            title="查看参数示例"
-          />
-        </div>
-      </div>
-
-      <!-- 验证状态和错误提示 -->
-      <div class="flex items-center justify-between">
-        <div class="flex items-center gap-8px">
-          <Icon
-            :icon="jsonError ? 'ep:warning' : 'ep:circle-check'"
-            :class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
-            class="text-14px"
-          />
-          <span
-            :class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
-            class="text-12px"
-          >
-            {{ jsonError || 'JSON 格式正确' }}
-          </span>
-        </div>
-
-        <!-- 快速填充按钮 -->
-        <div v-if="inputParams.length > 0" class="flex items-center gap-8px">
-          <span class="text-12px text-[var(--el-text-color-secondary)]">快速填充:</span>
-          <el-button size="small" type="primary" plain @click="fillExampleJson">
-            示例数据
-          </el-button>
-          <!-- TODO @puhui999:这里的 type 有告警 -->
-          <el-button size="small" type="default" plain @click="clearParams"> 清空 </el-button>
-        </div>
-      </div>
-
-      <!-- 详细示例弹出层 -->
-      <Teleport to="body">
-        <div
-          v-if="showExampleDetail"
-          ref="exampleDetailRef"
-          class="example-detail-popover"
-          :style="examplePopoverStyle"
-        >
-          <div
-            class="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-400px max-w-500px"
-          >
-            <div class="flex items-center gap-8px mb-16px">
-              <Icon icon="ep:service" class="text-[var(--el-color-primary)] text-18px" />
-              <span class="text-16px font-600 text-[var(--el-text-color-primary)]">
-                {{ serviceConfig.name }} - 参数示例
-              </span>
-            </div>
-
-            <div class="space-y-16px">
-              <!-- 服务参数示例 -->
-              <div v-if="inputParams.length > 0">
-                <div class="flex items-center gap-8px mb-8px">
-                  <Icon icon="ep:edit" class="text-[var(--el-color-primary)] text-14px" />
-                  <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
-                    输入参数
-                  </span>
-                </div>
-                <div class="ml-22px space-y-8px">
-                  <div
-                    v-for="param in inputParams"
-                    :key="param.identifier"
-                    class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
-                  >
-                    <div class="flex-1">
-                      <div class="text-12px font-500 text-[var(--el-text-color-primary)]">
-                        {{ param.name }}
-                        <el-tag v-if="param.required" size="small" type="danger" class="ml-4px">
-                          必填
-                        </el-tag>
-                      </div>
-                      <div class="text-11px text-[var(--el-text-color-secondary)]">
-                        {{ param.identifier }}
-                      </div>
-                    </div>
-                    <div class="flex items-center gap-8px">
-                      <el-tag :type="getParamTypeTag(param.dataType)" size="small">
-                        {{ getParamTypeName(param.dataType) }}
-                      </el-tag>
-                      <span class="text-11px text-[var(--el-text-color-secondary)]">
-                        {{ getExampleValue(param) }}
-                      </span>
-                    </div>
-                  </div>
-                </div>
-
-                <div class="mt-12px ml-22px">
-                  <div class="text-12px text-[var(--el-text-color-secondary)] mb-6px">
-                    完整 JSON 格式:
-                  </div>
-                  <pre
-                    class="p-12px bg-[var(--el-fill-color-light)] rounded-4px text-11px text-[var(--el-text-color-primary)] overflow-x-auto border-l-3px border-[var(--el-color-primary)]"
-                  >
-                    <code>{{ generateExampleJson() }}</code>
-                  </pre>
-                </div>
-              </div>
-
-              <!-- 无参数提示 -->
-              <div v-else>
-                <div class="text-center py-16px">
-                  <p class="text-14px text-[var(--el-text-color-secondary)]">此服务无需输入参数</p>
-                </div>
-              </div>
-            </div>
-
-            <!-- 关闭按钮 -->
-            <div class="flex justify-end mt-16px">
-              <el-button size="small" @click="hideExampleDetail">关闭</el-button>
-            </div>
-          </div>
-        </div>
-      </Teleport>
-    </div>
-
-    <!-- 无服务配置提示 -->
-    <div v-else class="text-center py-20px">
-      <p class="text-14px text-[var(--el-text-color-secondary)]">请先选择服务</p>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { useVModel } from '@vueuse/core'
-import { InfoFilled } from '@element-plus/icons-vue'
-
-/** 服务参数输入组件 */
-defineOptions({ name: 'ServiceParamsInput' })
-
-interface Props {
-  modelValue?: string
-  serviceConfig?: any
-}
-
-interface Emits {
-  (e: 'update:modelValue', value: string): void
-  (e: 'validate', result: { valid: boolean; message: string }): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
-
-const localValue = useVModel(props, 'modelValue', emit, {
-  defaultValue: ''
-})
-
-// TODO @puhui999:一些注释风格;
-
-// 状态
-const paramsJson = ref('')
-const jsonError = ref('')
-
-// 示例弹出层相关状态
-const showExampleDetail = ref(false)
-const exampleTriggerRef = ref()
-const exampleDetailRef = ref()
-const examplePopoverStyle = ref({})
-
-// 计算属性
-const inputParams = computed(() => {
-  return props.serviceConfig?.service?.inputParams || []
-})
-
-// 事件处理
-const handleParamsChange = () => {
-  try {
-    jsonError.value = '' // 清除之前的错误
-
-    if (paramsJson.value.trim()) {
-      const parsed = JSON.parse(paramsJson.value)
-      localValue.value = paramsJson.value
-
-      // 额外的参数验证
-      if (typeof parsed !== 'object' || parsed === null) {
-        jsonError.value = '参数必须是一个有效的 JSON 对象'
-        emit('validate', { valid: false, message: jsonError.value })
-        return
-      }
-
-      // 验证必填参数
-      for (const param of inputParams.value) {
-        if (param.required && (!parsed[param.identifier] || parsed[param.identifier] === '')) {
-          jsonError.value = `参数 ${param.name} 为必填项`
-          emit('validate', { valid: false, message: jsonError.value })
-          return
-        }
-      }
-    } else {
-      localValue.value = ''
-    }
-
-    // 验证通过
-    emit('validate', { valid: true, message: 'JSON格式正确' })
-  } catch (error) {
-    jsonError.value = `JSON格式错误: ${error instanceof Error ? error.message : '未知错误'}`
-    emit('validate', { valid: false, message: jsonError.value })
-  }
-}
-
-// 快速填充示例数据
-const fillExampleJson = () => {
-  paramsJson.value = generateExampleJson()
-  handleParamsChange()
-}
-
-// 清空参数
-const clearParams = () => {
-  paramsJson.value = ''
-  localValue.value = ''
-  jsonError.value = ''
-  emit('validate', { valid: true, message: '' })
-}
-
-// 工具函数
-// TODO @puhui999:这里的复用
-const getParamTypeName = (dataType: string) => {
-  const typeMap = {
-    int: '整数',
-    float: '浮点数',
-    double: '双精度',
-    text: '字符串',
-    bool: '布尔值',
-    enum: '枚举',
-    date: '日期',
-    struct: '结构体',
-    array: '数组'
-  }
-  return typeMap[dataType] || dataType
-}
-
-const getParamTypeTag = (dataType: string) => {
-  const tagMap = {
-    int: 'primary',
-    float: 'success',
-    double: 'success',
-    text: 'info',
-    bool: 'warning',
-    enum: 'danger',
-    date: 'primary',
-    struct: 'info',
-    array: 'warning'
-  }
-  return tagMap[dataType] || 'info'
-}
-
-const getExampleValue = (param: any) => {
-  switch (param.dataType) {
-    case 'int':
-      return '25'
-    case 'float':
-    case 'double':
-      return '25.5'
-    case 'bool':
-      return 'false'
-    case 'text':
-      return '"auto"'
-    case 'enum':
-      return '"option1"'
-    case 'struct':
-      return '{}'
-    case 'array':
-      return '[]'
-    default:
-      return '""'
-  }
-}
-
-const generateExampleJson = () => {
-  if (inputParams.value.length === 0) {
-    return '{}'
-  }
-
-  const example = {}
-  inputParams.value.forEach((param) => {
-    switch (param.dataType) {
-      case 'int':
-        example[param.identifier] = 25
-        break
-      case 'float':
-      case 'double':
-        example[param.identifier] = 25.5
-        break
-      case 'bool':
-        example[param.identifier] = false
-        break
-      case 'text':
-        example[param.identifier] = 'auto'
-        break
-      case 'struct':
-        example[param.identifier] = {}
-        break
-      case 'array':
-        example[param.identifier] = []
-        break
-      default:
-        example[param.identifier] = ''
-    }
-  })
-
-  return JSON.stringify(example, null, 2)
-}
-
-// 示例弹出层控制方法
-const toggleExampleDetail = () => {
-  if (showExampleDetail.value) {
-    hideExampleDetail()
-  } else {
-    showExampleDetailPopover()
-  }
-}
-
-const showExampleDetailPopover = () => {
-  if (!exampleTriggerRef.value) return
-
-  showExampleDetail.value = true
-
-  nextTick(() => {
-    updateExamplePopoverPosition()
-  })
-}
-
-const hideExampleDetail = () => {
-  showExampleDetail.value = false
-}
-
-const updateExamplePopoverPosition = () => {
-  if (!exampleTriggerRef.value || !exampleDetailRef.value) return
-
-  const triggerEl = exampleTriggerRef.value.$el
-  const triggerRect = triggerEl.getBoundingClientRect()
-
-  // 计算弹出层位置
-  const left = triggerRect.left + triggerRect.width + 8
-  const top = triggerRect.top
-
-  // 检查是否超出视窗右边界
-  const popoverWidth = 500 // 最大宽度
-  const viewportWidth = window.innerWidth
-
-  let finalLeft = left
-  if (left + popoverWidth > viewportWidth - 16) {
-    // 如果超出右边界,显示在左侧
-    finalLeft = triggerRect.left - popoverWidth - 8
-  }
-
-  // 检查是否超出视窗下边界
-  let finalTop = top
-  const popoverHeight = exampleDetailRef.value.offsetHeight || 300
-  const viewportHeight = window.innerHeight
-
-  if (top + popoverHeight > viewportHeight - 16) {
-    finalTop = Math.max(16, viewportHeight - popoverHeight - 16)
-  }
-
-  examplePopoverStyle.value = {
-    position: 'fixed',
-    left: `${finalLeft}px`,
-    top: `${finalTop}px`,
-    zIndex: 9999
-  }
-}
-
-// 点击外部关闭弹出层
-const handleClickOutside = (event: MouseEvent) => {
-  if (
-    showExampleDetail.value &&
-    exampleDetailRef.value &&
-    exampleTriggerRef.value &&
-    !exampleDetailRef.value.contains(event.target as Node) &&
-    !exampleTriggerRef.value.$el.contains(event.target as Node)
-  ) {
-    hideExampleDetail()
-  }
-}
-
-// 监听窗口大小变化,重新计算弹出层位置
-const handleResize = () => {
-  if (showExampleDetail.value) {
-    updateExamplePopoverPosition()
-  }
-}
-
-// 初始化
-onMounted(() => {
-  if (localValue.value) {
-    try {
-      paramsJson.value = localValue.value
-      jsonError.value = ''
-    } catch (error) {
-      console.error('初始化参数失败:', error)
-      jsonError.value = '初始参数格式错误'
-    }
-  }
-
-  // 添加事件监听器
-  document.addEventListener('click', handleClickOutside)
-  window.addEventListener('resize', handleResize)
-})
-
-// 组件卸载时清理事件监听器
-onUnmounted(() => {
-  document.removeEventListener('click', handleClickOutside)
-  window.removeEventListener('resize', handleResize)
-})
-
-// 监听输入值变化
-watch(
-  () => localValue.value,
-  (newValue) => {
-    if (newValue !== paramsJson.value) {
-      paramsJson.value = newValue || ''
-    }
-  }
-)
-
-// 监听服务配置变化
-watch(
-  () => props.serviceConfig,
-  () => {
-    // 服务变化时清空参数
-    paramsJson.value = ''
-    localValue.value = ''
-    jsonError.value = ''
-  }
-)
-</script>
-
-<style scoped>
-@keyframes fadeInScale {
-  from {
-    opacity: 0;
-    transform: scale(0.9) translateY(-4px);
-  }
-
-  to {
-    opacity: 1;
-    transform: scale(1) translateY(0);
-  }
-}
-
-.example-detail-popover {
-  animation: fadeInScale 0.2s ease-out;
-  transform-origin: top left;
-}
-
-/* 弹出层箭头效果 */
-.example-detail-popover::before {
-  position: absolute;
-  top: 20px;
-  left: -8px;
-  width: 0;
-  height: 0;
-  border-top: 8px solid transparent;
-  border-right: 8px solid var(--el-border-color);
-  border-bottom: 8px solid transparent;
-  content: '';
-}
-
-.example-detail-popover::after {
-  position: absolute;
-  top: 20px;
-  left: -7px;
-  width: 0;
-  height: 0;
-  border-top: 8px solid transparent;
-  border-right: 8px solid white;
-  border-bottom: 8px solid transparent;
-  content: '';
-}
-</style>

+ 35 - 25
src/views/iot/rule/scene/form/sections/ActionSection.vue

@@ -78,12 +78,27 @@
               @update:model-value="(value) => updateAction(index, value)"
             />
 
-            <!-- 告警配置 -->
+            <!-- 告警配置 - 只有恢复告警时才显示 -->
             <AlertConfig
-              v-if="isAlertAction(action.type)"
+              v-if="action.type === ActionTypeEnum.ALERT_RECOVER"
               :model-value="action.alertConfigId"
               @update:model-value="(value) => updateActionAlertConfig(index, value)"
             />
+
+            <!-- 触发告警提示 - 触发告警时显示 -->
+            <div
+              v-if="action.type === ActionTypeEnum.ALERT_TRIGGER"
+              class="border border-[var(--el-border-color-light)] rounded-6px p-16px bg-[var(--el-fill-color-blank)]"
+            >
+              <div class="flex items-center gap-8px mb-8px">
+                <Icon icon="ep:warning" class="text-[var(--el-color-warning)] text-16px" />
+                <span class="text-14px font-600 text-[var(--el-text-color-primary)]">触发告警</span>
+                <el-tag size="small" type="warning">自动执行</el-tag>
+              </div>
+              <div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">
+                当触发条件满足时,系统将自动发送告警通知,无需额外配置。
+              </div>
+            </div>
           </div>
         </div>
       </div>
@@ -107,7 +122,7 @@ import { useVModel } from '@vueuse/core'
 import ActionTypeSelector from '../selectors/ActionTypeSelector.vue'
 import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
 import AlertConfig from '../configs/AlertConfig.vue'
-import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
+import { Action } from '@/api/iot/rule/scene/scene.types'
 import {
   IotRuleSceneActionTypeEnum as ActionTypeEnum,
   isDeviceAction,
@@ -118,23 +133,20 @@ import {
 /** 执行器配置组件 */
 defineOptions({ name: 'ActionSection' })
 
-interface Props {
-  actions: ActionFormData[]
-}
+const props = defineProps<{
+  actions: Action[]
+}>()
 
-interface Emits {
-  (e: 'update:actions', value: ActionFormData[]): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+const emit = defineEmits<{
+  (e: 'update:actions', value: Action[]): void
+}>()
 
 const actions = useVModel(props, 'actions', emit)
 
 /**
  * 创建默认的执行器数据
  */
-const createDefaultActionData = (): ActionFormData => {
+const createDefaultActionData = (): Action => {
   return {
     type: ActionTypeEnum.DEVICE_PROPERTY_SET, // 默认为设备属性设置
     productId: undefined,
@@ -145,9 +157,7 @@ const createDefaultActionData = (): ActionFormData => {
   }
 }
 
-// 配置常量
-// TODO @puhui999:去掉最大;注释风格改下;
-const maxActions = 5
+const maxActions = 5 // 最大执行器数量
 
 // 工具函数
 const getActionTypeName = (type: number) => {
@@ -164,7 +174,7 @@ const getActionTypeTag = (type: number) => {
   return actionTypeTags[type] || 'info'
 }
 
-// 事件处理
+/** 添加执行器 */
 const addAction = () => {
   if (actions.value.length >= maxActions) {
     return
@@ -174,24 +184,29 @@ const addAction = () => {
   actions.value.push(newAction)
 }
 
+/** 删除执行器 */
 const removeAction = (index: number) => {
   actions.value.splice(index, 1)
 }
 
+/** 更新执行器类型 */
 const updateActionType = (index: number, type: number) => {
   actions.value[index].type = type
   onActionTypeChange(actions.value[index], type)
 }
 
-const updateAction = (index: number, action: ActionFormData) => {
+/** 更新执行器 */
+const updateAction = (index: number, action: Action) => {
   actions.value[index] = action
 }
 
+/** 更新告警配置 */
 const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
   actions.value[index].alertConfigId = alertConfigId
 }
 
-const onActionTypeChange = (action: ActionFormData, type: number) => {
+/** 监听执行器类型变化 */
+const onActionTypeChange = (action: Action, type: number) => {
   // 清理不相关的配置,确保数据结构干净
   if (isDeviceAction(type)) {
     // 设备控制类型:清理告警配置,确保设备参数存在
@@ -204,16 +219,11 @@ const onActionTypeChange = (action: ActionFormData, type: number) => {
       action.identifier = undefined
     }
   } else if (isAlertAction(type)) {
-    // 告警类型:清理设备配置
     action.productId = undefined
     action.deviceId = undefined
     action.identifier = undefined // 清理服务标识符
     action.params = undefined
+    action.alertConfigId = undefined
   }
-
-  // 触发重新校验
-  nextTick(() => {
-    // 这里可以添加校验逻辑
-  })
 }
 </script>

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

@@ -58,17 +58,17 @@
 <script setup lang="ts">
 import { useVModel } from '@vueuse/core'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
+import { IotSceneRule } from '@/api/iot/rule/scene/scene.types'
 
 /** 基础信息配置组件 */
 defineOptions({ name: 'BasicInfoSection' })
 
 const props = defineProps<{
-  modelValue: RuleSceneFormData
+  modelValue: IotSceneRule
   rules?: any
 }>()
 const emit = defineEmits<{
-  (e: 'update:modelValue', value: RuleSceneFormData): void
+  (e: 'update:modelValue', value: IotSceneRule): void
 }>()
 
 const formData = useVModel(props, 'modelValue', emit)

+ 159 - 228
src/views/iot/rule/scene/form/selectors/PropertySelector.vue

@@ -37,90 +37,122 @@
       </el-option-group>
     </el-select>
 
-    <!-- 属性详情触发按钮 -->
-    <div class="relative">
-      <el-button
-        v-if="selectedProperty"
-        ref="detailTriggerRef"
-        type="info"
-        :icon="InfoFilled"
-        circle
-        size="small"
-        @click="togglePropertyDetail"
-        class="flex-shrink-0"
-        title="查看属性详情"
-      />
-
-      <!-- 属性详情弹出层 -->
-      <Teleport to="body">
-        <div
-          v-if="showPropertyDetail && selectedProperty"
-          ref="propertyDetailRef"
-          class="property-detail-popover"
-          :style="popoverStyle"
-        >
+    <!-- 属性详情弹出层 -->
+    <el-popover
+      v-if="selectedProperty"
+      placement="right-start"
+      :width="350"
+      trigger="click"
+      :show-arrow="true"
+      :offset="8"
+      popper-class="property-detail-popover"
+    >
+      <template #reference>
+        <el-button
+          type="info"
+          :icon="InfoFilled"
+          circle
+          size="small"
+          class="flex-shrink-0"
+          title="查看属性详情"
+        />
+      </template>
+
+      <!-- 弹出层内容 -->
+      <div class="property-detail-content">
+        <div class="flex items-center gap-8px mb-12px">
+          <Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-16px" />
+          <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
+            {{ selectedProperty.name }}
+          </span>
+          <el-tag :type="getPropertyTypeTag(selectedProperty.dataType)" size="small">
+            {{ getPropertyTypeName(selectedProperty.dataType) }}
+          </el-tag>
+        </div>
+
+        <div class="space-y-8px ml-24px">
+          <div class="flex items-start gap-8px">
+            <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+              标识符:
+            </span>
+            <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+              {{ selectedProperty.identifier }}
+            </span>
+          </div>
+
+          <div v-if="selectedProperty.description" class="flex items-start gap-8px">
+            <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+              描述:
+            </span>
+            <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+              {{ selectedProperty.description }}
+            </span>
+          </div>
+
+          <div v-if="selectedProperty.unit" class="flex items-start gap-8px">
+            <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+              单位:
+            </span>
+            <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+              {{ selectedProperty.unit }}
+            </span>
+          </div>
+
+          <div v-if="selectedProperty.range" class="flex items-start gap-8px">
+            <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+              取值范围:
+            </span>
+            <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+              {{ selectedProperty.range }}
+            </span>
+          </div>
+
+          <!-- 根据属性类型显示额外信息 -->
           <div
-            class="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-300px max-w-400px"
+            v-if="
+              selectedProperty.type === IoTThingModelTypeEnum.PROPERTY &&
+              selectedProperty.accessMode
+            "
+            class="flex items-start gap-8px"
           >
-            <div class="flex items-center gap-8px mb-12px">
-              <Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-4px" />
-              <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
-                {{ selectedProperty.name }}
-              </span>
-              <el-tag :type="getPropertyTypeTag(selectedProperty.dataType)" size="small">
-                {{ getPropertyTypeName(selectedProperty.dataType) }}
-              </el-tag>
-            </div>
-            <div class="space-y-8px ml-24px">
-              <div class="flex items-start gap-8px">
-                <span
-                  class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
-                >
-                  标识符:
-                </span>
-                <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
-                  {{ selectedProperty.identifier }}
-                </span>
-              </div>
-              <div v-if="selectedProperty.description" class="flex items-start gap-8px">
-                <span
-                  class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
-                >
-                  描述:
-                </span>
-                <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
-                  {{ selectedProperty.description }}
-                </span>
-              </div>
-              <div v-if="selectedProperty.unit" class="flex items-start gap-8px">
-                <span
-                  class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
-                >
-                  单位:
-                </span>
-                <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
-                  {{ selectedProperty.unit }}
-                </span>
-              </div>
-              <div v-if="selectedProperty.range" class="flex items-start gap-8px">
-                <span
-                  class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
-                >
-                  取值范围:
-                </span>
-                <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
-                  {{ selectedProperty.range }}
-                </span>
-              </div>
-            </div>
-            <!-- 关闭按钮 -->
-            <div class="flex justify-end mt-12px">
-              <el-button size="small" @click="hidePropertyDetail">关闭</el-button>
-            </div>
+            <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+              访问模式:
+            </span>
+            <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+              {{ getAccessModeText(selectedProperty.accessMode) }}
+            </span>
+          </div>
+
+          <div
+            v-if="
+              selectedProperty.type === IoTThingModelTypeEnum.EVENT && selectedProperty.eventType
+            "
+            class="flex items-start gap-8px"
+          >
+            <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+              事件类型:
+            </span>
+            <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+              {{ getEventTypeText(selectedProperty.eventType) }}
+            </span>
+          </div>
+
+          <div
+            v-if="
+              selectedProperty.type === IoTThingModelTypeEnum.SERVICE && selectedProperty.callType
+            "
+            class="flex items-start gap-8px"
+          >
+            <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+              调用类型:
+            </span>
+            <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+              {{ getCallTypeText(selectedProperty.callType) }}
+            </span>
           </div>
         </div>
-      </Teleport>
-    </div>
+      </div>
+    </el-popover>
   </div>
 </template>
 
@@ -153,25 +185,6 @@ const loading = ref(false)
 const propertyList = ref<PropertySelectorItem[]>([])
 const thingModelTSL = ref<IotThingModelTSLRespVO | null>(null)
 
-// 属性详情弹出层相关状态
-const showPropertyDetail = ref(false)
-const detailTriggerRef = ref()
-const propertyDetailRef = ref()
-const popoverStyle = ref({})
-
-// 点击外部关闭弹出层
-const handleClickOutside = (event: MouseEvent) => {
-  if (
-    showPropertyDetail.value &&
-    propertyDetailRef.value &&
-    detailTriggerRef.value &&
-    !propertyDetailRef.value.contains(event.target as Node) &&
-    !detailTriggerRef.value.$el.contains(event.target as Node)
-  ) {
-    hidePropertyDetail()
-  }
-}
-
 // 计算属性
 const propertyGroups = computed(() => {
   const groups: { label: string; options: any[] }[] = []
@@ -235,65 +248,33 @@ const getPropertyTypeTag = (dataType: string) => {
   return tagMap[dataType] || 'info'
 }
 
-// 弹出层控制方法
-const togglePropertyDetail = () => {
-  if (showPropertyDetail.value) {
-    hidePropertyDetail()
-  } else {
-    showPropertyDetailPopover()
+// 工具函数 - 获取访问模式文本
+const getAccessModeText = (accessMode: string) => {
+  const modeMap = {
+    r: '只读',
+    w: '只写',
+    rw: '读写'
   }
+  return modeMap[accessMode] || accessMode
 }
 
-const showPropertyDetailPopover = () => {
-  if (!selectedProperty.value || !detailTriggerRef.value) return
-
-  showPropertyDetail.value = true
-
-  nextTick(() => {
-    updatePopoverPosition()
-  })
-}
-
-const hidePropertyDetail = () => {
-  showPropertyDetail.value = false
-}
-
-const updatePopoverPosition = () => {
-  if (!detailTriggerRef.value || !propertyDetailRef.value) return
-
-  const triggerEl = detailTriggerRef.value.$el
-  const triggerRect = triggerEl.getBoundingClientRect()
-  const popoverEl = propertyDetailRef.value
-
-  // 计算弹出层位置
-  const left = triggerRect.left + triggerRect.width + 8
-  const top = triggerRect.top
-
-  // 检查是否超出视窗右边界
-  const popoverWidth = 400 // 最大宽度
-  const viewportWidth = window.innerWidth
-
-  let finalLeft = left
-  if (left + popoverWidth > viewportWidth - 16) {
-    // 如果超出右边界,显示在左侧
-    finalLeft = triggerRect.left - popoverWidth - 8
-  }
-
-  // 检查是否超出视窗下边界
-  let finalTop = top
-  const popoverHeight = popoverEl.offsetHeight || 200
-  const viewportHeight = window.innerHeight
-
-  if (top + popoverHeight > viewportHeight - 16) {
-    finalTop = Math.max(16, viewportHeight - popoverHeight - 16)
+// 工具函数 - 获取事件类型文本
+const getEventTypeText = (eventType: string) => {
+  const typeMap = {
+    info: '信息',
+    alert: '告警',
+    error: '故障'
   }
+  return typeMap[eventType] || eventType
+}
 
-  popoverStyle.value = {
-    position: 'fixed',
-    left: `${finalLeft}px`,
-    top: `${finalTop}px`,
-    zIndex: 9999
+// 工具函数 - 获取调用类型文本
+const getCallTypeText = (callType: string) => {
+  const typeMap = {
+    sync: '同步',
+    async: '异步'
   }
+  return typeMap[callType] || callType
 }
 
 // 事件处理
@@ -305,11 +286,11 @@ const handleChange = (value: string) => {
       config: property
     })
   }
-  // 选择变化时隐藏详情弹出层
-  hidePropertyDetail()
 }
 
-// 获取物模型TSL数据
+/**
+ * 获取物模型TSL数据
+ */
 const getThingModelTSL = async () => {
   if (!props.productId) {
     thingModelTSL.value = null
@@ -319,8 +300,15 @@ const getThingModelTSL = async () => {
 
   loading.value = true
   try {
-    thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(props.productId)
-    parseThingModelData()
+    const tslData = await ThingModelApi.getThingModelTSLByProductId(props.productId)
+
+    if (tslData) {
+      thingModelTSL.value = tslData
+      parseThingModelData()
+    } else {
+      // 如果TSL获取失败,尝试获取物模型列表
+      await getThingModelList()
+    }
   } catch (error) {
     console.error('获取物模型TSL失败:', error)
     // 如果TSL获取失败,尝试获取物模型列表
@@ -330,7 +318,9 @@ const getThingModelTSL = async () => {
   }
 }
 
-// 获取物模型列表(备用方案)
+/**
+ * 获取物模型列表(备用方案)
+ */
 const getThingModelList = async () => {
   if (!props.productId) {
     propertyList.value = []
@@ -441,21 +431,6 @@ const getPropertyRange = (property: any) => {
   return undefined
 }
 
-// 获取数据范围描述(保留兼容性)
-const getDataRange = (dataSpecs: any) => {
-  if (!dataSpecs) return undefined
-
-  if (dataSpecs.min !== undefined && dataSpecs.max !== undefined) {
-    return `${dataSpecs.min}~${dataSpecs.max}`
-  }
-
-  if (dataSpecs.dataSpecsList && Array.isArray(dataSpecs.dataSpecsList)) {
-    return dataSpecs.dataSpecsList.map((item: any) => `${item.name}(${item.value})`).join(', ')
-  }
-
-  return undefined
-}
-
 // 监听产品变化
 watch(
   () => props.productId,
@@ -470,74 +445,30 @@ watch(
   () => props.triggerType,
   () => {
     localValue.value = ''
-    hidePropertyDetail()
+    // el-popover 会自动关闭,无需手动处理
   }
 )
-
-// 监听窗口大小变化,重新计算弹出层位置
-const handleResize = () => {
-  if (showPropertyDetail.value) {
-    updatePopoverPosition()
-  }
-}
-
-// 生命周期
-onMounted(() => {
-  document.addEventListener('click', handleClickOutside)
-  window.addEventListener('resize', handleResize)
-})
-
-onUnmounted(() => {
-  document.removeEventListener('click', handleClickOutside)
-  window.removeEventListener('resize', handleResize)
-})
 </script>
 
 <style scoped>
-@keyframes fadeInScale {
-  from {
-    opacity: 0;
-    transform: scale(0.9) translateY(-4px);
-  }
-
-  to {
-    opacity: 1;
-    transform: scale(1) translateY(0);
-  }
-}
-
+/* 下拉选项样式 */
 :deep(.el-select-dropdown__item) {
   height: auto;
   padding: 8px 20px;
 }
 
-.property-detail-popover {
-  animation: fadeInScale 0.2s ease-out;
-  transform-origin: top left;
+/* 弹出层内容样式 */
+.property-detail-content {
+  padding: 4px 0;
 }
 
-/* 弹出层箭头效果(可选) */
-.property-detail-popover::before {
-  position: absolute;
-  top: 20px;
-  left: -8px;
-  width: 0;
-  height: 0;
-  border-top: 8px solid transparent;
-  border-right: 8px solid var(--el-border-color);
-  border-bottom: 8px solid transparent;
-  content: '';
+/* 弹出层自定义样式 */
+:global(.property-detail-popover) {
+  /* 可以在这里添加全局弹出层样式 */
+  max-width: 400px !important;
 }
 
-.property-detail-popover::after {
-  position: absolute;
-  top: 20px;
-  left: -7px;
-  width: 0;
-  height: 0;
-  border-top: 8px solid transparent;
-  border-right: 8px solid white;
-  border-bottom: 8px solid transparent;
-  content: '';
+:global(.property-detail-popover .el-popover__content) {
+  padding: 16px !important;
 }
 </style>

+ 0 - 462
src/views/iot/rule/scene/form/selectors/ServiceSelector.vue

@@ -1,462 +0,0 @@
-<!-- 服务选择器组件 -->
-<template>
-  <div class="w-full">
-    <el-select
-      :model-value="modelValue"
-      @update:model-value="handleChange"
-      placeholder="请选择服务"
-      filterable
-      clearable
-      class="w-full"
-      :loading="loading"
-      :disabled="!productId"
-    >
-      <el-option
-        v-for="service in serviceList"
-        :key="service.identifier"
-        :label="service.name"
-        :value="service.identifier"
-      >
-        <div class="flex items-center justify-between w-full py-4px">
-          <div class="flex items-center gap-12px flex-1">
-            <Icon
-              icon="ep:service"
-              class="text-18px text-[var(--el-color-success)] flex-shrink-0"
-            />
-            <div class="flex-1">
-              <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">
-                {{ service.name }}
-              </div>
-              <div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">
-                {{ service.identifier }}
-              </div>
-              <div
-                v-if="service.description"
-                class="text-11px text-[var(--el-text-color-secondary)] mt-2px"
-              >
-                {{ service.description }}
-              </div>
-            </div>
-          </div>
-          <div class="flex items-center gap-8px">
-            <el-tag :type="getCallTypeTag(service.callType)" size="small">
-              {{ getCallTypeLabel(service.callType) }}
-            </el-tag>
-            <el-button
-              ref="detailTriggerRef"
-              type="info"
-              :icon="InfoFilled"
-              circle
-              size="small"
-              @click.stop="showServiceDetail(service)"
-              title="查看服务详情"
-            />
-          </div>
-        </div>
-      </el-option>
-    </el-select>
-
-    <!-- 服务详情弹出层 -->
-    <Teleport to="body">
-      <div
-        v-if="showServiceDetailPopover && selectedService"
-        ref="serviceDetailRef"
-        class="service-detail-popover"
-        :style="servicePopoverStyle"
-      >
-        <div
-          class="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-400px max-w-500px"
-        >
-          <div class="flex items-center gap-8px mb-16px">
-            <Icon icon="ep:service" class="text-[var(--el-color-success)] text-18px" />
-            <span class="text-16px font-600 text-[var(--el-text-color-primary)]">
-              {{ selectedService.name }}
-            </span>
-            <el-tag :type="getCallTypeTag(selectedService.callType)" size="small">
-              {{ getCallTypeLabel(selectedService.callType) }}
-            </el-tag>
-          </div>
-
-          <div class="space-y-16px">
-            <!-- 基本信息 -->
-            <div>
-              <div class="flex items-center gap-8px mb-8px">
-                <Icon icon="ep:info" class="text-[var(--el-color-info)] text-14px" />
-                <span class="text-14px font-500 text-[var(--el-text-color-primary)]">基本信息</span>
-              </div>
-              <div class="ml-22px space-y-4px">
-                <div class="text-12px">
-                  <span class="text-[var(--el-text-color-secondary)]">标识符:</span>
-                  <span class="text-[var(--el-text-color-primary)]">{{
-                    selectedService.identifier
-                  }}</span>
-                </div>
-                <div v-if="selectedService.description" class="text-12px">
-                  <span class="text-[var(--el-text-color-secondary)]">描述:</span>
-                  <span class="text-[var(--el-text-color-primary)]">{{
-                    selectedService.description
-                  }}</span>
-                </div>
-                <div class="text-12px">
-                  <span class="text-[var(--el-text-color-secondary)]">调用方式:</span>
-                  <span class="text-[var(--el-text-color-primary)]">{{
-                    getCallTypeLabel(selectedService.callType)
-                  }}</span>
-                </div>
-              </div>
-            </div>
-
-            <!-- 输入参数 -->
-            <div v-if="selectedService.inputParams && selectedService.inputParams.length > 0">
-              <div class="flex items-center gap-8px mb-8px">
-                <Icon icon="ep:download" class="text-[var(--el-color-primary)] text-14px" />
-                <span class="text-14px font-500 text-[var(--el-text-color-primary)]">输入参数</span>
-              </div>
-              <div class="ml-22px space-y-8px">
-                <div
-                  v-for="param in selectedService.inputParams"
-                  :key="param.identifier"
-                  class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
-                >
-                  <div class="flex-1">
-                    <div class="text-12px font-500 text-[var(--el-text-color-primary)]">
-                      {{ param.name }}
-                    </div>
-                    <div class="text-11px text-[var(--el-text-color-secondary)]">
-                      {{ param.identifier }}
-                    </div>
-                  </div>
-                  <div class="flex items-center gap-8px">
-                    <el-tag :type="getParamTypeTag(param.dataType)" size="small">
-                      {{ getParamTypeName(param.dataType) }}
-                    </el-tag>
-                  </div>
-                </div>
-              </div>
-            </div>
-
-            <!-- 输出参数 -->
-            <div v-if="selectedService.outputParams && selectedService.outputParams.length > 0">
-              <div class="flex items-center gap-8px mb-8px">
-                <Icon icon="ep:upload" class="text-[var(--el-color-warning)] text-14px" />
-                <span class="text-14px font-500 text-[var(--el-text-color-primary)]">输出参数</span>
-              </div>
-              <div class="ml-22px space-y-8px">
-                <div
-                  v-for="param in selectedService.outputParams"
-                  :key="param.identifier"
-                  class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
-                >
-                  <div class="flex-1">
-                    <div class="text-12px font-500 text-[var(--el-text-color-primary)]">
-                      {{ param.name }}
-                    </div>
-                    <div class="text-11px text-[var(--el-text-color-secondary)]">
-                      {{ param.identifier }}
-                    </div>
-                  </div>
-                  <div class="flex items-center gap-8px">
-                    <el-tag :type="getParamTypeTag(param.dataType)" size="small">
-                      {{ getParamTypeName(param.dataType) }}
-                    </el-tag>
-                  </div>
-                </div>
-              </div>
-            </div>
-          </div>
-
-          <!-- 关闭按钮 -->
-          <div class="flex justify-end mt-16px">
-            <el-button size="small" @click="hideServiceDetail">关闭</el-button>
-          </div>
-        </div>
-      </div>
-    </Teleport>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { useVModel } from '@vueuse/core'
-import { InfoFilled } from '@element-plus/icons-vue'
-import { ThingModelApi } from '@/api/iot/thingmodel'
-import { ThingModelService } from '@/api/iot/rule/scene/scene.types'
-import { getThingModelServiceCallTypeLabel } from '@/views/iot/utils/constants'
-
-/** 服务选择器组件 */
-defineOptions({ name: 'ServiceSelector' })
-
-const props = defineProps<{
-  modelValue?: string
-  productId?: number
-}>()
-
-const emit = defineEmits<{
-  (e: 'update:modelValue', value?: string): void
-  (e: 'change', value?: string, service?: ThingModelService): void
-}>()
-
-// TODO @puhui999:这里不用的
-const localValue = useVModel(props, 'modelValue', emit)
-
-// 状态
-const loading = ref(false)
-const serviceList = ref<ThingModelService[]>([])
-const showServiceDetailPopover = ref(false)
-const selectedService = ref<ThingModelService | null>(null)
-const detailTriggerRef = ref()
-const serviceDetailRef = ref()
-const servicePopoverStyle = ref({})
-
-// 事件处理
-const handleChange = (value?: string) => {
-  const service = serviceList.value.find((s) => s.identifier === value)
-  emit('change', value, service)
-}
-
-// 获取物模型TSL数据
-const getThingModelTSL = async () => {
-  if (!props.productId) {
-    serviceList.value = []
-    return
-  }
-
-  loading.value = true
-  try {
-    const tslData = await ThingModelApi.getThingModelTSLByProductId(props.productId)
-    serviceList.value = tslData?.services || []
-  } catch (error) {
-    console.error('获取物模型TSL失败:', error)
-    serviceList.value = []
-  } finally {
-    loading.value = false
-  }
-}
-
-// 工具函数
-const getCallTypeLabel = (callType: string) => {
-  return getThingModelServiceCallTypeLabel(callType) || callType
-}
-
-const getCallTypeTag = (callType: string) => {
-  return callType === 'sync' ? 'primary' : 'success'
-}
-
-// TODO @puhui999:一些注释风格;
-const getParamTypeName = (dataType: string) => {
-  const typeMap = {
-    int: '整数',
-    float: '浮点数',
-    double: '双精度',
-    text: '字符串',
-    bool: '布尔值',
-    enum: '枚举',
-    date: '日期',
-    struct: '结构体',
-    array: '数组'
-  }
-  return typeMap[dataType] || dataType
-}
-
-const getParamTypeTag = (dataType: string) => {
-  const tagMap = {
-    int: 'primary',
-    float: 'success',
-    double: 'success',
-    text: 'info',
-    bool: 'warning',
-    enum: 'danger',
-    date: 'primary',
-    struct: 'info',
-    array: 'warning'
-  }
-  return tagMap[dataType] || 'info'
-}
-
-// 服务详情弹出层控制
-const showServiceDetail = (service: ThingModelService) => {
-  selectedService.value = service
-  showServiceDetailPopover.value = true
-
-  nextTick(() => {
-    updateServicePopoverPosition()
-  })
-}
-
-const hideServiceDetail = () => {
-  showServiceDetailPopover.value = false
-  selectedService.value = null
-}
-
-const updateServicePopoverPosition = () => {
-  if (!detailTriggerRef.value || !serviceDetailRef.value) return
-
-  const triggerEl = detailTriggerRef.value.$el
-  const triggerRect = triggerEl.getBoundingClientRect()
-
-  // 计算弹出层位置
-  const left = triggerRect.left + triggerRect.width + 8
-  const top = triggerRect.top
-
-  // 检查是否超出视窗右边界
-  const popoverWidth = 500
-  const viewportWidth = window.innerWidth
-
-  let finalLeft = left
-  if (left + popoverWidth > viewportWidth - 16) {
-    finalLeft = triggerRect.left - popoverWidth - 8
-  }
-
-  // 检查是否超出视窗下边界
-  let finalTop = top
-  const popoverHeight = serviceDetailRef.value.offsetHeight || 300
-  const viewportHeight = window.innerHeight
-
-  if (top + popoverHeight > viewportHeight - 16) {
-    finalTop = Math.max(16, viewportHeight - popoverHeight - 16)
-  }
-
-  servicePopoverStyle.value = {
-    position: 'fixed',
-    left: `${finalLeft}px`,
-    top: `${finalTop}px`,
-    zIndex: 9999
-  }
-}
-
-// 监听产品变化
-watch(
-  () => props.productId,
-  () => {
-    getThingModelTSL()
-  },
-  { immediate: true }
-)
-
-// 监听modelValue变化,处理编辑模式的回显
-watch(
-  () => props.modelValue,
-  (newValue) => {
-    console.log('🔄 ServiceSelector modelValue changed:', {
-      newValue,
-      serviceListLength: serviceList.value.length,
-      serviceList: serviceList.value.map((s) => s.identifier)
-    })
-
-    if (newValue && serviceList.value.length > 0) {
-      // 确保服务列表已加载,然后设置选中的服务
-      const service = serviceList.value.find((s) => s.identifier === newValue)
-      console.log('🎯 ServiceSelector found service:', service)
-
-      if (service) {
-        selectedService.value = service
-        console.log('✅ ServiceSelector service set:', service.name)
-      } else {
-        console.warn('⚠️ ServiceSelector service not found for identifier:', newValue)
-      }
-    }
-  },
-  { immediate: true }
-)
-
-// 监听服务列表变化,处理异步加载后的回显
-watch(
-  () => serviceList.value,
-  (newServiceList) => {
-    console.log('📋 ServiceSelector serviceList changed:', {
-      length: newServiceList.length,
-      services: newServiceList.map((s) => s.identifier),
-      modelValue: props.modelValue
-    })
-
-    if (newServiceList.length > 0 && props.modelValue) {
-      // 服务列表加载完成后,如果有modelValue,设置选中的服务
-      const service = newServiceList.find((s) => s.identifier === props.modelValue)
-      console.log('🎯 ServiceSelector found service in list:', service)
-
-      if (service) {
-        selectedService.value = service
-        console.log('✅ ServiceSelector service set from list:', service.name)
-      }
-    }
-  },
-  { immediate: true }
-)
-
-// 监听窗口大小变化
-const handleResize = () => {
-  if (showServiceDetailPopover.value) {
-    updateServicePopoverPosition()
-  }
-}
-
-// 点击外部关闭弹出层
-const handleClickOutside = (event: MouseEvent) => {
-  if (
-    showServiceDetailPopover.value &&
-    serviceDetailRef.value &&
-    detailTriggerRef.value &&
-    !serviceDetailRef.value.contains(event.target as Node) &&
-    !detailTriggerRef.value.$el.contains(event.target as Node)
-  ) {
-    hideServiceDetail()
-  }
-}
-
-// 生命周期
-onMounted(() => {
-  document.addEventListener('click', handleClickOutside)
-  window.addEventListener('resize', handleResize)
-})
-
-onUnmounted(() => {
-  document.removeEventListener('click', handleClickOutside)
-  window.removeEventListener('resize', handleResize)
-})
-</script>
-
-<style scoped>
-@keyframes fadeInScale {
-  from {
-    opacity: 0;
-    transform: scale(0.9) translateY(-4px);
-  }
-  to {
-    opacity: 1;
-    transform: scale(1) translateY(0);
-  }
-}
-
-.service-detail-popover {
-  animation: fadeInScale 0.2s ease-out;
-  transform-origin: top left;
-}
-
-.service-detail-popover::before {
-  position: absolute;
-  top: 20px;
-  left: -8px;
-  width: 0;
-  height: 0;
-  border-top: 8px solid transparent;
-  border-right: 8px solid var(--el-border-color);
-  border-bottom: 8px solid transparent;
-  content: '';
-}
-
-.service-detail-popover::after {
-  position: absolute;
-  top: 20px;
-  left: -7px;
-  width: 0;
-  height: 0;
-  border-top: 8px solid transparent;
-  border-right: 8px solid white;
-  border-bottom: 8px solid transparent;
-  content: '';
-}
-
-:deep(.el-select-dropdown__item) {
-  height: auto;
-  padding: 8px 20px;
-}
-</style>

+ 10 - 10
src/views/iot/rule/scene/index.vue

@@ -239,7 +239,7 @@
 import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict'
 import { ContentWrap } from '@/components/ContentWrap'
 import RuleSceneForm from './form/RuleSceneForm.vue'
-import { IotRuleSceneDO } from '@/api/iot/rule/scene/scene.types'
+import { IotSceneRule } from '@/api/iot/rule/scene/scene.types'
 import { RuleSceneApi } from '@/api/iot/rule/scene'
 import {
   IotRuleSceneTriggerTypeEnum,
@@ -264,14 +264,14 @@ const queryParams = reactive({
 })
 
 const loading = ref(true) // 列表的加载中
-const list = ref<IotRuleSceneDO[]>([]) // 列表的数据
+const list = ref<IotSceneRule[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
-const selectedRows = ref<IotRuleSceneDO[]>([]) // 选中的行数据
+const selectedRows = ref<IotSceneRule[]>([]) // 选中的行数据
 const queryFormRef = ref() // 搜索的表单
 
 /** 表单状态 */
 const formVisible = ref(false) // 是否可见
-const currentRule = ref<IotRuleSceneDO>() // 表单数据
+const currentRule = ref<IotSceneRule>() // 表单数据
 
 /** 统计数据 */
 const statistics = ref({
@@ -327,7 +327,7 @@ const formatCronExpression = (cron: string): string => {
 }
 
 /** 获取规则摘要信息 */
-const getRuleSceneSummary = (rule: IotRuleSceneDO) => {
+const getRuleSceneSummary = (rule: IotSceneRule) => {
   const triggerSummary =
     rule.triggers?.map((trigger: any) => {
       // 构建基础描述
@@ -455,12 +455,12 @@ const updateStatistics = () => {
 }
 
 /** 获取触发器摘要 */
-const getTriggerSummary = (rule: IotRuleSceneDO) => {
+const getTriggerSummary = (rule: IotSceneRule) => {
   return getRuleSceneSummary(rule).triggerSummary
 }
 
 /** 获取执行器摘要 */
-const getActionSummary = (rule: IotRuleSceneDO) => {
+const getActionSummary = (rule: IotSceneRule) => {
   return getRuleSceneSummary(rule).actionSummary
 }
 
@@ -484,7 +484,7 @@ const handleAdd = () => {
 }
 
 /** 修改操作 */
-const handleEdit = (row: IotRuleSceneDO) => {
+const handleEdit = (row: IotSceneRule) => {
   currentRule.value = row
   formVisible.value = true
 }
@@ -506,7 +506,7 @@ const handleDelete = async (id: number) => {
 }
 
 /** 修改状态 */
-const handleToggleStatus = async (row: IotRuleSceneDO) => {
+const handleToggleStatus = async (row: IotSceneRule) => {
   try {
     // 修改状态的二次确认
     // TODO @puhui999:status 枚举;
@@ -525,7 +525,7 @@ const handleToggleStatus = async (row: IotRuleSceneDO) => {
 }
 
 /** 多选框选中数据 */
-const handleSelectionChange = (selection: IotRuleSceneDO[]) => {
+const handleSelectionChange = (selection: IotSceneRule[]) => {
   selectedRows.value = selection
 }
 

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

@@ -109,9 +109,19 @@ export const IoTThingModelAccessModeEnum = {
   READ_ONLY: {
     label: '只读',
     value: 'r'
+  },
+  WRITE_ONLY: {
+    label: '只写',
+    value: 'w'
   }
 } as const
 
+/** 获取访问模式标签 */
+export const getAccessModeLabel = (value: string): string => {
+  const mode = Object.values(IoTThingModelAccessModeEnum).find((mode) => mode.value === value)
+  return mode?.label || value
+}
+
 /** 属性值的数据类型 */
 export const IoTDataSpecsDataTypeEnum = {
   INT: 'int',