Sfoglia il codice sorgente

!802 perf:【IoT 物联网】场景联动优化数据结构对齐后端
Merge pull request !802 from puhui999/feature/iot

芋道源码 8 mesi fa
parent
commit
11e78b78a7
27 ha cambiato i file con 1174 aggiunte e 1653 eliminazioni
  1. 205 156
      src/api/iot/rule/scene/scene.types.ts
  2. 158 182
      src/views/iot/rule/scene/form/RuleSceneForm.vue
  3. 30 68
      src/views/iot/rule/scene/form/configs/ConditionConfig.vue
  4. 0 255
      src/views/iot/rule/scene/form/configs/ConditionGroupConfig.vue
  5. 22 46
      src/views/iot/rule/scene/form/configs/ConditionGroupContainerConfig.vue
  6. 4 58
      src/views/iot/rule/scene/form/configs/CurrentTimeConditionConfig.vue
  7. 10 9
      src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue
  8. 29 76
      src/views/iot/rule/scene/form/configs/DeviceStatusConditionConfig.vue
  9. 28 69
      src/views/iot/rule/scene/form/configs/DeviceTriggerConfig.vue
  10. 15 24
      src/views/iot/rule/scene/form/configs/MainConditionConfig.vue
  11. 5 29
      src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue
  12. 24 56
      src/views/iot/rule/scene/form/configs/SubConditionGroupConfig.vue
  13. 4 9
      src/views/iot/rule/scene/form/configs/TimerTriggerConfig.vue
  14. 14 10
      src/views/iot/rule/scene/form/sections/ActionSection.vue
  15. 7 16
      src/views/iot/rule/scene/form/sections/BasicInfoSection.vue
  16. 156 121
      src/views/iot/rule/scene/form/sections/TriggerSection.vue
  17. 11 4
      src/views/iot/rule/scene/form/selectors/ActionTypeSelector.vue
  18. 5 8
      src/views/iot/rule/scene/form/selectors/ConditionTypeSelector.vue
  19. 7 10
      src/views/iot/rule/scene/form/selectors/DeviceSelector.vue
  20. 58 57
      src/views/iot/rule/scene/form/selectors/OperatorSelector.vue
  21. 32 13
      src/views/iot/rule/scene/form/selectors/ProductDeviceSelector.vue
  22. 12 16
      src/views/iot/rule/scene/form/selectors/ProductSelector.vue
  23. 240 40
      src/views/iot/rule/scene/form/selectors/PropertySelector.vue
  24. 0 132
      src/views/iot/rule/scene/form/selectors/types.ts
  25. 1 1
      src/views/iot/rule/scene/index.vue
  26. 0 188
      src/views/iot/rule/scene/utils/validation.ts
  27. 97 0
      src/views/iot/utils/constants.ts

+ 205 - 156
src/api/iot/rule/scene/scene.types.ts

@@ -2,91 +2,140 @@
  * IoT 场景联动接口定义
  */
 
-// TODO @puhui999:枚举挪到 views/iot/utils/constants.ts 里
-// 枚举定义
-const IotRuleSceneTriggerTypeEnum = {
-  DEVICE_STATE_UPDATE: 1, // 设备上下线变更
-  DEVICE_PROPERTY_POST: 2, // 物模型属性上报
-  DEVICE_EVENT_POST: 3, // 设备事件上报
-  DEVICE_SERVICE_INVOKE: 4, // 设备服务调用
-  TIMER: 100 // 定时触发
-} as const
-
-const IotRuleSceneActionTypeEnum = {
-  DEVICE_PROPERTY_SET: 1, // 设备属性设置,
-  DEVICE_SERVICE_INVOKE: 2, // 设备服务调用
-  ALERT_TRIGGER: 100, // 告警触发
-  ALERT_RECOVER: 101 // 告警恢复
-} as const
-
-const IotDeviceMessageTypeEnum = {
-  PROPERTY: 'property', // 属性
-  SERVICE: 'service', // 服务
-  EVENT: 'event' // 事件
-} as const
-
-// TODO @puhui999:这个貌似可以不要?
-const IotDeviceMessageIdentifierEnum = {
-  PROPERTY_SET: 'set', // 属性设置
-  SERVICE_INVOKE: '${identifier}' // 服务调用
-} as const
-
-const IotRuleSceneTriggerConditionParameterOperatorEnum = {
-  EQUALS: { name: '等于', value: '=' }, // 等于
-  NOT_EQUALS: { name: '不等于', value: '!=' }, // 不等于
-  GREATER_THAN: { name: '大于', value: '>' }, // 大于
-  GREATER_THAN_OR_EQUALS: { name: '大于等于', value: '>=' }, // 大于等于
-  LESS_THAN: { name: '小于', value: '<' }, // 小于
-  LESS_THAN_OR_EQUALS: { name: '小于等于', value: '<=' }, // 小于等于
-  IN: { name: '在...之中', value: 'in' }, // 在...之中
-  NOT_IN: { name: '不在...之中', value: 'not in' }, // 不在...之中
-  BETWEEN: { name: '在...之间', value: 'between' }, // 在...之间
-  NOT_BETWEEN: { name: '不在...之间', value: 'not between' }, // 不在...之间
-  LIKE: { name: '字符串匹配', value: 'like' }, // 字符串匹配
-  NOT_NULL: { name: '非空', value: 'not null' } // 非空
-} as const
-
-// 条件类型枚举
-const IotRuleSceneTriggerConditionTypeEnum = {
-  DEVICE_STATUS: 1, // 设备状态
-  DEVICE_PROPERTY: 2, // 设备属性
-  CURRENT_TIME: 3 // 当前时间
-} as const
-
-// 时间运算符枚举
-const IotRuleSceneTriggerTimeOperatorEnum = {
-  BEFORE_TIME: { name: '在时间之前', value: 'before_time' }, // 在时间之前
-  AFTER_TIME: { name: '在时间之后', value: 'after_time' }, // 在时间之后
-  BETWEEN_TIME: { name: '在时间之间', value: 'between_time' }, // 在时间之间
-  AT_TIME: { name: '在指定时间', value: 'at_time' }, // 在指定时间
-  BEFORE_TODAY: { name: '在今日之前', value: 'before_today' }, // 在今日之前
-  AFTER_TODAY: { name: '在今日之后', value: 'after_today' }, // 在今日之后
-  TODAY: { name: '在今日之间', value: 'today' } // 在今日之间
-} as const
-
-// TODO @puhui999:下面 IotAlertConfigReceiveTypeEnum、DeviceStateEnum 没用到,貌似可以删除下?
-const IotAlertConfigReceiveTypeEnum = {
-  SMS: 1, // 短信
-  MAIL: 2, // 邮箱
-  NOTIFY: 3 // 通知
-} as const
-
-// 设备状态枚举
-const DeviceStateEnum = {
-  INACTIVE: 0, // 未激活
-  ONLINE: 1, // 在线
-  OFFLINE: 2 // 离线
-} as const
-
-// TODO @puhui999:这个全局已经有啦
-// 通用状态枚举
-const CommonStatusEnum = {
-  ENABLE: 0, // 开启
-  DISABLE: 1 // 关闭
-} as const
-
-// 基础接口
-// TODO @puhui999:这个貌似可以不要?
+// ========== IoT物模型TSL数据类型定义 ==========
+
+/** 物模型TSL响应数据结构 */
+export interface IotThingModelTSLRespVO {
+  productId: number
+  productKey: string
+  properties: ThingModelProperty[]
+  events: ThingModelEvent[]
+  services: ThingModelService[]
+}
+
+/** 物模型属性 */
+export interface ThingModelProperty {
+  identifier: string
+  name: string
+  accessMode: string
+  required?: boolean
+  dataType: string
+  description?: string
+  dataSpecs?: ThingModelDataSpecs
+  dataSpecsList?: ThingModelDataSpecs[]
+}
+
+/** 物模型事件 */
+export interface ThingModelEvent {
+  identifier: string
+  name: string
+  required?: boolean
+  type: string
+  description?: string
+  outputParams?: ThingModelParam[]
+  method?: string
+}
+
+/** 物模型服务 */
+export interface ThingModelService {
+  identifier: string
+  name: string
+  required?: boolean
+  callType: string
+  description?: string
+  inputParams?: ThingModelParam[]
+  outputParams?: ThingModelParam[]
+  method?: string
+}
+
+/** 物模型参数 */
+export interface ThingModelParam {
+  identifier: string
+  name: string
+  direction: string
+  paraOrder?: number
+  dataType: string
+  dataSpecs?: ThingModelDataSpecs
+  dataSpecsList?: ThingModelDataSpecs[]
+}
+
+/** 数值型数据规范 */
+export interface ThingModelNumericDataSpec {
+  dataType: 'int' | 'float' | 'double'
+  max: string
+  min: string
+  step: string
+  precise?: string
+  defaultValue?: string
+  unit?: string
+  unitName?: string
+}
+
+/** 布尔/枚举型数据规范 */
+export interface ThingModelBoolOrEnumDataSpecs {
+  dataType: 'bool' | 'enum'
+  name: string
+  value: number
+}
+
+/** 文本/时间型数据规范 */
+export interface ThingModelDateOrTextDataSpecs {
+  dataType: 'text' | 'date'
+  length?: number
+  defaultValue?: string
+}
+
+/** 数组型数据规范 */
+export interface ThingModelArrayDataSpecs {
+  dataType: 'array'
+  size: number
+  childDataType: string
+  dataSpecsList?: ThingModelDataSpecs[]
+}
+
+/** 结构体型数据规范 */
+export interface ThingModelStructDataSpecs {
+  dataType: 'struct'
+  identifier: string
+  name: string
+  accessMode: string
+  required?: boolean
+  childDataType: string
+  dataSpecs?: ThingModelDataSpecs
+  dataSpecsList?: ThingModelDataSpecs[]
+}
+
+/** 数据规范联合类型 */
+export type ThingModelDataSpecs =
+  | ThingModelNumericDataSpec
+  | ThingModelBoolOrEnumDataSpecs
+  | ThingModelDateOrTextDataSpecs
+  | ThingModelArrayDataSpecs
+  | ThingModelStructDataSpecs
+
+/** 属性选择器内部使用的统一数据结构 */
+export interface PropertySelectorItem {
+  identifier: string
+  name: string
+  description?: string
+  dataType: string
+  type: number // IoTThingModelTypeEnum
+  accessMode?: string
+  required?: boolean
+  unit?: string
+  range?: string
+  eventType?: string
+  callType?: string
+  inputParams?: ThingModelParam[]
+  outputParams?: ThingModelParam[]
+  property?: ThingModelProperty
+  event?: ThingModelEvent
+  service?: ThingModelService
+}
+
+// ========== 场景联动规则相关接口定义 ==========
+
+// 基础接口(如果项目中有全局的 BaseDO,可以使用全局的)
 interface TenantBaseDO {
   createTime?: Date // 创建时间
   updateTime?: Date // 更新时间
@@ -138,67 +187,48 @@ interface ActionConfig {
   alertConfigId?: number // 告警配置ID(告警恢复时必填)
 }
 
-// 表单数据接口
+// 表单数据接口 - 直接对应后端 DO 结构
 interface RuleSceneFormData {
   id?: number
   name: string
   description?: string
   status: number
-  trigger: TriggerFormData
+  triggers: TriggerFormData[] // 支持多个触发器
   actions: ActionFormData[]
 }
 
+// 触发器表单数据 - 直接对应 TriggerDO
 interface TriggerFormData {
-  type: number
-  productId?: number
-  deviceId?: number
-  identifier?: string
-  operator?: string
-  value?: string
-  cronExpression?: string
-  // 新的条件结构
-  mainCondition?: ConditionFormData // 主条件(必须满足)
-  conditionGroup?: ConditionGroupContainerFormData // 条件组容器(可选,与主条件为且关系)
-}
-
-interface ActionFormData {
-  type: number
-  productId?: number
-  deviceId?: number
-  params?: Record<string, any>
-  alertConfigId?: number
-}
-
-// 条件组容器(包含多个子条件组,子条件组间为或关系)
-interface ConditionGroupContainerFormData {
-  subGroups: SubConditionGroupFormData[] // 子条件组数组,子条件组间为或关系
+  type: number // 触发类型
+  productId?: number // 产品编号
+  deviceId?: number // 设备编号
+  identifier?: string // 物模型标识符
+  operator?: string // 操作符
+  value?: string // 参数值
+  cronExpression?: string // CRON 表达式
+  conditionGroups?: TriggerConditionFormData[][] // 条件组(二维数组)
 }
 
-// 子条件组(内部条件为且关系)
-interface SubConditionGroupFormData {
-  conditions: ConditionFormData[] // 条件数组,条件间为且关系
-}
-
-// 保留原有接口用于兼容性
-interface ConditionGroupFormData {
-  conditions: ConditionFormData[]
-  // 注意:条件组内部的条件固定为"且"关系,条件组之间固定为"或"关系
-  // logicOperator 字段保留用于兼容性,但在UI中固定为 'AND'
-  logicOperator: 'AND' | 'OR'
-}
-
-interface ConditionFormData {
+// 触发条件表单数据 - 直接对应 TriggerConditionDO
+interface TriggerConditionFormData {
   type: number // 条件类型:1-设备状态,2-设备属性,3-当前时间
-  productId?: number // 产品ID(设备状态和设备属性时必填)
-  deviceId?: number // 设备ID(设备状态和设备属性时必填)
-  identifier?: string // 标识符(设备属性时必填)
+  productId?: number // 产品编号
+  deviceId?: number // 设备编号
+  identifier?: string // 标识符
   operator: string // 操作符
   param: string // 参数值
-  timeValue?: string // 时间值(当前时间条件时使用)
-  timeValue2?: string // 第二个时间值(时间范围条件时使用)
 }
 
-// 主接口
+// 执行器表单数据 - 直接对应 ActionDO
+interface ActionFormData {
+  type: number // 执行类型
+  productId?: number // 产品编号
+  deviceId?: number // 设备编号
+  params?: Record<string, any> // 请求参数
+  alertConfigId?: number // 告警配置编号
+}
+
+// 主接口 - 原有的 API 接口格式(保持兼容性)
 interface IotRuleScene extends TenantBaseDO {
   id?: number // 场景编号(新增时为空)
   name: string // 场景名称(必填)
@@ -208,14 +238,46 @@ interface IotRuleScene extends TenantBaseDO {
   actions: ActionConfig[] // 执行器数组(必填,至少一个)
 }
 
-// 工具类型 - 从枚举中提取类型
-export type TriggerType =
-  (typeof IotRuleSceneTriggerTypeEnum)[keyof typeof IotRuleSceneTriggerTypeEnum]
-export type ActionType =
-  (typeof IotRuleSceneActionTypeEnum)[keyof typeof IotRuleSceneActionTypeEnum]
-export type MessageType = (typeof IotDeviceMessageTypeEnum)[keyof typeof IotDeviceMessageTypeEnum]
-export type OperatorType =
-  (typeof IotRuleSceneTriggerConditionParameterOperatorEnum)[keyof typeof IotRuleSceneTriggerConditionParameterOperatorEnum]['value']
+// 后端 DO 接口 - 匹配后端数据结构
+interface IotRuleSceneDO {
+  id?: number // 场景编号
+  name: string // 场景名称
+  description?: string // 场景描述
+  status: number // 场景状态:0-开启,1-关闭
+  triggers: TriggerDO[] // 触发器数组
+  actions: ActionDO[] // 执行器数组
+}
+
+// 触发器 DO 结构
+interface TriggerDO {
+  type: number // 触发类型
+  productId?: number // 产品编号
+  deviceId?: number // 设备编号
+  identifier?: string // 物模型标识符
+  operator?: string // 操作符
+  value?: string // 参数值
+  cronExpression?: string // CRON 表达式
+  conditionGroups?: TriggerConditionDO[][] // 条件组(二维数组)
+}
+
+// 触发条件 DO 结构
+interface TriggerConditionDO {
+  type: number // 条件类型
+  productId?: number // 产品编号
+  deviceId?: number // 设备编号
+  identifier?: string // 标识符
+  operator: string // 操作符
+  param: string // 参数
+}
+
+// 执行器 DO 结构
+interface ActionDO {
+  type: number // 执行类型
+  productId?: number // 产品编号
+  deviceId?: number // 设备编号
+  params?: Record<string, any> // 请求参数
+  alertConfigId?: number // 告警配置编号
+}
 
 // 表单验证规则类型
 interface ValidationRule {
@@ -234,6 +296,10 @@ interface FormValidationRules {
 
 export {
   IotRuleScene,
+  IotRuleSceneDO,
+  TriggerDO,
+  TriggerConditionDO,
+  ActionDO,
   TriggerConfig,
   TriggerCondition,
   TriggerConditionParameter,
@@ -241,25 +307,8 @@ export {
   ActionDeviceControl,
   RuleSceneFormData,
   TriggerFormData,
+  TriggerConditionFormData,
   ActionFormData,
-  ConditionGroupFormData,
-  ConditionGroupContainerFormData,
-  SubConditionGroupFormData,
-  ConditionFormData,
-  IotRuleSceneTriggerTypeEnum,
-  IotRuleSceneActionTypeEnum,
-  IotDeviceMessageTypeEnum,
-  IotDeviceMessageIdentifierEnum,
-  IotRuleSceneTriggerConditionParameterOperatorEnum,
-  IotRuleSceneTriggerConditionTypeEnum,
-  IotRuleSceneTriggerTimeOperatorEnum,
-  IotAlertConfigReceiveTypeEnum,
-  DeviceStateEnum,
-  CommonStatusEnum,
-  TriggerType,
-  ActionType,
-  MessageType,
-  OperatorType,
   ValidationRule,
   FormValidationRules
 }

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

@@ -1,5 +1,5 @@
 <template>
-  <!-- TODO @puhui999:这个抽屉的高度太高了?! -->
+  <!-- 场景联动规则表单抽屉 - 优化高度和布局 -->
   <el-drawer
     v-model="drawerVisible"
     :title="drawerTitle"
@@ -8,29 +8,28 @@
     :close-on-click-modal="false"
     :close-on-press-escape="false"
     @close="handleClose"
-    class="[--el-drawer-padding-primary:20px]"
   >
-    <div class="h-[calc(100vh-120px)] overflow-y-auto p-20px pb-80px">
-      <el-form
-        ref="formRef"
-        :model="formData"
-        :rules="formRules"
-        label-width="120px"
-        class="flex flex-col gap-24px"
-      >
-        <!-- 基础信息配置 -->
-        <BasicInfoSection v-model="formData" :rules="formRules" />
+    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="110px">
+      <!-- 基础信息配置 -->
+      <BasicInfoSection v-model="formData" :rules="formRules" />
 
-        <!-- 触发器配置 -->
-        <TriggerSection v-model:trigger="formData.trigger" @validate="handleTriggerValidate" />
+      <!-- 触发器配置 -->
+      <TriggerSection v-model:triggers="formData.triggers" @validate="handleTriggerValidate" />
 
-        <!-- 执行器配置 -->
-        <ActionSection v-model:actions="formData.actions" @validate="handleActionValidate" />
-      </el-form>
-    </div>
+      <!-- 执行器配置 -->
+      <ActionSection v-model:actions="formData.actions" @validate="handleActionValidate" />
+    </el-form>
     <template #footer>
-      <el-button :disabled="submitLoading" type="primary" @click="handleSubmit">确 定</el-button>
-      <el-button @click="handleClose">取 消</el-button>
+      <div class="drawer-footer">
+        <el-button :disabled="submitLoading" type="primary" @click="handleSubmit">
+          <Icon icon="ep:check" />
+          确 定
+        </el-button>
+        <el-button @click="handleClose">
+          <Icon icon="ep:close" />
+          取 消
+        </el-button>
+      </div>
     </template>
   </el-drawer>
 </template>
@@ -40,116 +39,106 @@ import { useVModel } from '@vueuse/core'
 import BasicInfoSection from './sections/BasicInfoSection.vue'
 import TriggerSection from './sections/TriggerSection.vue'
 import ActionSection from './sections/ActionSection.vue'
-import {
-  RuleSceneFormData,
-  IotRuleScene,
-  IotRuleSceneActionTypeEnum,
-  IotRuleSceneTriggerTypeEnum,
-  CommonStatusEnum
-} from '@/api/iot/rule/scene/scene.types'
-import { getBaseValidationRules } from '../utils/validation'
+import { IotRuleSceneDO, RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
+import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
 import { ElMessage } from 'element-plus'
 import { generateUUID } from '@/utils'
 
+// 导入全局的 CommonStatusEnum
+const CommonStatusEnum = {
+  ENABLE: 0, // 开启
+  DISABLE: 1 // 关闭
+} as const
+
 /** IoT 场景联动规则表单 - 主表单组件 */
 defineOptions({ name: 'RuleSceneForm' })
 
-// TODO @puhui999:是不是融合到 props
-interface Props {
+/** 组件属性定义 */
+const props = defineProps<{
+  /** 抽屉显示状态 */
   modelValue: boolean
-  ruleScene?: IotRuleScene
-}
+}>()
 
-// TODO @puhui999:Emits 是不是融合到 emit
-interface Emits {
+/** 组件事件定义 */
+const emit = defineEmits<{
   (e: 'update:modelValue', value: boolean): void
   (e: 'success'): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 const drawerVisible = useVModel(props, 'modelValue', emit) // 是否可见
 
-// TODO @puhui999:使用 /** 注释风格哈 */
-
 /** 创建默认的表单数据 */
 const createDefaultFormData = (): RuleSceneFormData => {
   return {
     name: '',
     description: '',
     status: CommonStatusEnum.ENABLE, // 默认启用状态
-    trigger: {
-      type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
-      productId: undefined,
-      deviceId: undefined,
-      identifier: undefined,
-      operator: undefined,
-      value: undefined,
-      cronExpression: undefined,
-      mainCondition: undefined,
-      conditionGroup: undefined
-    },
+    triggers: [
+      {
+        type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+        productId: undefined,
+        deviceId: undefined,
+        identifier: undefined,
+        operator: undefined,
+        value: undefined,
+        cronExpression: undefined,
+        conditionGroups: [] // 空的条件组数组
+      }
+    ],
     actions: []
   }
 }
 
-// TODO @puhui999:使用 convertFormToVO;下面也是类似哈;
 /**
- * 将表单数据转换为 API 请求格式
+ * 将表单数据转换为后端 DO 格式
+ * 由于数据结构已对齐,转换变得非常简单
  */
-const transformFormToApi = (formData: RuleSceneFormData): IotRuleScene => {
+const convertFormToVO = (formData: RuleSceneFormData): IotRuleSceneDO => {
   return {
     id: formData.id,
     name: formData.name,
     description: formData.description,
     status: Number(formData.status),
-    triggers: [
-      {
-        type: formData.trigger.type,
-        productKey: formData.trigger.productId
-          ? `product_${formData.trigger.productId}`
-          : undefined,
-        deviceNames: formData.trigger.deviceId
-          ? [`device_${formData.trigger.deviceId}`]
-          : undefined,
-        cronExpression: formData.trigger.cronExpression,
-        conditions: [] // TODO: 实现新的条件转换逻辑
-      }
-    ],
+    triggers: formData.triggers.map((trigger) => ({
+      type: trigger.type,
+      productId: trigger.productId,
+      deviceId: trigger.deviceId,
+      identifier: trigger.identifier,
+      operator: trigger.operator,
+      value: trigger.value,
+      cronExpression: trigger.cronExpression,
+      conditionGroups: trigger.conditionGroups || []
+    })),
     actions:
       formData.actions?.map((action) => ({
         type: action.type,
-        alertConfigId: action.alertConfigId,
-        deviceControl:
-          action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
-          action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
-            ? {
-                productKey: action.productId ? `product_${action.productId}` : '',
-                deviceNames: action.deviceId ? [`device_${action.deviceId}`] : [],
-                type: 'property',
-                identifier: 'set',
-                params: action.params || {}
-              }
-            : undefined
+        productId: action.productId,
+        deviceId: action.deviceId,
+        params: action.params,
+        alertConfigId: action.alertConfigId
       })) || []
-  } as IotRuleScene
+  }
 }
 
 /**
- * 将 API 响应数据转换为表单格式
+ * 将后端 DO 数据转换为表单格式
+ * 由于数据结构已对齐,转换变得非常简单
  */
-const transformApiToForm = (apiData: IotRuleScene): RuleSceneFormData => {
-  const firstTrigger = apiData.triggers?.[0]
-  return {
-    ...apiData,
-    status: Number(apiData.status), // 确保状态为数字类型
-    trigger: firstTrigger
-      ? {
-          ...firstTrigger,
-          type: Number(firstTrigger.type)
-        }
-      : {
+const convertVOToForm = (apiData: IotRuleSceneDO): RuleSceneFormData => {
+  // 转换所有触发器
+  const triggers = apiData.triggers?.length
+    ? apiData.triggers.map((trigger: any) => ({
+        type: Number(trigger.type),
+        productId: trigger.productId,
+        deviceId: trigger.deviceId,
+        identifier: trigger.identifier,
+        operator: trigger.operator,
+        value: trigger.value,
+        cronExpression: trigger.cronExpression,
+        conditionGroups: trigger.conditionGroups || []
+      }))
+    : [
+        {
           type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
           productId: undefined,
           deviceId: undefined,
@@ -157,13 +146,23 @@ const transformApiToForm = (apiData: IotRuleScene): RuleSceneFormData => {
           operator: undefined,
           value: undefined,
           cronExpression: undefined,
-          mainCondition: undefined,
-          conditionGroup: undefined
-        },
+          conditionGroups: []
+        }
+      ]
+
+  return {
+    id: apiData.id,
+    name: apiData.name,
+    description: apiData.description,
+    status: Number(apiData.status),
+    triggers,
     actions:
-      apiData.actions?.map((action) => ({
-        ...action,
+      apiData.actions?.map((action: any) => ({
         type: Number(action.type),
+        productId: action.productId,
+        deviceId: action.deviceId,
+        params: action.params || {},
+        alertConfigId: action.alertConfigId,
         // 为每个执行器添加唯一标识符,解决组件索引重用问题
         key: generateUUID()
       })) || []
@@ -173,7 +172,33 @@ const transformApiToForm = (apiData: IotRuleScene): RuleSceneFormData => {
 // 表单数据和状态
 const formRef = ref()
 const formData = ref<RuleSceneFormData>(createDefaultFormData())
-const formRules = getBaseValidationRules()
+const formRules = reactive({
+  name: [
+    { required: true, message: '场景名称不能为空', trigger: 'blur' },
+    { type: 'string', min: 1, max: 50, message: '场景名称长度应在1-50个字符之间', trigger: 'blur' }
+  ],
+  status: [
+    { required: true, message: '场景状态不能为空', trigger: 'change' },
+    {
+      type: 'enum',
+      enum: [CommonStatusEnum.ENABLE, CommonStatusEnum.DISABLE],
+      message: '状态值必须为启用或禁用',
+      trigger: 'change'
+    }
+  ],
+  description: [
+    { type: 'string', max: 200, message: '场景描述不能超过200个字符', trigger: 'blur' }
+  ],
+  triggers: [
+    { required: true, message: '触发器数组不能为空', trigger: 'change' },
+    { type: 'array', min: 1, message: '至少需要一个触发器', trigger: 'change' }
+  ],
+  actions: [
+    { required: true, message: '执行器数组不能为空', trigger: 'change' },
+    { type: 'array', min: 1, message: '至少需要一个执行器', trigger: 'change' }
+  ]
+})
+
 const submitLoading = ref(false)
 
 // 验证状态
@@ -181,7 +206,7 @@ const triggerValidation = ref({ valid: true, message: '' })
 const actionValidation = ref({ valid: true, message: '' })
 
 // 计算属性
-const isEdit = computed(() => !!props.ruleScene?.id)
+const isEdit = ref(false)
 const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' : '新增场景联动规则'))
 
 // 事件处理
@@ -211,12 +236,23 @@ const handleSubmit = async () => {
   // 提交请求
   submitLoading.value = true
   try {
+    console.log(formData.value)
     // 转换数据格式
-    const apiData = transformFormToApi(formData.value)
-
-    // 这里应该调用API保存数据
-    // TODO @puhui999:貌似还没接入
-    console.log('提交数据:', apiData)
+    const apiData = convertFormToVO(formData.value)
+    if (true) {
+      console.log('转换后', apiData)
+      return
+    }
+    // 调用API保存数据
+    if (isEdit.value) {
+      // 更新场景联动规则
+      // await RuleSceneApi.updateRuleScene(apiData)
+      console.log('更新数据:', apiData)
+    } else {
+      // 创建场景联动规则
+      // await RuleSceneApi.createRuleScene(apiData)
+      console.log('创建数据:', apiData)
+    }
 
     // 模拟API调用
     await new Promise((resolve) => setTimeout(resolve, 1000))
@@ -224,6 +260,9 @@ const handleSubmit = async () => {
     ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
     drawerVisible.value = false
     emit('success')
+  } catch (error) {
+    console.error('保存失败:', error)
+    ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
   } finally {
     submitLoading.value = false
   }
@@ -233,93 +272,30 @@ const handleClose = () => {
   drawerVisible.value = false
 }
 
-// 初始化表单数据
+/** 初始化表单数据 */
 const initFormData = () => {
-  if (props.ruleScene) {
-    formData.value = transformApiToForm(props.ruleScene)
-  } else {
-    formData.value = createDefaultFormData()
-  }
+  // TODO @puhui999: 编辑的情况后面实现
+  formData.value = createDefaultFormData()
 }
 
 // 监听抽屉显示
 watch(drawerVisible, (visible) => {
   if (visible) {
     initFormData()
-    nextTick(() => {
-      formRef.value?.clearValidate()
-    })
+    // TODO @puhui999: 重置表单的情况
+    // nextTick(() => {
+    //   formRef.value?.clearValidate()
+    // })
   }
 })
 
 // 监听 props 变化
-watch(
-  () => props.ruleScene,
-  () => {
-    if (drawerVisible.value) {
-      initFormData()
-    }
-  }
-)
+// watch(
+//   () => props.ruleScene,
+//   () => {
+//     if (drawerVisible.value) {
+//       initFormData()
+//     }
+//   }
+// )
 </script>
-
-<!-- TODO @puhui999:看看下面样式,哪些是必要添加的 -->
-<style scoped>
-/* 滚动条样式 */
-.h-\[calc\(100vh-120px\)\]::-webkit-scrollbar {
-  width: 6px;
-}
-
-.h-\[calc\(100vh-120px\)\]::-webkit-scrollbar-track {
-  background: var(--el-fill-color-light);
-  border-radius: 3px;
-}
-
-.h-\[calc\(100vh-120px\)\]::-webkit-scrollbar-thumb {
-  background: var(--el-border-color);
-  border-radius: 3px;
-}
-
-.h-\[calc\(100vh-120px\)\]::-webkit-scrollbar-thumb:hover {
-  background: var(--el-border-color-dark);
-}
-
-/* 抽屉内容区域优化 */
-:deep(.el-drawer__body) {
-  padding: 0;
-  position: relative;
-}
-
-:deep(.el-drawer__header) {
-  padding: 20px 20px 16px 20px;
-  border-bottom: 1px solid var(--el-border-color-light);
-  margin-bottom: 0;
-}
-
-:deep(.el-drawer__title) {
-  font-size: 18px;
-  font-weight: 600;
-  color: var(--el-text-color-primary);
-}
-
-/* 响应式设计 */
-@media (max-width: 768px) {
-  .el-drawer {
-    --el-drawer-size: 100% !important;
-  }
-
-  .h-\[calc\(100vh-120px\)\] {
-    padding: 16px;
-    padding-bottom: 80px;
-  }
-
-  .flex.flex-col.gap-24px {
-    gap: 20px;
-  }
-
-  .absolute.bottom-0 {
-    padding: 12px 16px;
-    gap: 12px;
-  }
-}
-</style>

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

@@ -89,24 +89,6 @@
           </el-form-item>
         </el-col>
       </el-row>
-
-      <!-- 条件预览 -->
-      <!-- TODO puhui999:可以去掉。。。因为表单选择了,可以看懂的呀。 -->
-      <div
-        v-if="conditionPreview"
-        class="p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"
-      >
-        <div class="flex items-center gap-8px mb-8px">
-          <Icon icon="ep:view" class="text-[var(--el-color-info)] text-16px" />
-          <span class="text-14px font-500 text-[var(--el-text-color-primary)]">条件预览</span>
-        </div>
-        <div class="pl-24px">
-          <code
-            class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] p-8px rounded-4px font-mono"
-            >{{ conditionPreview }}</code
-          >
-        </div>
-      </div>
     </div>
 
     <!-- 当前时间条件配置 -->
@@ -139,26 +121,24 @@ 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 {
-  ConditionFormData,
-  IotRuleSceneTriggerConditionTypeEnum
-} from '@/api/iot/rule/scene/scene.types'
+  IotRuleSceneTriggerConditionTypeEnum,
+  IotRuleSceneTriggerConditionParameterOperatorEnum
+} from '@/views/iot/utils/constants'
 
 /** 单个条件配置组件 */
 defineOptions({ name: 'ConditionConfig' })
 
-interface Props {
-  modelValue: ConditionFormData
+const props = defineProps<{
+  modelValue: TriggerConditionFormData
   triggerType: number
-}
+}>()
 
-interface Emits {
-  (e: 'update:modelValue', value: ConditionFormData): void
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: TriggerConditionFormData): void
   (e: 'validate', result: { valid: boolean; message: string }): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 const condition = useVModel(props, 'modelValue', emit)
 
@@ -172,41 +152,13 @@ const validationMessage = ref('')
 const isValid = ref(true)
 const valueValidation = ref({ valid: true, message: '' })
 
-// 计算属性
-const conditionPreview = computed(() => {
-  if (!condition.value.identifier || !condition.value.operator || !condition.value.param) {
-    return ''
-  }
-
-  const propertyName = propertyConfig.value?.name || condition.value.identifier
-  const operatorText = getOperatorText(condition.value.operator)
-  const value = condition.value.param
-
-  return `当 ${propertyName} ${operatorText} ${value} 时触发`
-})
-
-// 工具函数
-const getOperatorText = (operator: string) => {
-  const operatorMap = {
-    '=': '等于',
-    '!=': '不等于',
-    '>': '大于',
-    '>=': '大于等于',
-    '<': '小于',
-    '<=': '小于等于',
-    in: '包含于',
-    between: '介于'
-  }
-  return operatorMap[operator] || operator
-}
-
 // 事件处理
-const updateConditionField = (field: keyof ConditionFormData, value: any) => {
+const updateConditionField = (field: keyof TriggerConditionFormData, value: any) => {
   ;(condition.value as any)[field] = value
   emit('update:modelValue', condition.value)
 }
 
-const updateCondition = (newCondition: ConditionFormData) => {
+const updateCondition = (newCondition: TriggerConditionFormData) => {
   condition.value = newCondition
   emit('update:modelValue', condition.value)
 }
@@ -215,19 +167,29 @@ const handleConditionTypeChange = (type: number) => {
   // 清理不相关的字段
   if (type === ConditionTypeEnum.DEVICE_STATUS) {
     condition.value.identifier = undefined
-    condition.value.timeValue = undefined
-    condition.value.timeValue2 = undefined
+    // 清理时间相关字段(如果存在)
+    if ('timeValue' in condition.value) {
+      delete (condition.value as any).timeValue
+    }
+    if ('timeValue2' in condition.value) {
+      delete (condition.value as any).timeValue2
+    }
   } else if (type === ConditionTypeEnum.CURRENT_TIME) {
     condition.value.identifier = undefined
     condition.value.productId = undefined
     condition.value.deviceId = undefined
   } else if (type === ConditionTypeEnum.DEVICE_PROPERTY) {
-    condition.value.timeValue = undefined
-    condition.value.timeValue2 = undefined
+    // 清理时间相关字段(如果存在)
+    if ('timeValue' in condition.value) {
+      delete (condition.value as any).timeValue
+    }
+    if ('timeValue2' in condition.value) {
+      delete (condition.value as any).timeValue2
+    }
   }
 
-  // 重置操作符和参数
-  condition.value.operator = '='
+  // 重置操作符和参数,使用枚举中的默认值
+  condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
   condition.value.param = ''
 
   updateValidationResult()
@@ -239,14 +201,14 @@ const handleValidate = (result: { valid: boolean; message: string }) => {
   emit('validate', result)
 }
 
-const handleProductChange = (productId: number) => {
+const handleProductChange = (_: number) => {
   // 产品变化时清空设备和属性
   condition.value.deviceId = undefined
   condition.value.identifier = ''
   updateValidationResult()
 }
 
-const handleDeviceChange = (deviceId: number) => {
+const handleDeviceChange = (_: number) => {
   // 设备变化时清空属性
   condition.value.identifier = ''
   updateValidationResult()

+ 0 - 255
src/views/iot/rule/scene/form/configs/ConditionGroupConfig.vue

@@ -1,255 +0,0 @@
-<!-- 条件组配置组件 -->
-<template>
-  <div class="p-16px">
-    <!-- 条件组说明 -->
-    <div
-      v-if="group.conditions && group.conditions.length > 1"
-      class="mb-12px flex items-center justify-center"
-    >
-      <div
-        class="flex items-center gap-6px px-10px py-4px bg-green-50 border border-green-200 rounded-full text-11px text-green-600"
-      >
-        <Icon icon="ep:info-filled" />
-        <span>组内所有条件必须同时满足(且关系)</span>
-      </div>
-    </div>
-
-    <div class="space-y-12px">
-      <!-- 条件列表 -->
-      <div v-if="group.conditions && group.conditions.length > 0" class="space-y-12px">
-        <div
-          v-for="(condition, index) in group.conditions"
-          :key="`condition-${index}`"
-          class="p-12px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-light)] shadow-sm hover:shadow-md transition-shadow"
-        >
-          <div class="flex items-center justify-between mb-12px">
-            <div class="flex items-center gap-8px">
-              <div class="flex items-center gap-6px">
-                <div
-                  class="w-18px h-18px bg-green-500 text-white rounded-full flex items-center justify-center text-10px font-bold"
-                >
-                  {{ index + 1 }}
-                </div>
-                <span class="text-14px font-500 text-[var(--el-text-color-primary)]">条件</span>
-              </div>
-              <el-tag size="small" type="primary">
-                {{ getConditionTypeName(condition.type) }}
-              </el-tag>
-            </div>
-            <el-button
-              type="danger"
-              size="small"
-              text
-              @click="removeCondition(index)"
-              v-if="group.conditions!.length > 1"
-            >
-              <Icon icon="ep:delete" />
-              删除
-            </el-button>
-          </div>
-
-          <div class="p-12px bg-[var(--el-fill-color-blank)] rounded-4px">
-            <ConditionConfig
-              :model-value="condition"
-              @update:model-value="(value) => updateCondition(index, value)"
-              :trigger-type="triggerType"
-              :product-id="productId"
-              :device-id="deviceId"
-              @validate="(result) => handleConditionValidate(index, result)"
-            />
-          </div>
-
-          <!-- 条件间的"且"连接符 -->
-          <div
-            v-if="index < group.conditions!.length - 1"
-            class="flex items-center justify-center py-8px"
-          >
-            <div class="flex items-center gap-6px">
-              <!-- 连接线 -->
-              <div class="w-24px h-1px bg-green-300"></div>
-              <!-- 且标签 -->
-              <div class="px-12px py-4px bg-green-100 border-2 border-green-300 rounded-full">
-                <span class="text-12px font-600 text-green-600">且</span>
-              </div>
-              <!-- 连接线 -->
-              <div class="w-24px h-1px bg-green-300"></div>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 空状态 -->
-      <div v-else class="py-40px text-center">
-        <el-empty description="暂无条件配置" :image-size="80">
-          <template #description>
-            <div class="space-y-8px">
-              <p class="text-[var(--el-text-color-secondary)]">暂无条件配置</p>
-              <p class="text-12px text-[var(--el-text-color-placeholder)]">
-                条件组需要至少包含一个条件才能生效
-              </p>
-            </div>
-          </template>
-        </el-empty>
-      </div>
-
-      <!-- 添加条件按钮 -->
-      <div
-        v-if="
-          group.conditions && group.conditions.length > 0 && group.conditions.length < maxConditions
-        "
-        class="text-center py-16px"
-      >
-        <el-button type="primary" plain @click="addCondition">
-          <Icon icon="ep:plus" />
-          继续添加条件
-        </el-button>
-        <span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]">
-          最多可添加 {{ maxConditions }} 个条件
-        </span>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { useVModel } from '@vueuse/core'
-import ConditionConfig from './ConditionConfig.vue'
-import {
-  ConditionGroupFormData,
-  ConditionFormData,
-  IotRuleSceneTriggerTypeEnum
-} from '@/api/iot/rule/scene/scene.types'
-
-/** 条件组配置组件 */
-defineOptions({ name: 'ConditionGroupConfig' })
-
-interface Props {
-  modelValue: ConditionGroupFormData
-  triggerType: number
-  productId?: number
-  deviceId?: number
-  maxConditions?: number
-}
-
-interface Emits {
-  (e: 'update:modelValue', value: ConditionGroupFormData): void
-  (e: 'validate', result: { valid: boolean; message: string }): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
-
-const group = useVModel(props, 'modelValue', emit)
-
-// 配置常量
-const maxConditions = computed(() => props.maxConditions || 3)
-
-// 验证状态
-const conditionValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({})
-const validationMessage = ref('')
-const isValid = ref(true)
-
-// 条件类型映射
-const conditionTypeNames = {
-  [IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST]: '属性条件',
-  [IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST]: '事件条件',
-  [IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE]: '服务条件'
-}
-
-// 工具函数
-const getConditionTypeName = (type: number) => {
-  return conditionTypeNames[type] || '未知条件'
-}
-
-// 事件处理
-const updateCondition = (index: number, condition: ConditionFormData) => {
-  if (group.value.conditions) {
-    group.value.conditions[index] = condition
-  }
-}
-
-const addCondition = () => {
-  if (!group.value.conditions) {
-    group.value.conditions = []
-  }
-
-  if (group.value.conditions.length >= maxConditions.value) {
-    return
-  }
-
-  const newCondition: ConditionFormData = {
-    type: 2, // 默认为设备属性条件
-    productId: props.productId || 0,
-    deviceId: props.deviceId || 0,
-    identifier: '',
-    operator: '=',
-    param: ''
-  }
-
-  group.value.conditions.push(newCondition)
-}
-
-const removeCondition = (index: number) => {
-  if (group.value.conditions) {
-    group.value.conditions.splice(index, 1)
-    delete conditionValidations.value[index]
-
-    // 重新索引验证结果
-    const newValidations: { [key: number]: { valid: boolean; message: string } } = {}
-    Object.keys(conditionValidations.value).forEach((key) => {
-      const numKey = parseInt(key)
-      if (numKey > index) {
-        newValidations[numKey - 1] = conditionValidations.value[numKey]
-      } else if (numKey < index) {
-        newValidations[numKey] = conditionValidations.value[numKey]
-      }
-    })
-    conditionValidations.value = newValidations
-
-    updateValidationResult()
-  }
-}
-
-const handleConditionValidate = (index: number, result: { valid: boolean; message: string }) => {
-  conditionValidations.value[index] = result
-  updateValidationResult()
-}
-
-const updateValidationResult = () => {
-  if (!group.value.conditions || group.value.conditions.length === 0) {
-    isValid.value = false
-    validationMessage.value = '请至少添加一个条件'
-    emit('validate', { valid: false, message: validationMessage.value })
-    return
-  }
-
-  const validations = Object.values(conditionValidations.value)
-  const allValid = validations.every((v) => v.valid)
-
-  if (allValid) {
-    isValid.value = true
-    validationMessage.value = '条件组配置验证通过'
-  } else {
-    isValid.value = false
-    const errorMessages = validations.filter((v) => !v.valid).map((v) => v.message)
-    validationMessage.value = `条件配置错误: ${errorMessages.join('; ')}`
-  }
-
-  emit('validate', { valid: isValid.value, message: validationMessage.value })
-}
-
-// 监听条件数量变化
-watch(
-  () => group.value.conditions?.length,
-  () => {
-    updateValidationResult()
-  }
-)
-
-// 初始化
-onMounted(() => {
-  if (!group.value.conditions || group.value.conditions.length === 0) {
-    addCondition()
-  }
-})
-</script>

+ 22 - 46
src/views/iot/rule/scene/form/configs/ConditionGroupContainerConfig.vue

@@ -1,7 +1,6 @@
 <template>
   <div class="flex flex-col gap-16px">
     <!-- 条件组容器头部 -->
-    <!-- TODO @puhui999:这个是不是删除,不然有两个“附件条件组”的 header -->
     <div
       class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-8px"
     >
@@ -15,16 +14,14 @@
           <span>附加条件组</span>
         </div>
         <el-tag size="small" type="success">与主条件为且关系</el-tag>
-        <el-tag size="small" type="info">
-          {{ modelValue.subGroups?.length || 0 }}个子条件组
-        </el-tag>
+        <el-tag size="small" type="info"> {{ modelValue?.length || 0 }}个子条件组 </el-tag>
       </div>
       <div class="flex items-center gap-8px">
         <el-button
           type="primary"
           size="small"
           @click="addSubGroup"
-          :disabled="(modelValue.subGroups?.length || 0) >= maxSubGroups"
+          :disabled="(modelValue?.length || 0) >= maxSubGroups"
         >
           <Icon icon="ep:plus" />
           添加子条件组
@@ -37,21 +34,11 @@
     </div>
 
     <!-- 子条件组列表 -->
-    <div v-if="modelValue.subGroups && modelValue.subGroups.length > 0" class="space-y-16px">
+    <div v-if="modelValue && modelValue.length > 0" class="space-y-16px">
       <!-- 逻辑关系说明 -->
-      <!-- TODO @puhui999:这个可以去掉。。。提示有点太多了。 -->
-      <div v-if="modelValue.subGroups.length > 1" class="flex items-center justify-center">
-        <div
-          class="flex items-center gap-8px px-12px py-6px bg-orange-50 border border-orange-200 rounded-full text-12px text-orange-600"
-        >
-          <Icon icon="ep:info-filled" />
-          <span>子条件组之间为"或"关系,满足任意一组即可触发</span>
-        </div>
-      </div>
-
       <div class="relative">
         <div
-          v-for="(subGroup, subGroupIndex) in modelValue.subGroups"
+          v-for="(subGroup, subGroupIndex) in modelValue"
           :key="`sub-group-${subGroupIndex}`"
           class="relative"
         >
@@ -99,7 +86,7 @@
 
           <!-- 子条件组间的"或"连接符 -->
           <div
-            v-if="subGroupIndex < modelValue.subGroups!.length - 1"
+            v-if="subGroupIndex < modelValue!.length - 1"
             class="flex items-center justify-center py-12px"
           >
             <div class="flex items-center gap-8px">
@@ -136,27 +123,20 @@
 <script setup lang="ts">
 import { useVModel } from '@vueuse/core'
 import SubConditionGroupConfig from './SubConditionGroupConfig.vue'
-import {
-  ConditionGroupContainerFormData,
-  SubConditionGroupFormData
-} from '@/api/iot/rule/scene/scene.types'
 
 /** 条件组容器配置组件 */
 defineOptions({ name: 'ConditionGroupContainerConfig' })
 
-interface Props {
-  modelValue: ConditionGroupContainerFormData
+const props = defineProps<{
+  modelValue: any
   triggerType: number
-}
+}>()
 
-interface Emits {
-  (e: 'update:modelValue', value: ConditionGroupContainerFormData): void
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: any): void
   (e: 'validate', result: { valid: boolean; message: string }): void
   (e: 'remove'): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 const container = useVModel(props, 'modelValue', emit)
 
@@ -169,24 +149,20 @@ const subGroupValidations = ref<{ [key: number]: { valid: boolean; message: stri
 
 // 事件处理
 const addSubGroup = () => {
-  if (!container.value.subGroups) {
-    container.value.subGroups = []
+  if (!container.value) {
+    container.value = []
   }
 
-  if (container.value.subGroups.length >= maxSubGroups) {
+  if (container.value.length >= maxSubGroups) {
     return
   }
 
-  const newSubGroup: SubConditionGroupFormData = {
-    conditions: []
-  }
-
-  container.value.subGroups.push(newSubGroup)
+  container.value.push([])
 }
 
 const removeSubGroup = (index: number) => {
-  if (container.value.subGroups) {
-    container.value.subGroups.splice(index, 1)
+  if (container.value) {
+    container.value.splice(index, 1)
     delete subGroupValidations.value[index]
 
     // 重新索引验证结果
@@ -205,9 +181,9 @@ const removeSubGroup = (index: number) => {
   }
 }
 
-const updateSubGroup = (index: number, subGroup: SubConditionGroupFormData) => {
-  if (container.value.subGroups) {
-    container.value.subGroups[index] = subGroup
+const updateSubGroup = (index: number, subGroup: any) => {
+  if (container.value) {
+    container.value[index] = subGroup
   }
 }
 
@@ -221,7 +197,7 @@ const handleSubGroupValidate = (index: number, result: { valid: boolean; message
 }
 
 const updateValidationResult = () => {
-  if (!container.value.subGroups || container.value.subGroups.length === 0) {
+  if (!container.value || container.value.length === 0) {
     emit('validate', { valid: true, message: '条件组容器为空,验证通过' })
     return
   }
@@ -239,7 +215,7 @@ const updateValidationResult = () => {
 
 // 监听变化
 watch(
-  () => container.value.subGroups,
+  () => container.value,
   () => {
     updateValidationResult()
   },

+ 4 - 58
src/views/iot/rule/scene/form/configs/CurrentTimeConditionConfig.vue

@@ -89,34 +89,6 @@
         </el-form-item>
       </el-col>
     </el-row>
-
-    <!-- 条件预览 -->
-    <!-- puhui999:可以去掉。。。因为表单选择了,可以看懂的呀。 -->
-    <div
-      v-if="conditionPreview"
-      class="p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"
-    >
-      <div class="flex items-center gap-8px mb-8px">
-        <Icon icon="ep:view" class="text-[var(--el-color-info)] text-16px" />
-        <span class="text-14px font-500 text-[var(--el-text-color-primary)]">条件预览</span>
-      </div>
-      <div class="pl-24px">
-        <code
-          class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] p-8px rounded-4px font-mono"
-          >{{ conditionPreview }}</code
-        >
-      </div>
-    </div>
-
-    <!-- 验证结果 -->
-    <div v-if="validationMessage" class="mt-8px">
-      <el-alert
-        :title="validationMessage"
-        :type="isValid ? 'success' : 'error'"
-        :closable="false"
-        show-icon
-      />
-    </div>
   </div>
 </template>
 
@@ -130,17 +102,14 @@ import {
 /** 当前时间条件配置组件 */
 defineOptions({ name: 'CurrentTimeConditionConfig' })
 
-interface Props {
+const props = defineProps<{
   modelValue: ConditionFormData
-}
+}>()
 
-interface Emits {
+const emit = defineEmits<{
   (e: 'update:modelValue', value: ConditionFormData): void
   (e: 'validate', result: { valid: boolean; message: string }): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 const condition = useVModel(props, 'modelValue', emit)
 
@@ -211,29 +180,6 @@ const needsSecondTimeInput = computed(() => {
   return condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value
 })
 
-const conditionPreview = computed(() => {
-  if (!condition.value.operator) {
-    return ''
-  }
-
-  const operatorOption = timeOperatorOptions.find((opt) => opt.value === condition.value.operator)
-  const operatorLabel = operatorOption?.label || condition.value.operator
-
-  if (condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
-    return `当前时间 ${operatorLabel}`
-  }
-
-  if (!condition.value.timeValue) {
-    return `当前时间 ${operatorLabel} [未设置时间]`
-  }
-
-  if (needsSecondTimeInput.value && condition.value.timeValue2) {
-    return `当前时间 ${operatorLabel} ${condition.value.timeValue} 和 ${condition.value.timeValue2}`
-  }
-
-  return `当前时间 ${operatorLabel} ${condition.value.timeValue}`
-})
-
 // 事件处理
 const updateConditionField = (field: keyof ConditionFormData, value: any) => {
   condition.value[field] = value

+ 10 - 9
src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue

@@ -27,9 +27,13 @@
           <template #default>
             <div class="space-y-8px">
               <p class="m-0 text-14px text-[var(--el-text-color-primary)]">属性设置示例:</p>
-              <pre class="m-0 p-8px bg-[var(--el-fill-color-light)] rounded-4px text-12px text-[var(--el-text-color-regular)] overflow-x-auto"><code>{ "temperature": 25, "power": true }</code></pre>
+              <pre
+                class="m-0 p-8px bg-[var(--el-fill-color-light)] rounded-4px text-12px text-[var(--el-text-color-regular)] overflow-x-auto"
+              ><code>{ "temperature": 25, "power": true }</code></pre>
               <p class="m-0 text-14px text-[var(--el-text-color-primary)]">服务调用示例:</p>
-              <pre class="m-0 p-8px bg-[var(--el-fill-color-light)] rounded-4px text-12px text-[var(--el-text-color-regular)] overflow-x-auto"><code>{ "method": "restart", "params": { "delay": 5 } }</code></pre>
+              <pre
+                class="m-0 p-8px bg-[var(--el-fill-color-light)] rounded-4px text-12px text-[var(--el-text-color-regular)] overflow-x-auto"
+              ><code>{ "method": "restart", "params": { "delay": 5 } }</code></pre>
             </div>
           </template>
         </el-alert>
@@ -56,17 +60,14 @@ import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
 /** 设备控制配置组件 */
 defineOptions({ name: 'DeviceControlConfig' })
 
-interface Props {
+const props = defineProps<{
   modelValue: ActionFormData
-}
+}>()
 
-interface Emits {
+const emit = defineEmits<{
   (e: 'update:modelValue', value: ActionFormData): void
   (e: 'validate', result: { valid: boolean; message: string }): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 const action = useVModel(props, 'modelValue', emit)
 

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

@@ -26,85 +26,57 @@
 
     <!-- 状态和操作符选择 -->
     <el-row :gutter="16">
-      <!-- 状态选择 -->
+      <!-- 操作符选择 -->
       <el-col :span="12">
-        <el-form-item label="设备状态" required>
+        <el-form-item label="操作符" required>
           <el-select
-            :model-value="condition.param"
-            @update:model-value="(value) => updateConditionField('param', value)"
-            placeholder="请选择设备状态"
+            :model-value="condition.operator"
+            @update:model-value="(value) => updateConditionField('operator', value)"
+            placeholder="请选择操作符"
             class="w-full"
           >
             <el-option
-              v-for="option in deviceStatusOptions"
+              v-for="option in statusOperatorOptions"
               :key="option.value"
               :label="option.label"
               :value="option.value"
             >
-              <div class="flex items-center gap-8px">
-                <Icon :icon="option.icon" :class="option.iconClass" />
+              <div class="flex items-center justify-between w-full">
                 <span>{{ option.label }}</span>
-                <el-tag :type="option.tag" size="small">{{ option.description }}</el-tag>
+                <span class="text-12px text-[var(--el-text-color-secondary)]">{{
+                  option.description
+                }}</span>
               </div>
             </el-option>
           </el-select>
         </el-form-item>
       </el-col>
 
-      <!-- 操作符选择 -->
+      <!-- 状态选择 -->
       <el-col :span="12">
-        <el-form-item label="操作符" required>
+        <el-form-item label="设备状态" required>
           <el-select
-            :model-value="condition.operator"
-            @update:model-value="(value) => updateConditionField('operator', value)"
-            placeholder="请选择操作符"
+            :model-value="condition.param"
+            @update:model-value="(value) => updateConditionField('param', value)"
+            placeholder="请选择设备状态"
             class="w-full"
           >
             <el-option
-              v-for="option in statusOperatorOptions"
+              v-for="option in deviceStatusOptions"
               :key="option.value"
               :label="option.label"
               :value="option.value"
             >
-              <div class="flex items-center justify-between w-full">
+              <div class="flex items-center gap-8px">
+                <Icon :icon="option.icon" :class="option.iconClass" />
                 <span>{{ option.label }}</span>
-                <span class="text-12px text-[var(--el-text-color-secondary)]">{{
-                  option.description
-                }}</span>
+                <el-tag :type="option.tag" size="small">{{ option.description }}</el-tag>
               </div>
             </el-option>
           </el-select>
         </el-form-item>
       </el-col>
     </el-row>
-
-    <!-- 条件预览 -->
-    <!-- TODO puhui999:可以去掉。。。因为表单选择了,可以看懂的呀。 -->
-    <div
-      v-if="conditionPreview"
-      class="p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"
-    >
-      <div class="flex items-center gap-8px mb-8px">
-        <Icon icon="ep:view" class="text-[var(--el-color-info)] text-16px" />
-        <span class="text-14px font-500 text-[var(--el-text-color-primary)]">条件预览</span>
-      </div>
-      <div class="pl-24px">
-        <code
-          class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] p-8px rounded-4px font-mono"
-          >{{ conditionPreview }}</code
-        >
-      </div>
-    </div>
-
-    <!-- 验证结果 -->
-    <div v-if="validationMessage" class="mt-8px">
-      <el-alert
-        :title="validationMessage"
-        :type="isValid ? 'success' : 'error'"
-        :closable="false"
-        show-icon
-      />
-    </div>
   </div>
 </template>
 
@@ -112,22 +84,19 @@
 import { useVModel } from '@vueuse/core'
 import ProductSelector from '../selectors/ProductSelector.vue'
 import DeviceSelector from '../selectors/DeviceSelector.vue'
-import { ConditionFormData } from '@/api/iot/rule/scene/scene.types'
+import { TriggerConditionFormData } from '@/api/iot/rule/scene/scene.types'
 
 /** 设备状态条件配置组件 */
 defineOptions({ name: 'DeviceStatusConditionConfig' })
 
-interface Props {
-  modelValue: ConditionFormData
-}
+const props = defineProps<{
+  modelValue: TriggerConditionFormData
+}>()
 
-interface Emits {
-  (e: 'update:modelValue', value: ConditionFormData): void
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: TriggerConditionFormData): void
   (e: 'validate', result: { valid: boolean; message: string }): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 const condition = useVModel(props, 'modelValue', emit)
 
@@ -169,35 +138,19 @@ const statusOperatorOptions = [
 const validationMessage = ref('')
 const isValid = ref(true)
 
-// 计算属性
-const conditionPreview = computed(() => {
-  if (!condition.value.param || !condition.value.operator) {
-    return ''
-  }
-
-  const statusLabel =
-    deviceStatusOptions.find((opt) => opt.value === condition.value.param)?.label ||
-    condition.value.param
-  const operatorLabel =
-    statusOperatorOptions.find((opt) => opt.value === condition.value.operator)?.label ||
-    condition.value.operator
-
-  return `设备状态 ${operatorLabel} ${statusLabel}`
-})
-
 // 事件处理
-const updateConditionField = (field: keyof ConditionFormData, value: any) => {
+const updateConditionField = (field: any, value: any) => {
   condition.value[field] = value
   updateValidationResult()
 }
 
-const handleProductChange = (productId: number) => {
+const handleProductChange = (_: number) => {
   // 产品变化时清空设备
   condition.value.deviceId = undefined
   updateValidationResult()
 }
 
-const handleDeviceChange = (deviceId: number) => {
+const handleDeviceChange = (_: number) => {
   // 设备变化时可以进行其他处理
   updateValidationResult()
 }

+ 28 - 69
src/views/iot/rule/scene/form/configs/DeviceTriggerConfig.vue

@@ -1,58 +1,24 @@
-<!-- 设备触发配置组件 - 优化版本 -->
+<!-- 设备触发配置组件 -->
 <template>
   <div class="flex flex-col gap-16px">
     <!-- 主条件配置 - 默认直接展示 -->
     <div class="space-y-16px">
       <MainConditionConfig
-        v-model="trigger.mainCondition"
+        v-model="trigger"
         :trigger-type="trigger.type"
         @validate="handleMainConditionValidate"
       />
     </div>
 
     <!-- 条件组配置 -->
-    <div v-if="trigger.mainCondition" class="space-y-16px">
-      <div class="flex items-center justify-between">
-        <div class="flex items-center gap-8px">
-          <span class="text-14px font-500 text-[var(--el-text-color-primary)]">附加条件组</span>
-          <el-tag size="small" type="success">与主条件为且关系</el-tag>
-          <el-tag size="small" type="info">
-            {{ trigger.conditionGroup?.subGroups?.length || 0 }} 个子条件组
-          </el-tag>
-        </div>
-        <el-button
-          type="primary"
-          size="small"
-          @click="addConditionGroup"
-          v-if="!trigger.conditionGroup"
-        >
-          <Icon icon="ep:plus" />
-          添加条件组
-        </el-button>
-      </div>
-
+    <div class="space-y-16px">
       <!-- 条件组配置 -->
       <ConditionGroupContainerConfig
-        v-if="trigger.conditionGroup"
-        v-model="trigger.conditionGroup"
+        v-model="trigger.conditionGroups"
         :trigger-type="trigger.type"
         @validate="handleConditionGroupValidate"
         @remove="removeConditionGroup"
       />
-
-      <!-- 空状态 -->
-      <div v-else class="py-40px text-center">
-        <el-empty description="暂无触发条件">
-          <template #description>
-            <div class="space-y-8px">
-              <p class="text-[var(--el-text-color-secondary)]">暂无触发条件</p>
-              <p class="text-12px text-[var(--el-text-color-placeholder)]">
-                请使用上方的"添加条件组"按钮来设置触发规则
-              </p>
-            </div>
-          </template>
-        </el-empty>
-      </div>
     </div>
   </div>
 </template>
@@ -62,26 +28,21 @@ import { useVModel } from '@vueuse/core'
 
 import MainConditionConfig from './MainConditionConfig.vue'
 import ConditionGroupContainerConfig from './ConditionGroupContainerConfig.vue'
-import {
-  TriggerFormData,
-  IotRuleSceneTriggerTypeEnum as TriggerTypeEnum
-} from '@/api/iot/rule/scene/scene.types'
+import { TriggerFormData } from '@/api/iot/rule/scene/scene.types'
+import { IotRuleSceneTriggerTypeEnum as TriggerTypeEnum } from '@/views/iot/utils/constants'
 
 /** 设备触发配置组件 */
 defineOptions({ name: 'DeviceTriggerConfig' })
 
-// TODO @puhui999:下面的 Props、Emits 可以合并到变量那;
-interface Props {
+const props = defineProps<{
   modelValue: TriggerFormData
-}
+  index: number
+}>()
 
-interface Emits {
+const emit = defineEmits<{
   (e: 'update:modelValue', value: TriggerFormData): void
-  (e: 'validate', result: { valid: boolean; message: string }): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+  (e: 'validate', value: { valid: boolean; message: string }): void
+}>()
 
 const trigger = useVModel(props, 'modelValue', emit)
 
@@ -95,16 +56,17 @@ const isValid = ref(true)
 
 // 初始化主条件
 const initMainCondition = () => {
-  if (!trigger.value.mainCondition) {
-    trigger.value.mainCondition = {
-      type: trigger.value.type, // 使用触发事件类型作为条件类型
-      productId: undefined,
-      deviceId: undefined,
-      identifier: '',
-      operator: '=',
-      param: ''
-    }
-  }
+  // TODO @puhui999: 等到编辑回显时联调
+  // if (!trigger.value.mainCondition) {
+  //   trigger.value = {
+  //     type: trigger.value.type, // 使用触发事件类型作为条件类型
+  //     productId: undefined,
+  //     deviceId: undefined,
+  //     identifier: '',
+  //     operator: '=',
+  //     param: ''
+  //   }
+  // }
 }
 
 // 监听触发器类型变化,自动初始化主条件
@@ -116,28 +78,25 @@ watch(
   { immediate: true }
 )
 
-// 新的事件处理函数
 const handleMainConditionValidate = (result: { valid: boolean; message: string }) => {
   mainConditionValidation.value = result
   updateValidationResult()
 }
 
 const addConditionGroup = () => {
-  if (!trigger.value.conditionGroup) {
-    trigger.value.conditionGroup = {
-      subGroups: []
-    }
+  if (!trigger.value.conditionGroups) {
+    trigger.value.conditionGroups = []
   }
+  trigger.value.conditionGroups.push([])
 }
 
 // 事件处理
-
 const handleConditionGroupValidate = () => {
   updateValidationResult()
 }
 
 const removeConditionGroup = () => {
-  trigger.value.conditionGroup = undefined
+  trigger.value.conditionGroups = undefined
 }
 
 const updateValidationResult = () => {
@@ -158,7 +117,7 @@ const updateValidationResult = () => {
   }
 
   // 主条件验证
-  if (!trigger.value.mainCondition) {
+  if (!trigger.value.value) {
     isValid.value = false
     validationMessage.value = '请配置主条件'
     emit('validate', { valid: false, message: validationMessage.value })

+ 15 - 24
src/views/iot/rule/scene/form/configs/MainConditionConfig.vue

@@ -20,19 +20,13 @@
     </div>
 
     <!-- 主条件配置 -->
-    <!-- TODO @puhui999:这里可以简化下,主条件是不能删除的。。。 -->
     <div v-else class="space-y-16px">
       <div class="flex items-center justify-between">
         <div class="flex items-center gap-8px">
           <span class="text-14px font-500 text-[var(--el-text-color-primary)]">主条件</span>
           <el-tag size="small" type="primary">必须满足</el-tag>
         </div>
-        <el-button type="danger" size="small" text @click="removeMainCondition">
-          <Icon icon="ep:delete" />
-          删除
-        </el-button>
       </div>
-
       <MainConditionInnerConfig
         :model-value="modelValue"
         @update:model-value="updateCondition"
@@ -46,48 +40,45 @@
 <script setup lang="ts">
 import MainConditionInnerConfig from './MainConditionInnerConfig.vue'
 import {
-  ConditionFormData,
-  IotRuleSceneTriggerConditionTypeEnum
+  IotRuleSceneTriggerConditionTypeEnum,
+  TriggerFormData
 } from '@/api/iot/rule/scene/scene.types'
 
 /** 主条件配置组件 */
 defineOptions({ name: 'MainConditionConfig' })
 
-interface Props {
-  modelValue?: ConditionFormData
+defineProps<{
+  modelValue?: TriggerFormData
   triggerType: number
-}
+}>()
 
-interface Emits {
-  (e: 'update:modelValue', value?: ConditionFormData): void
+const emit = defineEmits<{
+  (e: 'update:modelValue', value?: TriggerFormData): void
   (e: 'validate', result: { valid: boolean; message: string }): void
-}
-
-defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 // 事件处理
 const addMainCondition = () => {
-  const newCondition: ConditionFormData = {
+  const newCondition: TriggerFormData = {
     type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
     productId: undefined,
     deviceId: undefined,
     identifier: '',
     operator: '=',
-    param: ''
+    value: ''
   }
   emit('update:modelValue', newCondition)
 }
 
-const removeMainCondition = () => {
-  emit('update:modelValue', undefined)
-}
-
-const updateCondition = (condition: ConditionFormData) => {
+const updateCondition = (condition: TriggerFormData) => {
   emit('update:modelValue', condition)
 }
 
 const handleValidate = (result: { valid: boolean; message: string }) => {
   emit('validate', result)
 }
+
+onMounted(() => {
+  addMainCondition()
+})
 </script>

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

@@ -1,11 +1,5 @@
 <template>
   <div class="space-y-16px">
-    <!-- 触发事件类型显示 -->
-    <div class="flex items-center gap-8px mb-16px">
-      <span class="text-14px text-[var(--el-text-color-regular)]">触发事件类型:</span>
-      <el-tag size="small" type="primary">{{ getTriggerTypeText(triggerType) }}</el-tag>
-    </div>
-
     <!-- 设备属性条件配置 -->
     <div v-if="isDevicePropertyTrigger" class="space-y-16px">
       <!-- 产品设备选择 -->
@@ -73,14 +67,6 @@
           </el-form-item>
         </el-col>
       </el-row>
-
-      <!-- 条件预览 -->
-      <!-- TODO puhui999:可以去掉。。。因为表单选择了,可以看懂的呀。 -->
-      <div v-if="conditionPreview" class="mt-12px">
-        <div class="text-12px text-[var(--el-text-color-secondary)]">
-          预览:{{ conditionPreview }}
-        </div>
-      </div>
     </div>
 
     <!-- 设备状态条件配置 -->
@@ -112,24 +98,21 @@ import OperatorSelector from '../selectors/OperatorSelector.vue'
 import ValueInput from '../inputs/ValueInput.vue'
 import DeviceStatusConditionConfig from './DeviceStatusConditionConfig.vue'
 import { ConditionFormData } from '@/api/iot/rule/scene/scene.types'
-import { IotRuleSceneTriggerTypeEnum } from '@/api/iot/rule/scene/scene.types'
+import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
 import { useVModel } from '@vueuse/core'
 
 /** 主条件内部配置组件 */
 defineOptions({ name: 'MainConditionInnerConfig' })
 
-interface Props {
+const props = defineProps<{
   modelValue: ConditionFormData
   triggerType: number
-}
+}>()
 
-interface Emits {
+const emit = defineEmits<{
   (e: 'update:modelValue', value: ConditionFormData): void
   (e: 'validate', result: { valid: boolean; message: string }): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 // 响应式数据
 const condition = useVModel(props, 'modelValue', emit)
@@ -152,13 +135,6 @@ const isDeviceStatusTrigger = computed(() => {
   return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE
 })
 
-const conditionPreview = computed(() => {
-  if (!condition.value.productId || !condition.value.deviceId || !condition.value.identifier) {
-    return ''
-  }
-  return `设备[${condition.value.deviceId}]的${condition.value.identifier} ${condition.value.operator} ${condition.value.param}`
-})
-
 // 获取触发类型文本
 // TODO @puhui999:是不是有枚举可以服用哈;
 const getTriggerTypeText = (type: number) => {

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

@@ -1,10 +1,7 @@
 <template>
   <div class="p-16px">
     <!-- 空状态 -->
-    <div
-      v-if="!subGroup.conditions || subGroup.conditions.length === 0"
-      class="text-center py-24px"
-    >
+    <div v-if="!subGroup || subGroup.length === 0" class="text-center py-24px">
       <div class="flex flex-col items-center gap-12px">
         <Icon icon="ep:plus" class="text-32px text-[var(--el-text-color-placeholder)]" />
         <div class="text-[var(--el-text-color-secondary)]">
@@ -21,7 +18,7 @@
     <!-- 条件列表 -->
     <div v-else class="space-y-16px">
       <div
-        v-for="(condition, conditionIndex) in subGroup.conditions"
+        v-for="(condition, conditionIndex) in subGroup"
         :key="`condition-${conditionIndex}`"
         class="relative"
       >
@@ -47,7 +44,7 @@
               size="small"
               text
               @click="removeCondition(conditionIndex)"
-              v-if="subGroup.conditions!.length > 1"
+              v-if="subGroup!.length > 1"
               class="hover:bg-red-50"
             >
               <Icon icon="ep:delete" />
@@ -63,33 +60,11 @@
             />
           </div>
         </div>
-
-        <!-- 条件间的"且"连接符 -->
-        <!-- TODO @puhu999:建议去掉,有点元素太丰富了。 -->
-        <div
-          v-if="conditionIndex < subGroup.conditions!.length - 1"
-          class="flex items-center justify-center py-8px"
-        >
-          <div class="flex items-center gap-8px">
-            <!-- 连接线 -->
-            <div class="w-24px h-1px bg-green-300"></div>
-            <!-- 且标签 -->
-            <div class="px-12px py-4px bg-green-100 border border-green-300 rounded-full">
-              <span class="text-12px font-600 text-green-600">且</span>
-            </div>
-            <!-- 连接线 -->
-            <div class="w-24px h-1px bg-green-300"></div>
-          </div>
-        </div>
       </div>
 
       <!-- 添加条件按钮 -->
       <div
-        v-if="
-          subGroup.conditions &&
-          subGroup.conditions.length > 0 &&
-          subGroup.conditions.length < maxConditions
-        "
+        v-if="subGroup && subGroup.length > 0 && subGroup.length < maxConditions"
         class="text-center py-16px"
       >
         <el-button type="primary" plain @click="addCondition">
@@ -108,27 +83,23 @@
 import { useVModel } from '@vueuse/core'
 import ConditionConfig from './ConditionConfig.vue'
 import {
-  SubConditionGroupFormData,
-  ConditionFormData,
-  IotRuleSceneTriggerConditionTypeEnum
+  IotRuleSceneTriggerConditionTypeEnum,
+  TriggerConditionFormData
 } from '@/api/iot/rule/scene/scene.types'
 
 /** 子条件组配置组件 */
 defineOptions({ name: 'SubConditionGroupConfig' })
 
-interface Props {
-  modelValue: SubConditionGroupFormData
+const props = defineProps<{
+  modelValue: TriggerConditionFormData[]
   triggerType: number
   maxConditions?: number
-}
+}>()
 
-interface Emits {
-  (e: 'update:modelValue', value: SubConditionGroupFormData): void
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: TriggerConditionFormData[]): void
   (e: 'validate', result: { valid: boolean; message: string }): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 const subGroup = useVModel(props, 'modelValue', emit)
 
@@ -140,15 +111,13 @@ const conditionValidations = ref<{ [key: number]: { valid: boolean; message: str
 
 // 事件处理
 const addCondition = () => {
-  if (!subGroup.value.conditions) {
-    subGroup.value.conditions = []
+  if (!subGroup.value) {
+    subGroup.value = []
   }
-
-  if (subGroup.value.conditions.length >= maxConditions.value) {
+  if (subGroup.value.length >= maxConditions.value) {
     return
   }
-
-  const newCondition: ConditionFormData = {
+  const newCondition: TriggerConditionFormData = {
     type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
     productId: undefined,
     deviceId: undefined,
@@ -156,13 +125,12 @@ const addCondition = () => {
     operator: '=',
     param: ''
   }
-
-  subGroup.value.conditions.push(newCondition)
+  subGroup.value.push(newCondition)
 }
 
 const removeCondition = (index: number) => {
-  if (subGroup.value.conditions) {
-    subGroup.value.conditions.splice(index, 1)
+  if (subGroup.value) {
+    subGroup.value.splice(index, 1)
     delete conditionValidations.value[index]
 
     // 重新索引验证结果
@@ -181,9 +149,9 @@ const removeCondition = (index: number) => {
   }
 }
 
-const updateCondition = (index: number, condition: ConditionFormData) => {
-  if (subGroup.value.conditions) {
-    subGroup.value.conditions[index] = condition
+const updateCondition = (index: number, condition: TriggerConditionFormData) => {
+  if (subGroup.value) {
+    subGroup.value[index] = condition
   }
 }
 
@@ -193,7 +161,7 @@ const handleConditionValidate = (index: number, result: { valid: boolean; messag
 }
 
 const updateValidationResult = () => {
-  if (!subGroup.value.conditions || subGroup.value.conditions.length === 0) {
+  if (!subGroup.value || subGroup.value.length === 0) {
     emit('validate', { valid: false, message: '子条件组至少需要一个条件' })
     return
   }
@@ -211,7 +179,7 @@ const updateValidationResult = () => {
 
 // 监听变化
 watch(
-  () => subGroup.value.conditions,
+  () => subGroup.value,
   () => {
     updateValidationResult()
   },

+ 4 - 9
src/views/iot/rule/scene/form/configs/TimerTriggerConfig.vue

@@ -25,18 +25,13 @@ import { Crontab } from '@/components/Crontab'
 /** 定时触发配置组件 */
 defineOptions({ name: 'TimerTriggerConfig' })
 
-// TODO @puhui999:下面的 Props、Emits 可以合并到变量那;
-interface Props {
+const props = defineProps<{
   modelValue?: string
-}
-
-interface Emits {
+}>()
+const emit = defineEmits<{
   (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: '0 0 12 * * ?'

+ 14 - 10
src/views/iot/rule/scene/form/sections/ActionSection.vue

@@ -36,7 +36,11 @@
 
       <!-- 执行器列表 -->
       <div v-else class="space-y-16px">
-        <div v-for="(action, index) in actions" :key="`action-${index}`" class="p-16px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]">
+        <div
+          v-for="(action, index) in actions"
+          :key="`action-${index}`"
+          class="p-16px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]"
+        >
           <div class="flex items-center justify-between mb-16px">
             <div class="flex items-center gap-8px">
               <Icon icon="ep:setting" class="text-[var(--el-color-success)] text-16px" />
@@ -92,7 +96,9 @@
           <Icon icon="ep:plus" />
           继续添加执行器
         </el-button>
-        <span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]"> 最多可添加 {{ maxActions }} 个执行器 </span>
+        <span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]">
+          最多可添加 {{ maxActions }} 个执行器
+        </span>
       </div>
 
       <!-- 验证结果 -->
@@ -113,10 +119,8 @@ 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,
-  IotRuleSceneActionTypeEnum as ActionTypeEnum
-} from '@/api/iot/rule/scene/scene.types'
+import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
+import { IotRuleSceneActionTypeEnum as ActionTypeEnum } from '@/views/iot/utils/constants'
 
 /** 执行器配置组件 */
 defineOptions({ name: 'ActionSection' })
@@ -173,11 +177,13 @@ const actionTypeTags = {
 
 // 工具函数
 const isDeviceAction = (type: number) => {
-  return [ActionTypeEnum.DEVICE_PROPERTY_SET, ActionTypeEnum.DEVICE_SERVICE_INVOKE].includes(type)
+  return [ActionTypeEnum.DEVICE_PROPERTY_SET, ActionTypeEnum.DEVICE_SERVICE_INVOKE].includes(
+    type as any
+  )
 }
 
 const isAlertAction = (type: number) => {
-  return [ActionTypeEnum.ALERT_TRIGGER, ActionTypeEnum.ALERT_RECOVER].includes(type)
+  return [ActionTypeEnum.ALERT_TRIGGER, ActionTypeEnum.ALERT_RECOVER].includes(type as any)
 }
 
 const getActionTypeName = (type: number) => {
@@ -277,5 +283,3 @@ watch(
   }
 )
 </script>
-
-

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

@@ -1,6 +1,6 @@
 <!-- 基础信息配置组件 -->
 <template>
-  <el-card class="border border-[var(--el-border-color-light)] rounded-8px" shadow="never">
+  <el-card class="border border-[var(--el-border-color-light)] rounded-8px mb-10px" shadow="never">
     <template #header>
       <div class="flex items-center justify-between">
         <div class="flex items-center gap-8px">
@@ -8,10 +8,7 @@
           <span class="text-16px font-600 text-[var(--el-text-color-primary)]">基础信息</span>
         </div>
         <div class="flex items-center gap-8px">
-          <!-- TODO @puhui999:dict-tag 可以哇? -->
-          <el-tag :type="formData.status === 0 ? 'success' : 'danger'" size="small">
-            {{ formData.status === 0 ? '启用' : '禁用' }}
-          </el-tag>
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData.status" />
         </div>
       </div>
     </template>
@@ -60,25 +57,19 @@
 
 <script setup lang="ts">
 import { useVModel } from '@vueuse/core'
-import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
 
 /** 基础信息配置组件 */
 defineOptions({ name: 'BasicInfoSection' })
 
-// TODO @puhui999:下面的 Props、Emits 可以合并到变量那;
-
-interface Props {
+const props = defineProps<{
   modelValue: RuleSceneFormData
   rules?: any
-}
-
-interface Emits {
+}>()
+const emit = defineEmits<{
   (e: 'update:modelValue', value: RuleSceneFormData): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 const formData = useVModel(props, 'modelValue', emit)
 </script>

+ 156 - 121
src/views/iot/rule/scene/form/sections/TriggerSection.vue

@@ -1,47 +1,98 @@
 <template>
-  <el-card class="border border-[var(--el-border-color-light)] rounded-8px" shadow="never">
-    <!-- TODO @puhui999:触发器还是多个。。。每个触发器里面有事件类型 + 附加条件组(最好文案上,和阿里 iot 保持相对一致) -->
+  <el-card class="border border-[var(--el-border-color-light)] rounded-8px mb-10px" shadow="never">
     <template #header>
-      <div class="flex items-center gap-8px">
-        <Icon icon="ep:lightning" class="text-[var(--el-color-primary)] text-18px" />
-        <span class="text-16px font-600 text-[var(--el-text-color-primary)]">触发器配置</span>
-        <el-tag size="small" type="info">场景触发器</el-tag>
+      <div class="flex items-center justify-between">
+        <div class="flex items-center gap-8px">
+          <Icon icon="ep:lightning" class="text-[var(--el-color-primary)] text-18px" />
+          <span class="text-16px font-600 text-[var(--el-text-color-primary)]">触发器配置</span>
+          <el-tag size="small" type="info">{{ triggers.length }} 个触发器</el-tag>
+        </div>
+        <el-button type="primary" size="small" @click="addTrigger">
+          <Icon icon="ep:plus" />
+          添加触发器
+        </el-button>
       </div>
     </template>
 
-    <div class="p-16px space-y-16px">
-      <!-- 触发事件类型选择 -->
-      <el-form-item label="触发事件类型" required>
-        <el-select
-          :model-value="trigger.type"
-          @update:model-value="(value) => updateTriggerType(value)"
-          @change="onTriggerTypeChange"
-          placeholder="请选择触发事件类型"
-          class="w-full"
+    <div class="p-16px space-y-24px">
+      <!-- 触发器列表 -->
+      <div v-if="triggers.length > 0" class="space-y-24px">
+        <div
+          v-for="(triggerItem, index) in triggers"
+          :key="`trigger-${index}`"
+          class="border border-[var(--el-border-color-light)] rounded-8px p-16px relative"
         >
-          <el-option
-            v-for="option in triggerTypeOptions"
-            :key="option.value"
-            :label="option.label"
-            :value="option.value"
+          <!-- 触发器头部 -->
+          <div class="flex items-center justify-between mb-16px">
+            <div class="flex items-center gap-8px">
+              <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
+                触发器 {{ index + 1 }}
+              </span>
+              <el-tag size="small" :type="getTriggerTagType(triggerItem.type)">
+                {{ getTriggerTypeLabel(triggerItem.type) }}
+              </el-tag>
+            </div>
+            <div class="flex items-center gap-8px">
+              <el-button
+                v-if="triggers.length > 1"
+                type="danger"
+                size="small"
+                text
+                @click="removeTrigger(index)"
+              >
+                <Icon icon="ep:delete" />
+                删除
+              </el-button>
+            </div>
+          </div>
+
+          <!-- 触发事件类型选择 -->
+          <el-form-item label="触发事件类型" required>
+            <el-select
+              :model-value="triggerItem.type"
+              @update:model-value="(value) => updateTriggerType(index, value)"
+              placeholder="请选择触发事件类型"
+              class="w-full"
+            >
+              <el-option
+                v-for="option in triggerTypeOptions"
+                :key="option.value"
+                :label="option.label"
+                :value="option.value"
+              />
+            </el-select>
+          </el-form-item>
+
+          <!-- 设备触发配置 -->
+          <DeviceTriggerConfig
+            v-if="isDeviceTrigger(triggerItem.type)"
+            :model-value="triggerItem"
+            :index="index"
+            @update:model-value="(value) => updateTriggerDeviceConfig(index, value)"
           />
-        </el-select>
-      </el-form-item>
-
-      <!-- 设备触发配置 -->
-      <DeviceTriggerConfig
-        v-if="isDeviceTrigger(trigger.type)"
-        :model-value="trigger"
-        @update:model-value="updateTrigger"
-      />
-
-      <!-- 定时触发配置 -->
-      <!-- TODO @puhui999:这里要不 v-else 好了? -->
-      <TimerTriggerConfig
-        v-if="trigger.type === TriggerTypeEnum.TIMER"
-        :model-value="trigger.cronExpression"
-        @update:model-value="updateTriggerCronExpression"
-      />
+
+          <!-- 定时触发配置 -->
+          <TimerTriggerConfig
+            v-else-if="triggerItem.type === TriggerTypeEnum.TIMER"
+            :model-value="triggerItem.cronExpression"
+            @update:model-value="(value) => updateTriggerCronConfig(index, value)"
+          />
+        </div>
+      </div>
+
+      <!-- 空状态 -->
+      <div v-else class="py-40px text-center">
+        <el-empty description="暂无触发器">
+          <template #description>
+            <div class="space-y-8px">
+              <p class="text-[var(--el-text-color-secondary)]">暂无触发器配置</p>
+              <p class="text-12px text-[var(--el-text-color-placeholder)]">
+                请使用上方的"添加触发器"按钮来设置触发规则
+              </p>
+            </div>
+          </template>
+        </el-empty>
+      </div>
     </div>
   </el-card>
 </template>
@@ -50,108 +101,92 @@
 import { useVModel } from '@vueuse/core'
 import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue'
 import TimerTriggerConfig from '../configs/TimerTriggerConfig.vue'
+import { TriggerFormData } from '@/api/iot/rule/scene/scene.types'
 import {
-  TriggerFormData,
-  IotRuleSceneTriggerTypeEnum as TriggerTypeEnum
-} from '@/api/iot/rule/scene/scene.types'
+  getTriggerTypeOptions,
+  IotRuleSceneTriggerTypeEnum as TriggerTypeEnum,
+  IotRuleSceneTriggerTypeEnum,
+  isDeviceTrigger
+} from '@/views/iot/utils/constants'
 
 /** 触发器配置组件 */
 defineOptions({ name: 'TriggerSection' })
 
-// TODO @puhui999:下面的 Props、Emits 可以合并到变量那;
-interface Props {
-  trigger: TriggerFormData
+const props = defineProps<{
+  triggers: TriggerFormData[]
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:triggers', value: TriggerFormData[]): void
+}>()
+
+const triggers = useVModel(props, 'triggers', emit)
+
+// 触发器类型选项(从 constants 中获取)
+const triggerTypeOptions = getTriggerTypeOptions()
+
+// 工具函数
+const getTriggerTypeLabel = (type: number): string => {
+  const option = triggerTypeOptions.find((opt) => opt.value === type)
+  return option?.label || '未知类型'
 }
 
-interface Emits {
-  (e: 'update:trigger', value: TriggerFormData): void
+const getTriggerTagType = (type: number): string => {
+  if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
+    return 'warning'
+  }
+  return isDeviceTrigger(type) ? 'success' : 'info'
 }
 
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
-
-const trigger = useVModel(props, 'trigger', emit)
-
-// 触发器类型选项
-// TODO @puhui999:/Users/yunai/Java/yudao-ui-admin-vue3/src/views/iot/utils/constants.ts
-const triggerTypeOptions = [
-  {
-    value: TriggerTypeEnum.DEVICE_STATE_UPDATE,
-    label: '设备状态变更'
-  },
-  {
-    value: TriggerTypeEnum.DEVICE_PROPERTY_POST,
-    label: '设备属性上报'
-  },
-  {
-    value: TriggerTypeEnum.DEVICE_EVENT_POST,
-    label: '设备事件上报'
-  },
-  {
-    value: TriggerTypeEnum.DEVICE_SERVICE_INVOKE,
-    label: '设备服务调用'
-  },
-  {
-    value: TriggerTypeEnum.TIMER,
-    label: '定时触发'
+// 事件处理函数
+const addTrigger = () => {
+  const newTrigger: TriggerFormData = {
+    type: TriggerTypeEnum.DEVICE_STATE_UPDATE,
+    productId: undefined,
+    deviceId: undefined,
+    identifier: undefined,
+    operator: undefined,
+    value: undefined,
+    cronExpression: undefined,
+    conditionGroups: [] // 空的条件组数组
   }
-]
+  triggers.value.push(newTrigger)
+}
 
-// 工具函数
-// TODO @puhui999:/Users/yunai/Java/yudao-ui-admin-vue3/src/views/iot/utils/constants.ts
-const isDeviceTrigger = (type: number) => {
-  const deviceTriggerTypes = [
-    TriggerTypeEnum.DEVICE_STATE_UPDATE,
-    TriggerTypeEnum.DEVICE_PROPERTY_POST,
-    TriggerTypeEnum.DEVICE_EVENT_POST,
-    TriggerTypeEnum.DEVICE_SERVICE_INVOKE
-  ] as number[]
-  return deviceTriggerTypes.includes(type)
+const removeTrigger = (index: number) => {
+  if (triggers.value.length > 1) {
+    triggers.value.splice(index, 1)
+  }
 }
 
-// 事件处理
-const updateTriggerType = (type: number) => {
-  trigger.value.type = type
-  onTriggerTypeChange(type)
+const updateTriggerType = (index: number, type: number) => {
+  triggers.value[index].type = type
+  onTriggerTypeChange(index, type)
 }
 
-// TODO @puhui999:updateTriggerDeviceConfig
-const updateTrigger = (newTrigger: TriggerFormData) => {
-  trigger.value = newTrigger
+const updateTriggerDeviceConfig = (index: number, newTrigger: TriggerFormData) => {
+  triggers.value[index] = newTrigger
 }
 
-// TODO @puhui999:updateTriggerCronConfig
-const updateTriggerCronExpression = (cronExpression?: string) => {
-  trigger.value.cronExpression = cronExpression
+const updateTriggerCronConfig = (index: number, cronExpression?: string) => {
+  triggers.value[index].cronExpression = cronExpression
 }
 
-const onTriggerTypeChange = (type: number) => {
-  // 清理不相关的配置
-  if (type === TriggerTypeEnum.TIMER) {
-    trigger.value.productId = undefined
-    trigger.value.deviceId = undefined
-    trigger.value.identifier = undefined
-    trigger.value.operator = undefined
-    trigger.value.value = undefined
-    trigger.value.mainCondition = undefined
-    trigger.value.conditionGroup = undefined
-    if (!trigger.value.cronExpression) {
-      trigger.value.cronExpression = '0 0 12 * * ?'
-    }
-  } else {
-    trigger.value.cronExpression = undefined
-    if (type === TriggerTypeEnum.DEVICE_STATE_UPDATE) {
-      trigger.value.mainCondition = undefined
-      trigger.value.conditionGroup = undefined
-    } else {
-      // 设备属性、事件、服务触发需要条件配置
-      if (!trigger.value.mainCondition) {
-        trigger.value.mainCondition = undefined // 等待用户配置
-      }
-      if (!trigger.value.conditionGroup) {
-        trigger.value.conditionGroup = undefined // 可选的条件组
-      }
-    }
-  }
+const onTriggerTypeChange = (index: number, _: number) => {
+  const triggerItem = triggers.value[index]
+  triggerItem.productId = undefined
+  triggerItem.deviceId = undefined
+  triggerItem.identifier = undefined
+  triggerItem.operator = undefined
+  triggerItem.value = undefined
+  triggerItem.cronExpression = undefined
+  triggerItem.conditionGroups = []
 }
+
+// 初始化:确保至少有一个触发器
+onMounted(() => {
+  if (triggers.value.length === 0) {
+    addTrigger()
+  }
+})
 </script>

+ 11 - 4
src/views/iot/rule/scene/form/selectors/ActionTypeSelector.vue

@@ -17,10 +17,17 @@
         >
           <div class="flex items-center justify-between w-full py-4px">
             <div class="flex items-center gap-12px flex-1">
-              <Icon :icon="option.icon" class="text-18px text-[var(--el-color-primary)] flex-shrink-0" />
+              <Icon
+                :icon="option.icon"
+                class="text-18px text-[var(--el-color-primary)] flex-shrink-0"
+              />
               <div class="flex-1">
-                <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ option.label }}</div>
-                <div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">{{ option.description }}</div>
+                <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{
+                  option.label
+                }}</div>
+                <div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">{{
+                  option.description
+                }}</div>
               </div>
             </div>
             <el-tag :type="option.tag" size="small">
@@ -35,7 +42,7 @@
 
 <script setup lang="ts">
 import { useVModel } from '@vueuse/core'
-import { IotRuleSceneActionTypeEnum } from '@/api/iot/rule/scene/scene.types'
+import { IotRuleSceneActionTypeEnum } from '@/views/iot/utils/constants'
 
 /** 执行器类型选择组件 */
 defineOptions({ name: 'ActionTypeSelector' })

+ 5 - 8
src/views/iot/rule/scene/form/selectors/ConditionTypeSelector.vue

@@ -24,22 +24,19 @@
 </template>
 
 <script setup lang="ts">
-import { IotRuleSceneTriggerConditionTypeEnum } from '@/api/iot/rule/scene/scene.types'
+import { IotRuleSceneTriggerConditionTypeEnum } from '@/views/iot/utils/constants'
 
 /** 条件类型选择器组件 */
 defineOptions({ name: 'ConditionTypeSelector' })
 
-interface Props {
+defineProps<{
   modelValue?: number
-}
+}>()
 
-interface Emits {
+const emit = defineEmits<{
   (e: 'update:modelValue', value: number): void
   (e: 'change', value: number): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 // 条件类型选项
 const conditionTypeOptions = [

+ 7 - 10
src/views/iot/rule/scene/form/selectors/DeviceSelector.vue

@@ -18,9 +18,9 @@
     >
       <div class="flex items-center justify-between w-full py-4px">
         <div class="flex-1">
-          <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{
-            device.deviceName
-          }}</div>
+          <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px"
+            >{{ device.deviceName }}
+          </div>
           <div class="text-12px text-[var(--el-text-color-secondary)]">{{ device.deviceKey }}</div>
         </div>
         <div class="flex items-center gap-4px">
@@ -42,18 +42,15 @@ import { DeviceApi } from '@/api/iot/device/device'
 /** 设备选择器组件 */
 defineOptions({ name: 'DeviceSelector' })
 
-interface Props {
+const props = defineProps<{
   modelValue?: number
   productId?: number
-}
+}>()
 
-interface Emits {
+const emit = defineEmits<{
   (e: 'update:modelValue', value?: number): void
   (e: 'change', value?: number): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 // 状态
 const deviceLoading = ref(false)

+ 58 - 57
src/views/iot/rule/scene/form/selectors/OperatorSelector.vue

@@ -15,139 +15,140 @@
       >
         <div class="flex items-center justify-between w-full py-4px">
           <div class="flex items-center gap-8px">
-            <div class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ operator.label }}</div>
-            <div class="text-12px text-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)] px-6px py-2px rounded-4px font-mono">{{ operator.symbol }}</div>
+            <div class="text-14px font-500 text-[var(--el-text-color-primary)]">
+              {{ operator.label }}
+            </div>
+            <div
+              class="text-12px text-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)] px-6px py-2px rounded-4px font-mono"
+            >
+              {{ operator.symbol }}
+            </div>
+          </div>
+          <div class="text-12px text-[var(--el-text-color-secondary)]">
+            {{ operator.description }}
           </div>
-          <div class="text-12px text-[var(--el-text-color-secondary)]">{{ operator.description }}</div>
         </div>
       </el-option>
     </el-select>
-
-    <!-- 操作符说明 -->
-    <!-- TODO @puhui999:这个去掉 -->
-    <div v-if="selectedOperator" class="mt-8px p-8px bg-[var(--el-fill-color-light)] rounded-4px border border-[var(--el-border-color-lighter)]">
-      <div class="flex items-center gap-6px">
-        <Icon icon="ep:info-filled" class="text-12px text-[var(--el-color-info)]" />
-        <span class="text-12px text-[var(--el-text-color-secondary)]">{{ selectedOperator.description }}</span>
-      </div>
-      <div v-if="selectedOperator.example" class="flex items-center gap-6px mt-4px">
-        <span class="text-12px text-[var(--el-text-color-secondary)]">示例:</span>
-        <code class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] px-4px py-2px rounded-2px font-mono">{{ selectedOperator.example }}</code>
-      </div>
-    </div>
   </div>
 </template>
 
 <script setup lang="ts">
 import { useVModel } from '@vueuse/core'
+import { IotRuleSceneTriggerConditionParameterOperatorEnum } from '@/views/iot/utils/constants'
 
 /** 操作符选择器组件 */
 defineOptions({ name: 'OperatorSelector' })
 
-interface Props {
+const props = defineProps<{
   modelValue?: string
   propertyType?: string
-}
+}>()
 
-interface Emits {
+const emit = defineEmits<{
   (e: 'update:modelValue', value: string): void
   (e: 'change', value: string): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 const localValue = useVModel(props, 'modelValue', emit)
 
-// 所有操作符定义
+// 基于枚举的操作符定义
 const allOperators = [
   {
-    value: '=',
-    label: '等于',
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.name,
     symbol: '=',
     description: '值完全相等时触发',
     example: 'temperature = 25',
     supportedTypes: ['int', 'float', 'double', 'string', 'bool', 'enum']
   },
   {
-    value: '!=',
-    label: '不等于',
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.name,
     symbol: '≠',
     description: '值不相等时触发',
     example: 'power != false',
     supportedTypes: ['int', 'float', 'double', 'string', 'bool', 'enum']
   },
   {
-    value: '>',
-    label: '大于',
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN.name,
     symbol: '>',
     description: '值大于指定值时触发',
     example: 'temperature > 30',
     supportedTypes: ['int', 'float', 'double', 'date']
   },
   {
-    value: '>=',
-    label: '大于等于',
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS.name,
     symbol: '≥',
     description: '值大于或等于指定值时触发',
     example: 'humidity >= 80',
     supportedTypes: ['int', 'float', 'double', 'date']
   },
   {
-    value: '<',
-    label: '小于',
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN.name,
     symbol: '<',
     description: '值小于指定值时触发',
     example: 'temperature < 10',
     supportedTypes: ['int', 'float', 'double', 'date']
   },
   {
-    value: '<=',
-    label: '小于等于',
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS.name,
     symbol: '≤',
     description: '值小于或等于指定值时触发',
     example: 'battery <= 20',
     supportedTypes: ['int', 'float', 'double', 'date']
   },
   {
-    value: 'in',
-    label: '包含于',
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.IN.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.IN.name,
     symbol: '∈',
     description: '值在指定列表中时触发',
     example: 'status in [1,2,3]',
     supportedTypes: ['int', 'float', 'string', 'enum']
   },
   {
-    value: 'between',
-    label: '介于',
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN.name,
+    symbol: '∉',
+    description: '值不在指定列表中时触发',
+    example: 'status not in [1,2,3]',
+    supportedTypes: ['int', 'float', 'string', 'enum']
+  },
+  {
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.name,
     symbol: '⊆',
     description: '值在指定范围内时触发',
     example: 'temperature between 20,30',
     supportedTypes: ['int', 'float', 'double', 'date']
   },
   {
-    value: 'contains',
-    label: '包含',
-    symbol: '',
-    description: '字符串包含指定内容时触发',
-    example: 'message contains "error"',
-    supportedTypes: ['string']
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN.name,
+    symbol: '',
+    description: '值不在指定范围内时触发',
+    example: 'temperature not between 20,30',
+    supportedTypes: ['int', 'float', 'double', 'date']
   },
   {
-    value: 'startsWith',
-    label: '开始于',
-    symbol: '',
-    description: '字符串以指定内容开始时触发',
-    example: 'deviceName startsWith "sensor"',
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.LIKE.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.LIKE.name,
+    symbol: '',
+    description: '字符串匹配指定模式时触发',
+    example: 'message like "%error%"',
     supportedTypes: ['string']
   },
   {
-    value: 'endsWith',
-    label: '结束于',
-    symbol: '',
-    description: '字符串以指定内容结束时触发',
-    example: 'fileName endsWith ".log"',
-    supportedTypes: ['string']
+    value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_NULL.value,
+    label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_NULL.name,
+    symbol: '≠∅',
+    description: '值非空时触发',
+    example: 'data not null',
+    supportedTypes: ['int', 'float', 'double', 'string', 'bool', 'enum', 'date']
   }
 ]
 

+ 32 - 13
src/views/iot/rule/scene/form/selectors/ProductDeviceSelector.vue

@@ -22,13 +22,14 @@
             >
               <div class="flex items-center justify-between w-full py-4px">
                 <div class="flex-1">
-                  <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ product.name }}</div>
-                  <div class="text-12px text-[var(--el-text-color-secondary)]">{{ product.productKey }}</div>
+                  <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px"
+                    >{{ product.name }}
+                  </div>
+                  <div class="text-12px text-[var(--el-text-color-secondary)]"
+                    >{{ product.productKey }}
+                  </div>
                 </div>
-                <!-- TODO @puhui999:是不是用字典 -->
-                <el-tag size="small" :type="product.status === 0 ? 'success' : 'danger'">
-                  {{ product.status === 0 ? '正常' : '禁用' }}
-                </el-tag>
+                <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" />
               </div>
             </el-option>
           </el-select>
@@ -70,8 +71,12 @@
             >
               <div class="flex items-center justify-between w-full py-4px">
                 <div class="flex-1">
-                  <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ device.deviceName }}</div>
-                  <div class="text-12px text-[var(--el-text-color-secondary)]">{{ device.nickname || '无备注' }}</div>
+                  <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px"
+                    >{{ device.deviceName }}
+                  </div>
+                  <div class="text-12px text-[var(--el-text-color-secondary)]"
+                    >{{ device.nickname || '无备注' }}
+                  </div>
                 </div>
                 <el-tag size="small" :type="getDeviceStatusTag(device.state)">
                   {{ getDeviceStatusText(device.state) }}
@@ -84,7 +89,10 @@
     </el-row>
 
     <!-- 选择结果展示 -->
-    <div v-if="localProductId && localDeviceId !== undefined" class="mt-16px p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]">
+    <div
+      v-if="localProductId && localDeviceId !== undefined"
+      class="mt-16px p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"
+    >
       <div class="flex items-center gap-6px mb-8px">
         <Icon icon="ep:check" class="text-[var(--el-color-success)] text-16px" />
         <span class="text-14px font-500 text-[var(--el-text-color-primary)]">已选择设备</span>
@@ -92,14 +100,22 @@
       <div class="flex flex-col gap-6px ml-22px">
         <div class="flex items-center gap-8px">
           <span class="text-12px text-[var(--el-text-color-secondary)] min-w-40px">产品:</span>
-          <span class="text-12px text-[var(--el-text-color-primary)] font-500">{{ selectedProduct?.name }}</span>
+          <span class="text-12px text-[var(--el-text-color-primary)] font-500">{{
+            selectedProduct?.name
+          }}</span>
           <el-tag size="small" type="primary">{{ selectedProduct?.productKey }}</el-tag>
         </div>
         <div class="flex items-center gap-8px">
           <span class="text-12px text-[var(--el-text-color-secondary)] min-w-40px">设备:</span>
-          <span v-if="deviceSelectionMode === 'all'" class="text-12px text-[var(--el-text-color-primary)] font-500">全部设备</span>
-          <span v-else class="text-12px text-[var(--el-text-color-primary)] font-500">{{ selectedDevice?.deviceName }}</span>
-          <el-tag v-if="deviceSelectionMode === 'all'" size="small" type="warning"> 全部 </el-tag>
+          <span
+            v-if="deviceSelectionMode === 'all'"
+            class="text-12px text-[var(--el-text-color-primary)] font-500"
+            >全部设备</span
+          >
+          <span v-else class="text-12px text-[var(--el-text-color-primary)] font-500">{{
+            selectedDevice?.deviceName
+          }}</span>
+          <el-tag v-if="deviceSelectionMode === 'all'" size="small" type="warning"> 全部</el-tag>
           <el-tag v-else size="small" :type="getDeviceStatusTag(selectedDevice?.state)">
             {{ getDeviceStatusText(selectedDevice?.state) }}
           </el-tag>
@@ -113,6 +129,7 @@
 import { useVModel } from '@vueuse/core'
 import { ProductApi } from '@/api/iot/product/product'
 import { DeviceApi } from '@/api/iot/device/device'
+import { DICT_TYPE } from '@/utils/dict'
 
 /** 产品设备选择器组件 */
 defineOptions({ name: 'ProductDeviceSelector' })
@@ -124,7 +141,9 @@ interface Props {
 
 interface Emits {
   (e: 'update:productId', value?: number): void
+
   (e: 'update:deviceId', value?: number): void
+
   (e: 'change', value: { productId?: number; deviceId?: number }): void
 }
 

+ 12 - 16
src/views/iot/rule/scene/form/selectors/ProductSelector.vue

@@ -17,16 +17,14 @@
     >
       <div class="flex items-center justify-between w-full py-4px">
         <div class="flex-1">
-          <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{
-            product.name
-          }}</div>
-          <div class="text-12px text-[var(--el-text-color-secondary)]">{{
-            product.productKey
-          }}</div>
+          <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px"
+            >{{ product.name }}
+          </div>
+          <div class="text-12px text-[var(--el-text-color-secondary)]"
+            >{{ product.productKey }}
+          </div>
         </div>
-        <el-tag size="small" :type="product.status === 0 ? 'success' : 'danger'">
-          {{ product.status === 0 ? '正常' : '禁用' }}
-        </el-tag>
+        <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" />
       </div>
     </el-option>
   </el-select>
@@ -34,21 +32,19 @@
 
 <script setup lang="ts">
 import { ProductApi } from '@/api/iot/product/product'
+import { DICT_TYPE } from '@/utils/dict'
 
 /** 产品选择器组件 */
 defineOptions({ name: 'ProductSelector' })
 
-interface Props {
+defineProps<{
   modelValue?: number
-}
+}>()
 
-interface Emits {
+const emit = defineEmits<{
   (e: 'update:modelValue', value?: number): void
   (e: 'change', value?: number): void
-}
-
-defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 // 状态
 const productLoading = ref(false)

+ 240 - 40
src/views/iot/rule/scene/form/selectors/PropertySelector.vue

@@ -1,14 +1,14 @@
 <!-- 属性选择器组件 -->
 <!-- TODO @yunai:可能要在 review 下 -->
 <template>
-  <div class="w-full">
+  <div class="flex items-center gap-8px">
     <el-select
       v-model="localValue"
       placeholder="请选择监控项"
       filterable
       clearable
       @change="handleChange"
-      class="w-full"
+      class="!w-150px"
       :loading="loading"
     >
       <el-option-group v-for="group in propertyGroups" :key="group.label" :label="group.label">
@@ -20,8 +20,12 @@
         >
           <div class="flex items-center justify-between w-full py-4px">
             <div class="flex-1">
-              <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ property.name }}</div>
-              <div class="text-12px text-[var(--el-text-color-secondary)]">{{ property.identifier }}</div>
+              <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">
+                {{ property.name }}
+              </div>
+              <div class="text-12px text-[var(--el-text-color-secondary)]">
+                {{ property.identifier }}
+              </div>
             </div>
             <div class="flex-shrink-0">
               <el-tag :type="getPropertyTypeTag(property.dataType)" size="small">
@@ -33,61 +37,114 @@
       </el-option-group>
     </el-select>
 
-    <!-- 属性详情 -->
-    <div v-if="selectedProperty" class="mt-16px p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]">
-      <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 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"
+        >
+          <div
+            class="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-300px max-w-400px"
+          >
+            <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>
+          </div>
         </div>
-      </div>
+      </Teleport>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
 import { useVModel } from '@vueuse/core'
-import { IotRuleSceneTriggerTypeEnum } from '@/api/iot/rule/scene/scene.types'
+import { InfoFilled } from '@element-plus/icons-vue'
+import { IotRuleSceneTriggerTypeEnum, IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
 import { ThingModelApi } from '@/api/iot/thingmodel'
-import { IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
-import type { IotThingModelTSLRespVO, PropertySelectorItem } from './types'
+import type { IotThingModelTSLRespVO, PropertySelectorItem } from '@/api/iot/rule/scene/scene.types'
 
 /** 属性选择器组件 */
 defineOptions({ name: 'PropertySelector' })
 
-interface Props {
+const props = defineProps<{
   modelValue?: string
   triggerType: number
   productId?: number
   deviceId?: number
-}
+}>()
 
-interface Emits {
+const emit = defineEmits<{
   (e: 'update:modelValue', value: string): void
   (e: 'change', value: { type: string; config: any }): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
+}>()
 
 const localValue = useVModel(props, 'modelValue', emit)
 
@@ -96,6 +153,25 @@ 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[] }[] = []
@@ -159,6 +235,67 @@ const getPropertyTypeTag = (dataType: string) => {
   return tagMap[dataType] || 'info'
 }
 
+// 弹出层控制方法
+const togglePropertyDetail = () => {
+  if (showPropertyDetail.value) {
+    hidePropertyDetail()
+  } else {
+    showPropertyDetailPopover()
+  }
+}
+
+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)
+  }
+
+  popoverStyle.value = {
+    position: 'fixed',
+    left: `${finalLeft}px`,
+    top: `${finalTop}px`,
+    zIndex: 9999
+  }
+}
+
 // 事件处理
 const handleChange = (value: string) => {
   const property = propertyList.value.find((p) => p.identifier === value)
@@ -168,6 +305,8 @@ const handleChange = (value: string) => {
       config: property
     })
   }
+  // 选择变化时隐藏详情弹出层
+  hidePropertyDetail()
 }
 
 // 获取物模型TSL数据
@@ -331,13 +470,74 @@ watch(
   () => props.triggerType,
   () => {
     localValue.value = ''
+    hidePropertyDetail()
   }
 )
+
+// 监听窗口大小变化,重新计算弹出层位置
+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-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: '';
+}
+
+.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: '';
+}
 </style>

+ 0 - 132
src/views/iot/rule/scene/form/selectors/types.ts

@@ -1,132 +0,0 @@
-// IoT物模型TSL数据类型定义
-
-// TODO @puhui999:看看这些里面,是不是一些已经有了哈?可以复用下~
-
-/** 物模型TSL响应数据结构 */
-export interface IotThingModelTSLRespVO {
-  productId: number
-  productKey: string
-  properties: ThingModelProperty[]
-  events: ThingModelEvent[]
-  services: ThingModelService[]
-}
-
-/** 物模型属性 */
-export interface ThingModelProperty {
-  identifier: string
-  name: string
-  accessMode: string
-  required?: boolean
-  dataType: string
-  description?: string
-  dataSpecs?: ThingModelDataSpecs
-  dataSpecsList?: ThingModelDataSpecs[]
-}
-
-/** 物模型事件 */
-export interface ThingModelEvent {
-  identifier: string
-  name: string
-  required?: boolean
-  type: string
-  description?: string
-  outputParams?: ThingModelParam[]
-  method?: string
-}
-
-/** 物模型服务 */
-export interface ThingModelService {
-  identifier: string
-  name: string
-  required?: boolean
-  callType: string
-  description?: string
-  inputParams?: ThingModelParam[]
-  outputParams?: ThingModelParam[]
-  method?: string
-}
-
-/** 物模型参数 */
-export interface ThingModelParam {
-  identifier: string
-  name: string
-  direction: string
-  paraOrder?: number
-  dataType: string
-  dataSpecs?: ThingModelDataSpecs
-  dataSpecsList?: ThingModelDataSpecs[]
-}
-
-/** 数值型数据规范 */
-export interface ThingModelNumericDataSpec {
-  dataType: 'int' | 'float' | 'double'
-  max: string
-  min: string
-  step: string
-  precise?: string
-  defaultValue?: string
-  unit?: string
-  unitName?: string
-}
-
-/** 布尔/枚举型数据规范 */
-export interface ThingModelBoolOrEnumDataSpecs {
-  dataType: 'bool' | 'enum'
-  name: string
-  value: number
-}
-
-/** 文本/时间型数据规范 */
-export interface ThingModelDateOrTextDataSpecs {
-  dataType: 'text' | 'date'
-  length?: number
-  defaultValue?: string
-}
-
-/** 数组型数据规范 */
-export interface ThingModelArrayDataSpecs {
-  dataType: 'array'
-  size: number
-  childDataType: string
-  dataSpecsList?: ThingModelDataSpecs[]
-}
-
-/** 结构体型数据规范 */
-export interface ThingModelStructDataSpecs {
-  dataType: 'struct'
-  identifier: string
-  name: string
-  accessMode: string
-  required?: boolean
-  childDataType: string
-  dataSpecs?: ThingModelDataSpecs
-  dataSpecsList?: ThingModelDataSpecs[]
-}
-
-/** 数据规范联合类型 */
-export type ThingModelDataSpecs =
-  | ThingModelNumericDataSpec
-  | ThingModelBoolOrEnumDataSpecs
-  | ThingModelDateOrTextDataSpecs
-  | ThingModelArrayDataSpecs
-  | ThingModelStructDataSpecs
-
-/** 属性选择器内部使用的统一数据结构 */
-export interface PropertySelectorItem {
-  identifier: string
-  name: string
-  description?: string
-  dataType: string
-  type: number // IoTThingModelTypeEnum
-  accessMode?: string
-  required?: boolean
-  unit?: string
-  range?: string
-  eventType?: string
-  callType?: string
-  inputParams?: ThingModelParam[]
-  outputParams?: ThingModelParam[]
-  property?: ThingModelProperty
-  event?: ThingModelEvent
-  service?: ThingModelService
-}

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

@@ -270,7 +270,7 @@
     </div>
 
     <!-- 表单对话框 -->
-    <RuleSceneForm v-model="formVisible" :rule-scene="currentRule" @success="getList" />
+    <RuleSceneForm v-model="formVisible" @success="getList" />
   </ContentWrap>
 </template>
 

+ 0 - 188
src/views/iot/rule/scene/utils/validation.ts

@@ -1,188 +0,0 @@
-// TODO @puhui999:貌似很多地方,都用不到啦?这个文件
-/**
- * IoT 场景联动表单验证工具函数
- */
-import { FormValidationRules, TriggerConfig, ActionConfig } from '@/api/iot/rule/scene/scene.types'
-import {
-  IotRuleSceneTriggerTypeEnum,
-  IotRuleSceneActionTypeEnum,
-  CommonStatusEnum
-} from '@/api/iot/rule/scene/scene.types'
-
-/** 基础表单验证规则 */
-export const getBaseValidationRules = (): FormValidationRules => ({
-  name: [
-    { required: true, message: '场景名称不能为空', trigger: 'blur' },
-    { type: 'string', min: 1, max: 50, message: '场景名称长度应在1-50个字符之间', trigger: 'blur' }
-  ],
-  status: [
-    { required: true, message: '场景状态不能为空', trigger: 'change' },
-    {
-      type: 'enum',
-      enum: [CommonStatusEnum.ENABLE, CommonStatusEnum.DISABLE],
-      message: '状态值必须为启用或禁用',
-      trigger: 'change'
-    }
-  ],
-  description: [
-    { type: 'string', max: 200, message: '场景描述不能超过200个字符', trigger: 'blur' }
-  ],
-  triggers: [
-    { required: true, message: '触发器数组不能为空', trigger: 'change' },
-    { type: 'array', min: 1, message: '至少需要一个触发器', trigger: 'change' }
-  ],
-  actions: [
-    { required: true, message: '执行器数组不能为空', trigger: 'change' },
-    { type: 'array', min: 1, message: '至少需要一个执行器', trigger: 'change' }
-  ]
-})
-
-/** 验证CRON表达式格式 */
-// TODO @puhui999:这个可以拿到 cron 组件里哇?
-export function validateCronExpression(cron: string): boolean {
-  if (!cron || cron.trim().length === 0) return false
-  // 基础的 CRON 表达式正则验证(支持 6 位和 7 位格式)
-  const cronRegex =
-    /^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\*|([0-9]|1[0-9]|2[0-3])|\*\/([0-9]|1[0-9]|2[0-3])) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\*\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\*|([1-9]|1[0-2])|\*\/([1-9]|1[0-2])) (\*|([0-6])|\*\/([0-6]))( (\*|([1-9][0-9]{3})|\*\/([1-9][0-9]{3})))?$/
-  return cronRegex.test(cron.trim())
-}
-
-/** 验证设备名称数组 */
-export function validateDeviceNames(deviceNames: string[]): boolean {
-  return (
-    Array.isArray(deviceNames) &&
-    deviceNames.length > 0 &&
-    deviceNames.every((name) => name && name.trim().length > 0)
-  )
-}
-
-/** 验证比较值格式 */
-export function validateCompareValue(operator: string, value: string): boolean {
-  if (!value || value.trim().length === 0) return false
-  const trimmedValue = value.trim()
-  // TODO @puhui999:这里要用下枚举哇?
-  switch (operator) {
-    case 'between':
-    case 'not between':
-      const betweenValues = trimmedValue.split(',')
-      return (
-        betweenValues.length === 2 &&
-        betweenValues.every((v) => v.trim().length > 0) &&
-        !isNaN(Number(betweenValues[0].trim())) &&
-        !isNaN(Number(betweenValues[1].trim()))
-      )
-    case 'in':
-    case 'not in':
-      const inValues = trimmedValue.split(',')
-      return inValues.length > 0 && inValues.every((v) => v.trim().length > 0)
-    case '>':
-    case '>=':
-    case '<':
-    case '<=':
-      return !isNaN(Number(trimmedValue))
-    case '=':
-    case '!=':
-    case 'like':
-    case 'not null':
-    // TODO @puhui999:这里要不加个 default 抛出异常?
-    default:
-      return true
-  }
-}
-
-// TODO @puhui999:貌似没用到?
-/** 验证触发器配置 */
-export function validateTriggerConfig(trigger: TriggerConfig): {
-  valid: boolean
-  message?: string
-} {
-  if (!trigger.type) {
-    return { valid: false, message: '触发类型不能为空' }
-  }
-  // 定时触发验证
-  if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
-    if (!trigger.cronExpression) {
-      return { valid: false, message: 'CRON表达式不能为空' }
-    }
-    if (!validateCronExpression(trigger.cronExpression)) {
-      return { valid: false, message: 'CRON表达式格式不正确' }
-    }
-    return { valid: true }
-  }
-  // 设备触发验证
-  if (!trigger.productKey) {
-    return { valid: false, message: '产品标识不能为空' }
-  }
-  if (!trigger.deviceNames || !validateDeviceNames(trigger.deviceNames)) {
-    return { valid: false, message: '设备名称不能为空' }
-  }
-  // 设备状态变更无需额外条件验证
-  if (trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
-    return { valid: true }
-  }
-  // 其他设备触发类型需要验证条件
-  if (!trigger.conditions || trigger.conditions.length === 0) {
-    return { valid: false, message: '触发条件不能为空' }
-  }
-  // 验证每个条件的参数
-  for (const condition of trigger.conditions) {
-    if (!condition.parameters || condition.parameters.length === 0) {
-      return { valid: false, message: '触发条件参数不能为空' }
-    }
-    for (const param of condition.parameters) {
-      if (!param.operator) {
-        return { valid: false, message: '操作符不能为空' }
-      }
-      if (!validateCompareValue(param.operator, param.value)) {
-        return { valid: false, message: `操作符 "${param.operator}" 对应的比较值格式不正确` }
-      }
-    }
-  }
-  return { valid: true }
-}
-
-// TODO @puhui999:貌似没用到?
-/** 验证执行器配置 */
-export function validateActionConfig(action: ActionConfig): { valid: boolean; message?: string } {
-  if (!action.type) {
-    return { valid: false, message: '执行类型不能为空' }
-  }
-  // 告警触发/恢复验证
-  if (
-    action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER ||
-    action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER
-  ) {
-    if (!action.alertConfigId) {
-      return { valid: false, message: '告警配置ID不能为空' }
-    }
-    return { valid: true }
-  }
-  // 设备控制验证
-  if (
-    action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
-    action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
-  ) {
-    if (!action.deviceControl) {
-      return { valid: false, message: '设备控制配置不能为空' }
-    }
-    const { deviceControl } = action
-    if (!deviceControl.productKey) {
-      return { valid: false, message: '产品标识不能为空' }
-    }
-    if (!deviceControl.deviceNames || !validateDeviceNames(deviceControl.deviceNames)) {
-      return { valid: false, message: '设备名称不能为空' }
-    }
-    if (!deviceControl.type) {
-      return { valid: false, message: '消息类型不能为空' }
-    }
-    if (!deviceControl.identifier) {
-      return { valid: false, message: '消息标识符不能为空' }
-    }
-    if (!deviceControl.params || Object.keys(deviceControl.params).length === 0) {
-      return { valid: false, message: '参数不能为空' }
-    }
-    return { valid: true }
-  }
-
-  return { valid: false, message: '未知的执行类型' }
-}

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

@@ -203,3 +203,100 @@ export const IoTOtaTaskRecordStatusEnum = {
     value: 50
   }
 } as const
+
+// ========== 场景联动规则相关常量 ==========
+
+/** IoT 场景联动触发器类型枚举 */
+export const IotRuleSceneTriggerTypeEnum = {
+  DEVICE_STATE_UPDATE: 1, // 设备上下线变更
+  DEVICE_PROPERTY_POST: 2, // 物模型属性上报
+  DEVICE_EVENT_POST: 3, // 设备事件上报
+  DEVICE_SERVICE_INVOKE: 4, // 设备服务调用
+  TIMER: 100 // 定时触发
+} as const
+
+/** 触发器类型选项配置 */
+export const getTriggerTypeOptions = () => [
+  {
+    value: IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
+    label: '设备状态变更'
+  },
+  {
+    value: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+    label: '设备属性上报'
+  },
+  {
+    value: IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
+    label: '设备事件上报'
+  },
+  {
+    value: IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
+    label: '设备服务调用'
+  },
+  {
+    value: IotRuleSceneTriggerTypeEnum.TIMER,
+    label: '定时触发'
+  }
+]
+
+/** 判断是否为设备触发器类型 */
+export const isDeviceTrigger = (type: number): boolean => {
+  const deviceTriggerTypes = [
+    IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
+    IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+    IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
+    IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+  ] as number[]
+  return deviceTriggerTypes.includes(type)
+}
+
+// ========== 场景联动规则执行器相关常量 ==========
+
+/** IoT 场景联动执行器类型枚举 */
+export const IotRuleSceneActionTypeEnum = {
+  DEVICE_PROPERTY_SET: 1, // 设备属性设置
+  DEVICE_SERVICE_INVOKE: 2, // 设备服务调用
+  ALERT_TRIGGER: 100, // 告警触发
+  ALERT_RECOVER: 101 // 告警恢复
+} as const
+
+/** IoT 设备消息类型枚举 */
+export const IotDeviceMessageTypeEnum = {
+  PROPERTY: 'property', // 属性
+  SERVICE: 'service', // 服务
+  EVENT: 'event' // 事件
+} as const
+
+/** IoT 场景联动触发条件参数操作符枚举 */
+export const IotRuleSceneTriggerConditionParameterOperatorEnum = {
+  EQUALS: { name: '等于', value: '=' }, // 等于
+  NOT_EQUALS: { name: '不等于', value: '!=' }, // 不等于
+  GREATER_THAN: { name: '大于', value: '>' }, // 大于
+  GREATER_THAN_OR_EQUALS: { name: '大于等于', value: '>=' }, // 大于等于
+  LESS_THAN: { name: '小于', value: '<' }, // 小于
+  LESS_THAN_OR_EQUALS: { name: '小于等于', value: '<=' }, // 小于等于
+  IN: { name: '在...之中', value: 'in' }, // 在...之中
+  NOT_IN: { name: '不在...之中', value: 'not in' }, // 不在...之中
+  BETWEEN: { name: '在...之间', value: 'between' }, // 在...之间
+  NOT_BETWEEN: { name: '不在...之间', value: 'not between' }, // 不在...之间
+  LIKE: { name: '字符串匹配', value: 'like' }, // 字符串匹配
+  NOT_NULL: { name: '非空', value: 'not null' } // 非空
+} as const
+
+/** IoT 场景联动触发条件类型枚举 */
+export const IotRuleSceneTriggerConditionTypeEnum = {
+  DEVICE_STATUS: 1, // 设备状态
+  DEVICE_PROPERTY: 2, // 设备属性
+  CURRENT_TIME: 3 // 当前时间
+} as const
+
+/** IoT 场景联动触发时间操作符枚举 */
+export const IotRuleSceneTriggerTimeOperatorEnum = {
+  BEFORE_TIME: { name: '在时间之前', value: 'before_time' }, // 在时间之前
+  AFTER_TIME: { name: '在时间之后', value: 'after_time' }, // 在时间之后
+  BETWEEN_TIME: { name: '在时间之间', value: 'between_time' }, // 在时间之间
+  AT_TIME: { name: '在指定时间', value: 'at_time' }, // 在指定时间
+  BEFORE_TODAY: { name: '在今日之前', value: 'before_today' }, // 在今日之前
+  AFTER_TODAY: { name: '在今日之后', value: 'after_today' }, // 在今日之后
+  TODAY: { name: '在今日之间', value: 'today' } // 在今日之间
+} as const