Forráskód Böngészése

perf:【IoT 物联网】场景联动目录结构优化

puhui999 9 hónapja
szülő
commit
d3d6f8f8ab

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

@@ -26,13 +26,6 @@
 
         <!-- 执行器配置 -->
         <ActionSection v-model:actions="formData.actions" @validate="handleActionValidate" />
-
-        <!-- 预览区域 -->
-        <PreviewSection
-          :form-data="formData"
-          :validation-result="validationResult"
-          @validate="handleValidate"
-        />
       </el-form>
     </div>
 
@@ -48,11 +41,16 @@ import { useVModel } from '@vueuse/core'
 import BasicInfoSection from './sections/BasicInfoSection.vue'
 import TriggerSection from './sections/TriggerSection.vue'
 import ActionSection from './sections/ActionSection.vue'
-import PreviewSection from './sections/PreviewSection.vue'
-import { RuleSceneFormData, IotRuleScene } from '@/api/iot/rule/scene/scene.types'
+import {
+  RuleSceneFormData,
+  IotRuleScene,
+  IotRuleSceneTriggerTypeEnum,
+  IotRuleSceneActionTypeEnum,
+  CommonStatusEnum
+} from '@/api/iot/rule/scene/scene.types'
 import { getBaseValidationRules } from '../utils/validation'
-import { transformFormToApi, transformApiToForm, createDefaultFormData } from '../utils/transform'
 import { ElMessage } from 'element-plus'
+import { generateUUID } from '@/utils'
 
 /** IoT 场景联动规则表单 - 主表单组件 */
 defineOptions({ name: 'RuleSceneForm' })
@@ -72,12 +70,93 @@ const emit = defineEmits<Emits>()
 
 const drawerVisible = useVModel(props, 'modelValue', emit)
 
+/**
+ * 创建默认的表单数据
+ */
+const createDefaultFormData = (): RuleSceneFormData => {
+  return {
+    name: '',
+    description: '',
+    status: CommonStatusEnum.ENABLE, // 默认启用状态
+    triggers: [],
+    actions: []
+  }
+}
+
+/**
+ * 将表单数据转换为API请求格式
+ */
+const transformFormToApi = (formData: RuleSceneFormData): IotRuleScene => {
+  return {
+    id: formData.id,
+    name: formData.name,
+    description: formData.description,
+    status: Number(formData.status),
+    triggers:
+      formData.triggers?.map((trigger) => ({
+        type: trigger.type,
+        productKey: trigger.productId ? `product_${trigger.productId}` : undefined,
+        deviceNames: trigger.deviceId ? [`device_${trigger.deviceId}`] : undefined,
+        cronExpression: trigger.cronExpression,
+        conditions:
+          trigger.conditionGroups?.map((group) => ({
+            type: 'property',
+            identifier: trigger.identifier || '',
+            parameters: group.conditions.map((condition) => ({
+              identifier: condition.identifier,
+              operator: condition.operator,
+              value: condition.param
+            }))
+          })) || []
+      })) || [],
+    actions:
+      formData.actions?.map((action) => ({
+        type: action.type,
+        alertConfigId: action.alertConfigId,
+        deviceControl:
+          action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
+          action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
+            ? {
+                productKey: action.productId ? `product_${action.productId}` : '',
+                deviceNames: action.deviceId ? [`device_${action.deviceId}`] : [],
+                type: 'property',
+                identifier: 'set',
+                params: action.params || {}
+              }
+            : undefined
+      })) || []
+  } as IotRuleScene
+}
+
+/**
+ * 将 API 响应数据转换为表单格式
+ */
+const transformApiToForm = (apiData: IotRuleScene): RuleSceneFormData => {
+  return {
+    ...apiData,
+    status: Number(apiData.status), // 确保状态为数字类型
+    triggers:
+      apiData.triggers?.map((trigger) => ({
+        ...trigger,
+        type: Number(trigger.type),
+        // 为每个触发器添加唯一标识符,解决组件索引重用问题
+        key: generateUUID()
+      })) || [],
+    actions:
+      apiData.actions?.map((action) => ({
+        ...action,
+        type: Number(action.type),
+        // 为每个执行器添加唯一标识符,解决组件索引重用问题
+        key: generateUUID()
+      })) || []
+  }
+}
+
 // 表单数据和状态
 const formRef = ref()
 const formData = ref<RuleSceneFormData>(createDefaultFormData())
 const formRules = getBaseValidationRules()
 const submitLoading = ref(false)
-const validationResult = ref<{ valid: boolean; message?: string } | null>(null)
 
 // 验证状态
 const triggerValidation = ref({ valid: true, message: '' })
@@ -87,16 +166,6 @@ const actionValidation = ref({ valid: true, message: '' })
 const isEdit = computed(() => !!props.ruleScene?.id)
 const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' : '新增场景联动规则'))
 
-const canSubmit = computed(() => {
-  return (
-    formData.value.name &&
-    formData.value.triggers.length > 0 &&
-    formData.value.actions.length > 0 &&
-    triggerValidation.value.valid &&
-    actionValidation.value.valid
-  )
-})
-
 // 事件处理
 const handleTriggerValidate = (result: { valid: boolean; message: string }) => {
   triggerValidation.value = result
@@ -106,29 +175,6 @@ const handleActionValidate = (result: { valid: boolean; message: string }) => {
   actionValidation.value = result
 }
 
-const handleValidate = async () => {
-  try {
-    await formRef.value?.validate()
-
-    if (!triggerValidation.value.valid) {
-      throw new Error(triggerValidation.value.message)
-    }
-
-    if (!actionValidation.value.valid) {
-      throw new Error(actionValidation.value.message)
-    }
-
-    validationResult.value = { valid: true, message: '验证通过' }
-    ElMessage.success('规则验证通过')
-    return true
-  } catch (error: any) {
-    const message = error.message || '表单验证失败'
-    validationResult.value = { valid: false, message }
-    ElMessage.error(message)
-    return false
-  }
-}
-
 const handleSubmit = async () => {
   // 校验表单
   if (!formRef.value) return
@@ -167,7 +213,6 @@ const handleSubmit = async () => {
 
 const handleClose = () => {
   drawerVisible.value = false
-  validationResult.value = null
 }
 
 // 初始化表单数据

+ 0 - 76
src/views/iot/rule/scene/form/previews/ActionPreview.vue

@@ -1,76 +0,0 @@
-<!-- 执行器预览组件 -->
-<template>
-  <div class="w-full">
-    <div v-if="actions.length === 0" class="text-center py-20px">
-      <el-text type="info" size="small">暂无执行器配置</el-text>
-    </div>
-    <div v-else class="space-y-12px">
-      <div
-        v-for="(action, index) in actions"
-        :key="index"
-        class="p-12px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]"
-      >
-        <div class="flex items-center gap-8px mb-8px">
-          <Icon icon="ep:setting" class="text-[var(--el-color-success)] text-16px" />
-          <span class="text-14px font-500 text-[var(--el-text-color-primary)]">执行器 {{ index + 1 }}</span>
-          <el-tag :type="getActionTypeTag(action.type)" size="small">
-            {{ getActionTypeName(action.type) }}
-          </el-tag>
-        </div>
-        <div class="pl-24px">
-          <div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">
-            {{ getActionSummary(action) }}
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ActionFormData, IotRuleSceneActionTypeEnum } from '@/api/iot/rule/scene/scene.types'
-
-/** 执行器预览组件 */
-defineOptions({ name: 'ActionPreview' })
-
-interface Props {
-  actions: ActionFormData[]
-}
-
-const props = defineProps<Props>()
-
-// 执行器类型映射
-const actionTypeNames = {
-  [IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET]: '属性设置',
-  [IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE]: '服务调用',
-  [IotRuleSceneActionTypeEnum.ALERT_TRIGGER]: '触发告警',
-  [IotRuleSceneActionTypeEnum.ALERT_RECOVER]: '恢复告警'
-}
-
-const actionTypeTags = {
-  [IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET]: 'primary',
-  [IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE]: 'success',
-  [IotRuleSceneActionTypeEnum.ALERT_TRIGGER]: 'danger',
-  [IotRuleSceneActionTypeEnum.ALERT_RECOVER]: 'warning'
-}
-
-// 工具函数
-const getActionTypeName = (type: number) => {
-  return actionTypeNames[type] || '未知类型'
-}
-
-const getActionTypeTag = (type: number) => {
-  return actionTypeTags[type] || 'info'
-}
-
-const getActionSummary = (action: ActionFormData) => {
-  if (action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER || action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER) {
-    return `告警配置: ${action.alertConfigId ? `配置ID ${action.alertConfigId}` : '未选择'}`
-  } else {
-    const paramsCount = action.params ? Object.keys(action.params).length : 0
-    return `设备控制: 产品${action.productId || '未选择'} 设备${action.deviceId || '未选择'} (${paramsCount}个参数)`
-  }
-}
-</script>
-
-

+ 0 - 37
src/views/iot/rule/scene/form/previews/ConfigPreview.vue

@@ -1,37 +0,0 @@
-<!-- 配置预览组件 -->
-<!-- TODO @puhui999:应该暂时不用预览哈 -->
-<template>
-  <div class="w-full">
-    <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 flex-shrink-0">场景名称:</span>
-        <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ formData.name || '未设置' }}</span>
-      </div>
-      <div class="flex items-start gap-8px">
-        <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">场景状态:</span>
-        <el-tag :type="formData.status === 0 ? 'success' : 'danger'" size="small">
-          {{ formData.status === 0 ? '启用' : '禁用' }}
-        </el-tag>
-      </div>
-      <div v-if="formData.description" class="flex items-start gap-8px">
-        <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">场景描述:</span>
-        <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ formData.description }}</span>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
-
-/** 配置预览组件 */
-defineOptions({ name: 'ConfigPreview' })
-
-interface Props {
-  formData: RuleSceneFormData
-}
-
-defineProps<Props>()
-</script>
-
-

+ 0 - 226
src/views/iot/rule/scene/form/previews/NextExecutionPreview.vue

@@ -1,226 +0,0 @@
-<!-- 下次执行时间预览组件 -->
-<template>
-  <div class="next-execution-preview">
-    <div class="preview-header">
-      <Icon icon="ep:timer" class="preview-icon" />
-      <span class="preview-title">执行时间预览</span>
-    </div>
-    
-    <div v-if="isValidCron" class="preview-content">
-      <div class="current-expression">
-        <span class="expression-label">CRON表达式:</span>
-        <code class="expression-code">{{ cronExpression }}</code>
-      </div>
-      
-      <div class="description">
-        <span class="description-label">执行规律:</span>
-        <span class="description-text">{{ cronDescription }}</span>
-      </div>
-      
-      <div class="next-times">
-        <span class="times-label">接下来5次执行时间:</span>
-        <div class="times-list">
-          <div
-            v-for="(time, index) in nextExecutionTimes"
-            :key="index"
-            class="time-item"
-          >
-            <Icon icon="ep:clock" class="time-icon" />
-            <span class="time-text">{{ time }}</span>
-          </div>
-        </div>
-      </div>
-    </div>
-    
-    <div v-else class="preview-error">
-      <el-alert
-        title="CRON表达式无效"
-        description="请检查CRON表达式格式是否正确"
-        type="error"
-        :closable="false"
-        show-icon
-      />
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { validateCronExpression } from '../../utils/validation'
-
-/** 下次执行时间预览组件 */
-defineOptions({ name: 'NextExecutionPreview' })
-
-interface Props {
-  cronExpression?: string
-}
-
-const props = defineProps<Props>()
-
-// 计算属性
-const isValidCron = computed(() => {
-  return props.cronExpression ? validateCronExpression(props.cronExpression) : false
-})
-
-const cronDescription = computed(() => {
-  if (!isValidCron.value) return ''
-  
-  // 简单的CRON描述生成
-  const parts = props.cronExpression?.split(' ') || []
-  if (parts.length < 6) return '无法解析'
-  
-  const [second, minute, hour, day, month, week] = parts
-  
-  // 生成描述
-  let description = ''
-  
-  if (second === '0' && minute === '0' && hour === '12' && day === '*' && month === '*' && week === '?') {
-    description = '每天中午12点执行'
-  } else if (second === '0' && minute === '*' && hour === '*' && day === '*' && month === '*' && week === '?') {
-    description = '每分钟执行一次'
-  } else if (second === '0' && minute === '0' && hour === '*' && day === '*' && month === '*' && week === '?') {
-    description = '每小时执行一次'
-  } else {
-    description = '按自定义时间规律执行'
-  }
-  
-  return description
-})
-
-const nextExecutionTimes = computed(() => {
-  if (!isValidCron.value) return []
-  
-  // 模拟生成下次执行时间
-  const now = new Date()
-  const times = []
-  
-  for (let i = 1; i <= 5; i++) {
-    // 这里应该使用真实的CRON解析库来计算
-    // 暂时生成模拟时间
-    const nextTime = new Date(now.getTime() + i * 60 * 60 * 1000)
-    times.push(nextTime.toLocaleString('zh-CN', {
-      year: 'numeric',
-      month: '2-digit',
-      day: '2-digit',
-      hour: '2-digit',
-      minute: '2-digit',
-      second: '2-digit'
-    }))
-  }
-  
-  return times
-})
-</script>
-
-<style scoped>
-.next-execution-preview {
-  margin-top: 16px;
-  border: 1px solid var(--el-border-color-light);
-  border-radius: 6px;
-  background: var(--el-fill-color-blank);
-}
-
-.preview-header {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  padding: 12px 16px;
-  background: var(--el-fill-color-light);
-  border-bottom: 1px solid var(--el-border-color-lighter);
-}
-
-.preview-icon {
-  color: var(--el-color-primary);
-  font-size: 16px;
-}
-
-.preview-title {
-  font-size: 14px;
-  font-weight: 500;
-  color: var(--el-text-color-primary);
-}
-
-.preview-content {
-  padding: 16px;
-  display: flex;
-  flex-direction: column;
-  gap: 12px;
-}
-
-.current-expression {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-}
-
-.expression-label {
-  font-size: 12px;
-  color: var(--el-text-color-secondary);
-  min-width: 80px;
-}
-
-.expression-code {
-  font-family: 'Courier New', monospace;
-  background: var(--el-fill-color-light);
-  padding: 4px 8px;
-  border-radius: 4px;
-  font-size: 12px;
-  color: var(--el-color-primary);
-}
-
-.description {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-}
-
-.description-label {
-  font-size: 12px;
-  color: var(--el-text-color-secondary);
-  min-width: 80px;
-}
-
-.description-text {
-  font-size: 12px;
-  color: var(--el-text-color-primary);
-  font-weight: 500;
-}
-
-.next-times {
-  display: flex;
-  flex-direction: column;
-  gap: 8px;
-}
-
-.times-label {
-  font-size: 12px;
-  color: var(--el-text-color-secondary);
-}
-
-.times-list {
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-  margin-left: 12px;
-}
-
-.time-item {
-  display: flex;
-  align-items: center;
-  gap: 6px;
-}
-
-.time-icon {
-  color: var(--el-color-success);
-  font-size: 12px;
-}
-
-.time-text {
-  font-size: 12px;
-  color: var(--el-text-color-primary);
-  font-family: 'Courier New', monospace;
-}
-
-.preview-error {
-  padding: 16px;
-}
-</style>

+ 0 - 80
src/views/iot/rule/scene/form/previews/TriggerPreview.vue

@@ -1,80 +0,0 @@
-<!-- 触发器预览组件 -->
-<template>
-  <div class="w-full">
-    <div v-if="triggers.length === 0" class="text-center py-20px">
-      <el-text type="info" size="small">暂无触发器配置</el-text>
-    </div>
-    <div v-else class="space-y-12px">
-      <div
-        v-for="(trigger, index) in triggers"
-        :key="index"
-        class="p-12px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]"
-      >
-        <div class="flex items-center gap-8px mb-8px">
-          <Icon icon="ep:lightning" class="text-[var(--el-color-warning)] text-16px" />
-          <span class="text-14px font-500 text-[var(--el-text-color-primary)]">触发器 {{ index + 1 }}</span>
-          <el-tag :type="getTriggerTypeTag(trigger.type)" size="small">
-            {{ getTriggerTypeName(trigger.type) }}
-          </el-tag>
-        </div>
-        <div class="pl-24px">
-          <div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">
-            {{ getTriggerSummary(trigger) }}
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { TriggerFormData, IotRuleSceneTriggerTypeEnum } from '@/api/iot/rule/scene/scene.types'
-
-/** 触发器预览组件 */
-defineOptions({ name: 'TriggerPreview' })
-
-interface Props {
-  triggers: TriggerFormData[]
-}
-
-const props = defineProps<Props>()
-
-// 触发器类型映射
-const triggerTypeNames = {
-  [IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE]: '设备状态变更',
-  [IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST]: '属性上报',
-  [IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST]: '事件上报',
-  [IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE]: '服务调用',
-  [IotRuleSceneTriggerTypeEnum.TIMER]: '定时触发'
-}
-
-const triggerTypeTags = {
-  [IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE]: 'warning',
-  [IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST]: 'primary',
-  [IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST]: 'success',
-  [IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE]: 'info',
-  [IotRuleSceneTriggerTypeEnum.TIMER]: 'danger'
-}
-
-// 工具函数
-const getTriggerTypeName = (type: number) => {
-  return triggerTypeNames[type] || '未知类型'
-}
-
-const getTriggerTypeTag = (type: number) => {
-  return triggerTypeTags[type] || 'info'
-}
-
-const getTriggerSummary = (trigger: TriggerFormData) => {
-  if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
-    return `定时执行: ${trigger.cronExpression || '未配置'}`
-  } else if (trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
-    return `设备状态变更: 产品${trigger.productId || '未选择'} 设备${trigger.deviceId || '未选择'}`
-  } else {
-    const conditionCount = trigger.conditionGroups?.reduce((total, group) => total + (group.conditions?.length || 0), 0) || 0
-    return `设备监控: 产品${trigger.productId || '未选择'} 设备${trigger.deviceId || '未选择'} (${conditionCount}个条件)`
-  }
-}
-</script>
-
-

+ 0 - 120
src/views/iot/rule/scene/form/previews/ValidationResult.vue

@@ -1,120 +0,0 @@
-<!-- 验证结果组件 -->
-<template>
-  <div class="validation-result">
-    <div v-if="!validationResult" class="no-validation">
-      <el-text type="info" size="small">
-        <Icon icon="ep:info-filled" />
-        点击"验证配置"按钮检查规则配置
-      </el-text>
-    </div>
-    <div v-else class="validation-content">
-      <el-alert
-        :title="validationResult.valid ? '配置验证通过' : '配置验证失败'"
-        :description="validationResult.message"
-        :type="validationResult.valid ? 'success' : 'error'"
-        :closable="false"
-        show-icon
-      >
-        <template #default>
-          <div v-if="validationResult.valid" class="success-content">
-            <p>{{ validationResult.message || '所有配置项验证通过,规则可以正常运行' }}</p>
-            <div class="success-tips">
-              <Icon icon="ep:check" class="tip-icon" />
-              <span class="tip-text">规则配置完整且有效</span>
-            </div>
-          </div>
-          <div v-else class="error-content">
-            <p>{{ validationResult.message || '配置验证失败,请检查以下问题' }}</p>
-            <div class="error-tips">
-              <div class="tip-item">
-                <Icon icon="ep:warning-filled" class="tip-icon error" />
-                <span class="tip-text">请确保所有必填项都已配置</span>
-              </div>
-              <div class="tip-item">
-                <Icon icon="ep:warning-filled" class="tip-icon error" />
-                <span class="tip-text">请检查触发器和执行器配置是否正确</span>
-              </div>
-            </div>
-          </div>
-        </template>
-      </el-alert>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-/** 验证结果组件 */
-defineOptions({ name: 'ValidationResult' })
-
-interface Props {
-  validationResult?: { valid: boolean; message?: string } | null
-}
-
-defineProps<Props>()
-</script>
-
-<style scoped>
-.validation-result {
-  width: 100%;
-}
-
-.no-validation {
-  text-align: center;
-  padding: 20px 0;
-}
-
-.validation-content {
-  width: 100%;
-}
-
-.success-content,
-.error-content {
-  margin-top: 8px;
-}
-
-.success-content p,
-.error-content p {
-  margin: 0 0 8px 0;
-  font-size: 14px;
-  line-height: 1.5;
-}
-
-.success-tips,
-.error-tips {
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-}
-
-.tip-item {
-  display: flex;
-  align-items: center;
-  gap: 6px;
-}
-
-.tip-icon {
-  font-size: 12px;
-  flex-shrink: 0;
-}
-
-.tip-icon:not(.error) {
-  color: var(--el-color-success);
-}
-
-.tip-icon.error {
-  color: var(--el-color-danger);
-}
-
-.tip-text {
-  font-size: 12px;
-  color: var(--el-text-color-secondary);
-}
-
-.success-tips .tip-text {
-  color: var(--el-color-success-dark-2);
-}
-
-.error-tips .tip-text {
-  color: var(--el-color-danger-dark-2);
-}
-</style>

+ 13 - 1
src/views/iot/rule/scene/form/sections/ActionSection.vue

@@ -117,7 +117,6 @@ import {
   ActionFormData,
   IotRuleSceneActionTypeEnum as ActionTypeEnum
 } from '@/api/iot/rule/scene/scene.types'
-import { createDefaultActionData } from '../../utils/transform'
 
 /** 执行器配置组件 */
 defineOptions({ name: 'ActionSection' })
@@ -136,6 +135,19 @@ const emit = defineEmits<Emits>()
 
 const actions = useVModel(props, 'actions', emit)
 
+/**
+ * 创建默认的执行器数据
+ */
+const createDefaultActionData = (): ActionFormData => {
+  return {
+    type: ActionTypeEnum.DEVICE_PROPERTY_SET, // 默认为设备属性设置
+    productId: undefined,
+    deviceId: undefined,
+    params: {},
+    alertConfigId: undefined
+  }
+}
+
 // 配置常量
 const maxActions = 5
 

+ 0 - 108
src/views/iot/rule/scene/form/sections/PreviewSection.vue

@@ -1,108 +0,0 @@
-<!-- 预览区域组件 -->
-<!-- TODO @puhui999:是不是不用这个哈? -->
-<template>
-  <el-card class="border border-[var(--el-border-color-light)] rounded-8px" shadow="never">
-    <template #header>
-      <div class="flex items-center justify-between">
-        <div class="flex items-center gap-8px">
-          <Icon icon="ep:view" class="text-[var(--el-color-primary)] text-18px" />
-          <span class="text-16px font-600 text-[var(--el-text-color-primary)]">配置预览</span>
-        </div>
-        <div class="flex items-center gap-8px">
-          <el-button type="primary" size="small" @click="handleValidate" :loading="validating">
-            <Icon icon="ep:check" />
-            验证配置
-          </el-button>
-        </div>
-      </div>
-    </template>
-
-    <div class="p-0">
-      <!-- 基础信息预览 -->
-      <div class="mb-20px">
-        <div class="flex items-center gap-8px mb-12px">
-          <Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-16px" />
-          <span class="text-14px font-500 text-[var(--el-text-color-primary)]">基础信息</span>
-        </div>
-        <div class="p-12px bg-[var(--el-fill-color-light)] rounded-6px">
-          <ConfigPreview :form-data="formData" />
-        </div>
-      </div>
-
-      <!-- 触发器预览 -->
-      <div class="mb-20px">
-        <div class="flex items-center gap-8px mb-12px">
-          <Icon icon="ep:lightning" class="text-[var(--el-color-warning)] text-16px" />
-          <span class="text-14px font-500 text-[var(--el-text-color-primary)]">触发器配置</span>
-          <el-tag size="small" type="primary">{{ formData.triggers.length }}</el-tag>
-        </div>
-        <div class="p-12px bg-[var(--el-fill-color-light)] rounded-6px">
-          <TriggerPreview :triggers="formData.triggers" />
-        </div>
-      </div>
-
-      <!-- 执行器预览 -->
-      <div class="mb-20px">
-        <div class="flex items-center gap-8px mb-12px">
-          <Icon icon="ep:setting" class="text-[var(--el-color-success)] text-16px" />
-          <span class="text-14px font-500 text-[var(--el-text-color-primary)]">执行器配置</span>
-          <el-tag size="small" type="success">{{ formData.actions.length }}</el-tag>
-        </div>
-        <div class="p-12px bg-[var(--el-fill-color-light)] rounded-6px">
-          <ActionPreview :actions="formData.actions" />
-        </div>
-      </div>
-
-      <!-- 验证结果 -->
-      <div class="mb-20px">
-        <div class="flex items-center gap-8px mb-12px">
-          <Icon icon="ep:circle-check" class="text-[var(--el-color-primary)] text-16px" />
-          <span class="text-14px font-500 text-[var(--el-text-color-primary)]">验证结果</span>
-        </div>
-        <div class="p-12px bg-[var(--el-fill-color-light)] rounded-6px">
-          <ValidationResult :validation-result="validationResult" />
-        </div>
-      </div>
-    </div>
-  </el-card>
-</template>
-
-<script setup lang="ts">
-import ConfigPreview from '../previews/ConfigPreview.vue'
-import TriggerPreview from '../previews/TriggerPreview.vue'
-import ActionPreview from '../previews/ActionPreview.vue'
-import ValidationResult from '../previews/ValidationResult.vue'
-import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
-
-/** 预览区域组件 */
-defineOptions({ name: 'PreviewSection' })
-
-interface Props {
-  formData: RuleSceneFormData
-  validationResult?: { valid: boolean; message?: string } | null
-}
-
-interface Emits {
-  (e: 'validate'): void
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<Emits>()
-
-// 状态
-const validating = ref(false)
-
-// 事件处理
-const handleValidate = async () => {
-  validating.value = true
-  try {
-    // 延迟一下模拟验证过程
-    await new Promise((resolve) => setTimeout(resolve, 500))
-    emit('validate')
-  } finally {
-    validating.value = false
-  }
-}
-</script>
-
-

+ 16 - 1
src/views/iot/rule/scene/form/sections/TriggerSection.vue

@@ -119,7 +119,6 @@ import {
   TriggerFormData,
   IotRuleSceneTriggerTypeEnum as TriggerTypeEnum
 } from '@/api/iot/rule/scene/scene.types'
-import { createDefaultTriggerData } from '../../utils/transform'
 
 /** 触发器配置组件 */
 defineOptions({ name: 'TriggerSection' })
@@ -138,6 +137,22 @@ const emit = defineEmits<Emits>()
 
 const triggers = useVModel(props, 'triggers', emit)
 
+/**
+ * 创建默认的触发器数据
+ */
+const createDefaultTriggerData = (): TriggerFormData => {
+  return {
+    type: TriggerTypeEnum.DEVICE_PROPERTY_POST, // 默认为设备属性上报
+    productId: undefined,
+    deviceId: undefined,
+    identifier: undefined,
+    operator: undefined,
+    value: undefined,
+    cronExpression: undefined,
+    conditionGroups: []
+  }
+}
+
 // 配置常量
 const maxTriggers = 5
 

+ 149 - 73
src/views/iot/rule/scene/index.vue

@@ -35,7 +35,6 @@
             class="!w-240px"
           />
         </el-form-item>
-        <!-- TODO @puhui999:字典 -->
         <el-form-item label="规则状态">
           <el-select
             v-model="queryParams.status"
@@ -43,8 +42,12 @@
             clearable
             class="!w-240px"
           >
-            <el-option label="启用" :value="0" />
-            <el-option label="禁用" :value="1" />
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
           </el-select>
         </el-form-item>
         <el-form-item>
@@ -61,7 +64,6 @@
     </el-card>
 
     <!-- 统计卡片 -->
-    <!-- TODO @puhui999:这种需要服用的 stats-content、stats-info 的属性,到底 unocss 好,还是现有的 style css 好~ -->
     <el-row :gutter="16" class="mb-16px">
       <el-col :span="6">
         <el-card class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px" shadow="hover">
@@ -125,14 +127,7 @@
           <template #default="{ row }">
             <div class="flex items-center gap-8px">
               <span class="font-500 text-[#303133]">{{ row.name }}</span>
-              <!-- TODO @puhui999:字典 -->
-              <el-tag
-                :type="row.status === 0 ? 'success' : 'danger'"
-                size="small"
-                class="flex-shrink-0"
-              >
-                {{ row.status === 0 ? '启用' : '禁用' }}
-              </el-tag>
+              <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
             </div>
             <div v-if="row.description" class="text-12px text-[#909399] mt-4px">
               {{ row.description }}
@@ -169,7 +164,6 @@
             </div>
           </template>
         </el-table-column>
-        <!-- TODO @puhui999:貌似要新增一个字段? -->
         <el-table-column label="最近触发" prop="lastTriggeredTime" width="180">
           <template #default="{ row }">
             <span v-if="row.lastTriggeredTime">
@@ -185,7 +179,6 @@
         </el-table-column>
         <el-table-column label="操作" width="200" fixed="right">
           <template #default="{ row }">
-            <!-- TODO @puhui999:间隙大了点 -->
             <div class="flex gap-8px">
               <el-button type="primary" link @click="handleEdit(row)">
                 <Icon icon="ep:edit" />
@@ -197,10 +190,9 @@
                 @click="handleToggleStatus(row)"
               >
                 <Icon :icon="row.status === 0 ? 'ep:video-pause' : 'ep:video-play'" />
-                <!-- TODO @puhui999:翻译,字典 -->
                 {{ row.status === 0 ? '禁用' : '启用' }}
               </el-button>
-              <el-button type="danger" link @click="handleDelete(row)">
+              <el-button type="danger" link @click="handleDelete(row.id)">
                 <Icon icon="ep:delete" />
                 删除
               </el-button>
@@ -247,17 +239,17 @@
 </template>
 
 <script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { ContentWrap } from '@/components/ContentWrap'
 import RuleSceneForm from './form/RuleSceneForm.vue'
 import { IotRuleScene } from '@/api/iot/rule/scene/scene.types'
-import { getRuleSceneSummary } from './utils/transform'
 import { formatDate } from '@/utils/formatTime'
 
 /** 场景联动规则管理页面 */
 defineOptions({ name: 'IoTSceneRule' })
 
-const message = useMessage()
-// const { t } = useI18n() // TODO @puhui999:可以删除
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
 
 // 查询参数
 const queryParams = reactive({
@@ -267,12 +259,11 @@ const queryParams = reactive({
   status: undefined as number | undefined
 })
 
-// 数据状态
-// TODO @puhui999:变量名,和别的页面保持一致哈
-const loading = ref(true)
-const list = ref<IotRuleScene[]>([])
-const total = ref(0)
-const selectedRows = ref<IotRuleScene[]>([])
+const loading = ref(true) // 列表的加载中
+const list = ref<IotRuleScene[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const selectedRows = ref<IotRuleScene[]>([]) // 选中的行数据
+const queryFormRef = ref() // 搜索的表单
 
 // 表单状态
 const formVisible = ref(false)
@@ -286,8 +277,96 @@ const statistics = ref({
   triggered: 0
 })
 
-// 获取列表数据
-// TODO @puhui999:接入
+/**
+ * 格式化CRON表达式显示
+ */
+const formatCronExpression = (cron: string): string => {
+  if (!cron) return ''
+
+  // 简单的CRON表达式解析和格式化
+  const parts = cron.trim().split(' ')
+  if (parts.length < 5) return cron
+
+  const [second, minute, hour] = parts
+
+  // 构建可读的描述
+  let description = ''
+
+  if (second === '0' && minute === '0') {
+    if (hour === '*') {
+      description = '每小时'
+    } else if (hour.includes('/')) {
+      const interval = hour.split('/')[1]
+      description = `每${interval}小时`
+    } else {
+      description = `每天${hour}点`
+    }
+  } else if (second === '0') {
+    if (minute === '*') {
+      description = '每分钟'
+    } else if (minute.includes('/')) {
+      const interval = minute.split('/')[1]
+      description = `每${interval}分钟`
+    } else {
+      description = `每小时第${minute}分钟`
+    }
+  } else {
+    if (second === '*') {
+      description = '每秒'
+    } else if (second.includes('/')) {
+      const interval = second.split('/')[1]
+      description = `每${interval}秒`
+    }
+  }
+
+  return description || cron
+}
+
+/**
+ * 获取规则摘要信息
+ */
+const getRuleSceneSummary = (rule: IotRuleScene) => {
+  const triggerSummary =
+    rule.triggers?.map((trigger) => {
+      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 || '')})`
+        default:
+          return '未知触发类型'
+      }
+    }) || []
+
+  const actionSummary =
+    rule.actions?.map((action) => {
+      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 '发送邮件通知'
+        default:
+          return '未知执行类型'
+      }
+    }) || []
+
+  return {
+    triggerSummary: triggerSummary.join(', ') || '无触发器',
+    actionSummary: actionSummary.join(', ') || '无执行器'
+  }
+}
+
+/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
@@ -355,9 +434,7 @@ const getList = async () => {
   }
 }
 
-// TODO @puhui999:方法注释,使用 /** */ 风格
-
-// 更新统计数据
+/** 更新统计数据 */
 const updateStatistics = () => {
   statistics.value = {
     total: list.value.length,
@@ -377,19 +454,20 @@ const getActionSummary = (rule: IotRuleScene) => {
   return getRuleSceneSummary(rule).actionSummary
 }
 
-// 事件处理
+/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
+/** 重置按钮操作 */
 const resetQuery = () => {
   queryParams.name = ''
   queryParams.status = undefined
   handleQuery()
 }
 
-// TODO @puhui999:这个要不还是使用 open 方式,只是弹出的右侧;
+/** 添加/修改操作 */
 const handleAdd = () => {
   currentRule.value = undefined
   formVisible.value = true
@@ -400,78 +478,76 @@ const handleEdit = (row: IotRuleScene) => {
   formVisible.value = true
 }
 
-// TODO @puhui999:handleDelete、handleToggleStatus 保持和别的模块一致哇?
-const handleDelete = async (row: IotRuleScene) => {
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
   try {
-    await ElMessageBox.confirm('确定要删除这个规则吗?', '提示', {
-      type: 'warning'
-    })
-
-    // 这里应该调用删除API
-    message.success('删除成功')
-    getList()
-  } catch (error) {
-    // 用户取消删除
-  }
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    // await RuleSceneApi.deleteRuleScene(id)
+
+    // 模拟删除操作
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
 }
 
+/** 修改状态 */
 const handleToggleStatus = async (row: IotRuleScene) => {
   try {
-    const newStatus = row.status === 0 ? 1 : 0
-    const action = newStatus === 0 ? '启用' : '禁用'
-
-    await ElMessageBox.confirm(`确定要${action}这个规则吗?`, '提示', {
-      type: 'warning'
-    })
-
-    // 这里应该调用状态切换API
-    row.status = newStatus
-    message.success(`${action}成功`)
+    // 修改状态的二次确认
+    const text = row.status === 0 ? '禁用' : '启用'
+    await message.confirm('确认要' + text + '"' + row.name + '"吗?')
+    // 发起修改状态
+    // await RuleSceneApi.updateRuleSceneStatus(row.id, row.status === 0 ? 1 : 0)
+
+    // 模拟状态切换
+    row.status = row.status === 0 ? 1 : 0
+    message.success(text + '成功')
+    // 刷新统计
     updateStatistics()
-  } catch (error) {
-    // 用户取消操作
+  } catch {
+    // 取消后,进行恢复按钮
+    row.status = row.status === 0 ? 1 : 0
   }
 }
 
+/** 多选框选中数据 */
 const handleSelectionChange = (selection: IotRuleScene[]) => {
   selectedRows.value = selection
 }
 
-// TODO @puhui999:batch 操作的逻辑,要不和其它 UI 界面保持一致,或者相对一致哈;
+/** 批量启用操作 */
 const handleBatchEnable = async () => {
   try {
-    await ElMessageBox.confirm(`确定要启用选中的 ${selectedRows.value.length} 个规则吗?`, '提示', {
-      type: 'warning'
-    })
-
+    await message.confirm(`确定要启用选中的 ${selectedRows.value.length} 个规则吗?`)
     // 这里应该调用批量启用API
+    // await RuleSceneApi.updateRuleSceneStatusBatch(selectedRows.value.map(row => row.id), 0)
+
+    // 模拟批量启用
     selectedRows.value.forEach((row) => {
       row.status = 0
     })
-
     message.success('批量启用成功')
     updateStatistics()
-  } catch (error) {
-    // 用户取消操作
-  }
+  } catch {}
 }
 
+/** 批量禁用操作 */
 const handleBatchDisable = async () => {
   try {
-    await ElMessageBox.confirm(`确定要禁用选中的 ${selectedRows.value.length} 个规则吗?`, '提示', {
-      type: 'warning'
-    })
-
+    await message.confirm(`确定要禁用选中的 ${selectedRows.value.length} 个规则吗?`)
     // 这里应该调用批量禁用API
+    // await RuleSceneApi.updateRuleSceneStatusBatch(selectedRows.value.map(row => row.id), 1)
+
+    // 模拟批量禁用
     selectedRows.value.forEach((row) => {
       row.status = 1
     })
-
     message.success('批量禁用成功')
     updateStatistics()
-  } catch (error) {
-    // 用户取消操作
-  }
+  } catch {}
 }
 
 const handleBatchDelete = async () => {

+ 0 - 550
src/views/iot/rule/scene/utils/errorHandler.ts

@@ -1,550 +0,0 @@
-/**
- * IoT 场景联动错误处理和用户反馈工具
- */
-
-// TODO @puhui999:这个貌似用不到?
-
-import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
-
-// 错误类型枚举
-export enum ErrorType {
-  VALIDATION = 'validation',
-  NETWORK = 'network',
-  BUSINESS = 'business',
-  SYSTEM = 'system',
-  PERMISSION = 'permission'
-}
-
-// 错误级别枚举
-export enum ErrorLevel {
-  INFO = 'info',
-  WARNING = 'warning',
-  ERROR = 'error',
-  CRITICAL = 'critical'
-}
-
-// 错误信息接口
-export interface ErrorInfo {
-  type: ErrorType
-  level: ErrorLevel
-  code?: string
-  message: string
-  details?: any
-  timestamp?: Date
-  context?: string
-}
-
-// 用户反馈选项
-export interface FeedbackOptions {
-  showMessage?: boolean
-  showNotification?: boolean
-  showDialog?: boolean
-  autoClose?: boolean
-  duration?: number
-  confirmText?: string
-  cancelText?: string
-}
-
-/**
- * 错误处理器类
- */
-export class SceneRuleErrorHandler {
-  private static instance: SceneRuleErrorHandler
-  private errorLog: ErrorInfo[] = []
-  private maxLogSize = 100
-
-  private constructor() {}
-
-  static getInstance(): SceneRuleErrorHandler {
-    if (!SceneRuleErrorHandler.instance) {
-      SceneRuleErrorHandler.instance = new SceneRuleErrorHandler()
-    }
-    return SceneRuleErrorHandler.instance
-  }
-
-  /**
-   * 处理错误
-   */
-  handleError(error: ErrorInfo, options: FeedbackOptions = {}): Promise<boolean> {
-    // 记录错误日志
-    this.logError(error)
-
-    // 根据错误类型和级别选择处理方式
-    return this.processError(error, options)
-  }
-
-  /**
-   * 记录错误日志
-   */
-  private logError(error: ErrorInfo): void {
-    const errorWithTimestamp = {
-      ...error,
-      timestamp: new Date()
-    }
-
-    this.errorLog.unshift(errorWithTimestamp)
-
-    // 限制日志大小
-    if (this.errorLog.length > this.maxLogSize) {
-      this.errorLog = this.errorLog.slice(0, this.maxLogSize)
-    }
-
-    // 开发环境下打印到控制台
-    if (import.meta.env.DEV) {
-      console.error('[SceneRule Error]', errorWithTimestamp)
-    }
-  }
-
-  /**
-   * 处理错误
-   */
-  private async processError(error: ErrorInfo, options: FeedbackOptions): Promise<boolean> {
-    const defaultOptions: FeedbackOptions = {
-      showMessage: true,
-      showNotification: false,
-      showDialog: false,
-      autoClose: true,
-      duration: 3000,
-      confirmText: '确定',
-      cancelText: '取消'
-    }
-
-    const finalOptions = { ...defaultOptions, ...options }
-
-    try {
-      // 根据错误级别决定反馈方式
-      switch (error.level) {
-        case ErrorLevel.INFO:
-          return this.handleInfoError(error, finalOptions)
-        case ErrorLevel.WARNING:
-          return this.handleWarningError(error, finalOptions)
-        case ErrorLevel.ERROR:
-          return this.handleNormalError(error, finalOptions)
-        case ErrorLevel.CRITICAL:
-          return this.handleCriticalError(error, finalOptions)
-        default:
-          return this.handleNormalError(error, finalOptions)
-      }
-    } catch (e) {
-      console.error('Error handler failed:', e)
-      return false
-    }
-  }
-
-  /**
-   * 处理信息级错误
-   */
-  private async handleInfoError(error: ErrorInfo, options: FeedbackOptions): Promise<boolean> {
-    if (options.showMessage) {
-      ElMessage.info({
-        message: error.message,
-        duration: options.duration,
-        showClose: !options.autoClose
-      })
-    }
-    return true
-  }
-
-  /**
-   * 处理警告级错误
-   */
-  private async handleWarningError(error: ErrorInfo, options: FeedbackOptions): Promise<boolean> {
-    if (options.showNotification) {
-      ElNotification.warning({
-        title: '警告',
-        message: error.message,
-        duration: options.duration
-      })
-    } else if (options.showMessage) {
-      ElMessage.warning({
-        message: error.message,
-        duration: options.duration,
-        showClose: !options.autoClose
-      })
-    }
-    return true
-  }
-
-  /**
-   * 处理普通错误
-   */
-  private async handleNormalError(error: ErrorInfo, options: FeedbackOptions): Promise<boolean> {
-    if (options.showDialog) {
-      try {
-        await ElMessageBox.alert(error.message, '错误', {
-          type: 'error',
-          confirmButtonText: options.confirmText
-        })
-        return true
-      } catch (e) {
-        return false
-      }
-    } else if (options.showNotification) {
-      ElNotification.error({
-        title: '错误',
-        message: error.message,
-        duration: options.duration
-      })
-    } else if (options.showMessage) {
-      ElMessage.error({
-        message: error.message,
-        duration: options.duration,
-        showClose: !options.autoClose
-      })
-    }
-    return true
-  }
-
-  /**
-   * 处理严重错误
-   */
-  private async handleCriticalError(error: ErrorInfo, _: FeedbackOptions): Promise<boolean> {
-    try {
-      await ElMessageBox.confirm(`${error.message}\n\n是否重新加载页面?`, '严重错误', {
-        type: 'error',
-        confirmButtonText: '重新加载',
-        cancelButtonText: '继续使用'
-      })
-      // 用户选择重新加载
-      window.location.reload()
-      return true
-    } catch (e) {
-      // 用户选择继续使用
-      return false
-    }
-  }
-
-  /**
-   * 获取错误日志
-   */
-  getErrorLog(): ErrorInfo[] {
-    return [...this.errorLog]
-  }
-
-  /**
-   * 清空错误日志
-   */
-  clearErrorLog(): void {
-    this.errorLog = []
-  }
-
-  /**
-   * 导出错误日志
-   */
-  exportErrorLog(): string {
-    return JSON.stringify(this.errorLog, null, 2)
-  }
-}
-
-/**
- * 预定义的错误处理函数
- */
-export const errorHandler = SceneRuleErrorHandler.getInstance()
-
-/**
- * 验证错误处理
- */
-export function handleValidationError(message: string, context?: string): Promise<boolean> {
-  return errorHandler.handleError(
-    {
-      type: ErrorType.VALIDATION,
-      level: ErrorLevel.WARNING,
-      message,
-      context
-    },
-    {
-      showMessage: true,
-      duration: 4000
-    }
-  )
-}
-
-/**
- * 网络错误处理
- */
-export function handleNetworkError(error: any, context?: string): Promise<boolean> {
-  let message = '网络请求失败'
-
-  if (error?.response?.status) {
-    switch (error.response.status) {
-      case 400:
-        message = '请求参数错误'
-        break
-      case 401:
-        message = '未授权,请重新登录'
-        break
-      case 403:
-        message = '权限不足'
-        break
-      case 404:
-        message = '请求的资源不存在'
-        break
-      case 500:
-        message = '服务器内部错误'
-        break
-      case 502:
-        message = '网关错误'
-        break
-      case 503:
-        message = '服务暂不可用'
-        break
-      default:
-        message = `网络错误 (${error.response.status})`
-    }
-  } else if (error?.message) {
-    message = error.message
-  }
-
-  return errorHandler.handleError(
-    {
-      type: ErrorType.NETWORK,
-      level: ErrorLevel.ERROR,
-      code: error?.response?.status?.toString(),
-      message,
-      details: error,
-      context
-    },
-    {
-      showMessage: true,
-      duration: 5000
-    }
-  )
-}
-
-/**
- * 业务逻辑错误处理
- */
-export function handleBusinessError(
-  message: string,
-  code?: string,
-  context?: string
-): Promise<boolean> {
-  return errorHandler.handleError(
-    {
-      type: ErrorType.BUSINESS,
-      level: ErrorLevel.ERROR,
-      code,
-      message,
-      context
-    },
-    {
-      showMessage: true,
-      duration: 4000
-    }
-  )
-}
-
-/**
- * 系统错误处理
- */
-export function handleSystemError(error: any, context?: string): Promise<boolean> {
-  const message = error?.message || '系统发生未知错误'
-
-  return errorHandler.handleError(
-    {
-      type: ErrorType.SYSTEM,
-      level: ErrorLevel.CRITICAL,
-      message,
-      details: error,
-      context
-    },
-    {
-      showDialog: true
-    }
-  )
-}
-
-/**
- * 权限错误处理
- */
-export function handlePermissionError(
-  message: string = '权限不足',
-  context?: string
-): Promise<boolean> {
-  return errorHandler.handleError(
-    {
-      type: ErrorType.PERMISSION,
-      level: ErrorLevel.WARNING,
-      message,
-      context
-    },
-    {
-      showNotification: true,
-      duration: 5000
-    }
-  )
-}
-
-/**
- * 成功反馈
- */
-export function showSuccess(message: string, duration: number = 3000): void {
-  ElMessage.success({
-    message,
-    duration,
-    showClose: false
-  })
-}
-
-/**
- * 信息反馈
- */
-export function showInfo(message: string, duration: number = 3000): void {
-  ElMessage.info({
-    message,
-    duration,
-    showClose: false
-  })
-}
-
-/**
- * 警告反馈
- */
-export function showWarning(message: string, duration: number = 4000): void {
-  ElMessage.warning({
-    message,
-    duration,
-    showClose: true
-  })
-}
-
-/**
- * 确认对话框
- */
-export function showConfirm(
-  message: string,
-  title: string = '确认',
-  options: {
-    type?: 'info' | 'success' | 'warning' | 'error'
-    confirmText?: string
-    cancelText?: string
-  } = {}
-): Promise<boolean> {
-  const defaultOptions = {
-    type: 'warning' as const,
-    confirmText: '确定',
-    cancelText: '取消'
-  }
-
-  const finalOptions = { ...defaultOptions, ...options }
-
-  return ElMessageBox.confirm(message, title, {
-    type: finalOptions.type,
-    confirmButtonText: finalOptions.confirmText,
-    cancelButtonText: finalOptions.cancelText
-  })
-    .then(() => true)
-    .catch(() => false)
-}
-
-/**
- * 加载状态管理
- */
-export class LoadingManager {
-  private loadingStates = new Map<string, boolean>()
-  private loadingInstances = new Map<string, any>()
-
-  /**
-   * 开始加载
-   */
-  startLoading(key: string, _: string = '加载中...'): void {
-    if (this.loadingStates.get(key)) {
-      return // 已经在加载中
-    }
-
-    this.loadingStates.set(key, true)
-
-    // 这里可以根据需要创建全局加载实例
-    // const loading = ElLoading.service({
-    //   lock: true,
-    //   text,
-    //   background: 'rgba(0, 0, 0, 0.7)'
-    // })
-    // this.loadingInstances.set(key, loading)
-  }
-
-  /**
-   * 结束加载
-   */
-  stopLoading(key: string): void {
-    this.loadingStates.set(key, false)
-
-    const loading = this.loadingInstances.get(key)
-    if (loading) {
-      loading.close()
-      this.loadingInstances.delete(key)
-    }
-  }
-
-  /**
-   * 检查是否在加载中
-   */
-  isLoading(key: string): boolean {
-    return this.loadingStates.get(key) || false
-  }
-
-  /**
-   * 清空所有加载状态
-   */
-  clearAll(): void {
-    this.loadingInstances.forEach((loading) => loading.close())
-    this.loadingStates.clear()
-    this.loadingInstances.clear()
-  }
-}
-
-export const loadingManager = new LoadingManager()
-
-/**
- * 异步操作包装器,自动处理错误和加载状态
- */
-export async function withErrorHandling<T>(
-  operation: () => Promise<T>,
-  options: {
-    loadingKey?: string
-    loadingText?: string
-    context?: string
-    showSuccess?: boolean
-    successMessage?: string
-    errorHandler?: (error: any) => Promise<boolean>
-  } = {}
-): Promise<T | null> {
-  const {
-    loadingKey,
-    loadingText = '处理中...',
-    context,
-    showSuccess = false,
-    // successMessage = '操作成功',
-    errorHandler: customErrorHandler
-  } = options
-
-  try {
-    // 开始加载
-    if (loadingKey) {
-      loadingManager.startLoading(loadingKey, loadingText)
-    }
-
-    // 执行操作
-    const result = await operation()
-
-    // 显示成功消息
-    if (showSuccess) {
-      // showSuccess(successMessage)
-    }
-
-    return result
-  } catch (error) {
-    // 使用自定义错误处理器或默认处理器
-    if (customErrorHandler) {
-      await customErrorHandler(error)
-    } else {
-      await handleNetworkError(error, context)
-    }
-    return null
-  } finally {
-    // 结束加载
-    if (loadingKey) {
-      loadingManager.stopLoading(loadingKey)
-    }
-  }
-}

+ 0 - 413
src/views/iot/rule/scene/utils/transform.ts

@@ -1,413 +0,0 @@
-/**
- * IoT 场景联动数据转换工具函数
- */
-import {
-  IotRuleScene,
-  TriggerConfig,
-  ActionConfig,
-  RuleSceneFormData,
-  TriggerFormData,
-  ActionFormData
-} from '@/api/iot/rule/scene/scene.types'
-import { generateUUID } from '@/utils'
-
-// TODO @puhui999:这些是不是放到对应的界面,会好一丢丢哈?
-
-/**
- * 创建默认的表单数据
- */
-export function createDefaultFormData(): RuleSceneFormData {
-  return {
-    name: '',
-    description: '',
-    status: 0, // TODO @puhui999:枚举值
-    triggers: [],
-    actions: []
-  }
-}
-
-/**
- * 创建默认的触发器数据
- */
-export function createDefaultTriggerData(): TriggerFormData {
-  return {
-    type: 2, // 默认为属性上报 TODO @puhui999:枚举值
-    productId: undefined,
-    deviceId: undefined,
-    identifier: undefined,
-    operator: undefined,
-    value: undefined,
-    cronExpression: undefined,
-    conditionGroups: []
-  }
-}
-
-/**
- * 创建默认的执行器数据
- */
-export function createDefaultActionData(): ActionFormData {
-  return {
-    type: 1, // 默认为属性设置 TODO @puhui999:枚举值
-    productId: undefined,
-    deviceId: undefined,
-    params: {},
-    alertConfigId: undefined
-  }
-}
-
-/**
- * 将表单数据转换为API请求格式
- */
-export function transformFormToApi(formData: RuleSceneFormData): IotRuleScene {
-  // TODO @puhui999:这个关注下
-  // 这里需要根据实际 API 结构进行转换
-  // 暂时返回基本结构
-  return {
-    id: formData.id,
-    name: formData.name,
-    description: formData.description,
-    status: Number(formData.status),
-    triggers: [], // 需要根据实际API结构转换
-    actions: [] // 需要根据实际API结构转换
-  } as IotRuleScene
-}
-
-/**
- * 将 API 响应数据转换为表单格式
- */
-export function transformApiToForm(apiData: IotRuleScene): RuleSceneFormData {
-  return {
-    ...apiData,
-    status: Number(apiData.status), // 确保状态为数字类型
-    triggers:
-      apiData.triggers?.map((trigger) => ({
-        ...trigger,
-        type: Number(trigger.type),
-        // 为每个触发器添加唯一标识符,解决组件索引重用问题
-        key: generateUUID()
-      })) || [],
-    actions:
-      apiData.actions?.map((action) => ({
-        ...action,
-        type: Number(action.type),
-        // 为每个执行器添加唯一标识符,解决组件索引重用问题
-        key: generateUUID()
-      })) || []
-  }
-}
-
-// TODO @puhui999:貌似没用到;
-/**
- * 创建默认的触发器配置
- */
-export function createDefaultTriggerConfig(type?: number): TriggerConfig {
-  const baseConfig: TriggerConfig = {
-    key: generateUUID(),
-    type: type || 2, // 默认为物模型属性上报
-    productKey: '',
-    deviceNames: [],
-    conditions: []
-  }
-
-  // 定时触发的默认配置
-  if (type === 100) {
-    return {
-      ...baseConfig,
-      cronExpression: '0 0 12 * * ?', // 默认每天中午12点
-      productKey: undefined,
-      deviceNames: undefined,
-      conditions: undefined
-    }
-  }
-
-  // 设备状态变更的默认配置
-  if (type === 1) {
-    return {
-      ...baseConfig,
-      conditions: undefined // 设备状态变更不需要条件
-    }
-  }
-
-  // 其他设备触发类型的默认配置
-  return {
-    ...baseConfig,
-    conditions: [
-      {
-        type: 'property',
-        identifier: 'set',
-        parameters: [
-          {
-            identifier: '',
-            operator: '=',
-            value: ''
-          }
-        ]
-      }
-    ]
-  }
-}
-
-// TODO @puhui999:貌似没用到;
-/**
- * 创建默认的执行器配置
- */
-export function createDefaultActionConfig(type?: number): ActionConfig {
-  const baseConfig: ActionConfig = {
-    key: generateUUID(),
-    type: type || 1 // 默认为设备属性设置
-  }
-
-  // 告警相关的默认配置
-  if (type === 100 || type === 101) {
-    return {
-      ...baseConfig,
-      alertConfigId: undefined
-    }
-  }
-
-  // 设备控制的默认配置
-  return {
-    ...baseConfig,
-    deviceControl: {
-      productKey: '',
-      deviceNames: [],
-      type: 'property',
-      identifier: 'set',
-      params: {}
-    }
-  }
-}
-
-// TODO @puhui999:全局已经有类似的
-/**
- * 深度克隆对象(用于避免引用问题)
- */
-export function deepClone<T>(obj: T): T {
-  if (obj === null || typeof obj !== 'object') {
-    return obj
-  }
-
-  if (obj instanceof Date) {
-    return new Date(obj.getTime()) as unknown as T
-  }
-
-  if (obj instanceof Array) {
-    return obj.map((item) => deepClone(item)) as unknown as T
-  }
-
-  if (typeof obj === 'object') {
-    const clonedObj = {} as T
-    for (const key in obj) {
-      if (obj.hasOwnProperty(key)) {
-        clonedObj[key] = deepClone(obj[key])
-      }
-    }
-    return clonedObj
-  }
-
-  return obj
-}
-
-// TODO @puhui999:貌似没用到;
-/**
- * 清理空值和无效数据
- */
-export function cleanFormData(data: IotRuleScene): IotRuleScene {
-  const cleaned = deepClone(data)
-
-  // 清理触发器数据
-  cleaned.triggers =
-    cleaned.triggers?.filter((trigger) => {
-      // 移除类型为空的触发器
-      if (!trigger.type) return false
-
-      // 定时触发器必须有CRON表达式
-      if (trigger.type === 100 && !trigger.cronExpression) return false
-
-      // 设备触发器必须有产品和设备
-      if (trigger.type !== 100 && (!trigger.productKey || !trigger.deviceNames?.length))
-        return false
-
-      return true
-    }) || []
-
-  // 清理执行器数据
-  cleaned.actions =
-    cleaned.actions?.filter((action) => {
-      // 移除类型为空的执行器
-      if (!action.type) return false
-
-      // 告警类型必须有告警配置ID
-      if ((action.type === 100 || action.type === 101) && !action.alertConfigId) return false
-
-      // 设备控制类型必须有完整的设备控制配置
-      if (
-        (action.type === 1 || action.type === 2) &&
-        (!action.deviceControl?.productKey ||
-          !action.deviceControl?.deviceNames?.length ||
-          !action.deviceControl?.identifier ||
-          !action.deviceControl?.params ||
-          Object.keys(action.deviceControl.params).length === 0)
-      ) {
-        return false
-      }
-
-      return true
-    }) || []
-
-  return cleaned
-}
-
-/**
- * 格式化CRON表达式显示
- */
-export function formatCronExpression(cron: string): string {
-  if (!cron) return ''
-
-  // 简单的CRON表达式解析和格式化
-  const parts = cron.trim().split(' ')
-  if (parts.length < 5) return cron
-
-  const [second, minute, hour] = parts
-
-  // 构建可读的描述
-  let description = ''
-
-  if (second === '0' && minute === '0') {
-    if (hour === '*') {
-      description = '每小时'
-    } else if (hour.includes('/')) {
-      const interval = hour.split('/')[1]
-      description = `每${interval}小时`
-    } else {
-      description = `每天${hour}点`
-    }
-  } else if (second === '0') {
-    if (minute === '*') {
-      description = '每分钟'
-    } else if (minute.includes('/')) {
-      const interval = minute.split('/')[1]
-      description = `每${interval}分钟`
-    } else {
-      description = `每小时第${minute}分钟`
-    }
-  } else {
-    if (second === '*') {
-      description = '每秒'
-    } else if (second.includes('/')) {
-      const interval = second.split('/')[1]
-      description = `每${interval}秒`
-    }
-  }
-
-  return description || cron
-}
-
-// TODO @puhui999:貌似没用到;
-/**
- * 验证并修复数据结构
- */
-export function validateAndFixData(data: IotRuleScene): IotRuleScene {
-  const fixed = deepClone(data)
-
-  // 确保必要字段存在
-  if (!fixed.triggers) fixed.triggers = []
-  if (!fixed.actions) fixed.actions = []
-
-  // 修复触发器数据
-  fixed.triggers = fixed.triggers.map((trigger) => {
-    const fixedTrigger = { ...trigger }
-
-    // 确保有key
-    if (!fixedTrigger.key) {
-      fixedTrigger.key = generateUUID()
-    }
-    // 定时触发器不需要产品和设备信息
-    if (fixedTrigger.type === 100) {
-      fixedTrigger.productKey = undefined
-      fixedTrigger.deviceNames = undefined
-      fixedTrigger.conditions = undefined
-    }
-
-    return fixedTrigger
-  })
-
-  // 修复执行器数据
-  fixed.actions = fixed.actions.map((action) => {
-    const fixedAction = { ...action }
-
-    // 确保有key
-    if (!fixedAction.key) {
-      fixedAction.key = generateUUID()
-    }
-
-    // 确保类型为数字
-    if (typeof fixedAction.type === 'string') {
-      fixedAction.type = Number(fixedAction.type)
-    }
-
-    // 修复设备控制参数字段名
-    if (fixedAction.deviceControl && 'data' in fixedAction.deviceControl) {
-      fixedAction.deviceControl.params = (fixedAction.deviceControl as any).data
-      delete (fixedAction.deviceControl as any).data
-    }
-
-    return fixedAction
-  })
-
-  return fixed
-}
-
-// TODO @puhui999:貌似没用到;
-/**
- * 比较两个场景联动规则是否相等(忽略key字段)
- */
-export function isRuleSceneEqual(a: IotRuleScene, b: IotRuleScene): boolean {
-  const cleanA = transformFormToApi(a)
-  const cleanB = transformFormToApi(b)
-
-  return JSON.stringify(cleanA) === JSON.stringify(cleanB)
-}
-
-/**
- * 获取场景联动规则的摘要信息
- */
-export function getRuleSceneSummary(ruleScene: IotRuleScene): {
-  triggerSummary: string[]
-  actionSummary: string[]
-} {
-  const triggerSummary =
-    ruleScene.triggers?.map((trigger) => {
-      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 || '')})`
-        default:
-          return '未知触发类型'
-      }
-    }) || []
-
-  const actionSummary =
-    ruleScene.actions?.map((action) => {
-      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 '告警恢复'
-        default:
-          return '未知执行类型'
-      }
-    }) || []
-  return { triggerSummary, actionSummary }
-}

+ 9 - 91
src/views/iot/rule/scene/utils/validation.ts

@@ -1,15 +1,11 @@
 /**
  * IoT 场景联动表单验证工具函数
  */
-import {
-  FormValidationRules,
-  IotRuleScene,
-  TriggerConfig,
-  ActionConfig
-} from '@/api/iot/rule/scene/scene.types'
+import { FormValidationRules, TriggerConfig, ActionConfig } from '@/api/iot/rule/scene/scene.types'
 import {
   IotRuleSceneTriggerTypeEnum,
-  IotRuleSceneActionTypeEnum
+  IotRuleSceneActionTypeEnum,
+  CommonStatusEnum
 } from '@/api/iot/rule/scene/scene.types'
 
 /** 基础表单验证规则 */
@@ -20,7 +16,12 @@ export const getBaseValidationRules = (): FormValidationRules => ({
   ],
   status: [
     { required: true, message: '场景状态不能为空', trigger: 'change' },
-    { type: 'enum', enum: [0, 1], message: '状态值必须为0或1', trigger: 'change' }
+    {
+      type: 'enum',
+      enum: [CommonStatusEnum.ENABLE, CommonStatusEnum.DISABLE],
+      message: '状态值必须为启用或禁用',
+      trigger: 'change'
+    }
   ],
   description: [
     { type: 'string', max: 200, message: '场景描述不能超过200个字符', trigger: 'blur' }
@@ -179,86 +180,3 @@ export function validateActionConfig(action: ActionConfig): { valid: boolean; me
 
   return { valid: false, message: '未知的执行类型' }
 }
-
-// TODO @puhui999:貌似没用到?
-/** 验证完整的场景联动规则 */
-export function validateRuleScene(ruleScene: IotRuleScene): { valid: boolean; message?: string } {
-  // 基础字段验证
-  if (!ruleScene.name || ruleScene.name.trim().length === 0) {
-    return { valid: false, message: '场景名称不能为空' }
-  }
-  if (ruleScene.status !== 0 && ruleScene.status !== 1) {
-    return { valid: false, message: '场景状态必须为0或1' }
-  }
-  if (!ruleScene.triggers || ruleScene.triggers.length === 0) {
-    return { valid: false, message: '至少需要一个触发器' }
-  }
-  if (!ruleScene.actions || ruleScene.actions.length === 0) {
-    return { valid: false, message: '至少需要一个执行器' }
-  }
-  // 验证每个触发器
-  for (let i = 0; i < ruleScene.triggers.length; i++) {
-    const triggerResult = validateTriggerConfig(ruleScene.triggers[i])
-    if (!triggerResult.valid) {
-      return { valid: false, message: `触发器${i + 1}: ${triggerResult.message}` }
-    }
-  }
-  // 验证每个执行器
-  for (let i = 0; i < ruleScene.actions.length; i++) {
-    const actionResult = validateActionConfig(ruleScene.actions[i])
-    if (!actionResult.valid) {
-      return { valid: false, message: `执行器${i + 1}: ${actionResult.message}` }
-    }
-  }
-  return { valid: true }
-}
-
-// TODO @puhui999:下面 getOperatorOptions、getTriggerTypeOptions、getActionTypeOptions 三个貌似没用到?如果用到的话,要不放到 yudao-ui-admin-vue3/src/views/iot/utils/constants.ts 里
-
-/**
- * 获取操作符选项
- */
-export function getOperatorOptions() {
-  // TODO @puhui999:这个能不能从枚举计算出来,减少后续添加枚举的维护
-  return [
-    { value: '=', label: '等于' },
-    { value: '!=', label: '不等于' },
-    { value: '>', label: '大于' },
-    { value: '>=', label: '大于等于' },
-    { value: '<', label: '小于' },
-    { value: '<=', label: '小于等于' },
-    { value: 'in', label: '包含' },
-    { value: 'not in', label: '不包含' },
-    { value: 'between', label: '介于之间' },
-    { value: 'not between', label: '不在之间' },
-    { value: 'like', label: '字符串匹配' },
-    { value: 'not null', label: '非空' }
-  ]
-}
-
-/**
- * 获取触发类型选项
- */
-export function getTriggerTypeOptions() {
-  // TODO @puhui999:这个能不能从枚举计算出来,减少后续添加枚举的维护
-  return [
-    { value: IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE, label: '设备上下线变更' },
-    { value: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST, label: '物模型属性上报' },
-    { value: IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST, label: '设备事件上报' },
-    { value: IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE, label: '设备服务调用' },
-    { value: IotRuleSceneTriggerTypeEnum.TIMER, label: '定时触发' }
-  ]
-}
-
-/**
- * 获取执行类型选项
- */
-export function getActionTypeOptions() {
-  // TODO @puhui999:这个能不能从枚举计算出来,减少后续添加枚举的维护
-  return [
-    { value: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET, label: '设备属性设置' },
-    { value: IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE, label: '设备服务调用' },
-    { value: IotRuleSceneActionTypeEnum.ALERT_TRIGGER, label: '告警触发' },
-    { value: IotRuleSceneActionTypeEnum.ALERT_RECOVER, label: '告警恢复' }
-  ]
-}