From 1b6ba339217a9587c20b623429df6cb4e286fcc3 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Thu, 17 Jul 2025 21:31:12 +0800 Subject: [PATCH 1/5] =?UTF-8?q?perf=EF=BC=9A=E3=80=90IoT=20=E7=89=A9?= =?UTF-8?q?=E8=81=94=E7=BD=91=E3=80=91=E5=9C=BA=E6=99=AF=E8=81=94=E5=8A=A8?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/iot/rule/scene/scene.types.ts | 143 ++- .../device/detail/DeviceDetailsMessage.vue | 5 +- src/views/iot/ota/task/OtaTaskList.vue | 5 +- src/views/iot/rule/scene/111index.vue | 192 +++ ...35\350\267\257\346\226\207\346\241\243.md" | 1101 +++++++++++++++++ .../rule/scene/components/RuleSceneForm.vue | 315 +++++ .../components/action/ActionExecutor.vue | 2 +- .../scene/components/configs/AlertConfig.vue | 288 +++++ .../components/configs/ConditionConfig.vue | 260 ++++ .../configs/ConditionGroupConfig.vue | 331 +++++ .../configs/DeviceControlConfig.vue | 173 +++ .../configs/DeviceTriggerConfig.vue | 347 ++++++ .../components/configs/TimerTriggerConfig.vue | 142 +++ .../scene/components/inputs/CronBuilder.vue | 233 ++++ .../scene/components/inputs/CronInput.vue | 142 +++ .../components/inputs/DescriptionInput.vue | 184 +++ .../scene/components/inputs/NameInput.vue | 162 +++ .../scene/components/inputs/StatusRadio.vue | 185 +++ .../scene/components/inputs/ValueInput.vue | 406 ++++++ .../components/previews/ActionPreview.vue | 127 ++ .../components/previews/ConfigPreview.vue | 65 + .../previews/NextExecutionPreview.vue | 226 ++++ .../components/previews/TriggerPreview.vue | 131 ++ .../components/previews/ValidationResult.vue | 120 ++ .../components/sections/ActionSection.vue | 390 ++++++ .../components/sections/BasicInfoSection.vue | 110 ++ .../components/sections/PreviewSection.vue | 183 +++ .../components/sections/TriggerSection.vue | 395 ++++++ .../selectors/ActionTypeSelector.vue | 145 +++ .../components/selectors/OperatorSelector.vue | 275 ++++ .../selectors/ProductDeviceSelector.vue | 325 +++++ .../components/selectors/PropertySelector.vue | 341 +++++ .../selectors/TriggerTypeSelector.vue | 264 ++++ src/views/iot/rule/scene/index.vue | 756 ++++++++--- .../iot/rule/scene/utils/errorHandler.ts | 548 ++++++++ src/views/iot/rule/scene/utils/transform.ts | 406 ++++++ src/views/iot/rule/scene/utils/validation.ts | 278 +++++ 37 files changed, 9524 insertions(+), 177 deletions(-) create mode 100644 src/views/iot/rule/scene/111index.vue create mode 100644 "src/views/iot/rule/scene/IoT\345\234\272\346\231\257\350\201\224\345\212\250\350\247\204\345\210\231\350\241\250\345\215\225\350\256\276\350\256\241\346\200\235\350\267\257\346\226\207\346\241\243.md" create mode 100644 src/views/iot/rule/scene/components/RuleSceneForm.vue create mode 100644 src/views/iot/rule/scene/components/configs/AlertConfig.vue create mode 100644 src/views/iot/rule/scene/components/configs/ConditionConfig.vue create mode 100644 src/views/iot/rule/scene/components/configs/ConditionGroupConfig.vue create mode 100644 src/views/iot/rule/scene/components/configs/DeviceControlConfig.vue create mode 100644 src/views/iot/rule/scene/components/configs/DeviceTriggerConfig.vue create mode 100644 src/views/iot/rule/scene/components/configs/TimerTriggerConfig.vue create mode 100644 src/views/iot/rule/scene/components/inputs/CronBuilder.vue create mode 100644 src/views/iot/rule/scene/components/inputs/CronInput.vue create mode 100644 src/views/iot/rule/scene/components/inputs/DescriptionInput.vue create mode 100644 src/views/iot/rule/scene/components/inputs/NameInput.vue create mode 100644 src/views/iot/rule/scene/components/inputs/StatusRadio.vue create mode 100644 src/views/iot/rule/scene/components/inputs/ValueInput.vue create mode 100644 src/views/iot/rule/scene/components/previews/ActionPreview.vue create mode 100644 src/views/iot/rule/scene/components/previews/ConfigPreview.vue create mode 100644 src/views/iot/rule/scene/components/previews/NextExecutionPreview.vue create mode 100644 src/views/iot/rule/scene/components/previews/TriggerPreview.vue create mode 100644 src/views/iot/rule/scene/components/previews/ValidationResult.vue create mode 100644 src/views/iot/rule/scene/components/sections/ActionSection.vue create mode 100644 src/views/iot/rule/scene/components/sections/BasicInfoSection.vue create mode 100644 src/views/iot/rule/scene/components/sections/PreviewSection.vue create mode 100644 src/views/iot/rule/scene/components/sections/TriggerSection.vue create mode 100644 src/views/iot/rule/scene/components/selectors/ActionTypeSelector.vue create mode 100644 src/views/iot/rule/scene/components/selectors/OperatorSelector.vue create mode 100644 src/views/iot/rule/scene/components/selectors/ProductDeviceSelector.vue create mode 100644 src/views/iot/rule/scene/components/selectors/PropertySelector.vue create mode 100644 src/views/iot/rule/scene/components/selectors/TriggerTypeSelector.vue create mode 100644 src/views/iot/rule/scene/utils/errorHandler.ts create mode 100644 src/views/iot/rule/scene/utils/transform.ts create mode 100644 src/views/iot/rule/scene/utils/validation.ts diff --git a/src/api/iot/rule/scene/scene.types.ts b/src/api/iot/rule/scene/scene.types.ts index 0e4158296..bf0cffe83 100644 --- a/src/api/iot/rule/scene/scene.types.ts +++ b/src/api/iot/rule/scene/scene.types.ts @@ -50,6 +50,19 @@ const IotAlertConfigReceiveTypeEnum = { NOTIFY: 3 // 通知 } as const +// 设备状态枚举 +const DeviceStateEnum = { + INACTIVE: 0, // 未激活 + ONLINE: 1, // 在线 + OFFLINE: 2 // 离线 +} as const + +// 通用状态枚举 +const CommonStatusEnum = { + ENABLE: 0, // 开启 + DISABLE: 1 // 关闭 +} as const + // 基础接口 interface TenantBaseDO { createTime?: Date // 创建时间 @@ -62,10 +75,10 @@ interface TenantBaseDO { // 触发条件参数 interface TriggerConditionParameter { - identifier0: string // 标识符(事件、服务) - identifier: string // 标识符(属性) - operator: string // 操作符 - value: string // 比较值 + identifier0?: string // 标识符(事件、服务) + identifier?: string // 标识符(属性) + operator: string // 操作符(必填) + value: string // 比较值(必填,多值用逗号分隔) } // 触发条件 @@ -77,39 +90,104 @@ interface TriggerCondition { // 触发器配置 interface TriggerConfig { - key: any // 解决组件索引重用 - type: number // 触发类型 - productKey: string // 产品标识 - deviceNames: string[] // 设备名称数组 - conditions?: TriggerCondition[] // 触发条件数组 - cronExpression?: string // CRON 表达式 + key?: string // 组件唯一标识符,用于解决索引重用问题 + type: number // 触发类型(必填) + productKey?: string // 产品标识(设备触发时必填) + deviceNames?: string[] // 设备名称数组(设备触发时必填) + conditions?: TriggerCondition[] // 触发条件数组(设备触发时必填) + cronExpression?: string // CRON表达式(定时触发时必填) } // 执行设备控制 interface ActionDeviceControl { - productKey: string // 产品标识 - deviceNames: string[] // 设备名称数组 - type: string // 消息类型 - identifier: string // 消息标识符 - data: Record // 具体数据 + productKey: string // 产品标识(必填) + deviceNames: string[] // 设备名称数组(必填) + type: string // 消息类型(必填) + identifier: string // 消息标识符(必填) + params: Record // 参数对象(必填)- 统一使用 params 字段 } // 执行器配置 interface ActionConfig { - key: any // 解决组件索引重用 TODO @puhui999:看看有没更好的解决方案呢。 - type: number // 执行类型 - deviceControl?: ActionDeviceControl // 设备控制 - alertConfigId?: number // 告警配置ID(告警恢复时需要) + key?: string // 组件唯一标识符,用于解决索引重用问题 + type: number // 执行类型(必填) + deviceControl?: ActionDeviceControl // 设备控制(设备控制时必填) + alertConfigId?: number // 告警配置ID(告警恢复时必填) +} + +// 表单数据接口 +interface RuleSceneFormData { + id?: number + name: string + description?: string + status: number + triggers: TriggerFormData[] + actions: ActionFormData[] +} + +interface TriggerFormData { + type: number + productId?: number + deviceId?: number + identifier?: string + operator?: string + value?: string + cronExpression?: string + conditionGroups?: ConditionGroupFormData[] +} + +interface ActionFormData { + type: number + productId?: number + deviceId?: number + params?: Record + alertConfigId?: number +} + +interface ConditionGroupFormData { + conditions: ConditionFormData[] + logicOperator: 'AND' | 'OR' +} + +interface ConditionFormData { + type: number + productId: number + deviceId: number + identifier: string + operator: string + param: string } // 主接口 interface IotRuleScene extends TenantBaseDO { - id: number // 场景编号 - name: string // 场景名称 - description: string // 场景描述 - status: number // 场景状态 - triggers: TriggerConfig[] // 触发器数组 - actions: ActionConfig[] // 执行器数组 + id?: number // 场景编号(新增时为空) + name: string // 场景名称(必填) + description?: string // 场景描述(可选) + status: number // 场景状态:0-开启,1-关闭 + triggers: TriggerConfig[] // 触发器数组(必填,至少一个) + actions: ActionConfig[] // 执行器数组(必填,至少一个) +} + +// 工具类型 +type TriggerType = (typeof IotRuleSceneTriggerTypeEnum)[keyof typeof IotRuleSceneTriggerTypeEnum] +type ActionType = (typeof IotRuleSceneActionTypeEnum)[keyof typeof IotRuleSceneActionTypeEnum] +type MessageType = (typeof IotDeviceMessageTypeEnum)[keyof typeof IotDeviceMessageTypeEnum] +type OperatorType = + (typeof IotRuleSceneTriggerConditionParameterOperatorEnum)[keyof typeof IotRuleSceneTriggerConditionParameterOperatorEnum]['value'] + +// 表单验证规则类型 +interface ValidationRule { + required?: boolean + message?: string + trigger?: string | string[] + type?: string + min?: number + max?: number + enum?: any[] +} + +interface FormValidationRules { + [key: string]: ValidationRule[] } export { @@ -119,10 +197,23 @@ export { TriggerConditionParameter, ActionConfig, ActionDeviceControl, + RuleSceneFormData, + TriggerFormData, + ActionFormData, + ConditionGroupFormData, + ConditionFormData, IotRuleSceneTriggerTypeEnum, IotRuleSceneActionTypeEnum, IotDeviceMessageTypeEnum, IotDeviceMessageIdentifierEnum, IotRuleSceneTriggerConditionParameterOperatorEnum, - IotAlertConfigReceiveTypeEnum + IotAlertConfigReceiveTypeEnum, + DeviceStateEnum, + CommonStatusEnum, + TriggerType, + ActionType, + MessageType, + OperatorType, + ValidationRule, + FormValidationRules } diff --git a/src/views/iot/device/device/detail/DeviceDetailsMessage.vue b/src/views/iot/device/device/detail/DeviceDetailsMessage.vue index 637cb7e75..aebb03bf0 100644 --- a/src/views/iot/device/device/detail/DeviceDetailsMessage.vue +++ b/src/views/iot/device/device/detail/DeviceDetailsMessage.vue @@ -116,8 +116,8 @@ const queryParams = reactive({ const loading = ref(false) const total = ref(0) const list = ref([]) -const autoRefresh = ref(false) -let autoRefreshTimer: any = null // TODO @super:autoRefreshEnable,autoRefreshTimer;对应上 +const autoRefresh = ref(false) // 自动刷新开关 +let autoRefreshTimer: any = null // 自动刷新定时器 // 消息方法选项 const methodOptions = computed(() => { @@ -172,6 +172,7 @@ watch( onBeforeUnmount(() => { if (autoRefreshTimer) { clearInterval(autoRefreshTimer) + autoRefreshTimer = null } }) diff --git a/src/views/iot/ota/task/OtaTaskList.vue b/src/views/iot/ota/task/OtaTaskList.vue index ee3748290..f6c3a6be5 100644 --- a/src/views/iot/ota/task/OtaTaskList.vue +++ b/src/views/iot/ota/task/OtaTaskList.vue @@ -7,15 +7,14 @@ ref="queryFormRef" :inline="true" label-width="68px" + @submit.prevent > 新增 - - - + + + + + + + + + + + + + + + + + 搜索 + 重置 + + 新增 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/src/views/iot/rule/scene/IoT\345\234\272\346\231\257\350\201\224\345\212\250\350\247\204\345\210\231\350\241\250\345\215\225\350\256\276\350\256\241\346\200\235\350\267\257\346\226\207\346\241\243.md" "b/src/views/iot/rule/scene/IoT\345\234\272\346\231\257\350\201\224\345\212\250\350\247\204\345\210\231\350\241\250\345\215\225\350\256\276\350\256\241\346\200\235\350\267\257\346\226\207\346\241\243.md" new file mode 100644 index 000000000..4563f2483 --- /dev/null +++ "b/src/views/iot/rule/scene/IoT\345\234\272\346\231\257\350\201\224\345\212\250\350\247\204\345\210\231\350\241\250\345\215\225\350\256\276\350\256\241\346\200\235\350\267\257\346\226\207\346\241\243.md" @@ -0,0 +1,1101 @@ +# IoT场景联动规则表单设计思路文档 + +## 概述 + +本文档详细描述了IoT场景联动规则表单的设计思路,包括表单结构、组件设计、数据流转和用户交互逻辑。通过Mermaid图直观展示各个组件之间的关系和数据流向。 + +## 表单整体架构设计 + +### 1. 表单主体结构 + +表单采用分步骤设计,包含以下主要部分: + +- **基础信息配置**:场景名称、描述、状态 +- **触发器配置**:设备触发或定时触发 +- **执行器配置**:设备控制或告警配置 +- **预览与保存**:配置预览和最终保存 + +### 2. 组件层次结构图 + +```mermaid +graph TB + A[RuleSceneForm
主表单组件] --> B[BasicInfoSection
基础信息] + A --> C[TriggerSection
触发器配置] + A --> D[ActionSection
执行器配置] + A --> E[PreviewSection
预览区域] + + %% 基础信息组件 + B --> B1[NameInput
场景名称输入] + B --> B2[DescriptionInput
场景描述输入] + B --> B3[StatusRadio
状态选择] + + %% 触发器配置组件 + C --> C1[TriggerTypeSelector
触发类型选择器] + C --> C2[DeviceTriggerConfig
设备触发配置] + C --> C3[TimerTriggerConfig
定时触发配置] + + %% 设备触发配置子组件 + C2 --> C21[ProductSelector
产品选择器] + C2 --> C22[DeviceSelector
设备选择器] + C2 --> C23[PropertySelector
属性选择器] + C2 --> C24[OperatorSelector
操作符选择器] + C2 --> C25[ValueInput
值输入] + C2 --> C26[ConditionGroupConfig
条件分组配置] + + %% 定时触发配置子组件 + C3 --> C31[CronInput
CRON表达式输入] + C3 --> C32[CronBuilder
可视化CRON构建器] + C3 --> C33[NextExecutionPreview
下次执行时间预览] + + %% 执行器配置组件 + D --> D1[ActionTypeSelector
执行类型选择器] + D --> D2[DeviceControlConfig
设备控制配置] + D --> D3[AlertConfig
告警配置] + + %% 设备控制配置子组件 + D2 --> D21[TargetProductSelector
目标产品选择器] + D2 --> D22[TargetDeviceSelector
目标设备选择器] + D2 --> D23[ControlTypeSelector
控制类型选择器] + D2 --> D24[ParamsConfig
参数配置] + + %% 告警配置子组件 + D3 --> D31[AlertConfigSelector
告警配置选择器] + + %% 预览区域组件 + E --> E1[ConfigPreview
配置预览] + E --> E2[ValidationResult
验证结果] + E --> E3[SaveButton
保存按钮] + + %% 样式定义 + classDef mainComponent fill:#e1f5fe,stroke:#01579b,stroke-width:2px + classDef sectionComponent fill:#f3e5f5,stroke:#4a148c,stroke-width:2px + classDef subComponent fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px + + class A mainComponent + class B,C,D,E sectionComponent + class B1,B2,B3,C1,C2,C3,C21,C22,C23,C24,C25,C26,C31,C32,C33,D1,D2,D3,D21,D22,D23,D24,D31,E1,E2,E3 subComponent +``` + +### 3. 组件层次结构文本 + +```text +RuleSceneForm (主表单) +├── BasicInfoSection (基础信息) +│ ├── NameInput (场景名称输入) +│ ├── DescriptionInput (场景描述输入) +│ └── StatusRadio (状态选择) +├── TriggerSection (触发器配置) +│ ├── TriggerTypeSelector (触发类型选择) +│ ├── DeviceTriggerConfig (设备触发配置) +│ │ ├── ProductSelector (产品选择器) +│ │ ├── DeviceSelector (设备选择器) +│ │ ├── PropertySelector (属性选择器) +│ │ ├── OperatorSelector (操作符选择器) +│ │ ├── ValueInput (值输入) +│ │ └── ConditionGroupConfig (条件分组配置) +│ └── TimerTriggerConfig (定时触发配置) +│ ├── CronInput (CRON表达式输入) +│ ├── CronBuilder (可视化CRON构建器) +│ └── NextExecutionPreview (下次执行时间预览) +├── ActionSection (执行器配置) +│ ├── ActionTypeSelector (执行类型选择) +│ ├── DeviceControlConfig (设备控制配置) +│ │ ├── TargetProductSelector (目标产品选择器) +│ │ ├── TargetDeviceSelector (目标设备选择器) +│ │ ├── ControlTypeSelector (控制类型选择器) +│ │ └── ParamsConfig (参数配置) +│ └── AlertConfig (告警配置) +│ └── AlertConfigSelector (告警配置选择器) +└── PreviewSection (预览区域) + ├── ConfigPreview (配置预览) + ├── ValidationResult (验证结果) + └── SaveButton (保存按钮) +``` + +## 表单数据结构设计 + +### 1. 表单数据模型结构图 + +```mermaid +classDiagram + class RuleSceneFormData { + +number id? + +string name + +string description? + +number status + +TriggerFormData[] triggers + +ActionFormData[] actions + +validateForm() boolean + +toApiFormat() ApiRequestData + } + + class TriggerFormData { + +number type + +number productId? + +number deviceId? + +string identifier? + +string operator? + +string value? + +string cronExpression? + +ConditionGroupFormData[] conditionGroups? + +validateTrigger() boolean + +isDeviceTrigger() boolean + +isTimerTrigger() boolean + } + + class ActionFormData { + +number type + +number productId? + +number deviceId? + +Record params? + +number alertConfigId? + +validateAction() boolean + +isDeviceAction() boolean + +isAlertAction() boolean + } + + class ConditionGroupFormData { + +ConditionFormData[] conditions + +string logicOperator + +validateGroup() boolean + } + + class ConditionFormData { + +number type + +number productId + +number deviceId + +string identifier + +string operator + +string param + +validateCondition() boolean + } + + class TriggerTypeEnum { + <> + DEVICE_STATE_UPDATE: 1 + DEVICE_PROPERTY_POST: 2 + DEVICE_EVENT_POST: 3 + DEVICE_SERVICE_INVOKE: 4 + TIMER: 100 + } + + class ActionTypeEnum { + <> + DEVICE_PROPERTY_SET: 1 + DEVICE_SERVICE_INVOKE: 2 + ALERT_TRIGGER: 100 + ALERT_RECOVER: 101 + } + + class OperatorEnum { + <> + EQUALS: "=" + NOT_EQUALS: "!=" + GREATER_THAN: ">" + LESS_THAN: "<" + IN: "in" + BETWEEN: "between" + } + + RuleSceneFormData "1" --> "*" TriggerFormData : contains + RuleSceneFormData "1" --> "*" ActionFormData : contains + TriggerFormData "1" --> "*" ConditionGroupFormData : contains + ConditionGroupFormData "1" --> "*" ConditionFormData : contains + TriggerFormData --> TriggerTypeEnum : uses + ActionFormData --> ActionTypeEnum : uses + ConditionFormData --> OperatorEnum : uses +``` + +### 2. 表单数据模型代码 + +```typescript +interface RuleSceneFormData { + // 基础信息 + id?: number; + name: string; + description?: string; + status: number; + + // 触发器配置 + triggers: TriggerFormData[]; + + // 执行器配置 + actions: ActionFormData[]; +} + +interface TriggerFormData { + type: number; + productId?: number; + deviceId?: number; + identifier?: string; + operator?: string; + value?: string; + cronExpression?: string; + conditionGroups?: ConditionGroupFormData[]; +} + +interface ActionFormData { + type: number; + productId?: number; + deviceId?: number; + params?: Record; + alertConfigId?: number; +} + +interface ConditionGroupFormData { + conditions: ConditionFormData[]; + logicOperator: 'AND' | 'OR'; +} + +interface ConditionFormData { + type: number; + productId: number; + deviceId: number; + identifier: string; + operator: string; + param: string; +} +``` + +### 2. 表单验证规则 + +```typescript +const validationRules = { + name: [ + { required: true, message: '场景名称不能为空' }, + { max: 50, message: '场景名称不能超过50个字符' } + ], + status: [ + { required: true, message: '场景状态不能为空' }, + { type: 'enum', enum: [0, 1], message: '状态值必须为0或1' } + ], + triggers: [ + { required: true, message: '触发器配置不能为空' }, + { type: 'array', min: 1, message: '至少需要一个触发器' } + ], + actions: [ + { required: true, message: '执行器配置不能为空' }, + { type: 'array', min: 1, message: '至少需要一个执行器' } + ] +}; +``` + +## 核心组件设计 + +### 1. 基础信息组件 (BasicInfoSection) + +```vue + +``` + +### 2. 触发器配置组件 (TriggerSection) + +```vue + +``` + +### 3. 执行器配置组件 (ActionSection) + +```vue + +``` + +## 表单交互流程设计 + +### 1. 表单初始化流程图 + +```mermaid +flowchart TD + A[页面加载] --> B[初始化表单数据结构] + B --> C[获取基础数据] + C --> C1[加载产品列表] + C --> C2[加载告警配置列表] + C --> C3[加载用户权限信息] + C1 --> D[表单渲染] + C2 --> D + C3 --> D + D --> E[建立双向数据绑定] + E --> F[表单就绪] + + %% 错误处理 + C --> G{数据加载失败?} + G -->|是| H[显示错误信息] + G -->|否| D + H --> I[提供重试选项] + I --> C +``` + +### 2. 触发器配置流程图 + +```mermaid +flowchart TD + A[开始配置触发器] --> B[选择触发类型] + B --> C{触发类型} + + %% 设备触发分支 + C -->|设备触发| D[设备触发配置] + D --> D1[选择产品] + D1 --> D2[加载设备列表] + D2 --> D3[选择设备] + D3 --> D4[加载物模型] + D4 --> D5[选择属性/事件] + D5 --> D6[选择操作符] + D6 --> D7[输入比较值] + D7 --> D8[配置条件分组] + D8 --> E[触发器配置完成] + + %% 定时触发分支 + C -->|定时触发| F[定时触发配置] + F --> F1[输入CRON表达式] + F1 --> F2{表达式格式} + F2 -->|正确| F3[显示下次执行时间] + F2 -->|错误| F4[显示错误提示] + F4 --> F5[提供可视化编辑器] + F5 --> F1 + F3 --> E + + %% 验证 + E --> G[验证触发器配置] + G --> H{验证通过?} + H -->|是| I[保存触发器配置] + H -->|否| J[显示验证错误] + J --> D +``` + +### 3. 执行器配置流程图 + +```mermaid +flowchart TD + A[开始配置执行器] --> B[选择执行类型] + B --> C{执行类型} + + %% 设备控制分支 + C -->|设备控制| D[设备控制配置] + D --> D1[选择目标产品] + D1 --> D2[加载目标设备列表] + D2 --> D3[选择目标设备] + D3 --> D4[选择控制类型] + D4 --> D5{控制类型} + D5 -->|属性设置| D6[配置属性参数] + D5 -->|服务调用| D7[配置服务参数] + D6 --> E[执行器配置完成] + D7 --> E + + %% 告警分支 + C -->|告警触发/恢复| F[告警配置] + F --> F1[选择告警配置项] + F1 --> F2[配置告警参数] + F2 --> E + + %% 验证 + E --> G[验证执行器配置] + G --> H{验证通过?} + H -->|是| I[保存执行器配置] + H -->|否| J[显示验证错误] + J --> D +``` + +### 4. 表单提交流程图 + +```mermaid +flowchart TD + A[用户点击保存] --> B[表单验证] + B --> C{验证通过?} + C -->|否| D[显示验证错误] + D --> E[用户修正错误] + E --> B + + C -->|是| F[数据转换] + F --> G[转换为API格式] + G --> H[提交请求] + H --> I{请求成功?} + + I -->|是| J[显示成功消息] + J --> K[跳转到列表页面] + + I -->|否| L[显示错误消息] + L --> M[提供重试选项] + M --> H + + %% 加载状态 + H --> N[显示加载状态] + N --> I +``` + +### 5. 数据流转图 + +```mermaid +flowchart LR + A[用户输入] --> B[表单组件] + B --> C[数据验证] + C --> D[状态管理] + D --> E[API调用] + E --> F[后端处理] + F --> G[数据库存储] + + %% 反向数据流 + G --> H[响应数据] + H --> I[状态更新] + I --> J[UI更新] + J --> K[用户反馈] + + %% 错误处理流 + C --> L{验证失败?} + L -->|是| M[错误提示] + M --> A + + E --> N{请求失败?} + N -->|是| O[错误处理] + O --> K +``` + +## 组件状态管理设计 + +### 1. 状态管理架构图 + +```mermaid +graph TB + A[全局状态管理] --> B[表单状态] + A --> C[UI状态] + A --> D[数据缓存状态] + + %% 表单状态 + B --> B1[formData
表单数据] + B --> B2[validationErrors
验证错误] + B --> B3[isDirty
数据变更标识] + B --> B4[isSubmitting
提交状态] + + %% UI状态 + C --> C1[loading
加载状态] + C --> C2[activeStep
当前步骤] + C --> C3[expandedSections
展开的区域] + C --> C4[modalVisible
弹窗显示状态] + + %% 数据缓存状态 + D --> D1[productList
产品列表] + D --> D2[deviceList
设备列表] + D --> D3[thingModelList
物模型列表] + D --> D4[alertConfigList
告警配置列表] + + %% 状态操作 + E[状态操作] --> E1[updateFormData
更新表单数据] + E --> E2[validateForm
验证表单] + E --> E3[resetForm
重置表单] + E --> E4[submitForm
提交表单] + + %% 样式定义 + classDef stateClass fill:#e3f2fd,stroke:#1976d2,stroke-width:2px + classDef actionClass fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px + + class A,B,C,D stateClass + class E,E1,E2,E3,E4 actionClass +``` + +### 2. 组件通信图 + +```mermaid +sequenceDiagram + participant U as User + participant F as FormComponent + participant T as TriggerSection + participant A as ActionSection + participant S as StateManager + participant API as BackendAPI + + U->>F: 填写基础信息 + F->>S: updateFormData(basicInfo) + S-->>F: 状态更新完成 + + U->>T: 配置触发器 + T->>S: updateTriggers(triggerData) + S->>API: loadDeviceList(productId) + API-->>S: 返回设备列表 + S-->>T: 更新设备选项 + + U->>A: 配置执行器 + A->>S: updateActions(actionData) + S-->>A: 状态更新完成 + + U->>F: 点击保存 + F->>S: validateForm() + S-->>F: 验证结果 + + alt 验证通过 + F->>S: submitForm() + S->>API: saveRuleScene(formData) + API-->>S: 保存结果 + S-->>F: 显示成功消息 + else 验证失败 + S-->>F: 显示错误信息 + end +``` + +### 3. 数据流向图 + +```mermaid +flowchart LR + A[用户操作] --> B[组件事件] + B --> C[状态更新] + C --> D[数据验证] + D --> E{验证通过?} + + E -->|是| F[更新状态] + E -->|否| G[显示错误] + + F --> H[触发副作用] + H --> I[API调用] + I --> J[更新缓存] + J --> K[UI重新渲染] + + G --> L[用户修正] + L --> A + + %% 样式 + classDef processClass fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px + classDef decisionClass fill:#fff3e0,stroke:#f57c00,stroke-width:2px + classDef errorClass fill:#ffebee,stroke:#d32f2f,stroke-width:2px + + class A,B,C,D,F,H,I,J,K,L processClass + class E decisionClass + class G errorClass +``` + +## 用户体验优化 + +### 1. 智能提示和帮助 + +- **字段说明**:为复杂字段提供详细说明和示例 +- **实时验证**:输入时实时验证数据格式 +- **智能推荐**:根据已选择的产品推荐相关设备 +- **预览功能**:实时预览配置效果 + +### 2. 错误处理和反馈 + +- **表单验证**:清晰的错误提示信息 +- **数据加载**:加载状态和错误重试机制 +- **保存反馈**:明确的成功/失败反馈 + +### 3. 操作便利性 + +- **批量操作**:支持批量添加/删除触发器和执行器 +- **模板功能**:提供常用场景模板 +- **导入导出**:支持配置的导入和导出 + +## 响应式设计考虑 + +### 1. 移动端适配 + +- **布局调整**:在小屏幕上采用垂直布局 +- **操作优化**:增大点击区域,优化触摸操作 +- **内容精简**:在移动端隐藏非必要信息 + +### 2. 不同屏幕尺寸适配 + +- **大屏幕**:充分利用空间,并排显示更多内容 +- **中等屏幕**:平衡内容密度和可读性 +- **小屏幕**:优先显示核心功能 + +## 性能优化策略 + +### 1. 组件懒加载 + +```javascript +// 懒加载复杂组件 +const DeviceTriggerConfig = defineAsyncComponent(() => + import('./components/DeviceTriggerConfig.vue') +); +``` + +### 2. 数据缓存 + +```javascript +// 缓存产品和设备数据 +const productCache = new Map(); +const deviceCache = new Map(); +``` + +### 3. 防抖处理 + +```javascript +// 搜索防抖 +const debouncedSearch = debounce(searchDevices, 300); +``` + +## 可访问性设计 + +### 1. 键盘导航 + +- 支持Tab键在表单元素间导航 +- 提供快捷键操作 + +### 2. 屏幕阅读器支持 + +- 为表单元素提供适当的标签 +- 使用ARIA属性增强可访问性 + +### 3. 颜色和对比度 + +- 确保足够的颜色对比度 +- 不仅依赖颜色传达信息 + +## 表单验证策略 + +### 1. 验证层次结构图 + +```mermaid +graph TB + A[表单验证] --> B[字段级验证] + A --> C[组件级验证] + A --> D[表单级验证] + A --> E[业务级验证] + + %% 字段级验证 + B --> B1[必填验证] + B --> B2[格式验证] + B --> B3[长度验证] + B --> B4[类型验证] + + %% 组件级验证 + C --> C1[触发器验证] + C --> C2[执行器验证] + C --> C3[条件组合验证] + + %% 表单级验证 + D --> D1[数据完整性验证] + D --> D2[逻辑一致性验证] + D --> D3[依赖关系验证] + + %% 业务级验证 + E --> E1[设备权限验证] + E --> E2[产品可用性验证] + E --> E3[规则冲突验证] + + %% 样式定义 + classDef levelClass fill:#e1f5fe,stroke:#01579b,stroke-width:2px + classDef validationClass fill:#f3e5f5,stroke:#4a148c,stroke-width:2px + + class A levelClass + class B,C,D,E levelClass + class B1,B2,B3,B4,C1,C2,C3,D1,D2,D3,E1,E2,E3 validationClass +``` + +### 2. 验证时机图 + +```mermaid +stateDiagram-v2 + [*] --> 字段输入 + 字段输入 --> 实时验证: onChange + 实时验证 --> 显示错误: 验证失败 + 实时验证 --> 清除错误: 验证通过 + 显示错误 --> 字段输入: 用户修正 + 清除错误 --> 字段输入: 继续输入 + + 字段输入 --> 失焦验证: onBlur + 失焦验证 --> 显示警告: 格式错误 + 失焦验证 --> 正常状态: 格式正确 + 显示警告 --> 字段输入: 重新聚焦 + 正常状态 --> 字段输入: 重新聚焦 + + 字段输入 --> 表单提交: 用户提交 + 表单提交 --> 全量验证 + 全量验证 --> 提交成功: 验证通过 + 全量验证 --> 显示错误: 验证失败 + 提交成功 --> [*] + 显示错误 --> 字段输入: 用户修正 +``` + +## 测试策略 + +### 1. 测试金字塔图 + +```mermaid +graph TB + A[测试金字塔] --> B[单元测试
Unit Tests
70%] + A --> C[集成测试
Integration Tests
20%] + A --> D[端到端测试
E2E Tests
10%] + + %% 单元测试详细 + B --> B1[组件渲染测试] + B --> B2[数据验证逻辑测试] + B --> B3[用户交互测试] + B --> B4[工具函数测试] + + %% 集成测试详细 + C --> C1[表单提交流程测试] + C --> C2[API调用测试] + C --> C3[数据转换测试] + C --> C4[组件间通信测试] + + %% 端到端测试详细 + D --> D1[完整用户流程测试] + D --> D2[浏览器兼容性测试] + D --> D3[响应式设计测试] + D --> D4[性能测试] + + %% 样式定义 + classDef pyramidClass fill:#e8f5e8,stroke:#2e7d32,stroke-width:3px + classDef unitClass fill:#e3f2fd,stroke:#1976d2,stroke-width:2px + classDef integrationClass fill:#fff3e0,stroke:#f57c00,stroke-width:2px + classDef e2eClass fill:#ffebee,stroke:#d32f2f,stroke-width:2px + + class A pyramidClass + class B,B1,B2,B3,B4 unitClass + class C,C1,C2,C3,C4 integrationClass + class D,D1,D2,D3,D4 e2eClass +``` + +### 2. 测试用例覆盖图 + +```mermaid +mindmap + root((测试用例覆盖)) + 功能测试 + 基础信息 + 名称输入验证 + 描述输入验证 + 状态切换测试 + 触发器配置 + 设备触发配置 + 定时触发配置 + 条件组合测试 + 执行器配置 + 设备控制配置 + 告警配置测试 + 表单提交 + 验证流程测试 + 保存流程测试 + + 异常测试 + 网络异常 + API调用失败 + 超时处理 + 数据异常 + 格式错误处理 + 空数据处理 + 用户异常 + 权限不足 + 操作冲突 + + 性能测试 + 加载性能 + 首屏加载时间 + 组件渲染性能 + 交互性能 + 表单响应速度 + 数据处理性能 + 内存性能 + 内存泄漏检测 + 组件销毁测试 + + 兼容性测试 + 浏览器兼容 + Chrome测试 + Firefox测试 + Safari测试 + Edge测试 + 设备兼容 + 桌面端测试 + 平板端测试 + 移动端测试 +``` + +## 表单设计架构总览 + +### 完整架构图 + +```mermaid +graph TB + %% 用户界面层 + subgraph "用户界面层 (UI Layer)" + A[RuleSceneForm 主表单] + B[BasicInfoSection 基础信息] + C[TriggerSection 触发器配置] + D[ActionSection 执行器配置] + E[PreviewSection 预览区域] + end + + %% 状态管理层 + subgraph "状态管理层 (State Management)" + F[FormState 表单状态] + G[ValidationState 验证状态] + H[UIState 界面状态] + I[CacheState 缓存状态] + end + + %% 业务逻辑层 + subgraph "业务逻辑层 (Business Logic)" + J[FormValidator 表单验证器] + K[DataTransformer 数据转换器] + L[ConfigBuilder 配置构建器] + M[RuleEngine 规则引擎] + end + + %% 数据访问层 + subgraph "数据访问层 (Data Access)" + N[ProductAPI 产品接口] + O[DeviceAPI 设备接口] + P[RuleSceneAPI 规则场景接口] + Q[AlertAPI 告警接口] + end + + %% 工具层 + subgraph "工具层 (Utilities)" + R[CronValidator CRON验证器] + S[TypeChecker 类型检查器] + T[ErrorHandler 错误处理器] + U[Logger 日志记录器] + end + + %% 连接关系 + A --> B + A --> C + A --> D + A --> E + + B --> F + C --> F + D --> F + E --> F + + F --> J + G --> J + H --> K + I --> L + + J --> M + K --> M + L --> M + + M --> N + M --> O + M --> P + M --> Q + + J --> R + K --> S + M --> T + A --> U + + %% 样式定义 + classDef uiClass fill:#e3f2fd,stroke:#1976d2,stroke-width:2px + classDef stateClass fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px + classDef businessClass fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px + classDef dataClass fill:#fff3e0,stroke:#f57c00,stroke-width:2px + classDef utilClass fill:#fce4ec,stroke:#c2185b,stroke-width:2px + + class A,B,C,D,E uiClass + class F,G,H,I stateClass + class J,K,L,M businessClass + class N,O,P,Q dataClass + class R,S,T,U utilClass +``` + +### 设计原则总结 + +```mermaid +mindmap + root((表单设计原则)) + 用户体验 + 直观易用 + 清晰的视觉层次 + 一致的交互模式 + 智能的操作提示 + 响应迅速 + 实时验证反馈 + 快速数据加载 + 流畅的动画效果 + 错误友好 + 明确的错误信息 + 便捷的错误修正 + 优雅的异常处理 + + 技术架构 + 组件化设计 + 高内聚低耦合 + 可复用的组件 + 清晰的组件边界 + 状态管理 + 集中式状态管理 + 可预测的状态变更 + 高效的状态同步 + 数据流控制 + 单向数据流 + 明确的数据来源 + 可追踪的数据变更 + + 质量保证 + 代码质量 + 类型安全 + 代码规范 + 充分的注释 + 测试覆盖 + 单元测试 + 集成测试 + 端到端测试 + 性能优化 + 懒加载 + 缓存策略 + 防抖节流 + + 可维护性 + 模块化结构 + 清晰的目录结构 + 合理的文件组织 + 明确的依赖关系 + 文档完善 + API文档 + 组件文档 + 使用说明 + 扩展性设计 + 插件化架构 + 配置化开发 + 版本兼容性 +``` + +## 总结 + +IoT场景联动规则表单设计需要考虑: + +### 1. 核心设计要点 + +- **复杂性管理**:通过组件化和分步骤设计降低复杂度 +- **用户体验**:提供直观的操作界面和智能提示 +- **数据完整性**:完善的验证机制确保数据质量 +- **扩展性**:模块化设计支持功能扩展 +- **性能优化**:合理的加载和缓存策略 +- **可访问性**:确保所有用户都能正常使用 + +### 2. 技术实现要点 + +- **状态管理**:采用集中式状态管理,确保数据流的可控性 +- **组件设计**:高内聚低耦合的组件架构,提高代码复用性 +- **验证策略**:多层次的验证机制,保证数据质量 +- **错误处理**:完善的错误处理和用户反馈机制 +- **性能优化**:懒加载、缓存、防抖等优化策略 + +### 3. 质量保证 + +- **测试覆盖**:完整的测试金字塔,确保代码质量 +- **文档完善**:详细的设计文档和使用说明 +- **代码规范**:统一的编码规范和类型安全 + +通过以上设计思路和详细的Mermaid图表,可以构建一个功能完善、用户友好、技术先进的IoT场景联动规则配置表单系统。 diff --git a/src/views/iot/rule/scene/components/RuleSceneForm.vue b/src/views/iot/rule/scene/components/RuleSceneForm.vue new file mode 100644 index 000000000..11c82f478 --- /dev/null +++ b/src/views/iot/rule/scene/components/RuleSceneForm.vue @@ -0,0 +1,315 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/action/ActionExecutor.vue b/src/views/iot/rule/scene/components/action/ActionExecutor.vue index 9baeb17ad..cd26c6c6e 100644 --- a/src/views/iot/rule/scene/components/action/ActionExecutor.vue +++ b/src/views/iot/rule/scene/components/action/ActionExecutor.vue @@ -25,7 +25,7 @@ {{ product ? product.name : '选择产品' }} - +
设备 diff --git a/src/views/iot/rule/scene/components/configs/AlertConfig.vue b/src/views/iot/rule/scene/components/configs/AlertConfig.vue new file mode 100644 index 000000000..4bf4c2d34 --- /dev/null +++ b/src/views/iot/rule/scene/components/configs/AlertConfig.vue @@ -0,0 +1,288 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/configs/ConditionConfig.vue b/src/views/iot/rule/scene/components/configs/ConditionConfig.vue new file mode 100644 index 000000000..676440513 --- /dev/null +++ b/src/views/iot/rule/scene/components/configs/ConditionConfig.vue @@ -0,0 +1,260 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/configs/ConditionGroupConfig.vue b/src/views/iot/rule/scene/components/configs/ConditionGroupConfig.vue new file mode 100644 index 000000000..6215b01e7 --- /dev/null +++ b/src/views/iot/rule/scene/components/configs/ConditionGroupConfig.vue @@ -0,0 +1,331 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/configs/DeviceControlConfig.vue b/src/views/iot/rule/scene/components/configs/DeviceControlConfig.vue new file mode 100644 index 000000000..cae0839b2 --- /dev/null +++ b/src/views/iot/rule/scene/components/configs/DeviceControlConfig.vue @@ -0,0 +1,173 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/configs/DeviceTriggerConfig.vue b/src/views/iot/rule/scene/components/configs/DeviceTriggerConfig.vue new file mode 100644 index 000000000..e02922a11 --- /dev/null +++ b/src/views/iot/rule/scene/components/configs/DeviceTriggerConfig.vue @@ -0,0 +1,347 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/configs/TimerTriggerConfig.vue b/src/views/iot/rule/scene/components/configs/TimerTriggerConfig.vue new file mode 100644 index 000000000..b72336383 --- /dev/null +++ b/src/views/iot/rule/scene/components/configs/TimerTriggerConfig.vue @@ -0,0 +1,142 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/inputs/CronBuilder.vue b/src/views/iot/rule/scene/components/inputs/CronBuilder.vue new file mode 100644 index 000000000..6c705097d --- /dev/null +++ b/src/views/iot/rule/scene/components/inputs/CronBuilder.vue @@ -0,0 +1,233 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/inputs/CronInput.vue b/src/views/iot/rule/scene/components/inputs/CronInput.vue new file mode 100644 index 000000000..9e961e021 --- /dev/null +++ b/src/views/iot/rule/scene/components/inputs/CronInput.vue @@ -0,0 +1,142 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/inputs/DescriptionInput.vue b/src/views/iot/rule/scene/components/inputs/DescriptionInput.vue new file mode 100644 index 000000000..dd6cf1079 --- /dev/null +++ b/src/views/iot/rule/scene/components/inputs/DescriptionInput.vue @@ -0,0 +1,184 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/inputs/NameInput.vue b/src/views/iot/rule/scene/components/inputs/NameInput.vue new file mode 100644 index 000000000..d6548b5e7 --- /dev/null +++ b/src/views/iot/rule/scene/components/inputs/NameInput.vue @@ -0,0 +1,162 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/inputs/StatusRadio.vue b/src/views/iot/rule/scene/components/inputs/StatusRadio.vue new file mode 100644 index 000000000..4019074f1 --- /dev/null +++ b/src/views/iot/rule/scene/components/inputs/StatusRadio.vue @@ -0,0 +1,185 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/inputs/ValueInput.vue b/src/views/iot/rule/scene/components/inputs/ValueInput.vue new file mode 100644 index 000000000..9373c04a8 --- /dev/null +++ b/src/views/iot/rule/scene/components/inputs/ValueInput.vue @@ -0,0 +1,406 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/previews/ActionPreview.vue b/src/views/iot/rule/scene/components/previews/ActionPreview.vue new file mode 100644 index 000000000..e1876a790 --- /dev/null +++ b/src/views/iot/rule/scene/components/previews/ActionPreview.vue @@ -0,0 +1,127 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/previews/ConfigPreview.vue b/src/views/iot/rule/scene/components/previews/ConfigPreview.vue new file mode 100644 index 000000000..c227f4e66 --- /dev/null +++ b/src/views/iot/rule/scene/components/previews/ConfigPreview.vue @@ -0,0 +1,65 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/previews/NextExecutionPreview.vue b/src/views/iot/rule/scene/components/previews/NextExecutionPreview.vue new file mode 100644 index 000000000..2e79929b3 --- /dev/null +++ b/src/views/iot/rule/scene/components/previews/NextExecutionPreview.vue @@ -0,0 +1,226 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/previews/TriggerPreview.vue b/src/views/iot/rule/scene/components/previews/TriggerPreview.vue new file mode 100644 index 000000000..0b7d779e6 --- /dev/null +++ b/src/views/iot/rule/scene/components/previews/TriggerPreview.vue @@ -0,0 +1,131 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/previews/ValidationResult.vue b/src/views/iot/rule/scene/components/previews/ValidationResult.vue new file mode 100644 index 000000000..f83739f93 --- /dev/null +++ b/src/views/iot/rule/scene/components/previews/ValidationResult.vue @@ -0,0 +1,120 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/sections/ActionSection.vue b/src/views/iot/rule/scene/components/sections/ActionSection.vue new file mode 100644 index 000000000..c14efc2d1 --- /dev/null +++ b/src/views/iot/rule/scene/components/sections/ActionSection.vue @@ -0,0 +1,390 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/sections/BasicInfoSection.vue b/src/views/iot/rule/scene/components/sections/BasicInfoSection.vue new file mode 100644 index 000000000..659759e0c --- /dev/null +++ b/src/views/iot/rule/scene/components/sections/BasicInfoSection.vue @@ -0,0 +1,110 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/sections/PreviewSection.vue b/src/views/iot/rule/scene/components/sections/PreviewSection.vue new file mode 100644 index 000000000..1a203a5b0 --- /dev/null +++ b/src/views/iot/rule/scene/components/sections/PreviewSection.vue @@ -0,0 +1,183 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/sections/TriggerSection.vue b/src/views/iot/rule/scene/components/sections/TriggerSection.vue new file mode 100644 index 000000000..4306a25f4 --- /dev/null +++ b/src/views/iot/rule/scene/components/sections/TriggerSection.vue @@ -0,0 +1,395 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/selectors/ActionTypeSelector.vue b/src/views/iot/rule/scene/components/selectors/ActionTypeSelector.vue new file mode 100644 index 000000000..12a0db0fa --- /dev/null +++ b/src/views/iot/rule/scene/components/selectors/ActionTypeSelector.vue @@ -0,0 +1,145 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/selectors/OperatorSelector.vue b/src/views/iot/rule/scene/components/selectors/OperatorSelector.vue new file mode 100644 index 000000000..d9297e8b0 --- /dev/null +++ b/src/views/iot/rule/scene/components/selectors/OperatorSelector.vue @@ -0,0 +1,275 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/selectors/ProductDeviceSelector.vue b/src/views/iot/rule/scene/components/selectors/ProductDeviceSelector.vue new file mode 100644 index 000000000..380476e6b --- /dev/null +++ b/src/views/iot/rule/scene/components/selectors/ProductDeviceSelector.vue @@ -0,0 +1,325 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/selectors/PropertySelector.vue b/src/views/iot/rule/scene/components/selectors/PropertySelector.vue new file mode 100644 index 000000000..26f558e5b --- /dev/null +++ b/src/views/iot/rule/scene/components/selectors/PropertySelector.vue @@ -0,0 +1,341 @@ + + + + + + diff --git a/src/views/iot/rule/scene/components/selectors/TriggerTypeSelector.vue b/src/views/iot/rule/scene/components/selectors/TriggerTypeSelector.vue new file mode 100644 index 000000000..c27367b06 --- /dev/null +++ b/src/views/iot/rule/scene/components/selectors/TriggerTypeSelector.vue @@ -0,0 +1,264 @@ + + + + + + diff --git a/src/views/iot/rule/scene/index.vue b/src/views/iot/rule/scene/index.vue index 4a05a201d..30723d539 100644 --- a/src/views/iot/rule/scene/index.vue +++ b/src/views/iot/rule/scene/index.vue @@ -1,192 +1,658 @@ + + + diff --git a/src/views/iot/rule/scene/utils/errorHandler.ts b/src/views/iot/rule/scene/utils/errorHandler.ts new file mode 100644 index 000000000..c7b10f316 --- /dev/null +++ b/src/views/iot/rule/scene/utils/errorHandler.ts @@ -0,0 +1,548 @@ +/** + * IoT 场景联动错误处理和用户反馈工具 + */ + +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 { + // 记录错误日志 + 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 { + 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 { + if (options.showMessage) { + ElMessage.info({ + message: error.message, + duration: options.duration, + showClose: !options.autoClose + }) + } + return true + } + + /** + * 处理警告级错误 + */ + private async handleWarningError(error: ErrorInfo, options: FeedbackOptions): Promise { + 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 { + 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 { + 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 { + return errorHandler.handleError( + { + type: ErrorType.VALIDATION, + level: ErrorLevel.WARNING, + message, + context + }, + { + showMessage: true, + duration: 4000 + } + ) +} + +/** + * 网络错误处理 + */ +export function handleNetworkError(error: any, context?: string): Promise { + 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 { + return errorHandler.handleError( + { + type: ErrorType.BUSINESS, + level: ErrorLevel.ERROR, + code, + message, + context + }, + { + showMessage: true, + duration: 4000 + } + ) +} + +/** + * 系统错误处理 + */ +export function handleSystemError(error: any, context?: string): Promise { + 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 { + 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 { + 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() + private loadingInstances = new Map() + + /** + * 开始加载 + */ + 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( + operation: () => Promise, + options: { + loadingKey?: string + loadingText?: string + context?: string + showSuccess?: boolean + successMessage?: string + errorHandler?: (error: any) => Promise + } = {} +): Promise { + 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) + } + } +} diff --git a/src/views/iot/rule/scene/utils/transform.ts b/src/views/iot/rule/scene/utils/transform.ts new file mode 100644 index 000000000..c01d398f4 --- /dev/null +++ b/src/views/iot/rule/scene/utils/transform.ts @@ -0,0 +1,406 @@ +/** + * IoT 场景联动数据转换工具函数 + */ + +import { + IotRuleScene, + TriggerConfig, + ActionConfig, + RuleSceneFormData, + TriggerFormData, + ActionFormData +} from '@/api/iot/rule/scene/scene.types' +import { generateUUID } from '@/utils' + +/** + * 创建默认的表单数据 + */ +export function createDefaultFormData(): RuleSceneFormData { + return { + name: '', + description: '', + status: 0, + triggers: [], + actions: [] + } +} + +/** + * 创建默认的触发器数据 + */ +export function createDefaultTriggerData(): TriggerFormData { + return { + type: 2, // 默认为属性上报 + productId: undefined, + deviceId: undefined, + identifier: undefined, + operator: undefined, + value: undefined, + cronExpression: undefined, + conditionGroups: [] + } +} + +/** + * 创建默认的执行器数据 + */ +export function createDefaultActionData(): ActionFormData { + return { + type: 1, // 默认为属性设置 + productId: undefined, + deviceId: undefined, + params: {}, + alertConfigId: undefined + } +} + +/** + * 将表单数据转换为API请求格式 + */ +export function transformFormToApi(formData: RuleSceneFormData): IotRuleScene { + // 这里需要根据实际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() + })) || [] + } +} + +/** + * 创建默认的触发器配置 + */ +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: '' + } + ] + } + ] + } +} + +/** + * 创建默认的执行器配置 + */ +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: {} + } + } +} + +/** + * 深度克隆对象(用于避免引用问题) + */ +export function deepClone(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 +} + +/** + * 清理空值和无效数据 + */ +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 +} + +/** + * 验证并修复数据结构 + */ +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 +} + +/** + * 比较两个场景联动规则是否相等(忽略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 } +} diff --git a/src/views/iot/rule/scene/utils/validation.ts b/src/views/iot/rule/scene/utils/validation.ts new file mode 100644 index 000000000..479be793c --- /dev/null +++ b/src/views/iot/rule/scene/utils/validation.ts @@ -0,0 +1,278 @@ +/** + * IoT 场景联动表单验证工具函数 + */ + +import { FormValidationRules, IotRuleScene, TriggerConfig, ActionConfig } from '@/api/iot/rule/scene/scene.types' +import { IotRuleSceneTriggerTypeEnum, IotRuleSceneActionTypeEnum } from '@/api/iot/rule/scene/scene.types' + +/** + * 基础表单验证规则 + */ +export const getBaseValidationRules = (): FormValidationRules => ({ + name: [ + { required: true, message: '场景名称不能为空', trigger: 'blur' }, + { type: 'string', min: 1, max: 50, message: '场景名称长度应在1-50个字符之间', trigger: 'blur' } + ], + status: [ + { required: true, message: '场景状态不能为空', trigger: 'change' }, + { type: 'enum', enum: [0, 1], message: '状态值必须为0或1', trigger: 'change' } + ], + description: [ + { type: 'string', max: 200, message: '场景描述不能超过200个字符', trigger: 'blur' } + ], + triggers: [ + { required: true, message: '触发器数组不能为空', trigger: 'change' }, + { type: 'array', min: 1, message: '至少需要一个触发器', trigger: 'change' } + ], + actions: [ + { required: true, message: '执行器数组不能为空', trigger: 'change' }, + { type: 'array', min: 1, message: '至少需要一个执行器', trigger: 'change' } + ] +}) + +/** + * 验证CRON表达式格式 + */ +export function validateCronExpression(cron: string): boolean { + if (!cron || cron.trim().length === 0) return false + + // 基础的CRON表达式正则验证(支持6位和7位格式) + const cronRegex = /^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\*|([0-9]|1[0-9]|2[0-3])|\*\/([0-9]|1[0-9]|2[0-3])) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\*\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\*|([1-9]|1[0-2])|\*\/([1-9]|1[0-2])) (\*|([0-6])|\*\/([0-6]))( (\*|([1-9][0-9]{3})|\*\/([1-9][0-9]{3})))?$/ + + return cronRegex.test(cron.trim()) +} + +/** + * 验证设备名称数组 + */ +export function validateDeviceNames(deviceNames: string[]): boolean { + return Array.isArray(deviceNames) && + deviceNames.length > 0 && + deviceNames.every(name => name && name.trim().length > 0) +} + +/** + * 验证比较值格式 + */ +export function validateCompareValue(operator: string, value: string): boolean { + if (!value || value.trim().length === 0) return false + + const trimmedValue = value.trim() + + switch (operator) { + case 'between': + case 'not between': + const betweenValues = trimmedValue.split(',') + return betweenValues.length === 2 && + betweenValues.every(v => v.trim().length > 0) && + !isNaN(Number(betweenValues[0].trim())) && + !isNaN(Number(betweenValues[1].trim())) + + case 'in': + case 'not in': + const inValues = trimmedValue.split(',') + return inValues.length > 0 && inValues.every(v => v.trim().length > 0) + + case '>': + case '>=': + case '<': + case '<=': + return !isNaN(Number(trimmedValue)) + + case '=': + case '!=': + case 'like': + case 'not null': + default: + return true + } +} + +/** + * 验证触发器配置 + */ +export function validateTriggerConfig(trigger: TriggerConfig): { valid: boolean; message?: string } { + if (!trigger.type) { + return { valid: false, message: '触发类型不能为空' } + } + + // 定时触发验证 + if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) { + if (!trigger.cronExpression) { + return { valid: false, message: 'CRON表达式不能为空' } + } + if (!validateCronExpression(trigger.cronExpression)) { + return { valid: false, message: 'CRON表达式格式不正确' } + } + return { valid: true } + } + + // 设备触发验证 + if (!trigger.productKey) { + return { valid: false, message: '产品标识不能为空' } + } + + if (!trigger.deviceNames || !validateDeviceNames(trigger.deviceNames)) { + return { valid: false, message: '设备名称不能为空' } + } + + // 设备状态变更无需额外条件验证 + if (trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) { + return { valid: true } + } + + // 其他设备触发类型需要验证条件 + if (!trigger.conditions || trigger.conditions.length === 0) { + return { valid: false, message: '触发条件不能为空' } + } + + // 验证每个条件的参数 + for (const condition of trigger.conditions) { + if (!condition.parameters || condition.parameters.length === 0) { + return { valid: false, message: '触发条件参数不能为空' } + } + + for (const param of condition.parameters) { + if (!param.operator) { + return { valid: false, message: '操作符不能为空' } + } + if (!validateCompareValue(param.operator, param.value)) { + return { valid: false, message: `操作符 "${param.operator}" 对应的比较值格式不正确` } + } + } + } + + return { valid: true } +} + +/** + * 验证执行器配置 + */ +export function validateActionConfig(action: ActionConfig): { valid: boolean; message?: string } { + if (!action.type) { + return { valid: false, message: '执行类型不能为空' } + } + + // 告警触发/恢复验证 + if (action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER || + action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER) { + if (!action.alertConfigId) { + return { valid: false, message: '告警配置ID不能为空' } + } + return { valid: true } + } + + // 设备控制验证 + if (action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET || + action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE) { + if (!action.deviceControl) { + return { valid: false, message: '设备控制配置不能为空' } + } + + const { deviceControl } = action + if (!deviceControl.productKey) { + return { valid: false, message: '产品标识不能为空' } + } + if (!deviceControl.deviceNames || !validateDeviceNames(deviceControl.deviceNames)) { + return { valid: false, message: '设备名称不能为空' } + } + if (!deviceControl.type) { + return { valid: false, message: '消息类型不能为空' } + } + if (!deviceControl.identifier) { + return { valid: false, message: '消息标识符不能为空' } + } + if (!deviceControl.params || Object.keys(deviceControl.params).length === 0) { + return { valid: false, message: '参数不能为空' } + } + + return { valid: true } + } + + return { valid: false, message: '未知的执行类型' } +} + +/** + * 验证完整的场景联动规则 + */ +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 } +} + +/** + * 获取操作符选项 + */ +export function getOperatorOptions() { + 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() { + 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() { + return [ + { value: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET, label: '设备属性设置' }, + { value: IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE, label: '设备服务调用' }, + { value: IotRuleSceneActionTypeEnum.ALERT_TRIGGER, label: '告警触发' }, + { value: IotRuleSceneActionTypeEnum.ALERT_RECOVER, label: '告警恢复' } + ] +} -- Gitee From 7f4d3d72f8a8262d8fc070e5ce901980a7b774e8 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Thu, 17 Jul 2025 21:54:40 +0800 Subject: [PATCH 2/5] =?UTF-8?q?perf=EF=BC=9A=E3=80=90IoT=20=E7=89=A9?= =?UTF-8?q?=E8=81=94=E7=BD=91=E3=80=91=E5=9C=BA=E6=99=AF=E8=81=94=E5=8A=A8?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/iot/rule/scene/111index.vue | 192 --------- src/views/iot/rule/scene/RuleSceneForm.vue | 224 ----------- .../rule/scene/components/RuleSceneForm.vue | 71 ++-- .../scene/components/ThingModelDualView.vue | 81 ---- .../scene/components/ThingModelParamInput.vue | 142 ------- .../components/action/ActionExecutor.vue | 307 --------------- .../components/action/DeviceControlAction.vue | 248 ------------ .../components/listener/ConditionSelector.vue | 106 ----- .../components/listener/DeviceListener.vue | 367 ------------------ .../listener/DeviceListenerCondition.vue | 87 ----- .../listener/DeviceStateListener.vue | 166 -------- 11 files changed, 29 insertions(+), 1962 deletions(-) delete mode 100644 src/views/iot/rule/scene/111index.vue delete mode 100644 src/views/iot/rule/scene/RuleSceneForm.vue delete mode 100644 src/views/iot/rule/scene/components/ThingModelDualView.vue delete mode 100644 src/views/iot/rule/scene/components/ThingModelParamInput.vue delete mode 100644 src/views/iot/rule/scene/components/action/ActionExecutor.vue delete mode 100644 src/views/iot/rule/scene/components/action/DeviceControlAction.vue delete mode 100644 src/views/iot/rule/scene/components/listener/ConditionSelector.vue delete mode 100644 src/views/iot/rule/scene/components/listener/DeviceListener.vue delete mode 100644 src/views/iot/rule/scene/components/listener/DeviceListenerCondition.vue delete mode 100644 src/views/iot/rule/scene/components/listener/DeviceStateListener.vue diff --git a/src/views/iot/rule/scene/111index.vue b/src/views/iot/rule/scene/111index.vue deleted file mode 100644 index 4a05a201d..000000000 --- a/src/views/iot/rule/scene/111index.vue +++ /dev/null @@ -1,192 +0,0 @@ - - - diff --git a/src/views/iot/rule/scene/RuleSceneForm.vue b/src/views/iot/rule/scene/RuleSceneForm.vue deleted file mode 100644 index b79b6b4d4..000000000 --- a/src/views/iot/rule/scene/RuleSceneForm.vue +++ /dev/null @@ -1,224 +0,0 @@ - - diff --git a/src/views/iot/rule/scene/components/RuleSceneForm.vue b/src/views/iot/rule/scene/components/RuleSceneForm.vue index 11c82f478..73914a3ae 100644 --- a/src/views/iot/rule/scene/components/RuleSceneForm.vue +++ b/src/views/iot/rule/scene/components/RuleSceneForm.vue @@ -19,22 +19,13 @@ class="form-container" > - + - + - + !!props.ruleScene?.id) -const drawerTitle = computed(() => isEdit.value ? '编辑场景联动规则' : '新增场景联动规则') +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 + return ( + formData.value.name && + formData.value.triggers.length > 0 && + formData.value.actions.length > 0 && + triggerValidation.value.valid && + actionValidation.value.valid + ) }) // 事件处理 @@ -136,15 +120,15 @@ const handleActionValidate = (result: { valid: boolean; message: string }) => { 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: '验证通过' } showSuccess('规则验证通过') return true @@ -164,16 +148,16 @@ const handleSubmit = async () => { if (!isValid) { throw new Error('表单验证失败') } - + // 转换数据格式 const apiData = transformFormToApi(formData.value) - + // 这里应该调用API保存数据 console.log('提交数据:', apiData) - + // 模拟API调用 - await new Promise(resolve => setTimeout(resolve, 1000)) - + await new Promise((resolve) => setTimeout(resolve, 1000)) + return apiData }, { @@ -184,7 +168,7 @@ const handleSubmit = async () => { successMessage: isEdit.value ? '更新成功' : '创建成功' } ) - + if (result) { emit('success') handleClose() @@ -216,11 +200,14 @@ watch(drawerVisible, (visible) => { }) // 监听props变化 -watch(() => props.ruleScene, () => { - if (drawerVisible.value) { - initFormData() +watch( + () => props.ruleScene, + () => { + if (drawerVisible.value) { + initFormData() + } } -}) +) -- Gitee From f31d034c793386c11af643c0a755d1b3a64c8c62 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Thu, 17 Jul 2025 23:32:45 +0800 Subject: [PATCH 5/5] =?UTF-8?q?perf=EF=BC=9A=E3=80=90IoT=20=E7=89=A9?= =?UTF-8?q?=E8=81=94=E7=BD=91=E3=80=91=E5=9C=BA=E6=99=AF=E8=81=94=E5=8A=A8?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=E6=9D=A1=E4=BB=B6=E5=B1=9E=E6=80=A7=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=99=A8=E7=BB=84=E4=BB=B6=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...23\346\236\204\346\226\207\346\241\243.md" | 468 ++++++++++++++++++ .../selectors/ProductDeviceSelector.vue | 65 ++- .../components/selectors/PropertySelector.vue | 236 ++++++--- .../rule/scene/components/selectors/types.ts | 168 +++++++ 4 files changed, 850 insertions(+), 87 deletions(-) create mode 100644 "src/views/iot/rule/scene/IotThingModelTSLRespVO\346\225\260\346\215\256\347\273\223\346\236\204\346\226\207\346\241\243.md" create mode 100644 src/views/iot/rule/scene/components/selectors/types.ts diff --git "a/src/views/iot/rule/scene/IotThingModelTSLRespVO\346\225\260\346\215\256\347\273\223\346\236\204\346\226\207\346\241\243.md" "b/src/views/iot/rule/scene/IotThingModelTSLRespVO\346\225\260\346\215\256\347\273\223\346\236\204\346\226\207\346\241\243.md" new file mode 100644 index 000000000..aff32ccce --- /dev/null +++ "b/src/views/iot/rule/scene/IotThingModelTSLRespVO\346\225\260\346\215\256\347\273\223\346\236\204\346\226\207\346\241\243.md" @@ -0,0 +1,468 @@ +# IotThingModelTSLRespVO 数据结构文档 + +## 概述 + +`IotThingModelTSLRespVO` 是IoT产品物模型TSL(Thing Specification Language)的响应数据结构,用于返回完整的产品物模型定义,包括属性、事件和服务的详细信息。TSL是阿里云IoT平台定义的一套物模型描述规范。 + +## 主体数据结构 + +### IotThingModelTSLRespVO + +```typescript +interface IotThingModelTSLRespVO { + productId: number; // 产品编号(必填) + productKey: string; // 产品标识(必填) + properties: ThingModelProperty[]; // 属性列表(必填) + events: ThingModelEvent[]; // 事件列表(必填) + services: ThingModelService[]; // 服务列表(必填) +} +``` + +**字段说明:** +- `productId`: 产品编号,唯一标识一个IoT产品 +- `productKey`: 产品标识符,用于设备连接和识别 +- `properties`: 设备属性列表,描述设备的状态信息 +- `events`: 设备事件列表,描述设备主动上报的事件 +- `services`: 设备服务列表,描述可以调用的设备功能 + +## 属性数据结构 (ThingModelProperty) + +### 基本结构 + +```typescript +interface ThingModelProperty { + identifier: string; // 属性标识符(必填) + name: string; // 属性名称(必填) + accessMode: string; // 访问模式(必填) + required?: boolean; // 是否必选 + dataType: string; // 数据类型(必填) + dataSpecs?: ThingModelDataSpecs; // 数据规范(非列表型) + dataSpecsList?: ThingModelDataSpecs[]; // 数据规范(列表型) +} +``` + +### 字段详细说明 + +#### identifier(属性标识符) +- **类型**: `string` +- **必填**: 是 +- **格式**: 正则表达式 `^[a-zA-Z][a-zA-Z0-9_]{0,31}$` +- **说明**: 只能由字母、数字和下划线组成,必须以字母开头,长度不超过32个字符 +- **示例**: `"temperature"`, `"humidity"`, `"power_status"` + +#### name(属性名称) +- **类型**: `string` +- **必填**: 是 +- **说明**: 属性的显示名称,用于界面展示 +- **示例**: `"温度"`, `"湿度"`, `"电源状态"` + +#### accessMode(访问模式) +- **类型**: `string` +- **必填**: 是 +- **枚举值**: + - `"r"`: 只读,设备只能上报,平台不能下发 + - `"rw"`: 读写,设备可以上报,平台也可以下发 +- **示例**: `"r"`, `"rw"` + +#### dataType(数据类型) +- **类型**: `string` +- **必填**: 是 +- **枚举值**: + - `"int"`: 整数型 + - `"float"`: 单精度浮点型 + - `"double"`: 双精度浮点型 + - `"enum"`: 枚举型 + - `"bool"`: 布尔型 + - `"text"`: 文本型 + - `"date"`: 时间型 + - `"struct"`: 结构体型 + - `"array"`: 数组型 + +## 事件数据结构 (ThingModelEvent) + +### 基本结构 + +```typescript +interface ThingModelEvent { + identifier: string; // 事件标识符(必填) + name: string; // 事件名称(必填) + required?: boolean; // 是否必选 + type: string; // 事件类型(必填) + outputParams?: ThingModelParam[]; // 输出参数 + method?: string; // 执行方法 +} +``` + +### 字段详细说明 + +#### type(事件类型) +- **类型**: `string` +- **必填**: 是 +- **枚举值**: + - `"info"`: 信息事件 + - `"alert"`: 告警事件 + - `"error"`: 故障事件 + +#### outputParams(输出参数) +- **类型**: `ThingModelParam[]` +- **必填**: 否 +- **说明**: 事件触发时返回的参数信息 + +## 服务数据结构 (ThingModelService) + +### 基本结构 + +```typescript +interface ThingModelService { + identifier: string; // 服务标识符(必填) + name: string; // 服务名称(必填) + required?: boolean; // 是否必选 + callType: string; // 调用类型(必填) + inputParams?: ThingModelParam[]; // 输入参数 + outputParams?: ThingModelParam[]; // 输出参数 + method?: string; // 执行方法 +} +``` + +### 字段详细说明 + +#### callType(调用类型) +- **类型**: `string` +- **必填**: 是 +- **枚举值**: + - `"async"`: 异步调用 + - `"sync"`: 同步调用 + +## 参数数据结构 (ThingModelParam) + +### 基本结构 + +```typescript +interface ThingModelParam { + identifier: string; // 参数标识符(必填) + name: string; // 参数名称(必填) + direction: string; // 参数方向(必填) + paraOrder?: number; // 参数序号 + dataType: string; // 数据类型(必填) + dataSpecs?: ThingModelDataSpecs; // 数据规范(非列表型) + dataSpecsList?: ThingModelDataSpecs[]; // 数据规范(列表型) +} +``` + +### 字段详细说明 + +#### direction(参数方向) +- **类型**: `string` +- **必填**: 是 +- **枚举值**: + - `"input"`: 输入参数 + - `"output"`: 输出参数 + +## 数据规范结构 (ThingModelDataSpecs) + +数据规范是一个抽象基类,根据不同的数据类型有不同的具体实现: + +### 1. 数值型数据规范 (ThingModelNumericDataSpec) + +适用于 `int`、`float`、`double` 类型: + +```typescript +interface ThingModelNumericDataSpec { + dataType: "int" | "float" | "double"; + max: string; // 最大值(必填) + min: string; // 最小值(必填) + step: string; // 步长(必填) + precise?: string; // 精度(float/double可选) + defaultValue?: string; // 默认值 + unit?: string; // 单位符号 + unitName?: string; // 单位名称 +} +``` + +### 2. 布尔/枚举型数据规范 (ThingModelBoolOrEnumDataSpecs) + +适用于 `bool`、`enum` 类型: + +```typescript +interface ThingModelBoolOrEnumDataSpecs { + dataType: "bool" | "enum"; + name: string; // 枚举项名称(必填) + value: number; // 枚举值(必填) +} +``` + +### 3. 文本/时间型数据规范 (ThingModelDateOrTextDataSpecs) + +适用于 `text`、`date` 类型: + +```typescript +interface ThingModelDateOrTextDataSpecs { + dataType: "text" | "date"; + length?: number; // 数据长度(text类型需要,最大2048) + defaultValue?: string; // 默认值 +} +``` + +### 4. 数组型数据规范 (ThingModelArrayDataSpecs) + +适用于 `array` 类型: + +```typescript +interface ThingModelArrayDataSpecs { + dataType: "array"; + size: number; // 数组元素个数(必填) + childDataType: string; // 数组元素数据类型(必填) + dataSpecsList?: ThingModelDataSpecs[]; // 子元素数据规范(struct类型时) +} +``` + +**childDataType 枚举值**: +- `"struct"`: 结构体 +- `"int"`: 整数 +- `"float"`: 单精度浮点 +- `"double"`: 双精度浮点 +- `"text"`: 文本 + +### 5. 结构体型数据规范 (ThingModelStructDataSpecs) + +适用于 `struct` 类型: + +```typescript +interface ThingModelStructDataSpecs { + dataType: "struct"; + identifier: string; // 属性标识符(必填) + name: string; // 属性名称(必填) + accessMode: string; // 操作类型(必填) + required?: boolean; // 是否必选 + childDataType: string; // 子数据类型(必填) + dataSpecs?: ThingModelDataSpecs; // 数据规范(非列表型) + dataSpecsList?: ThingModelDataSpecs[]; // 数据规范(列表型) +} +``` + +**childDataType 枚举值**: +- `"int"`: 整数 +- `"float"`: 单精度浮点 +- `"double"`: 双精度浮点 +- `"text"`: 文本 +- `"date"`: 时间 +- `"enum"`: 枚举 +- `"bool"`: 布尔 + +## 数据类型映射关系 + +### dataSpecs vs dataSpecsList + +- **dataSpecs**: 用于非列表型数据类型(`int`、`float`、`double`、`text`、`date`、`array`) +- **dataSpecsList**: 用于列表型数据类型(`enum`、`bool`、`struct`) + +### JSON多态序列化 + +数据规范使用Jackson的`@JsonTypeInfo`和`@JsonSubTypes`注解实现多态序列化: + +```json +{ + "dataType": "int", + "max": "100", + "min": "0", + "step": "1", + "unit": "°C", + "unitName": "摄氏度" +} +``` + +## 完整示例 + +### 温度传感器物模型示例 + +```json +{ + "productId": 1024, + "productKey": "temperature_sensor", + "properties": [ + { + "identifier": "temperature", + "name": "温度", + "accessMode": "r", + "required": true, + "dataType": "float", + "dataSpecs": { + "dataType": "float", + "max": "100.0", + "min": "-40.0", + "step": "0.1", + "precise": "1", + "unit": "°C", + "unitName": "摄氏度" + } + }, + { + "identifier": "power_switch", + "name": "电源开关", + "accessMode": "rw", + "required": false, + "dataType": "bool", + "dataSpecsList": [ + { + "dataType": "bool", + "name": "关闭", + "value": 0 + }, + { + "dataType": "bool", + "name": "开启", + "value": 1 + } + ] + } + ], + "events": [ + { + "identifier": "high_temperature_alert", + "name": "高温告警", + "required": false, + "type": "alert", + "outputParams": [ + { + "identifier": "current_temp", + "name": "当前温度", + "direction": "output", + "dataType": "float", + "dataSpecs": { + "dataType": "float", + "max": "100.0", + "min": "-40.0", + "step": "0.1" + } + } + ] + } + ], + "services": [ + { + "identifier": "reset_device", + "name": "重置设备", + "required": false, + "callType": "async", + "inputParams": [ + { + "identifier": "reset_type", + "name": "重置类型", + "direction": "input", + "dataType": "enum", + "dataSpecsList": [ + { + "dataType": "enum", + "name": "软重置", + "value": 1 + }, + { + "dataType": "enum", + "name": "硬重置", + "value": 2 + } + ] + } + ], + "outputParams": [ + { + "identifier": "result", + "name": "执行结果", + "direction": "output", + "dataType": "bool", + "dataSpecsList": [ + { + "dataType": "bool", + "name": "失败", + "value": 0 + }, + { + "dataType": "bool", + "name": "成功", + "value": 1 + } + ] + } + ] + } + ] +} +``` + +## 前端使用建议 + +### 1. TypeScript类型定义 + +建议在前端项目中定义完整的TypeScript接口,确保类型安全: + +```typescript +// 定义完整的类型接口 +export interface IotThingModelTSLRespVO { + productId: number; + productKey: string; + properties: ThingModelProperty[]; + events: ThingModelEvent[]; + services: ThingModelService[]; +} + +// 使用联合类型处理数据规范的多态性 +export type ThingModelDataSpecs = + | ThingModelNumericDataSpec + | ThingModelBoolOrEnumDataSpecs + | ThingModelDateOrTextDataSpecs + | ThingModelArrayDataSpecs + | ThingModelStructDataSpecs; +``` + +### 2. 数据验证 + +```typescript +// 验证数据类型和数据规范的一致性 +function validateDataSpecs(dataType: string, dataSpecs: any): boolean { + switch (dataType) { + case 'int': + case 'float': + case 'double': + return dataSpecs.dataType === dataType && + dataSpecs.max !== undefined && + dataSpecs.min !== undefined; + case 'bool': + case 'enum': + return Array.isArray(dataSpecs) && + dataSpecs.every(spec => spec.name && spec.value !== undefined); + // ... 其他类型验证 + default: + return false; + } +} +``` + +### 3. 数据转换工具 + +```typescript +// 将后端数据转换为前端展示格式 +function formatPropertyValue(property: ThingModelProperty, value: any): string { + if (property.dataType === 'enum' || property.dataType === 'bool') { + const spec = property.dataSpecsList?.find(s => s.value === value); + return spec?.name || String(value); + } + + if (property.dataType === 'float' || property.dataType === 'double') { + const unit = property.dataSpecs?.unit || ''; + return `${value}${unit}`; + } + + return String(value); +} +``` + +## 注意事项 + +1. **数据规范选择**: 根据`dataType`选择使用`dataSpecs`还是`dataSpecsList` +2. **标识符唯一性**: 在同一产品下,所有功能的`identifier`必须唯一 +3. **数据类型一致性**: 参数的`dataType`必须与其`dataSpecs`的`dataType`保持一致 +4. **枚举值处理**: 布尔型和枚举型数据使用`dataSpecsList`数组存储可选值 +5. **嵌套结构**: 结构体和数组类型可能包含嵌套的数据规范定义 +6. **版本兼容**: 物模型结构可能随版本演进,前端需要做好兼容性处理 + +这个数据结构为IoT设备的完整功能描述提供了标准化的格式,支持复杂的数据类型和嵌套结构,能够满足各种IoT设备的建模需求。 diff --git a/src/views/iot/rule/scene/components/selectors/ProductDeviceSelector.vue b/src/views/iot/rule/scene/components/selectors/ProductDeviceSelector.vue index 380476e6b..9db8d11fe 100644 --- a/src/views/iot/rule/scene/components/selectors/ProductDeviceSelector.vue +++ b/src/views/iot/rule/scene/components/selectors/ProductDeviceSelector.vue @@ -34,8 +34,20 @@ - + + + + 选择设备 + 全部设备 + + + + + + + + {{ device.deviceName }}
{{ device.nickname || '无备注' }}
- {{ getDeviceStatusText(device.state) }} @@ -72,7 +84,7 @@ -
+
已选择设备 @@ -85,9 +97,18 @@
设备: - {{ selectedDevice?.deviceName }} - 全部设备 + {{ selectedDevice?.deviceName }} + + 全部 + + {{ getDeviceStatusText(selectedDevice?.state) }} @@ -123,6 +144,9 @@ const emit = defineEmits() const localProductId = useVModel(props, 'productId', emit) const localDeviceId = useVModel(props, 'deviceId', emit) +// 设备选择模式 +const deviceSelectionMode = ref<'specific' | 'all'>('specific') + // 数据状态 const productLoading = ref(false) const deviceLoading = ref(false) @@ -162,11 +186,11 @@ const handleProductChange = async (productId?: number) => { localProductId.value = productId localDeviceId.value = undefined deviceList.value = [] - + if (productId) { await getDeviceList(productId) } - + emitChange() } @@ -175,6 +199,20 @@ const handleDeviceChange = (deviceId?: number) => { emitChange() } +const handleDeviceSelectionModeChange = (mode: 'specific' | 'all') => { + deviceSelectionMode.value = mode + + if (mode === 'all') { + // 全部设备时,设备ID设为0 + localDeviceId.value = 0 + } else { + // 选择设备时,清空设备ID + localDeviceId.value = undefined + } + + emitChange() +} + const emitChange = () => { emit('change', { productId: localProductId.value, @@ -222,7 +260,14 @@ const getDeviceList = async (productId: number) => { // 初始化 onMounted(async () => { await getProductList() - + + // 根据初始设备ID设置选择模式 + if (localDeviceId.value === 0) { + deviceSelectionMode.value = 'all' + } else if (localDeviceId.value) { + deviceSelectionMode.value = 'specific' + } + if (localProductId.value) { await getDeviceList(localProductId.value) } diff --git a/src/views/iot/rule/scene/components/selectors/PropertySelector.vue b/src/views/iot/rule/scene/components/selectors/PropertySelector.vue index 26f558e5b..fbd15153e 100644 --- a/src/views/iot/rule/scene/components/selectors/PropertySelector.vue +++ b/src/views/iot/rule/scene/components/selectors/PropertySelector.vue @@ -27,8 +27,8 @@
{{ property.identifier }}
- - {{ getPropertyTypeName(property.type) }} + + {{ getPropertyTypeName(property.dataType) }}
@@ -41,8 +41,8 @@
{{ selectedProperty.name }} - - {{ getPropertyTypeName(selectedProperty.type) }} + + {{ getPropertyTypeName(selectedProperty.dataType) }}
@@ -70,6 +70,9 @@