commit
						9b88826a17
					
				|  | @ -88,7 +88,7 @@ | ||||||
|   }, |   }, | ||||||
|   "editor.formatOnSave": true, |   "editor.formatOnSave": true, | ||||||
|   "[vue]": { |   "[vue]": { | ||||||
|     "editor.defaultFormatter": "octref.vetur" |     "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||||
|   }, |   }, | ||||||
|   "i18n-ally.localesPaths": ["src/locales"], |   "i18n-ally.localesPaths": ["src/locales"], | ||||||
|   "i18n-ally.keystyle": "nested", |   "i18n-ally.keystyle": "nested", | ||||||
|  |  | ||||||
|  | @ -46,6 +46,24 @@ const IotRuleSceneTriggerConditionParameterOperatorEnum = { | ||||||
|   NOT_NULL: { name: '非空', value: 'not null' } // 非空
 |   NOT_NULL: { name: '非空', value: 'not null' } // 非空
 | ||||||
| } as const | } as const | ||||||
| 
 | 
 | ||||||
|  | // 条件类型枚举
 | ||||||
|  | const IotRuleSceneTriggerConditionTypeEnum = { | ||||||
|  |   DEVICE_STATUS: 1, // 设备状态
 | ||||||
|  |   DEVICE_PROPERTY: 2, // 设备属性
 | ||||||
|  |   CURRENT_TIME: 3 // 当前时间
 | ||||||
|  | } as const | ||||||
|  | 
 | ||||||
|  | // 时间运算符枚举
 | ||||||
|  | const IotRuleSceneTriggerTimeOperatorEnum = { | ||||||
|  |   BEFORE_TIME: { name: '在时间之前', value: 'before_time' }, // 在时间之前
 | ||||||
|  |   AFTER_TIME: { name: '在时间之后', value: 'after_time' }, // 在时间之后
 | ||||||
|  |   BETWEEN_TIME: { name: '在时间之间', value: 'between_time' }, // 在时间之间
 | ||||||
|  |   AT_TIME: { name: '在指定时间', value: 'at_time' }, // 在指定时间
 | ||||||
|  |   BEFORE_TODAY: { name: '在今日之前', value: 'before_today' }, // 在今日之前
 | ||||||
|  |   AFTER_TODAY: { name: '在今日之后', value: 'after_today' }, // 在今日之后
 | ||||||
|  |   TODAY: { name: '在今日之间', value: 'today' } // 在今日之间
 | ||||||
|  | } as const | ||||||
|  | 
 | ||||||
| // TODO @puhui999:下面 IotAlertConfigReceiveTypeEnum、DeviceStateEnum 没用到,貌似可以删除下?
 | // TODO @puhui999:下面 IotAlertConfigReceiveTypeEnum、DeviceStateEnum 没用到,貌似可以删除下?
 | ||||||
| const IotAlertConfigReceiveTypeEnum = { | const IotAlertConfigReceiveTypeEnum = { | ||||||
|   SMS: 1, // 短信
 |   SMS: 1, // 短信
 | ||||||
|  | @ -126,7 +144,7 @@ interface RuleSceneFormData { | ||||||
|   name: string |   name: string | ||||||
|   description?: string |   description?: string | ||||||
|   status: number |   status: number | ||||||
|   triggers: TriggerFormData[] |   trigger: TriggerFormData // 改为单个触发器
 | ||||||
|   actions: ActionFormData[] |   actions: ActionFormData[] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -138,7 +156,9 @@ interface TriggerFormData { | ||||||
|   operator?: string |   operator?: string | ||||||
|   value?: string |   value?: string | ||||||
|   cronExpression?: string |   cronExpression?: string | ||||||
|   conditionGroups?: ConditionGroupFormData[] |   // 新的条件结构
 | ||||||
|  |   mainCondition?: ConditionFormData // 主条件(必须满足)
 | ||||||
|  |   conditionGroup?: ConditionGroupContainerFormData // 条件组容器(可选,与主条件为且关系)
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface ActionFormData { | interface ActionFormData { | ||||||
|  | @ -149,18 +169,33 @@ interface ActionFormData { | ||||||
|   alertConfigId?: number |   alertConfigId?: number | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // 条件组容器(包含多个子条件组,子条件组间为或关系)
 | ||||||
|  | interface ConditionGroupContainerFormData { | ||||||
|  |   subGroups: SubConditionGroupFormData[] // 子条件组数组,子条件组间为或关系
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 子条件组(内部条件为且关系)
 | ||||||
|  | interface SubConditionGroupFormData { | ||||||
|  |   conditions: ConditionFormData[] // 条件数组,条件间为且关系
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 保留原有接口用于兼容性
 | ||||||
| interface ConditionGroupFormData { | interface ConditionGroupFormData { | ||||||
|   conditions: ConditionFormData[] |   conditions: ConditionFormData[] | ||||||
|  |   // 注意:条件组内部的条件固定为"且"关系,条件组之间固定为"或"关系
 | ||||||
|  |   // logicOperator 字段保留用于兼容性,但在UI中固定为 'AND'
 | ||||||
|   logicOperator: 'AND' | 'OR' |   logicOperator: 'AND' | 'OR' | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface ConditionFormData { | interface ConditionFormData { | ||||||
|   type: number |   type: number // 条件类型:1-设备状态,2-设备属性,3-当前时间
 | ||||||
|   productId: number |   productId?: number // 产品ID(设备状态和设备属性时必填)
 | ||||||
|   deviceId: number |   deviceId?: number // 设备ID(设备状态和设备属性时必填)
 | ||||||
|   identifier: string |   identifier?: string // 标识符(设备属性时必填)
 | ||||||
|   operator: string |   operator: string // 操作符
 | ||||||
|   param: string |   param: string // 参数值
 | ||||||
|  |   timeValue?: string // 时间值(当前时间条件时使用)
 | ||||||
|  |   timeValue2?: string // 第二个时间值(时间范围条件时使用)
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // 主接口
 | // 主接口
 | ||||||
|  | @ -173,12 +208,13 @@ interface IotRuleScene extends TenantBaseDO { | ||||||
|   actions: ActionConfig[] // 执行器数组(必填,至少一个)
 |   actions: ActionConfig[] // 执行器数组(必填,至少一个)
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // 工具类型
 | // 工具类型 - 从枚举中提取类型
 | ||||||
| // TODO @puhui999:这些在瞅瞅~
 | export type TriggerType = | ||||||
| type TriggerType = (typeof IotRuleSceneTriggerTypeEnum)[keyof typeof IotRuleSceneTriggerTypeEnum] |   (typeof IotRuleSceneTriggerTypeEnum)[keyof typeof IotRuleSceneTriggerTypeEnum] | ||||||
| type ActionType = (typeof IotRuleSceneActionTypeEnum)[keyof typeof IotRuleSceneActionTypeEnum] | export type ActionType = | ||||||
| type MessageType = (typeof IotDeviceMessageTypeEnum)[keyof typeof IotDeviceMessageTypeEnum] |   (typeof IotRuleSceneActionTypeEnum)[keyof typeof IotRuleSceneActionTypeEnum] | ||||||
| type OperatorType = | export type MessageType = (typeof IotDeviceMessageTypeEnum)[keyof typeof IotDeviceMessageTypeEnum] | ||||||
|  | export type OperatorType = | ||||||
|   (typeof IotRuleSceneTriggerConditionParameterOperatorEnum)[keyof typeof IotRuleSceneTriggerConditionParameterOperatorEnum]['value'] |   (typeof IotRuleSceneTriggerConditionParameterOperatorEnum)[keyof typeof IotRuleSceneTriggerConditionParameterOperatorEnum]['value'] | ||||||
| 
 | 
 | ||||||
| // 表单验证规则类型
 | // 表单验证规则类型
 | ||||||
|  | @ -207,12 +243,16 @@ export { | ||||||
|   TriggerFormData, |   TriggerFormData, | ||||||
|   ActionFormData, |   ActionFormData, | ||||||
|   ConditionGroupFormData, |   ConditionGroupFormData, | ||||||
|  |   ConditionGroupContainerFormData, | ||||||
|  |   SubConditionGroupFormData, | ||||||
|   ConditionFormData, |   ConditionFormData, | ||||||
|   IotRuleSceneTriggerTypeEnum, |   IotRuleSceneTriggerTypeEnum, | ||||||
|   IotRuleSceneActionTypeEnum, |   IotRuleSceneActionTypeEnum, | ||||||
|   IotDeviceMessageTypeEnum, |   IotDeviceMessageTypeEnum, | ||||||
|   IotDeviceMessageIdentifierEnum, |   IotDeviceMessageIdentifierEnum, | ||||||
|   IotRuleSceneTriggerConditionParameterOperatorEnum, |   IotRuleSceneTriggerConditionParameterOperatorEnum, | ||||||
|  |   IotRuleSceneTriggerConditionTypeEnum, | ||||||
|  |   IotRuleSceneTriggerTimeOperatorEnum, | ||||||
|   IotAlertConfigReceiveTypeEnum, |   IotAlertConfigReceiveTypeEnum, | ||||||
|   DeviceStateEnum, |   DeviceStateEnum, | ||||||
|   CommonStatusEnum, |   CommonStatusEnum, | ||||||
|  |  | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -1,469 +0,0 @@ | ||||||
| // TODO @puhui999:这些后续需要删除哈 |  | ||||||
| # 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设备的建模需求。 |  | ||||||
|  | @ -1,305 +0,0 @@ | ||||||
| <!-- IoT场景联动规则表单 - 主表单组件 --> |  | ||||||
| <!-- TODO @puhui999:要不搞个 form 目录,不用 components;保持和别的模块风格一致哈; --> |  | ||||||
| <template> |  | ||||||
|   <el-drawer |  | ||||||
|     v-model="drawerVisible" |  | ||||||
|     :title="drawerTitle" |  | ||||||
|     size="80%" |  | ||||||
|     direction="rtl" |  | ||||||
|     :close-on-click-modal="false" |  | ||||||
|     :close-on-press-escape="false" |  | ||||||
|     @close="handleClose" |  | ||||||
|     class="rule-scene-drawer" |  | ||||||
|   > |  | ||||||
|     <div class="rule-scene-form"> |  | ||||||
|       <el-form |  | ||||||
|         ref="formRef" |  | ||||||
|         :model="formData" |  | ||||||
|         :rules="formRules" |  | ||||||
|         label-width="120px" |  | ||||||
|         class="form-container" |  | ||||||
|       > |  | ||||||
|         <!-- 基础信息配置 --> |  | ||||||
|         <BasicInfoSection v-model="formData" :rules="formRules" /> |  | ||||||
| 
 |  | ||||||
|         <!-- 触发器配置 --> |  | ||||||
|         <TriggerSection v-model:triggers="formData.triggers" @validate="handleTriggerValidate" /> |  | ||||||
| 
 |  | ||||||
|         <!-- 执行器配置 --> |  | ||||||
|         <ActionSection v-model:actions="formData.actions" @validate="handleActionValidate" /> |  | ||||||
| 
 |  | ||||||
|         <!-- 预览区域 --> |  | ||||||
|         <PreviewSection |  | ||||||
|           :form-data="formData" |  | ||||||
|           :validation-result="validationResult" |  | ||||||
|           @validate="handleValidate" |  | ||||||
|         /> |  | ||||||
|       </el-form> |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|     <!-- 抽屉底部操作栏 --> |  | ||||||
|     <!-- TODO @puhui999:这个按钮逻辑,和别的模块一致 --> |  | ||||||
|     <template #footer> |  | ||||||
|       <div class="drawer-footer"> |  | ||||||
|         <el-button @click="handleClose" size="large">取消</el-button> |  | ||||||
|         <el-button |  | ||||||
|           type="primary" |  | ||||||
|           @click="handleSubmit" |  | ||||||
|           :loading="submitLoading" |  | ||||||
|           :disabled="!canSubmit" |  | ||||||
|           size="large" |  | ||||||
|         > |  | ||||||
|           {{ isEdit ? '更新' : '创建' }} |  | ||||||
|         </el-button> |  | ||||||
|       </div> |  | ||||||
|     </template> |  | ||||||
|   </el-drawer> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| 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 { getBaseValidationRules } from '../utils/validation' |  | ||||||
| import { transformFormToApi, transformApiToForm, createDefaultFormData } from '../utils/transform' |  | ||||||
| import { handleValidationError, showSuccess, withErrorHandling } from '../utils/errorHandler' |  | ||||||
| 
 |  | ||||||
| /** IoT 场景联动规则表单 - 主表单组件 */ |  | ||||||
| defineOptions({ name: 'RuleSceneForm' }) |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   modelValue: boolean |  | ||||||
|   ruleScene?: IotRuleScene |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface Emits { |  | ||||||
|   (e: 'update:modelValue', value: boolean): void |  | ||||||
|   (e: 'success'): void |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const props = defineProps<Props>() |  | ||||||
| const emit = defineEmits<Emits>() |  | ||||||
| 
 |  | ||||||
| const drawerVisible = useVModel(props, 'modelValue', emit) |  | ||||||
| 
 |  | ||||||
| // 表单数据和状态 |  | ||||||
| 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: '' }) |  | ||||||
| const actionValidation = ref({ valid: true, message: '' }) |  | ||||||
| 
 |  | ||||||
| // 计算属性 |  | ||||||
| const isEdit = computed(() => !!props.ruleScene?.id) |  | ||||||
| const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' : '新增场景联动规则')) // TODO @puhui999:这个风格,和别的模块一致; |  | ||||||
| 
 |  | ||||||
| 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 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 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: '验证通过' } |  | ||||||
|     showSuccess('规则验证通过') |  | ||||||
|     return true |  | ||||||
|   } catch (error: any) { |  | ||||||
|     const message = error.message || '表单验证失败' |  | ||||||
|     validationResult.value = { valid: false, message } |  | ||||||
|     await handleValidationError(message, 'rule-scene-form') |  | ||||||
|     return false |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // TODO @puhui999:参考下别的模块,不用这么复杂哈; |  | ||||||
| const handleSubmit = async () => { |  | ||||||
|   const result = await withErrorHandling( |  | ||||||
|     async () => { |  | ||||||
|       // 验证表单 |  | ||||||
|       const isValid = await handleValidate() |  | ||||||
|       if (!isValid) { |  | ||||||
|         throw new Error('表单验证失败') |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       // 转换数据格式 |  | ||||||
|       const apiData = transformFormToApi(formData.value) |  | ||||||
| 
 |  | ||||||
|       // 这里应该调用API保存数据 |  | ||||||
|       console.log('提交数据:', apiData) |  | ||||||
| 
 |  | ||||||
|       // 模拟API调用 |  | ||||||
|       await new Promise((resolve) => setTimeout(resolve, 1000)) |  | ||||||
| 
 |  | ||||||
|       return apiData |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       loadingKey: 'rule-scene-submit', |  | ||||||
|       loadingText: isEdit.value ? '更新中...' : '创建中...', |  | ||||||
|       context: 'rule-scene-form', |  | ||||||
|       showSuccess: true, |  | ||||||
|       successMessage: isEdit.value ? '更新成功' : '创建成功' |  | ||||||
|     } |  | ||||||
|   ) |  | ||||||
| 
 |  | ||||||
|   if (result) { |  | ||||||
|     emit('success') |  | ||||||
|     handleClose() |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const handleClose = () => { |  | ||||||
|   drawerVisible.value = false |  | ||||||
|   validationResult.value = null |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 初始化表单数据 |  | ||||||
| const initFormData = () => { |  | ||||||
|   if (props.ruleScene) { |  | ||||||
|     formData.value = transformApiToForm(props.ruleScene) |  | ||||||
|   } else { |  | ||||||
|     formData.value = createDefaultFormData() |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 监听抽屉显示 |  | ||||||
| watch(drawerVisible, (visible) => { |  | ||||||
|   if (visible) { |  | ||||||
|     initFormData() |  | ||||||
|     nextTick(() => { |  | ||||||
|       formRef.value?.clearValidate() |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| // 监听props变化 |  | ||||||
| watch( |  | ||||||
|   () => props.ruleScene, |  | ||||||
|   () => { |  | ||||||
|     if (drawerVisible.value) { |  | ||||||
|       initFormData() |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| ) |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .rule-scene-drawer { |  | ||||||
|   --el-drawer-padding-primary: 20px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .rule-scene-form { |  | ||||||
|   height: calc(100vh - 120px); |  | ||||||
|   overflow-y: auto; |  | ||||||
|   padding: 20px; |  | ||||||
|   padding-bottom: 80px; /* 为底部操作栏留出空间 */ |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .form-container { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 24px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .drawer-footer { |  | ||||||
|   position: absolute; |  | ||||||
|   bottom: 0; |  | ||||||
|   left: 0; |  | ||||||
|   right: 0; |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: flex-end; |  | ||||||
|   gap: 16px; |  | ||||||
|   padding: 16px 20px; |  | ||||||
|   background: var(--el-bg-color); |  | ||||||
|   border-top: 1px solid var(--el-border-color-light); |  | ||||||
|   box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /* 滚动条样式 */ |  | ||||||
| .rule-scene-form::-webkit-scrollbar { |  | ||||||
|   width: 6px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .rule-scene-form::-webkit-scrollbar-track { |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-radius: 3px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .rule-scene-form::-webkit-scrollbar-thumb { |  | ||||||
|   background: var(--el-border-color); |  | ||||||
|   border-radius: 3px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .rule-scene-form::-webkit-scrollbar-thumb:hover { |  | ||||||
|   background: var(--el-border-color-dark); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /* 抽屉内容区域优化 */ |  | ||||||
| :deep(.el-drawer__body) { |  | ||||||
|   padding: 0; |  | ||||||
|   position: relative; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-drawer__header) { |  | ||||||
|   padding: 20px 20px 16px 20px; |  | ||||||
|   border-bottom: 1px solid var(--el-border-color-light); |  | ||||||
|   margin-bottom: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-drawer__title) { |  | ||||||
|   font-size: 18px; |  | ||||||
|   font-weight: 600; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /* 响应式设计 */ |  | ||||||
| @media (max-width: 768px) { |  | ||||||
|   .rule-scene-drawer { |  | ||||||
|     --el-drawer-size: 100% !important; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   .rule-scene-form { |  | ||||||
|     padding: 16px; |  | ||||||
|     padding-bottom: 80px; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   .form-container { |  | ||||||
|     gap: 20px; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   .drawer-footer { |  | ||||||
|     padding: 12px 16px; |  | ||||||
|     gap: 12px; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -1,265 +0,0 @@ | ||||||
| <!-- 单个条件配置组件 --> |  | ||||||
| <!-- TODO @puhui999:这里需要在对下阿里云 IoT,不太对;它是条件类型;然后选择产品、设备;接着选条件类型对应的比较; --> |  | ||||||
| <template> |  | ||||||
|   <div class="condition-config"> |  | ||||||
|     <el-row :gutter="16"> |  | ||||||
|       <!-- 属性/事件/服务选择 --> |  | ||||||
|       <el-col :span="8"> |  | ||||||
|         <el-form-item label="监控项" required> |  | ||||||
|           <PropertySelector |  | ||||||
|             :model-value="condition.identifier" |  | ||||||
|             @update:model-value="(value) => updateConditionField('identifier', value)" |  | ||||||
|             :trigger-type="triggerType" |  | ||||||
|             :product-id="productId" |  | ||||||
|             :device-id="deviceId" |  | ||||||
|             @change="handlePropertyChange" |  | ||||||
|           /> |  | ||||||
|         </el-form-item> |  | ||||||
|       </el-col> |  | ||||||
| 
 |  | ||||||
|       <!-- 操作符选择 --> |  | ||||||
|       <el-col :span="6"> |  | ||||||
|         <el-form-item label="操作符" required> |  | ||||||
|           <OperatorSelector |  | ||||||
|             :model-value="condition.operator" |  | ||||||
|             @update:model-value="(value) => updateConditionField('operator', value)" |  | ||||||
|             :property-type="propertyType" |  | ||||||
|             @change="handleOperatorChange" |  | ||||||
|           /> |  | ||||||
|         </el-form-item> |  | ||||||
|       </el-col> |  | ||||||
| 
 |  | ||||||
|       <!-- 值输入 --> |  | ||||||
|       <el-col :span="10"> |  | ||||||
|         <el-form-item label="比较值" required> |  | ||||||
|           <ValueInput |  | ||||||
|             :model-value="condition.param" |  | ||||||
|             @update:model-value="(value) => updateConditionField('param', value)" |  | ||||||
|             :property-type="propertyType" |  | ||||||
|             :operator="condition.operator" |  | ||||||
|             :property-config="propertyConfig" |  | ||||||
|             @validate="handleValueValidate" |  | ||||||
|           /> |  | ||||||
|         </el-form-item> |  | ||||||
|       </el-col> |  | ||||||
|     </el-row> |  | ||||||
| 
 |  | ||||||
|     <!-- 条件预览 --> |  | ||||||
|     <div v-if="conditionPreview" class="condition-preview"> |  | ||||||
|       <div class="preview-header"> |  | ||||||
|         <Icon icon="ep:view" class="preview-icon" /> |  | ||||||
|         <span class="preview-title">条件预览</span> |  | ||||||
|       </div> |  | ||||||
|       <div class="preview-content"> |  | ||||||
|         <code class="preview-text">{{ conditionPreview }}</code> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|     <!-- 验证结果 --> |  | ||||||
|     <div v-if="validationMessage" class="validation-result"> |  | ||||||
|       <el-alert |  | ||||||
|         :title="validationMessage" |  | ||||||
|         :type="isValid ? 'success' : 'error'" |  | ||||||
|         :closable="false" |  | ||||||
|         show-icon |  | ||||||
|       /> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useVModel } from '@vueuse/core' |  | ||||||
| import PropertySelector from '../selectors/PropertySelector.vue' |  | ||||||
| import OperatorSelector from '../selectors/OperatorSelector.vue' |  | ||||||
| import ValueInput from '../inputs/ValueInput.vue' |  | ||||||
| import { ConditionFormData } from '@/api/iot/rule/scene/scene.types' |  | ||||||
| 
 |  | ||||||
| /** 单个条件配置组件 */ |  | ||||||
| defineOptions({ name: 'ConditionConfig' }) |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   modelValue: ConditionFormData |  | ||||||
|   triggerType: number |  | ||||||
|   productId?: number |  | ||||||
|   deviceId?: number |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface Emits { |  | ||||||
|   (e: 'update:modelValue', value: ConditionFormData): void |  | ||||||
|   (e: 'validate', result: { valid: boolean; message: string }): void |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const props = defineProps<Props>() |  | ||||||
| const emit = defineEmits<Emits>() |  | ||||||
| 
 |  | ||||||
| const condition = useVModel(props, 'modelValue', emit) |  | ||||||
| 
 |  | ||||||
| // 状态 |  | ||||||
| const propertyType = ref<string>('string') |  | ||||||
| const propertyConfig = ref<any>(null) |  | ||||||
| const validationMessage = ref('') |  | ||||||
| const isValid = ref(true) |  | ||||||
| const valueValidation = ref({ valid: true, message: '' }) |  | ||||||
| 
 |  | ||||||
| // 计算属性 |  | ||||||
| const conditionPreview = computed(() => { |  | ||||||
|   if (!condition.value.identifier || !condition.value.operator || !condition.value.param) { |  | ||||||
|     return '' |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   const propertyName = propertyConfig.value?.name || condition.value.identifier |  | ||||||
|   const operatorText = getOperatorText(condition.value.operator) |  | ||||||
|   const value = condition.value.param |  | ||||||
| 
 |  | ||||||
|   return `当 ${propertyName} ${operatorText} ${value} 时触发` |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| // 工具函数 |  | ||||||
| const getOperatorText = (operator: string) => { |  | ||||||
|   const operatorMap = { |  | ||||||
|     '=': '等于', |  | ||||||
|     '!=': '不等于', |  | ||||||
|     '>': '大于', |  | ||||||
|     '>=': '大于等于', |  | ||||||
|     '<': '小于', |  | ||||||
|     '<=': '小于等于', |  | ||||||
|     in: '包含于', |  | ||||||
|     between: '介于' |  | ||||||
|   } |  | ||||||
|   return operatorMap[operator] || operator |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 事件处理 |  | ||||||
| const updateConditionField = (field: keyof ConditionFormData, value: any) => { |  | ||||||
|   condition.value[field] = value |  | ||||||
|   emit('update:modelValue', condition.value) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const handlePropertyChange = (propertyInfo: { type: string; config: any }) => { |  | ||||||
|   propertyType.value = propertyInfo.type |  | ||||||
|   propertyConfig.value = propertyInfo.config |  | ||||||
| 
 |  | ||||||
|   // 重置操作符和值 |  | ||||||
|   condition.value.operator = '=' |  | ||||||
|   condition.value.param = '' |  | ||||||
| 
 |  | ||||||
|   updateValidationResult() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const handleOperatorChange = () => { |  | ||||||
|   // 重置值 |  | ||||||
|   condition.value.param = '' |  | ||||||
|   updateValidationResult() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const handleValueValidate = (result: { valid: boolean; message: string }) => { |  | ||||||
|   valueValidation.value = result |  | ||||||
|   updateValidationResult() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const updateValidationResult = () => { |  | ||||||
|   // 基础验证 |  | ||||||
|   if (!condition.value.identifier) { |  | ||||||
|     isValid.value = false |  | ||||||
|     validationMessage.value = '请选择监控项' |  | ||||||
|     emit('validate', { valid: false, message: validationMessage.value }) |  | ||||||
|     return |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   if (!condition.value.operator) { |  | ||||||
|     isValid.value = false |  | ||||||
|     validationMessage.value = '请选择操作符' |  | ||||||
|     emit('validate', { valid: false, message: validationMessage.value }) |  | ||||||
|     return |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   if (!condition.value.param) { |  | ||||||
|     isValid.value = false |  | ||||||
|     validationMessage.value = '请输入比较值' |  | ||||||
|     emit('validate', { valid: false, message: validationMessage.value }) |  | ||||||
|     return |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // 值验证 |  | ||||||
|   if (!valueValidation.value.valid) { |  | ||||||
|     isValid.value = false |  | ||||||
|     validationMessage.value = valueValidation.value.message |  | ||||||
|     emit('validate', { valid: false, message: validationMessage.value }) |  | ||||||
|     return |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // 验证通过 |  | ||||||
|   isValid.value = true |  | ||||||
|   validationMessage.value = '条件配置验证通过' |  | ||||||
|   emit('validate', { valid: true, message: validationMessage.value }) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 监听条件变化 |  | ||||||
| watch( |  | ||||||
|   () => [condition.value.identifier, condition.value.operator, condition.value.param], |  | ||||||
|   () => { |  | ||||||
|     updateValidationResult() |  | ||||||
|   }, |  | ||||||
|   { deep: true } |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // 初始化 |  | ||||||
| onMounted(() => { |  | ||||||
|   updateValidationResult() |  | ||||||
| }) |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .condition-config { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .condition-preview { |  | ||||||
|   padding: 12px; |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   border: 1px solid var(--el-border-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .preview-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 6px; |  | ||||||
|   margin-bottom: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .preview-icon { |  | ||||||
|   color: var(--el-color-primary); |  | ||||||
|   font-size: 14px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .preview-title { |  | ||||||
|   font-size: 12px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .preview-content { |  | ||||||
|   margin-left: 20px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .preview-text { |  | ||||||
|   font-size: 14px; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
|   background: var(--el-fill-color-blank); |  | ||||||
|   padding: 8px 12px; |  | ||||||
|   border-radius: 4px; |  | ||||||
|   display: block; |  | ||||||
|   font-family: inherit; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .validation-result { |  | ||||||
|   margin-top: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-form-item) { |  | ||||||
|   margin-bottom: 0; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -1,347 +0,0 @@ | ||||||
| <!-- 设备触发配置组件 --> |  | ||||||
| <template> |  | ||||||
|   <div class="device-trigger-config"> |  | ||||||
|     <!-- 产品和设备选择 --> |  | ||||||
|     <ProductDeviceSelector |  | ||||||
|       v-model:product-id="trigger.productId" |  | ||||||
|       v-model:device-id="trigger.deviceId" |  | ||||||
|       @change="handleDeviceChange" |  | ||||||
|     /> |  | ||||||
| 
 |  | ||||||
|     <!-- TODO @puhui999:这里有点冗余,建议去掉 --> |  | ||||||
|     <!-- 设备状态变更提示 --> |  | ||||||
|     <div v-if="trigger.type === TriggerTypeEnum.DEVICE_STATE_UPDATE" class="state-update-notice"> |  | ||||||
|       <el-alert title="设备状态变更触发" type="info" :closable="false" show-icon> |  | ||||||
|         <template #default> |  | ||||||
|           <p>当选中的设备上线或离线时将自动触发场景规则</p> |  | ||||||
|           <p class="notice-tip">无需配置额外的触发条件</p> |  | ||||||
|         </template> |  | ||||||
|       </el-alert> |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|     <!-- 条件组配置 --> |  | ||||||
|     <div v-else-if="needsConditions" class="condition-groups"> |  | ||||||
|       <div class="condition-groups-header"> |  | ||||||
|         <div class="header-left"> |  | ||||||
|           <span class="header-title">触发条件</span> |  | ||||||
|           <!-- TODO @puhui999:去掉数量限制 --> |  | ||||||
|           <el-tag size="small" type="info"> |  | ||||||
|             {{ trigger.conditionGroups?.length || 0 }}/{{ maxConditionGroups }} |  | ||||||
|           </el-tag> |  | ||||||
|         </div> |  | ||||||
|         <div class="header-right"> |  | ||||||
|           <el-button |  | ||||||
|             type="primary" |  | ||||||
|             size="small" |  | ||||||
|             @click="addConditionGroup" |  | ||||||
|             :disabled="(trigger.conditionGroups?.length || 0) >= maxConditionGroups" |  | ||||||
|           > |  | ||||||
|             <Icon icon="ep:plus" /> |  | ||||||
|             添加条件组 |  | ||||||
|           </el-button> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <!-- 条件组列表 --> |  | ||||||
|       <div |  | ||||||
|         v-if="trigger.conditionGroups && trigger.conditionGroups.length > 0" |  | ||||||
|         class="condition-groups-list" |  | ||||||
|       > |  | ||||||
|         <div |  | ||||||
|           v-for="(group, groupIndex) in trigger.conditionGroups" |  | ||||||
|           :key="`group-${groupIndex}`" |  | ||||||
|           class="condition-group" |  | ||||||
|         > |  | ||||||
|           <div class="group-header"> |  | ||||||
|             <div class="group-title"> |  | ||||||
|               <span>条件组 {{ groupIndex + 1 }}</span> |  | ||||||
|               <!-- TODO @puhui999:不用“且、或”哈。条件组之间,就是或;条件之间就是且 --> |  | ||||||
|               <el-select |  | ||||||
|                 v-model="group.logicOperator" |  | ||||||
|                 size="small" |  | ||||||
|                 style="width: 80px; margin-left: 12px" |  | ||||||
|               > |  | ||||||
|                 <el-option label="且" value="AND" /> |  | ||||||
|                 <el-option label="或" value="OR" /> |  | ||||||
|               </el-select> |  | ||||||
|             </div> |  | ||||||
|             <el-button |  | ||||||
|               type="danger" |  | ||||||
|               size="small" |  | ||||||
|               text |  | ||||||
|               @click="removeConditionGroup(groupIndex)" |  | ||||||
|               v-if="trigger.conditionGroups!.length > 1" |  | ||||||
|             > |  | ||||||
|               <Icon icon="ep:delete" /> |  | ||||||
|               删除组 |  | ||||||
|             </el-button> |  | ||||||
|           </div> |  | ||||||
| 
 |  | ||||||
|           <ConditionGroupConfig |  | ||||||
|             :model-value="group" |  | ||||||
|             @update:model-value="(value) => updateConditionGroup(groupIndex, value)" |  | ||||||
|             :trigger-type="trigger.type" |  | ||||||
|             :product-id="trigger.productId" |  | ||||||
|             :device-id="trigger.deviceId" |  | ||||||
|             @validate="(result) => handleGroupValidate(groupIndex, result)" |  | ||||||
|           /> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <!-- 空状态 --> |  | ||||||
|       <div v-else class="empty-conditions"> |  | ||||||
|         <el-empty description="暂无触发条件"> |  | ||||||
|           <el-button type="primary" @click="addConditionGroup"> |  | ||||||
|             <Icon icon="ep:plus" /> |  | ||||||
|             添加第一个条件组 |  | ||||||
|           </el-button> |  | ||||||
|         </el-empty> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|     <!-- 验证结果 --> |  | ||||||
|     <div v-if="validationMessage" class="validation-result"> |  | ||||||
|       <el-alert |  | ||||||
|         :title="validationMessage" |  | ||||||
|         :type="isValid ? 'success' : 'error'" |  | ||||||
|         :closable="false" |  | ||||||
|         show-icon |  | ||||||
|       /> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useVModel } from '@vueuse/core' |  | ||||||
| import ProductDeviceSelector from '../selectors/ProductDeviceSelector.vue' |  | ||||||
| import ConditionGroupConfig from './ConditionGroupConfig.vue' |  | ||||||
| import { |  | ||||||
|   TriggerFormData, |  | ||||||
|   ConditionGroupFormData, |  | ||||||
|   IotRuleSceneTriggerTypeEnum as TriggerTypeEnum |  | ||||||
| } from '@/api/iot/rule/scene/scene.types' |  | ||||||
| 
 |  | ||||||
| /** 设备触发配置组件 */ |  | ||||||
| defineOptions({ name: 'DeviceTriggerConfig' }) |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   modelValue: TriggerFormData |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface Emits { |  | ||||||
|   (e: 'update:modelValue', value: TriggerFormData): void |  | ||||||
|   (e: 'validate', result: { valid: boolean; message: string }): void |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const props = defineProps<Props>() |  | ||||||
| const emit = defineEmits<Emits>() |  | ||||||
| 
 |  | ||||||
| const trigger = useVModel(props, 'modelValue', emit) |  | ||||||
| 
 |  | ||||||
| // 配置常量 |  | ||||||
| const maxConditionGroups = 3 |  | ||||||
| 
 |  | ||||||
| // 验证状态 |  | ||||||
| const groupValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({}) |  | ||||||
| const validationMessage = ref('') |  | ||||||
| const isValid = ref(true) |  | ||||||
| 
 |  | ||||||
| // 计算属性 |  | ||||||
| const needsConditions = computed(() => { |  | ||||||
|   return trigger.value.type !== TriggerTypeEnum.DEVICE_STATE_UPDATE |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| // 事件处理 |  | ||||||
| const updateConditionGroup = (index: number, group: ConditionGroupFormData) => { |  | ||||||
|   if (trigger.value.conditionGroups) { |  | ||||||
|     trigger.value.conditionGroups[index] = group |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const handleDeviceChange = ({ productId, deviceId }: { productId?: number; deviceId?: number }) => { |  | ||||||
|   trigger.value.productId = productId |  | ||||||
|   trigger.value.deviceId = deviceId |  | ||||||
|   updateValidationResult() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const addConditionGroup = () => { |  | ||||||
|   if (!trigger.value.conditionGroups) { |  | ||||||
|     trigger.value.conditionGroups = [] |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   if (trigger.value.conditionGroups.length >= maxConditionGroups) { |  | ||||||
|     return |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   const newGroup: ConditionGroupFormData = { |  | ||||||
|     conditions: [], |  | ||||||
|     logicOperator: 'AND' |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   trigger.value.conditionGroups.push(newGroup) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const removeConditionGroup = (index: number) => { |  | ||||||
|   if (trigger.value.conditionGroups) { |  | ||||||
|     trigger.value.conditionGroups.splice(index, 1) |  | ||||||
|     delete groupValidations.value[index] |  | ||||||
| 
 |  | ||||||
|     // 重新索引验证结果 |  | ||||||
|     const newValidations: { [key: number]: { valid: boolean; message: string } } = {} |  | ||||||
|     Object.keys(groupValidations.value).forEach((key) => { |  | ||||||
|       const numKey = parseInt(key) |  | ||||||
|       if (numKey > index) { |  | ||||||
|         newValidations[numKey - 1] = groupValidations.value[numKey] |  | ||||||
|       } else if (numKey < index) { |  | ||||||
|         newValidations[numKey] = groupValidations.value[numKey] |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|     groupValidations.value = newValidations |  | ||||||
| 
 |  | ||||||
|     updateValidationResult() |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const handleGroupValidate = (index: number, result: { valid: boolean; message: string }) => { |  | ||||||
|   groupValidations.value[index] = result |  | ||||||
|   updateValidationResult() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const updateValidationResult = () => { |  | ||||||
|   // 基础验证 |  | ||||||
|   if (!trigger.value.productId || !trigger.value.deviceId) { |  | ||||||
|     isValid.value = false |  | ||||||
|     validationMessage.value = '请选择产品和设备' |  | ||||||
|     emit('validate', { valid: false, message: validationMessage.value }) |  | ||||||
|     return |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // 设备状态变更不需要条件验证 |  | ||||||
|   if (trigger.value.type === TriggerTypeEnum.DEVICE_STATE_UPDATE) { |  | ||||||
|     isValid.value = true |  | ||||||
|     validationMessage.value = '设备触发配置验证通过' |  | ||||||
|     emit('validate', { valid: true, message: validationMessage.value }) |  | ||||||
|     return |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // 条件组验证 |  | ||||||
|   if (!trigger.value.conditionGroups || trigger.value.conditionGroups.length === 0) { |  | ||||||
|     isValid.value = false |  | ||||||
|     validationMessage.value = '请至少添加一个触发条件组' |  | ||||||
|     emit('validate', { valid: false, message: validationMessage.value }) |  | ||||||
|     return |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   const validations = Object.values(groupValidations.value) |  | ||||||
|   const allValid = validations.every((v) => v.valid) |  | ||||||
| 
 |  | ||||||
|   if (allValid) { |  | ||||||
|     isValid.value = true |  | ||||||
|     validationMessage.value = '设备触发配置验证通过' |  | ||||||
|   } else { |  | ||||||
|     isValid.value = false |  | ||||||
|     const errorMessages = validations.filter((v) => !v.valid).map((v) => v.message) |  | ||||||
|     validationMessage.value = `条件组配置错误: ${errorMessages.join('; ')}` |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   emit('validate', { valid: isValid.value, message: validationMessage.value }) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 监听触发器类型变化 |  | ||||||
| watch( |  | ||||||
|   () => trigger.value.type, |  | ||||||
|   () => { |  | ||||||
|     updateValidationResult() |  | ||||||
|   } |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // 监听产品设备变化 |  | ||||||
| watch( |  | ||||||
|   () => [trigger.value.productId, trigger.value.deviceId], |  | ||||||
|   () => { |  | ||||||
|     updateValidationResult() |  | ||||||
|   } |  | ||||||
| ) |  | ||||||
| // TODO @puhui999:unocss |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .device-trigger-config { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .state-update-notice { |  | ||||||
|   margin-top: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .notice-tip { |  | ||||||
|   margin: 4px 0 0 0; |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .condition-groups-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   margin-bottom: 12px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-left { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-title { |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-right { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .condition-groups-list { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 12px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .condition-group { |  | ||||||
|   border: 1px solid var(--el-border-color-lighter); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   background: var(--el-fill-color-blank); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .group-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   padding: 12px 16px; |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-bottom: 1px solid var(--el-border-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .group-title { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .empty-conditions { |  | ||||||
|   padding: 40px 0; |  | ||||||
|   text-align: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .validation-result { |  | ||||||
|   margin-top: 8px; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -1,139 +0,0 @@ | ||||||
| <!-- 定时触发配置组件 --> |  | ||||||
| <template> |  | ||||||
|   <div class="timer-trigger-config"> |  | ||||||
|     <div class="config-header"> |  | ||||||
|       <div class="header-left"> |  | ||||||
|         <Icon icon="ep:timer" class="header-icon" /> |  | ||||||
|         <span class="header-title">定时触发配置</span> |  | ||||||
|       </div> |  | ||||||
|       <div class="header-right"> |  | ||||||
|         <el-button type="text" size="small" @click="showBuilder = !showBuilder"> |  | ||||||
|           <Icon :icon="showBuilder ? 'ep:edit' : 'ep:setting'" /> |  | ||||||
|           {{ showBuilder ? '手动编辑' : '可视化编辑' }} |  | ||||||
|         </el-button> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|     <!-- 可视化编辑器 --> |  | ||||||
|     <!-- TODO @puhui999:是不是复用现有的 cron 组件;不然有点重复哈;维护比较复杂 --> |  | ||||||
|     <div v-if="showBuilder" class="visual-builder"> |  | ||||||
|       <CronBuilder v-model="localValue" @validate="handleValidate" /> |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|     <!-- 手动编辑 --> |  | ||||||
|     <div v-else class="manual-editor"> |  | ||||||
|       <el-form-item label="CRON表达式" required> |  | ||||||
|         <CronInput v-model="localValue" @validate="handleValidate" /> |  | ||||||
|       </el-form-item> |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|     <!-- 下次执行时间预览 --> |  | ||||||
|     <NextExecutionPreview :cron-expression="localValue" /> |  | ||||||
| 
 |  | ||||||
|     <!-- 验证结果 --> |  | ||||||
|     <div v-if="validationMessage" class="validation-result"> |  | ||||||
|       <el-alert |  | ||||||
|         :title="validationMessage" |  | ||||||
|         :type="isValid ? 'success' : 'error'" |  | ||||||
|         :closable="false" |  | ||||||
|         show-icon |  | ||||||
|       /> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useVModel } from '@vueuse/core' |  | ||||||
| import CronBuilder from '../inputs/CronBuilder.vue' |  | ||||||
| import CronInput from '../inputs/CronInput.vue' |  | ||||||
| import NextExecutionPreview from '../previews/NextExecutionPreview.vue' |  | ||||||
| 
 |  | ||||||
| /** 定时触发配置组件 */ |  | ||||||
| defineOptions({ name: 'TimerTriggerConfig' }) |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   modelValue?: string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface Emits { |  | ||||||
|   (e: 'update:modelValue', value: string): void |  | ||||||
|   (e: 'validate', result: { valid: boolean; message: string }): void |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const props = defineProps<Props>() |  | ||||||
| const emit = defineEmits<Emits>() |  | ||||||
| 
 |  | ||||||
| const localValue = useVModel(props, 'modelValue', emit, { |  | ||||||
|   defaultValue: '0 0 12 * * ?' |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| // 状态 |  | ||||||
| const showBuilder = ref(true) |  | ||||||
| const validationMessage = ref('') |  | ||||||
| const isValid = ref(true) |  | ||||||
| 
 |  | ||||||
| // 事件处理 |  | ||||||
| const handleValidate = (result: { valid: boolean; message: string }) => { |  | ||||||
|   isValid.value = result.valid |  | ||||||
|   validationMessage.value = result.message |  | ||||||
|   emit('validate', result) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 初始验证 |  | ||||||
| onMounted(() => { |  | ||||||
|   handleValidate({ valid: true, message: '定时触发配置验证通过' }) |  | ||||||
| }) |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .timer-trigger-config { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .config-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   padding: 12px 16px; |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   border: 1px solid var(--el-border-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-left { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-icon { |  | ||||||
|   color: var(--el-color-danger); |  | ||||||
|   font-size: 18px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-title { |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-right { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .visual-builder, |  | ||||||
| .manual-editor { |  | ||||||
|   padding: 16px; |  | ||||||
|   border: 1px solid var(--el-border-color-lighter); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   background: var(--el-fill-color-blank); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .validation-result { |  | ||||||
|   margin-top: 8px; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -1,242 +0,0 @@ | ||||||
| <!-- CRON 可视化构建器组件 --> |  | ||||||
| <!-- TODO @puhui999:看看能不能复用全局的 cron 组件 --> |  | ||||||
| <template> |  | ||||||
|   <div class="cron-builder"> |  | ||||||
|     <div class="builder-header"> |  | ||||||
|       <span class="header-title">可视化 CRON 编辑器</span> |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|     <div class="builder-content"> |  | ||||||
|       <!-- 快捷选项 --> |  | ||||||
|       <div class="quick-options"> |  | ||||||
|         <span class="options-label">常用配置:</span> |  | ||||||
|         <el-button |  | ||||||
|           v-for="option in quickOptions" |  | ||||||
|           :key="option.label" |  | ||||||
|           size="small" |  | ||||||
|           @click="applyQuickOption(option)" |  | ||||||
|         > |  | ||||||
|           {{ option.label }} |  | ||||||
|         </el-button> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <!-- 详细配置 --> |  | ||||||
|       <div class="detailed-config"> |  | ||||||
|         <el-row :gutter="16"> |  | ||||||
|           <el-col :span="4"> |  | ||||||
|             <el-form-item label="秒"> |  | ||||||
|               <el-select v-model="cronParts.second" @change="updateCronExpression"> |  | ||||||
|                 <el-option label="每秒" value="*" /> |  | ||||||
|                 <el-option label="0秒" value="0" /> |  | ||||||
|               </el-select> |  | ||||||
|             </el-form-item> |  | ||||||
|           </el-col> |  | ||||||
|           <el-col :span="4"> |  | ||||||
|             <el-form-item label="分钟"> |  | ||||||
|               <el-select v-model="cronParts.minute" @change="updateCronExpression"> |  | ||||||
|                 <el-option label="每分钟" value="*" /> |  | ||||||
|                 <el-option |  | ||||||
|                   v-for="i in 60" |  | ||||||
|                   :key="i - 1" |  | ||||||
|                   :label="`${i - 1}分`" |  | ||||||
|                   :value="String(i - 1)" |  | ||||||
|                 /> |  | ||||||
|               </el-select> |  | ||||||
|             </el-form-item> |  | ||||||
|           </el-col> |  | ||||||
|           <el-col :span="4"> |  | ||||||
|             <el-form-item label="小时"> |  | ||||||
|               <el-select v-model="cronParts.hour" @change="updateCronExpression"> |  | ||||||
|                 <el-option label="每小时" value="*" /> |  | ||||||
|                 <el-option |  | ||||||
|                   v-for="i in 24" |  | ||||||
|                   :key="i - 1" |  | ||||||
|                   :label="`${i - 1}时`" |  | ||||||
|                   :value="String(i - 1)" |  | ||||||
|                 /> |  | ||||||
|               </el-select> |  | ||||||
|             </el-form-item> |  | ||||||
|           </el-col> |  | ||||||
|           <el-col :span="4"> |  | ||||||
|             <el-form-item label="日"> |  | ||||||
|               <el-select v-model="cronParts.day" @change="updateCronExpression"> |  | ||||||
|                 <el-option label="每日" value="*" /> |  | ||||||
|                 <el-option v-for="i in 31" :key="i" :label="`${i}日`" :value="String(i)" /> |  | ||||||
|               </el-select> |  | ||||||
|             </el-form-item> |  | ||||||
|           </el-col> |  | ||||||
|           <el-col :span="4"> |  | ||||||
|             <el-form-item label="月"> |  | ||||||
|               <el-select v-model="cronParts.month" @change="updateCronExpression"> |  | ||||||
|                 <el-option label="每月" value="*" /> |  | ||||||
|                 <el-option |  | ||||||
|                   v-for="(month, index) in months" |  | ||||||
|                   :key="index" |  | ||||||
|                   :label="month" |  | ||||||
|                   :value="String(index + 1)" |  | ||||||
|                 /> |  | ||||||
|               </el-select> |  | ||||||
|             </el-form-item> |  | ||||||
|           </el-col> |  | ||||||
|           <el-col :span="4"> |  | ||||||
|             <el-form-item label="周"> |  | ||||||
|               <el-select v-model="cronParts.week" @change="updateCronExpression"> |  | ||||||
|                 <el-option label="每周" value="*" /> |  | ||||||
|                 <el-option |  | ||||||
|                   v-for="(week, index) in weeks" |  | ||||||
|                   :key="index" |  | ||||||
|                   :label="week" |  | ||||||
|                   :value="String(index)" |  | ||||||
|                 /> |  | ||||||
|               </el-select> |  | ||||||
|             </el-form-item> |  | ||||||
|           </el-col> |  | ||||||
|         </el-row> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useVModel } from '@vueuse/core' |  | ||||||
| 
 |  | ||||||
| /** CRON 可视化构建器组件 */ |  | ||||||
| defineOptions({ name: 'CronBuilder' }) |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   modelValue: string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface Emits { |  | ||||||
|   (e: 'update:modelValue', value: string): void |  | ||||||
|   (e: 'validate', result: { valid: boolean; message: string }): void |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const props = defineProps<Props>() |  | ||||||
| const emit = defineEmits<Emits>() |  | ||||||
| 
 |  | ||||||
| const localValue = useVModel(props, 'modelValue', emit) |  | ||||||
| 
 |  | ||||||
| // CRON 各部分 |  | ||||||
| const cronParts = reactive({ |  | ||||||
|   second: '0', |  | ||||||
|   minute: '0', |  | ||||||
|   hour: '12', |  | ||||||
|   day: '*', |  | ||||||
|   month: '*', |  | ||||||
|   week: '?' |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| // 常量数据 |  | ||||||
| const months = [ |  | ||||||
|   '1月', |  | ||||||
|   '2月', |  | ||||||
|   '3月', |  | ||||||
|   '4月', |  | ||||||
|   '5月', |  | ||||||
|   '6月', |  | ||||||
|   '7月', |  | ||||||
|   '8月', |  | ||||||
|   '9月', |  | ||||||
|   '10月', |  | ||||||
|   '11月', |  | ||||||
|   '12月' |  | ||||||
| ] |  | ||||||
| const weeks = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] |  | ||||||
| 
 |  | ||||||
| // 快捷选项 |  | ||||||
| const quickOptions = [ |  | ||||||
|   { label: '每分钟', cron: '0 * * * * ?' }, |  | ||||||
|   { label: '每小时', cron: '0 0 * * * ?' }, |  | ||||||
|   { label: '每天中午', cron: '0 0 12 * * ?' }, |  | ||||||
|   { label: '每天凌晨', cron: '0 0 0 * * ?' }, |  | ||||||
|   { label: '工作日9点', cron: '0 0 9 * * MON-FRI' }, |  | ||||||
|   { label: '每周一', cron: '0 0 9 * * MON' } |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| // 方法 |  | ||||||
| const updateCronExpression = () => { |  | ||||||
|   localValue.value = `${cronParts.second} ${cronParts.minute} ${cronParts.hour} ${cronParts.day} ${cronParts.month} ${cronParts.week}` |  | ||||||
|   emit('validate', { valid: true, message: 'CRON表达式验证通过' }) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const applyQuickOption = (option: any) => { |  | ||||||
|   localValue.value = option.cron |  | ||||||
|   parseCronExpression() |  | ||||||
|   emit('validate', { valid: true, message: 'CRON表达式验证通过' }) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const parseCronExpression = () => { |  | ||||||
|   if (!localValue.value) return |  | ||||||
| 
 |  | ||||||
|   const parts = localValue.value.split(' ') |  | ||||||
|   if (parts.length >= 6) { |  | ||||||
|     cronParts.second = parts[0] || '0' |  | ||||||
|     cronParts.minute = parts[1] || '0' |  | ||||||
|     cronParts.hour = parts[2] || '12' |  | ||||||
|     cronParts.day = parts[3] || '*' |  | ||||||
|     cronParts.month = parts[4] || '*' |  | ||||||
|     cronParts.week = parts[5] || '?' |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 初始化 |  | ||||||
| onMounted(() => { |  | ||||||
|   if (localValue.value) { |  | ||||||
|     parseCronExpression() |  | ||||||
|   } else { |  | ||||||
|     updateCronExpression() |  | ||||||
|   } |  | ||||||
| }) |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .cron-builder { |  | ||||||
|   border: 1px solid var(--el-border-color-light); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   background: var(--el-fill-color-blank); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .builder-header { |  | ||||||
|   padding: 12px 16px; |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-bottom: 1px solid var(--el-border-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-title { |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .builder-content { |  | ||||||
|   padding: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .quick-options { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
|   margin-bottom: 16px; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .options-label { |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
|   white-space: nowrap; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .detailed-config { |  | ||||||
|   margin-top: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-form-item) { |  | ||||||
|   margin-bottom: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-form-item__label) { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -1,141 +0,0 @@ | ||||||
| <!-- CRON 表达式输入组件 --> |  | ||||||
| <!-- TODO @puhui999:看看能不能复用全局的 cron 组件 --> |  | ||||||
| <template> |  | ||||||
|   <div class="cron-input"> |  | ||||||
|     <el-input |  | ||||||
|       v-model="localValue" |  | ||||||
|       placeholder="请输入 CRON 表达式,如:0 0 12 * * ?" |  | ||||||
|       @blur="handleBlur" |  | ||||||
|       @input="handleInput" |  | ||||||
|     > |  | ||||||
|       <template #suffix> |  | ||||||
|         <el-tooltip content="CRON 表达式帮助" placement="top"> |  | ||||||
|           <Icon icon="ep:question-filled" class="input-help" @click="showHelp = !showHelp" /> |  | ||||||
|         </el-tooltip> |  | ||||||
|       </template> |  | ||||||
|     </el-input> |  | ||||||
| 
 |  | ||||||
|     <!-- 帮助信息 --> |  | ||||||
|     <div v-if="showHelp" class="cron-help"> |  | ||||||
|       <el-alert title="CRON 表达式格式:秒 分 时 日 月 周" type="info" :closable="false" show-icon> |  | ||||||
|         <template #default> |  | ||||||
|           <div class="help-content"> |  | ||||||
|             <p><strong>示例:</strong></p> |  | ||||||
|             <ul> |  | ||||||
|               <li><code>0 0 12 * * ?</code> - 每天中午12点执行</li> |  | ||||||
|               <li><code>0 */5 * * * ?</code> - 每5分钟执行一次</li> |  | ||||||
|               <li><code>0 0 9-17 * * MON-FRI</code> - 工作日9-17点每小时执行</li> |  | ||||||
|             </ul> |  | ||||||
|             <p><strong>特殊字符:</strong></p> |  | ||||||
|             <ul> |  | ||||||
|               <li><code>*</code> - 匹配任意值</li> |  | ||||||
|               <li><code>?</code> - 不指定值(用于日和周)</li> |  | ||||||
|               <li><code>/</code> - 间隔触发,如 */5 表示每5个单位</li> |  | ||||||
|               <li><code>-</code> - 范围,如 9-17 表示9到17</li> |  | ||||||
|               <li><code>,</code> - 列举,如 MON,WED,FRI</li> |  | ||||||
|             </ul> |  | ||||||
|           </div> |  | ||||||
|         </template> |  | ||||||
|       </el-alert> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useVModel } from '@vueuse/core' |  | ||||||
| import { validateCronExpression } from '../../utils/validation' |  | ||||||
| 
 |  | ||||||
| /** CRON 表达式输入组件 */ |  | ||||||
| defineOptions({ name: 'CronInput' }) |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   modelValue: string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface Emits { |  | ||||||
|   (e: 'update:modelValue', value: string): void |  | ||||||
|   (e: 'validate', result: { valid: boolean; message: string }): void |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const props = defineProps<Props>() |  | ||||||
| const emit = defineEmits<Emits>() |  | ||||||
| 
 |  | ||||||
| const localValue = useVModel(props, 'modelValue', emit) |  | ||||||
| 
 |  | ||||||
| // 状态 |  | ||||||
| const showHelp = ref(false) |  | ||||||
| 
 |  | ||||||
| // 事件处理 |  | ||||||
| const handleInput = () => { |  | ||||||
|   validateExpression() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const handleBlur = () => { |  | ||||||
|   validateExpression() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const validateExpression = () => { |  | ||||||
|   if (!localValue.value) { |  | ||||||
|     emit('validate', { valid: false, message: '请输入CRON表达式' }) |  | ||||||
|     return |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   const isValid = validateCronExpression(localValue.value) |  | ||||||
|   if (isValid) { |  | ||||||
|     emit('validate', { valid: true, message: 'CRON表达式验证通过' }) |  | ||||||
|   } else { |  | ||||||
|     emit('validate', { valid: false, message: 'CRON表达式格式不正确' }) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 监听值变化 |  | ||||||
| watch( |  | ||||||
|   () => localValue.value, |  | ||||||
|   () => { |  | ||||||
|     validateExpression() |  | ||||||
|   } |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // 初始化 |  | ||||||
| onMounted(() => { |  | ||||||
|   if (localValue.value) { |  | ||||||
|     validateExpression() |  | ||||||
|   } |  | ||||||
| }) |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .cron-input { |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .input-help { |  | ||||||
|   color: var(--el-text-color-placeholder); |  | ||||||
|   cursor: pointer; |  | ||||||
|   transition: color 0.2s; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .input-help:hover { |  | ||||||
|   color: var(--el-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .cron-help { |  | ||||||
|   margin-top: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .help-content ul { |  | ||||||
|   margin: 8px 0 0 0; |  | ||||||
|   padding-left: 20px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .help-content li { |  | ||||||
|   margin-bottom: 4px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .help-content code { |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   padding: 2px 4px; |  | ||||||
|   border-radius: 2px; |  | ||||||
|   font-family: 'Courier New', monospace; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -1,249 +0,0 @@ | ||||||
| <!-- 场景描述输入组件 --> |  | ||||||
| <template> |  | ||||||
|   <div class="description-input"> |  | ||||||
|     <el-input |  | ||||||
|       ref="inputRef" |  | ||||||
|       v-model="localValue" |  | ||||||
|       type="textarea" |  | ||||||
|       placeholder="请输入场景描述(可选)" |  | ||||||
|       :rows="3" |  | ||||||
|       maxlength="200" |  | ||||||
|       show-word-limit |  | ||||||
|       resize="none" |  | ||||||
|       @input="handleInput" |  | ||||||
|     /> |  | ||||||
| 
 |  | ||||||
|     <!-- 描述模板 --> |  | ||||||
|     <teleport to="body"> |  | ||||||
|       <div v-if="showTemplates" ref="templateDropdownRef" class="templates" :style="dropdownStyle"> |  | ||||||
|         <div class="templates-header"> |  | ||||||
|           <span class="templates-title">描述模板</span> |  | ||||||
|           <el-button type="text" size="small" @click="showTemplates = false"> |  | ||||||
|             <Icon icon="ep:close" /> |  | ||||||
|           </el-button> |  | ||||||
|         </div> |  | ||||||
|         <div class="templates-list"> |  | ||||||
|           <div |  | ||||||
|             v-for="template in descriptionTemplates" |  | ||||||
|             :key="template.title" |  | ||||||
|             class="template-item" |  | ||||||
|             @click="applyTemplate(template)" |  | ||||||
|           > |  | ||||||
|             <div class="template-title">{{ template.title }}</div> |  | ||||||
|             <div class="template-content">{{ template.content }}</div> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </teleport> |  | ||||||
| 
 |  | ||||||
|     <!-- TODO @puhui999:不用模版哈,简单点。。。 --> |  | ||||||
|     <!-- 模板按钮 --> |  | ||||||
|     <div v-if="!localValue && !showTemplates" class="template-trigger"> |  | ||||||
|       <el-button type="text" size="small" @click="toggleTemplates"> |  | ||||||
|         <Icon icon="ep:document" class="mr-1" /> |  | ||||||
|         使用模板 |  | ||||||
|       </el-button> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useVModel } from '@vueuse/core' |  | ||||||
| 
 |  | ||||||
| /** 场景描述输入组件 */ |  | ||||||
| defineOptions({ name: 'DescriptionInput' }) |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   modelValue?: string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface Emits { |  | ||||||
|   (e: 'update:modelValue', value: string): void |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const props = defineProps<Props>() |  | ||||||
| const emit = defineEmits<Emits>() |  | ||||||
| 
 |  | ||||||
| const localValue = useVModel(props, 'modelValue', emit, { |  | ||||||
|   defaultValue: '' |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| const showTemplates = ref(false) |  | ||||||
| const templateDropdownRef = ref() |  | ||||||
| const inputRef = ref() |  | ||||||
| const dropdownStyle = ref({}) |  | ||||||
| 
 |  | ||||||
| // 描述模板 |  | ||||||
| const descriptionTemplates = [ |  | ||||||
|   { |  | ||||||
|     title: '温度控制场景', |  | ||||||
|     content: '当环境温度超过设定阈值时,自动启动空调降温设备,确保环境温度保持在舒适范围内。' |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     title: '设备监控场景', |  | ||||||
|     content: '实时监控关键设备的运行状态,当设备出现异常或离线时,立即发送告警通知相关人员。' |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     title: '节能控制场景', |  | ||||||
|     content: '根据时间段和环境条件,自动调节设备功率或关闭非必要设备,实现智能节能管理。' |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     title: '安防联动场景', |  | ||||||
|     content: '当检测到异常情况时,自动触发安防设备联动,包括报警器、摄像头录制等安全措施。' |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     title: '定时任务场景', |  | ||||||
|     content: '按照预设的时间计划,定期执行设备检查、数据备份或系统维护等自动化任务。' |  | ||||||
|   } |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| // 计算下拉框位置 |  | ||||||
| const calculateDropdownPosition = () => { |  | ||||||
|   if (!inputRef.value) return |  | ||||||
| 
 |  | ||||||
|   const inputElement = inputRef.value.$el || inputRef.value |  | ||||||
|   const rect = inputElement.getBoundingClientRect() |  | ||||||
|   const viewportHeight = window.innerHeight |  | ||||||
|   const dropdownHeight = 300 // 预估下拉框高度 |  | ||||||
| 
 |  | ||||||
|   let top = rect.bottom + 4 |  | ||||||
|   let left = rect.left |  | ||||||
| 
 |  | ||||||
|   // 如果下方空间不够,显示在上方 |  | ||||||
|   if (top + dropdownHeight > viewportHeight) { |  | ||||||
|     top = rect.top - dropdownHeight - 4 |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // 确保不超出左右边界 |  | ||||||
|   const maxLeft = window.innerWidth - 400 // 下拉框最大宽度 |  | ||||||
|   if (left > maxLeft) { |  | ||||||
|     left = maxLeft |  | ||||||
|   } |  | ||||||
|   if (left < 10) { |  | ||||||
|     left = 10 |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   dropdownStyle.value = { |  | ||||||
|     top: `${top}px`, |  | ||||||
|     left: `${left}px` |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const handleInput = (value: string) => { |  | ||||||
|   if (value.length > 0) { |  | ||||||
|     showTemplates.value = false |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const applyTemplate = (template: any) => { |  | ||||||
|   localValue.value = template.content |  | ||||||
|   showTemplates.value = false |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const toggleTemplates = () => { |  | ||||||
|   showTemplates.value = !showTemplates.value |  | ||||||
|   if (showTemplates.value) { |  | ||||||
|     nextTick(() => { |  | ||||||
|       calculateDropdownPosition() |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 点击外部关闭下拉框 |  | ||||||
| const handleClickOutside = (event: Event) => { |  | ||||||
|   if ( |  | ||||||
|     templateDropdownRef.value && |  | ||||||
|     !templateDropdownRef.value.contains(event.target as Node) && |  | ||||||
|     inputRef.value && |  | ||||||
|     !inputRef.value.$el.contains(event.target as Node) |  | ||||||
|   ) { |  | ||||||
|     showTemplates.value = false |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 监听窗口大小变化和点击事件 |  | ||||||
| onMounted(() => { |  | ||||||
|   window.addEventListener('resize', calculateDropdownPosition) |  | ||||||
|   window.addEventListener('scroll', calculateDropdownPosition) |  | ||||||
|   document.addEventListener('click', handleClickOutside) |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| onUnmounted(() => { |  | ||||||
|   window.removeEventListener('resize', calculateDropdownPosition) |  | ||||||
|   window.removeEventListener('scroll', calculateDropdownPosition) |  | ||||||
|   document.removeEventListener('click', handleClickOutside) |  | ||||||
| }) |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .description-input { |  | ||||||
|   position: relative; |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .templates { |  | ||||||
|   position: fixed; |  | ||||||
|   z-index: 9999; |  | ||||||
|   background: white; |  | ||||||
|   border: 1px solid var(--el-border-color-light); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |  | ||||||
|   min-width: 300px; |  | ||||||
|   max-width: 400px; |  | ||||||
|   max-height: 400px; |  | ||||||
|   overflow: hidden; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .templates-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   padding: 8px 12px; |  | ||||||
|   border-bottom: 1px solid var(--el-border-color-lighter); |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .templates-title { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
|   font-weight: 500; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .templates-list { |  | ||||||
|   max-height: 300px; |  | ||||||
|   overflow-y: auto; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .template-item { |  | ||||||
|   padding: 12px; |  | ||||||
|   cursor: pointer; |  | ||||||
|   transition: background-color 0.2s; |  | ||||||
|   border-bottom: 1px solid var(--el-border-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .template-item:hover { |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .template-item:last-child { |  | ||||||
|   border-bottom: none; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .template-title { |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
|   margin-bottom: 4px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .template-content { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
|   line-height: 1.4; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .template-trigger { |  | ||||||
|   margin-top: 8px; |  | ||||||
|   text-align: right; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -1,166 +0,0 @@ | ||||||
| <!-- 场景名称输入组件 --> |  | ||||||
| <template> |  | ||||||
|   <div class="name-input"> |  | ||||||
|     <el-input |  | ||||||
|       v-model="localValue" |  | ||||||
|       placeholder="请输入场景名称" |  | ||||||
|       maxlength="50" |  | ||||||
|       show-word-limit |  | ||||||
|       clearable |  | ||||||
|       @blur="handleBlur" |  | ||||||
|       @input="handleInput" |  | ||||||
|     > |  | ||||||
|       <template #prefix> |  | ||||||
|         <Icon icon="ep:edit" class="input-icon" /> |  | ||||||
|       </template> |  | ||||||
|     </el-input> |  | ||||||
| 
 |  | ||||||
|     <!-- 智能提示 --> |  | ||||||
|     <!-- TODO @puhui999:暂时不用考虑智能推荐哈。用途不大 --> |  | ||||||
|     <div v-if="showSuggestions && suggestions.length > 0" class="suggestions"> |  | ||||||
|       <div class="suggestions-header"> |  | ||||||
|         <span class="suggestions-title">推荐名称</span> |  | ||||||
|       </div> |  | ||||||
|       <div class="suggestions-list"> |  | ||||||
|         <div |  | ||||||
|           v-for="suggestion in suggestions" |  | ||||||
|           :key="suggestion" |  | ||||||
|           class="suggestion-item" |  | ||||||
|           @click="applySuggestion(suggestion)" |  | ||||||
|         > |  | ||||||
|           {{ suggestion }} |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useVModel } from '@vueuse/core' |  | ||||||
| 
 |  | ||||||
| /** 场景名称输入组件 */ |  | ||||||
| defineOptions({ name: 'NameInput' }) |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   modelValue: string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface Emits { |  | ||||||
|   (e: 'update:modelValue', value: string): void |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const props = defineProps<Props>() |  | ||||||
| const emit = defineEmits<Emits>() |  | ||||||
| 
 |  | ||||||
| const localValue = useVModel(props, 'modelValue', emit) |  | ||||||
| 
 |  | ||||||
| // 智能提示相关 |  | ||||||
| const showSuggestions = ref(false) |  | ||||||
| const suggestions = ref<string[]>([]) |  | ||||||
| 
 |  | ||||||
| // 常用场景名称模板 |  | ||||||
| const nameTemplates = [ |  | ||||||
|   '温度过高自动降温', |  | ||||||
|   '设备离线告警通知', |  | ||||||
|   '湿度异常自动调节', |  | ||||||
|   '夜间安防模式启动', |  | ||||||
|   '能耗超标自动关闭', |  | ||||||
|   '故障设备自动重启', |  | ||||||
|   '定时设备状态检查', |  | ||||||
|   '环境数据异常告警' |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| const handleInput = (value: string) => { |  | ||||||
|   if (value.length > 0 && value.length < 10) { |  | ||||||
|     // 根据输入内容过滤建议 |  | ||||||
|     suggestions.value = nameTemplates |  | ||||||
|       .filter( |  | ||||||
|         (template) => |  | ||||||
|           template.includes(value) || (value.includes('温度') && template.includes('温度')) |  | ||||||
|       ) |  | ||||||
|       .slice(0, 5) |  | ||||||
|     showSuggestions.value = suggestions.value.length > 0 |  | ||||||
|   } else { |  | ||||||
|     showSuggestions.value = false |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const handleBlur = () => { |  | ||||||
|   // 延迟隐藏建议,允许点击建议项 |  | ||||||
|   setTimeout(() => { |  | ||||||
|     showSuggestions.value = false |  | ||||||
|   }, 200) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const applySuggestion = (suggestion: string) => { |  | ||||||
|   localValue.value = suggestion |  | ||||||
|   showSuggestions.value = false |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 监听外部点击隐藏建议 |  | ||||||
| onMounted(() => { |  | ||||||
|   document.addEventListener('click', (e) => { |  | ||||||
|     const target = e.target as HTMLElement |  | ||||||
|     if (!target.closest('.name-input')) { |  | ||||||
|       showSuggestions.value = false |  | ||||||
|     } |  | ||||||
|   }) |  | ||||||
| }) |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .name-input { |  | ||||||
|   position: relative; |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .input-icon { |  | ||||||
|   color: var(--el-text-color-placeholder); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .suggestions { |  | ||||||
|   position: absolute; |  | ||||||
|   top: 100%; |  | ||||||
|   left: 0; |  | ||||||
|   right: 0; |  | ||||||
|   z-index: 1000; |  | ||||||
|   background: white; |  | ||||||
|   border: 1px solid var(--el-border-color-light); |  | ||||||
|   border-radius: 4px; |  | ||||||
|   box-shadow: var(--el-box-shadow-light); |  | ||||||
|   margin-top: 4px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .suggestions-header { |  | ||||||
|   padding: 8px 12px; |  | ||||||
|   border-bottom: 1px solid var(--el-border-color-lighter); |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .suggestions-title { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
|   font-weight: 500; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .suggestions-list { |  | ||||||
|   max-height: 200px; |  | ||||||
|   overflow-y: auto; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .suggestion-item { |  | ||||||
|   padding: 8px 12px; |  | ||||||
|   cursor: pointer; |  | ||||||
|   transition: background-color 0.2s; |  | ||||||
|   font-size: 14px; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .suggestion-item:hover { |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .suggestion-item:last-child { |  | ||||||
|   border-bottom: none; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -1,158 +0,0 @@ | ||||||
| <!-- 场景状态选择组件 --> |  | ||||||
| <template> |  | ||||||
|   <div class="status-radio"> |  | ||||||
|     <el-radio-group  |  | ||||||
|       v-model="localValue"  |  | ||||||
|       @change="handleChange" |  | ||||||
|     > |  | ||||||
|       <el-radio :value="0" class="status-option"> |  | ||||||
|         <div class="status-content"> |  | ||||||
|           <div class="status-indicator enabled"></div> |  | ||||||
|           <div class="status-info"> |  | ||||||
|             <div class="status-label">启用</div> |  | ||||||
|             <div class="status-desc">场景规则生效,满足条件时自动执行</div> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </el-radio> |  | ||||||
|        |  | ||||||
|       <el-radio :value="1" class="status-option"> |  | ||||||
|         <div class="status-content"> |  | ||||||
|           <div class="status-indicator disabled"></div> |  | ||||||
|           <div class="status-info"> |  | ||||||
|             <div class="status-label">禁用</div> |  | ||||||
|             <div class="status-desc">场景规则暂停,不会触发任何执行动作</div> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </el-radio> |  | ||||||
|     </el-radio-group> |  | ||||||
| 
 |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useVModel } from '@vueuse/core' |  | ||||||
| 
 |  | ||||||
| /** 场景状态选择组件 */ |  | ||||||
| defineOptions({ name: 'StatusRadio' }) |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   modelValue: number |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface Emits { |  | ||||||
|   (e: 'update:modelValue', value: number): void |  | ||||||
|   (e: 'change', value: number): void |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const props = defineProps<Props>() |  | ||||||
| const emit = defineEmits<Emits>() |  | ||||||
| 
 |  | ||||||
| const localValue = useVModel(props, 'modelValue', emit) |  | ||||||
| 
 |  | ||||||
| const handleChange = (value: number) => { |  | ||||||
|   emit('change', value) |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .status-radio { |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .status-radio :deep(.el-radio) { |  | ||||||
|   margin-bottom: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .status-radio :deep(.el-radio:last-child) { |  | ||||||
|   margin-bottom: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-radio-group) { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: row; |  | ||||||
|   gap: 16px; |  | ||||||
|   width: 100%; |  | ||||||
|   align-items: flex-start; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-radio) { |  | ||||||
|   margin-right: 0; |  | ||||||
|   width: auto; |  | ||||||
|   flex: 1; |  | ||||||
|   height: auto; |  | ||||||
|   align-items: flex-start; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .status-option { |  | ||||||
|   width: auto; |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-radio__input) { |  | ||||||
|   margin-top: 12px; |  | ||||||
|   flex-shrink: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-radio__label) { |  | ||||||
|   width: 100%; |  | ||||||
|   padding-left: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .status-content { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: flex-start; |  | ||||||
|   gap: 12px; |  | ||||||
|   padding: 12px; |  | ||||||
|   border: 1px solid var(--el-border-color-light); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   transition: all 0.2s; |  | ||||||
|   width: 100%; |  | ||||||
|   margin-left: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-radio.is-checked) .status-content { |  | ||||||
|   border-color: var(--el-color-primary); |  | ||||||
|   background: var(--el-color-primary-light-9); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .status-content:hover { |  | ||||||
|   border-color: var(--el-color-primary-light-3); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .status-indicator { |  | ||||||
|   width: 12px; |  | ||||||
|   height: 12px; |  | ||||||
|   border-radius: 50%; |  | ||||||
|   margin-top: 4px; |  | ||||||
|   flex-shrink: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .status-indicator.enabled { |  | ||||||
|   background: var(--el-color-success); |  | ||||||
|   box-shadow: 0 0 0 2px var(--el-color-success-light-8); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .status-indicator.disabled { |  | ||||||
|   background: var(--el-color-danger); |  | ||||||
|   box-shadow: 0 0 0 2px var(--el-color-danger-light-8); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .status-info { |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .status-label { |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
|   margin-bottom: 4px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .status-desc { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
|   line-height: 1.4; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| </style> |  | ||||||
|  | @ -1,127 +0,0 @@ | ||||||
| <!-- 执行器预览组件 --> |  | ||||||
| <template> |  | ||||||
|   <div class="action-preview"> |  | ||||||
|     <div v-if="actions.length === 0" class="empty-preview"> |  | ||||||
|       <el-text type="info" size="small">暂无执行器配置</el-text> |  | ||||||
|     </div> |  | ||||||
|     <div v-else class="action-list"> |  | ||||||
|       <div |  | ||||||
|         v-for="(action, index) in actions" |  | ||||||
|         :key="index" |  | ||||||
|         class="action-item" |  | ||||||
|       > |  | ||||||
|         <div class="action-header"> |  | ||||||
|           <Icon icon="ep:setting" class="action-icon" /> |  | ||||||
|           <span class="action-title">执行器 {{ index + 1 }}</span> |  | ||||||
|           <el-tag :type="getActionTypeTag(action.type)" size="small"> |  | ||||||
|             {{ getActionTypeName(action.type) }} |  | ||||||
|           </el-tag> |  | ||||||
|         </div> |  | ||||||
|         <div class="action-content"> |  | ||||||
|           <div class="action-summary"> |  | ||||||
|             {{ 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> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .action-preview { |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .empty-preview { |  | ||||||
|   text-align: center; |  | ||||||
|   padding: 20px 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .action-list { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 12px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .action-item { |  | ||||||
|   border: 1px solid var(--el-border-color-lighter); |  | ||||||
|   border-radius: 4px; |  | ||||||
|   background: var(--el-fill-color-blank); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .action-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
|   padding: 8px 12px; |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-bottom: 1px solid var(--el-border-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .action-icon { |  | ||||||
|   color: var(--el-color-success); |  | ||||||
|   font-size: 14px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .action-title { |  | ||||||
|   font-size: 12px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .action-content { |  | ||||||
|   padding: 8px 12px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .action-summary { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
|   line-height: 1.4; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -1,66 +0,0 @@ | ||||||
| <!-- 配置预览组件 --> |  | ||||||
| <!-- TODO @puhui999:应该暂时不用预览哈 --> |  | ||||||
| <template> |  | ||||||
|   <div class="config-preview"> |  | ||||||
|     <div class="preview-items"> |  | ||||||
|       <div class="preview-item"> |  | ||||||
|         <span class="item-label">场景名称:</span> |  | ||||||
|         <span class="item-value">{{ formData.name || '未设置' }}</span> |  | ||||||
|       </div> |  | ||||||
|       <div class="preview-item"> |  | ||||||
|         <span class="item-label">场景状态:</span> |  | ||||||
|         <el-tag :type="formData.status === 0 ? 'success' : 'danger'" size="small"> |  | ||||||
|           {{ formData.status === 0 ? '启用' : '禁用' }} |  | ||||||
|         </el-tag> |  | ||||||
|       </div> |  | ||||||
|       <div v-if="formData.description" class="preview-item"> |  | ||||||
|         <span class="item-label">场景描述:</span> |  | ||||||
|         <span class="item-value">{{ 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> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .config-preview { |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .preview-items { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .preview-item { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .item-label { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
|   min-width: 80px; |  | ||||||
|   flex-shrink: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .item-value { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -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> |  | ||||||
|  | @ -1,131 +0,0 @@ | ||||||
| <!-- 触发器预览组件 --> |  | ||||||
| <template> |  | ||||||
|   <div class="trigger-preview"> |  | ||||||
|     <div v-if="triggers.length === 0" class="empty-preview"> |  | ||||||
|       <el-text type="info" size="small">暂无触发器配置</el-text> |  | ||||||
|     </div> |  | ||||||
|     <div v-else class="trigger-list"> |  | ||||||
|       <div |  | ||||||
|         v-for="(trigger, index) in triggers" |  | ||||||
|         :key="index" |  | ||||||
|         class="trigger-item" |  | ||||||
|       > |  | ||||||
|         <div class="trigger-header"> |  | ||||||
|           <Icon icon="ep:lightning" class="trigger-icon" /> |  | ||||||
|           <span class="trigger-title">触发器 {{ index + 1 }}</span> |  | ||||||
|           <el-tag :type="getTriggerTypeTag(trigger.type)" size="small"> |  | ||||||
|             {{ getTriggerTypeName(trigger.type) }} |  | ||||||
|           </el-tag> |  | ||||||
|         </div> |  | ||||||
|         <div class="trigger-content"> |  | ||||||
|           <div class="trigger-summary"> |  | ||||||
|             {{ 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> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .trigger-preview { |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .empty-preview { |  | ||||||
|   text-align: center; |  | ||||||
|   padding: 20px 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .trigger-list { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 12px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .trigger-item { |  | ||||||
|   border: 1px solid var(--el-border-color-lighter); |  | ||||||
|   border-radius: 4px; |  | ||||||
|   background: var(--el-fill-color-blank); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .trigger-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
|   padding: 8px 12px; |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-bottom: 1px solid var(--el-border-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .trigger-icon { |  | ||||||
|   color: var(--el-color-warning); |  | ||||||
|   font-size: 14px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .trigger-title { |  | ||||||
|   font-size: 12px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .trigger-content { |  | ||||||
|   padding: 8px 12px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .trigger-summary { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
|   line-height: 1.4; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -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> |  | ||||||
|  | @ -1,113 +0,0 @@ | ||||||
| <!-- 基础信息配置组件 --> |  | ||||||
| <template> |  | ||||||
|   <el-card class="basic-info-section" shadow="never"> |  | ||||||
|     <template #header> |  | ||||||
|       <div class="section-header"> |  | ||||||
|         <div class="header-left"> |  | ||||||
|           <Icon icon="ep:info-filled" class="section-icon" /> |  | ||||||
|           <span class="section-title">基础信息</span> |  | ||||||
|         </div> |  | ||||||
|         <div class="header-right"> |  | ||||||
|           <el-tag :type="formData.status === 0 ? 'success' : 'danger'" size="small"> |  | ||||||
|             {{ formData.status === 0 ? '启用' : '禁用' }} |  | ||||||
|           </el-tag> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </template> |  | ||||||
| 
 |  | ||||||
|     <div class="section-content"> |  | ||||||
|       <el-row :gutter="24"> |  | ||||||
|         <!-- TODO @puhui999:NameInput、StatusRadio、DescriptionInput 是不是直接写在当前界面哈。有点散; --> |  | ||||||
|         <el-col :span="12"> |  | ||||||
|           <el-form-item label="场景名称" prop="name" required> |  | ||||||
|             <NameInput v-model="formData.name" /> |  | ||||||
|           </el-form-item> |  | ||||||
|         </el-col> |  | ||||||
|         <!-- TODO @puhui999:每个一行会好点? --> |  | ||||||
|         <el-col :span="12"> |  | ||||||
|           <el-form-item label="场景状态" prop="status" required> |  | ||||||
|             <StatusRadio v-model="formData.status" /> |  | ||||||
|           </el-form-item> |  | ||||||
|         </el-col> |  | ||||||
|       </el-row> |  | ||||||
| 
 |  | ||||||
|       <el-form-item label="场景描述" prop="description"> |  | ||||||
|         <DescriptionInput v-model="formData.description" /> |  | ||||||
|       </el-form-item> |  | ||||||
|     </div> |  | ||||||
|   </el-card> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useVModel } from '@vueuse/core' |  | ||||||
| import NameInput from '../inputs/NameInput.vue' |  | ||||||
| import DescriptionInput from '../inputs/DescriptionInput.vue' |  | ||||||
| import StatusRadio from '../inputs/StatusRadio.vue' |  | ||||||
| import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types' |  | ||||||
| 
 |  | ||||||
| /** 基础信息配置组件 */ |  | ||||||
| defineOptions({ name: 'BasicInfoSection' }) |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   modelValue: RuleSceneFormData |  | ||||||
|   rules?: any |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface Emits { |  | ||||||
|   (e: 'update:modelValue', value: RuleSceneFormData): void |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const props = defineProps<Props>() |  | ||||||
| const emit = defineEmits<Emits>() |  | ||||||
| 
 |  | ||||||
| const formData = useVModel(props, 'modelValue', emit) |  | ||||||
| // TODO @puhui999:看看能不能 unocss |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .basic-info-section { |  | ||||||
|   border: 1px solid var(--el-border-color-light); |  | ||||||
|   border-radius: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .section-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-left { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .section-icon { |  | ||||||
|   color: var(--el-color-primary); |  | ||||||
|   font-size: 18px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .section-title { |  | ||||||
|   font-size: 16px; |  | ||||||
|   font-weight: 600; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-right { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .section-content { |  | ||||||
|   padding: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-form-item) { |  | ||||||
|   margin-bottom: 20px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-form-item:last-child) { |  | ||||||
|   margin-bottom: 0; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -1,179 +0,0 @@ | ||||||
| <!-- 预览区域组件 --> |  | ||||||
| <!-- TODO @puhui999:是不是不用这个哈? --> |  | ||||||
| <template> |  | ||||||
|   <el-card class="preview-section" shadow="never"> |  | ||||||
|     <template #header> |  | ||||||
|       <div class="section-header"> |  | ||||||
|         <div class="header-left"> |  | ||||||
|           <Icon icon="ep:view" class="section-icon" /> |  | ||||||
|           <span class="section-title">配置预览</span> |  | ||||||
|         </div> |  | ||||||
|         <div class="header-right"> |  | ||||||
|           <el-button type="primary" size="small" @click="handleValidate" :loading="validating"> |  | ||||||
|             <Icon icon="ep:check" /> |  | ||||||
|             验证配置 |  | ||||||
|           </el-button> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </template> |  | ||||||
| 
 |  | ||||||
|     <div class="section-content"> |  | ||||||
|       <!-- 基础信息预览 --> |  | ||||||
|       <div class="preview-group"> |  | ||||||
|         <div class="group-header"> |  | ||||||
|           <Icon icon="ep:info-filled" class="group-icon" /> |  | ||||||
|           <span class="group-title">基础信息</span> |  | ||||||
|         </div> |  | ||||||
|         <div class="group-content"> |  | ||||||
|           <ConfigPreview :form-data="formData" /> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <!-- 触发器预览 --> |  | ||||||
|       <div class="preview-group"> |  | ||||||
|         <div class="group-header"> |  | ||||||
|           <Icon icon="ep:lightning" class="group-icon" /> |  | ||||||
|           <span class="group-title">触发器配置</span> |  | ||||||
|           <el-tag size="small" type="primary">{{ formData.triggers.length }}</el-tag> |  | ||||||
|         </div> |  | ||||||
|         <div class="group-content"> |  | ||||||
|           <TriggerPreview :triggers="formData.triggers" /> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <!-- 执行器预览 --> |  | ||||||
|       <div class="preview-group"> |  | ||||||
|         <div class="group-header"> |  | ||||||
|           <Icon icon="ep:setting" class="group-icon" /> |  | ||||||
|           <span class="group-title">执行器配置</span> |  | ||||||
|           <el-tag size="small" type="success">{{ formData.actions.length }}</el-tag> |  | ||||||
|         </div> |  | ||||||
|         <div class="group-content"> |  | ||||||
|           <ActionPreview :actions="formData.actions" /> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <!-- 验证结果 --> |  | ||||||
|       <div class="preview-group"> |  | ||||||
|         <div class="group-header"> |  | ||||||
|           <Icon icon="ep:circle-check" class="group-icon" /> |  | ||||||
|           <span class="group-title">验证结果</span> |  | ||||||
|         </div> |  | ||||||
|         <div class="group-content"> |  | ||||||
|           <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> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .preview-section { |  | ||||||
|   border: 1px solid var(--el-border-color-light); |  | ||||||
|   border-radius: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .section-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-left { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .section-icon { |  | ||||||
|   color: var(--el-color-primary); |  | ||||||
|   font-size: 18px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .section-title { |  | ||||||
|   font-size: 16px; |  | ||||||
|   font-weight: 600; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-right { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .section-content { |  | ||||||
|   padding: 0; |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .preview-group { |  | ||||||
|   border: 1px solid var(--el-border-color-lighter); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   background: var(--el-fill-color-blank); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .group-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); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .group-icon { |  | ||||||
|   color: var(--el-color-primary); |  | ||||||
|   font-size: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .group-title { |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .group-content { |  | ||||||
|   padding: 16px; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -1,385 +0,0 @@ | ||||||
| <!-- 触发器配置组件 --> |  | ||||||
| <template> |  | ||||||
|   <el-card class="trigger-section" shadow="never"> |  | ||||||
|     <template #header> |  | ||||||
|       <div class="section-header"> |  | ||||||
|         <div class="header-left"> |  | ||||||
|           <Icon icon="ep:lightning" class="section-icon" /> |  | ||||||
|           <span class="section-title">触发器配置</span> |  | ||||||
|           <!-- TODO @puhui999:是不是去掉 maxTriggers;计数 --> |  | ||||||
|           <el-tag size="small" type="info">{{ triggers.length }}/{{ maxTriggers }}</el-tag> |  | ||||||
|         </div> |  | ||||||
|         <div class="header-right"> |  | ||||||
|           <el-button |  | ||||||
|             type="primary" |  | ||||||
|             size="small" |  | ||||||
|             @click="addTrigger" |  | ||||||
|             :disabled="triggers.length >= maxTriggers" |  | ||||||
|           > |  | ||||||
|             <Icon icon="ep:plus" /> |  | ||||||
|             添加触发器 |  | ||||||
|           </el-button> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </template> |  | ||||||
| 
 |  | ||||||
|     <div class="section-content"> |  | ||||||
|       <!-- 空状态 --> |  | ||||||
|       <div v-if="triggers.length === 0" class="empty-state"> |  | ||||||
|         <el-empty description="暂无触发器配置"> |  | ||||||
|           <!-- TODO @puhui999:这个要不要去掉哈;入口统一点 --> |  | ||||||
|           <el-button type="primary" @click="addTrigger"> |  | ||||||
|             <Icon icon="ep:plus" /> |  | ||||||
|             添加第一个触发器 |  | ||||||
|           </el-button> |  | ||||||
|         </el-empty> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <!-- 触发器列表 --> |  | ||||||
|       <div v-else class="triggers-list"> |  | ||||||
|         <div v-for="(trigger, index) in triggers" :key="`trigger-${index}`" class="trigger-item"> |  | ||||||
|           <div class="trigger-header"> |  | ||||||
|             <div class="trigger-title"> |  | ||||||
|               <Icon icon="ep:lightning" class="trigger-icon" /> |  | ||||||
|               <span>触发器 {{ index + 1 }}</span> |  | ||||||
|               <el-tag :type="getTriggerTypeTag(trigger.type)" size="small"> |  | ||||||
|                 {{ getTriggerTypeName(trigger.type) }} |  | ||||||
|               </el-tag> |  | ||||||
|             </div> |  | ||||||
|             <div class="trigger-actions"> |  | ||||||
|               <el-button |  | ||||||
|                 type="danger" |  | ||||||
|                 size="small" |  | ||||||
|                 text |  | ||||||
|                 @click="removeTrigger(index)" |  | ||||||
|                 v-if="triggers.length > 1" |  | ||||||
|               > |  | ||||||
|                 <Icon icon="ep:delete" /> |  | ||||||
|                 删除 |  | ||||||
|               </el-button> |  | ||||||
|             </div> |  | ||||||
|           </div> |  | ||||||
| 
 |  | ||||||
|           <div class="trigger-content"> |  | ||||||
|             <!-- 触发类型选择 --> |  | ||||||
|             <TriggerTypeSelector |  | ||||||
|               :model-value="trigger.type" |  | ||||||
|               @update:model-value="(value) => updateTriggerType(index, value)" |  | ||||||
|               @change="onTriggerTypeChange(trigger, $event)" |  | ||||||
|             /> |  | ||||||
| 
 |  | ||||||
|             <!-- 设备触发配置 --> |  | ||||||
|             <DeviceTriggerConfig |  | ||||||
|               v-if="isDeviceTrigger(trigger.type)" |  | ||||||
|               :model-value="trigger" |  | ||||||
|               @update:model-value="(value) => updateTrigger(index, value)" |  | ||||||
|               @validate="(result) => handleTriggerValidate(index, result)" |  | ||||||
|             /> |  | ||||||
| 
 |  | ||||||
|             <!-- 定时触发配置 --> |  | ||||||
|             <TimerTriggerConfig |  | ||||||
|               v-if="trigger.type === TriggerTypeEnum.TIMER" |  | ||||||
|               :model-value="trigger.cronExpression" |  | ||||||
|               @update:model-value="(value) => updateTriggerCronExpression(index, value)" |  | ||||||
|               @validate="(result) => handleTriggerValidate(index, result)" |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <!-- 添加提示 --> |  | ||||||
|       <!-- TODO @puhui999:这个要不要去掉哈;入口统一点 --> |  | ||||||
|       <div v-if="triggers.length > 0 && triggers.length < maxTriggers" class="add-more"> |  | ||||||
|         <el-button type="primary" plain @click="addTrigger" class="add-more-btn"> |  | ||||||
|           <Icon icon="ep:plus" /> |  | ||||||
|           继续添加触发器 |  | ||||||
|         </el-button> |  | ||||||
|         <span class="add-more-text"> 最多可添加 {{ maxTriggers }} 个触发器 </span> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <!-- 验证结果 --> |  | ||||||
|       <div v-if="validationMessage" class="validation-result"> |  | ||||||
|         <el-alert |  | ||||||
|           :title="validationMessage" |  | ||||||
|           :type="isValid ? 'success' : 'error'" |  | ||||||
|           :closable="false" |  | ||||||
|           show-icon |  | ||||||
|         /> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </el-card> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useVModel } from '@vueuse/core' |  | ||||||
| import TriggerTypeSelector from '../selectors/TriggerTypeSelector.vue' |  | ||||||
| import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue' |  | ||||||
| import TimerTriggerConfig from '../configs/TimerTriggerConfig.vue' |  | ||||||
| import { |  | ||||||
|   TriggerFormData, |  | ||||||
|   IotRuleSceneTriggerTypeEnum as TriggerTypeEnum |  | ||||||
| } from '@/api/iot/rule/scene/scene.types' |  | ||||||
| import { createDefaultTriggerData } from '../../utils/transform' |  | ||||||
| 
 |  | ||||||
| /** 触发器配置组件 */ |  | ||||||
| defineOptions({ name: 'TriggerSection' }) |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   triggers: TriggerFormData[] |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface Emits { |  | ||||||
|   (e: 'update:triggers', value: TriggerFormData[]): void |  | ||||||
|   (e: 'validate', result: { valid: boolean; message: string }): void |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const props = defineProps<Props>() |  | ||||||
| const emit = defineEmits<Emits>() |  | ||||||
| 
 |  | ||||||
| const triggers = useVModel(props, 'triggers', emit) |  | ||||||
| 
 |  | ||||||
| // 配置常量 |  | ||||||
| const maxTriggers = 5 |  | ||||||
| 
 |  | ||||||
| // 验证状态 |  | ||||||
| const triggerValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({}) |  | ||||||
| const validationMessage = ref('') |  | ||||||
| const isValid = ref(true) |  | ||||||
| 
 |  | ||||||
| // 触发器类型映射 |  | ||||||
| const triggerTypeNames = { |  | ||||||
|   [TriggerTypeEnum.DEVICE_STATE_UPDATE]: '设备状态变更', |  | ||||||
|   [TriggerTypeEnum.DEVICE_PROPERTY_POST]: '属性上报', |  | ||||||
|   [TriggerTypeEnum.DEVICE_EVENT_POST]: '事件上报', |  | ||||||
|   [TriggerTypeEnum.DEVICE_SERVICE_INVOKE]: '服务调用', |  | ||||||
|   [TriggerTypeEnum.TIMER]: '定时触发' |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const triggerTypeTags = { |  | ||||||
|   [TriggerTypeEnum.DEVICE_STATE_UPDATE]: 'warning', |  | ||||||
|   [TriggerTypeEnum.DEVICE_PROPERTY_POST]: 'primary', |  | ||||||
|   [TriggerTypeEnum.DEVICE_EVENT_POST]: 'success', |  | ||||||
|   [TriggerTypeEnum.DEVICE_SERVICE_INVOKE]: 'info', |  | ||||||
|   [TriggerTypeEnum.TIMER]: 'danger' |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 工具函数 |  | ||||||
| const isDeviceTrigger = (type: number) => { |  | ||||||
|   return [ |  | ||||||
|     TriggerTypeEnum.DEVICE_STATE_UPDATE, |  | ||||||
|     TriggerTypeEnum.DEVICE_PROPERTY_POST, |  | ||||||
|     TriggerTypeEnum.DEVICE_EVENT_POST, |  | ||||||
|     TriggerTypeEnum.DEVICE_SERVICE_INVOKE |  | ||||||
|   ].includes(type) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const getTriggerTypeName = (type: number) => { |  | ||||||
|   return triggerTypeNames[type] || '未知类型' |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const getTriggerTypeTag = (type: number) => { |  | ||||||
|   return triggerTypeTags[type] || 'info' |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 事件处理 |  | ||||||
| const addTrigger = () => { |  | ||||||
|   if (triggers.value.length >= maxTriggers) { |  | ||||||
|     return |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   const newTrigger = createDefaultTriggerData() |  | ||||||
|   triggers.value.push(newTrigger) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const removeTrigger = (index: number) => { |  | ||||||
|   triggers.value.splice(index, 1) |  | ||||||
|   delete triggerValidations.value[index] |  | ||||||
| 
 |  | ||||||
|   // 重新索引验证结果 |  | ||||||
|   const newValidations: { [key: number]: { valid: boolean; message: string } } = {} |  | ||||||
|   Object.keys(triggerValidations.value).forEach((key) => { |  | ||||||
|     const numKey = parseInt(key) |  | ||||||
|     if (numKey > index) { |  | ||||||
|       newValidations[numKey - 1] = triggerValidations.value[numKey] |  | ||||||
|     } else if (numKey < index) { |  | ||||||
|       newValidations[numKey] = triggerValidations.value[numKey] |  | ||||||
|     } |  | ||||||
|   }) |  | ||||||
|   triggerValidations.value = newValidations |  | ||||||
| 
 |  | ||||||
|   updateValidationResult() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const updateTriggerType = (index: number, type: number) => { |  | ||||||
|   triggers.value[index].type = type |  | ||||||
|   onTriggerTypeChange(triggers.value[index], type) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const updateTrigger = (index: number, trigger: TriggerFormData) => { |  | ||||||
|   triggers.value[index] = trigger |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const updateTriggerCronExpression = (index: number, cronExpression?: string) => { |  | ||||||
|   triggers.value[index].cronExpression = cronExpression |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const onTriggerTypeChange = (trigger: TriggerFormData, type: number) => { |  | ||||||
|   // 清理不相关的配置 |  | ||||||
|   if (type === TriggerTypeEnum.TIMER) { |  | ||||||
|     trigger.productId = undefined |  | ||||||
|     trigger.deviceId = undefined |  | ||||||
|     trigger.identifier = undefined |  | ||||||
|     trigger.operator = undefined |  | ||||||
|     trigger.value = undefined |  | ||||||
|     trigger.conditionGroups = undefined |  | ||||||
|     if (!trigger.cronExpression) { |  | ||||||
|       trigger.cronExpression = '0 0 12 * * ?' |  | ||||||
|     } |  | ||||||
|   } else { |  | ||||||
|     trigger.cronExpression = undefined |  | ||||||
|     if (type === TriggerTypeEnum.DEVICE_STATE_UPDATE) { |  | ||||||
|       trigger.conditionGroups = undefined |  | ||||||
|     } else if (!trigger.conditionGroups) { |  | ||||||
|       trigger.conditionGroups = [] |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const handleTriggerValidate = (index: number, result: { valid: boolean; message: string }) => { |  | ||||||
|   triggerValidations.value[index] = result |  | ||||||
|   updateValidationResult() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const updateValidationResult = () => { |  | ||||||
|   const validations = Object.values(triggerValidations.value) |  | ||||||
|   const allValid = validations.every((v) => v.valid) |  | ||||||
|   const hasValidations = validations.length > 0 |  | ||||||
| 
 |  | ||||||
|   if (!hasValidations) { |  | ||||||
|     isValid.value = true |  | ||||||
|     validationMessage.value = '' |  | ||||||
|   } else if (allValid) { |  | ||||||
|     isValid.value = true |  | ||||||
|     validationMessage.value = '所有触发器配置验证通过' |  | ||||||
|   } else { |  | ||||||
|     isValid.value = false |  | ||||||
|     const errorMessages = validations.filter((v) => !v.valid).map((v) => v.message) |  | ||||||
|     validationMessage.value = `触发器配置错误: ${errorMessages.join('; ')}` |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   emit('validate', { valid: isValid.value, message: validationMessage.value }) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 监听触发器数量变化 |  | ||||||
| watch( |  | ||||||
|   () => triggers.value.length, |  | ||||||
|   () => { |  | ||||||
|     updateValidationResult() |  | ||||||
|   } |  | ||||||
| ) |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .trigger-section { |  | ||||||
|   border: 1px solid var(--el-border-color-light); |  | ||||||
|   border-radius: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .section-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-left { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .section-icon { |  | ||||||
|   color: var(--el-color-primary); |  | ||||||
|   font-size: 18px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .section-title { |  | ||||||
|   font-size: 16px; |  | ||||||
|   font-weight: 600; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-right { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .section-content { |  | ||||||
|   padding: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .empty-state { |  | ||||||
|   padding: 40px 0; |  | ||||||
|   text-align: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .triggers-list { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .trigger-item { |  | ||||||
|   border: 1px solid var(--el-border-color-lighter); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   background: var(--el-fill-color-blank); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .trigger-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   padding: 12px 16px; |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-bottom: 1px solid var(--el-border-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .trigger-title { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .trigger-icon { |  | ||||||
|   color: var(--el-color-warning); |  | ||||||
|   font-size: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .trigger-content { |  | ||||||
|   padding: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .add-more { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 12px; |  | ||||||
|   margin-top: 16px; |  | ||||||
|   padding: 16px; |  | ||||||
|   border: 1px dashed var(--el-border-color); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   background: var(--el-fill-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .add-more-btn { |  | ||||||
|   flex-shrink: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .add-more-text { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .validation-result { |  | ||||||
|   margin-top: 16px; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -1,244 +0,0 @@ | ||||||
| <!-- 触发器类型选择组件 --> |  | ||||||
| <template> |  | ||||||
|   <div class="trigger-type-selector"> |  | ||||||
|     <el-form-item label="触发类型" required> |  | ||||||
|       <el-select |  | ||||||
|         v-model="localValue" |  | ||||||
|         placeholder="请选择触发类型" |  | ||||||
|         @change="handleChange" |  | ||||||
|         class="w-full" |  | ||||||
|       > |  | ||||||
|         <el-option |  | ||||||
|           v-for="option in triggerTypeOptions" |  | ||||||
|           :key="option.value" |  | ||||||
|           :label="option.label" |  | ||||||
|           :value="option.value" |  | ||||||
|         > |  | ||||||
|           <div class="trigger-option"> |  | ||||||
|             <div class="option-content"> |  | ||||||
|               <!-- TODO @puhui999:貌似没对齐? --> |  | ||||||
|               <Icon :icon="option.icon" class="option-icon" /> |  | ||||||
|               <div class="option-info"> |  | ||||||
|                 <div class="option-label">{{ option.label }}</div> |  | ||||||
|                 <div class="option-desc">{{ option.description }}</div> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|             <!-- TODO @puhui999:这个要不去掉? --> |  | ||||||
|             <el-tag :type="option.tag" size="small"> |  | ||||||
|               {{ option.category }} |  | ||||||
|             </el-tag> |  | ||||||
|           </div> |  | ||||||
|         </el-option> |  | ||||||
|       </el-select> |  | ||||||
|     </el-form-item> |  | ||||||
| 
 |  | ||||||
|     <!-- 类型说明 --> |  | ||||||
|     <!-- TODO @puhui999:这个去掉。感觉没啥内容哈; --> |  | ||||||
|     <div v-if="selectedOption" class="type-description"> |  | ||||||
|       <div class="desc-header"> |  | ||||||
|         <Icon :icon="selectedOption.icon" class="desc-icon" /> |  | ||||||
|         <span class="desc-title">{{ selectedOption.label }}</span> |  | ||||||
|       </div> |  | ||||||
|       <div class="desc-content"> |  | ||||||
|         <p class="desc-text">{{ selectedOption.description }}</p> |  | ||||||
|         <div class="desc-features"> |  | ||||||
|           <div v-for="feature in selectedOption.features" :key="feature" class="feature-item"> |  | ||||||
|             <Icon icon="ep:check" class="feature-icon" /> |  | ||||||
|             <span class="feature-text">{{ feature }}</span> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useVModel } from '@vueuse/core' |  | ||||||
| import { IotRuleSceneTriggerTypeEnum } from '@/api/iot/rule/scene/scene.types' |  | ||||||
| 
 |  | ||||||
| /** 触发器类型选择组件 */ |  | ||||||
| defineOptions({ name: 'TriggerTypeSelector' }) |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   modelValue: number |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface Emits { |  | ||||||
|   (e: 'update:modelValue', value: number): void |  | ||||||
|   (e: 'change', value: number): void |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const props = defineProps<Props>() |  | ||||||
| const emit = defineEmits<Emits>() |  | ||||||
| 
 |  | ||||||
| const localValue = useVModel(props, 'modelValue', emit) |  | ||||||
| 
 |  | ||||||
| // 触发器类型选项 |  | ||||||
| const triggerTypeOptions = [ |  | ||||||
|   { |  | ||||||
|     value: IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE, |  | ||||||
|     label: '设备状态变更', |  | ||||||
|     description: '当设备上线、离线状态发生变化时触发', |  | ||||||
|     icon: 'ep:connection', |  | ||||||
|     tag: 'warning', |  | ||||||
|     category: '设备状态', |  | ||||||
|     features: ['监控设备连接状态', '实时响应设备变化', '无需配置额外条件'] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     value: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST, |  | ||||||
|     label: '设备属性上报', |  | ||||||
|     description: '当设备属性值满足指定条件时触发', |  | ||||||
|     icon: 'ep:data-line', |  | ||||||
|     tag: 'primary', |  | ||||||
|     category: '数据监控', |  | ||||||
|     features: ['监控设备属性变化', '支持多种比较条件', '可配置阈值范围'] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     value: IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST, |  | ||||||
|     label: '设备事件上报', |  | ||||||
|     description: '当设备上报特定事件时触发', |  | ||||||
|     icon: 'ep:bell', |  | ||||||
|     tag: 'success', |  | ||||||
|     category: '事件监控', |  | ||||||
|     features: ['监控设备事件', '支持事件参数过滤', '实时事件响应'] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     value: IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE, |  | ||||||
|     label: '设备服务调用', |  | ||||||
|     description: '当设备服务被调用时触发', |  | ||||||
|     icon: 'ep:service', |  | ||||||
|     tag: 'info', |  | ||||||
|     category: '服务监控', |  | ||||||
|     features: ['监控服务调用', '支持参数条件', '服务执行跟踪'] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     value: IotRuleSceneTriggerTypeEnum.TIMER, |  | ||||||
|     label: '定时触发', |  | ||||||
|     description: '按照设定的时间计划定时触发', |  | ||||||
|     icon: 'ep:timer', |  | ||||||
|     tag: 'danger', |  | ||||||
|     category: '定时任务', |  | ||||||
|     features: ['支持CRON表达式', '灵活的时间配置', '可视化时间设置'] |  | ||||||
|   } |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| // 计算属性 |  | ||||||
| const selectedOption = computed(() => { |  | ||||||
|   return triggerTypeOptions.find((option) => option.value === localValue.value) |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| // 事件处理 |  | ||||||
| const handleChange = (value: number) => { |  | ||||||
|   emit('change', value) |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| /** TODO @puhui999:unocss 哈 */ |  | ||||||
| .trigger-type-selector { |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .trigger-option { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   width: 100%; |  | ||||||
|   padding: 4px 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-content { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 12px; |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-icon { |  | ||||||
|   font-size: 18px; |  | ||||||
|   color: var(--el-color-primary); |  | ||||||
|   flex-shrink: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-info { |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-label { |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
|   margin-bottom: 2px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-desc { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
|   line-height: 1.4; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .type-description { |  | ||||||
|   margin-top: 16px; |  | ||||||
|   padding: 16px; |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   border: 1px solid var(--el-border-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .desc-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
|   margin-bottom: 12px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .desc-icon { |  | ||||||
|   font-size: 20px; |  | ||||||
|   color: var(--el-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .desc-title { |  | ||||||
|   font-size: 16px; |  | ||||||
|   font-weight: 600; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .desc-content { |  | ||||||
|   margin-left: 28px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .desc-text { |  | ||||||
|   font-size: 14px; |  | ||||||
|   color: var(--el-text-color-regular); |  | ||||||
|   margin: 0 0 12px 0; |  | ||||||
|   line-height: 1.5; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .desc-features { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 6px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .feature-item { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 6px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .feature-icon { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-color-success); |  | ||||||
|   flex-shrink: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .feature-text { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-select-dropdown__item) { |  | ||||||
|   height: auto; |  | ||||||
|   padding: 8px 20px; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -0,0 +1,322 @@ | ||||||
|  | <!-- IoT场景联动规则表单 - 主表单组件 --> | ||||||
|  | <template> | ||||||
|  |   <el-drawer | ||||||
|  |     v-model="drawerVisible" | ||||||
|  |     :title="drawerTitle" | ||||||
|  |     size="80%" | ||||||
|  |     direction="rtl" | ||||||
|  |     :close-on-click-modal="false" | ||||||
|  |     :close-on-press-escape="false" | ||||||
|  |     @close="handleClose" | ||||||
|  |     class="[--el-drawer-padding-primary:20px]" | ||||||
|  |   > | ||||||
|  |     <div class="h-[calc(100vh-120px)] overflow-y-auto p-20px pb-80px"> | ||||||
|  |       <el-form | ||||||
|  |         ref="formRef" | ||||||
|  |         :model="formData" | ||||||
|  |         :rules="formRules" | ||||||
|  |         label-width="120px" | ||||||
|  |         class="flex flex-col gap-24px" | ||||||
|  |       > | ||||||
|  |         <!-- 基础信息配置 --> | ||||||
|  |         <BasicInfoSection v-model="formData" :rules="formRules" /> | ||||||
|  | 
 | ||||||
|  |         <!-- 触发器配置 --> | ||||||
|  |         <TriggerSection v-model:trigger="formData.trigger" @validate="handleTriggerValidate" /> | ||||||
|  | 
 | ||||||
|  |         <!-- 执行器配置 --> | ||||||
|  |         <ActionSection v-model:actions="formData.actions" @validate="handleActionValidate" /> | ||||||
|  |       </el-form> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <template #footer> | ||||||
|  |       <el-button :disabled="submitLoading" type="primary" @click="handleSubmit">确 定</el-button> | ||||||
|  |       <el-button @click="handleClose">取 消</el-button> | ||||||
|  |     </template> | ||||||
|  |   </el-drawer> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useVModel } from '@vueuse/core' | ||||||
|  | import BasicInfoSection from './sections/BasicInfoSection.vue' | ||||||
|  | import TriggerSection from './sections/TriggerSection.vue' | ||||||
|  | import ActionSection from './sections/ActionSection.vue' | ||||||
|  | import { | ||||||
|  |   RuleSceneFormData, | ||||||
|  |   IotRuleScene, | ||||||
|  |   IotRuleSceneActionTypeEnum, | ||||||
|  |   IotRuleSceneTriggerTypeEnum, | ||||||
|  |   CommonStatusEnum | ||||||
|  | } from '@/api/iot/rule/scene/scene.types' | ||||||
|  | import { getBaseValidationRules } from '../utils/validation' | ||||||
|  | import { ElMessage } from 'element-plus' | ||||||
|  | import { generateUUID } from '@/utils' | ||||||
|  | 
 | ||||||
|  | /** IoT 场景联动规则表单 - 主表单组件 */ | ||||||
|  | defineOptions({ name: 'RuleSceneForm' }) | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   modelValue: boolean | ||||||
|  |   ruleScene?: IotRuleScene | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Emits { | ||||||
|  |   (e: 'update:modelValue', value: boolean): void | ||||||
|  |   (e: 'success'): void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const props = defineProps<Props>() | ||||||
|  | const emit = defineEmits<Emits>() | ||||||
|  | 
 | ||||||
|  | const drawerVisible = useVModel(props, 'modelValue', emit) | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 创建默认的表单数据 | ||||||
|  |  */ | ||||||
|  | const createDefaultFormData = (): RuleSceneFormData => { | ||||||
|  |   return { | ||||||
|  |     name: '', | ||||||
|  |     description: '', | ||||||
|  |     status: CommonStatusEnum.ENABLE, // 默认启用状态 | ||||||
|  |     trigger: { | ||||||
|  |       type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST, | ||||||
|  |       productId: undefined, | ||||||
|  |       deviceId: undefined, | ||||||
|  |       identifier: undefined, | ||||||
|  |       operator: undefined, | ||||||
|  |       value: undefined, | ||||||
|  |       cronExpression: undefined, | ||||||
|  |       mainCondition: undefined, | ||||||
|  |       conditionGroup: undefined | ||||||
|  |     }, | ||||||
|  |     actions: [] | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 将表单数据转换为API请求格式 | ||||||
|  |  */ | ||||||
|  | const transformFormToApi = (formData: RuleSceneFormData): IotRuleScene => { | ||||||
|  |   return { | ||||||
|  |     id: formData.id, | ||||||
|  |     name: formData.name, | ||||||
|  |     description: formData.description, | ||||||
|  |     status: Number(formData.status), | ||||||
|  |     triggers: [ | ||||||
|  |       { | ||||||
|  |         type: formData.trigger.type, | ||||||
|  |         productKey: formData.trigger.productId | ||||||
|  |           ? `product_${formData.trigger.productId}` | ||||||
|  |           : undefined, | ||||||
|  |         deviceNames: formData.trigger.deviceId | ||||||
|  |           ? [`device_${formData.trigger.deviceId}`] | ||||||
|  |           : undefined, | ||||||
|  |         cronExpression: formData.trigger.cronExpression, | ||||||
|  |         conditions: [] // TODO: 实现新的条件转换逻辑 | ||||||
|  |       } | ||||||
|  |     ], | ||||||
|  |     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 => { | ||||||
|  |   const firstTrigger = apiData.triggers?.[0] | ||||||
|  |   return { | ||||||
|  |     ...apiData, | ||||||
|  |     status: Number(apiData.status), // 确保状态为数字类型 | ||||||
|  |     trigger: firstTrigger | ||||||
|  |       ? { | ||||||
|  |           ...firstTrigger, | ||||||
|  |           type: Number(firstTrigger.type) | ||||||
|  |         } | ||||||
|  |       : { | ||||||
|  |           type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST, | ||||||
|  |           productId: undefined, | ||||||
|  |           deviceId: undefined, | ||||||
|  |           identifier: undefined, | ||||||
|  |           operator: undefined, | ||||||
|  |           value: undefined, | ||||||
|  |           cronExpression: undefined, | ||||||
|  |           mainCondition: undefined, | ||||||
|  |           conditionGroup: undefined | ||||||
|  |         }, | ||||||
|  |     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 triggerValidation = ref({ valid: true, message: '' }) | ||||||
|  | const actionValidation = ref({ valid: true, message: '' }) | ||||||
|  | 
 | ||||||
|  | // 计算属性 | ||||||
|  | const isEdit = computed(() => !!props.ruleScene?.id) | ||||||
|  | const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' : '新增场景联动规则')) | ||||||
|  | 
 | ||||||
|  | // 事件处理 | ||||||
|  | const handleTriggerValidate = (result: { valid: boolean; message: string }) => { | ||||||
|  |   triggerValidation.value = result | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleActionValidate = (result: { valid: boolean; message: string }) => { | ||||||
|  |   actionValidation.value = result | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleSubmit = async () => { | ||||||
|  |   // 校验表单 | ||||||
|  |   if (!formRef.value) return | ||||||
|  |   const valid = await formRef.value.validate() | ||||||
|  |   if (!valid) return | ||||||
|  | 
 | ||||||
|  |   // 验证触发器和执行器 | ||||||
|  |   if (!triggerValidation.value.valid) { | ||||||
|  |     ElMessage.error(triggerValidation.value.message) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |   if (!actionValidation.value.valid) { | ||||||
|  |     ElMessage.error(actionValidation.value.message) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // 提交请求 | ||||||
|  |   submitLoading.value = true | ||||||
|  |   try { | ||||||
|  |     // 转换数据格式 | ||||||
|  |     const apiData = transformFormToApi(formData.value) | ||||||
|  | 
 | ||||||
|  |     // 这里应该调用API保存数据 | ||||||
|  |     console.log('提交数据:', apiData) | ||||||
|  | 
 | ||||||
|  |     // 模拟API调用 | ||||||
|  |     await new Promise((resolve) => setTimeout(resolve, 1000)) | ||||||
|  | 
 | ||||||
|  |     ElMessage.success(isEdit.value ? '更新成功' : '创建成功') | ||||||
|  |     drawerVisible.value = false | ||||||
|  |     emit('success') | ||||||
|  |   } finally { | ||||||
|  |     submitLoading.value = false | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleClose = () => { | ||||||
|  |   drawerVisible.value = false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 初始化表单数据 | ||||||
|  | const initFormData = () => { | ||||||
|  |   if (props.ruleScene) { | ||||||
|  |     formData.value = transformApiToForm(props.ruleScene) | ||||||
|  |   } else { | ||||||
|  |     formData.value = createDefaultFormData() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 监听抽屉显示 | ||||||
|  | watch(drawerVisible, (visible) => { | ||||||
|  |   if (visible) { | ||||||
|  |     initFormData() | ||||||
|  |     nextTick(() => { | ||||||
|  |       formRef.value?.clearValidate() | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | // 监听props变化 | ||||||
|  | watch( | ||||||
|  |   () => props.ruleScene, | ||||||
|  |   () => { | ||||||
|  |     if (drawerVisible.value) { | ||||||
|  |       initFormData() | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | ) | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | /* 滚动条样式 */ | ||||||
|  | .h-\[calc\(100vh-120px\)\]::-webkit-scrollbar { | ||||||
|  |   width: 6px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .h-\[calc\(100vh-120px\)\]::-webkit-scrollbar-track { | ||||||
|  |   background: var(--el-fill-color-light); | ||||||
|  |   border-radius: 3px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .h-\[calc\(100vh-120px\)\]::-webkit-scrollbar-thumb { | ||||||
|  |   background: var(--el-border-color); | ||||||
|  |   border-radius: 3px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .h-\[calc\(100vh-120px\)\]::-webkit-scrollbar-thumb:hover { | ||||||
|  |   background: var(--el-border-color-dark); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* 抽屉内容区域优化 */ | ||||||
|  | :deep(.el-drawer__body) { | ||||||
|  |   padding: 0; | ||||||
|  |   position: relative; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | :deep(.el-drawer__header) { | ||||||
|  |   padding: 20px 20px 16px 20px; | ||||||
|  |   border-bottom: 1px solid var(--el-border-color-light); | ||||||
|  |   margin-bottom: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | :deep(.el-drawer__title) { | ||||||
|  |   font-size: 18px; | ||||||
|  |   font-weight: 600; | ||||||
|  |   color: var(--el-text-color-primary); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* 响应式设计 */ | ||||||
|  | @media (max-width: 768px) { | ||||||
|  |   .el-drawer { | ||||||
|  |     --el-drawer-size: 100% !important; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .h-\[calc\(100vh-120px\)\] { | ||||||
|  |     padding: 16px; | ||||||
|  |     padding-bottom: 80px; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .flex.flex-col.gap-24px { | ||||||
|  |     gap: 20px; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .absolute.bottom-0 { | ||||||
|  |     padding: 12px 16px; | ||||||
|  |     gap: 12px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <!-- 告警配置组件 --> | <!-- 告警配置组件 --> | ||||||
| <template> | <template> | ||||||
|   <div class="alert-config"> |   <div class="w-full"> | ||||||
|     <!-- TODO @puhui999:触发告警时,不用选择配置哈; --> |     <!-- TODO @puhui999:触发告警时,不用选择配置哈; --> | ||||||
|     <el-form-item label="告警配置" required> |     <el-form-item label="告警配置" required> | ||||||
|       <el-select |       <el-select | ||||||
|  | @ -18,10 +18,10 @@ | ||||||
|           :label="config.name" |           :label="config.name" | ||||||
|           :value="config.id" |           :value="config.id" | ||||||
|         > |         > | ||||||
|           <div class="alert-option"> |           <div class="flex items-center justify-between w-full py-4px"> | ||||||
|             <div class="option-content"> |             <div class="flex-1"> | ||||||
|               <div class="option-name">{{ config.name }}</div> |               <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ config.name }}</div> | ||||||
|               <div class="option-desc">{{ config.description }}</div> |               <div class="text-12px text-[var(--el-text-color-secondary)]">{{ config.description }}</div> | ||||||
|             </div> |             </div> | ||||||
|             <el-tag :type="config.enabled ? 'success' : 'danger'" size="small"> |             <el-tag :type="config.enabled ? 'success' : 'danger'" size="small"> | ||||||
|               {{ config.enabled ? '启用' : '禁用' }} |               {{ config.enabled ? '启用' : '禁用' }} | ||||||
|  | @ -32,32 +32,32 @@ | ||||||
|     </el-form-item> |     </el-form-item> | ||||||
| 
 | 
 | ||||||
|     <!-- 告警配置详情 --> |     <!-- 告警配置详情 --> | ||||||
|     <div v-if="selectedConfig" class="alert-details"> |     <div v-if="selectedConfig" class="mt-16px p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"> | ||||||
|       <div class="details-header"> |       <div class="flex items-center gap-8px mb-12px"> | ||||||
|         <Icon icon="ep:bell" class="details-icon" /> |         <Icon icon="ep:bell" class="text-[var(--el-color-warning)] text-16px" /> | ||||||
|         <span class="details-title">{{ selectedConfig.name }}</span> |         <span class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ selectedConfig.name }}</span> | ||||||
|         <el-tag :type="selectedConfig.enabled ? 'success' : 'danger'" size="small"> |         <el-tag :type="selectedConfig.enabled ? 'success' : 'danger'" size="small"> | ||||||
|           {{ selectedConfig.enabled ? '启用' : '禁用' }} |           {{ selectedConfig.enabled ? '启用' : '禁用' }} | ||||||
|         </el-tag> |         </el-tag> | ||||||
|       </div> |       </div> | ||||||
|       <div class="details-content"> |       <div class="space-y-8px"> | ||||||
|         <div class="detail-item"> |         <div class="flex items-start gap-8px"> | ||||||
|           <span class="detail-label">描述:</span> |           <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">描述:</span> | ||||||
|           <span class="detail-value">{{ selectedConfig.description }}</span> |           <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedConfig.description }}</span> | ||||||
|         </div> |         </div> | ||||||
|         <div class="detail-item"> |         <div class="flex items-start gap-8px"> | ||||||
|           <span class="detail-label">通知方式:</span> |           <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">通知方式:</span> | ||||||
|           <span class="detail-value">{{ getNotifyTypeName(selectedConfig.notifyType) }}</span> |           <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ getNotifyTypeName(selectedConfig.notifyType) }}</span> | ||||||
|         </div> |         </div> | ||||||
|         <div v-if="selectedConfig.receivers" class="detail-item"> |         <div v-if="selectedConfig.receivers" class="flex items-start gap-8px"> | ||||||
|           <span class="detail-label">接收人:</span> |           <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">接收人:</span> | ||||||
|           <span class="detail-value">{{ selectedConfig.receivers.join(', ') }}</span> |           <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedConfig.receivers.join(', ') }}</span> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <!-- 验证结果 --> |     <!-- 验证结果 --> | ||||||
|     <div v-if="validationMessage" class="validation-result"> |     <div v-if="validationMessage" class="mt-16px"> | ||||||
|       <el-alert |       <el-alert | ||||||
|         :title="validationMessage" |         :title="validationMessage" | ||||||
|         :type="isValid ? 'success' : 'error'" |         :type="isValid ? 'success' : 'error'" | ||||||
|  | @ -202,90 +202,6 @@ onMounted(() => { | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| .alert-config { |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .alert-option { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   width: 100%; |  | ||||||
|   padding: 4px 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-content { |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-name { |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
|   margin-bottom: 2px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-desc { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .alert-details { |  | ||||||
|   margin-top: 12px; |  | ||||||
|   padding: 12px; |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   border: 1px solid var(--el-border-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .details-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
|   margin-bottom: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .details-icon { |  | ||||||
|   color: var(--el-color-warning); |  | ||||||
|   font-size: 14px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .details-title { |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .details-content { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 4px; |  | ||||||
|   margin-left: 22px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .detail-item { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: flex-start; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .detail-label { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
|   min-width: 60px; |  | ||||||
|   flex-shrink: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .detail-value { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .validation-result { |  | ||||||
|   margin-top: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-select-dropdown__item) { | :deep(.el-select-dropdown__item) { | ||||||
|   height: auto; |   height: auto; | ||||||
|   padding: 8px 20px; |   padding: 8px 20px; | ||||||
|  | @ -0,0 +1,332 @@ | ||||||
|  | <!-- 单个条件配置组件 --> | ||||||
|  | <template> | ||||||
|  |   <div class="flex flex-col gap-16px"> | ||||||
|  |     <!-- 条件类型选择 --> | ||||||
|  |     <el-row :gutter="16"> | ||||||
|  |       <el-col :span="8"> | ||||||
|  |         <el-form-item label="条件类型" required> | ||||||
|  |           <ConditionTypeSelector | ||||||
|  |             :model-value="condition.type" | ||||||
|  |             @update:model-value="(value) => updateConditionField('type', value)" | ||||||
|  |             @change="handleConditionTypeChange" | ||||||
|  |           /> | ||||||
|  |         </el-form-item> | ||||||
|  |       </el-col> | ||||||
|  |     </el-row> | ||||||
|  | 
 | ||||||
|  |     <!-- 设备状态条件配置 --> | ||||||
|  |     <DeviceStatusConditionConfig | ||||||
|  |       v-if="condition.type === ConditionTypeEnum.DEVICE_STATUS" | ||||||
|  |       :model-value="condition" | ||||||
|  |       @update:model-value="updateCondition" | ||||||
|  |       @validate="handleValidate" | ||||||
|  |     /> | ||||||
|  | 
 | ||||||
|  |     <!-- 设备属性条件配置 --> | ||||||
|  |     <div v-else-if="condition.type === ConditionTypeEnum.DEVICE_PROPERTY" class="space-y-16px"> | ||||||
|  |       <!-- 产品设备选择 --> | ||||||
|  |       <el-row :gutter="16"> | ||||||
|  |         <el-col :span="12"> | ||||||
|  |           <el-form-item label="产品" required> | ||||||
|  |             <ProductSelector | ||||||
|  |               :model-value="condition.productId" | ||||||
|  |               @update:model-value="(value) => updateConditionField('productId', value)" | ||||||
|  |               @change="handleProductChange" | ||||||
|  |             /> | ||||||
|  |           </el-form-item> | ||||||
|  |         </el-col> | ||||||
|  |         <el-col :span="12"> | ||||||
|  |           <el-form-item label="设备" required> | ||||||
|  |             <DeviceSelector | ||||||
|  |               :model-value="condition.deviceId" | ||||||
|  |               @update:model-value="(value) => updateConditionField('deviceId', value)" | ||||||
|  |               :product-id="condition.productId" | ||||||
|  |               @change="handleDeviceChange" | ||||||
|  |             /> | ||||||
|  |           </el-form-item> | ||||||
|  |         </el-col> | ||||||
|  |       </el-row> | ||||||
|  | 
 | ||||||
|  |       <!-- 属性配置 --> | ||||||
|  |       <el-row :gutter="16"> | ||||||
|  |         <!-- 属性/事件/服务选择 --> | ||||||
|  |         <el-col :span="6"> | ||||||
|  |           <el-form-item label="监控项" required> | ||||||
|  |             <PropertySelector | ||||||
|  |               :model-value="condition.identifier" | ||||||
|  |               @update:model-value="(value) => updateConditionField('identifier', value)" | ||||||
|  |               :trigger-type="triggerType" | ||||||
|  |               :product-id="condition.productId" | ||||||
|  |               :device-id="condition.deviceId" | ||||||
|  |               @change="handlePropertyChange" | ||||||
|  |             /> | ||||||
|  |           </el-form-item> | ||||||
|  |         </el-col> | ||||||
|  | 
 | ||||||
|  |         <!-- 操作符选择 --> | ||||||
|  |         <el-col :span="6"> | ||||||
|  |           <el-form-item label="操作符" required> | ||||||
|  |             <OperatorSelector | ||||||
|  |               :model-value="condition.operator" | ||||||
|  |               @update:model-value="(value) => updateConditionField('operator', value)" | ||||||
|  |               :property-type="propertyType" | ||||||
|  |               @change="handleOperatorChange" | ||||||
|  |             /> | ||||||
|  |           </el-form-item> | ||||||
|  |         </el-col> | ||||||
|  | 
 | ||||||
|  |         <!-- 值输入 --> | ||||||
|  |         <el-col :span="12"> | ||||||
|  |           <el-form-item label="比较值" required> | ||||||
|  |             <ValueInput | ||||||
|  |               :model-value="condition.param" | ||||||
|  |               @update:model-value="(value) => updateConditionField('param', value)" | ||||||
|  |               :property-type="propertyType" | ||||||
|  |               :operator="condition.operator" | ||||||
|  |               :property-config="propertyConfig" | ||||||
|  |               @validate="handleValueValidate" | ||||||
|  |             /> | ||||||
|  |           </el-form-item> | ||||||
|  |         </el-col> | ||||||
|  |       </el-row> | ||||||
|  | 
 | ||||||
|  |       <!-- 条件预览 --> | ||||||
|  |       <div | ||||||
|  |         v-if="conditionPreview" | ||||||
|  |         class="p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]" | ||||||
|  |       > | ||||||
|  |         <div class="flex items-center gap-8px mb-8px"> | ||||||
|  |           <Icon icon="ep:view" class="text-[var(--el-color-info)] text-16px" /> | ||||||
|  |           <span class="text-14px font-500 text-[var(--el-text-color-primary)]">条件预览</span> | ||||||
|  |         </div> | ||||||
|  |         <div class="pl-24px"> | ||||||
|  |           <code | ||||||
|  |             class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] p-8px rounded-4px font-mono" | ||||||
|  |             >{{ conditionPreview }}</code | ||||||
|  |           > | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <!-- 当前时间条件配置 --> | ||||||
|  |     <CurrentTimeConditionConfig | ||||||
|  |       v-else-if="condition.type === ConditionTypeEnum.CURRENT_TIME" | ||||||
|  |       :model-value="condition" | ||||||
|  |       @update:model-value="updateCondition" | ||||||
|  |       @validate="handleValidate" | ||||||
|  |     /> | ||||||
|  | 
 | ||||||
|  |     <!-- 验证结果 --> | ||||||
|  |     <div v-if="validationMessage" class="mt-8px"> | ||||||
|  |       <el-alert | ||||||
|  |         :title="validationMessage" | ||||||
|  |         :type="isValid ? 'success' : 'error'" | ||||||
|  |         :closable="false" | ||||||
|  |         show-icon | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useVModel } from '@vueuse/core' | ||||||
|  | import ConditionTypeSelector from '../selectors/ConditionTypeSelector.vue' | ||||||
|  | import DeviceStatusConditionConfig from './DeviceStatusConditionConfig.vue' | ||||||
|  | import CurrentTimeConditionConfig from './CurrentTimeConditionConfig.vue' | ||||||
|  | import ProductSelector from '../selectors/ProductSelector.vue' | ||||||
|  | import DeviceSelector from '../selectors/DeviceSelector.vue' | ||||||
|  | import PropertySelector from '../selectors/PropertySelector.vue' | ||||||
|  | import OperatorSelector from '../selectors/OperatorSelector.vue' | ||||||
|  | import ValueInput from '../inputs/ValueInput.vue' | ||||||
|  | import { | ||||||
|  |   ConditionFormData, | ||||||
|  |   IotRuleSceneTriggerConditionTypeEnum | ||||||
|  | } from '@/api/iot/rule/scene/scene.types' | ||||||
|  | 
 | ||||||
|  | /** 单个条件配置组件 */ | ||||||
|  | defineOptions({ name: 'ConditionConfig' }) | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   modelValue: ConditionFormData | ||||||
|  |   triggerType: number | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Emits { | ||||||
|  |   (e: 'update:modelValue', value: ConditionFormData): void | ||||||
|  |   (e: 'validate', result: { valid: boolean; message: string }): void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const props = defineProps<Props>() | ||||||
|  | const emit = defineEmits<Emits>() | ||||||
|  | 
 | ||||||
|  | const condition = useVModel(props, 'modelValue', emit) | ||||||
|  | 
 | ||||||
|  | // 常量定义 | ||||||
|  | const ConditionTypeEnum = IotRuleSceneTriggerConditionTypeEnum | ||||||
|  | 
 | ||||||
|  | // 状态 | ||||||
|  | const propertyType = ref<string>('string') | ||||||
|  | const propertyConfig = ref<any>(null) | ||||||
|  | const validationMessage = ref('') | ||||||
|  | const isValid = ref(true) | ||||||
|  | const valueValidation = ref({ valid: true, message: '' }) | ||||||
|  | 
 | ||||||
|  | // 计算属性 | ||||||
|  | const conditionPreview = computed(() => { | ||||||
|  |   if (!condition.value.identifier || !condition.value.operator || !condition.value.param) { | ||||||
|  |     return '' | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const propertyName = propertyConfig.value?.name || condition.value.identifier | ||||||
|  |   const operatorText = getOperatorText(condition.value.operator) | ||||||
|  |   const value = condition.value.param | ||||||
|  | 
 | ||||||
|  |   return `当 ${propertyName} ${operatorText} ${value} 时触发` | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | // 工具函数 | ||||||
|  | const getOperatorText = (operator: string) => { | ||||||
|  |   const operatorMap = { | ||||||
|  |     '=': '等于', | ||||||
|  |     '!=': '不等于', | ||||||
|  |     '>': '大于', | ||||||
|  |     '>=': '大于等于', | ||||||
|  |     '<': '小于', | ||||||
|  |     '<=': '小于等于', | ||||||
|  |     in: '包含于', | ||||||
|  |     between: '介于' | ||||||
|  |   } | ||||||
|  |   return operatorMap[operator] || operator | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 事件处理 | ||||||
|  | const updateConditionField = (field: keyof ConditionFormData, value: any) => { | ||||||
|  |   ;(condition.value as any)[field] = value | ||||||
|  |   emit('update:modelValue', condition.value) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const updateCondition = (newCondition: ConditionFormData) => { | ||||||
|  |   condition.value = newCondition | ||||||
|  |   emit('update:modelValue', condition.value) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleConditionTypeChange = (type: number) => { | ||||||
|  |   // 清理不相关的字段 | ||||||
|  |   if (type === ConditionTypeEnum.DEVICE_STATUS) { | ||||||
|  |     condition.value.identifier = undefined | ||||||
|  |     condition.value.timeValue = undefined | ||||||
|  |     condition.value.timeValue2 = undefined | ||||||
|  |   } else if (type === ConditionTypeEnum.CURRENT_TIME) { | ||||||
|  |     condition.value.identifier = undefined | ||||||
|  |     condition.value.productId = undefined | ||||||
|  |     condition.value.deviceId = undefined | ||||||
|  |   } else if (type === ConditionTypeEnum.DEVICE_PROPERTY) { | ||||||
|  |     condition.value.timeValue = undefined | ||||||
|  |     condition.value.timeValue2 = undefined | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // 重置操作符和参数 | ||||||
|  |   condition.value.operator = '=' | ||||||
|  |   condition.value.param = '' | ||||||
|  | 
 | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleValidate = (result: { valid: boolean; message: string }) => { | ||||||
|  |   isValid.value = result.valid | ||||||
|  |   validationMessage.value = result.message | ||||||
|  |   emit('validate', result) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleProductChange = (productId: number) => { | ||||||
|  |   // 产品变化时清空设备和属性 | ||||||
|  |   condition.value.deviceId = undefined | ||||||
|  |   condition.value.identifier = '' | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleDeviceChange = (deviceId: number) => { | ||||||
|  |   // 设备变化时清空属性 | ||||||
|  |   condition.value.identifier = '' | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handlePropertyChange = (propertyInfo: { type: string; config: any }) => { | ||||||
|  |   propertyType.value = propertyInfo.type | ||||||
|  |   propertyConfig.value = propertyInfo.config | ||||||
|  | 
 | ||||||
|  |   // 重置操作符和值 | ||||||
|  |   condition.value.operator = '=' | ||||||
|  |   condition.value.param = '' | ||||||
|  | 
 | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleOperatorChange = () => { | ||||||
|  |   // 重置值 | ||||||
|  |   condition.value.param = '' | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleValueValidate = (result: { valid: boolean; message: string }) => { | ||||||
|  |   valueValidation.value = result | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const updateValidationResult = () => { | ||||||
|  |   // 基础验证 | ||||||
|  |   if (!condition.value.identifier) { | ||||||
|  |     isValid.value = false | ||||||
|  |     validationMessage.value = '请选择监控项' | ||||||
|  |     emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (!condition.value.operator) { | ||||||
|  |     isValid.value = false | ||||||
|  |     validationMessage.value = '请选择操作符' | ||||||
|  |     emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (!condition.value.param) { | ||||||
|  |     isValid.value = false | ||||||
|  |     validationMessage.value = '请输入比较值' | ||||||
|  |     emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // 值验证 | ||||||
|  |   if (!valueValidation.value.valid) { | ||||||
|  |     isValid.value = false | ||||||
|  |     validationMessage.value = valueValidation.value.message | ||||||
|  |     emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // 验证通过 | ||||||
|  |   isValid.value = true | ||||||
|  |   validationMessage.value = '条件配置验证通过' | ||||||
|  |   emit('validate', { valid: true, message: validationMessage.value }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 监听条件变化 | ||||||
|  | watch( | ||||||
|  |   () => [condition.value.identifier, condition.value.operator, condition.value.param], | ||||||
|  |   () => { | ||||||
|  |     updateValidationResult() | ||||||
|  |   }, | ||||||
|  |   { deep: true } | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // 初始化 | ||||||
|  | onMounted(() => { | ||||||
|  |   updateValidationResult() | ||||||
|  | }) | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | :deep(.el-form-item) { | ||||||
|  |   margin-bottom: 0; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -1,17 +1,37 @@ | ||||||
| <!-- 条件组配置组件 --> | <!-- 条件组配置组件 --> | ||||||
| <template> | <template> | ||||||
|   <div class="condition-group-config"> |   <div class="p-16px"> | ||||||
|     <div class="group-content"> |     <!-- 条件组说明 --> | ||||||
|  |     <div | ||||||
|  |       v-if="group.conditions && group.conditions.length > 1" | ||||||
|  |       class="mb-12px flex items-center justify-center" | ||||||
|  |     > | ||||||
|  |       <div | ||||||
|  |         class="flex items-center gap-6px px-10px py-4px bg-green-50 border border-green-200 rounded-full text-11px text-green-600" | ||||||
|  |       > | ||||||
|  |         <Icon icon="ep:info-filled" /> | ||||||
|  |         <span>组内所有条件必须同时满足(且关系)</span> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <div class="space-y-12px"> | ||||||
|       <!-- 条件列表 --> |       <!-- 条件列表 --> | ||||||
|       <div v-if="group.conditions && group.conditions.length > 0" class="conditions-list"> |       <div v-if="group.conditions && group.conditions.length > 0" class="space-y-12px"> | ||||||
|         <div |         <div | ||||||
|           v-for="(condition, index) in group.conditions" |           v-for="(condition, index) in group.conditions" | ||||||
|           :key="`condition-${index}`" |           :key="`condition-${index}`" | ||||||
|           class="condition-item" |           class="p-12px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-light)] shadow-sm hover:shadow-md transition-shadow" | ||||||
|         > |         > | ||||||
|           <div class="condition-header"> |           <div class="flex items-center justify-between mb-12px"> | ||||||
|             <div class="condition-title"> |             <div class="flex items-center gap-8px"> | ||||||
|               <span>条件 {{ index + 1 }}</span> |               <div class="flex items-center gap-6px"> | ||||||
|  |                 <div | ||||||
|  |                   class="w-18px h-18px bg-green-500 text-white rounded-full flex items-center justify-center text-10px font-bold" | ||||||
|  |                 > | ||||||
|  |                   {{ index + 1 }} | ||||||
|  |                 </div> | ||||||
|  |                 <span class="text-14px font-500 text-[var(--el-text-color-primary)]">条件</span> | ||||||
|  |               </div> | ||||||
|               <el-tag size="small" type="primary"> |               <el-tag size="small" type="primary"> | ||||||
|                 {{ getConditionTypeName(condition.type) }} |                 {{ getConditionTypeName(condition.type) }} | ||||||
|               </el-tag> |               </el-tag> | ||||||
|  | @ -28,7 +48,7 @@ | ||||||
|             </el-button> |             </el-button> | ||||||
|           </div> |           </div> | ||||||
| 
 | 
 | ||||||
|           <div class="condition-content"> |           <div class="p-12px bg-[var(--el-fill-color-blank)] rounded-4px"> | ||||||
|             <ConditionConfig |             <ConditionConfig | ||||||
|               :model-value="condition" |               :model-value="condition" | ||||||
|               @update:model-value="(value) => updateCondition(index, value)" |               @update:model-value="(value) => updateCondition(index, value)" | ||||||
|  | @ -39,24 +59,36 @@ | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
| 
 | 
 | ||||||
|           <!-- 逻辑连接符 --> |           <!-- 条件间的"且"连接符 --> | ||||||
|           <!-- TODO @puhui999:不用这个哈; --> |           <div | ||||||
|           <div v-if="index < group.conditions!.length - 1" class="logic-connector"> |             v-if="index < group.conditions!.length - 1" | ||||||
|             <el-select v-model="group.logicOperator" size="small" style="width: 80px"> |             class="flex items-center justify-center py-8px" | ||||||
|               <el-option label="且" value="AND" /> |           > | ||||||
|               <el-option label="或" value="OR" /> |             <div class="flex items-center gap-6px"> | ||||||
|             </el-select> |               <!-- 连接线 --> | ||||||
|  |               <div class="w-24px h-1px bg-green-300"></div> | ||||||
|  |               <!-- 且标签 --> | ||||||
|  |               <div class="px-12px py-4px bg-green-100 border-2 border-green-300 rounded-full"> | ||||||
|  |                 <span class="text-12px font-600 text-green-600">且</span> | ||||||
|  |               </div> | ||||||
|  |               <!-- 连接线 --> | ||||||
|  |               <div class="w-24px h-1px bg-green-300"></div> | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <!-- 空状态 --> |       <!-- 空状态 --> | ||||||
|       <div v-else class="empty-conditions"> |       <div v-else class="py-40px text-center"> | ||||||
|         <el-empty description="暂无条件配置" :image-size="80"> |         <el-empty description="暂无条件配置" :image-size="80"> | ||||||
|           <el-button type="primary" @click="addCondition"> |           <template #description> | ||||||
|             <Icon icon="ep:plus" /> |             <div class="space-y-8px"> | ||||||
|             添加第一个条件 |               <p class="text-[var(--el-text-color-secondary)]">暂无条件配置</p> | ||||||
|           </el-button> |               <p class="text-12px text-[var(--el-text-color-placeholder)]"> | ||||||
|  |                 条件组需要至少包含一个条件才能生效 | ||||||
|  |               </p> | ||||||
|  |             </div> | ||||||
|  |           </template> | ||||||
|         </el-empty> |         </el-empty> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|  | @ -65,26 +97,17 @@ | ||||||
|         v-if=" |         v-if=" | ||||||
|           group.conditions && group.conditions.length > 0 && group.conditions.length < maxConditions |           group.conditions && group.conditions.length > 0 && group.conditions.length < maxConditions | ||||||
|         " |         " | ||||||
|         class="add-condition" |         class="text-center py-16px" | ||||||
|       > |       > | ||||||
|         <el-button type="primary" plain @click="addCondition" class="add-condition-btn"> |         <el-button type="primary" plain @click="addCondition"> | ||||||
|           <Icon icon="ep:plus" /> |           <Icon icon="ep:plus" /> | ||||||
|           继续添加条件 |           继续添加条件 | ||||||
|         </el-button> |         </el-button> | ||||||
|         <span class="add-condition-text"> 最多可添加 {{ maxConditions }} 个条件 </span> |         <span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]"> | ||||||
|  |           最多可添加 {{ maxConditions }} 个条件 | ||||||
|  |         </span> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| 
 |  | ||||||
|     <!-- 验证结果 --> |  | ||||||
|     <!-- TODO @puhui999:是不是不用这种提示;只要 validator rules 能展示出来就好了呀。。。 --> |  | ||||||
|     <div v-if="validationMessage" class="validation-result"> |  | ||||||
|       <el-alert |  | ||||||
|         :title="validationMessage" |  | ||||||
|         :type="isValid ? 'success' : 'error'" |  | ||||||
|         :closable="false" |  | ||||||
|         show-icon |  | ||||||
|       /> |  | ||||||
|     </div> |  | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -105,6 +128,7 @@ interface Props { | ||||||
|   triggerType: number |   triggerType: number | ||||||
|   productId?: number |   productId?: number | ||||||
|   deviceId?: number |   deviceId?: number | ||||||
|  |   maxConditions?: number | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface Emits { | interface Emits { | ||||||
|  | @ -118,7 +142,7 @@ const emit = defineEmits<Emits>() | ||||||
| const group = useVModel(props, 'modelValue', emit) | const group = useVModel(props, 'modelValue', emit) | ||||||
| 
 | 
 | ||||||
| // 配置常量 | // 配置常量 | ||||||
| const maxConditions = 5 | const maxConditions = computed(() => props.maxConditions || 3) | ||||||
| 
 | 
 | ||||||
| // 验证状态 | // 验证状态 | ||||||
| const conditionValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({}) | const conditionValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({}) | ||||||
|  | @ -149,12 +173,12 @@ const addCondition = () => { | ||||||
|     group.value.conditions = [] |     group.value.conditions = [] | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (group.value.conditions.length >= maxConditions) { |   if (group.value.conditions.length >= maxConditions.value) { | ||||||
|     return |     return | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const newCondition: ConditionFormData = { |   const newCondition: ConditionFormData = { | ||||||
|     type: props.triggerType, |     type: 2, // 默认为设备属性条件 | ||||||
|     productId: props.productId || 0, |     productId: props.productId || 0, | ||||||
|     deviceId: props.deviceId || 0, |     deviceId: props.deviceId || 0, | ||||||
|     identifier: '', |     identifier: '', | ||||||
|  | @ -229,97 +253,3 @@ onMounted(() => { | ||||||
|   } |   } | ||||||
| }) | }) | ||||||
| </script> | </script> | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .condition-group-config { |  | ||||||
|   padding: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .group-content { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .conditions-list { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 12px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .condition-item { |  | ||||||
|   position: relative; |  | ||||||
|   border: 1px solid var(--el-border-color-lighter); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   background: var(--el-fill-color-blank); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .condition-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   padding: 12px 16px; |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-bottom: 1px solid var(--el-border-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .condition-title { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .condition-content { |  | ||||||
|   padding: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .logic-connector { |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: center; |  | ||||||
|   align-items: center; |  | ||||||
|   padding: 8px 0; |  | ||||||
|   position: relative; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .logic-connector::before { |  | ||||||
|   content: ''; |  | ||||||
|   position: absolute; |  | ||||||
|   top: 0; |  | ||||||
|   left: 50%; |  | ||||||
|   transform: translateX(-50%); |  | ||||||
|   width: 1px; |  | ||||||
|   height: 100%; |  | ||||||
|   background: var(--el-border-color); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .empty-conditions { |  | ||||||
|   padding: 40px 0; |  | ||||||
|   text-align: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .add-condition { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 12px; |  | ||||||
|   padding: 16px; |  | ||||||
|   border: 1px dashed var(--el-border-color); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   background: var(--el-fill-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .add-condition-btn { |  | ||||||
|   flex-shrink: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .add-condition-text { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .validation-result { |  | ||||||
|   margin-top: 16px; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -0,0 +1,247 @@ | ||||||
|  | <!-- 条件组容器配置组件 --> | ||||||
|  | <template> | ||||||
|  |   <div class="flex flex-col gap-16px"> | ||||||
|  |     <!-- 条件组容器头部 --> | ||||||
|  |     <div | ||||||
|  |       class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-8px" | ||||||
|  |     > | ||||||
|  |       <div class="flex items-center gap-12px"> | ||||||
|  |         <div class="flex items-center gap-8px text-16px font-600 text-green-700"> | ||||||
|  |           <div | ||||||
|  |             class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold" | ||||||
|  |           > | ||||||
|  |             组 | ||||||
|  |           </div> | ||||||
|  |           <span>附加条件组</span> | ||||||
|  |         </div> | ||||||
|  |         <el-tag size="small" type="success">与主条件为且关系</el-tag> | ||||||
|  |         <el-tag size="small" type="info"> | ||||||
|  |           {{ modelValue.subGroups?.length || 0 }}个子条件组 | ||||||
|  |         </el-tag> | ||||||
|  |       </div> | ||||||
|  |       <div class="flex items-center gap-8px"> | ||||||
|  |         <el-button | ||||||
|  |           type="primary" | ||||||
|  |           size="small" | ||||||
|  |           @click="addSubGroup" | ||||||
|  |           :disabled="(modelValue.subGroups?.length || 0) >= maxSubGroups" | ||||||
|  |         > | ||||||
|  |           <Icon icon="ep:plus" /> | ||||||
|  |           添加子条件组 | ||||||
|  |         </el-button> | ||||||
|  |         <el-button type="danger" size="small" text @click="removeContainer"> | ||||||
|  |           <Icon icon="ep:delete" /> | ||||||
|  |           删除条件组 | ||||||
|  |         </el-button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <!-- 子条件组列表 --> | ||||||
|  |     <div v-if="modelValue.subGroups && modelValue.subGroups.length > 0" class="space-y-16px"> | ||||||
|  |       <!-- 逻辑关系说明 --> | ||||||
|  |       <div v-if="modelValue.subGroups.length > 1" class="flex items-center justify-center"> | ||||||
|  |         <div | ||||||
|  |           class="flex items-center gap-8px px-12px py-6px bg-orange-50 border border-orange-200 rounded-full text-12px text-orange-600" | ||||||
|  |         > | ||||||
|  |           <Icon icon="ep:info-filled" /> | ||||||
|  |           <span>子条件组之间为"或"关系,满足任意一组即可触发</span> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <div class="relative"> | ||||||
|  |         <div | ||||||
|  |           v-for="(subGroup, subGroupIndex) in modelValue.subGroups" | ||||||
|  |           :key="`sub-group-${subGroupIndex}`" | ||||||
|  |           class="relative" | ||||||
|  |         > | ||||||
|  |           <!-- 子条件组容器 --> | ||||||
|  |           <div | ||||||
|  |             class="border-2 border-orange-200 rounded-8px bg-orange-50 shadow-sm hover:shadow-md transition-shadow" | ||||||
|  |           > | ||||||
|  |             <div | ||||||
|  |               class="flex items-center justify-between p-16px bg-gradient-to-r from-orange-50 to-yellow-50 border-b border-orange-200 rounded-t-6px" | ||||||
|  |             > | ||||||
|  |               <div class="flex items-center gap-12px"> | ||||||
|  |                 <div class="flex items-center gap-8px text-16px font-600 text-orange-700"> | ||||||
|  |                   <div | ||||||
|  |                     class="w-24px h-24px bg-orange-500 text-white rounded-full flex items-center justify-center text-12px font-bold" | ||||||
|  |                   > | ||||||
|  |                     {{ subGroupIndex + 1 }} | ||||||
|  |                   </div> | ||||||
|  |                   <span>子条件组 {{ subGroupIndex + 1 }}</span> | ||||||
|  |                 </div> | ||||||
|  |                 <el-tag size="small" type="warning" class="font-500">组内条件为"且"关系</el-tag> | ||||||
|  |                 <el-tag size="small" type="info"> | ||||||
|  |                   {{ subGroup.conditions?.length || 0 }}个条件 | ||||||
|  |                 </el-tag> | ||||||
|  |               </div> | ||||||
|  |               <el-button | ||||||
|  |                 type="danger" | ||||||
|  |                 size="small" | ||||||
|  |                 text | ||||||
|  |                 @click="removeSubGroup(subGroupIndex)" | ||||||
|  |                 class="hover:bg-red-50" | ||||||
|  |               > | ||||||
|  |                 <Icon icon="ep:delete" /> | ||||||
|  |                 删除组 | ||||||
|  |               </el-button> | ||||||
|  |             </div> | ||||||
|  | 
 | ||||||
|  |             <SubConditionGroupConfig | ||||||
|  |               :model-value="subGroup" | ||||||
|  |               @update:model-value="(value) => updateSubGroup(subGroupIndex, value)" | ||||||
|  |               :trigger-type="triggerType" | ||||||
|  |               :max-conditions="maxConditionsPerGroup" | ||||||
|  |               @validate="(result) => handleSubGroupValidate(subGroupIndex, result)" | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|  | 
 | ||||||
|  |           <!-- 子条件组间的"或"连接符 --> | ||||||
|  |           <div | ||||||
|  |             v-if="subGroupIndex < modelValue.subGroups!.length - 1" | ||||||
|  |             class="flex items-center justify-center py-12px" | ||||||
|  |           > | ||||||
|  |             <div class="flex items-center gap-8px"> | ||||||
|  |               <!-- 连接线 --> | ||||||
|  |               <div class="w-32px h-1px bg-orange-300"></div> | ||||||
|  |               <!-- 或标签 --> | ||||||
|  |               <div class="px-16px py-6px bg-orange-100 border-2 border-orange-300 rounded-full"> | ||||||
|  |                 <span class="text-14px font-600 text-orange-600">或</span> | ||||||
|  |               </div> | ||||||
|  |               <!-- 连接线 --> | ||||||
|  |               <div class="w-32px h-1px bg-orange-300"></div> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <!-- 空状态 --> | ||||||
|  |     <div | ||||||
|  |       v-else | ||||||
|  |       class="p-24px border-2 border-dashed border-orange-200 rounded-8px text-center bg-orange-50" | ||||||
|  |     > | ||||||
|  |       <div class="flex flex-col items-center gap-12px"> | ||||||
|  |         <Icon icon="ep:plus" class="text-32px text-orange-400" /> | ||||||
|  |         <div class="text-orange-600"> | ||||||
|  |           <p class="text-14px font-500 mb-4px">暂无子条件组</p> | ||||||
|  |           <p class="text-12px">点击上方"添加子条件组"按钮开始配置</p> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useVModel } from '@vueuse/core' | ||||||
|  | import SubConditionGroupConfig from './SubConditionGroupConfig.vue' | ||||||
|  | import { | ||||||
|  |   ConditionGroupContainerFormData, | ||||||
|  |   SubConditionGroupFormData | ||||||
|  | } from '@/api/iot/rule/scene/scene.types' | ||||||
|  | 
 | ||||||
|  | /** 条件组容器配置组件 */ | ||||||
|  | defineOptions({ name: 'ConditionGroupContainerConfig' }) | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   modelValue: ConditionGroupContainerFormData | ||||||
|  |   triggerType: number | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Emits { | ||||||
|  |   (e: 'update:modelValue', value: ConditionGroupContainerFormData): void | ||||||
|  |   (e: 'validate', result: { valid: boolean; message: string }): void | ||||||
|  |   (e: 'remove'): void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const props = defineProps<Props>() | ||||||
|  | const emit = defineEmits<Emits>() | ||||||
|  | 
 | ||||||
|  | const container = useVModel(props, 'modelValue', emit) | ||||||
|  | 
 | ||||||
|  | // 配置常量 | ||||||
|  | const maxSubGroups = 3 // 最多3个子条件组 | ||||||
|  | const maxConditionsPerGroup = 3 // 每组最多3个条件 | ||||||
|  | 
 | ||||||
|  | // 验证状态 | ||||||
|  | const subGroupValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({}) | ||||||
|  | 
 | ||||||
|  | // 事件处理 | ||||||
|  | const addSubGroup = () => { | ||||||
|  |   if (!container.value.subGroups) { | ||||||
|  |     container.value.subGroups = [] | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (container.value.subGroups.length >= maxSubGroups) { | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const newSubGroup: SubConditionGroupFormData = { | ||||||
|  |     conditions: [] | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   container.value.subGroups.push(newSubGroup) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const removeSubGroup = (index: number) => { | ||||||
|  |   if (container.value.subGroups) { | ||||||
|  |     container.value.subGroups.splice(index, 1) | ||||||
|  |     delete subGroupValidations.value[index] | ||||||
|  | 
 | ||||||
|  |     // 重新索引验证结果 | ||||||
|  |     const newValidations: { [key: number]: { valid: boolean; message: string } } = {} | ||||||
|  |     Object.keys(subGroupValidations.value).forEach((key) => { | ||||||
|  |       const numKey = parseInt(key) | ||||||
|  |       if (numKey > index) { | ||||||
|  |         newValidations[numKey - 1] = subGroupValidations.value[numKey] | ||||||
|  |       } else if (numKey < index) { | ||||||
|  |         newValidations[numKey] = subGroupValidations.value[numKey] | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |     subGroupValidations.value = newValidations | ||||||
|  | 
 | ||||||
|  |     updateValidationResult() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const updateSubGroup = (index: number, subGroup: SubConditionGroupFormData) => { | ||||||
|  |   if (container.value.subGroups) { | ||||||
|  |     container.value.subGroups[index] = subGroup | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const removeContainer = () => { | ||||||
|  |   emit('remove') | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleSubGroupValidate = (index: number, result: { valid: boolean; message: string }) => { | ||||||
|  |   subGroupValidations.value[index] = result | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const updateValidationResult = () => { | ||||||
|  |   if (!container.value.subGroups || container.value.subGroups.length === 0) { | ||||||
|  |     emit('validate', { valid: true, message: '条件组容器为空,验证通过' }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const validations = Object.values(subGroupValidations.value) | ||||||
|  |   const allValid = validations.every((v: any) => v.valid) | ||||||
|  | 
 | ||||||
|  |   if (allValid) { | ||||||
|  |     emit('validate', { valid: true, message: '条件组容器配置验证通过' }) | ||||||
|  |   } else { | ||||||
|  |     const errorMessages = validations.filter((v: any) => !v.valid).map((v: any) => v.message) | ||||||
|  |     emit('validate', { valid: false, message: `子条件组配置错误: ${errorMessages.join('; ')}` }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 监听变化 | ||||||
|  | watch( | ||||||
|  |   () => container.value.subGroups, | ||||||
|  |   () => { | ||||||
|  |     updateValidationResult() | ||||||
|  |   }, | ||||||
|  |   { deep: true, immediate: true } | ||||||
|  | ) | ||||||
|  | </script> | ||||||
|  | @ -0,0 +1,287 @@ | ||||||
|  | <!-- 当前时间条件配置组件 --> | ||||||
|  | <template> | ||||||
|  |   <div class="flex flex-col gap-16px"> | ||||||
|  |     <div class="flex items-center gap-8px p-12px px-16px bg-orange-50 rounded-6px border border-orange-200"> | ||||||
|  |       <Icon icon="ep:timer" class="text-orange-500 text-18px" /> | ||||||
|  |       <span class="text-14px font-500 text-orange-700">当前时间条件配置</span> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <el-row :gutter="16"> | ||||||
|  |       <!-- 时间操作符选择 --> | ||||||
|  |       <el-col :span="8"> | ||||||
|  |         <el-form-item label="时间条件" required> | ||||||
|  |           <el-select | ||||||
|  |             :model-value="condition.operator" | ||||||
|  |             @update:model-value="(value) => updateConditionField('operator', value)" | ||||||
|  |             placeholder="请选择时间条件" | ||||||
|  |             class="w-full" | ||||||
|  |           > | ||||||
|  |             <el-option | ||||||
|  |               v-for="option in timeOperatorOptions" | ||||||
|  |               :key="option.value" | ||||||
|  |               :label="option.label" | ||||||
|  |               :value="option.value" | ||||||
|  |             > | ||||||
|  |               <div class="flex items-center justify-between w-full"> | ||||||
|  |                 <div class="flex items-center gap-8px"> | ||||||
|  |                   <Icon :icon="option.icon" :class="option.iconClass" /> | ||||||
|  |                   <span>{{ option.label }}</span> | ||||||
|  |                 </div> | ||||||
|  |                 <el-tag :type="option.tag" size="small">{{ option.category }}</el-tag> | ||||||
|  |               </div> | ||||||
|  |             </el-option> | ||||||
|  |           </el-select> | ||||||
|  |         </el-form-item> | ||||||
|  |       </el-col> | ||||||
|  | 
 | ||||||
|  |       <!-- 时间值输入 --> | ||||||
|  |       <el-col :span="8"> | ||||||
|  |         <el-form-item label="时间值" required> | ||||||
|  |           <el-time-picker | ||||||
|  |             v-if="needsTimeInput" | ||||||
|  |             :model-value="condition.timeValue" | ||||||
|  |             @update:model-value="(value) => updateConditionField('timeValue', value)" | ||||||
|  |             placeholder="请选择时间" | ||||||
|  |             format="HH:mm:ss" | ||||||
|  |             value-format="HH:mm:ss" | ||||||
|  |             class="w-full" | ||||||
|  |           /> | ||||||
|  |           <el-date-picker | ||||||
|  |             v-else-if="needsDateInput" | ||||||
|  |             :model-value="condition.timeValue" | ||||||
|  |             @update:model-value="(value) => updateConditionField('timeValue', value)" | ||||||
|  |             type="datetime" | ||||||
|  |             placeholder="请选择日期时间" | ||||||
|  |             format="YYYY-MM-DD HH:mm:ss" | ||||||
|  |             value-format="YYYY-MM-DD HH:mm:ss" | ||||||
|  |             class="w-full" | ||||||
|  |           /> | ||||||
|  |           <div v-else class="text-[var(--el-text-color-placeholder)] text-14px"> | ||||||
|  |             无需设置时间值 | ||||||
|  |           </div> | ||||||
|  |         </el-form-item> | ||||||
|  |       </el-col> | ||||||
|  | 
 | ||||||
|  |       <!-- 第二个时间值(范围条件) --> | ||||||
|  |       <el-col :span="8" v-if="needsSecondTimeInput"> | ||||||
|  |         <el-form-item label="结束时间" required> | ||||||
|  |           <el-time-picker | ||||||
|  |             v-if="needsTimeInput" | ||||||
|  |             :model-value="condition.timeValue2" | ||||||
|  |             @update:model-value="(value) => updateConditionField('timeValue2', value)" | ||||||
|  |             placeholder="请选择结束时间" | ||||||
|  |             format="HH:mm:ss" | ||||||
|  |             value-format="HH:mm:ss" | ||||||
|  |             class="w-full" | ||||||
|  |           /> | ||||||
|  |           <el-date-picker | ||||||
|  |             v-else | ||||||
|  |             :model-value="condition.timeValue2" | ||||||
|  |             @update:model-value="(value) => updateConditionField('timeValue2', value)" | ||||||
|  |             type="datetime" | ||||||
|  |             placeholder="请选择结束日期时间" | ||||||
|  |             format="YYYY-MM-DD HH:mm:ss" | ||||||
|  |             value-format="YYYY-MM-DD HH:mm:ss" | ||||||
|  |             class="w-full" | ||||||
|  |           /> | ||||||
|  |         </el-form-item> | ||||||
|  |       </el-col> | ||||||
|  |     </el-row> | ||||||
|  | 
 | ||||||
|  |     <!-- 条件预览 --> | ||||||
|  |     <div v-if="conditionPreview" class="p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"> | ||||||
|  |       <div class="flex items-center gap-8px mb-8px"> | ||||||
|  |         <Icon icon="ep:view" class="text-[var(--el-color-info)] text-16px" /> | ||||||
|  |         <span class="text-14px font-500 text-[var(--el-text-color-primary)]">条件预览</span> | ||||||
|  |       </div> | ||||||
|  |       <div class="pl-24px"> | ||||||
|  |         <code class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] p-8px rounded-4px font-mono">{{ conditionPreview }}</code> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <!-- 验证结果 --> | ||||||
|  |     <div v-if="validationMessage" class="mt-8px"> | ||||||
|  |       <el-alert | ||||||
|  |         :title="validationMessage" | ||||||
|  |         :type="isValid ? 'success' : 'error'" | ||||||
|  |         :closable="false" | ||||||
|  |         show-icon | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useVModel } from '@vueuse/core' | ||||||
|  | import { ConditionFormData, IotRuleSceneTriggerTimeOperatorEnum } from '@/api/iot/rule/scene/scene.types' | ||||||
|  | 
 | ||||||
|  | /** 当前时间条件配置组件 */ | ||||||
|  | defineOptions({ name: 'CurrentTimeConditionConfig' }) | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   modelValue: ConditionFormData | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Emits { | ||||||
|  |   (e: 'update:modelValue', value: ConditionFormData): void | ||||||
|  |   (e: 'validate', result: { valid: boolean; message: string }): void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const props = defineProps<Props>() | ||||||
|  | const emit = defineEmits<Emits>() | ||||||
|  | 
 | ||||||
|  | const condition = useVModel(props, 'modelValue', emit) | ||||||
|  | 
 | ||||||
|  | // 时间操作符选项 | ||||||
|  | const timeOperatorOptions = [ | ||||||
|  |   { | ||||||
|  |     value: IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.value, | ||||||
|  |     label: IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.name, | ||||||
|  |     icon: 'ep:arrow-left', | ||||||
|  |     iconClass: 'text-blue-500', | ||||||
|  |     tag: 'primary', | ||||||
|  |     category: '时间点' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.value, | ||||||
|  |     label: IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.name, | ||||||
|  |     icon: 'ep:arrow-right', | ||||||
|  |     iconClass: 'text-green-500', | ||||||
|  |     tag: 'success', | ||||||
|  |     category: '时间点' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value, | ||||||
|  |     label: IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.name, | ||||||
|  |     icon: 'ep:sort', | ||||||
|  |     iconClass: 'text-orange-500', | ||||||
|  |     tag: 'warning', | ||||||
|  |     category: '时间段' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.value, | ||||||
|  |     label: IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.name, | ||||||
|  |     icon: 'ep:position', | ||||||
|  |     iconClass: 'text-purple-500', | ||||||
|  |     tag: 'info', | ||||||
|  |     category: '时间点' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: IotRuleSceneTriggerTimeOperatorEnum.TODAY.value, | ||||||
|  |     label: IotRuleSceneTriggerTimeOperatorEnum.TODAY.name, | ||||||
|  |     icon: 'ep:calendar', | ||||||
|  |     iconClass: 'text-red-500', | ||||||
|  |     tag: 'danger', | ||||||
|  |     category: '日期' | ||||||
|  |   } | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | // 状态 | ||||||
|  | const validationMessage = ref('') | ||||||
|  | const isValid = ref(true) | ||||||
|  | 
 | ||||||
|  | // 计算属性 | ||||||
|  | const needsTimeInput = computed(() => { | ||||||
|  |   const timeOnlyOperators = [ | ||||||
|  |     IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.value, | ||||||
|  |     IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.value, | ||||||
|  |     IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value, | ||||||
|  |     IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.value | ||||||
|  |   ] | ||||||
|  |   return timeOnlyOperators.includes(condition.value.operator) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | const needsDateInput = computed(() => { | ||||||
|  |   return false // 暂时不支持日期输入,只支持时间 | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | const needsSecondTimeInput = computed(() => { | ||||||
|  |   return condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | const conditionPreview = computed(() => { | ||||||
|  |   if (!condition.value.operator) { | ||||||
|  |     return '' | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   const operatorOption = timeOperatorOptions.find(opt => opt.value === condition.value.operator) | ||||||
|  |   const operatorLabel = operatorOption?.label || condition.value.operator | ||||||
|  |    | ||||||
|  |   if (condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) { | ||||||
|  |     return `当前时间 ${operatorLabel}` | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   if (!condition.value.timeValue) { | ||||||
|  |     return `当前时间 ${operatorLabel} [未设置时间]` | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   if (needsSecondTimeInput.value && condition.value.timeValue2) { | ||||||
|  |     return `当前时间 ${operatorLabel} ${condition.value.timeValue} 和 ${condition.value.timeValue2}` | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   return `当前时间 ${operatorLabel} ${condition.value.timeValue}` | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | // 事件处理 | ||||||
|  | const updateConditionField = (field: keyof ConditionFormData, value: any) => { | ||||||
|  |   condition.value[field] = value | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const updateValidationResult = () => { | ||||||
|  |   if (!condition.value.operator) { | ||||||
|  |     isValid.value = false | ||||||
|  |     validationMessage.value = '请选择时间条件' | ||||||
|  |     emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // 今日条件不需要时间值 | ||||||
|  |   if (condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) { | ||||||
|  |     isValid.value = true | ||||||
|  |     validationMessage.value = '当前时间条件配置验证通过' | ||||||
|  |     emit('validate', { valid: true, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (needsTimeInput.value && !condition.value.timeValue) { | ||||||
|  |     isValid.value = false | ||||||
|  |     validationMessage.value = '请设置时间值' | ||||||
|  |     emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (needsSecondTimeInput.value && !condition.value.timeValue2) { | ||||||
|  |     isValid.value = false | ||||||
|  |     validationMessage.value = '请设置结束时间' | ||||||
|  |     emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   isValid.value = true | ||||||
|  |   validationMessage.value = '当前时间条件配置验证通过' | ||||||
|  |   emit('validate', { valid: true, message: validationMessage.value }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 监听变化 | ||||||
|  | watch( | ||||||
|  |   () => [condition.value.operator, condition.value.timeValue, condition.value.timeValue2], | ||||||
|  |   () => { | ||||||
|  |     updateValidationResult() | ||||||
|  |   }, | ||||||
|  |   { immediate: true } | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // 监听操作符变化,清理不相关的时间值 | ||||||
|  | watch( | ||||||
|  |   () => condition.value.operator, | ||||||
|  |   (newOperator) => { | ||||||
|  |     if (newOperator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) { | ||||||
|  |       condition.value.timeValue = undefined | ||||||
|  |       condition.value.timeValue2 = undefined | ||||||
|  |     } else if (!needsSecondTimeInput.value) { | ||||||
|  |       condition.value.timeValue2 = undefined | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | ) | ||||||
|  | </script> | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| <!-- 设备控制配置组件 --> | <!-- 设备控制配置组件 --> | ||||||
| <!-- TODO @puhui999:貌似没生效~~~ --> | <!-- TODO @puhui999:貌似没生效~~~ --> | ||||||
| <template> | <template> | ||||||
|   <div class="device-control-config"> |   <div class="flex flex-col gap-16px"> | ||||||
|     <!-- 产品和设备选择 --> |     <!-- 产品和设备选择 --> | ||||||
|     <ProductDeviceSelector |     <ProductDeviceSelector | ||||||
|       v-model:product-id="action.productId" |       v-model:product-id="action.productId" | ||||||
|  | @ -10,7 +10,7 @@ | ||||||
|     /> |     /> | ||||||
| 
 | 
 | ||||||
|     <!-- 控制参数配置 --> |     <!-- 控制参数配置 --> | ||||||
|     <div v-if="action.productId && action.deviceId" class="control-params"> |     <div v-if="action.productId && action.deviceId" class="space-y-16px"> | ||||||
|       <el-form-item label="控制参数" required> |       <el-form-item label="控制参数" required> | ||||||
|         <el-input |         <el-input | ||||||
|           v-model="paramsJson" |           v-model="paramsJson" | ||||||
|  | @ -22,14 +22,14 @@ | ||||||
|       </el-form-item> |       </el-form-item> | ||||||
| 
 | 
 | ||||||
|       <!-- 参数示例 --> |       <!-- 参数示例 --> | ||||||
|       <div class="params-example"> |       <div class="mt-12px"> | ||||||
|         <el-alert title="参数格式示例" type="info" :closable="false" show-icon> |         <el-alert title="参数格式示例" type="info" :closable="false" show-icon> | ||||||
|           <template #default> |           <template #default> | ||||||
|             <div class="example-content"> |             <div class="space-y-8px"> | ||||||
|               <p>属性设置示例:</p> |               <p class="m-0 text-14px text-[var(--el-text-color-primary)]">属性设置示例:</p> | ||||||
|               <pre><code>{ "temperature": 25, "power": true }</code></pre> |               <pre class="m-0 p-8px bg-[var(--el-fill-color-light)] rounded-4px text-12px text-[var(--el-text-color-regular)] overflow-x-auto"><code>{ "temperature": 25, "power": true }</code></pre> | ||||||
|               <p>服务调用示例:</p> |               <p class="m-0 text-14px text-[var(--el-text-color-primary)]">服务调用示例:</p> | ||||||
|               <pre><code>{ "method": "restart", "params": { "delay": 5 } }</code></pre> |               <pre class="m-0 p-8px bg-[var(--el-fill-color-light)] rounded-4px text-12px text-[var(--el-text-color-regular)] overflow-x-auto"><code>{ "method": "restart", "params": { "delay": 5 } }</code></pre> | ||||||
|             </div> |             </div> | ||||||
|           </template> |           </template> | ||||||
|         </el-alert> |         </el-alert> | ||||||
|  | @ -37,7 +37,7 @@ | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <!-- 验证结果 --> |     <!-- 验证结果 --> | ||||||
|     <div v-if="validationMessage" class="validation-result"> |     <div v-if="validationMessage" class="mt-16px"> | ||||||
|       <el-alert |       <el-alert | ||||||
|         :title="validationMessage" |         :title="validationMessage" | ||||||
|         :type="isValid ? 'success' : 'error'" |         :type="isValid ? 'success' : 'error'" | ||||||
|  | @ -140,34 +140,8 @@ watch( | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| .device-control-config { | :deep(.example-content code) { | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .control-params { |  | ||||||
|   margin-top: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .params-example { |  | ||||||
|   margin-top: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .example-content pre { |  | ||||||
|   margin: 4px 0; |  | ||||||
|   padding: 8px; |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-radius: 4px; |  | ||||||
|   font-size: 12px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .example-content code { |  | ||||||
|   font-family: 'Courier New', monospace; |   font-family: 'Courier New', monospace; | ||||||
|   color: var(--el-color-primary); |   color: var(--el-color-primary); | ||||||
| } | } | ||||||
| 
 |  | ||||||
| .validation-result { |  | ||||||
|   margin-top: 8px; |  | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|  | @ -0,0 +1,251 @@ | ||||||
|  | <!-- 设备状态条件配置组件 --> | ||||||
|  | <template> | ||||||
|  |   <div class="flex flex-col gap-16px"> | ||||||
|  |     <!-- 产品设备选择 --> | ||||||
|  |     <el-row :gutter="16"> | ||||||
|  |       <el-col :span="12"> | ||||||
|  |         <el-form-item label="产品" required> | ||||||
|  |           <ProductSelector | ||||||
|  |             :model-value="condition.productId" | ||||||
|  |             @update:model-value="(value) => updateConditionField('productId', value)" | ||||||
|  |             @change="handleProductChange" | ||||||
|  |           /> | ||||||
|  |         </el-form-item> | ||||||
|  |       </el-col> | ||||||
|  |       <el-col :span="12"> | ||||||
|  |         <el-form-item label="设备" required> | ||||||
|  |           <DeviceSelector | ||||||
|  |             :model-value="condition.deviceId" | ||||||
|  |             @update:model-value="(value) => updateConditionField('deviceId', value)" | ||||||
|  |             :product-id="condition.productId" | ||||||
|  |             @change="handleDeviceChange" | ||||||
|  |           /> | ||||||
|  |         </el-form-item> | ||||||
|  |       </el-col> | ||||||
|  |     </el-row> | ||||||
|  | 
 | ||||||
|  |     <!-- 状态和操作符选择 --> | ||||||
|  |     <el-row :gutter="16"> | ||||||
|  |       <!-- 状态选择 --> | ||||||
|  |       <el-col :span="12"> | ||||||
|  |         <el-form-item label="设备状态" required> | ||||||
|  |           <el-select | ||||||
|  |             :model-value="condition.param" | ||||||
|  |             @update:model-value="(value) => updateConditionField('param', value)" | ||||||
|  |             placeholder="请选择设备状态" | ||||||
|  |             class="w-full" | ||||||
|  |           > | ||||||
|  |             <el-option | ||||||
|  |               v-for="option in deviceStatusOptions" | ||||||
|  |               :key="option.value" | ||||||
|  |               :label="option.label" | ||||||
|  |               :value="option.value" | ||||||
|  |             > | ||||||
|  |               <div class="flex items-center gap-8px"> | ||||||
|  |                 <Icon :icon="option.icon" :class="option.iconClass" /> | ||||||
|  |                 <span>{{ option.label }}</span> | ||||||
|  |                 <el-tag :type="option.tag" size="small">{{ option.description }}</el-tag> | ||||||
|  |               </div> | ||||||
|  |             </el-option> | ||||||
|  |           </el-select> | ||||||
|  |         </el-form-item> | ||||||
|  |       </el-col> | ||||||
|  | 
 | ||||||
|  |       <!-- 操作符选择 --> | ||||||
|  |       <el-col :span="12"> | ||||||
|  |         <el-form-item label="操作符" required> | ||||||
|  |           <el-select | ||||||
|  |             :model-value="condition.operator" | ||||||
|  |             @update:model-value="(value) => updateConditionField('operator', value)" | ||||||
|  |             placeholder="请选择操作符" | ||||||
|  |             class="w-full" | ||||||
|  |           > | ||||||
|  |             <el-option | ||||||
|  |               v-for="option in statusOperatorOptions" | ||||||
|  |               :key="option.value" | ||||||
|  |               :label="option.label" | ||||||
|  |               :value="option.value" | ||||||
|  |             > | ||||||
|  |               <div class="flex items-center justify-between w-full"> | ||||||
|  |                 <span>{{ option.label }}</span> | ||||||
|  |                 <span class="text-12px text-[var(--el-text-color-secondary)]">{{ | ||||||
|  |                   option.description | ||||||
|  |                 }}</span> | ||||||
|  |               </div> | ||||||
|  |             </el-option> | ||||||
|  |           </el-select> | ||||||
|  |         </el-form-item> | ||||||
|  |       </el-col> | ||||||
|  |     </el-row> | ||||||
|  | 
 | ||||||
|  |     <!-- 条件预览 --> | ||||||
|  |     <div | ||||||
|  |       v-if="conditionPreview" | ||||||
|  |       class="p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]" | ||||||
|  |     > | ||||||
|  |       <div class="flex items-center gap-8px mb-8px"> | ||||||
|  |         <Icon icon="ep:view" class="text-[var(--el-color-info)] text-16px" /> | ||||||
|  |         <span class="text-14px font-500 text-[var(--el-text-color-primary)]">条件预览</span> | ||||||
|  |       </div> | ||||||
|  |       <div class="pl-24px"> | ||||||
|  |         <code | ||||||
|  |           class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] p-8px rounded-4px font-mono" | ||||||
|  |           >{{ conditionPreview }}</code | ||||||
|  |         > | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <!-- 验证结果 --> | ||||||
|  |     <div v-if="validationMessage" class="mt-8px"> | ||||||
|  |       <el-alert | ||||||
|  |         :title="validationMessage" | ||||||
|  |         :type="isValid ? 'success' : 'error'" | ||||||
|  |         :closable="false" | ||||||
|  |         show-icon | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useVModel } from '@vueuse/core' | ||||||
|  | import ProductSelector from '../selectors/ProductSelector.vue' | ||||||
|  | import DeviceSelector from '../selectors/DeviceSelector.vue' | ||||||
|  | import { ConditionFormData } from '@/api/iot/rule/scene/scene.types' | ||||||
|  | 
 | ||||||
|  | /** 设备状态条件配置组件 */ | ||||||
|  | defineOptions({ name: 'DeviceStatusConditionConfig' }) | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   modelValue: ConditionFormData | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Emits { | ||||||
|  |   (e: 'update:modelValue', value: ConditionFormData): void | ||||||
|  |   (e: 'validate', result: { valid: boolean; message: string }): void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const props = defineProps<Props>() | ||||||
|  | const emit = defineEmits<Emits>() | ||||||
|  | 
 | ||||||
|  | const condition = useVModel(props, 'modelValue', emit) | ||||||
|  | 
 | ||||||
|  | // 设备状态选项 | ||||||
|  | const deviceStatusOptions = [ | ||||||
|  |   { | ||||||
|  |     value: 'online', | ||||||
|  |     label: '在线', | ||||||
|  |     description: '设备已连接', | ||||||
|  |     icon: 'ep:circle-check', | ||||||
|  |     iconClass: 'text-green-500', | ||||||
|  |     tag: 'success' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: 'offline', | ||||||
|  |     label: '离线', | ||||||
|  |     description: '设备已断开', | ||||||
|  |     icon: 'ep:circle-close', | ||||||
|  |     iconClass: 'text-red-500', | ||||||
|  |     tag: 'danger' | ||||||
|  |   } | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | // 状态操作符选项 | ||||||
|  | const statusOperatorOptions = [ | ||||||
|  |   { | ||||||
|  |     value: '=', | ||||||
|  |     label: '等于', | ||||||
|  |     description: '状态完全匹配时触发' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: '!=', | ||||||
|  |     label: '不等于', | ||||||
|  |     description: '状态不匹配时触发' | ||||||
|  |   } | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | // 状态 | ||||||
|  | const validationMessage = ref('') | ||||||
|  | const isValid = ref(true) | ||||||
|  | 
 | ||||||
|  | // 计算属性 | ||||||
|  | const conditionPreview = computed(() => { | ||||||
|  |   if (!condition.value.param || !condition.value.operator) { | ||||||
|  |     return '' | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const statusLabel = | ||||||
|  |     deviceStatusOptions.find((opt) => opt.value === condition.value.param)?.label || | ||||||
|  |     condition.value.param | ||||||
|  |   const operatorLabel = | ||||||
|  |     statusOperatorOptions.find((opt) => opt.value === condition.value.operator)?.label || | ||||||
|  |     condition.value.operator | ||||||
|  | 
 | ||||||
|  |   return `设备状态 ${operatorLabel} ${statusLabel}` | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | // 事件处理 | ||||||
|  | const updateConditionField = (field: keyof ConditionFormData, value: any) => { | ||||||
|  |   condition.value[field] = value | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleProductChange = (productId: number) => { | ||||||
|  |   // 产品变化时清空设备 | ||||||
|  |   condition.value.deviceId = undefined | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleDeviceChange = (deviceId: number) => { | ||||||
|  |   // 设备变化时可以进行其他处理 | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const updateValidationResult = () => { | ||||||
|  |   if (!condition.value.productId) { | ||||||
|  |     isValid.value = false | ||||||
|  |     validationMessage.value = '请选择产品' | ||||||
|  |     emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (!condition.value.deviceId) { | ||||||
|  |     isValid.value = false | ||||||
|  |     validationMessage.value = '请选择设备' | ||||||
|  |     emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (!condition.value.param) { | ||||||
|  |     isValid.value = false | ||||||
|  |     validationMessage.value = '请选择设备状态' | ||||||
|  |     emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (!condition.value.operator) { | ||||||
|  |     isValid.value = false | ||||||
|  |     validationMessage.value = '请选择操作符' | ||||||
|  |     emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   isValid.value = true | ||||||
|  |   validationMessage.value = '设备状态条件配置验证通过' | ||||||
|  |   emit('validate', { valid: true, message: validationMessage.value }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 监听变化 | ||||||
|  | watch( | ||||||
|  |   () => [ | ||||||
|  |     condition.value.productId, | ||||||
|  |     condition.value.deviceId, | ||||||
|  |     condition.value.param, | ||||||
|  |     condition.value.operator | ||||||
|  |   ], | ||||||
|  |   () => { | ||||||
|  |     updateValidationResult() | ||||||
|  |   }, | ||||||
|  |   { immediate: true } | ||||||
|  | ) | ||||||
|  | </script> | ||||||
|  | @ -0,0 +1,197 @@ | ||||||
|  | <!-- 设备触发配置组件 - 优化版本 --> | ||||||
|  | <template> | ||||||
|  |   <div class="flex flex-col gap-16px"> | ||||||
|  |     <!-- 主条件配置 - 默认直接展示 --> | ||||||
|  |     <div class="space-y-16px"> | ||||||
|  |       <MainConditionConfig | ||||||
|  |         v-model="trigger.mainCondition" | ||||||
|  |         :trigger-type="trigger.type" | ||||||
|  |         @validate="handleMainConditionValidate" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <!-- 条件组配置 --> | ||||||
|  |     <div v-if="trigger.mainCondition" class="space-y-16px"> | ||||||
|  |       <div class="flex items-center justify-between"> | ||||||
|  |         <div class="flex items-center gap-8px"> | ||||||
|  |           <span class="text-14px font-500 text-[var(--el-text-color-primary)]">附加条件组</span> | ||||||
|  |           <el-tag size="small" type="success">与主条件为且关系</el-tag> | ||||||
|  |           <el-tag size="small" type="info"> | ||||||
|  |             {{ trigger.conditionGroup?.subGroups?.length || 0 }}个子条件组 | ||||||
|  |           </el-tag> | ||||||
|  |         </div> | ||||||
|  |         <el-button | ||||||
|  |           type="primary" | ||||||
|  |           size="small" | ||||||
|  |           @click="addConditionGroup" | ||||||
|  |           v-if="!trigger.conditionGroup" | ||||||
|  |         > | ||||||
|  |           <Icon icon="ep:plus" /> | ||||||
|  |           添加条件组 | ||||||
|  |         </el-button> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <!-- 条件组配置 --> | ||||||
|  |       <ConditionGroupContainerConfig | ||||||
|  |         v-if="trigger.conditionGroup" | ||||||
|  |         v-model="trigger.conditionGroup" | ||||||
|  |         :trigger-type="trigger.type" | ||||||
|  |         @validate="handleConditionGroupValidate" | ||||||
|  |         @remove="removeConditionGroup" | ||||||
|  |       /> | ||||||
|  | 
 | ||||||
|  |       <!-- 空状态 --> | ||||||
|  |       <div v-else class="py-40px text-center"> | ||||||
|  |         <el-empty description="暂无触发条件"> | ||||||
|  |           <template #description> | ||||||
|  |             <div class="space-y-8px"> | ||||||
|  |               <p class="text-[var(--el-text-color-secondary)]">暂无触发条件</p> | ||||||
|  |               <p class="text-12px text-[var(--el-text-color-placeholder)]"> | ||||||
|  |                 请使用上方的"添加条件组"按钮来设置触发规则 | ||||||
|  |               </p> | ||||||
|  |             </div> | ||||||
|  |           </template> | ||||||
|  |         </el-empty> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useVModel } from '@vueuse/core' | ||||||
|  | 
 | ||||||
|  | import MainConditionConfig from './MainConditionConfig.vue' | ||||||
|  | import ConditionGroupContainerConfig from './ConditionGroupContainerConfig.vue' | ||||||
|  | import { | ||||||
|  |   TriggerFormData, | ||||||
|  |   IotRuleSceneTriggerTypeEnum as TriggerTypeEnum | ||||||
|  | } from '@/api/iot/rule/scene/scene.types' | ||||||
|  | 
 | ||||||
|  | /** 设备触发配置组件 */ | ||||||
|  | defineOptions({ name: 'DeviceTriggerConfig' }) | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   modelValue: TriggerFormData | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Emits { | ||||||
|  |   (e: 'update:modelValue', value: TriggerFormData): void | ||||||
|  |   (e: 'validate', result: { valid: boolean; message: string }): void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const props = defineProps<Props>() | ||||||
|  | const emit = defineEmits<Emits>() | ||||||
|  | 
 | ||||||
|  | const trigger = useVModel(props, 'modelValue', emit) | ||||||
|  | 
 | ||||||
|  | // 验证状态 | ||||||
|  | const mainConditionValidation = ref<{ valid: boolean; message: string }>({ | ||||||
|  |   valid: true, | ||||||
|  |   message: '' | ||||||
|  | }) | ||||||
|  | const validationMessage = ref('') | ||||||
|  | const isValid = ref(true) | ||||||
|  | 
 | ||||||
|  | // 计算属性 | ||||||
|  | 
 | ||||||
|  | // 初始化主条件 | ||||||
|  | const initMainCondition = () => { | ||||||
|  |   if (!trigger.value.mainCondition) { | ||||||
|  |     trigger.value.mainCondition = { | ||||||
|  |       type: trigger.value.type, // 使用触发事件类型作为条件类型 | ||||||
|  |       productId: undefined, | ||||||
|  |       deviceId: undefined, | ||||||
|  |       identifier: '', | ||||||
|  |       operator: '=', | ||||||
|  |       param: '' | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 监听触发器类型变化,自动初始化主条件 | ||||||
|  | watch( | ||||||
|  |   () => trigger.value.type, | ||||||
|  |   () => { | ||||||
|  |     initMainCondition() | ||||||
|  |   }, | ||||||
|  |   { immediate: true } | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // 新的事件处理函数 | ||||||
|  | const handleMainConditionValidate = (result: { valid: boolean; message: string }) => { | ||||||
|  |   mainConditionValidation.value = result | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const addConditionGroup = () => { | ||||||
|  |   if (!trigger.value.conditionGroup) { | ||||||
|  |     trigger.value.conditionGroup = { | ||||||
|  |       subGroups: [] | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 事件处理 | ||||||
|  | 
 | ||||||
|  | const handleConditionGroupValidate = () => { | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const removeConditionGroup = () => { | ||||||
|  |   trigger.value.conditionGroup = undefined | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const updateValidationResult = () => { | ||||||
|  |   // 主条件验证 | ||||||
|  |   if (!mainConditionValidation.value.valid) { | ||||||
|  |     isValid.value = false | ||||||
|  |     validationMessage.value = mainConditionValidation.value.message | ||||||
|  |     emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // 设备状态变更不需要条件验证 | ||||||
|  |   if (trigger.value.type === TriggerTypeEnum.DEVICE_STATE_UPDATE) { | ||||||
|  |     isValid.value = true | ||||||
|  |     validationMessage.value = '设备触发配置验证通过' | ||||||
|  |     emit('validate', { valid: true, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // 主条件验证 | ||||||
|  |   if (!trigger.value.mainCondition) { | ||||||
|  |     isValid.value = false | ||||||
|  |     validationMessage.value = '请配置主条件' | ||||||
|  |     emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // 主条件详细验证 | ||||||
|  |   if (!mainConditionValidation.value.valid) { | ||||||
|  |     isValid.value = false | ||||||
|  |     validationMessage.value = `主条件配置错误: ${mainConditionValidation.value.message}` | ||||||
|  |     emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   isValid.value = true | ||||||
|  |   validationMessage.value = '设备触发配置验证通过' | ||||||
|  |   emit('validate', { valid: isValid.value, message: validationMessage.value }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 监听触发器类型变化 | ||||||
|  | watch( | ||||||
|  |   () => trigger.value.type, | ||||||
|  |   () => { | ||||||
|  |     updateValidationResult() | ||||||
|  |   } | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // 监听产品设备变化 | ||||||
|  | watch( | ||||||
|  |   () => [trigger.value.productId, trigger.value.deviceId], | ||||||
|  |   () => { | ||||||
|  |     updateValidationResult() | ||||||
|  |   } | ||||||
|  | ) | ||||||
|  | </script> | ||||||
|  | @ -0,0 +1,92 @@ | ||||||
|  | <!-- 主条件配置组件 --> | ||||||
|  | <template> | ||||||
|  |   <div class="flex flex-col gap-16px"> | ||||||
|  |     <!-- 条件配置提示 --> | ||||||
|  |     <div | ||||||
|  |       v-if="!modelValue" | ||||||
|  |       class="p-16px border-2 border-dashed border-[var(--el-border-color)] rounded-8px text-center" | ||||||
|  |     > | ||||||
|  |       <div class="flex flex-col items-center gap-12px"> | ||||||
|  |         <Icon icon="ep:plus" class="text-32px text-[var(--el-text-color-placeholder)]" /> | ||||||
|  |         <div class="text-[var(--el-text-color-secondary)]"> | ||||||
|  |           <p class="text-14px font-500 mb-4px">请配置主条件</p> | ||||||
|  |           <p class="text-12px">主条件是触发器的核心条件,必须满足才能触发场景</p> | ||||||
|  |         </div> | ||||||
|  |         <el-button type="primary" @click="addMainCondition"> | ||||||
|  |           <Icon icon="ep:plus" /> | ||||||
|  |           添加主条件 | ||||||
|  |         </el-button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <!-- 主条件配置 --> | ||||||
|  |     <div v-else class="space-y-16px"> | ||||||
|  |       <div class="flex items-center justify-between"> | ||||||
|  |         <div class="flex items-center gap-8px"> | ||||||
|  |           <span class="text-14px font-500 text-[var(--el-text-color-primary)]">主条件</span> | ||||||
|  |           <el-tag size="small" type="primary">必须满足</el-tag> | ||||||
|  |         </div> | ||||||
|  |         <el-button type="danger" size="small" text @click="removeMainCondition"> | ||||||
|  |           <Icon icon="ep:delete" /> | ||||||
|  |           删除 | ||||||
|  |         </el-button> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <MainConditionInnerConfig | ||||||
|  |         :model-value="modelValue" | ||||||
|  |         @update:model-value="updateCondition" | ||||||
|  |         :trigger-type="triggerType" | ||||||
|  |         @validate="handleValidate" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import MainConditionInnerConfig from './MainConditionInnerConfig.vue' | ||||||
|  | import { | ||||||
|  |   ConditionFormData, | ||||||
|  |   IotRuleSceneTriggerConditionTypeEnum | ||||||
|  | } from '@/api/iot/rule/scene/scene.types' | ||||||
|  | 
 | ||||||
|  | /** 主条件配置组件 */ | ||||||
|  | defineOptions({ name: 'MainConditionConfig' }) | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   modelValue?: ConditionFormData | ||||||
|  |   triggerType: number | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Emits { | ||||||
|  |   (e: 'update:modelValue', value?: ConditionFormData): void | ||||||
|  |   (e: 'validate', result: { valid: boolean; message: string }): void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | defineProps<Props>() | ||||||
|  | const emit = defineEmits<Emits>() | ||||||
|  | 
 | ||||||
|  | // 事件处理 | ||||||
|  | const addMainCondition = () => { | ||||||
|  |   const newCondition: ConditionFormData = { | ||||||
|  |     type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性 | ||||||
|  |     productId: undefined, | ||||||
|  |     deviceId: undefined, | ||||||
|  |     identifier: '', | ||||||
|  |     operator: '=', | ||||||
|  |     param: '' | ||||||
|  |   } | ||||||
|  |   emit('update:modelValue', newCondition) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const removeMainCondition = () => { | ||||||
|  |   emit('update:modelValue', undefined) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const updateCondition = (condition: ConditionFormData) => { | ||||||
|  |   emit('update:modelValue', condition) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleValidate = (result: { valid: boolean; message: string }) => { | ||||||
|  |   emit('validate', result) | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | @ -0,0 +1,282 @@ | ||||||
|  | <!-- 主条件内部配置组件 - 不显示条件类型选择 --> | ||||||
|  | <template> | ||||||
|  |   <div class="space-y-16px"> | ||||||
|  |     <!-- 触发事件类型显示 --> | ||||||
|  |     <div class="flex items-center gap-8px mb-16px"> | ||||||
|  |       <span class="text-14px text-[var(--el-text-color-regular)]">触发事件类型:</span> | ||||||
|  |       <el-tag size="small" type="primary">{{ getTriggerTypeText(triggerType) }}</el-tag> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <!-- 设备属性条件配置 --> | ||||||
|  |     <div v-if="isDevicePropertyTrigger" class="space-y-16px"> | ||||||
|  |       <!-- 产品设备选择 --> | ||||||
|  |       <el-row :gutter="16"> | ||||||
|  |         <el-col :span="12"> | ||||||
|  |           <el-form-item label="产品" required> | ||||||
|  |             <ProductSelector | ||||||
|  |               :model-value="condition.productId" | ||||||
|  |               @update:model-value="(value) => updateConditionField('productId', value)" | ||||||
|  |               @change="handleProductChange" | ||||||
|  |             /> | ||||||
|  |           </el-form-item> | ||||||
|  |         </el-col> | ||||||
|  |         <el-col :span="12"> | ||||||
|  |           <el-form-item label="设备" required> | ||||||
|  |             <DeviceSelector | ||||||
|  |               :model-value="condition.deviceId" | ||||||
|  |               @update:model-value="(value) => updateConditionField('deviceId', value)" | ||||||
|  |               :product-id="condition.productId" | ||||||
|  |               @change="handleDeviceChange" | ||||||
|  |             /> | ||||||
|  |           </el-form-item> | ||||||
|  |         </el-col> | ||||||
|  |       </el-row> | ||||||
|  | 
 | ||||||
|  |       <!-- 属性配置 --> | ||||||
|  |       <el-row :gutter="16"> | ||||||
|  |         <!-- 属性/事件/服务选择 --> | ||||||
|  |         <el-col :span="6"> | ||||||
|  |           <el-form-item label="监控项" required> | ||||||
|  |             <PropertySelector | ||||||
|  |               :model-value="condition.identifier" | ||||||
|  |               @update:model-value="(value) => updateConditionField('identifier', value)" | ||||||
|  |               :trigger-type="triggerType" | ||||||
|  |               :product-id="condition.productId" | ||||||
|  |               :device-id="condition.deviceId" | ||||||
|  |               @change="handlePropertyChange" | ||||||
|  |             /> | ||||||
|  |           </el-form-item> | ||||||
|  |         </el-col> | ||||||
|  | 
 | ||||||
|  |         <!-- 操作符选择 --> | ||||||
|  |         <el-col :span="6"> | ||||||
|  |           <el-form-item label="操作符" required> | ||||||
|  |             <OperatorSelector | ||||||
|  |               :model-value="condition.operator" | ||||||
|  |               @update:model-value="(value) => updateConditionField('operator', value)" | ||||||
|  |               :property-type="propertyType" | ||||||
|  |               @change="handleOperatorChange" | ||||||
|  |             /> | ||||||
|  |           </el-form-item> | ||||||
|  |         </el-col> | ||||||
|  | 
 | ||||||
|  |         <!-- 值输入 --> | ||||||
|  |         <el-col :span="12"> | ||||||
|  |           <el-form-item label="比较值" required> | ||||||
|  |             <ValueInput | ||||||
|  |               :model-value="condition.param" | ||||||
|  |               @update:model-value="(value) => updateConditionField('param', value)" | ||||||
|  |               :property-type="propertyType" | ||||||
|  |               :operator="condition.operator" | ||||||
|  |               :property-config="propertyConfig" | ||||||
|  |               @validate="handleValueValidate" | ||||||
|  |             /> | ||||||
|  |           </el-form-item> | ||||||
|  |         </el-col> | ||||||
|  |       </el-row> | ||||||
|  | 
 | ||||||
|  |       <!-- 条件预览 --> | ||||||
|  |       <div v-if="conditionPreview" class="mt-12px"> | ||||||
|  |         <div class="text-12px text-[var(--el-text-color-secondary)]"> | ||||||
|  |           预览:{{ conditionPreview }} | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <!-- 设备状态条件配置 --> | ||||||
|  |     <div v-else-if="isDeviceStatusTrigger" class="space-y-16px"> | ||||||
|  |       <DeviceStatusConditionConfig | ||||||
|  |         :model-value="condition" | ||||||
|  |         @update:model-value="updateCondition" | ||||||
|  |         @validate="handleValidate" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <!-- 其他触发类型的提示 --> | ||||||
|  |     <div v-else class="text-center py-20px"> | ||||||
|  |       <p class="text-14px text-[var(--el-text-color-secondary)] mb-4px"> | ||||||
|  |         当前触发事件类型:{{ getTriggerTypeText(triggerType) }} | ||||||
|  |       </p> | ||||||
|  |       <p class="text-12px text-[var(--el-text-color-placeholder)]"> | ||||||
|  |         此触发类型暂不需要配置额外条件 | ||||||
|  |       </p> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import ProductSelector from '../selectors/ProductSelector.vue' | ||||||
|  | import DeviceSelector from '../selectors/DeviceSelector.vue' | ||||||
|  | import PropertySelector from '../selectors/PropertySelector.vue' | ||||||
|  | import OperatorSelector from '../selectors/OperatorSelector.vue' | ||||||
|  | import ValueInput from '../inputs/ValueInput.vue' | ||||||
|  | import DeviceStatusConditionConfig from './DeviceStatusConditionConfig.vue' | ||||||
|  | import { ConditionFormData } from '@/api/iot/rule/scene/scene.types' | ||||||
|  | import { IotRuleSceneTriggerTypeEnum } from '@/api/iot/rule/scene/scene.types' | ||||||
|  | import { useVModel } from '@vueuse/core' | ||||||
|  | 
 | ||||||
|  | /** 主条件内部配置组件 */ | ||||||
|  | defineOptions({ name: 'MainConditionInnerConfig' }) | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   modelValue: ConditionFormData | ||||||
|  |   triggerType: number | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Emits { | ||||||
|  |   (e: 'update:modelValue', value: ConditionFormData): void | ||||||
|  |   (e: 'validate', result: { valid: boolean; message: string }): void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const props = defineProps<Props>() | ||||||
|  | const emit = defineEmits<Emits>() | ||||||
|  | 
 | ||||||
|  | // 响应式数据 | ||||||
|  | const condition = useVModel(props, 'modelValue', emit) | ||||||
|  | const isValid = ref(true) | ||||||
|  | const validationMessage = ref('') | ||||||
|  | const propertyType = ref('') | ||||||
|  | const propertyConfig = ref<any>(null) | ||||||
|  | 
 | ||||||
|  | // 计算属性 | ||||||
|  | const isDevicePropertyTrigger = computed(() => { | ||||||
|  |   return ( | ||||||
|  |     props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST || | ||||||
|  |     props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST || | ||||||
|  |     props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE | ||||||
|  |   ) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | const isDeviceStatusTrigger = computed(() => { | ||||||
|  |   return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | const conditionPreview = computed(() => { | ||||||
|  |   if (!condition.value.productId || !condition.value.deviceId || !condition.value.identifier) { | ||||||
|  |     return '' | ||||||
|  |   } | ||||||
|  |   return `设备[${condition.value.deviceId}]的${condition.value.identifier} ${condition.value.operator} ${condition.value.param}` | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | // 获取触发类型文本 | ||||||
|  | const getTriggerTypeText = (type: number) => { | ||||||
|  |   switch (type) { | ||||||
|  |     case IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST: | ||||||
|  |       return '设备属性上报' | ||||||
|  |     case IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST: | ||||||
|  |       return '设备事件上报' | ||||||
|  |     case IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE: | ||||||
|  |       return '设备服务调用' | ||||||
|  |     case IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE: | ||||||
|  |       return '设备状态变化' | ||||||
|  |     default: | ||||||
|  |       return '未知类型' | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 事件处理 | ||||||
|  | const updateConditionField = (field: keyof ConditionFormData, value: any) => { | ||||||
|  |   condition.value[field] = value | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const updateCondition = (value: ConditionFormData) => { | ||||||
|  |   emit('update:modelValue', value) | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleProductChange = () => { | ||||||
|  |   // 产品变化时清空设备和属性 | ||||||
|  |   condition.value.deviceId = undefined | ||||||
|  |   condition.value.identifier = '' | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleDeviceChange = () => { | ||||||
|  |   // 设备变化时清空属性 | ||||||
|  |   condition.value.identifier = '' | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handlePropertyChange = (propertyInfo: any) => { | ||||||
|  |   if (propertyInfo) { | ||||||
|  |     propertyType.value = propertyInfo.type | ||||||
|  |     propertyConfig.value = propertyInfo.config | ||||||
|  |   } | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleOperatorChange = () => { | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleValueValidate = (result: { valid: boolean; message: string }) => { | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleValidate = (result: { valid: boolean; message: string }) => { | ||||||
|  |   isValid.value = result.valid | ||||||
|  |   validationMessage.value = result.message | ||||||
|  |   emit('validate', result) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 验证逻辑 | ||||||
|  | const updateValidationResult = () => { | ||||||
|  |   if (isDevicePropertyTrigger.value) { | ||||||
|  |     // 设备属性触发验证 | ||||||
|  |     if (!condition.value.productId) { | ||||||
|  |       isValid.value = false | ||||||
|  |       validationMessage.value = '请选择产品' | ||||||
|  |       emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!condition.value.deviceId) { | ||||||
|  |       isValid.value = false | ||||||
|  |       validationMessage.value = '请选择设备' | ||||||
|  |       emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!condition.value.identifier) { | ||||||
|  |       isValid.value = false | ||||||
|  |       validationMessage.value = '请选择监控项' | ||||||
|  |       emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!condition.value.operator) { | ||||||
|  |       isValid.value = false | ||||||
|  |       validationMessage.value = '请选择操作符' | ||||||
|  |       emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!condition.value.param) { | ||||||
|  |       isValid.value = false | ||||||
|  |       validationMessage.value = '请输入比较值' | ||||||
|  |       emit('validate', { valid: false, message: validationMessage.value }) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   isValid.value = true | ||||||
|  |   validationMessage.value = '主条件配置验证通过' | ||||||
|  |   emit('validate', { valid: true, message: validationMessage.value }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 监听变化 | ||||||
|  | watch( | ||||||
|  |   () => [ | ||||||
|  |     condition.value.productId, | ||||||
|  |     condition.value.deviceId, | ||||||
|  |     condition.value.identifier, | ||||||
|  |     condition.value.operator, | ||||||
|  |     condition.value.param | ||||||
|  |   ], | ||||||
|  |   () => { | ||||||
|  |     updateValidationResult() | ||||||
|  |   }, | ||||||
|  |   { immediate: true } | ||||||
|  | ) | ||||||
|  | </script> | ||||||
|  | @ -0,0 +1,220 @@ | ||||||
|  | <!-- 子条件组配置组件 --> | ||||||
|  | <template> | ||||||
|  |   <div class="p-16px"> | ||||||
|  |     <!-- 空状态 --> | ||||||
|  |     <div | ||||||
|  |       v-if="!subGroup.conditions || subGroup.conditions.length === 0" | ||||||
|  |       class="text-center py-24px" | ||||||
|  |     > | ||||||
|  |       <div class="flex flex-col items-center gap-12px"> | ||||||
|  |         <Icon icon="ep:plus" class="text-32px text-[var(--el-text-color-placeholder)]" /> | ||||||
|  |         <div class="text-[var(--el-text-color-secondary)]"> | ||||||
|  |           <p class="text-14px font-500 mb-4px">暂无条件</p> | ||||||
|  |           <p class="text-12px">点击下方按钮添加第一个条件</p> | ||||||
|  |         </div> | ||||||
|  |         <el-button type="primary" @click="addCondition"> | ||||||
|  |           <Icon icon="ep:plus" /> | ||||||
|  |           添加条件 | ||||||
|  |         </el-button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <!-- 条件列表 --> | ||||||
|  |     <div v-else class="space-y-16px"> | ||||||
|  |       <div | ||||||
|  |         v-for="(condition, conditionIndex) in subGroup.conditions" | ||||||
|  |         :key="`condition-${conditionIndex}`" | ||||||
|  |         class="relative" | ||||||
|  |       > | ||||||
|  |         <!-- 条件配置 --> | ||||||
|  |         <div | ||||||
|  |           class="border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)] shadow-sm" | ||||||
|  |         > | ||||||
|  |           <div | ||||||
|  |             class="flex items-center justify-between p-12px bg-[var(--el-fill-color-light)] border-b border-[var(--el-border-color-lighter)] rounded-t-4px" | ||||||
|  |           > | ||||||
|  |             <div class="flex items-center gap-8px"> | ||||||
|  |               <div | ||||||
|  |                 class="w-20px h-20px bg-blue-500 text-white rounded-full flex items-center justify-center text-10px font-bold" | ||||||
|  |               > | ||||||
|  |                 {{ conditionIndex + 1 }} | ||||||
|  |               </div> | ||||||
|  |               <span class="text-12px font-500 text-[var(--el-text-color-primary)]" | ||||||
|  |                 >条件 {{ conditionIndex + 1 }}</span | ||||||
|  |               > | ||||||
|  |             </div> | ||||||
|  |             <el-button | ||||||
|  |               type="danger" | ||||||
|  |               size="small" | ||||||
|  |               text | ||||||
|  |               @click="removeCondition(conditionIndex)" | ||||||
|  |               v-if="subGroup.conditions!.length > 1" | ||||||
|  |               class="hover:bg-red-50" | ||||||
|  |             > | ||||||
|  |               <Icon icon="ep:delete" /> | ||||||
|  |             </el-button> | ||||||
|  |           </div> | ||||||
|  | 
 | ||||||
|  |           <div class="p-12px"> | ||||||
|  |             <ConditionConfig | ||||||
|  |               :model-value="condition" | ||||||
|  |               @update:model-value="(value) => updateCondition(conditionIndex, value)" | ||||||
|  |               :trigger-type="triggerType" | ||||||
|  |               @validate="(result) => handleConditionValidate(conditionIndex, result)" | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <!-- 条件间的"且"连接符 --> | ||||||
|  |         <div | ||||||
|  |           v-if="conditionIndex < subGroup.conditions!.length - 1" | ||||||
|  |           class="flex items-center justify-center py-8px" | ||||||
|  |         > | ||||||
|  |           <div class="flex items-center gap-8px"> | ||||||
|  |             <!-- 连接线 --> | ||||||
|  |             <div class="w-24px h-1px bg-green-300"></div> | ||||||
|  |             <!-- 且标签 --> | ||||||
|  |             <div class="px-12px py-4px bg-green-100 border border-green-300 rounded-full"> | ||||||
|  |               <span class="text-12px font-600 text-green-600">且</span> | ||||||
|  |             </div> | ||||||
|  |             <!-- 连接线 --> | ||||||
|  |             <div class="w-24px h-1px bg-green-300"></div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <!-- 添加条件按钮 --> | ||||||
|  |       <div | ||||||
|  |         v-if=" | ||||||
|  |           subGroup.conditions && | ||||||
|  |           subGroup.conditions.length > 0 && | ||||||
|  |           subGroup.conditions.length < maxConditions | ||||||
|  |         " | ||||||
|  |         class="text-center py-16px" | ||||||
|  |       > | ||||||
|  |         <el-button type="primary" plain @click="addCondition"> | ||||||
|  |           <Icon icon="ep:plus" /> | ||||||
|  |           继续添加条件 | ||||||
|  |         </el-button> | ||||||
|  |         <span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]"> | ||||||
|  |           最多可添加 {{ maxConditions }} 个条件 | ||||||
|  |         </span> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useVModel } from '@vueuse/core' | ||||||
|  | import ConditionConfig from './ConditionConfig.vue' | ||||||
|  | import { | ||||||
|  |   SubConditionGroupFormData, | ||||||
|  |   ConditionFormData, | ||||||
|  |   IotRuleSceneTriggerConditionTypeEnum | ||||||
|  | } from '@/api/iot/rule/scene/scene.types' | ||||||
|  | 
 | ||||||
|  | /** 子条件组配置组件 */ | ||||||
|  | defineOptions({ name: 'SubConditionGroupConfig' }) | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   modelValue: SubConditionGroupFormData | ||||||
|  |   triggerType: number | ||||||
|  |   maxConditions?: number | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Emits { | ||||||
|  |   (e: 'update:modelValue', value: SubConditionGroupFormData): void | ||||||
|  |   (e: 'validate', result: { valid: boolean; message: string }): void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const props = defineProps<Props>() | ||||||
|  | const emit = defineEmits<Emits>() | ||||||
|  | 
 | ||||||
|  | const subGroup = useVModel(props, 'modelValue', emit) | ||||||
|  | 
 | ||||||
|  | // 配置常量 | ||||||
|  | const maxConditions = computed(() => props.maxConditions || 3) | ||||||
|  | 
 | ||||||
|  | // 验证状态 | ||||||
|  | const conditionValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({}) | ||||||
|  | 
 | ||||||
|  | // 事件处理 | ||||||
|  | const addCondition = () => { | ||||||
|  |   if (!subGroup.value.conditions) { | ||||||
|  |     subGroup.value.conditions = [] | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (subGroup.value.conditions.length >= maxConditions.value) { | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const newCondition: ConditionFormData = { | ||||||
|  |     type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性 | ||||||
|  |     productId: undefined, | ||||||
|  |     deviceId: undefined, | ||||||
|  |     identifier: '', | ||||||
|  |     operator: '=', | ||||||
|  |     param: '' | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   subGroup.value.conditions.push(newCondition) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const removeCondition = (index: number) => { | ||||||
|  |   if (subGroup.value.conditions) { | ||||||
|  |     subGroup.value.conditions.splice(index, 1) | ||||||
|  |     delete conditionValidations.value[index] | ||||||
|  | 
 | ||||||
|  |     // 重新索引验证结果 | ||||||
|  |     const newValidations: { [key: number]: { valid: boolean; message: string } } = {} | ||||||
|  |     Object.keys(conditionValidations.value).forEach((key) => { | ||||||
|  |       const numKey = parseInt(key) | ||||||
|  |       if (numKey > index) { | ||||||
|  |         newValidations[numKey - 1] = conditionValidations.value[numKey] | ||||||
|  |       } else if (numKey < index) { | ||||||
|  |         newValidations[numKey] = conditionValidations.value[numKey] | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |     conditionValidations.value = newValidations | ||||||
|  | 
 | ||||||
|  |     updateValidationResult() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const updateCondition = (index: number, condition: ConditionFormData) => { | ||||||
|  |   if (subGroup.value.conditions) { | ||||||
|  |     subGroup.value.conditions[index] = condition | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const handleConditionValidate = (index: number, result: { valid: boolean; message: string }) => { | ||||||
|  |   conditionValidations.value[index] = result | ||||||
|  |   updateValidationResult() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const updateValidationResult = () => { | ||||||
|  |   if (!subGroup.value.conditions || subGroup.value.conditions.length === 0) { | ||||||
|  |     emit('validate', { valid: false, message: '子条件组至少需要一个条件' }) | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const validations = Object.values(conditionValidations.value) | ||||||
|  |   const allValid = validations.every((v: any) => v.valid) | ||||||
|  | 
 | ||||||
|  |   if (allValid) { | ||||||
|  |     emit('validate', { valid: true, message: '子条件组配置验证通过' }) | ||||||
|  |   } else { | ||||||
|  |     const errorMessages = validations.filter((v: any) => !v.valid).map((v: any) => v.message) | ||||||
|  |     emit('validate', { valid: false, message: `条件配置错误: ${errorMessages.join('; ')}` }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 监听变化 | ||||||
|  | watch( | ||||||
|  |   () => subGroup.value.conditions, | ||||||
|  |   () => { | ||||||
|  |     updateValidationResult() | ||||||
|  |   }, | ||||||
|  |   { deep: true, immediate: true } | ||||||
|  | ) | ||||||
|  | </script> | ||||||
|  | @ -0,0 +1,40 @@ | ||||||
|  | <!-- 定时触发配置组件 --> | ||||||
|  | <template> | ||||||
|  |   <div class="flex flex-col gap-16px"> | ||||||
|  |     <div class="flex items-center gap-8px p-12px px-16px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"> | ||||||
|  |       <Icon icon="ep:timer" class="text-[var(--el-color-danger)] text-18px" /> | ||||||
|  |       <span class="text-14px font-500 text-[var(--el-text-color-primary)]">定时触发配置</span> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <!-- CRON表达式配置 --> | ||||||
|  |     <div class="p-16px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]"> | ||||||
|  |       <el-form-item label="CRON表达式" required> | ||||||
|  |         <Crontab v-model="localValue" /> | ||||||
|  |       </el-form-item> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useVModel } from '@vueuse/core' | ||||||
|  | import { Crontab } from '@/components/Crontab' | ||||||
|  | 
 | ||||||
|  | /** 定时触发配置组件 */ | ||||||
|  | defineOptions({ name: 'TimerTriggerConfig' }) | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   modelValue?: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Emits { | ||||||
|  |   (e: 'update:modelValue', value: string): void | ||||||
|  |   (e: 'validate', result: { valid: boolean; message: string }): void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const props = defineProps<Props>() | ||||||
|  | const emit = defineEmits<Emits>() | ||||||
|  | 
 | ||||||
|  | const localValue = useVModel(props, 'modelValue', emit, { | ||||||
|  |   defaultValue: '0 0 12 * * ?' | ||||||
|  | }) | ||||||
|  | </script> | ||||||
|  | @ -1,15 +1,15 @@ | ||||||
| <!-- 执行器配置组件 --> | <!-- 执行器配置组件 --> | ||||||
| <!-- todo @puhui999:参考“触发器配置”,简化下。 --> | <!-- todo @puhui999:参考“触发器配置”,简化下。 --> | ||||||
| <template> | <template> | ||||||
|   <el-card class="action-section" shadow="never"> |   <el-card class="border border-[var(--el-border-color-light)] rounded-8px" shadow="never"> | ||||||
|     <template #header> |     <template #header> | ||||||
|       <div class="section-header"> |       <div class="flex items-center justify-between"> | ||||||
|         <div class="header-left"> |         <div class="flex items-center gap-8px"> | ||||||
|           <Icon icon="ep:setting" class="section-icon" /> |           <Icon icon="ep:setting" class="text-[var(--el-color-primary)] text-18px" /> | ||||||
|           <span class="section-title">执行器配置</span> |           <span class="text-16px font-600 text-[var(--el-text-color-primary)]">执行器配置</span> | ||||||
|           <el-tag size="small" type="info">{{ actions.length }}/{{ maxActions }}</el-tag> |           <el-tag size="small" type="info">{{ actions.length }}/{{ maxActions }}</el-tag> | ||||||
|         </div> |         </div> | ||||||
|         <div class="header-right"> |         <div class="flex items-center gap-8px"> | ||||||
|           <el-button |           <el-button | ||||||
|             type="primary" |             type="primary" | ||||||
|             size="small" |             size="small" | ||||||
|  | @ -23,9 +23,9 @@ | ||||||
|       </div> |       </div> | ||||||
|     </template> |     </template> | ||||||
| 
 | 
 | ||||||
|     <div class="section-content"> |     <div class="p-0"> | ||||||
|       <!-- 空状态 --> |       <!-- 空状态 --> | ||||||
|       <div v-if="actions.length === 0" class="empty-state"> |       <div v-if="actions.length === 0"> | ||||||
|         <el-empty description="暂无执行器配置"> |         <el-empty description="暂无执行器配置"> | ||||||
|           <el-button type="primary" @click="addAction"> |           <el-button type="primary" @click="addAction"> | ||||||
|             <Icon icon="ep:plus" /> |             <Icon icon="ep:plus" /> | ||||||
|  | @ -35,17 +35,17 @@ | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <!-- 执行器列表 --> |       <!-- 执行器列表 --> | ||||||
|       <div v-else class="actions-list"> |       <div v-else class="space-y-16px"> | ||||||
|         <div v-for="(action, index) in actions" :key="`action-${index}`" class="action-item"> |         <div v-for="(action, index) in actions" :key="`action-${index}`" class="p-16px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]"> | ||||||
|           <div class="action-header"> |           <div class="flex items-center justify-between mb-16px"> | ||||||
|             <div class="action-title"> |             <div class="flex items-center gap-8px"> | ||||||
|               <Icon icon="ep:setting" class="action-icon" /> |               <Icon icon="ep:setting" class="text-[var(--el-color-success)] text-16px" /> | ||||||
|               <span>执行器 {{ index + 1 }}</span> |               <span>执行器 {{ index + 1 }}</span> | ||||||
|               <el-tag :type="getActionTypeTag(action.type)" size="small"> |               <el-tag :type="getActionTypeTag(action.type)" size="small"> | ||||||
|                 {{ getActionTypeName(action.type) }} |                 {{ getActionTypeName(action.type) }} | ||||||
|               </el-tag> |               </el-tag> | ||||||
|             </div> |             </div> | ||||||
|             <div class="action-actions"> |             <div> | ||||||
|               <el-button |               <el-button | ||||||
|                 type="danger" |                 type="danger" | ||||||
|                 size="small" |                 size="small" | ||||||
|  | @ -59,7 +59,7 @@ | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
| 
 | 
 | ||||||
|           <div class="action-content"> |           <div class="space-y-16px"> | ||||||
|             <!-- 执行类型选择 --> |             <!-- 执行类型选择 --> | ||||||
|             <ActionTypeSelector |             <ActionTypeSelector | ||||||
|               :model-value="action.type" |               :model-value="action.type" | ||||||
|  | @ -87,12 +87,12 @@ | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <!-- 添加提示 --> |       <!-- 添加提示 --> | ||||||
|       <div v-if="actions.length > 0 && actions.length < maxActions" class="add-more"> |       <div v-if="actions.length > 0 && actions.length < maxActions" class="text-center py-16px"> | ||||||
|         <el-button type="primary" plain @click="addAction" class="add-more-btn"> |         <el-button type="primary" plain @click="addAction"> | ||||||
|           <Icon icon="ep:plus" /> |           <Icon icon="ep:plus" /> | ||||||
|           继续添加执行器 |           继续添加执行器 | ||||||
|         </el-button> |         </el-button> | ||||||
|         <span class="add-more-text"> 最多可添加 {{ maxActions }} 个执行器 </span> |         <span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]"> 最多可添加 {{ maxActions }} 个执行器 </span> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <!-- 验证结果 --> |       <!-- 验证结果 --> | ||||||
|  | @ -117,7 +117,6 @@ import { | ||||||
|   ActionFormData, |   ActionFormData, | ||||||
|   IotRuleSceneActionTypeEnum as ActionTypeEnum |   IotRuleSceneActionTypeEnum as ActionTypeEnum | ||||||
| } from '@/api/iot/rule/scene/scene.types' | } from '@/api/iot/rule/scene/scene.types' | ||||||
| import { createDefaultActionData } from '../../utils/transform' |  | ||||||
| 
 | 
 | ||||||
| /** 执行器配置组件 */ | /** 执行器配置组件 */ | ||||||
| defineOptions({ name: 'ActionSection' }) | defineOptions({ name: 'ActionSection' }) | ||||||
|  | @ -136,6 +135,19 @@ const emit = defineEmits<Emits>() | ||||||
| 
 | 
 | ||||||
| const actions = useVModel(props, 'actions', emit) | 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 | const maxActions = 5 | ||||||
| 
 | 
 | ||||||
|  | @ -266,107 +278,4 @@ watch( | ||||||
| ) | ) | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped> |  | ||||||
| .action-section { |  | ||||||
|   border: 1px solid var(--el-border-color-light); |  | ||||||
|   border-radius: 8px; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| .section-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-left { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .section-icon { |  | ||||||
|   color: var(--el-color-primary); |  | ||||||
|   font-size: 18px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .section-title { |  | ||||||
|   font-size: 16px; |  | ||||||
|   font-weight: 600; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .header-right { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .section-content { |  | ||||||
|   padding: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .empty-state { |  | ||||||
|   padding: 40px 0; |  | ||||||
|   text-align: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .actions-list { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .action-item { |  | ||||||
|   border: 1px solid var(--el-border-color-lighter); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   background: var(--el-fill-color-blank); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .action-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   padding: 12px 16px; |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-bottom: 1px solid var(--el-border-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .action-title { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .action-icon { |  | ||||||
|   color: var(--el-color-success); |  | ||||||
|   font-size: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .action-content { |  | ||||||
|   padding: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .add-more { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 12px; |  | ||||||
|   margin-top: 16px; |  | ||||||
|   padding: 16px; |  | ||||||
|   border: 1px dashed var(--el-border-color); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   background: var(--el-fill-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .add-more-btn { |  | ||||||
|   flex-shrink: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .add-more-text { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .validation-result { |  | ||||||
|   margin-top: 16px; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -0,0 +1,91 @@ | ||||||
|  | <!-- 基础信息配置组件 --> | ||||||
|  | <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:info-filled" 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-tag :type="formData.status === 0 ? 'success' : 'danger'" size="small"> | ||||||
|  |             {{ formData.status === 0 ? '启用' : '禁用' }} | ||||||
|  |           </el-tag> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </template> | ||||||
|  | 
 | ||||||
|  |     <div class="p-0"> | ||||||
|  |       <el-row :gutter="24" class="mb-24px"> | ||||||
|  |         <el-col :span="12"> | ||||||
|  |           <el-form-item label="场景名称" prop="name" required> | ||||||
|  |             <el-input | ||||||
|  |               v-model="formData.name" | ||||||
|  |               placeholder="请输入场景名称" | ||||||
|  |               maxlength="50" | ||||||
|  |               show-word-limit | ||||||
|  |               clearable | ||||||
|  |             /> | ||||||
|  |           </el-form-item> | ||||||
|  |         </el-col> | ||||||
|  |         <el-col :span="12"> | ||||||
|  |           <el-form-item label="场景状态" prop="status" required> | ||||||
|  |             <el-radio-group v-model="formData.status"> | ||||||
|  |               <el-radio | ||||||
|  |                 v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" | ||||||
|  |                 :key="dict.value" | ||||||
|  |                 :label="dict.value" | ||||||
|  |               > | ||||||
|  |                 {{ dict.label }} | ||||||
|  |               </el-radio> | ||||||
|  |             </el-radio-group> | ||||||
|  |           </el-form-item> | ||||||
|  |         </el-col> | ||||||
|  |       </el-row> | ||||||
|  |       <el-form-item label="场景描述" prop="description"> | ||||||
|  |         <el-input | ||||||
|  |           v-model="formData.description" | ||||||
|  |           type="textarea" | ||||||
|  |           placeholder="请输入场景描述(可选)" | ||||||
|  |           :rows="3" | ||||||
|  |           maxlength="200" | ||||||
|  |           show-word-limit | ||||||
|  |           resize="none" | ||||||
|  |         /> | ||||||
|  |       </el-form-item> | ||||||
|  |     </div> | ||||||
|  |   </el-card> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useVModel } from '@vueuse/core' | ||||||
|  | import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' | ||||||
|  | import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types' | ||||||
|  | 
 | ||||||
|  | /** 基础信息配置组件 */ | ||||||
|  | defineOptions({ name: 'BasicInfoSection' }) | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   modelValue: RuleSceneFormData | ||||||
|  |   rules?: any | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Emits { | ||||||
|  |   (e: 'update:modelValue', value: RuleSceneFormData): void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const props = defineProps<Props>() | ||||||
|  | const emit = defineEmits<Emits>() | ||||||
|  | 
 | ||||||
|  | const formData = useVModel(props, 'modelValue', emit) | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | :deep(.el-form-item) { | ||||||
|  |   margin-bottom: 20px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | :deep(.el-form-item:last-child) { | ||||||
|  |   margin-bottom: 0; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -0,0 +1,151 @@ | ||||||
|  | <!-- 场景触发器配置组件 --> | ||||||
|  | <template> | ||||||
|  |   <el-card class="border border-[var(--el-border-color-light)] rounded-8px" shadow="never"> | ||||||
|  |     <template #header> | ||||||
|  |       <div class="flex items-center gap-8px"> | ||||||
|  |         <Icon icon="ep:lightning" class="text-[var(--el-color-primary)] text-18px" /> | ||||||
|  |         <span class="text-16px font-600 text-[var(--el-text-color-primary)]">触发器配置</span> | ||||||
|  |         <el-tag size="small" type="info">场景触发器</el-tag> | ||||||
|  |       </div> | ||||||
|  |     </template> | ||||||
|  | 
 | ||||||
|  |     <div class="p-16px space-y-16px"> | ||||||
|  |       <!-- 触发事件类型选择 --> | ||||||
|  |       <el-form-item label="触发事件类型" required> | ||||||
|  |         <el-select | ||||||
|  |           :model-value="trigger.type" | ||||||
|  |           @update:model-value="(value) => updateTriggerType(value)" | ||||||
|  |           @change="onTriggerTypeChange" | ||||||
|  |           placeholder="请选择触发事件类型" | ||||||
|  |           class="w-full" | ||||||
|  |         > | ||||||
|  |           <el-option | ||||||
|  |             v-for="option in triggerTypeOptions" | ||||||
|  |             :key="option.value" | ||||||
|  |             :label="option.label" | ||||||
|  |             :value="option.value" | ||||||
|  |           /> | ||||||
|  |         </el-select> | ||||||
|  |       </el-form-item> | ||||||
|  | 
 | ||||||
|  |       <!-- 设备触发配置 --> | ||||||
|  |       <DeviceTriggerConfig | ||||||
|  |         v-if="isDeviceTrigger(trigger.type)" | ||||||
|  |         :model-value="trigger" | ||||||
|  |         @update:model-value="updateTrigger" | ||||||
|  |       /> | ||||||
|  | 
 | ||||||
|  |       <!-- 定时触发配置 --> | ||||||
|  |       <TimerTriggerConfig | ||||||
|  |         v-if="trigger.type === TriggerTypeEnum.TIMER" | ||||||
|  |         :model-value="trigger.cronExpression" | ||||||
|  |         @update:model-value="updateTriggerCronExpression" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |   </el-card> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useVModel } from '@vueuse/core' | ||||||
|  | import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue' | ||||||
|  | import TimerTriggerConfig from '../configs/TimerTriggerConfig.vue' | ||||||
|  | import { | ||||||
|  |   TriggerFormData, | ||||||
|  |   IotRuleSceneTriggerTypeEnum as TriggerTypeEnum | ||||||
|  | } from '@/api/iot/rule/scene/scene.types' | ||||||
|  | 
 | ||||||
|  | /** 触发器配置组件 */ | ||||||
|  | defineOptions({ name: 'TriggerSection' }) | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   trigger: TriggerFormData | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Emits { | ||||||
|  |   (e: 'update:trigger', value: TriggerFormData): void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const props = defineProps<Props>() | ||||||
|  | const emit = defineEmits<Emits>() | ||||||
|  | 
 | ||||||
|  | const trigger = useVModel(props, 'trigger', emit) | ||||||
|  | 
 | ||||||
|  | // 触发器类型选项 | ||||||
|  | const triggerTypeOptions = [ | ||||||
|  |   { | ||||||
|  |     value: TriggerTypeEnum.DEVICE_STATE_UPDATE, | ||||||
|  |     label: '设备状态变更' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: TriggerTypeEnum.DEVICE_PROPERTY_POST, | ||||||
|  |     label: '设备属性上报' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: TriggerTypeEnum.DEVICE_EVENT_POST, | ||||||
|  |     label: '设备事件上报' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: TriggerTypeEnum.DEVICE_SERVICE_INVOKE, | ||||||
|  |     label: '设备服务调用' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: TriggerTypeEnum.TIMER, | ||||||
|  |     label: '定时触发' | ||||||
|  |   } | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | // 工具函数 | ||||||
|  | const isDeviceTrigger = (type: number) => { | ||||||
|  |   const deviceTriggerTypes = [ | ||||||
|  |     TriggerTypeEnum.DEVICE_STATE_UPDATE, | ||||||
|  |     TriggerTypeEnum.DEVICE_PROPERTY_POST, | ||||||
|  |     TriggerTypeEnum.DEVICE_EVENT_POST, | ||||||
|  |     TriggerTypeEnum.DEVICE_SERVICE_INVOKE | ||||||
|  |   ] as number[] | ||||||
|  |   return deviceTriggerTypes.includes(type) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 事件处理 | ||||||
|  | const updateTriggerType = (type: number) => { | ||||||
|  |   trigger.value.type = type | ||||||
|  |   onTriggerTypeChange(type) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const updateTrigger = (newTrigger: TriggerFormData) => { | ||||||
|  |   trigger.value = newTrigger | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const updateTriggerCronExpression = (cronExpression?: string) => { | ||||||
|  |   trigger.value.cronExpression = cronExpression | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const onTriggerTypeChange = (type: number) => { | ||||||
|  |   // 清理不相关的配置 | ||||||
|  |   if (type === TriggerTypeEnum.TIMER) { | ||||||
|  |     trigger.value.productId = undefined | ||||||
|  |     trigger.value.deviceId = undefined | ||||||
|  |     trigger.value.identifier = undefined | ||||||
|  |     trigger.value.operator = undefined | ||||||
|  |     trigger.value.value = undefined | ||||||
|  |     trigger.value.mainCondition = undefined | ||||||
|  |     trigger.value.conditionGroup = undefined | ||||||
|  |     if (!trigger.value.cronExpression) { | ||||||
|  |       trigger.value.cronExpression = '0 0 12 * * ?' | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     trigger.value.cronExpression = undefined | ||||||
|  |     if (type === TriggerTypeEnum.DEVICE_STATE_UPDATE) { | ||||||
|  |       trigger.value.mainCondition = undefined | ||||||
|  |       trigger.value.conditionGroup = undefined | ||||||
|  |     } else { | ||||||
|  |       // 设备属性、事件、服务触发需要条件配置 | ||||||
|  |       if (!trigger.value.mainCondition) { | ||||||
|  |         trigger.value.mainCondition = undefined // 等待用户配置 | ||||||
|  |       } | ||||||
|  |       if (!trigger.value.conditionGroup) { | ||||||
|  |         trigger.value.conditionGroup = undefined // 可选的条件组 | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <!-- 执行器类型选择组件 --> | <!-- 执行器类型选择组件 --> | ||||||
| <template> | <template> | ||||||
|   <div class="action-type-selector"> |   <div class="w-full"> | ||||||
|     <!-- TODO @puhui999:1)设备属性设置时,貌似没选属性;2)服务调用时,貌似也没的设置哈; --> |     <!-- TODO @puhui999:1)设备属性设置时,貌似没选属性;2)服务调用时,貌似也没的设置哈; --> | ||||||
|     <el-form-item label="执行类型" required> |     <el-form-item label="执行类型" required> | ||||||
|       <el-select |       <el-select | ||||||
|  | @ -15,12 +15,12 @@ | ||||||
|           :label="option.label" |           :label="option.label" | ||||||
|           :value="option.value" |           :value="option.value" | ||||||
|         > |         > | ||||||
|           <div class="action-option"> |           <div class="flex items-center justify-between w-full py-4px"> | ||||||
|             <div class="option-content"> |             <div class="flex items-center gap-12px flex-1"> | ||||||
|               <Icon :icon="option.icon" class="option-icon" /> |               <Icon :icon="option.icon" class="text-18px text-[var(--el-color-primary)] flex-shrink-0" /> | ||||||
|               <div class="option-info"> |               <div class="flex-1"> | ||||||
|                 <div class="option-label">{{ option.label }}</div> |                 <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ option.label }}</div> | ||||||
|                 <div class="option-desc">{{ option.description }}</div> |                 <div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">{{ option.description }}</div> | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|             <el-tag :type="option.tag" size="small"> |             <el-tag :type="option.tag" size="small"> | ||||||
|  | @ -97,48 +97,6 @@ const handleChange = (value: number) => { | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| .action-type-selector { |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .action-option { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   width: 100%; |  | ||||||
|   padding: 4px 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-content { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 12px; |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-icon { |  | ||||||
|   font-size: 18px; |  | ||||||
|   color: var(--el-color-primary); |  | ||||||
|   flex-shrink: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-info { |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-label { |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
|   margin-bottom: 2px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-desc { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
|   line-height: 1.4; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-select-dropdown__item) { | :deep(.el-select-dropdown__item) { | ||||||
|   height: auto; |   height: auto; | ||||||
|   padding: 8px 20px; |   padding: 8px 20px; | ||||||
|  | @ -0,0 +1,80 @@ | ||||||
|  | <!-- 条件类型选择器组件 --> | ||||||
|  | <template> | ||||||
|  |   <el-select | ||||||
|  |     :model-value="modelValue" | ||||||
|  |     @update:model-value="handleChange" | ||||||
|  |     placeholder="请选择条件类型" | ||||||
|  |     class="w-full" | ||||||
|  |   > | ||||||
|  |     <el-option | ||||||
|  |       v-for="option in conditionTypeOptions" | ||||||
|  |       :key="option.value" | ||||||
|  |       :label="option.label" | ||||||
|  |       :value="option.value" | ||||||
|  |     > | ||||||
|  |       <div class="flex items-center justify-between w-full"> | ||||||
|  |         <div class="flex items-center gap-8px"> | ||||||
|  |           <Icon :icon="option.icon" :class="option.iconClass" /> | ||||||
|  |           <span>{{ option.label }}</span> | ||||||
|  |         </div> | ||||||
|  |         <el-tag :type="option.tag" size="small">{{ option.category }}</el-tag> | ||||||
|  |       </div> | ||||||
|  |     </el-option> | ||||||
|  |   </el-select> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { IotRuleSceneTriggerConditionTypeEnum } from '@/api/iot/rule/scene/scene.types' | ||||||
|  | 
 | ||||||
|  | /** 条件类型选择器组件 */ | ||||||
|  | defineOptions({ name: 'ConditionTypeSelector' }) | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   modelValue?: number | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Emits { | ||||||
|  |   (e: 'update:modelValue', value: number): void | ||||||
|  |   (e: 'change', value: number): void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const props = defineProps<Props>() | ||||||
|  | const emit = defineEmits<Emits>() | ||||||
|  | 
 | ||||||
|  | // 条件类型选项 | ||||||
|  | const conditionTypeOptions = [ | ||||||
|  |   { | ||||||
|  |     value: IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS, | ||||||
|  |     label: '设备状态', | ||||||
|  |     description: '监控设备的在线/离线状态变化', | ||||||
|  |     icon: 'ep:connection', | ||||||
|  |     iconClass: 'text-blue-500', | ||||||
|  |     tag: 'primary', | ||||||
|  |     category: '设备' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, | ||||||
|  |     label: '设备属性', | ||||||
|  |     description: '监控设备属性值的变化', | ||||||
|  |     icon: 'ep:data-analysis', | ||||||
|  |     iconClass: 'text-green-500', | ||||||
|  |     tag: 'success', | ||||||
|  |     category: '属性' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME, | ||||||
|  |     label: '当前时间', | ||||||
|  |     description: '基于当前时间的条件判断', | ||||||
|  |     icon: 'ep:timer', | ||||||
|  |     iconClass: 'text-orange-500', | ||||||
|  |     tag: 'warning', | ||||||
|  |     category: '时间' | ||||||
|  |   } | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | // 事件处理 | ||||||
|  | const handleChange = (value: number) => { | ||||||
|  |   emit('update:modelValue', value) | ||||||
|  |   emit('change', value) | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | @ -0,0 +1,128 @@ | ||||||
|  | <!-- 设备选择器组件 --> | ||||||
|  | <template> | ||||||
|  |   <el-select | ||||||
|  |     :model-value="modelValue" | ||||||
|  |     @update:model-value="handleChange" | ||||||
|  |     placeholder="请选择设备" | ||||||
|  |     filterable | ||||||
|  |     clearable | ||||||
|  |     class="w-full" | ||||||
|  |     :loading="deviceLoading" | ||||||
|  |     :disabled="!productId" | ||||||
|  |   > | ||||||
|  |     <el-option | ||||||
|  |       v-for="device in deviceList" | ||||||
|  |       :key="device.id" | ||||||
|  |       :label="device.deviceName" | ||||||
|  |       :value="device.id" | ||||||
|  |     > | ||||||
|  |       <div class="flex items-center justify-between w-full py-4px"> | ||||||
|  |         <div class="flex-1"> | ||||||
|  |           <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ | ||||||
|  |             device.deviceName | ||||||
|  |           }}</div> | ||||||
|  |           <div class="text-12px text-[var(--el-text-color-secondary)]">{{ device.deviceKey }}</div> | ||||||
|  |         </div> | ||||||
|  |         <div class="flex items-center gap-4px"> | ||||||
|  |           <el-tag size="small" :type="getStatusType(device.status)"> | ||||||
|  |             {{ getStatusText(device.status) }} | ||||||
|  |           </el-tag> | ||||||
|  |           <el-tag size="small" :type="device.activeTime ? 'success' : 'info'"> | ||||||
|  |             {{ device.activeTime ? '已激活' : '未激活' }} | ||||||
|  |           </el-tag> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </el-option> | ||||||
|  |   </el-select> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { DeviceApi } from '@/api/iot/device/device' | ||||||
|  | 
 | ||||||
|  | /** 设备选择器组件 */ | ||||||
|  | defineOptions({ name: 'DeviceSelector' }) | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   modelValue?: number | ||||||
|  |   productId?: number | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Emits { | ||||||
|  |   (e: 'update:modelValue', value?: number): void | ||||||
|  |   (e: 'change', value?: number): void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const props = defineProps<Props>() | ||||||
|  | const emit = defineEmits<Emits>() | ||||||
|  | 
 | ||||||
|  | // 状态 | ||||||
|  | const deviceLoading = ref(false) | ||||||
|  | const deviceList = ref<any[]>([]) | ||||||
|  | 
 | ||||||
|  | // 事件处理 | ||||||
|  | const handleChange = (value?: number) => { | ||||||
|  |   emit('update:modelValue', value) | ||||||
|  |   emit('change', value) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 获取设备列表 | ||||||
|  | const getDeviceList = async () => { | ||||||
|  |   if (!props.productId) { | ||||||
|  |     deviceList.value = [] | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   try { | ||||||
|  |     deviceLoading.value = true | ||||||
|  |     const res = await DeviceApi.getDeviceListByProductId(props.productId) | ||||||
|  |     deviceList.value = res || [] | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('获取设备列表失败:', error) | ||||||
|  |     deviceList.value = [] | ||||||
|  |   } finally { | ||||||
|  |     deviceList.value.push({ id: 0, deviceName: '全部设备' }) | ||||||
|  |     deviceLoading.value = false | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 设备状态映射 | ||||||
|  | const getStatusType = (status: number) => { | ||||||
|  |   switch (status) { | ||||||
|  |     case 0: | ||||||
|  |       return 'success' // 正常 | ||||||
|  |     case 1: | ||||||
|  |       return 'danger' // 禁用 | ||||||
|  |     default: | ||||||
|  |       return 'info' | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const getStatusText = (status: number) => { | ||||||
|  |   switch (status) { | ||||||
|  |     case 0: | ||||||
|  |       return '正常' | ||||||
|  |     case 1: | ||||||
|  |       return '禁用' | ||||||
|  |     default: | ||||||
|  |       return '未知' | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 监听产品变化 | ||||||
|  | watch( | ||||||
|  |   () => props.productId, | ||||||
|  |   (newProductId) => { | ||||||
|  |     if (newProductId) { | ||||||
|  |       getDeviceList() | ||||||
|  |     } else { | ||||||
|  |       deviceList.value = [] | ||||||
|  |       // 清空当前选择的设备 | ||||||
|  |       if (props.modelValue) { | ||||||
|  |         emit('update:modelValue', undefined) | ||||||
|  |         emit('change', undefined) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   { immediate: true } | ||||||
|  | ) | ||||||
|  | </script> | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <!-- 操作符选择器组件 --> | <!-- 操作符选择器组件 --> | ||||||
| <template> | <template> | ||||||
|   <div class="operator-selector"> |   <div class="w-full"> | ||||||
|     <el-select |     <el-select | ||||||
|       v-model="localValue" |       v-model="localValue" | ||||||
|       placeholder="请选择操作符" |       placeholder="请选择操作符" | ||||||
|  | @ -13,26 +13,26 @@ | ||||||
|         :label="operator.label" |         :label="operator.label" | ||||||
|         :value="operator.value" |         :value="operator.value" | ||||||
|       > |       > | ||||||
|         <div class="operator-option"> |         <div class="flex items-center justify-between w-full py-4px"> | ||||||
|           <div class="option-content"> |           <div class="flex items-center gap-8px"> | ||||||
|             <div class="option-label">{{ operator.label }}</div> |             <div class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ operator.label }}</div> | ||||||
|             <div class="option-symbol">{{ operator.symbol }}</div> |             <div class="text-12px text-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)] px-6px py-2px rounded-4px font-mono">{{ operator.symbol }}</div> | ||||||
|           </div> |           </div> | ||||||
|           <div class="option-desc">{{ operator.description }}</div> |           <div class="text-12px text-[var(--el-text-color-secondary)]">{{ operator.description }}</div> | ||||||
|         </div> |         </div> | ||||||
|       </el-option> |       </el-option> | ||||||
|     </el-select> |     </el-select> | ||||||
| 
 | 
 | ||||||
|     <!-- 操作符说明 --> |     <!-- 操作符说明 --> | ||||||
|     <!-- TODO @puhui999:这个去掉 --> |     <!-- TODO @puhui999:这个去掉 --> | ||||||
|     <div v-if="selectedOperator" class="operator-description"> |     <div v-if="selectedOperator" class="mt-8px p-8px bg-[var(--el-fill-color-light)] rounded-4px border border-[var(--el-border-color-lighter)]"> | ||||||
|       <div class="desc-content"> |       <div class="flex items-center gap-6px"> | ||||||
|         <Icon icon="ep:info-filled" class="desc-icon" /> |         <Icon icon="ep:info-filled" class="text-12px text-[var(--el-color-info)]" /> | ||||||
|         <span class="desc-text">{{ selectedOperator.description }}</span> |         <span class="text-12px text-[var(--el-text-color-secondary)]">{{ selectedOperator.description }}</span> | ||||||
|       </div> |       </div> | ||||||
|       <div v-if="selectedOperator.example" class="desc-example"> |       <div v-if="selectedOperator.example" class="flex items-center gap-6px mt-4px"> | ||||||
|         <span class="example-label">示例:</span> |         <span class="text-12px text-[var(--el-text-color-secondary)]">示例:</span> | ||||||
|         <code class="example-code">{{ selectedOperator.example }}</code> |         <code class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] px-4px py-2px rounded-2px font-mono">{{ selectedOperator.example }}</code> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|  | @ -184,92 +184,6 @@ watch( | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| .operator-selector { |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .operator-option { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   width: 100%; |  | ||||||
|   padding: 4px 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-content { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-label { |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-symbol { |  | ||||||
|   font-size: 16px; |  | ||||||
|   color: var(--el-color-primary); |  | ||||||
|   font-weight: bold; |  | ||||||
|   min-width: 20px; |  | ||||||
|   text-align: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-desc { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
|   max-width: 120px; |  | ||||||
|   text-align: right; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .operator-description { |  | ||||||
|   margin-top: 8px; |  | ||||||
|   padding: 8px 12px; |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-radius: 4px; |  | ||||||
|   border: 1px solid var(--el-border-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .desc-content { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 6px; |  | ||||||
|   margin-bottom: 4px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .desc-icon { |  | ||||||
|   color: var(--el-color-primary); |  | ||||||
|   font-size: 12px; |  | ||||||
|   flex-shrink: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .desc-text { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .desc-example { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 6px; |  | ||||||
|   margin-left: 18px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .example-label { |  | ||||||
|   font-size: 11px; |  | ||||||
|   color: var(--el-text-color-placeholder); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .example-code { |  | ||||||
|   font-size: 11px; |  | ||||||
|   color: var(--el-color-primary); |  | ||||||
|   background: var(--el-fill-color-blank); |  | ||||||
|   padding: 2px 4px; |  | ||||||
|   border-radius: 2px; |  | ||||||
|   font-family: 'Courier New', monospace; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-select-dropdown__item) { | :deep(.el-select-dropdown__item) { | ||||||
|   height: auto; |   height: auto; | ||||||
|   padding: 8px 20px; |   padding: 8px 20px; | ||||||
|  | @ -20,10 +20,10 @@ | ||||||
|               :label="product.name" |               :label="product.name" | ||||||
|               :value="product.id" |               :value="product.id" | ||||||
|             > |             > | ||||||
|               <div class="product-option"> |               <div class="flex items-center justify-between w-full py-4px"> | ||||||
|                 <div class="option-content"> |                 <div class="flex-1"> | ||||||
|                   <div class="option-name">{{ product.name }}</div> |                   <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ product.name }}</div> | ||||||
|                   <div class="option-key">{{ product.productKey }}</div> |                   <div class="text-12px text-[var(--el-text-color-secondary)]">{{ product.productKey }}</div> | ||||||
|                 </div> |                 </div> | ||||||
|                 <!-- TODO @puhui999:是不是用字典 --> |                 <!-- TODO @puhui999:是不是用字典 --> | ||||||
|                 <el-tag size="small" :type="product.status === 0 ? 'success' : 'danger'"> |                 <el-tag size="small" :type="product.status === 0 ? 'success' : 'danger'"> | ||||||
|  | @ -68,10 +68,10 @@ | ||||||
|               :label="device.deviceName" |               :label="device.deviceName" | ||||||
|               :value="device.id" |               :value="device.id" | ||||||
|             > |             > | ||||||
|               <div class="device-option"> |               <div class="flex items-center justify-between w-full py-4px"> | ||||||
|                 <div class="option-content"> |                 <div class="flex-1"> | ||||||
|                   <div class="option-name">{{ device.deviceName }}</div> |                   <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ device.deviceName }}</div> | ||||||
|                   <div class="option-nickname">{{ device.nickname || '无备注' }}</div> |                   <div class="text-12px text-[var(--el-text-color-secondary)]">{{ device.nickname || '无备注' }}</div> | ||||||
|                 </div> |                 </div> | ||||||
|                 <el-tag size="small" :type="getDeviceStatusTag(device.state)"> |                 <el-tag size="small" :type="getDeviceStatusTag(device.state)"> | ||||||
|                   {{ getDeviceStatusText(device.state) }} |                   {{ getDeviceStatusText(device.state) }} | ||||||
|  | @ -84,21 +84,21 @@ | ||||||
|     </el-row> |     </el-row> | ||||||
| 
 | 
 | ||||||
|     <!-- 选择结果展示 --> |     <!-- 选择结果展示 --> | ||||||
|     <div v-if="localProductId && localDeviceId !== undefined" class="selection-result"> |     <div v-if="localProductId && localDeviceId !== undefined" class="mt-16px p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"> | ||||||
|       <div class="result-header"> |       <div class="flex items-center gap-6px mb-8px"> | ||||||
|         <Icon icon="ep:check" class="result-icon" /> |         <Icon icon="ep:check" class="text-[var(--el-color-success)] text-16px" /> | ||||||
|         <span class="result-title">已选择设备</span> |         <span class="text-14px font-500 text-[var(--el-text-color-primary)]">已选择设备</span> | ||||||
|       </div> |       </div> | ||||||
|       <div class="result-content"> |       <div class="flex flex-col gap-6px ml-22px"> | ||||||
|         <div class="result-item"> |         <div class="flex items-center gap-8px"> | ||||||
|           <span class="result-label">产品:</span> |           <span class="text-12px text-[var(--el-text-color-secondary)] min-w-40px">产品:</span> | ||||||
|           <span class="result-value">{{ selectedProduct?.name }}</span> |           <span class="text-12px text-[var(--el-text-color-primary)] font-500">{{ selectedProduct?.name }}</span> | ||||||
|           <el-tag size="small" type="primary">{{ selectedProduct?.productKey }}</el-tag> |           <el-tag size="small" type="primary">{{ selectedProduct?.productKey }}</el-tag> | ||||||
|         </div> |         </div> | ||||||
|         <div class="result-item"> |         <div class="flex items-center gap-8px"> | ||||||
|           <span class="result-label">设备:</span> |           <span class="text-12px text-[var(--el-text-color-secondary)] min-w-40px">设备:</span> | ||||||
|           <span v-if="deviceSelectionMode === 'all'" class="result-value">全部设备</span> |           <span v-if="deviceSelectionMode === 'all'" class="text-12px text-[var(--el-text-color-primary)] font-500">全部设备</span> | ||||||
|           <span v-else class="result-value">{{ selectedDevice?.deviceName }}</span> |           <span v-else class="text-12px text-[var(--el-text-color-primary)] font-500">{{ selectedDevice?.deviceName }}</span> | ||||||
|           <el-tag v-if="deviceSelectionMode === 'all'" size="small" type="warning"> 全部 </el-tag> |           <el-tag v-if="deviceSelectionMode === 'all'" size="small" type="warning"> 全部 </el-tag> | ||||||
|           <el-tag v-else size="small" :type="getDeviceStatusTag(selectedDevice?.state)"> |           <el-tag v-else size="small" :type="getDeviceStatusTag(selectedDevice?.state)"> | ||||||
|             {{ getDeviceStatusText(selectedDevice?.state) }} |             {{ getDeviceStatusText(selectedDevice?.state) }} | ||||||
|  | @ -288,87 +288,6 @@ watch( | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| .product-device-selector { |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .product-option, |  | ||||||
| .device-option { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   width: 100%; |  | ||||||
|   padding: 4px 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-content { |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-name { |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
|   margin-bottom: 2px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-key, |  | ||||||
| .option-nickname { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .selection-result { |  | ||||||
|   margin-top: 16px; |  | ||||||
|   padding: 12px; |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   border: 1px solid var(--el-border-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .result-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 6px; |  | ||||||
|   margin-bottom: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .result-icon { |  | ||||||
|   color: var(--el-color-success); |  | ||||||
|   font-size: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .result-title { |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .result-content { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 6px; |  | ||||||
|   margin-left: 22px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .result-item { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .result-label { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
|   min-width: 40px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .result-value { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
|   font-weight: 500; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-select-dropdown__item) { | :deep(.el-select-dropdown__item) { | ||||||
|   height: auto; |   height: auto; | ||||||
|   padding: 8px 20px; |   padding: 8px 20px; | ||||||
|  | @ -0,0 +1,81 @@ | ||||||
|  | <!-- 产品选择器组件 --> | ||||||
|  | <template> | ||||||
|  |   <el-select | ||||||
|  |     :model-value="modelValue" | ||||||
|  |     @update:model-value="handleChange" | ||||||
|  |     placeholder="请选择产品" | ||||||
|  |     filterable | ||||||
|  |     clearable | ||||||
|  |     class="w-full" | ||||||
|  |     :loading="productLoading" | ||||||
|  |   > | ||||||
|  |     <el-option | ||||||
|  |       v-for="product in productList" | ||||||
|  |       :key="product.id" | ||||||
|  |       :label="product.name" | ||||||
|  |       :value="product.id" | ||||||
|  |     > | ||||||
|  |       <div class="flex items-center justify-between w-full py-4px"> | ||||||
|  |         <div class="flex-1"> | ||||||
|  |           <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ | ||||||
|  |             product.name | ||||||
|  |           }}</div> | ||||||
|  |           <div class="text-12px text-[var(--el-text-color-secondary)]">{{ | ||||||
|  |             product.productKey | ||||||
|  |           }}</div> | ||||||
|  |         </div> | ||||||
|  |         <el-tag size="small" :type="product.status === 0 ? 'success' : 'danger'"> | ||||||
|  |           {{ product.status === 0 ? '正常' : '禁用' }} | ||||||
|  |         </el-tag> | ||||||
|  |       </div> | ||||||
|  |     </el-option> | ||||||
|  |   </el-select> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { ProductApi } from '@/api/iot/product/product' | ||||||
|  | 
 | ||||||
|  | /** 产品选择器组件 */ | ||||||
|  | defineOptions({ name: 'ProductSelector' }) | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   modelValue?: number | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface Emits { | ||||||
|  |   (e: 'update:modelValue', value?: number): void | ||||||
|  |   (e: 'change', value?: number): void | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | defineProps<Props>() | ||||||
|  | const emit = defineEmits<Emits>() | ||||||
|  | 
 | ||||||
|  | // 状态 | ||||||
|  | const productLoading = ref(false) | ||||||
|  | const productList = ref<any[]>([]) | ||||||
|  | 
 | ||||||
|  | // 事件处理 | ||||||
|  | const handleChange = (value?: number) => { | ||||||
|  |   emit('update:modelValue', value) | ||||||
|  |   emit('change', value) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 获取产品列表 | ||||||
|  | const getProductList = async () => { | ||||||
|  |   try { | ||||||
|  |     productLoading.value = true | ||||||
|  |     const res = await ProductApi.getSimpleProductList() | ||||||
|  |     productList.value = res || [] | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('获取产品列表失败:', error) | ||||||
|  |     productList.value = [] | ||||||
|  |   } finally { | ||||||
|  |     productLoading.value = false | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 组件挂载时获取产品列表 | ||||||
|  | onMounted(() => { | ||||||
|  |   getProductList() | ||||||
|  | }) | ||||||
|  | </script> | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| <!-- 属性选择器组件 --> | <!-- 属性选择器组件 --> | ||||||
| <!-- TODO @yunai:可能要在 review 下 --> | <!-- TODO @yunai:可能要在 review 下 --> | ||||||
| <template> | <template> | ||||||
|   <div class="property-selector"> |   <div class="w-full"> | ||||||
|     <el-select |     <el-select | ||||||
|       v-model="localValue" |       v-model="localValue" | ||||||
|       placeholder="请选择监控项" |       placeholder="请选择监控项" | ||||||
|  | @ -18,12 +18,12 @@ | ||||||
|           :label="property.name" |           :label="property.name" | ||||||
|           :value="property.identifier" |           :value="property.identifier" | ||||||
|         > |         > | ||||||
|           <div class="property-option"> |           <div class="flex items-center justify-between w-full py-4px"> | ||||||
|             <div class="option-content"> |             <div class="flex-1"> | ||||||
|               <div class="option-name">{{ property.name }}</div> |               <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ property.name }}</div> | ||||||
|               <div class="option-identifier">{{ property.identifier }}</div> |               <div class="text-12px text-[var(--el-text-color-secondary)]">{{ property.identifier }}</div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="option-meta"> |             <div class="flex-shrink-0"> | ||||||
|               <el-tag :type="getPropertyTypeTag(property.dataType)" size="small"> |               <el-tag :type="getPropertyTypeTag(property.dataType)" size="small"> | ||||||
|                 {{ getPropertyTypeName(property.dataType) }} |                 {{ getPropertyTypeName(property.dataType) }} | ||||||
|               </el-tag> |               </el-tag> | ||||||
|  | @ -34,30 +34,30 @@ | ||||||
|     </el-select> |     </el-select> | ||||||
| 
 | 
 | ||||||
|     <!-- 属性详情 --> |     <!-- 属性详情 --> | ||||||
|     <div v-if="selectedProperty" class="property-details"> |     <div v-if="selectedProperty" class="mt-16px p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"> | ||||||
|       <div class="details-header"> |       <div class="flex items-center gap-8px mb-12px"> | ||||||
|         <Icon icon="ep:info-filled" class="details-icon" /> |         <Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-16px" /> | ||||||
|         <span class="details-title">{{ selectedProperty.name }}</span> |         <span class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ selectedProperty.name }}</span> | ||||||
|         <el-tag :type="getPropertyTypeTag(selectedProperty.dataType)" size="small"> |         <el-tag :type="getPropertyTypeTag(selectedProperty.dataType)" size="small"> | ||||||
|           {{ getPropertyTypeName(selectedProperty.dataType) }} |           {{ getPropertyTypeName(selectedProperty.dataType) }} | ||||||
|         </el-tag> |         </el-tag> | ||||||
|       </div> |       </div> | ||||||
|       <div class="details-content"> |       <div class="space-y-8px ml-24px"> | ||||||
|         <div class="detail-item"> |         <div class="flex items-start gap-8px"> | ||||||
|           <span class="detail-label">标识符:</span> |           <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">标识符:</span> | ||||||
|           <span class="detail-value">{{ selectedProperty.identifier }}</span> |           <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.identifier }}</span> | ||||||
|         </div> |         </div> | ||||||
|         <div v-if="selectedProperty.description" class="detail-item"> |         <div v-if="selectedProperty.description" class="flex items-start gap-8px"> | ||||||
|           <span class="detail-label">描述:</span> |           <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">描述:</span> | ||||||
|           <span class="detail-value">{{ selectedProperty.description }}</span> |           <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.description }}</span> | ||||||
|         </div> |         </div> | ||||||
|         <div v-if="selectedProperty.unit" class="detail-item"> |         <div v-if="selectedProperty.unit" class="flex items-start gap-8px"> | ||||||
|           <span class="detail-label">单位:</span> |           <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">单位:</span> | ||||||
|           <span class="detail-value">{{ selectedProperty.unit }}</span> |           <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.unit }}</span> | ||||||
|         </div> |         </div> | ||||||
|         <div v-if="selectedProperty.range" class="detail-item"> |         <div v-if="selectedProperty.range" class="flex items-start gap-8px"> | ||||||
|           <span class="detail-label">取值范围:</span> |           <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">取值范围:</span> | ||||||
|           <span class="detail-value">{{ selectedProperty.range }}</span> |           <span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.range }}</span> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  | @ -336,90 +336,6 @@ watch( | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| .property-selector { |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .property-option { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   width: 100%; |  | ||||||
|   padding: 4px 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-content { |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-name { |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
|   margin-bottom: 2px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-identifier { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
|   font-family: 'Courier New', monospace; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .option-meta { |  | ||||||
|   flex-shrink: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .property-details { |  | ||||||
|   margin-top: 12px; |  | ||||||
|   padding: 12px; |  | ||||||
|   background: var(--el-fill-color-light); |  | ||||||
|   border-radius: 6px; |  | ||||||
|   border: 1px solid var(--el-border-color-lighter); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .details-header { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
|   margin-bottom: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .details-icon { |  | ||||||
|   color: var(--el-color-primary); |  | ||||||
|   font-size: 14px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .details-title { |  | ||||||
|   font-size: 14px; |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .details-content { |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   gap: 4px; |  | ||||||
|   margin-left: 22px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .detail-item { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .detail-label { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-secondary); |  | ||||||
|   min-width: 60px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .detail-value { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: var(--el-text-color-primary); |  | ||||||
|   font-family: 'Courier New', monospace; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| :deep(.el-select-dropdown__item) { | :deep(.el-select-dropdown__item) { | ||||||
|   height: auto; |   height: auto; | ||||||
|   padding: 8px 20px; |   padding: 8px 20px; | ||||||
|  | @ -130,41 +130,3 @@ export interface PropertySelectorItem { | ||||||
|   event?: ThingModelEvent |   event?: ThingModelEvent | ||||||
|   service?: ThingModelService |   service?: ThingModelService | ||||||
| } | } | ||||||
| 
 |  | ||||||
| /** 数据类型枚举 */ |  | ||||||
| export enum DataTypeEnum { |  | ||||||
|   INT = 'int', |  | ||||||
|   FLOAT = 'float', |  | ||||||
|   DOUBLE = 'double', |  | ||||||
|   ENUM = 'enum', |  | ||||||
|   BOOL = 'bool', |  | ||||||
|   TEXT = 'text', |  | ||||||
|   DATE = 'date', |  | ||||||
|   STRUCT = 'struct', |  | ||||||
|   ARRAY = 'array' |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** 访问模式枚举 */ |  | ||||||
| export enum AccessModeEnum { |  | ||||||
|   READ = 'r', |  | ||||||
|   READ_write = 'rw' |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** 事件类型枚举 */ |  | ||||||
| export enum EventTypeEnum { |  | ||||||
|   INFO = 'info', |  | ||||||
|   ALERT = 'alert', |  | ||||||
|   ERROR = 'error' |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** 调用类型枚举 */ |  | ||||||
| export enum CallTypeEnum { |  | ||||||
|   ASYNC = 'async', |  | ||||||
|   SYNC = 'sync' |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** 参数方向枚举 */ |  | ||||||
| export enum ParamDirectionEnum { |  | ||||||
|   INPUT = 'input', |  | ||||||
|   OUTPUT = 'output' |  | ||||||
| } |  | ||||||
|  | @ -1,15 +1,15 @@ | ||||||
| <template> | <template> | ||||||
|   <ContentWrap> |   <ContentWrap> | ||||||
|     <!-- 页面头部 --> |     <!-- 页面头部 --> | ||||||
|     <div class="page-header"> |     <div class="flex justify-between items-start mb-20px"> | ||||||
|       <div class="header-left"> |       <div class="flex-1"> | ||||||
|         <h2 class="page-title"> |         <h2 class="flex items-center m-0 mb-8px text-24px font-600 text-[#303133]"> | ||||||
|           <Icon icon="ep:connection" class="title-icon" /> |           <Icon icon="ep:connection" class="mr-12px text-[#409eff]" /> | ||||||
|           场景联动规则 |           场景联动规则 | ||||||
|         </h2> |         </h2> | ||||||
|         <p class="page-description"> 通过配置触发条件和执行动作,实现设备间的智能联动控制 </p> |         <p class="m-0 text-[#606266] text-14px"> 通过配置触发条件和执行动作,实现设备间的智能联动控制 </p> | ||||||
|       </div> |       </div> | ||||||
|       <div class="header-right"> |       <div> | ||||||
|         <el-button type="primary" @click="handleAdd"> |         <el-button type="primary" @click="handleAdd"> | ||||||
|           <Icon icon="ep:plus" /> |           <Icon icon="ep:plus" /> | ||||||
|           新增规则 |           新增规则 | ||||||
|  | @ -18,7 +18,7 @@ | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <!-- 搜索和筛选 --> |     <!-- 搜索和筛选 --> | ||||||
|     <el-card class="search-card" shadow="never"> |     <el-card class="mb-16px" shadow="never"> | ||||||
|       <el-form |       <el-form | ||||||
|         ref="queryFormRef" |         ref="queryFormRef" | ||||||
|         :model="queryParams" |         :model="queryParams" | ||||||
|  | @ -35,7 +35,6 @@ | ||||||
|             class="!w-240px" |             class="!w-240px" | ||||||
|           /> |           /> | ||||||
|         </el-form-item> |         </el-form-item> | ||||||
|         <!-- TODO @puhui999:字典 --> |  | ||||||
|         <el-form-item label="规则状态"> |         <el-form-item label="规则状态"> | ||||||
|           <el-select |           <el-select | ||||||
|             v-model="queryParams.status" |             v-model="queryParams.status" | ||||||
|  | @ -43,8 +42,12 @@ | ||||||
|             clearable |             clearable | ||||||
|             class="!w-240px" |             class="!w-240px" | ||||||
|           > |           > | ||||||
|             <el-option label="启用" :value="0" /> |             <el-option | ||||||
|             <el-option label="禁用" :value="1" /> |               v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" | ||||||
|  |               :key="dict.value" | ||||||
|  |               :label="dict.label" | ||||||
|  |               :value="dict.value" | ||||||
|  |             /> | ||||||
|           </el-select> |           </el-select> | ||||||
|         </el-form-item> |         </el-form-item> | ||||||
|         <el-form-item> |         <el-form-item> | ||||||
|  | @ -61,56 +64,55 @@ | ||||||
|     </el-card> |     </el-card> | ||||||
| 
 | 
 | ||||||
|     <!-- 统计卡片 --> |     <!-- 统计卡片 --> | ||||||
|     <!-- TODO @puhui999:这种需要服用的 stats-content、stats-info 的属性,到底 unocss 好,还是现有的 style css 好~ --> |     <el-row :gutter="16" class="mb-16px"> | ||||||
|     <el-row :gutter="16" class="stats-row"> |  | ||||||
|       <el-col :span="6"> |       <el-col :span="6"> | ||||||
|         <el-card class="stats-card" shadow="hover"> |         <el-card class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px" shadow="hover"> | ||||||
|           <div class="stats-content"> |           <div class="flex items-center"> | ||||||
|             <div class="stats-icon total"> |             <div class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#667eea] to-[#764ba2]"> | ||||||
|               <Icon icon="ep:document" /> |               <Icon icon="ep:document" /> | ||||||
|             </div> |             </div> | ||||||
|             <div class="stats-info"> |             <div> | ||||||
|               <div class="stats-number">{{ statistics.total }}</div> |               <div class="text-24px font-600 text-[#303133] leading-none">{{ statistics.total }}</div> | ||||||
|               <div class="stats-label">总规则数</div> |               <div class="text-14px text-[#909399] mt-4px">总规则数</div> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </el-card> |         </el-card> | ||||||
|       </el-col> |       </el-col> | ||||||
|       <el-col :span="6"> |       <el-col :span="6"> | ||||||
|         <el-card class="stats-card" shadow="hover"> |         <el-card class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px" shadow="hover"> | ||||||
|           <div class="stats-content"> |           <div class="flex items-center"> | ||||||
|             <div class="stats-icon enabled"> |             <div class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#f093fb] to-[#f5576c]"> | ||||||
|               <Icon icon="ep:check" /> |               <Icon icon="ep:check" /> | ||||||
|             </div> |             </div> | ||||||
|             <div class="stats-info"> |             <div> | ||||||
|               <div class="stats-number">{{ statistics.enabled }}</div> |               <div class="text-24px font-600 text-[#303133] leading-none">{{ statistics.enabled }}</div> | ||||||
|               <div class="stats-label">启用规则</div> |               <div class="text-14px text-[#909399] mt-4px">启用规则</div> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </el-card> |         </el-card> | ||||||
|       </el-col> |       </el-col> | ||||||
|       <el-col :span="6"> |       <el-col :span="6"> | ||||||
|         <el-card class="stats-card" shadow="hover"> |         <el-card class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px" shadow="hover"> | ||||||
|           <div class="stats-content"> |           <div class="flex items-center"> | ||||||
|             <div class="stats-icon disabled"> |             <div class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#4facfe] to-[#00f2fe]"> | ||||||
|               <Icon icon="ep:close" /> |               <Icon icon="ep:close" /> | ||||||
|             </div> |             </div> | ||||||
|             <div class="stats-info"> |             <div> | ||||||
|               <div class="stats-number">{{ statistics.disabled }}</div> |               <div class="text-24px font-600 text-[#303133] leading-none">{{ statistics.disabled }}</div> | ||||||
|               <div class="stats-label">禁用规则</div> |               <div class="text-14px text-[#909399] mt-4px">禁用规则</div> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </el-card> |         </el-card> | ||||||
|       </el-col> |       </el-col> | ||||||
|       <el-col :span="6"> |       <el-col :span="6"> | ||||||
|         <el-card class="stats-card" shadow="hover"> |         <el-card class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px" shadow="hover"> | ||||||
|           <div class="stats-content"> |           <div class="flex items-center"> | ||||||
|             <div class="stats-icon active"> |             <div class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#43e97b] to-[#38f9d7]"> | ||||||
|               <Icon icon="ep:lightning" /> |               <Icon icon="ep:lightning" /> | ||||||
|             </div> |             </div> | ||||||
|             <div class="stats-info"> |             <div> | ||||||
|               <div class="stats-number">{{ statistics.triggered }}</div> |               <div class="text-24px font-600 text-[#303133] leading-none">{{ statistics.triggered }}</div> | ||||||
|               <div class="stats-label">今日触发</div> |               <div class="text-14px text-[#909399] mt-4px">今日触发</div> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </el-card> |         </el-card> | ||||||
|  | @ -118,36 +120,29 @@ | ||||||
|     </el-row> |     </el-row> | ||||||
| 
 | 
 | ||||||
|     <!-- 数据表格 --> |     <!-- 数据表格 --> | ||||||
|     <el-card class="table-card" shadow="never"> |     <el-card class="mb-20px" shadow="never"> | ||||||
|       <el-table v-loading="loading" :data="list" stripe @selection-change="handleSelectionChange"> |       <el-table v-loading="loading" :data="list" stripe @selection-change="handleSelectionChange"> | ||||||
|         <el-table-column type="selection" width="55" /> |         <el-table-column type="selection" width="55" /> | ||||||
|         <el-table-column label="规则名称" prop="name" min-width="200"> |         <el-table-column label="规则名称" prop="name" min-width="200"> | ||||||
|           <template #default="{ row }"> |           <template #default="{ row }"> | ||||||
|             <div class="rule-name-cell"> |             <div class="flex items-center gap-8px"> | ||||||
|               <span class="rule-name">{{ row.name }}</span> |               <span class="font-500 text-[#303133]">{{ row.name }}</span> | ||||||
|               <!-- TODO @puhui999:字典 --> |               <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" /> | ||||||
|               <el-tag |  | ||||||
|                 :type="row.status === 0 ? 'success' : 'danger'" |  | ||||||
|                 size="small" |  | ||||||
|                 class="status-tag" |  | ||||||
|               > |  | ||||||
|                 {{ row.status === 0 ? '启用' : '禁用' }} |  | ||||||
|               </el-tag> |  | ||||||
|             </div> |             </div> | ||||||
|             <div v-if="row.description" class="rule-description"> |             <div v-if="row.description" class="text-12px text-[#909399] mt-4px"> | ||||||
|               {{ row.description }} |               {{ row.description }} | ||||||
|             </div> |             </div> | ||||||
|           </template> |           </template> | ||||||
|         </el-table-column> |         </el-table-column> | ||||||
|         <el-table-column label="触发条件" min-width="250"> |         <el-table-column label="触发条件" min-width="250"> | ||||||
|           <template #default="{ row }"> |           <template #default="{ row }"> | ||||||
|             <div class="trigger-summary"> |             <div class="flex flex-wrap gap-4px"> | ||||||
|               <el-tag |               <el-tag | ||||||
|                 v-for="(trigger, index) in getTriggerSummary(row)" |                 v-for="(trigger, index) in getTriggerSummary(row)" | ||||||
|                 :key="index" |                 :key="index" | ||||||
|                 type="primary" |                 type="primary" | ||||||
|                 size="small" |                 size="small" | ||||||
|                 class="trigger-tag" |                 class="m-0" | ||||||
|               > |               > | ||||||
|                 {{ trigger }} |                 {{ trigger }} | ||||||
|               </el-tag> |               </el-tag> | ||||||
|  | @ -156,20 +151,19 @@ | ||||||
|         </el-table-column> |         </el-table-column> | ||||||
|         <el-table-column label="执行动作" min-width="250"> |         <el-table-column label="执行动作" min-width="250"> | ||||||
|           <template #default="{ row }"> |           <template #default="{ row }"> | ||||||
|             <div class="action-summary"> |             <div class="flex flex-wrap gap-4px"> | ||||||
|               <el-tag |               <el-tag | ||||||
|                 v-for="(action, index) in getActionSummary(row)" |                 v-for="(action, index) in getActionSummary(row)" | ||||||
|                 :key="index" |                 :key="index" | ||||||
|                 type="success" |                 type="success" | ||||||
|                 size="small" |                 size="small" | ||||||
|                 class="action-tag" |                 class="m-0" | ||||||
|               > |               > | ||||||
|                 {{ action }} |                 {{ action }} | ||||||
|               </el-tag> |               </el-tag> | ||||||
|             </div> |             </div> | ||||||
|           </template> |           </template> | ||||||
|         </el-table-column> |         </el-table-column> | ||||||
|         <!-- TODO @puhui999:貌似要新增一个字段? --> |  | ||||||
|         <el-table-column label="最近触发" prop="lastTriggeredTime" width="180"> |         <el-table-column label="最近触发" prop="lastTriggeredTime" width="180"> | ||||||
|           <template #default="{ row }"> |           <template #default="{ row }"> | ||||||
|             <span v-if="row.lastTriggeredTime"> |             <span v-if="row.lastTriggeredTime"> | ||||||
|  | @ -185,8 +179,7 @@ | ||||||
|         </el-table-column> |         </el-table-column> | ||||||
|         <el-table-column label="操作" width="200" fixed="right"> |         <el-table-column label="操作" width="200" fixed="right"> | ||||||
|           <template #default="{ row }"> |           <template #default="{ row }"> | ||||||
|             <!-- TODO @puhui999:间隙大了点 --> |             <div class="flex gap-8px"> | ||||||
|             <div class="action-buttons"> |  | ||||||
|               <el-button type="primary" link @click="handleEdit(row)"> |               <el-button type="primary" link @click="handleEdit(row)"> | ||||||
|                 <Icon icon="ep:edit" /> |                 <Icon icon="ep:edit" /> | ||||||
|                 编辑 |                 编辑 | ||||||
|  | @ -197,10 +190,9 @@ | ||||||
|                 @click="handleToggleStatus(row)" |                 @click="handleToggleStatus(row)" | ||||||
|               > |               > | ||||||
|                 <Icon :icon="row.status === 0 ? 'ep:video-pause' : 'ep:video-play'" /> |                 <Icon :icon="row.status === 0 ? 'ep:video-pause' : 'ep:video-play'" /> | ||||||
|                 <!-- TODO @puhui999:翻译,字典 --> |  | ||||||
|                 {{ row.status === 0 ? '禁用' : '启用' }} |                 {{ row.status === 0 ? '禁用' : '启用' }} | ||||||
|               </el-button> |               </el-button> | ||||||
|               <el-button type="danger" link @click="handleDelete(row)"> |               <el-button type="danger" link @click="handleDelete(row.id)"> | ||||||
|                 <Icon icon="ep:delete" /> |                 <Icon icon="ep:delete" /> | ||||||
|                 删除 |                 删除 | ||||||
|               </el-button> |               </el-button> | ||||||
|  | @ -219,11 +211,11 @@ | ||||||
|     </el-card> |     </el-card> | ||||||
| 
 | 
 | ||||||
|     <!-- 批量操作 --> |     <!-- 批量操作 --> | ||||||
|     <div v-if="selectedRows.length > 0" class="batch-actions"> |     <div v-if="selectedRows.length > 0" class="fixed bottom-20px left-1/2 transform -translate-x-1/2 z-1000"> | ||||||
|       <el-card shadow="always"> |       <el-card shadow="always"> | ||||||
|         <div class="batch-content"> |         <div class="flex items-center gap-16px"> | ||||||
|           <span class="batch-info"> 已选择 {{ selectedRows.length }} 项 </span> |           <span class="font-500 text-[#303133]"> 已选择 {{ selectedRows.length }} 项 </span> | ||||||
|           <div class="batch-buttons"> |           <div class="flex gap-8px"> | ||||||
|             <el-button @click="handleBatchEnable"> |             <el-button @click="handleBatchEnable"> | ||||||
|               <Icon icon="ep:video-play" /> |               <Icon icon="ep:video-play" /> | ||||||
|               批量启用 |               批量启用 | ||||||
|  | @ -247,17 +239,17 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|  | import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' | ||||||
| import { ContentWrap } from '@/components/ContentWrap' | import { ContentWrap } from '@/components/ContentWrap' | ||||||
| import RuleSceneForm from './components/RuleSceneForm.vue' | import RuleSceneForm from './form/RuleSceneForm.vue' | ||||||
| import { IotRuleScene } from '@/api/iot/rule/scene/scene.types' | import { IotRuleScene } from '@/api/iot/rule/scene/scene.types' | ||||||
| import { getRuleSceneSummary } from './utils/transform' |  | ||||||
| import { formatDate } from '@/utils/formatTime' | import { formatDate } from '@/utils/formatTime' | ||||||
| 
 | 
 | ||||||
| /** 场景联动规则管理页面 */ | /** 场景联动规则管理页面 */ | ||||||
| defineOptions({ name: 'IoTSceneRule' }) | defineOptions({ name: 'IoTSceneRule' }) | ||||||
| 
 | 
 | ||||||
| const message = useMessage() | const message = useMessage() // 消息弹窗 | ||||||
| // const { t } = useI18n() // TODO @puhui999:可以删除 | const { t } = useI18n() // 国际化 | ||||||
| 
 | 
 | ||||||
| // 查询参数 | // 查询参数 | ||||||
| const queryParams = reactive({ | const queryParams = reactive({ | ||||||
|  | @ -267,12 +259,11 @@ const queryParams = reactive({ | ||||||
|   status: undefined as number | undefined |   status: undefined as number | undefined | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| // 数据状态 | const loading = ref(true) // 列表的加载中 | ||||||
| // TODO @puhui999:变量名,和别的页面保持一致哈 | const list = ref<IotRuleScene[]>([]) // 列表的数据 | ||||||
| const loading = ref(true) | const total = ref(0) // 列表的总页数 | ||||||
| const list = ref<IotRuleScene[]>([]) | const selectedRows = ref<IotRuleScene[]>([]) // 选中的行数据 | ||||||
| const total = ref(0) | const queryFormRef = ref() // 搜索的表单 | ||||||
| const selectedRows = ref<IotRuleScene[]>([]) |  | ||||||
| 
 | 
 | ||||||
| // 表单状态 | // 表单状态 | ||||||
| const formVisible = ref(false) | const formVisible = ref(false) | ||||||
|  | @ -286,8 +277,96 @@ const statistics = ref({ | ||||||
|   triggered: 0 |   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 () => { | const getList = async () => { | ||||||
|   loading.value = true |   loading.value = true | ||||||
|   try { |   try { | ||||||
|  | @ -355,9 +434,7 @@ const getList = async () => { | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO @puhui999:方法注释,使用 /** */ 风格 | /** 更新统计数据 */ | ||||||
| 
 |  | ||||||
| // 更新统计数据 |  | ||||||
| const updateStatistics = () => { | const updateStatistics = () => { | ||||||
|   statistics.value = { |   statistics.value = { | ||||||
|     total: list.value.length, |     total: list.value.length, | ||||||
|  | @ -377,19 +454,20 @@ const getActionSummary = (rule: IotRuleScene) => { | ||||||
|   return getRuleSceneSummary(rule).actionSummary |   return getRuleSceneSummary(rule).actionSummary | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // 事件处理 | /** 搜索按钮操作 */ | ||||||
| const handleQuery = () => { | const handleQuery = () => { | ||||||
|   queryParams.pageNo = 1 |   queryParams.pageNo = 1 | ||||||
|   getList() |   getList() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** 重置按钮操作 */ | ||||||
| const resetQuery = () => { | const resetQuery = () => { | ||||||
|   queryParams.name = '' |   queryParams.name = '' | ||||||
|   queryParams.status = undefined |   queryParams.status = undefined | ||||||
|   handleQuery() |   handleQuery() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO @puhui999:这个要不还是使用 open 方式,只是弹出的右侧; | /** 添加/修改操作 */ | ||||||
| const handleAdd = () => { | const handleAdd = () => { | ||||||
|   currentRule.value = undefined |   currentRule.value = undefined | ||||||
|   formVisible.value = true |   formVisible.value = true | ||||||
|  | @ -400,78 +478,76 @@ const handleEdit = (row: IotRuleScene) => { | ||||||
|   formVisible.value = true |   formVisible.value = true | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO @puhui999:handleDelete、handleToggleStatus 保持和别的模块一致哇? | /** 删除按钮操作 */ | ||||||
| const handleDelete = async (row: IotRuleScene) => { | const handleDelete = async (id: number) => { | ||||||
|   try { |   try { | ||||||
|     await ElMessageBox.confirm('确定要删除这个规则吗?', '提示', { |     // 删除的二次确认 | ||||||
|       type: 'warning' |     await message.delConfirm() | ||||||
|     }) |     // 发起删除 | ||||||
|  |     // await RuleSceneApi.deleteRuleScene(id) | ||||||
| 
 | 
 | ||||||
|     // 这里应该调用删除API |     // 模拟删除操作 | ||||||
|     message.success('删除成功') |     message.success(t('common.delSuccess')) | ||||||
|     getList() |     // 刷新列表 | ||||||
|   } catch (error) { |     await getList() | ||||||
|     // 用户取消删除 |   } catch {} | ||||||
|   } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** 修改状态 */ | ||||||
| const handleToggleStatus = async (row: IotRuleScene) => { | const handleToggleStatus = async (row: IotRuleScene) => { | ||||||
|   try { |   try { | ||||||
|     const newStatus = row.status === 0 ? 1 : 0 |     // 修改状态的二次确认 | ||||||
|     const action = newStatus === 0 ? '启用' : '禁用' |     const text = row.status === 0 ? '禁用' : '启用' | ||||||
|  |     await message.confirm('确认要' + text + '"' + row.name + '"吗?') | ||||||
|  |     // 发起修改状态 | ||||||
|  |     // await RuleSceneApi.updateRuleSceneStatus(row.id, row.status === 0 ? 1 : 0) | ||||||
| 
 | 
 | ||||||
|     await ElMessageBox.confirm(`确定要${action}这个规则吗?`, '提示', { |     // 模拟状态切换 | ||||||
|       type: 'warning' |     row.status = row.status === 0 ? 1 : 0 | ||||||
|     }) |     message.success(text + '成功') | ||||||
| 
 |     // 刷新统计 | ||||||
|     // 这里应该调用状态切换API |  | ||||||
|     row.status = newStatus |  | ||||||
|     message.success(`${action}成功`) |  | ||||||
|     updateStatistics() |     updateStatistics() | ||||||
|   } catch (error) { |   } catch { | ||||||
|     // 用户取消操作 |     // 取消后,进行恢复按钮 | ||||||
|  |     row.status = row.status === 0 ? 1 : 0 | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** 多选框选中数据 */ | ||||||
| const handleSelectionChange = (selection: IotRuleScene[]) => { | const handleSelectionChange = (selection: IotRuleScene[]) => { | ||||||
|   selectedRows.value = selection |   selectedRows.value = selection | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO @puhui999:batch 操作的逻辑,要不和其它 UI 界面保持一致,或者相对一致哈; | /** 批量启用操作 */ | ||||||
| const handleBatchEnable = async () => { | const handleBatchEnable = async () => { | ||||||
|   try { |   try { | ||||||
|     await ElMessageBox.confirm(`确定要启用选中的 ${selectedRows.value.length} 个规则吗?`, '提示', { |     await message.confirm(`确定要启用选中的 ${selectedRows.value.length} 个规则吗?`) | ||||||
|       type: 'warning' |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     // 这里应该调用批量启用API |     // 这里应该调用批量启用API | ||||||
|  |     // await RuleSceneApi.updateRuleSceneStatusBatch(selectedRows.value.map(row => row.id), 0) | ||||||
|  | 
 | ||||||
|  |     // 模拟批量启用 | ||||||
|     selectedRows.value.forEach((row) => { |     selectedRows.value.forEach((row) => { | ||||||
|       row.status = 0 |       row.status = 0 | ||||||
|     }) |     }) | ||||||
| 
 |  | ||||||
|     message.success('批量启用成功') |     message.success('批量启用成功') | ||||||
|     updateStatistics() |     updateStatistics() | ||||||
|   } catch (error) { |   } catch {} | ||||||
|     // 用户取消操作 |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** 批量禁用操作 */ | ||||||
| const handleBatchDisable = async () => { | const handleBatchDisable = async () => { | ||||||
|   try { |   try { | ||||||
|     await ElMessageBox.confirm(`确定要禁用选中的 ${selectedRows.value.length} 个规则吗?`, '提示', { |     await message.confirm(`确定要禁用选中的 ${selectedRows.value.length} 个规则吗?`) | ||||||
|       type: 'warning' |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     // 这里应该调用批量禁用API |     // 这里应该调用批量禁用API | ||||||
|  |     // await RuleSceneApi.updateRuleSceneStatusBatch(selectedRows.value.map(row => row.id), 1) | ||||||
|  | 
 | ||||||
|  |     // 模拟批量禁用 | ||||||
|     selectedRows.value.forEach((row) => { |     selectedRows.value.forEach((row) => { | ||||||
|       row.status = 1 |       row.status = 1 | ||||||
|     }) |     }) | ||||||
| 
 |  | ||||||
|     message.success('批量禁用成功') |     message.success('批量禁用成功') | ||||||
|     updateStatistics() |     updateStatistics() | ||||||
|   } catch (error) { |   } catch {} | ||||||
|     // 用户取消操作 |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const handleBatchDelete = async () => { | const handleBatchDelete = async () => { | ||||||
|  | @ -494,165 +570,4 @@ onMounted(() => { | ||||||
| }) | }) | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped> |  | ||||||
| /** TODO @puhui999:看看下面的,是不是可以用 unocss 替代 */ |  | ||||||
| .page-header { |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   align-items: flex-start; |  | ||||||
|   margin-bottom: 20px; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| .header-left { |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .page-title { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   margin: 0 0 8px 0; |  | ||||||
|   font-size: 24px; |  | ||||||
|   font-weight: 600; |  | ||||||
|   color: #303133; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .title-icon { |  | ||||||
|   margin-right: 12px; |  | ||||||
|   color: #409eff; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .page-description { |  | ||||||
|   margin: 0; |  | ||||||
|   color: #606266; |  | ||||||
|   font-size: 14px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .search-card { |  | ||||||
|   margin-bottom: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .stats-row { |  | ||||||
|   margin-bottom: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .stats-card { |  | ||||||
|   cursor: pointer; |  | ||||||
|   transition: all 0.3s; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .stats-card:hover { |  | ||||||
|   transform: translateY(-2px); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .stats-content { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .stats-icon { |  | ||||||
|   width: 48px; |  | ||||||
|   height: 48px; |  | ||||||
|   border-radius: 8px; |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: center; |  | ||||||
|   font-size: 24px; |  | ||||||
|   color: white; |  | ||||||
|   margin-right: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .stats-icon.total { |  | ||||||
|   background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .stats-icon.enabled { |  | ||||||
|   background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .stats-icon.disabled { |  | ||||||
|   background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .stats-icon.active { |  | ||||||
|   background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .stats-number { |  | ||||||
|   font-size: 24px; |  | ||||||
|   font-weight: 600; |  | ||||||
|   color: #303133; |  | ||||||
|   line-height: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .stats-label { |  | ||||||
|   font-size: 14px; |  | ||||||
|   color: #909399; |  | ||||||
|   margin-top: 4px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .table-card { |  | ||||||
|   margin-bottom: 20px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .rule-name-cell { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .rule-name { |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: #303133; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .status-tag { |  | ||||||
|   flex-shrink: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .rule-description { |  | ||||||
|   font-size: 12px; |  | ||||||
|   color: #909399; |  | ||||||
|   margin-top: 4px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .trigger-summary, |  | ||||||
| .action-summary { |  | ||||||
|   display: flex; |  | ||||||
|   flex-wrap: wrap; |  | ||||||
|   gap: 4px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .trigger-tag, |  | ||||||
| .action-tag { |  | ||||||
|   margin: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .action-buttons { |  | ||||||
|   display: flex; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .batch-actions { |  | ||||||
|   position: fixed; |  | ||||||
|   bottom: 20px; |  | ||||||
|   left: 50%; |  | ||||||
|   transform: translateX(-50%); |  | ||||||
|   z-index: 1000; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .batch-content { |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   gap: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .batch-info { |  | ||||||
|   font-weight: 500; |  | ||||||
|   color: #303133; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .batch-buttons { |  | ||||||
|   display: flex; |  | ||||||
|   gap: 8px; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  |  | ||||||
|  | @ -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) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -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 } |  | ||||||
| } |  | ||||||
|  | @ -1,15 +1,11 @@ | ||||||
| /** | /** | ||||||
|  * IoT 场景联动表单验证工具函数 |  * IoT 场景联动表单验证工具函数 | ||||||
|  */ |  */ | ||||||
| import { | import { FormValidationRules, TriggerConfig, ActionConfig } from '@/api/iot/rule/scene/scene.types' | ||||||
|   FormValidationRules, |  | ||||||
|   IotRuleScene, |  | ||||||
|   TriggerConfig, |  | ||||||
|   ActionConfig |  | ||||||
| } from '@/api/iot/rule/scene/scene.types' |  | ||||||
| import { | import { | ||||||
|   IotRuleSceneTriggerTypeEnum, |   IotRuleSceneTriggerTypeEnum, | ||||||
|   IotRuleSceneActionTypeEnum |   IotRuleSceneActionTypeEnum, | ||||||
|  |   CommonStatusEnum | ||||||
| } from '@/api/iot/rule/scene/scene.types' | } from '@/api/iot/rule/scene/scene.types' | ||||||
| 
 | 
 | ||||||
| /** 基础表单验证规则 */ | /** 基础表单验证规则 */ | ||||||
|  | @ -20,7 +16,12 @@ export const getBaseValidationRules = (): FormValidationRules => ({ | ||||||
|   ], |   ], | ||||||
|   status: [ |   status: [ | ||||||
|     { required: true, message: '场景状态不能为空', trigger: 'change' }, |     { 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: [ |   description: [ | ||||||
|     { type: 'string', max: 200, message: '场景描述不能超过200个字符', trigger: 'blur' } |     { type: 'string', max: 200, message: '场景描述不能超过200个字符', trigger: 'blur' } | ||||||
|  | @ -179,86 +180,3 @@ export function validateActionConfig(action: ActionConfig): { valid: boolean; me | ||||||
| 
 | 
 | ||||||
|   return { valid: false, message: '未知的执行类型' } |   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: '告警恢复' } |  | ||||||
|   ] |  | ||||||
| } |  | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	 芋道源码
						芋道源码