Jelajahi Sumber

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

芋道源码 9 bulan lalu
induk
melakukan
9ca86c292b

+ 8 - 0
src/api/iot/rule/scene/index.ts

@@ -23,6 +23,14 @@ export const RuleSceneApi = {
     return await request.put({ url: `/iot/rule-scene/update`, data })
   },
 
+  // 修改场景联动
+  updateRuleSceneStatus: async (id: number, status: number) => {
+    return await request.put({ url: `/iot/rule-scene/update-status`, data: {
+      id,
+      status
+    }})
+  },
+
   // 删除场景联动
   deleteRuleScene: async (id: number) => {
     return await request.delete({ url: `/iot/rule-scene/delete?id=` + id })

+ 2 - 0
src/api/iot/rule/scene/scene.types.ts

@@ -226,6 +226,7 @@ interface ActionFormData {
   type: number // 执行类型
   productId?: number // 产品编号
   deviceId?: number // 设备编号
+  identifier?: string // 物模型标识符(服务调用时使用)
   params?: Record<string, any> // 请求参数
   alertConfigId?: number // 告警配置编号
 }
@@ -277,6 +278,7 @@ interface ActionDO {
   type: number // 执行类型
   productId?: number // 产品编号
   deviceId?: number // 设备编号
+  identifier?: string // 物模型标识符(服务调用时使用)
   params?: Record<string, any> // 请求参数
   alertConfigId?: number // 告警配置编号
 }

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

@@ -12,9 +12,9 @@
       <!-- 基础信息配置 -->
       <BasicInfoSection v-model="formData" :rules="formRules" />
       <!-- 触发器配置 -->
-      <TriggerSection v-model:triggers="formData.triggers" @validate="handleTriggerValidate" />
+      <TriggerSection v-model:triggers="formData.triggers" />
       <!-- 执行器配置 -->
-      <ActionSection v-model:actions="formData.actions" @validate="handleActionValidate" />
+      <ActionSection v-model:actions="formData.actions" />
     </el-form>
     <template #footer>
       <div class="drawer-footer">
@@ -37,16 +37,14 @@ import BasicInfoSection from './sections/BasicInfoSection.vue'
 import TriggerSection from './sections/TriggerSection.vue'
 import ActionSection from './sections/ActionSection.vue'
 import { IotRuleSceneDO, RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
-import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
+import { RuleSceneApi } from '@/api/iot/rule/scene'
+import {
+  IotRuleSceneTriggerTypeEnum,
+  IotRuleSceneActionTypeEnum,
+  isDeviceTrigger
+} from '@/views/iot/utils/constants'
 import { ElMessage } from 'element-plus'
-import { generateUUID } from '@/utils'
-
-// 导入全局的 CommonStatusEnum
-// TODO @puhui999:这里直接复用全局的哈;
-const CommonStatusEnum = {
-  ENABLE: 0, // 开启
-  DISABLE: 1 // 关闭
-} as const
+import { CommonStatusEnum } from '@/utils/constants'
 
 /** IoT 场景联动规则表单 - 主表单组件 */
 defineOptions({ name: 'RuleSceneForm' })
@@ -55,6 +53,8 @@ defineOptions({ name: 'RuleSceneForm' })
 const props = defineProps<{
   /** 抽屉显示状态 */
   modelValue: boolean
+  /** 编辑的场景联动规则数据 */
+  ruleScene?: IotRuleSceneDO
 }>()
 
 /** 组件事件定义 */
@@ -87,90 +87,119 @@ const createDefaultFormData = (): RuleSceneFormData => {
   }
 }
 
-/**
- * 将表单数据转换为后端 DO 格式
- * 由于数据结构已对齐,转换变得非常简单
- */
-const convertFormToVO = (formData: RuleSceneFormData): IotRuleSceneDO => {
-  return {
-    id: formData.id,
-    name: formData.name,
-    description: formData.description,
-    status: Number(formData.status),
-    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,
-        productId: action.productId,
-        deviceId: action.deviceId,
-        params: action.params,
-        alertConfigId: action.alertConfigId
-      })) || []
+// 表单数据和状态
+const formRef = ref()
+const formData = ref<RuleSceneFormData>(createDefaultFormData())
+// 自定义校验器
+const validateTriggers = (_rule: any, value: any, callback: any) => {
+  if (!value || !Array.isArray(value) || value.length === 0) {
+    callback(new Error('至少需要一个触发器'))
+    return
+  }
+
+  for (let i = 0; i < value.length; i++) {
+    const trigger = value[i]
+
+    // 校验触发器类型
+    if (!trigger.type) {
+      callback(new Error(`触发器 ${i + 1}: 触发器类型不能为空`))
+      return
+    }
+
+    // 校验设备触发器
+    if (isDeviceTrigger(trigger.type)) {
+      if (!trigger.productId) {
+        callback(new Error(`触发器 ${i + 1}: 产品不能为空`))
+        return
+      }
+      if (!trigger.deviceId) {
+        callback(new Error(`触发器 ${i + 1}: 设备不能为空`))
+        return
+      }
+      if (!trigger.identifier) {
+        callback(new Error(`触发器 ${i + 1}: 物模型标识符不能为空`))
+        return
+      }
+      if (!trigger.operator) {
+        callback(new Error(`触发器 ${i + 1}: 操作符不能为空`))
+        return
+      }
+      if (trigger.value === undefined || trigger.value === null || trigger.value === '') {
+        callback(new Error(`触发器 ${i + 1}: 参数值不能为空`))
+        return
+      }
+    }
+
+    // 校验定时触发器
+    if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
+      if (!trigger.cronExpression) {
+        callback(new Error(`触发器 ${i + 1}: CRON表达式不能为空`))
+        return
+      }
+    }
   }
+
+  callback()
 }
 
-// TODO @puhui999:下面好像没用到?
-/**
- * 将后端 DO 数据转换为表单格式
- * 由于数据结构已对齐,转换变得非常简单
- */
-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,
-          identifier: undefined,
-          operator: undefined,
-          value: undefined,
-          cronExpression: undefined,
-          conditionGroups: []
+const validateActions = (_rule: any, value: any, callback: any) => {
+  if (!value || !Array.isArray(value) || value.length === 0) {
+    callback(new Error('至少需要一个执行器'))
+    return
+  }
+
+  for (let i = 0; i < value.length; i++) {
+    const action = value[i]
+
+    // 校验执行器类型
+    if (!action.type) {
+      callback(new Error(`执行器 ${i + 1}: 执行器类型不能为空`))
+      return
+    }
+
+    // 校验设备控制执行器
+    if (
+      action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
+      action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
+    ) {
+      if (!action.productId) {
+        callback(new Error(`执行器 ${i + 1}: 产品不能为空`))
+        return
+      }
+      if (!action.deviceId) {
+        callback(new Error(`执行器 ${i + 1}: 设备不能为空`))
+        return
+      }
+
+      // 服务调用需要验证服务标识符
+      if (action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE) {
+        if (!action.identifier) {
+          callback(new Error(`执行器 ${i + 1}: 服务不能为空`))
+          return
         }
-      ]
+      }
 
-  return {
-    id: apiData.id,
-    name: apiData.name,
-    description: apiData.description,
-    status: Number(apiData.status),
-    triggers,
-    actions:
-      apiData.actions?.map((action: any) => ({
-        type: Number(action.type),
-        productId: action.productId,
-        deviceId: action.deviceId,
-        params: action.params || {},
-        alertConfigId: action.alertConfigId,
-        // 为每个执行器添加唯一标识符,解决组件索引重用问题
-        key: generateUUID()
-      })) || []
+      if (!action.params || Object.keys(action.params).length === 0) {
+        callback(new Error(`执行器 ${i + 1}: 参数配置不能为空`))
+        return
+      }
+    }
+
+    // 校验告警执行器
+    if (
+      action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER ||
+      action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER
+    ) {
+      if (!action.alertConfigId) {
+        callback(new Error(`执行器 ${i + 1}: 告警配置不能为空`))
+        return
+      }
+    }
   }
+
+  callback()
 }
 
-// 表单数据和状态
-const formRef = ref()
-const formData = ref<RuleSceneFormData>(createDefaultFormData())
 const formRules = reactive({
   name: [
     { required: true, message: '场景名称不能为空', trigger: 'blur' },
@@ -188,77 +217,41 @@ const formRules = reactive({
   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' }
-  ]
+  triggers: [{ required: true, validator: validateTriggers, trigger: 'change' }],
+  actions: [{ required: true, validator: validateActions, trigger: 'change' }]
 })
 
 const submitLoading = ref(false)
 
-// 验证状态
-const triggerValidation = ref({ valid: true, message: '' })
-const actionValidation = ref({ valid: true, message: '' })
-
 // 计算属性
 const isEdit = ref(false)
 const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' : '新增场景联动规则'))
 
-// TODO @puhui999:方法的注释风格统一;
-// 事件处理
-const handleTriggerValidate = (result: { valid: boolean; message: string }) => {
-  triggerValidation.value = result
-}
-
-const handleActionValidate = (result: { valid: boolean; message: string }) => {
-  actionValidation.value = result
-}
-
-// TODO @puhui999:API 调用
+/** 提交表单 */
 const handleSubmit = async () => {
   // 校验表单
   if (!formRef.value) return
   const valid = await formRef.value.validate()
   if (!valid) return
-  // 验证触发器和执行器
-  if (!triggerValidation.value.valid) {
-    ElMessage.error(triggerValidation.value.message)
-    return
-  }
-  if (!actionValidation.value.valid) {
-    ElMessage.error(actionValidation.value.message)
-    return
-  }
 
   // 提交请求
   submitLoading.value = true
   try {
-    console.log(formData.value)
-    // 转换数据格式
-    const apiData = convertFormToVO(formData.value)
-    if (true) {
-      console.log('转换后', apiData)
-      return
-    }
+    // 数据结构已对齐,直接使用表单数据
+    console.log('提交数据:', formData.value)
+
     // 调用API保存数据
     if (isEdit.value) {
       // 更新场景联动规则
-      // await RuleSceneApi.updateRuleScene(apiData)
-      console.log('更新数据:', apiData)
+      await RuleSceneApi.updateRuleScene(formData.value)
+      ElMessage.success('更新成功')
     } else {
       // 创建场景联动规则
-      // await RuleSceneApi.createRuleScene(apiData)
-      console.log('创建数据:', apiData)
+      await RuleSceneApi.createRuleScene(formData.value)
+      ElMessage.success('创建成功')
     }
 
-    // 模拟API调用
-    await new Promise((resolve) => setTimeout(resolve, 1000))
-
-    ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
+    // 关闭抽屉并触发成功事件
     drawerVisible.value = false
     emit('success')
   } catch (error) {
@@ -275,28 +268,55 @@ const handleClose = () => {
 
 /** 初始化表单数据 */
 const initFormData = () => {
-  // TODO @puhui999: 编辑的情况后面实现
-  formData.value = createDefaultFormData()
+  if (props.ruleScene) {
+    // 编辑模式:数据结构已对齐,直接使用后端数据
+    isEdit.value = true
+    formData.value = {
+      ...props.ruleScene,
+      // 确保触发器数组不为空
+      triggers: props.ruleScene.triggers?.length
+        ? props.ruleScene.triggers
+        : [
+            {
+              type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+              productId: undefined,
+              deviceId: undefined,
+              identifier: undefined,
+              operator: undefined,
+              value: undefined,
+              cronExpression: undefined,
+              conditionGroups: []
+            }
+          ],
+      // 确保执行器数组不为空
+      actions: props.ruleScene.actions || []
+    }
+  } else {
+    // 新增模式:使用默认数据
+    isEdit.value = false
+    formData.value = createDefaultFormData()
+  }
 }
 
 // 监听抽屉显示
 watch(drawerVisible, (visible) => {
   if (visible) {
     initFormData()
-    // TODO @puhui999: 重置表单的情况
-    // nextTick(() => {
-    //   formRef.value?.clearValidate()
-    // })
+    // 重置表单验证状态
+    nextTick(() => {
+      formRef.value?.clearValidate()
+    })
   }
 })
 
-// 监听 props 变化
-// watch(
-//   () => props.ruleScene,
-//   () => {
-//     if (drawerVisible.value) {
-//       initFormData()
-//     }
-//   }
-// )
+// 监听编辑数据变化
+watch(
+  () => props.ruleScene,
+  () => {
+    if (drawerVisible.value) {
+      initFormData()
+    }
+  },
+  { deep: true }
+)
 </script>

+ 152 - 99
src/views/iot/rule/scene/form/configs/AlertConfig.vue

@@ -1,70 +1,155 @@
 <!-- 告警配置组件 -->
 <template>
   <div class="w-full">
-    <!-- TODO @puhui999:触发告警时,不用选择配置哈; -->
-    <el-form-item label="告警配置" required>
-      <el-select
-        v-model="localValue"
-        placeholder="请选择告警配置"
-        filterable
-        clearable
-        @change="handleChange"
-        class="w-full"
-        :loading="loading"
-      >
-        <el-option
-          v-for="config in alertConfigs"
-          :key="config.id"
-          :label="config.name"
-          :value="config.id"
+    <!-- 告警配置选择区域 -->
+    <div
+      class="border border-[var(--el-border-color-light)] rounded-6px p-16px bg-[var(--el-fill-color-blank)]"
+    >
+      <div class="flex items-center gap-8px mb-12px">
+        <Icon icon="ep:bell" class="text-[var(--el-color-warning)] text-16px" />
+        <span class="text-14px font-600 text-[var(--el-text-color-primary)]">告警配置选择</span>
+        <el-tag size="small" type="warning">必选</el-tag>
+      </div>
+
+      <el-form-item label="告警配置" required>
+        <el-select
+          v-model="localValue"
+          placeholder="请选择告警配置"
+          filterable
+          clearable
+          @change="handleChange"
+          class="w-full"
+          :loading="loading"
         >
-          <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">{{ config.name }}</div>
-              <div class="text-12px text-[var(--el-text-color-secondary)]">{{ config.description }}</div>
+          <template #empty>
+            <div class="text-center py-20px">
+              <Icon
+                icon="ep:warning"
+                class="text-24px text-[var(--el-text-color-placeholder)] mb-8px"
+              />
+              <p class="text-12px text-[var(--el-text-color-secondary)]">暂无可用的告警配置</p>
             </div>
-            <el-tag :type="config.enabled ? 'success' : 'danger'" size="small">
-              {{ config.enabled ? '启用' : '禁用' }}
-            </el-tag>
-          </div>
-        </el-option>
-      </el-select>
-    </el-form-item>
+          </template>
+          <el-option
+            v-for="config in alertConfigs"
+            :key="config.id"
+            :label="config.name"
+            :value="config.id"
+            :disabled="!config.enabled"
+          >
+            <div class="flex items-center justify-between w-full py-6px">
+              <div class="flex items-center gap-12px flex-1">
+                <Icon
+                  :icon="config.enabled ? 'ep:circle-check' : 'ep:circle-close'"
+                  :class="
+                    config.enabled
+                      ? 'text-[var(--el-color-success)]'
+                      : 'text-[var(--el-color-danger)]'
+                  "
+                  class="text-16px flex-shrink-0"
+                />
+                <div class="flex-1">
+                  <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{
+                    config.name
+                  }}</div>
+                  <div class="text-12px text-[var(--el-text-color-secondary)] line-clamp-1">{{
+                    config.description
+                  }}</div>
+                </div>
+              </div>
+              <div class="flex items-center gap-8px">
+                <el-tag :type="getNotifyTypeTag(config.notifyType)" size="small">
+                  {{ getNotifyTypeName(config.notifyType) }}
+                </el-tag>
+                <el-tag :type="config.enabled ? 'success' : 'danger'" size="small">
+                  {{ config.enabled ? '启用' : '禁用' }}
+                </el-tag>
+              </div>
+            </div>
+          </el-option>
+        </el-select>
+      </el-form-item>
+    </div>
 
     <!-- 告警配置详情 -->
-    <div v-if="selectedConfig" class="mt-16px 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:bell" class="text-[var(--el-color-warning)] text-16px" />
-        <span class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ selectedConfig.name }}</span>
-        <el-tag :type="selectedConfig.enabled ? 'success' : 'danger'" size="small">
-          {{ selectedConfig.enabled ? '启用' : '禁用' }}
-        </el-tag>
+    <div
+      v-if="selectedConfig"
+      class="mt-16px border border-[var(--el-border-color-light)] rounded-6px p-16px bg-gradient-to-r from-orange-50 to-yellow-50"
+    >
+      <div class="flex items-center gap-8px mb-16px">
+        <Icon icon="ep:info-filled" class="text-[var(--el-color-warning)] text-18px" />
+        <span class="text-16px font-600 text-[var(--el-text-color-primary)]">配置详情</span>
       </div>
-      <div class="space-y-8px">
-        <div class="flex items-start gap-8px">
-          <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">描述:</span>
-          <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedConfig.description }}</span>
-        </div>
-        <div class="flex items-start gap-8px">
-          <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">通知方式:</span>
-          <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ getNotifyTypeName(selectedConfig.notifyType) }}</span>
+
+      <div class="grid grid-cols-1 md:grid-cols-2 gap-16px">
+        <!-- 基本信息 -->
+        <div class="space-y-12px">
+          <div class="flex items-center gap-8px">
+            <Icon icon="ep:document" class="text-[var(--el-color-primary)] text-14px" />
+            <span class="text-14px font-500 text-[var(--el-text-color-primary)]">基本信息</span>
+          </div>
+          <div class="pl-22px space-y-8px">
+            <div class="flex items-start gap-8px">
+              <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">名称:</span>
+              <span class="text-12px text-[var(--el-text-color-primary)] flex-1 font-500">{{
+                selectedConfig.name
+              }}</span>
+            </div>
+            <div class="flex items-start gap-8px">
+              <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">描述:</span>
+              <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{
+                selectedConfig.description
+              }}</span>
+            </div>
+            <div class="flex items-start gap-8px">
+              <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">状态:</span>
+              <el-tag :type="selectedConfig.enabled ? 'success' : 'danger'" size="small">
+                {{ selectedConfig.enabled ? '启用' : '禁用' }}
+              </el-tag>
+            </div>
+          </div>
         </div>
-        <div v-if="selectedConfig.receivers" class="flex items-start gap-8px">
-          <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">接收人:</span>
-          <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedConfig.receivers.join(', ') }}</span>
+
+        <!-- 通知配置 -->
+        <div class="space-y-12px">
+          <div class="flex items-center gap-8px">
+            <Icon icon="ep:message" class="text-[var(--el-color-success)] text-14px" />
+            <span class="text-14px font-500 text-[var(--el-text-color-primary)]">通知配置</span>
+          </div>
+          <div class="pl-22px space-y-8px">
+            <div class="flex items-start gap-8px">
+              <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">方式:</span>
+              <el-tag :type="getNotifyTypeTag(selectedConfig.notifyType)" size="small">
+                {{ getNotifyTypeName(selectedConfig.notifyType) }}
+              </el-tag>
+            </div>
+            <div
+              v-if="selectedConfig.receivers && selectedConfig.receivers.length > 0"
+              class="flex items-start gap-8px"
+            >
+              <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px"
+                >接收人:</span
+              >
+              <div class="flex-1">
+                <div class="flex flex-wrap gap-4px">
+                  <el-tag
+                    v-for="receiver in selectedConfig.receivers.slice(0, 3)"
+                    :key="receiver"
+                    size="small"
+                    type="info"
+                  >
+                    {{ receiver }}
+                  </el-tag>
+                  <el-tag v-if="selectedConfig.receivers.length > 3" size="small" type="info">
+                    +{{ selectedConfig.receivers.length - 3 }}
+                  </el-tag>
+                </div>
+              </div>
+            </div>
+          </div>
         </div>
       </div>
     </div>
-
-    <!-- 验证结果 -->
-    <div v-if="validationMessage" class="mt-16px">
-      <el-alert
-        :title="validationMessage"
-        :type="isValid ? 'success' : 'error'"
-        :closable="false"
-        show-icon
-      />
-    </div>
   </div>
 </template>
 
@@ -80,7 +165,6 @@ interface Props {
 
 interface Emits {
   (e: 'update:modelValue', value?: number): void
-  (e: 'validate', result: { valid: boolean; message: string }): void
 }
 
 const props = defineProps<Props>()
@@ -91,8 +175,6 @@ const localValue = useVModel(props, 'modelValue', emit)
 // 状态
 const loading = ref(false)
 const alertConfigs = ref<any[]>([])
-const validationMessage = ref('')
-const isValid = ref(true)
 
 // 计算属性
 const selectedConfig = computed(() => {
@@ -110,38 +192,20 @@ const getNotifyTypeName = (type: number) => {
   return typeMap[type] || '未知'
 }
 
-// 事件处理
-const handleChange = () => {
-  updateValidationResult()
-}
-
-const updateValidationResult = () => {
-  if (!localValue.value) {
-    isValid.value = false
-    validationMessage.value = '请选择告警配置'
-    emit('validate', { valid: false, message: validationMessage.value })
-    return
-  }
-
-  const config = selectedConfig.value
-  if (!config) {
-    isValid.value = false
-    validationMessage.value = '告警配置不存在'
-    emit('validate', { valid: false, message: validationMessage.value })
-    return
-  }
-
-  if (!config.enabled) {
-    isValid.value = false
-    validationMessage.value = '选择的告警配置已禁用'
-    emit('validate', { valid: false, message: validationMessage.value })
-    return
+const getNotifyTypeTag = (type: number) => {
+  const tagMap = {
+    1: 'primary', // 邮件
+    2: 'success', // 短信
+    3: 'warning', // 微信
+    4: 'info' // 钉钉
   }
+  return tagMap[type] || 'info'
+}
 
-  // 验证通过
-  isValid.value = true
-  validationMessage.value = '告警配置验证通过'
-  emit('validate', { valid: true, message: validationMessage.value })
+// 事件处理
+const handleChange = (value?: number) => {
+  // 可以在这里添加额外的处理逻辑
+  console.log('告警配置选择变化:', value)
 }
 
 // API 调用
@@ -184,20 +248,9 @@ const getAlertConfigs = async () => {
   }
 }
 
-// 监听值变化
-watch(
-  () => localValue.value,
-  () => {
-    updateValidationResult()
-  }
-)
-
 // 初始化
 onMounted(() => {
   getAlertConfigs()
-  if (localValue.value) {
-    updateValidationResult()
-  }
 })
 </script>
 

+ 8 - 2
src/views/iot/rule/scene/form/configs/ConditionGroupContainerConfig.vue

@@ -154,11 +154,17 @@ const addSubGroup = () => {
     container.value = []
   }
 
-  if (container.value.length >= maxSubGroups) {
+  // 检查是否达到最大子组数量限制
+  if (container.value?.length >= maxSubGroups) {
     return
   }
 
-  container.value.push([])
+  // 使用 nextTick 确保响应式更新完成后再添加新的子组
+  nextTick(() => {
+    if (container.value) {
+      container.value.push([])
+    }
+  })
 }
 
 const removeSubGroup = (index: number) => {

+ 862 - 74
src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue

@@ -2,60 +2,309 @@
 <!-- TODO @puhui999:貌似没生效~~~ -->
 <template>
   <div class="flex flex-col gap-16px">
-    <!-- 产品和设备选择 -->
-    <ProductDeviceSelector
-      v-model:product-id="action.productId"
-      v-model:device-id="action.deviceId"
-      @change="handleDeviceChange"
-    />
-
-    <!-- 控制参数配置 -->
-    <div v-if="action.productId && action.deviceId" class="space-y-16px">
-      <el-form-item label="控制参数" required>
-        <el-input
-          v-model="paramsJson"
-          type="textarea"
-          :rows="4"
-          placeholder="请输入JSON格式的控制参数"
-          @input="handleParamsChange"
+    <!-- 产品和设备选择 - 与触发器保持一致的分离式选择器 -->
+    <el-row :gutter="16">
+      <el-col :span="12">
+        <el-form-item label="产品" required>
+          <ProductSelector v-model="action.productId" @change="handleProductChange" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="设备" required>
+          <DeviceSelector
+            v-model="action.deviceId"
+            :product-id="action.productId"
+            @change="handleDeviceChange"
+          />
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <!-- 服务选择 - 服务调用类型时显示 -->
+    <div v-if="action.productId && isServiceInvokeAction" class="space-y-16px">
+      <el-form-item label="服务" required>
+        <ServiceSelector
+          v-model="action.identifier"
+          :product-id="action.productId"
+          @change="handleServiceChange"
         />
       </el-form-item>
 
-      <!-- 参数示例 -->
-      <div class="mt-12px">
-        <el-alert title="参数格式示例" type="info" :closable="false" show-icon>
-          <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>
-              <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>
+      <!-- 服务参数配置 -->
+      <div v-if="action.identifier" class="space-y-16px">
+        <el-form-item label="服务参数" required>
+          <div class="w-full space-y-8px">
+            <!-- JSON 输入框 -->
+            <div class="relative">
+              <el-input
+                v-model="paramsJson"
+                type="textarea"
+                :rows="6"
+                placeholder="请输入JSON格式的服务参数"
+                @input="handleParamsChange"
+                :class="{ 'is-error': jsonError }"
+              />
+              <!-- 查看详细示例按钮 -->
+              <div class="absolute top-8px right-8px">
+                <el-button
+                  ref="exampleTriggerRef"
+                  type="info"
+                  :icon="InfoFilled"
+                  circle
+                  size="small"
+                  @click="toggleExampleDetail"
+                  title="查看详细示例"
+                />
+              </div>
             </div>
-          </template>
-        </el-alert>
+
+            <!-- 验证状态和错误提示 -->
+            <div class="flex items-center justify-between">
+              <div class="flex items-center gap-8px">
+                <Icon
+                  :icon="jsonError ? 'ep:warning' : 'ep:circle-check'"
+                  :class="
+                    jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'
+                  "
+                  class="text-14px"
+                />
+                <span
+                  :class="
+                    jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'
+                  "
+                  class="text-12px"
+                >
+                  {{ jsonError || 'JSON格式正确' }}
+                </span>
+              </div>
+
+              <!-- 快速填充按钮 -->
+              <div
+                v-if="selectedService?.inputParams?.length > 0"
+                class="flex items-center gap-8px"
+              >
+                <span class="text-12px text-[var(--el-text-color-secondary)]">快速填充:</span>
+                <el-button size="small" type="primary" plain @click="fillServiceExampleJson">
+                  示例数据
+                </el-button>
+                <el-button size="small" type="default" plain @click="clearParams"> 清空 </el-button>
+              </div>
+            </div>
+          </div>
+        </el-form-item>
       </div>
     </div>
 
-    <!-- 验证结果 -->
-    <div v-if="validationMessage" class="mt-16px">
-      <el-alert
-        :title="validationMessage"
-        :type="isValid ? 'success' : 'error'"
-        :closable="false"
-        show-icon
-      />
+    <!-- 控制参数配置 - 属性设置类型时显示 -->
+    <div v-if="action.productId && isPropertySetAction" class="space-y-16px">
+      <!-- 参数配置 -->
+      <el-form-item label="参数" required>
+        <div class="w-full space-y-8px">
+          <!-- JSON 输入框 -->
+          <div class="relative">
+            <el-input
+              v-model="paramsJson"
+              type="textarea"
+              :rows="6"
+              placeholder="请输入JSON格式的控制参数"
+              @input="handleParamsChange"
+              :class="{ 'is-error': jsonError }"
+            />
+            <!-- 查看详细示例按钮 -->
+            <div class="absolute top-8px right-8px">
+              <el-button
+                ref="exampleTriggerRef"
+                type="info"
+                :icon="InfoFilled"
+                circle
+                size="small"
+                @click="toggleExampleDetail"
+                title="查看详细示例"
+              />
+            </div>
+          </div>
+
+          <!-- 验证状态和错误提示 -->
+          <div class="flex items-center justify-between">
+            <div class="flex items-center gap-8px">
+              <Icon
+                :icon="jsonError ? 'ep:warning' : 'ep:circle-check'"
+                :class="
+                  jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'
+                "
+                class="text-14px"
+              />
+              <span
+                :class="
+                  jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'
+                "
+                class="text-12px"
+              >
+                {{ jsonError || 'JSON格式正确' }}
+              </span>
+            </div>
+
+            <!-- 快速填充按钮 -->
+            <div v-if="thingModelProperties.length > 0" class="flex items-center gap-8px">
+              <span class="text-12px text-[var(--el-text-color-secondary)]">快速填充:</span>
+              <el-button size="small" type="primary" plain @click="fillExampleJson">
+                示例数据
+              </el-button>
+              <el-button size="small" type="default" plain @click="clearParams"> 清空 </el-button>
+            </div>
+          </div>
+        </div>
+      </el-form-item>
+
+      <!-- 详细示例弹出层 -->
+      <Teleport to="body">
+        <div
+          v-if="showExampleDetail"
+          ref="exampleDetailRef"
+          class="example-detail-popover"
+          :style="examplePopoverStyle"
+        >
+          <div
+            class="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-400px max-w-500px"
+          >
+            <div class="flex items-center gap-8px mb-16px">
+              <Icon icon="ep:document" class="text-[var(--el-color-info)] text-18px" />
+              <span class="text-16px font-600 text-[var(--el-text-color-primary)]">
+                参数配置详细示例
+              </span>
+            </div>
+
+            <div class="space-y-16px">
+              <!-- 服务参数示例 - 服务调用时显示 -->
+              <div v-if="isServiceInvokeAction && selectedService?.inputParams?.length > 0">
+                <div class="flex items-center gap-8px mb-8px">
+                  <Icon icon="ep:service" class="text-[var(--el-color-success)] text-14px" />
+                  <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
+                    当前服务输入参数
+                  </span>
+                </div>
+                <div class="ml-22px space-y-8px">
+                  <div
+                    v-for="param in selectedService.inputParams.slice(0, 4)"
+                    :key="param.identifier"
+                    class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
+                  >
+                    <div class="flex-1">
+                      <div class="text-12px font-500 text-[var(--el-text-color-primary)]">
+                        {{ param.name }}
+                      </div>
+                      <div class="text-11px text-[var(--el-text-color-secondary)]">
+                        {{ param.identifier }}
+                      </div>
+                    </div>
+                    <div class="flex items-center gap-8px">
+                      <el-tag :type="getPropertyTypeTag(param.dataType)" size="small">
+                        {{ getPropertyTypeName(param.dataType) }}
+                      </el-tag>
+                      <span class="text-11px text-[var(--el-text-color-secondary)]">
+                        {{ getExampleValueForParam(param) }}
+                      </span>
+                    </div>
+                  </div>
+                </div>
+
+                <div class="mt-12px ml-22px">
+                  <div class="text-12px text-[var(--el-text-color-secondary)] mb-6px">
+                    完整JSON格式:
+                  </div>
+                  <pre
+                    class="p-12px bg-[var(--el-fill-color-light)] rounded-4px text-11px text-[var(--el-text-color-primary)] overflow-x-auto border-l-3px border-[var(--el-color-success)]"
+                  ><code>{{ generateServiceExampleJson() }}</code></pre>
+                </div>
+              </div>
+
+              <!-- 物模型属性示例 - 属性设置时显示 -->
+              <div v-if="isPropertySetAction && thingModelProperties.length > 0">
+                <div class="flex items-center gap-8px mb-8px">
+                  <Icon icon="ep:edit" class="text-[var(--el-color-primary)] text-14px" />
+                  <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
+                    当前物模型属性
+                  </span>
+                </div>
+                <div class="ml-22px space-y-8px">
+                  <div
+                    v-for="property in thingModelProperties.slice(0, 4)"
+                    :key="property.identifier"
+                    class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
+                  >
+                    <div class="flex-1">
+                      <div class="text-12px font-500 text-[var(--el-text-color-primary)]">
+                        {{ property.name }}
+                      </div>
+                      <div class="text-11px text-[var(--el-text-color-secondary)]">
+                        {{ property.identifier }}
+                      </div>
+                    </div>
+                    <div class="flex items-center gap-8px">
+                      <el-tag :type="getPropertyTypeTag(property.dataType)" size="small">
+                        {{ getPropertyTypeName(property.dataType) }}
+                      </el-tag>
+                      <span class="text-11px text-[var(--el-text-color-secondary)]">
+                        {{ getExampleValue(property) }}
+                      </span>
+                    </div>
+                  </div>
+                </div>
+
+                <div class="mt-12px ml-22px">
+                  <div class="text-12px text-[var(--el-text-color-secondary)] mb-6px">
+                    完整JSON格式:
+                  </div>
+                  <pre
+                    class="p-12px bg-[var(--el-fill-color-light)] rounded-4px text-11px text-[var(--el-text-color-primary)] overflow-x-auto border-l-3px border-[var(--el-color-primary)]"
+                  ><code>{{ generateExampleJson() }}</code></pre>
+                </div>
+              </div>
+
+              <!-- 通用示例 -->
+              <div>
+                <div class="flex items-center gap-8px mb-8px">
+                  <Icon icon="ep:service" class="text-[var(--el-color-success)] text-14px" />
+                  <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
+                    通用格式示例
+                  </span>
+                </div>
+                <div class="ml-22px space-y-8px">
+                  <div class="text-12px text-[var(--el-text-color-secondary)]">
+                    服务调用格式:
+                  </div>
+                  <pre
+                    class="p-12px bg-[var(--el-fill-color-light)] rounded-4px text-11px text-[var(--el-text-color-primary)] overflow-x-auto border-l-3px border-[var(--el-color-success)]"
+                  ><code>{
+  "method": "restart",
+  "params": {
+    "delay": 5,
+    "force": false
+  }
+}</code></pre>
+                </div>
+              </div>
+            </div>
+
+            <!-- 关闭按钮 -->
+            <div class="flex justify-end mt-16px">
+              <el-button size="small" @click="hideExampleDetail">关闭</el-button>
+            </div>
+          </div>
+        </div>
+      </Teleport>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
 import { useVModel } from '@vueuse/core'
-import ProductDeviceSelector from '../selectors/ProductDeviceSelector.vue'
-import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
+import { InfoFilled } from '@element-plus/icons-vue'
+import ProductSelector from '../selectors/ProductSelector.vue'
+import DeviceSelector from '../selectors/DeviceSelector.vue'
+import ServiceSelector from '../selectors/ServiceSelector.vue'
+import { ActionFormData, ThingModelService } from '@/api/iot/rule/scene/scene.types'
+import { IotRuleSceneActionTypeEnum } from '@/views/iot/utils/constants'
 
 /** 设备控制配置组件 */
 defineOptions({ name: 'DeviceControlConfig' })
@@ -66,81 +315,620 @@ const props = defineProps<{
 
 const emit = defineEmits<{
   (e: 'update:modelValue', value: ActionFormData): void
-  (e: 'validate', result: { valid: boolean; message: string }): void
 }>()
 
 const action = useVModel(props, 'modelValue', emit)
 
 // 状态
 const paramsJson = ref('')
-const validationMessage = ref('')
-const isValid = ref(true)
+const jsonError = ref('')
+const thingModelProperties = ref<any[]>([])
+const loadingThingModel = ref(false)
+const propertyValues = ref<Record<string, any>>({})
+
+// 服务调用相关状态
+const selectedService = ref<ThingModelService | null>(null)
+
+// 示例弹出层相关状态
+const showExampleDetail = ref(false)
+const exampleTriggerRef = ref()
+const exampleDetailRef = ref()
+const examplePopoverStyle = ref({})
+
+// 计算属性
+const isPropertySetAction = computed(() => {
+  return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
+})
+
+const isServiceInvokeAction = computed(() => {
+  return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
+})
 
 // 事件处理
-const handleDeviceChange = ({ productId, deviceId }: { productId?: number; deviceId?: number }) => {
-  action.value.productId = productId
-  action.value.deviceId = deviceId
-  updateValidationResult()
+const handleProductChange = (productId?: number) => {
+  console.log('🔄 handleProductChange called:', {
+    productId,
+    currentProductId: action.value.productId
+  })
+
+  // 当产品变化时,清空设备选择和参数配置
+  if (action.value.productId !== productId) {
+    action.value.deviceId = undefined
+    action.value.identifier = undefined // 清空服务标识符
+    action.value.params = {}
+    paramsJson.value = ''
+    jsonError.value = ''
+    propertyValues.value = {}
+    selectedService.value = null // 清空选中的服务
+
+    console.log('🧹 Cleared action data due to product change')
+  }
+
+  // 加载新产品的物模型属性
+  if (productId && isPropertySetAction.value) {
+    loadThingModelProperties(productId)
+  }
+}
+
+const handleDeviceChange = (deviceId?: number) => {
+  // 当设备变化时,清空参数配置
+  if (action.value.deviceId !== deviceId) {
+    action.value.params = {}
+    paramsJson.value = ''
+    jsonError.value = ''
+  }
+}
+
+const handleServiceChange = (serviceIdentifier?: string, service?: ThingModelService) => {
+  console.log('🔄 handleServiceChange called:', { serviceIdentifier, service: service?.name })
+
+  // 更新服务对象
+  selectedService.value = service || null
+
+  // 当服务变化时,清空参数配置并根据服务输入参数生成默认参数结构
+  action.value.params = {}
+  paramsJson.value = ''
+  jsonError.value = ''
+
+  // 如果选择了服务且有输入参数,生成默认参数结构
+  if (service && service.inputParams && service.inputParams.length > 0) {
+    const defaultParams = {}
+    service.inputParams.forEach((param) => {
+      defaultParams[param.identifier] = getDefaultValueForParam(param)
+    })
+    action.value.params = defaultParams
+    paramsJson.value = JSON.stringify(defaultParams, null, 2)
+
+    console.log('✅ Generated default params:', defaultParams)
+  }
+}
+
+// 快速填充示例数据
+const fillExampleJson = () => {
+  const exampleData = generateExampleJson()
+  paramsJson.value = exampleData
+  handleParamsChange()
+}
+
+// 快速填充服务示例数据
+const fillServiceExampleJson = () => {
+  if (selectedService.value && selectedService.value.inputParams) {
+    const exampleData = generateServiceExampleJson()
+    paramsJson.value = exampleData
+    handleParamsChange()
+  }
+}
+
+// 清空参数
+const clearParams = () => {
+  paramsJson.value = ''
+  action.value.params = {}
+  propertyValues.value = {}
+  jsonError.value = ''
+}
+
+// 更新属性值(保留但不在模板中使用)
+// const updatePropertyValue = (identifier: string, value: any) => {
+//   propertyValues.value[identifier] = value
+//   // 同步更新到 action.params
+//   action.value.params = { ...propertyValues.value }
+//   // 同步更新 JSON 显示
+//   paramsJson.value = JSON.stringify(action.value.params, null, 2)
+//   jsonError.value = ''
+// }
+
+// 加载物模型属性
+const loadThingModelProperties = async (productId: number) => {
+  if (!productId) {
+    thingModelProperties.value = []
+    return
+  }
+
+  try {
+    loadingThingModel.value = true
+    // TODO: 这里需要调用实际的物模型API
+    // const response = await ProductApi.getThingModel(productId)
+    // 暂时使用模拟数据
+    thingModelProperties.value = [
+      {
+        identifier: 'BatteryLevel',
+        name: '电池电量',
+        dataType: 'int',
+        description: '设备电池电量百分比'
+      },
+      {
+        identifier: 'WaterLeachState',
+        name: '漏水状态',
+        dataType: 'bool',
+        description: '设备漏水检测状态'
+      },
+      {
+        identifier: 'Temperature',
+        name: '温度',
+        dataType: 'float',
+        description: '环境温度值'
+      },
+      {
+        identifier: 'Humidity',
+        name: '湿度',
+        dataType: 'float',
+        description: '环境湿度值'
+      }
+    ]
+
+    // 初始化属性值
+    thingModelProperties.value.forEach((property) => {
+      if (!(property.identifier in propertyValues.value)) {
+        propertyValues.value[property.identifier] = ''
+      }
+    })
+  } catch (error) {
+    console.error('加载物模型失败:', error)
+    thingModelProperties.value = []
+  } finally {
+    loadingThingModel.value = false
+  }
+}
+
+// 从TSL加载服务信息
+const loadServiceFromTSL = async (productId: number, serviceIdentifier: string) => {
+  console.log('🔍 loadServiceFromTSL called:', { productId, serviceIdentifier })
+  try {
+    const { ThingModelApi } = await import('@/api/iot/thingmodel')
+    const tslData = await ThingModelApi.getThingModelTSLByProductId(productId)
+    console.log('📡 TSL data loaded:', tslData)
+
+    if (tslData?.services) {
+      const service = tslData.services.find((s: any) => s.identifier === serviceIdentifier)
+      console.log('🎯 Found service:', service)
+
+      if (service) {
+        // 设置服务对象
+        selectedService.value = service
+
+        console.log('✅ Service set:', {
+          serviceIdentifier,
+          selectedService: selectedService.value?.name
+        })
+
+        // 确保在下一个tick中更新,让ServiceSelector有时间处理
+        await nextTick()
+      } else {
+        console.warn('⚠️ Service not found in TSL')
+      }
+    } else {
+      console.warn('⚠️ No services in TSL data')
+    }
+  } catch (error) {
+    console.error('❌ 加载服务信息失败:', error)
+  }
 }
 
 const handleParamsChange = () => {
   try {
+    jsonError.value = '' // 清除之前的错误
+
     if (paramsJson.value.trim()) {
-      action.value.params = JSON.parse(paramsJson.value)
+      const parsed = JSON.parse(paramsJson.value)
+      action.value.params = parsed
+
+      // 同步更新到属性值
+      propertyValues.value = { ...parsed }
+
+      // 额外的参数验证
+      if (typeof parsed !== 'object' || parsed === null) {
+        jsonError.value = '参数必须是一个有效的JSON对象'
+        return
+      }
     } else {
       action.value.params = {}
+      propertyValues.value = {}
     }
-    updateValidationResult()
   } catch (error) {
-    isValid.value = false
-    validationMessage.value = 'JSON格式错误'
-    emit('validate', { valid: false, message: validationMessage.value })
+    jsonError.value = `JSON格式错误: ${error instanceof Error ? error.message : '未知错误'}`
+    console.error('JSON格式错误:', error)
   }
 }
 
-const updateValidationResult = () => {
-  // 基础验证
-  if (!action.value.productId || !action.value.deviceId) {
-    isValid.value = false
-    validationMessage.value = '请选择产品和设备'
-    emit('validate', { valid: false, message: validationMessage.value })
-    return
+// 工具函数 - 参考 PropertySelector 的设计
+const getPropertyTypeName = (dataType: string) => {
+  const typeMap = {
+    int: '整数',
+    float: '浮点数',
+    double: '双精度',
+    text: '字符串',
+    bool: '布尔值',
+    enum: '枚举',
+    date: '日期',
+    struct: '结构体',
+    array: '数组'
   }
+  return typeMap[dataType] || dataType
+}
 
-  if (!action.value.params || Object.keys(action.value.params).length === 0) {
-    isValid.value = false
-    validationMessage.value = '请配置控制参数'
-    emit('validate', { valid: false, message: validationMessage.value })
-    return
+// 根据参数类型获取默认值
+const getDefaultValueForParam = (param: any) => {
+  switch (param.dataType) {
+    case 'int':
+      return 0
+    case 'float':
+    case 'double':
+      return 0.0
+    case 'bool':
+      return false
+    case 'text':
+      return ''
+    case 'enum':
+      // 如果有枚举值,使用第一个
+      if (param.dataSpecs?.dataSpecsList && param.dataSpecs.dataSpecsList.length > 0) {
+        return param.dataSpecs.dataSpecsList[0].value
+      }
+      return ''
+    default:
+      return ''
+  }
+}
+
+const getPropertyTypeTag = (dataType: string) => {
+  const tagMap = {
+    int: 'primary',
+    float: 'success',
+    double: 'success',
+    text: 'info',
+    bool: 'warning',
+    enum: 'danger',
+    date: 'primary',
+    struct: 'info',
+    array: 'warning'
+  }
+  return tagMap[dataType] || 'info'
+}
+
+const getExampleValue = (property: any) => {
+  switch (property.dataType) {
+    case 'int':
+      return property.identifier === 'BatteryLevel' ? '85' : '25'
+    case 'float':
+    case 'double':
+      return property.identifier === 'Temperature' ? '25.5' : '60.0'
+    case 'bool':
+      return 'false'
+    case 'text':
+      return '"auto"'
+    case 'enum':
+      return '"option1"'
+    default:
+      return '""'
+  }
+}
+
+// 获取参数示例值
+const getExampleValueForParam = (param: any) => {
+  switch (param.dataType) {
+    case 'int':
+      return '0'
+    case 'float':
+    case 'double':
+      return '0.0'
+    case 'bool':
+      return 'false'
+    case 'text':
+      return '"text"'
+    case 'enum':
+      if (param.dataSpecs?.dataSpecsList && param.dataSpecs.dataSpecsList.length > 0) {
+        return `"${param.dataSpecs.dataSpecsList[0].name}"`
+      }
+      return '"option1"'
+    default:
+      return '""'
+  }
+}
+
+const generateExampleJson = () => {
+  if (thingModelProperties.value.length === 0) {
+    return JSON.stringify(
+      {
+        BatteryLevel: '',
+        WaterLeachState: ''
+      },
+      null,
+      2
+    )
+  }
+
+  const example = {}
+  thingModelProperties.value.forEach((property) => {
+    switch (property.dataType) {
+      case 'int':
+        example[property.identifier] = property.identifier === 'BatteryLevel' ? 85 : 25
+        break
+      case 'float':
+      case 'double':
+        example[property.identifier] = property.identifier === 'Temperature' ? 25.5 : 60.0
+        break
+      case 'bool':
+        example[property.identifier] = false
+        break
+      case 'text':
+        example[property.identifier] = 'auto'
+        break
+      default:
+        example[property.identifier] = ''
+    }
+  })
+
+  return JSON.stringify(example, null, 2)
+}
+
+// 生成服务示例JSON
+const generateServiceExampleJson = () => {
+  if (!selectedService.value || !selectedService.value.inputParams) {
+    return JSON.stringify({}, null, 2)
+  }
+
+  const example = {}
+  selectedService.value.inputParams.forEach((param) => {
+    example[param.identifier] = getDefaultValueForParam(param)
+  })
+
+  return JSON.stringify(example, null, 2)
+}
+
+// 示例弹出层控制方法 - 参考 PropertySelector 的设计
+const toggleExampleDetail = () => {
+  if (showExampleDetail.value) {
+    hideExampleDetail()
+  } else {
+    showExampleDetailPopover()
+  }
+}
+
+const showExampleDetailPopover = () => {
+  if (!exampleTriggerRef.value) return
+
+  showExampleDetail.value = true
+
+  nextTick(() => {
+    updateExamplePopoverPosition()
+  })
+}
+
+const hideExampleDetail = () => {
+  showExampleDetail.value = false
+}
+
+const updateExamplePopoverPosition = () => {
+  if (!exampleTriggerRef.value || !exampleDetailRef.value) return
+
+  const triggerEl = exampleTriggerRef.value.$el
+  const triggerRect = triggerEl.getBoundingClientRect()
+
+  // 计算弹出层位置
+  const left = triggerRect.left + triggerRect.width + 8
+  const top = triggerRect.top
+
+  // 检查是否超出视窗右边界
+  const popoverWidth = 500 // 最大宽度
+  const viewportWidth = window.innerWidth
+
+  let finalLeft = left
+  if (left + popoverWidth > viewportWidth - 16) {
+    // 如果超出右边界,显示在左侧
+    finalLeft = triggerRect.left - popoverWidth - 8
   }
 
-  // 验证通过
-  isValid.value = true
-  validationMessage.value = '设备控制配置验证通过'
-  emit('validate', { valid: true, message: validationMessage.value })
+  // 检查是否超出视窗下边界
+  let finalTop = top
+  const popoverHeight = exampleDetailRef.value.offsetHeight || 300
+  const viewportHeight = window.innerHeight
+
+  if (top + popoverHeight > viewportHeight - 16) {
+    finalTop = Math.max(16, viewportHeight - popoverHeight - 16)
+  }
+
+  examplePopoverStyle.value = {
+    position: 'fixed',
+    left: `${finalLeft}px`,
+    top: `${finalTop}px`,
+    zIndex: 9999
+  }
+}
+
+// 点击外部关闭弹出层
+const handleClickOutside = (event: MouseEvent) => {
+  if (
+    showExampleDetail.value &&
+    exampleDetailRef.value &&
+    exampleTriggerRef.value &&
+    !exampleDetailRef.value.contains(event.target as Node) &&
+    !exampleTriggerRef.value.$el.contains(event.target as Node)
+  ) {
+    hideExampleDetail()
+  }
+}
+
+// 监听窗口大小变化,重新计算弹出层位置
+const handleResize = () => {
+  if (showExampleDetail.value) {
+    updateExamplePopoverPosition()
+  }
 }
 
 // 初始化
 onMounted(() => {
-  if (action.value.params) {
-    paramsJson.value = JSON.stringify(action.value.params, null, 2)
+  if (action.value.params && Object.keys(action.value.params).length > 0) {
+    try {
+      paramsJson.value = JSON.stringify(action.value.params, null, 2)
+      propertyValues.value = { ...action.value.params }
+      jsonError.value = '' // 清除错误状态
+    } catch (error) {
+      console.error('初始化参数格式化失败:', error)
+      jsonError.value = '初始参数格式错误'
+    }
+  }
+
+  // 如果已经选择了产品且是属性设置类型,加载物模型
+  if (action.value.productId && isPropertySetAction.value) {
+    loadThingModelProperties(action.value.productId)
+  }
+
+  // 如果是服务调用类型且已有标识符,初始化服务选择
+  if (action.value.productId && isServiceInvokeAction.value && action.value.identifier) {
+    // 加载物模型TSL以获取服务信息
+    loadServiceFromTSL(action.value.productId, action.value.identifier)
   }
-  updateValidationResult()
+
+  // 添加事件监听器
+  document.addEventListener('click', handleClickOutside)
+  window.addEventListener('resize', handleResize)
+})
+
+// 组件卸载时清理事件监听器
+onUnmounted(() => {
+  document.removeEventListener('click', handleClickOutside)
+  window.removeEventListener('resize', handleResize)
 })
 
 // 监听参数变化
 watch(
   () => action.value.params,
   (newParams) => {
-    if (newParams && typeof newParams === 'object') {
-      paramsJson.value = JSON.stringify(newParams, null, 2)
+    if (newParams && typeof newParams === 'object' && Object.keys(newParams).length > 0) {
+      try {
+        const newJsonString = JSON.stringify(newParams, null, 2)
+        // 只有当JSON字符串真正改变时才更新,避免循环更新
+        if (newJsonString !== paramsJson.value) {
+          paramsJson.value = newJsonString
+          jsonError.value = ''
+        }
+      } catch (error) {
+        console.error('参数格式化失败:', error)
+        jsonError.value = '参数格式化失败'
+      }
+    } else {
+      // 参数为空时清空JSON显示
+      if (paramsJson.value !== '') {
+        paramsJson.value = ''
+        jsonError.value = ''
+      }
     }
   },
   { deep: true }
 )
+
+// 监听action.value变化,处理编辑模式的数据回显
+watch(
+  () => action.value,
+  async (newAction) => {
+    console.log('🔄 action.value changed:', {
+      type: newAction?.type,
+      productId: newAction?.productId,
+      identifier: newAction?.identifier,
+      isServiceInvokeAction: isServiceInvokeAction.value
+    })
+
+    if (newAction) {
+      // 处理服务调用的数据回显
+      if (isServiceInvokeAction.value && newAction.productId && newAction.identifier) {
+        // 异步加载服务信息以设置selectedService
+        await loadServiceFromTSL(newAction.productId, newAction.identifier)
+      } else if (isServiceInvokeAction.value) {
+        // 清空服务选择
+        selectedService.value = null
+      }
+
+      // 处理参数回显
+      if (newAction.params && Object.keys(newAction.params).length > 0) {
+        try {
+          const newJsonString = JSON.stringify(newAction.params, null, 2)
+          if (paramsJson.value !== newJsonString) {
+            paramsJson.value = newJsonString
+            propertyValues.value = { ...newAction.params }
+            jsonError.value = ''
+            console.log('✅ Params restored:', newAction.params)
+          }
+        } catch (error) {
+          console.error('❌ 参数格式化失败:', error)
+          jsonError.value = '参数格式化失败'
+        }
+      } else {
+        if (paramsJson.value !== '') {
+          paramsJson.value = ''
+          propertyValues.value = {}
+          jsonError.value = ''
+          console.log('🧹 Params cleared')
+        }
+      }
+    }
+  },
+  { deep: true, immediate: true }
+)
 </script>
 
 <style scoped>
+/* 参考 PropertySelector 的弹出层样式 */
+@keyframes fadeInScale {
+  from {
+    opacity: 0;
+    transform: scale(0.9) translateY(-4px);
+  }
+
+  to {
+    opacity: 1;
+    transform: scale(1) translateY(0);
+  }
+}
+
+.example-detail-popover {
+  animation: fadeInScale 0.2s ease-out;
+  transform-origin: top left;
+}
+
+/* 弹出层箭头效果 */
+.example-detail-popover::before {
+  position: absolute;
+  top: 20px;
+  left: -8px;
+  width: 0;
+  height: 0;
+  border-top: 8px solid transparent;
+  border-right: 8px solid var(--el-border-color);
+  border-bottom: 8px solid transparent;
+  content: '';
+}
+
+.example-detail-popover::after {
+  position: absolute;
+  top: 20px;
+  left: -7px;
+  width: 0;
+  height: 0;
+  border-top: 8px solid transparent;
+  border-right: 8px solid white;
+  border-bottom: 8px solid transparent;
+  content: '';
+}
+
 :deep(.example-content code) {
   font-family: 'Courier New', monospace;
   color: var(--el-color-primary);

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

@@ -6,7 +6,7 @@
       <MainConditionConfig
         v-model="trigger"
         :trigger-type="trigger.type"
-        @validate="handleMainConditionValidate"
+        @trigger-type-change="handleTriggerTypeChange"
       />
     </div>
 
@@ -16,7 +16,6 @@
       <ConditionGroupContainerConfig
         v-model="trigger.conditionGroups"
         :trigger-type="trigger.type"
-        @validate="handleConditionGroupValidate"
         @remove="removeConditionGroup"
       />
     </div>
@@ -42,18 +41,11 @@ const props = defineProps<{
 const emit = defineEmits<{
   (e: 'update:modelValue', value: TriggerFormData): void
   (e: 'validate', value: { valid: boolean; message: string }): void
+  (e: 'trigger-type-change', type: number): void
 }>()
 
 const trigger = useVModel(props, 'modelValue', emit)
 
-// 验证状态
-const mainConditionValidation = ref<{ valid: boolean; message: string }>({
-  valid: true,
-  message: ''
-})
-const validationMessage = ref('')
-const isValid = ref(true)
-
 // 初始化主条件
 const initMainCondition = () => {
   // TODO @puhui999: 等到编辑回显时联调
@@ -78,78 +70,12 @@ watch(
   { immediate: true }
 )
 
-const handleMainConditionValidate = (result: { valid: boolean; message: string }) => {
-  mainConditionValidation.value = result
-  updateValidationResult()
-}
-
-const addConditionGroup = () => {
-  if (!trigger.value.conditionGroups) {
-    trigger.value.conditionGroups = []
-  }
-  trigger.value.conditionGroups.push([])
-}
-
-// 事件处理
-const handleConditionGroupValidate = () => {
-  updateValidationResult()
+const handleTriggerTypeChange = (type: number) => {
+  trigger.value.type = type
+  emit('trigger-type-change', type)
 }
 
 const removeConditionGroup = () => {
   trigger.value.conditionGroups = undefined
 }
-
-const updateValidationResult = () => {
-  // 主条件验证
-  if (!mainConditionValidation.value.valid) {
-    isValid.value = false
-    validationMessage.value = mainConditionValidation.value.message
-    emit('validate', { valid: false, message: validationMessage.value })
-    return
-  }
-
-  // 设备状态变更不需要条件验证
-  if (trigger.value.type === TriggerTypeEnum.DEVICE_STATE_UPDATE) {
-    isValid.value = true
-    validationMessage.value = '设备触发配置验证通过'
-    emit('validate', { valid: true, message: validationMessage.value })
-    return
-  }
-
-  // 主条件验证
-  if (!trigger.value.value) {
-    isValid.value = false
-    validationMessage.value = '请配置主条件'
-    emit('validate', { valid: false, message: validationMessage.value })
-    return
-  }
-
-  // 主条件详细验证
-  if (!mainConditionValidation.value.valid) {
-    isValid.value = false
-    validationMessage.value = `主条件配置错误: ${mainConditionValidation.value.message}`
-    emit('validate', { valid: false, message: validationMessage.value })
-    return
-  }
-
-  isValid.value = true
-  validationMessage.value = '设备触发配置验证通过'
-  emit('validate', { valid: isValid.value, message: validationMessage.value })
-}
-
-// 监听触发器类型变化
-watch(
-  () => trigger.value.type,
-  () => {
-    updateValidationResult()
-  }
-)
-
-// 监听产品设备变化
-watch(
-  () => [trigger.value.productId, trigger.value.deviceId],
-  () => {
-    updateValidationResult()
-  }
-)
 </script>

+ 25 - 41
src/views/iot/rule/scene/form/configs/MainConditionConfig.vue

@@ -1,38 +1,33 @@
 <!-- 主条件配置组件 -->
 <template>
   <div class="flex flex-col gap-16px">
-    <!-- 条件配置提示 -->
-    <div
-      v-if="!modelValue"
-      class="p-16px border-2 border-dashed border-[var(--el-border-color)] rounded-8px text-center"
-    >
-      <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)]">
-          <p class="text-14px font-500 mb-4px">请配置主条件</p>
-          <p class="text-12px">主条件是触发器的核心条件,必须满足才能触发场景</p>
-        </div>
-        <el-button type="primary" @click="addMainCondition">
-          <Icon icon="ep:plus" />
-          添加主条件
-        </el-button>
-      </div>
-    </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 class="space-y-16px">
+      <!-- 主条件头部 - 与附加条件组保持一致的绿色风格 -->
+      <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"
+      >
+        <div class="flex items-center gap-12px">
+          <div class="flex items-center gap-8px text-16px font-600 text-green-700">
+            <div
+              class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
+            >
+              主
+            </div>
+            <span>主条件</span>
+          </div>
+          <el-tag size="small" type="success">必须满足</el-tag>
         </div>
       </div>
+
+      <!-- 主条件内容配置 -->
       <MainConditionInnerConfig
         :model-value="modelValue"
         @update:model-value="updateCondition"
         :trigger-type="triggerType"
         @validate="handleValidate"
+        @trigger-type-change="handleTriggerTypeChange"
       />
     </div>
   </div>
@@ -45,29 +40,18 @@ import { IotRuleSceneTriggerConditionTypeEnum } from '@/views/iot/utils/constant
 /** 主条件配置组件 */
 defineOptions({ name: 'MainConditionConfig' })
 
-defineProps<{
-  modelValue?: TriggerFormData
+const props = defineProps<{
+  modelValue: TriggerFormData
   triggerType: number
 }>()
 
 const emit = defineEmits<{
-  (e: 'update:modelValue', value?: TriggerFormData): void
+  (e: 'update:modelValue', value: TriggerFormData): void
   (e: 'validate', result: { valid: boolean; message: string }): void
+  (e: 'trigger-type-change', type: number): void
 }>()
 
 // 事件处理
-const addMainCondition = () => {
-  const newCondition: TriggerFormData = {
-    type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
-    productId: undefined,
-    deviceId: undefined,
-    identifier: '',
-    operator: '=',
-    value: ''
-  }
-  emit('update:modelValue', newCondition)
-}
-
 const updateCondition = (condition: TriggerFormData) => {
   emit('update:modelValue', condition)
 }
@@ -76,7 +60,7 @@ const handleValidate = (result: { valid: boolean; message: string }) => {
   emit('validate', result)
 }
 
-onMounted(() => {
-  addMainCondition()
-})
+const handleTriggerTypeChange = (type: number) => {
+  emit('trigger-type-change', type)
+}
 </script>

+ 69 - 18
src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue

@@ -1,5 +1,23 @@
 <template>
   <div class="space-y-16px">
+    <!-- 触发事件类型选择 -->
+    <el-form-item label="触发事件类型" required>
+      <el-select
+        :model-value="triggerType"
+        @update:model-value="handleTriggerTypeChange"
+        placeholder="请选择触发事件类型"
+        class="w-full"
+        style="width: 100%"
+      >
+        <el-option
+          v-for="option in triggerTypeOptions"
+          :key="option.value"
+          :label="option.label"
+          :value="option.value"
+        />
+      </el-select>
+    </el-form-item>
+
     <!-- 设备属性条件配置 -->
     <div v-if="isDevicePropertyTrigger" class="space-y-16px">
       <!-- 产品设备选择 -->
@@ -41,8 +59,8 @@
           </el-form-item>
         </el-col>
 
-        <!-- 操作符选择 -->
-        <el-col :span="6">
+        <!-- 操作符选择 - 服务调用不需要操作符 -->
+        <el-col v-if="triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE" :span="6">
           <el-form-item label="操作符" required>
             <OperatorSelector
               :model-value="condition.operator"
@@ -54,11 +72,28 @@
         </el-col>
 
         <!-- 值输入 -->
-        <el-col :span="12">
-          <el-form-item label="比较值" required>
+        <el-col :span="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE ? 18 : 12">
+          <el-form-item
+            :label="
+              triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+                ? '服务参数'
+                : '比较值'
+            "
+            required
+          >
+            <!-- 服务调用参数配置 -->
+            <ServiceParamsInput
+              v-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE"
+              :model-value="condition.value"
+              @update:model-value="(value) => updateConditionField('value', value)"
+              :service-config="propertyConfig"
+              @validate="handleValueValidate"
+            />
+            <!-- 普通值输入 -->
             <ValueInput
-              :model-value="condition.param"
-              @update:model-value="(value) => updateConditionField('param', value)"
+              v-else
+              :model-value="condition.value"
+              @update:model-value="(value) => updateConditionField('value', value)"
               :property-type="propertyType"
               :operator="condition.operator"
               :property-config="propertyConfig"
@@ -96,22 +131,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 ServiceParamsInput from '../inputs/ServiceParamsInput.vue'
 import DeviceStatusConditionConfig from './DeviceStatusConditionConfig.vue'
-import { ConditionFormData } from '@/api/iot/rule/scene/scene.types'
-import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
+import { TriggerFormData } from '@/api/iot/rule/scene/scene.types'
+import { IotRuleSceneTriggerTypeEnum, getTriggerTypeOptions } from '@/views/iot/utils/constants'
 import { useVModel } from '@vueuse/core'
 
 /** 主条件内部配置组件 */
 defineOptions({ name: 'MainConditionInnerConfig' })
 
 const props = defineProps<{
-  modelValue: ConditionFormData
+  modelValue: TriggerFormData
   triggerType: number
 }>()
 
 const emit = defineEmits<{
-  (e: 'update:modelValue', value: ConditionFormData): void
+  (e: 'update:modelValue', value: TriggerFormData): void
   (e: 'validate', result: { valid: boolean; message: string }): void
+  (e: 'trigger-type-change', value: number): void
 }>()
 
 // 响应式数据
@@ -152,17 +189,24 @@ const getTriggerTypeText = (type: number) => {
   }
 }
 
+// 触发器类型选项
+const triggerTypeOptions = getTriggerTypeOptions()
+
 // 事件处理
-const updateConditionField = (field: keyof ConditionFormData, value: any) => {
-  condition.value[field] = value
+const updateConditionField = (field: keyof TriggerFormData, value: any) => {
+  ;(condition.value as any)[field] = value
   updateValidationResult()
 }
 
-const updateCondition = (value: ConditionFormData) => {
+const updateCondition = (value: TriggerFormData) => {
   emit('update:modelValue', value)
   updateValidationResult()
 }
 
+const handleTriggerTypeChange = (type: number) => {
+  emit('trigger-type-change', type)
+}
+
 const handleProductChange = () => {
   // 产品变化时清空设备和属性
   condition.value.deviceId = undefined
@@ -188,7 +232,7 @@ const handleOperatorChange = () => {
   updateValidationResult()
 }
 
-const handleValueValidate = (result: { valid: boolean; message: string }) => {
+const handleValueValidate = (_result: { valid: boolean; message: string }) => {
   updateValidationResult()
 }
 
@@ -224,14 +268,18 @@ const updateValidationResult = () => {
       return
     }
 
-    if (!condition.value.operator) {
+    // 服务调用不需要操作符
+    if (
+      props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE &&
+      !condition.value.operator
+    ) {
       isValid.value = false
       validationMessage.value = '请选择操作符'
       emit('validate', { valid: false, message: validationMessage.value })
       return
     }
 
-    if (!condition.value.param) {
+    if (!condition.value.value) {
       isValid.value = false
       validationMessage.value = '请输入比较值'
       emit('validate', { valid: false, message: validationMessage.value })
@@ -250,8 +298,11 @@ watch(
     condition.value.productId,
     condition.value.deviceId,
     condition.value.identifier,
-    condition.value.operator,
-    condition.value.param
+    // 服务调用不需要监听操作符
+    props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+      ? condition.value.operator
+      : null,
+    condition.value.value
   ],
   () => {
     updateValidationResult()

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

@@ -80,10 +80,14 @@
 </template>
 
 <script setup lang="ts">
+import { nextTick } from 'vue'
 import { useVModel } from '@vueuse/core'
 import ConditionConfig from './ConditionConfig.vue'
 import { TriggerConditionFormData } from '@/api/iot/rule/scene/scene.types'
-import { IotRuleSceneTriggerConditionTypeEnum } from '@/views/iot/utils/constants'
+import {
+  IotRuleSceneTriggerConditionTypeEnum,
+  IotRuleSceneTriggerConditionParameterOperatorEnum
+} from '@/views/iot/utils/constants'
 
 /** 子条件组配置组件 */
 defineOptions({ name: 'SubConditionGroupConfig' })
@@ -109,21 +113,31 @@ const conditionValidations = ref<{ [key: number]: { valid: boolean; message: str
 
 // 事件处理
 const addCondition = () => {
+  // 确保 subGroup.value 是一个数组
   if (!subGroup.value) {
     subGroup.value = []
   }
-  if (subGroup.value.length >= maxConditions.value) {
+
+  // 检查是否达到最大条件数量限制
+  if (subGroup.value?.length >= maxConditions.value) {
     return
   }
+
   const newCondition: TriggerConditionFormData = {
     type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
     productId: undefined,
     deviceId: undefined,
     identifier: '',
-    operator: '=',
+    operator: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value, // 使用枚举默认值
     param: ''
   }
-  subGroup.value.push(newCondition)
+
+  // 使用 nextTick 确保响应式更新完成后再添加新条件
+  nextTick(() => {
+    if (subGroup.value) {
+      subGroup.value.push(newCondition)
+    }
+  })
 }
 
 const removeCondition = (index: number) => {

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

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

+ 35 - 77
src/views/iot/rule/scene/form/inputs/ValueInput.vue

@@ -1,14 +1,15 @@
 <!-- 值输入组件 -->
 <!-- TODO @yunai:这个需要在看看。。。 -->
 <template>
-  <div class="value-input">
+  <div class="w-full min-w-0">
     <!-- 布尔值选择 -->
     <el-select
       v-if="propertyType === 'bool'"
       v-model="localValue"
       placeholder="请选择布尔值"
       @change="handleChange"
-      class="w-full"
+      class="w-full!"
+      style="width: 100% !important"
     >
       <el-option label="真 (true)" value="true" />
       <el-option label="假 (false)" value="false" />
@@ -20,7 +21,8 @@
       v-model="localValue"
       placeholder="请选择枚举值"
       @change="handleChange"
-      class="w-full"
+      class="w-full!"
+      style="width: 100% !important"
     >
       <el-option
         v-for="option in enumOptions"
@@ -31,41 +33,51 @@
     </el-select>
 
     <!-- 范围输入 (between 操作符) -->
-    <div v-else-if="operator === 'between'" class="range-input">
+    <div
+      v-else-if="operator === 'between'"
+      class="w-full! flex items-center gap-8px"
+      style="width: 100% !important"
+    >
       <el-input
         v-model="rangeStart"
         :type="getInputType()"
         placeholder="最小值"
         @input="handleRangeChange"
-        class="range-start"
+        class="flex-1 min-w-0"
+        style="width: auto !important"
       />
-      <span class="range-separator">至</span>
+      <span class="text-12px text-[var(--el-text-color-secondary)] whitespace-nowrap">至</span>
       <el-input
         v-model="rangeEnd"
         :type="getInputType()"
         placeholder="最大值"
         @input="handleRangeChange"
-        class="range-end"
+        class="flex-1 min-w-0"
+        style="width: auto !important"
       />
     </div>
 
     <!-- 列表输入 (in 操作符) -->
-    <div v-else-if="operator === 'in'" class="list-input">
+    <div v-else-if="operator === 'in'" class="w-full!" style="width: 100% !important">
       <el-input
         v-model="localValue"
         placeholder="请输入值列表,用逗号分隔"
         @input="handleChange"
-        class="w-full"
+        class="w-full!"
+        style="width: 100% !important"
       >
         <template #suffix>
           <el-tooltip content="多个值用逗号分隔,如:1,2,3" placement="top">
-            <Icon icon="ep:question-filled" class="input-tip" />
+            <Icon
+              icon="ep:question-filled"
+              class="text-[var(--el-text-color-placeholder)] cursor-help"
+            />
           </el-tooltip>
         </template>
       </el-input>
-      <div v-if="listPreview.length > 0" class="list-preview">
-        <span class="preview-label">解析结果:</span>
-        <el-tag v-for="(item, index) in listPreview" :key="index" size="small" class="preview-tag">
+      <div v-if="listPreview.length > 0" class="mt-8px flex items-center gap-6px flex-wrap">
+        <span class="text-12px text-[var(--el-text-color-secondary)]">解析结果:</span>
+        <el-tag v-for="(item, index) in listPreview" :key="index" size="small" class="m-0">
           {{ item }}
         </el-tag>
       </div>
@@ -80,7 +92,8 @@
       format="YYYY-MM-DD HH:mm:ss"
       value-format="YYYY-MM-DD HH:mm:ss"
       @change="handleDateChange"
-      class="w-full"
+      class="w-full!"
+      style="width: 100% !important"
     />
 
     <!-- 数字输入 -->
@@ -93,7 +106,8 @@
       :max="getMax()"
       placeholder="请输入数值"
       @change="handleNumberChange"
-      class="w-full"
+      class="w-full!"
+      style="width: 100% !important"
     />
 
     <!-- 文本输入 -->
@@ -103,7 +117,8 @@
       :type="getInputType()"
       :placeholder="getPlaceholder()"
       @input="handleChange"
-      class="w-full"
+      class="w-full!"
+      style="width: 100% !important"
     >
       <template #suffix>
         <el-tooltip
@@ -111,13 +126,15 @@
           :content="`单位:${propertyConfig.unit}`"
           placement="top"
         >
-          <span class="input-unit">{{ propertyConfig.unit }}</span>
+          <span class="text-12px text-[var(--el-text-color-secondary)] px-4px">{{
+            propertyConfig.unit
+          }}</span>
         </el-tooltip>
       </template>
     </el-input>
 
     <!-- 验证提示 -->
-    <div v-if="validationMessage" class="validation-message">
+    <div v-if="validationMessage" class="mt-4px">
       <el-text :type="isValid ? 'success' : 'danger'" size="small">
         <Icon :icon="isValid ? 'ep:check' : 'ep:warning-filled'" />
         {{ validationMessage }}
@@ -354,62 +371,3 @@ onMounted(() => {
   }
 })
 </script>
-
-<style scoped>
-.value-input {
-  width: 100%;
-}
-
-.range-input {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-}
-
-.range-start,
-.range-end {
-  flex: 1;
-}
-
-.range-separator {
-  font-size: 12px;
-  color: var(--el-text-color-secondary);
-  white-space: nowrap;
-}
-
-.list-input {
-  width: 100%;
-}
-
-.input-tip {
-  color: var(--el-text-color-placeholder);
-  cursor: help;
-}
-
-.input-unit {
-  font-size: 12px;
-  color: var(--el-text-color-secondary);
-  padding: 0 4px;
-}
-
-.list-preview {
-  margin-top: 8px;
-  display: flex;
-  align-items: center;
-  gap: 6px;
-  flex-wrap: wrap;
-}
-
-.preview-label {
-  font-size: 12px;
-  color: var(--el-text-color-secondary);
-}
-
-.preview-tag {
-  margin: 0;
-}
-
-.validation-message {
-  margin-top: 4px;
-}
-</style>

+ 26 - 93
src/views/iot/rule/scene/form/sections/ActionSection.vue

@@ -76,7 +76,6 @@
               v-if="isDeviceAction(action.type)"
               :model-value="action"
               @update:model-value="(value) => updateAction(index, value)"
-              @validate="(result) => handleActionValidate(index, result)"
             />
 
             <!-- 告警配置 -->
@@ -84,7 +83,6 @@
               v-if="isAlertAction(action.type)"
               :model-value="action.alertConfigId"
               @update:model-value="(value) => updateActionAlertConfig(index, value)"
-              @validate="(result) => handleActionValidate(index, result)"
             />
           </div>
         </div>
@@ -100,16 +98,6 @@
           最多可添加 {{ maxActions }} 个执行器
         </span>
       </div>
-
-      <!-- 验证结果 -->
-      <div v-if="validationMessage" class="validation-result">
-        <el-alert
-          :title="validationMessage"
-          :type="isValid ? 'success' : 'error'"
-          :closable="false"
-          show-icon
-        />
-      </div>
     </div>
   </el-card>
 </template>
@@ -120,7 +108,12 @@ import ActionTypeSelector from '../selectors/ActionTypeSelector.vue'
 import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
 import AlertConfig from '../configs/AlertConfig.vue'
 import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
-import { IotRuleSceneActionTypeEnum as ActionTypeEnum } from '@/views/iot/utils/constants'
+import {
+  IotRuleSceneActionTypeEnum as ActionTypeEnum,
+  isDeviceAction,
+  isAlertAction,
+  getActionTypeLabel
+} from '@/views/iot/utils/constants'
 
 /** 执行器配置组件 */
 defineOptions({ name: 'ActionSection' })
@@ -131,7 +124,6 @@ interface Props {
 
 interface Emits {
   (e: 'update:actions', value: ActionFormData[]): void
-  (e: 'validate', result: { valid: boolean; message: string }): void
 }
 
 const props = defineProps<Props>()
@@ -147,6 +139,7 @@ const createDefaultActionData = (): ActionFormData => {
     type: ActionTypeEnum.DEVICE_PROPERTY_SET, // 默认为设备属性设置
     productId: undefined,
     deviceId: undefined,
+    identifier: undefined, // 物模型标识符(服务调用时使用)
     params: {},
     alertConfigId: undefined
   }
@@ -155,42 +148,18 @@ const createDefaultActionData = (): ActionFormData => {
 // 配置常量
 const maxActions = 5
 
-// 验证状态
-const actionValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({})
-const validationMessage = ref('')
-const isValid = ref(true)
-
-// 执行器类型映射
-const actionTypeNames = {
-  [ActionTypeEnum.DEVICE_PROPERTY_SET]: '属性设置',
-  [ActionTypeEnum.DEVICE_SERVICE_INVOKE]: '服务调用',
-  [ActionTypeEnum.ALERT_TRIGGER]: '触发告警',
-  [ActionTypeEnum.ALERT_RECOVER]: '恢复告警'
-}
-
-const actionTypeTags = {
-  [ActionTypeEnum.DEVICE_PROPERTY_SET]: 'primary',
-  [ActionTypeEnum.DEVICE_SERVICE_INVOKE]: 'success',
-  [ActionTypeEnum.ALERT_TRIGGER]: 'danger',
-  [ActionTypeEnum.ALERT_RECOVER]: 'warning'
-}
-
 // 工具函数
-const isDeviceAction = (type: number) => {
-  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 as any)
-}
-
 const getActionTypeName = (type: number) => {
-  return actionTypeNames[type] || '未知类型'
+  return getActionTypeLabel(type)
 }
 
 const getActionTypeTag = (type: number) => {
+  const actionTypeTags = {
+    [ActionTypeEnum.DEVICE_PROPERTY_SET]: 'primary',
+    [ActionTypeEnum.DEVICE_SERVICE_INVOKE]: 'success',
+    [ActionTypeEnum.ALERT_TRIGGER]: 'danger',
+    [ActionTypeEnum.ALERT_RECOVER]: 'warning'
+  }
   return actionTypeTags[type] || 'info'
 }
 
@@ -206,21 +175,6 @@ const addAction = () => {
 
 const removeAction = (index: number) => {
   actions.value.splice(index, 1)
-  delete actionValidations.value[index]
-
-  // 重新索引验证结果
-  const newValidations: { [key: number]: { valid: boolean; message: string } } = {}
-  Object.keys(actionValidations.value).forEach((key) => {
-    const numKey = parseInt(key)
-    if (numKey > index) {
-      newValidations[numKey - 1] = actionValidations.value[numKey]
-    } else if (numKey < index) {
-      newValidations[numKey] = actionValidations.value[numKey]
-    }
-  })
-  actionValidations.value = newValidations
-
-  updateValidationResult()
 }
 
 const updateActionType = (index: number, type: number) => {
@@ -237,49 +191,28 @@ const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
 }
 
 const onActionTypeChange = (action: ActionFormData, type: number) => {
-  // 清理不相关的配置
+  // 清理不相关的配置,确保数据结构干净
   if (isDeviceAction(type)) {
+    // 设备控制类型:清理告警配置,确保设备参数存在
     action.alertConfigId = undefined
     if (!action.params) {
       action.params = {}
     }
+    // 如果从其他类型切换到设备控制类型,清空identifier(让用户重新选择)
+    if (action.identifier && type !== action.type) {
+      action.identifier = undefined
+    }
   } else if (isAlertAction(type)) {
+    // 告警类型:清理设备配置
     action.productId = undefined
     action.deviceId = undefined
+    action.identifier = undefined // 清理服务标识符
     action.params = undefined
   }
-}
-
-const handleActionValidate = (index: number, result: { valid: boolean; message: string }) => {
-  actionValidations.value[index] = result
-  updateValidationResult()
-}
 
-const updateValidationResult = () => {
-  const validations = Object.values(actionValidations.value)
-  const allValid = validations.every((v) => v.valid)
-  const hasValidations = validations.length > 0
-
-  if (!hasValidations) {
-    isValid.value = true
-    validationMessage.value = ''
-  } else 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 })
+  // 触发重新校验
+  nextTick(() => {
+    // 这里可以添加校验逻辑
+  })
 }
-
-// 监听执行器数量变化
-watch(
-  () => actions.value.length,
-  () => {
-    updateValidationResult()
-  }
-)
 </script>

+ 34 - 40
src/views/iot/rule/scene/form/sections/TriggerSection.vue

@@ -16,20 +16,26 @@
 
     <div class="p-16px space-y-24px">
       <!-- 触发器列表 -->
-      <!-- TODO 每个触发器,有个外框,会不会好点? -->
       <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"
+          class="border-2 border-green-200 rounded-8px bg-green-50 shadow-sm hover:shadow-md transition-shadow"
         >
-          <!-- 触发器头部 -->
-          <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)">
+          <!-- 触发器头部 - 绿色主题 -->
+          <div
+            class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border-b border-green-200 rounded-t-6px"
+          >
+            <div class="flex items-center gap-12px">
+              <div class="flex items-center gap-8px text-16px font-600 text-green-700">
+                <div
+                  class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
+                >
+                  {{ index + 1 }}
+                </div>
+                <span>触发器 {{ index + 1 }}</span>
+              </div>
+              <el-tag size="small" :type="getTriggerTagType(triggerItem.type)" class="font-500">
                 {{ getTriggerTypeLabel(triggerItem.type) }}
               </el-tag>
             </div>
@@ -40,6 +46,7 @@
                 size="small"
                 text
                 @click="removeTrigger(index)"
+                class="hover:bg-red-50"
               >
                 <Icon icon="ep:delete" />
                 删除
@@ -47,37 +54,24 @@
             </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)"
-          />
-
-          <!-- 定时触发配置 -->
-          <TimerTriggerConfig
-            v-else-if="triggerItem.type === TriggerTypeEnum.TIMER"
-            :model-value="triggerItem.cronExpression"
-            @update:model-value="(value) => updateTriggerCronConfig(index, value)"
-          />
+          <!-- 触发器内容区域 -->
+          <div class="p-16px space-y-16px">
+            <!-- 设备触发配置 -->
+            <DeviceTriggerConfig
+              v-if="isDeviceTrigger(triggerItem.type)"
+              :model-value="triggerItem"
+              :index="index"
+              @update:model-value="(value) => updateTriggerDeviceConfig(index, value)"
+              @trigger-type-change="(type) => updateTriggerType(index, type)"
+            />
+
+            <!-- 定时触发配置 -->
+            <TimerTriggerConfig
+              v-else-if="triggerItem.type === TriggerTypeEnum.TIMER"
+              :model-value="triggerItem.cronExpression"
+              @update:model-value="(value) => updateTriggerCronConfig(index, value)"
+            />
+          </div>
         </div>
       </div>
 

+ 14 - 6
src/views/iot/rule/scene/form/selectors/ProductDeviceSelector.vue

@@ -39,10 +39,20 @@
       <!-- 设备选择模式 -->
       <el-col :span="12">
         <el-form-item label="设备选择模式" required>
-          <el-radio-group v-model="deviceSelectionMode" @change="handleDeviceSelectionModeChange">
+          <el-radio-group
+            v-model="deviceSelectionMode"
+            @change="handleDeviceSelectionModeChange"
+            :disabled="!localProductId"
+          >
             <el-radio value="all">全部设备</el-radio>
             <el-radio value="specific">选择设备</el-radio>
           </el-radio-group>
+          <div
+            v-if="!localProductId"
+            class="text-12px text-[var(--el-text-color-placeholder)] mt-4px"
+          >
+            请先选择产品
+          </div>
         </el-form-item>
       </el-col>
     </el-row>
@@ -50,12 +60,10 @@
     <!-- 具体设备选择 -->
     <el-row v-if="deviceSelectionMode === 'specific'" :gutter="16">
       <el-col :span="24">
-        <!-- TODO @puhui999:貌似产品选择不上; -->
         <el-form-item label="选择设备" required>
-          <!-- TODO @puhui999:请先选择产品,是不是改成请选择设备?然后上面,localProductId 为空(未选择)的时候,禁用 deviceSelectionMode -->
           <el-select
             v-model="localDeviceId"
-            placeholder="请先选择产品"
+            :placeholder="localProductId ? '请选择设备' : '请先选择产品'"
             filterable
             clearable
             @change="handleDeviceChange"
@@ -152,8 +160,8 @@ const localProductId = useVModel(props, 'productId', emit)
 const localDeviceId = useVModel(props, 'deviceId', emit)
 
 // 设备选择模式
-// TODO @puhui999:默认选中 all
-const deviceSelectionMode = ref<'specific' | 'all'>('all')
+// 默认选择具体设备,这样用户可以看到设备选择器
+const deviceSelectionMode = ref<'specific' | 'all'>('specific')
 
 // 数据状态
 const productLoading = ref(false)

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

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

+ 124 - 205
src/views/iot/rule/scene/index.vue

@@ -164,34 +164,22 @@
             </div>
           </template>
         </el-table-column>
-        <!-- TODO puhui999:貌似展示不太对劲。。。一个字,一个 tab 哈了。 -->
+        <!-- 触发条件列 -->
         <el-table-column label="触发条件" min-width="250">
           <template #default="{ row }">
             <div class="flex flex-wrap gap-4px">
-              <el-tag
-                v-for="(trigger, index) in getTriggerSummary(row)"
-                :key="index"
-                type="primary"
-                size="small"
-                class="m-0"
-              >
-                {{ trigger }}
+              <el-tag type="primary" size="small" class="m-0">
+                {{ getTriggerSummary(row) }}
               </el-tag>
             </div>
           </template>
         </el-table-column>
-        <!-- TODO puhui999:貌似展示不太对劲。。。一个字,一个 tab 哈了。 -->
+        <!-- 执行动作列 -->
         <el-table-column label="执行动作" min-width="250">
           <template #default="{ row }">
             <div class="flex flex-wrap gap-4px">
-              <el-tag
-                v-for="(action, index) in getActionSummary(row)"
-                :key="index"
-                type="success"
-                size="small"
-                class="m-0"
-              >
-                {{ action }}
+              <el-tag type="success" size="small" class="m-0">
+                {{ getActionSummary(row) }}
               </el-tag>
             </div>
           </template>
@@ -222,8 +210,7 @@
                 @click="handleToggleStatus(row)"
               >
                 <Icon :icon="row.status === 0 ? 'ep:video-pause' : 'ep:video-play'" />
-                <!-- TODO @puhui999:字典翻译 -->
-                {{ row.status === 0 ? '禁用' : '启用' }}
+                {{ getDictLabel(DICT_TYPE.COMMON_STATUS, row.status === 0 ? 1 : 0) }}
               </el-button>
               <el-button type="danger" class="!mr-10px" link @click="handleDelete(row.id)">
                 <Icon icon="ep:delete" />
@@ -243,42 +230,23 @@
       />
     </el-card>
 
-    <!-- 批量操作 -->
-    <div
-      v-if="selectedRows.length > 0"
-      class="fixed bottom-20px left-1/2 transform -translate-x-1/2 z-1000"
-    >
-      <el-card shadow="always">
-        <div class="flex items-center gap-16px">
-          <span class="font-500 text-[#303133]"> 已选择 {{ selectedRows.length }} 项 </span>
-          <div class="flex gap-8px">
-            <el-button @click="handleBatchEnable">
-              <Icon icon="ep:video-play" />
-              批量启用
-            </el-button>
-            <el-button @click="handleBatchDisable">
-              <Icon icon="ep:video-pause" />
-              批量禁用
-            </el-button>
-            <el-button type="danger" @click="handleBatchDelete">
-              <Icon icon="ep:delete" />
-              批量删除
-            </el-button>
-          </div>
-        </div>
-      </el-card>
-    </div>
-
     <!-- 表单对话框 -->
-    <RuleSceneForm v-model="formVisible" @success="getList" />
+    <RuleSceneForm v-model="formVisible" :rule-scene="currentRule" @success="getList" />
   </ContentWrap>
 </template>
 
 <script setup lang="ts">
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict'
 import { ContentWrap } from '@/components/ContentWrap'
 import RuleSceneForm from './form/RuleSceneForm.vue'
-import { IotRuleScene } from '@/api/iot/rule/scene/scene.types'
+import { IotRuleSceneDO } from '@/api/iot/rule/scene/scene.types'
+import { RuleSceneApi } from '@/api/iot/rule/scene'
+import {
+  IotRuleSceneTriggerTypeEnum,
+  IotRuleSceneActionTypeEnum,
+  getTriggerTypeLabel,
+  getActionTypeLabel
+} from '@/views/iot/utils/constants'
 import { formatDate } from '@/utils/formatTime'
 
 /** 场景联动规则管理页面 */
@@ -287,7 +255,7 @@ defineOptions({ name: 'IoTSceneRule' })
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
-// 查询参数
+/** 查询参数 */
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
@@ -296,16 +264,16 @@ const queryParams = reactive({
 })
 
 const loading = ref(true) // 列表的加载中
-const list = ref<IotRuleScene[]>([]) // 列表的数据
+const list = ref<IotRuleSceneDO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
-const selectedRows = ref<IotRuleScene[]>([]) // 选中的行数据
+const selectedRows = ref<IotRuleSceneDO[]>([]) // 选中的行数据
 const queryFormRef = ref() // 搜索的表单
 
-// 表单状态
+/** 表单状态 */
 const formVisible = ref(false) // 是否可见
-const currentRule = ref<IotRuleScene>() // 表单数据
+const currentRule = ref<IotRuleSceneDO>() // 表单数据
 
-// 统计数据
+/** 统计数据 */
 const statistics = ref({
   total: 0,
   enabled: 0,
@@ -314,7 +282,7 @@ const statistics = ref({
 })
 
 /** 格式化 CRON 表达式显示 */
-// TODO @puhui999:这个能不能 cron 组件里翻译哈;
+/** 注:后续可考虑将此功能移至 CRON 组件内部 */
 const formatCronExpression = (cron: string): string => {
   if (!cron) return ''
 
@@ -358,41 +326,86 @@ const formatCronExpression = (cron: string): string => {
 }
 
 /** 获取规则摘要信息 */
-const getRuleSceneSummary = (rule: IotRuleScene) => {
-  // TODO @puhui999:是不是可以使用字段,或者枚举?
+const getRuleSceneSummary = (rule: IotRuleSceneDO) => {
   const triggerSummary =
-    rule.triggers?.map((trigger) => {
+    rule.triggers?.map((trigger: any) => {
+      // 构建基础描述
+      let description = ''
+
       switch (trigger.type) {
-        case 1:
-          return `设备状态变更 (${trigger.deviceNames?.length || 0}个设备)`
-        case 2:
-          return `属性上报 (${trigger.deviceNames?.length || 0}个设备)`
-        case 3:
-          return `事件上报 (${trigger.deviceNames?.length || 0}个设备)`
-        case 4:
-          return `服务调用 (${trigger.deviceNames?.length || 0}个设备)`
-        case 100:
-          return `定时触发 (${formatCronExpression(trigger.cronExpression || '')})`
+        case IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE:
+          description = '设备状态变更'
+          break
+        case IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST:
+          description = '属性上报'
+          if (trigger.identifier) {
+            description += ` (${trigger.identifier})`
+          }
+          break
+        case IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST:
+          description = '事件上报'
+          if (trigger.identifier) {
+            description += ` (${trigger.identifier})`
+          }
+          break
+        case IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE:
+          description = '服务调用'
+          if (trigger.identifier) {
+            description += ` (${trigger.identifier})`
+          }
+          break
+        case IotRuleSceneTriggerTypeEnum.TIMER:
+          description = `定时触发 (${formatCronExpression(trigger.cronExpression || '')})`
+          break
         default:
-          return '未知触发类型'
+          description = getTriggerTypeLabel(trigger.type)
+      }
+
+      // 添加设备信息(如果有)
+      if (trigger.deviceId) {
+        description += ` [设备ID: ${trigger.deviceId}]`
+      } else if (trigger.productId) {
+        description += ` [产品ID: ${trigger.productId}]`
       }
+
+      return description
     }) || []
 
-  // TODO @puhui999:是不是可以使用字段,或者枚举?
   const actionSummary =
-    rule.actions?.map((action) => {
+    rule.actions?.map((action: any) => {
+      // 构建基础描述
+      let description = ''
+
       switch (action.type) {
-        case 1:
-          return `设备属性设置 (${action.deviceControl?.deviceNames?.length || 0}个设备)`
-        case 2:
-          return `设备服务调用 (${action.deviceControl?.deviceNames?.length || 0}个设备)`
-        case 100:
-          return '发送告警通知'
-        case 101:
-          return '发送邮件通知'
+        case IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET:
+          description = '设备属性设置'
+          break
+        case IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE:
+          description = '设备服务调用'
+          break
+        case IotRuleSceneActionTypeEnum.ALERT_TRIGGER:
+          description = '发送告警通知'
+          break
+        case IotRuleSceneActionTypeEnum.ALERT_RECOVER:
+          description = '发送恢复通知'
+          break
         default:
-          return '未知执行类型'
+          description = getActionTypeLabel(action.type)
       }
+
+      // 添加设备信息(如果有)
+      if (action.deviceId) {
+        description += ` [设备ID: ${action.deviceId}]`
+      } else if (action.productId) {
+        description += ` [产品ID: ${action.productId}]`
+      }
+
+      // 添加告警配置信息(如果有)
+      if (action.alertConfigId) {
+        description += ` [告警配置ID: ${action.alertConfigId}]`
+      }
+
+      return description
     }) || []
 
   return {
@@ -402,69 +415,26 @@ const getRuleSceneSummary = (rule: IotRuleScene) => {
 }
 
 /** 查询列表 */
-// TODO @puhui999:这里使用真实数据;
 const getList = async () => {
   loading.value = true
   try {
-    // 模拟API调用
-    const mockData = {
-      list: [
-        {
-          id: 1,
-          name: '温度过高自动降温',
-          description: '当温度超过30度时自动开启空调',
-          status: 0,
-          triggers: [
-            {
-              type: 2,
-              productKey: 'temp_sensor',
-              deviceNames: ['sensor_001'],
-              conditions: [
-                {
-                  type: 'property',
-                  identifier: 'temperature',
-                  parameters: [{ operator: '>', value: '30' }]
-                }
-              ]
-            }
-          ],
-          actions: [
-            {
-              type: 1,
-              deviceControl: {
-                productKey: 'air_conditioner',
-                deviceNames: ['ac_001'],
-                type: 'property',
-                identifier: 'power',
-                params: { power: 1 }
-              }
-            }
-          ],
-          lastTriggeredTime: new Date().toISOString(),
-          createTime: new Date().toISOString()
-        },
-        {
-          id: 2,
-          name: '设备离线告警',
-          description: '设备离线时发送告警通知',
-          status: 0,
-          triggers: [
-            { type: 1, productKey: 'smart_device', deviceNames: ['device_001', 'device_002'] }
-          ],
-          actions: [{ type: 100, alertConfigId: 1 }],
-          createTime: new Date().toISOString()
-        }
-      ],
-      total: 2
-    }
-
-    list.value = mockData.list
-    total.value = mockData.total
+    // 调用真实API获取数据
+    const data = await RuleSceneApi.getRuleScenePage(queryParams)
+    list.value = data.list
+    total.value = data.total
 
     // 更新统计数据
     updateStatistics()
   } catch (error) {
     console.error('获取列表失败:', error)
+    ElMessage.error('获取列表失败')
+
+    // 清空列表数据
+    list.value = []
+    total.value = 0
+
+    // 更新统计数据
+    updateStatistics()
   } finally {
     loading.value = false
   }
@@ -476,18 +446,18 @@ const updateStatistics = () => {
     total: list.value.length,
     enabled: list.value.filter((item) => item.status === 0).length,
     disabled: list.value.filter((item) => item.status === 1).length,
-    // TODO @puhui999:这里缺了 lastTriggeredTime 定义
-    triggered: list.value.filter((item) => item.lastTriggeredTime).length
+    // 已触发的规则数量 (暂时使用启用状态的规则数量)
+    triggered: list.value.filter((item) => item.status === 0).length
   }
 }
 
-// 获取触发器摘要
-const getTriggerSummary = (rule: IotRuleScene) => {
+/** 获取触发器摘要 */
+const getTriggerSummary = (rule: IotRuleSceneDO) => {
   return getRuleSceneSummary(rule).triggerSummary
 }
 
-// 获取执行器摘要
-const getActionSummary = (rule: IotRuleScene) => {
+/** 获取执行器摘要 */
+const getActionSummary = (rule: IotRuleSceneDO) => {
   return getRuleSceneSummary(rule).actionSummary
 }
 
@@ -511,41 +481,38 @@ const handleAdd = () => {
 }
 
 /** 修改操作 */
-const handleEdit = (row: IotRuleScene) => {
+const handleEdit = (row: IotRuleSceneDO) => {
   currentRule.value = row
   formVisible.value = true
 }
 
 /** 删除按钮操作 */
-// TODO @puhui999:貌似 id 没用上
 const handleDelete = async (id: number) => {
   try {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    // await RuleSceneApi.deleteRuleScene(id)
-
-    // 模拟删除操作
+    await RuleSceneApi.deleteRuleScene(id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()
-  } catch {}
+  } catch (error) {
+    console.error('删除失败:', error)
+    ElMessage.error('删除失败')
+  }
 }
 
 /** 修改状态 */
-const handleToggleStatus = async (row: IotRuleScene) => {
+const handleToggleStatus = async (row: IotRuleSceneDO) => {
   try {
     // 修改状态的二次确认
     const text = row.status === 0 ? '禁用' : '启用'
     await message.confirm('确认要' + text + '"' + row.name + '"吗?')
     // 发起修改状态
-    // TODO @puhui999:这里缺了
-    // await RuleSceneApi.updateRuleSceneStatus(row.id, row.status === 0 ? 1 : 0)
-
-    // 模拟状态切换
-    row.status = row.status === 0 ? 1 : 0
+    await RuleSceneApi.updateRuleSceneStatus(row.id!, row.status === 0 ? 1 : 0)
     message.success(text + '成功')
-    // 刷新统计
+    // 刷新
+    await getList()
     updateStatistics()
   } catch {
     // 取消后,进行恢复按钮
@@ -554,59 +521,11 @@ const handleToggleStatus = async (row: IotRuleScene) => {
 }
 
 /** 多选框选中数据 */
-const handleSelectionChange = (selection: IotRuleScene[]) => {
+const handleSelectionChange = (selection: IotRuleSceneDO[]) => {
   selectedRows.value = selection
 }
 
-/** 批量启用操作 */
-const handleBatchEnable = async () => {
-  try {
-    await message.confirm(`确定要启用选中的 ${selectedRows.value.length} 个规则吗?`)
-    // 这里应该调用批量启用API
-    // TODO @puhui999:这里缺了
-    // await RuleSceneApi.updateRuleSceneStatusBatch(selectedRows.value.map(row => row.id), 0)
-
-    // 模拟批量启用
-    selectedRows.value.forEach((row) => {
-      row.status = 0
-    })
-    message.success('批量启用成功')
-    updateStatistics()
-  } catch {}
-}
-
-/** 批量禁用操作 */
-const handleBatchDisable = async () => {
-  try {
-    await message.confirm(`确定要禁用选中的 ${selectedRows.value.length} 个规则吗?`)
-    // 这里应该调用批量禁用API
-    // TODO @puhui999:这里缺了
-    // await RuleSceneApi.updateRuleSceneStatusBatch(selectedRows.value.map(row => row.id), 1)
-
-    // 模拟批量禁用
-    selectedRows.value.forEach((row) => {
-      row.status = 1
-    })
-    message.success('批量禁用成功')
-    updateStatistics()
-  } catch {}
-}
-
-/** 批量删除操作 */
-const handleBatchDelete = async () => {
-  try {
-    await ElMessageBox.confirm(`确定要删除选中的 ${selectedRows.value.length} 个规则吗?`, '提示', {
-      type: 'warning'
-    })
-
-    // TODO @puhui999:这里缺了
-    // 这里应该调用批量删除API
-    message.success('批量删除成功')
-    await getList()
-  } catch (error) {}
-}
-
-// 初始化
+/** 初始化 */
 onMounted(() => {
   getList()
 })

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

@@ -260,6 +260,66 @@ export const IotRuleSceneActionTypeEnum = {
   ALERT_RECOVER: 101 // 告警恢复
 } as const
 
+/** 执行器类型选项配置 */
+export const getActionTypeOptions = () => [
+  {
+    value: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
+    label: '设备属性设置',
+    description: '设置目标设备的属性值',
+    icon: 'ep:edit',
+    tag: 'primary',
+    category: '设备控制'
+  },
+  {
+    value: IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE,
+    label: '设备服务调用',
+    description: '调用目标设备的服务',
+    icon: 'ep:service',
+    tag: 'success',
+    category: '设备控制'
+  },
+  {
+    value: IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
+    label: '触发告警',
+    description: '触发系统告警通知',
+    icon: 'ep:warning',
+    tag: 'danger',
+    category: '告警通知'
+  },
+  {
+    value: IotRuleSceneActionTypeEnum.ALERT_RECOVER,
+    label: '恢复告警',
+    description: '恢复已触发的告警',
+    icon: 'ep:circle-check',
+    tag: 'warning',
+    category: '告警通知'
+  }
+]
+
+/** 判断是否为设备执行器类型 */
+export const isDeviceAction = (type: number): boolean => {
+  const deviceActionTypes = [
+    IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
+    IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
+  ] as number[]
+  return deviceActionTypes.includes(type)
+}
+
+/** 判断是否为告警执行器类型 */
+export const isAlertAction = (type: number): boolean => {
+  const alertActionTypes = [
+    IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
+    IotRuleSceneActionTypeEnum.ALERT_RECOVER
+  ] as number[]
+  return alertActionTypes.includes(type)
+}
+
+/** 获取执行器类型标签 */
+export const getActionTypeLabel = (type: number): string => {
+  const option = getActionTypeOptions().find((opt) => opt.value === type)
+  return option?.label || '未知类型'
+}
+
 /** IoT 设备消息类型枚举 */
 export const IotDeviceMessageTypeEnum = {
   PROPERTY: 'property', // 属性
@@ -300,3 +360,12 @@ export const IotRuleSceneTriggerTimeOperatorEnum = {
   AFTER_TODAY: { name: '在今日之后', value: 'after_today' }, // 在今日之后
   TODAY: { name: '在今日之间', value: 'today' } // 在今日之间
 } as const
+
+// ========== 辅助函数 ==========
+
+/** 获取触发器类型标签 */
+export const getTriggerTypeLabel = (type: number): string => {
+  const options = getTriggerTypeOptions()
+  const option = options.find((item) => item.value === type)
+  return option?.label || '未知类型'
+}