Эх сурвалжийг харах

perf:【IoT 物联网】场景联动执行器优化

puhui999 10 сар өмнө
parent
commit
38ad857c33

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

@@ -36,11 +36,12 @@ import { useVModel } from '@vueuse/core'
 import BasicInfoSection from './sections/BasicInfoSection.vue'
 import BasicInfoSection from './sections/BasicInfoSection.vue'
 import TriggerSection from './sections/TriggerSection.vue'
 import TriggerSection from './sections/TriggerSection.vue'
 import ActionSection from './sections/ActionSection.vue'
 import ActionSection from './sections/ActionSection.vue'
-import { IotRuleSceneDO, RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
+import { IotRuleScene, IotRuleSceneDO, RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
 import { RuleSceneApi } from '@/api/iot/rule/scene'
 import { RuleSceneApi } from '@/api/iot/rule/scene'
 import {
 import {
   IotRuleSceneTriggerTypeEnum,
   IotRuleSceneTriggerTypeEnum,
   IotRuleSceneActionTypeEnum,
   IotRuleSceneActionTypeEnum,
+  IotDeviceMessageTypeEnum,
   isDeviceTrigger
   isDeviceTrigger
 } from '@/views/iot/utils/constants'
 } from '@/views/iot/utils/constants'
 import { ElMessage } from 'element-plus'
 import { ElMessage } from 'element-plus'
@@ -53,6 +54,57 @@ const CommonStatusEnum = {
   DISABLE: 1 // 关闭
   DISABLE: 1 // 关闭
 } as const
 } as const
 
 
+// 工具函数:根据触发器类型获取消息类型
+const getMessageTypeByTriggerType = (triggerType: number): string => {
+  switch (triggerType) {
+    case IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST:
+      return IotDeviceMessageTypeEnum.PROPERTY
+    case IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST:
+      return IotDeviceMessageTypeEnum.EVENT
+    case IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE:
+      return IotDeviceMessageTypeEnum.SERVICE
+    default:
+      return IotDeviceMessageTypeEnum.PROPERTY
+  }
+}
+
+// 工具函数:根据执行器类型获取消息类型
+const getMessageTypeByActionType = (actionType: number): string => {
+  switch (actionType) {
+    case IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET:
+      return IotDeviceMessageTypeEnum.PROPERTY
+    case IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE:
+      return IotDeviceMessageTypeEnum.SERVICE
+    default:
+      return IotDeviceMessageTypeEnum.PROPERTY
+  }
+}
+
+// 工具函数:根据执行器类型和参数获取标识符
+const getIdentifierByActionType = (actionType: number, params?: Record<string, any>): string => {
+  if (!params) return ''
+
+  switch (actionType) {
+    case IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET:
+      // 属性设置:取第一个属性名作为标识符
+      return Object.keys(params)[0] || ''
+    case IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE:
+      // 服务调用:取 method 字段作为标识符
+      return params.method || ''
+    default:
+      return ''
+  }
+}
+
+// 工具函数:判断是否为设备执行器
+const isDeviceAction = (type: number): boolean => {
+  const deviceActionTypes = [
+    IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
+    IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
+  ] as number[]
+  return deviceActionTypes.includes(type)
+}
+
 /** IoT 场景联动规则表单 - 主表单组件 */
 /** IoT 场景联动规则表单 - 主表单组件 */
 defineOptions({ name: 'RuleSceneForm' })
 defineOptions({ name: 'RuleSceneForm' })
 
 
@@ -95,31 +147,50 @@ const createDefaultFormData = (): RuleSceneFormData => {
 }
 }
 
 
 /**
 /**
- * 将表单数据转换为后端 DO 格式
- * 由于数据结构已对齐,转换变得非常简单
+ * 将表单数据转换为后端 API 格式
+ * 转换为 IotRuleScene 格式,与后端 API 接口对齐
  */
  */
-const convertFormToVO = (formData: RuleSceneFormData): IotRuleSceneDO => {
+const convertFormToVO = (formData: RuleSceneFormData): IotRuleScene => {
   return {
   return {
     id: formData.id,
     id: formData.id,
     name: formData.name,
     name: formData.name,
     description: formData.description,
     description: formData.description,
     status: Number(formData.status),
     status: Number(formData.status),
     triggers: formData.triggers.map((trigger) => ({
     triggers: formData.triggers.map((trigger) => ({
+      key: generateUUID(), // 为每个触发器生成唯一标识
       type: trigger.type,
       type: trigger.type,
-      productId: trigger.productId,
-      deviceId: trigger.deviceId,
-      identifier: trigger.identifier,
-      operator: trigger.operator,
-      value: trigger.value,
-      cronExpression: trigger.cronExpression,
-      conditionGroups: trigger.conditionGroups || []
+      productKey: trigger.productId ? `product_${trigger.productId}` : undefined, // 转换为产品标识
+      deviceNames: trigger.deviceId ? [`device_${trigger.deviceId}`] : undefined, // 转换为设备名称数组
+      conditions: trigger.identifier
+        ? [
+            {
+              type: getMessageTypeByTriggerType(trigger.type),
+              identifier: trigger.identifier,
+              parameters: [
+                {
+                  identifier: trigger.identifier,
+                  operator: trigger.operator || '=',
+                  value: trigger.value || ''
+                }
+              ]
+            }
+          ]
+        : undefined,
+      cronExpression: trigger.cronExpression
     })),
     })),
     actions:
     actions:
       formData.actions?.map((action) => ({
       formData.actions?.map((action) => ({
+        key: generateUUID(), // 为每个执行器生成唯一标识
         type: action.type,
         type: action.type,
-        productId: action.productId,
-        deviceId: action.deviceId,
-        params: action.params,
+        deviceControl: isDeviceAction(action.type)
+          ? {
+              productKey: action.productId ? `product_${action.productId}` : '',
+              deviceNames: action.deviceId ? [`device_${action.deviceId}`] : [],
+              type: getMessageTypeByActionType(action.type),
+              identifier: getIdentifierByActionType(action.type, action.params),
+              params: action.params || {}
+            }
+          : undefined,
         alertConfigId: action.alertConfigId
         alertConfigId: action.alertConfigId
       })) || []
       })) || []
   }
   }

+ 153 - 57
src/views/iot/rule/scene/form/configs/AlertConfig.vue

@@ -1,72 +1,152 @@
 <!-- 告警配置组件 -->
 <!-- 告警配置组件 -->
 <template>
 <template>
   <div class="w-full">
   <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>
             </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
     <div
       v-if="selectedConfig"
       v-if="selectedConfig"
-      class="mt-16px p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"
+      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-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 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>
-      <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>
-        <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>
     </div>
     </div>
@@ -112,6 +192,22 @@ const getNotifyTypeName = (type: number) => {
   return typeMap[type] || '未知'
   return typeMap[type] || '未知'
 }
 }
 
 
+const getNotifyTypeTag = (type: number) => {
+  const tagMap = {
+    1: 'primary', // 邮件
+    2: 'success', // 短信
+    3: 'warning', // 微信
+    4: 'info' // 钉钉
+  }
+  return tagMap[type] || 'info'
+}
+
+// 事件处理
+const handleChange = (value?: number) => {
+  // 可以在这里添加额外的处理逻辑
+  console.log('告警配置选择变化:', value)
+}
+
 // API 调用
 // API 调用
 const getAlertConfigs = async () => {
 const getAlertConfigs = async () => {
   loading.value = true
   loading.value = true

+ 549 - 42
src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue

@@ -2,50 +2,190 @@
 <!-- TODO @puhui999:貌似没生效~~~ -->
 <!-- TODO @puhui999:貌似没生效~~~ -->
 <template>
 <template>
   <div class="flex flex-col gap-16px">
   <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 && 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>
       </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>
+      <!-- 详细示例弹出层 -->
+      <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="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>
-          </template>
-        </el-alert>
-      </div>
+          </div>
+        </div>
+      </Teleport>
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { useVModel } from '@vueuse/core'
 import { useVModel } from '@vueuse/core'
-import ProductDeviceSelector from '../selectors/ProductDeviceSelector.vue'
+import { InfoFilled } from '@element-plus/icons-vue'
+import ProductSelector from '../selectors/ProductSelector.vue'
+import DeviceSelector from '../selectors/DeviceSelector.vue'
 import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
 import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
+import { IotRuleSceneActionTypeEnum } from '@/views/iot/utils/constants'
 
 
 /** 设备控制配置组件 */
 /** 设备控制配置组件 */
 defineOptions({ name: 'DeviceControlConfig' })
 defineOptions({ name: 'DeviceControlConfig' })
@@ -62,38 +202,362 @@ const action = useVModel(props, 'modelValue', emit)
 
 
 // 状态
 // 状态
 const paramsJson = ref('')
 const paramsJson = ref('')
+const jsonError = ref('')
+const thingModelProperties = ref<any[]>([])
+const loadingThingModel = ref(false)
+const propertyValues = ref<Record<string, any>>({})
+
+// 示例弹出层相关状态
+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 handleDeviceChange = ({ productId, deviceId }: { productId?: number; deviceId?: number }) => {
-  action.value.productId = productId
-  action.value.deviceId = deviceId
+const handleProductChange = (productId?: number) => {
+  // 当产品变化时,清空设备选择和参数配置
+  if (action.value.productId !== productId) {
+    action.value.deviceId = undefined
+    action.value.params = {}
+    paramsJson.value = ''
+    jsonError.value = ''
+    propertyValues.value = {}
+  }
+
+  // 加载新产品的物模型属性
+  if (productId && isPropertySetAction.value) {
+    loadThingModelProperties(productId)
+  }
+}
+
+const handleDeviceChange = (deviceId?: number) => {
+  // 当设备变化时,清空参数配置
+  if (action.value.deviceId !== deviceId) {
+    action.value.params = {}
+    paramsJson.value = ''
+    jsonError.value = ''
+  }
+}
+
+// 快速填充示例数据
+const fillExampleJson = () => {
+  const exampleData = generateExampleJson()
+  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
+  }
 }
 }
 
 
 const handleParamsChange = () => {
 const handleParamsChange = () => {
   try {
   try {
+    jsonError.value = '' // 清除之前的错误
+
     if (paramsJson.value.trim()) {
     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 {
     } else {
       action.value.params = {}
       action.value.params = {}
+      propertyValues.value = {}
     }
     }
   } catch (error) {
   } catch (error) {
+    jsonError.value = `JSON格式错误: ${error instanceof Error ? error.message : '未知错误'}`
     console.error('JSON格式错误:', error)
     console.error('JSON格式错误:', error)
   }
   }
 }
 }
 
 
+// 工具函数 - 参考 PropertySelector 的设计
+const getPropertyTypeName = (dataType: string) => {
+  const typeMap = {
+    int: '整数',
+    float: '浮点数',
+    double: '双精度',
+    text: '字符串',
+    bool: '布尔值',
+    enum: '枚举',
+    date: '日期',
+    struct: '结构体',
+    array: '数组'
+  }
+  return typeMap[dataType] || dataType
+}
+
+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 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)
+}
+
+// 示例弹出层控制方法 - 参考 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
+  }
+
+  // 检查是否超出视窗下边界
+  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(() => {
 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)
   }
   }
+
+  // 添加事件监听器
+  document.addEventListener('click', handleClickOutside)
+  window.addEventListener('resize', handleResize)
+})
+
+// 组件卸载时清理事件监听器
+onUnmounted(() => {
+  document.removeEventListener('click', handleClickOutside)
+  window.removeEventListener('resize', handleResize)
 })
 })
 
 
 // 监听参数变化
 // 监听参数变化
 watch(
 watch(
   () => action.value.params,
   () => action.value.params,
   (newParams) => {
   (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 = '参数格式化失败'
+      }
     }
     }
   },
   },
   { deep: true }
   { deep: true }
@@ -101,6 +565,49 @@ watch(
 </script>
 </script>
 
 
 <style scoped>
 <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) {
 :deep(.example-content code) {
   font-family: 'Courier New', monospace;
   font-family: 'Courier New', monospace;
   color: var(--el-color-primary);
   color: var(--el-color-primary);

+ 21 - 28
src/views/iot/rule/scene/form/sections/ActionSection.vue

@@ -108,7 +108,12 @@ import ActionTypeSelector from '../selectors/ActionTypeSelector.vue'
 import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
 import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
 import AlertConfig from '../configs/AlertConfig.vue'
 import AlertConfig from '../configs/AlertConfig.vue'
 import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
 import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
-import { IotRuleSceneActionTypeEnum as ActionTypeEnum } from '@/views/iot/utils/constants'
+import {
+  IotRuleSceneActionTypeEnum as ActionTypeEnum,
+  isDeviceAction,
+  isAlertAction,
+  getActionTypeLabel
+} from '@/views/iot/utils/constants'
 
 
 /** 执行器配置组件 */
 /** 执行器配置组件 */
 defineOptions({ name: 'ActionSection' })
 defineOptions({ name: 'ActionSection' })
@@ -142,37 +147,18 @@ const createDefaultActionData = (): ActionFormData => {
 // 配置常量
 // 配置常量
 const maxActions = 5
 const maxActions = 5
 
 
-// 执行器类型映射
-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) => {
 const getActionTypeName = (type: number) => {
-  return actionTypeNames[type] || '未知类型'
+  return getActionTypeLabel(type)
 }
 }
 
 
 const getActionTypeTag = (type: number) => {
 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'
   return actionTypeTags[type] || 'info'
 }
 }
 
 
@@ -204,16 +190,23 @@ const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
 }
 }
 
 
 const onActionTypeChange = (action: ActionFormData, type: number) => {
 const onActionTypeChange = (action: ActionFormData, type: number) => {
-  // 清理不相关的配置
+  // 清理不相关的配置,确保数据结构干净
   if (isDeviceAction(type)) {
   if (isDeviceAction(type)) {
+    // 设备控制类型:清理告警配置,确保设备参数存在
     action.alertConfigId = undefined
     action.alertConfigId = undefined
     if (!action.params) {
     if (!action.params) {
       action.params = {}
       action.params = {}
     }
     }
   } else if (isAlertAction(type)) {
   } else if (isAlertAction(type)) {
+    // 告警类型:清理设备配置
     action.productId = undefined
     action.productId = undefined
     action.deviceId = undefined
     action.deviceId = undefined
     action.params = undefined
     action.params = undefined
   }
   }
+
+  // 触发重新校验
+  nextTick(() => {
+    // 这里可以添加校验逻辑
+  })
 }
 }
 </script>
 </script>

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

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

+ 60 - 12
src/views/iot/utils/constants.ts

@@ -260,6 +260,66 @@ export const IotRuleSceneActionTypeEnum = {
   ALERT_RECOVER: 101 // 告警恢复
   ALERT_RECOVER: 101 // 告警恢复
 } as const
 } 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 设备消息类型枚举 */
 /** IoT 设备消息类型枚举 */
 export const IotDeviceMessageTypeEnum = {
 export const IotDeviceMessageTypeEnum = {
   PROPERTY: 'property', // 属性
   PROPERTY: 'property', // 属性
@@ -309,15 +369,3 @@ export const getTriggerTypeLabel = (type: number): string => {
   const option = options.find((item) => item.value === type)
   const option = options.find((item) => item.value === type)
   return option?.label || '未知类型'
   return option?.label || '未知类型'
 }
 }
-
-/** 获取执行器类型标签 */
-export const getActionTypeLabel = (type: number): string => {
-  const actionTypeOptions = [
-    { label: '设备属性设置', value: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET },
-    { label: '设备服务调用', value: IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE },
-    { label: '告警触发', value: IotRuleSceneActionTypeEnum.ALERT_TRIGGER },
-    { label: '告警恢复', value: IotRuleSceneActionTypeEnum.ALERT_RECOVER }
-  ]
-  const option = actionTypeOptions.find((item) => item.value === type)
-  return option?.label || '未知类型'
-}