commit
11e78b78a7
|
|
@ -2,91 +2,140 @@
|
||||||
* IoT 场景联动接口定义
|
* IoT 场景联动接口定义
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// TODO @puhui999:枚举挪到 views/iot/utils/constants.ts 里
|
// ========== IoT物模型TSL数据类型定义 ==========
|
||||||
// 枚举定义
|
|
||||||
const IotRuleSceneTriggerTypeEnum = {
|
|
||||||
DEVICE_STATE_UPDATE: 1, // 设备上下线变更
|
|
||||||
DEVICE_PROPERTY_POST: 2, // 物模型属性上报
|
|
||||||
DEVICE_EVENT_POST: 3, // 设备事件上报
|
|
||||||
DEVICE_SERVICE_INVOKE: 4, // 设备服务调用
|
|
||||||
TIMER: 100 // 定时触发
|
|
||||||
} as const
|
|
||||||
|
|
||||||
const IotRuleSceneActionTypeEnum = {
|
/** 物模型TSL响应数据结构 */
|
||||||
DEVICE_PROPERTY_SET: 1, // 设备属性设置,
|
export interface IotThingModelTSLRespVO {
|
||||||
DEVICE_SERVICE_INVOKE: 2, // 设备服务调用
|
productId: number
|
||||||
ALERT_TRIGGER: 100, // 告警触发
|
productKey: string
|
||||||
ALERT_RECOVER: 101 // 告警恢复
|
properties: ThingModelProperty[]
|
||||||
} as const
|
events: ThingModelEvent[]
|
||||||
|
services: ThingModelService[]
|
||||||
|
}
|
||||||
|
|
||||||
const IotDeviceMessageTypeEnum = {
|
/** 物模型属性 */
|
||||||
PROPERTY: 'property', // 属性
|
export interface ThingModelProperty {
|
||||||
SERVICE: 'service', // 服务
|
identifier: string
|
||||||
EVENT: 'event' // 事件
|
name: string
|
||||||
} as const
|
accessMode: string
|
||||||
|
required?: boolean
|
||||||
|
dataType: string
|
||||||
|
description?: string
|
||||||
|
dataSpecs?: ThingModelDataSpecs
|
||||||
|
dataSpecsList?: ThingModelDataSpecs[]
|
||||||
|
}
|
||||||
|
|
||||||
// TODO @puhui999:这个貌似可以不要?
|
/** 物模型事件 */
|
||||||
const IotDeviceMessageIdentifierEnum = {
|
export interface ThingModelEvent {
|
||||||
PROPERTY_SET: 'set', // 属性设置
|
identifier: string
|
||||||
SERVICE_INVOKE: '${identifier}' // 服务调用
|
name: string
|
||||||
} as const
|
required?: boolean
|
||||||
|
type: string
|
||||||
|
description?: string
|
||||||
|
outputParams?: ThingModelParam[]
|
||||||
|
method?: string
|
||||||
|
}
|
||||||
|
|
||||||
const IotRuleSceneTriggerConditionParameterOperatorEnum = {
|
/** 物模型服务 */
|
||||||
EQUALS: { name: '等于', value: '=' }, // 等于
|
export interface ThingModelService {
|
||||||
NOT_EQUALS: { name: '不等于', value: '!=' }, // 不等于
|
identifier: string
|
||||||
GREATER_THAN: { name: '大于', value: '>' }, // 大于
|
name: string
|
||||||
GREATER_THAN_OR_EQUALS: { name: '大于等于', value: '>=' }, // 大于等于
|
required?: boolean
|
||||||
LESS_THAN: { name: '小于', value: '<' }, // 小于
|
callType: string
|
||||||
LESS_THAN_OR_EQUALS: { name: '小于等于', value: '<=' }, // 小于等于
|
description?: string
|
||||||
IN: { name: '在...之中', value: 'in' }, // 在...之中
|
inputParams?: ThingModelParam[]
|
||||||
NOT_IN: { name: '不在...之中', value: 'not in' }, // 不在...之中
|
outputParams?: ThingModelParam[]
|
||||||
BETWEEN: { name: '在...之间', value: 'between' }, // 在...之间
|
method?: string
|
||||||
NOT_BETWEEN: { name: '不在...之间', value: 'not between' }, // 不在...之间
|
}
|
||||||
LIKE: { name: '字符串匹配', value: 'like' }, // 字符串匹配
|
|
||||||
NOT_NULL: { name: '非空', value: 'not null' } // 非空
|
|
||||||
} as const
|
|
||||||
|
|
||||||
// 条件类型枚举
|
/** 物模型参数 */
|
||||||
const IotRuleSceneTriggerConditionTypeEnum = {
|
export interface ThingModelParam {
|
||||||
DEVICE_STATUS: 1, // 设备状态
|
identifier: string
|
||||||
DEVICE_PROPERTY: 2, // 设备属性
|
name: string
|
||||||
CURRENT_TIME: 3 // 当前时间
|
direction: string
|
||||||
} as const
|
paraOrder?: number
|
||||||
|
dataType: string
|
||||||
|
dataSpecs?: ThingModelDataSpecs
|
||||||
|
dataSpecsList?: ThingModelDataSpecs[]
|
||||||
|
}
|
||||||
|
|
||||||
// 时间运算符枚举
|
/** 数值型数据规范 */
|
||||||
const IotRuleSceneTriggerTimeOperatorEnum = {
|
export interface ThingModelNumericDataSpec {
|
||||||
BEFORE_TIME: { name: '在时间之前', value: 'before_time' }, // 在时间之前
|
dataType: 'int' | 'float' | 'double'
|
||||||
AFTER_TIME: { name: '在时间之后', value: 'after_time' }, // 在时间之后
|
max: string
|
||||||
BETWEEN_TIME: { name: '在时间之间', value: 'between_time' }, // 在时间之间
|
min: string
|
||||||
AT_TIME: { name: '在指定时间', value: 'at_time' }, // 在指定时间
|
step: string
|
||||||
BEFORE_TODAY: { name: '在今日之前', value: 'before_today' }, // 在今日之前
|
precise?: string
|
||||||
AFTER_TODAY: { name: '在今日之后', value: 'after_today' }, // 在今日之后
|
defaultValue?: string
|
||||||
TODAY: { name: '在今日之间', value: 'today' } // 在今日之间
|
unit?: string
|
||||||
} as const
|
unitName?: string
|
||||||
|
}
|
||||||
|
|
||||||
// TODO @puhui999:下面 IotAlertConfigReceiveTypeEnum、DeviceStateEnum 没用到,貌似可以删除下?
|
/** 布尔/枚举型数据规范 */
|
||||||
const IotAlertConfigReceiveTypeEnum = {
|
export interface ThingModelBoolOrEnumDataSpecs {
|
||||||
SMS: 1, // 短信
|
dataType: 'bool' | 'enum'
|
||||||
MAIL: 2, // 邮箱
|
name: string
|
||||||
NOTIFY: 3 // 通知
|
value: number
|
||||||
} as const
|
}
|
||||||
|
|
||||||
// 设备状态枚举
|
/** 文本/时间型数据规范 */
|
||||||
const DeviceStateEnum = {
|
export interface ThingModelDateOrTextDataSpecs {
|
||||||
INACTIVE: 0, // 未激活
|
dataType: 'text' | 'date'
|
||||||
ONLINE: 1, // 在线
|
length?: number
|
||||||
OFFLINE: 2 // 离线
|
defaultValue?: string
|
||||||
} as const
|
}
|
||||||
|
|
||||||
// TODO @puhui999:这个全局已经有啦
|
/** 数组型数据规范 */
|
||||||
// 通用状态枚举
|
export interface ThingModelArrayDataSpecs {
|
||||||
const CommonStatusEnum = {
|
dataType: 'array'
|
||||||
ENABLE: 0, // 开启
|
size: number
|
||||||
DISABLE: 1 // 关闭
|
childDataType: string
|
||||||
} as const
|
dataSpecsList?: ThingModelDataSpecs[]
|
||||||
|
}
|
||||||
|
|
||||||
// 基础接口
|
/** 结构体型数据规范 */
|
||||||
// TODO @puhui999:这个貌似可以不要?
|
export interface ThingModelStructDataSpecs {
|
||||||
|
dataType: 'struct'
|
||||||
|
identifier: string
|
||||||
|
name: string
|
||||||
|
accessMode: string
|
||||||
|
required?: boolean
|
||||||
|
childDataType: string
|
||||||
|
dataSpecs?: ThingModelDataSpecs
|
||||||
|
dataSpecsList?: ThingModelDataSpecs[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 数据规范联合类型 */
|
||||||
|
export type ThingModelDataSpecs =
|
||||||
|
| ThingModelNumericDataSpec
|
||||||
|
| ThingModelBoolOrEnumDataSpecs
|
||||||
|
| ThingModelDateOrTextDataSpecs
|
||||||
|
| ThingModelArrayDataSpecs
|
||||||
|
| ThingModelStructDataSpecs
|
||||||
|
|
||||||
|
/** 属性选择器内部使用的统一数据结构 */
|
||||||
|
export interface PropertySelectorItem {
|
||||||
|
identifier: string
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
dataType: string
|
||||||
|
type: number // IoTThingModelTypeEnum
|
||||||
|
accessMode?: string
|
||||||
|
required?: boolean
|
||||||
|
unit?: string
|
||||||
|
range?: string
|
||||||
|
eventType?: string
|
||||||
|
callType?: string
|
||||||
|
inputParams?: ThingModelParam[]
|
||||||
|
outputParams?: ThingModelParam[]
|
||||||
|
property?: ThingModelProperty
|
||||||
|
event?: ThingModelEvent
|
||||||
|
service?: ThingModelService
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 场景联动规则相关接口定义 ==========
|
||||||
|
|
||||||
|
// 基础接口(如果项目中有全局的 BaseDO,可以使用全局的)
|
||||||
interface TenantBaseDO {
|
interface TenantBaseDO {
|
||||||
createTime?: Date // 创建时间
|
createTime?: Date // 创建时间
|
||||||
updateTime?: Date // 更新时间
|
updateTime?: Date // 更新时间
|
||||||
|
|
@ -138,67 +187,48 @@ interface ActionConfig {
|
||||||
alertConfigId?: number // 告警配置ID(告警恢复时必填)
|
alertConfigId?: number // 告警配置ID(告警恢复时必填)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表单数据接口
|
// 表单数据接口 - 直接对应后端 DO 结构
|
||||||
interface RuleSceneFormData {
|
interface RuleSceneFormData {
|
||||||
id?: number
|
id?: number
|
||||||
name: string
|
name: string
|
||||||
description?: string
|
description?: string
|
||||||
status: number
|
status: number
|
||||||
trigger: TriggerFormData
|
triggers: TriggerFormData[] // 支持多个触发器
|
||||||
actions: ActionFormData[]
|
actions: ActionFormData[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 触发器表单数据 - 直接对应 TriggerDO
|
||||||
interface TriggerFormData {
|
interface TriggerFormData {
|
||||||
type: number
|
type: number // 触发类型
|
||||||
productId?: number
|
productId?: number // 产品编号
|
||||||
deviceId?: number
|
deviceId?: number // 设备编号
|
||||||
identifier?: string
|
identifier?: string // 物模型标识符
|
||||||
operator?: string
|
operator?: string // 操作符
|
||||||
value?: string
|
value?: string // 参数值
|
||||||
cronExpression?: string
|
cronExpression?: string // CRON 表达式
|
||||||
// 新的条件结构
|
conditionGroups?: TriggerConditionFormData[][] // 条件组(二维数组)
|
||||||
mainCondition?: ConditionFormData // 主条件(必须满足)
|
|
||||||
conditionGroup?: ConditionGroupContainerFormData // 条件组容器(可选,与主条件为且关系)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActionFormData {
|
// 触发条件表单数据 - 直接对应 TriggerConditionDO
|
||||||
type: number
|
interface TriggerConditionFormData {
|
||||||
productId?: number
|
|
||||||
deviceId?: number
|
|
||||||
params?: Record<string, any>
|
|
||||||
alertConfigId?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// 条件组容器(包含多个子条件组,子条件组间为或关系)
|
|
||||||
interface ConditionGroupContainerFormData {
|
|
||||||
subGroups: SubConditionGroupFormData[] // 子条件组数组,子条件组间为或关系
|
|
||||||
}
|
|
||||||
|
|
||||||
// 子条件组(内部条件为且关系)
|
|
||||||
interface SubConditionGroupFormData {
|
|
||||||
conditions: ConditionFormData[] // 条件数组,条件间为且关系
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保留原有接口用于兼容性
|
|
||||||
interface ConditionGroupFormData {
|
|
||||||
conditions: ConditionFormData[]
|
|
||||||
// 注意:条件组内部的条件固定为"且"关系,条件组之间固定为"或"关系
|
|
||||||
// logicOperator 字段保留用于兼容性,但在UI中固定为 'AND'
|
|
||||||
logicOperator: 'AND' | 'OR'
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConditionFormData {
|
|
||||||
type: number // 条件类型:1-设备状态,2-设备属性,3-当前时间
|
type: number // 条件类型:1-设备状态,2-设备属性,3-当前时间
|
||||||
productId?: number // 产品ID(设备状态和设备属性时必填)
|
productId?: number // 产品编号
|
||||||
deviceId?: number // 设备ID(设备状态和设备属性时必填)
|
deviceId?: number // 设备编号
|
||||||
identifier?: string // 标识符(设备属性时必填)
|
identifier?: string // 标识符
|
||||||
operator: string // 操作符
|
operator: string // 操作符
|
||||||
param: string // 参数值
|
param: string // 参数值
|
||||||
timeValue?: string // 时间值(当前时间条件时使用)
|
|
||||||
timeValue2?: string // 第二个时间值(时间范围条件时使用)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主接口
|
// 执行器表单数据 - 直接对应 ActionDO
|
||||||
|
interface ActionFormData {
|
||||||
|
type: number // 执行类型
|
||||||
|
productId?: number // 产品编号
|
||||||
|
deviceId?: number // 设备编号
|
||||||
|
params?: Record<string, any> // 请求参数
|
||||||
|
alertConfigId?: number // 告警配置编号
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主接口 - 原有的 API 接口格式(保持兼容性)
|
||||||
interface IotRuleScene extends TenantBaseDO {
|
interface IotRuleScene extends TenantBaseDO {
|
||||||
id?: number // 场景编号(新增时为空)
|
id?: number // 场景编号(新增时为空)
|
||||||
name: string // 场景名称(必填)
|
name: string // 场景名称(必填)
|
||||||
|
|
@ -208,14 +238,46 @@ interface IotRuleScene extends TenantBaseDO {
|
||||||
actions: ActionConfig[] // 执行器数组(必填,至少一个)
|
actions: ActionConfig[] // 执行器数组(必填,至少一个)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 工具类型 - 从枚举中提取类型
|
// 后端 DO 接口 - 匹配后端数据结构
|
||||||
export type TriggerType =
|
interface IotRuleSceneDO {
|
||||||
(typeof IotRuleSceneTriggerTypeEnum)[keyof typeof IotRuleSceneTriggerTypeEnum]
|
id?: number // 场景编号
|
||||||
export type ActionType =
|
name: string // 场景名称
|
||||||
(typeof IotRuleSceneActionTypeEnum)[keyof typeof IotRuleSceneActionTypeEnum]
|
description?: string // 场景描述
|
||||||
export type MessageType = (typeof IotDeviceMessageTypeEnum)[keyof typeof IotDeviceMessageTypeEnum]
|
status: number // 场景状态:0-开启,1-关闭
|
||||||
export type OperatorType =
|
triggers: TriggerDO[] // 触发器数组
|
||||||
(typeof IotRuleSceneTriggerConditionParameterOperatorEnum)[keyof typeof IotRuleSceneTriggerConditionParameterOperatorEnum]['value']
|
actions: ActionDO[] // 执行器数组
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发器 DO 结构
|
||||||
|
interface TriggerDO {
|
||||||
|
type: number // 触发类型
|
||||||
|
productId?: number // 产品编号
|
||||||
|
deviceId?: number // 设备编号
|
||||||
|
identifier?: string // 物模型标识符
|
||||||
|
operator?: string // 操作符
|
||||||
|
value?: string // 参数值
|
||||||
|
cronExpression?: string // CRON 表达式
|
||||||
|
conditionGroups?: TriggerConditionDO[][] // 条件组(二维数组)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发条件 DO 结构
|
||||||
|
interface TriggerConditionDO {
|
||||||
|
type: number // 条件类型
|
||||||
|
productId?: number // 产品编号
|
||||||
|
deviceId?: number // 设备编号
|
||||||
|
identifier?: string // 标识符
|
||||||
|
operator: string // 操作符
|
||||||
|
param: string // 参数
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行器 DO 结构
|
||||||
|
interface ActionDO {
|
||||||
|
type: number // 执行类型
|
||||||
|
productId?: number // 产品编号
|
||||||
|
deviceId?: number // 设备编号
|
||||||
|
params?: Record<string, any> // 请求参数
|
||||||
|
alertConfigId?: number // 告警配置编号
|
||||||
|
}
|
||||||
|
|
||||||
// 表单验证规则类型
|
// 表单验证规则类型
|
||||||
interface ValidationRule {
|
interface ValidationRule {
|
||||||
|
|
@ -234,6 +296,10 @@ interface FormValidationRules {
|
||||||
|
|
||||||
export {
|
export {
|
||||||
IotRuleScene,
|
IotRuleScene,
|
||||||
|
IotRuleSceneDO,
|
||||||
|
TriggerDO,
|
||||||
|
TriggerConditionDO,
|
||||||
|
ActionDO,
|
||||||
TriggerConfig,
|
TriggerConfig,
|
||||||
TriggerCondition,
|
TriggerCondition,
|
||||||
TriggerConditionParameter,
|
TriggerConditionParameter,
|
||||||
|
|
@ -241,25 +307,8 @@ export {
|
||||||
ActionDeviceControl,
|
ActionDeviceControl,
|
||||||
RuleSceneFormData,
|
RuleSceneFormData,
|
||||||
TriggerFormData,
|
TriggerFormData,
|
||||||
|
TriggerConditionFormData,
|
||||||
ActionFormData,
|
ActionFormData,
|
||||||
ConditionGroupFormData,
|
|
||||||
ConditionGroupContainerFormData,
|
|
||||||
SubConditionGroupFormData,
|
|
||||||
ConditionFormData,
|
|
||||||
IotRuleSceneTriggerTypeEnum,
|
|
||||||
IotRuleSceneActionTypeEnum,
|
|
||||||
IotDeviceMessageTypeEnum,
|
|
||||||
IotDeviceMessageIdentifierEnum,
|
|
||||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
|
||||||
IotRuleSceneTriggerConditionTypeEnum,
|
|
||||||
IotRuleSceneTriggerTimeOperatorEnum,
|
|
||||||
IotAlertConfigReceiveTypeEnum,
|
|
||||||
DeviceStateEnum,
|
|
||||||
CommonStatusEnum,
|
|
||||||
TriggerType,
|
|
||||||
ActionType,
|
|
||||||
MessageType,
|
|
||||||
OperatorType,
|
|
||||||
ValidationRule,
|
ValidationRule,
|
||||||
FormValidationRules
|
FormValidationRules
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- TODO @puhui999:这个抽屉的高度太高了?! -->
|
<!-- 场景联动规则表单抽屉 - 优化高度和布局 -->
|
||||||
<el-drawer
|
<el-drawer
|
||||||
v-model="drawerVisible"
|
v-model="drawerVisible"
|
||||||
:title="drawerTitle"
|
:title="drawerTitle"
|
||||||
|
|
@ -8,29 +8,28 @@
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:close-on-press-escape="false"
|
:close-on-press-escape="false"
|
||||||
@close="handleClose"
|
@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="110px">
|
||||||
<el-form
|
<!-- 基础信息配置 -->
|
||||||
ref="formRef"
|
<BasicInfoSection v-model="formData" :rules="formRules" />
|
||||||
: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" />
|
<TriggerSection v-model:triggers="formData.triggers" @validate="handleTriggerValidate" />
|
||||||
|
|
||||||
<!-- 执行器配置 -->
|
<!-- 执行器配置 -->
|
||||||
<ActionSection v-model:actions="formData.actions" @validate="handleActionValidate" />
|
<ActionSection v-model:actions="formData.actions" @validate="handleActionValidate" />
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button :disabled="submitLoading" type="primary" @click="handleSubmit">确 定</el-button>
|
<div class="drawer-footer">
|
||||||
<el-button @click="handleClose">取 消</el-button>
|
<el-button :disabled="submitLoading" type="primary" @click="handleSubmit">
|
||||||
|
<Icon icon="ep:check" />
|
||||||
|
确 定
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleClose">
|
||||||
|
<Icon icon="ep:close" />
|
||||||
|
取 消
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -40,116 +39,106 @@ import { useVModel } from '@vueuse/core'
|
||||||
import BasicInfoSection from './sections/BasicInfoSection.vue'
|
import BasicInfoSection from './sections/BasicInfoSection.vue'
|
||||||
import TriggerSection from './sections/TriggerSection.vue'
|
import TriggerSection from './sections/TriggerSection.vue'
|
||||||
import ActionSection from './sections/ActionSection.vue'
|
import ActionSection from './sections/ActionSection.vue'
|
||||||
import {
|
import { IotRuleSceneDO, RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
|
||||||
RuleSceneFormData,
|
import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
|
||||||
IotRuleScene,
|
|
||||||
IotRuleSceneActionTypeEnum,
|
|
||||||
IotRuleSceneTriggerTypeEnum,
|
|
||||||
CommonStatusEnum
|
|
||||||
} from '@/api/iot/rule/scene/scene.types'
|
|
||||||
import { getBaseValidationRules } from '../utils/validation'
|
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { generateUUID } from '@/utils'
|
import { generateUUID } from '@/utils'
|
||||||
|
|
||||||
|
// 导入全局的 CommonStatusEnum
|
||||||
|
const CommonStatusEnum = {
|
||||||
|
ENABLE: 0, // 开启
|
||||||
|
DISABLE: 1 // 关闭
|
||||||
|
} as const
|
||||||
|
|
||||||
/** IoT 场景联动规则表单 - 主表单组件 */
|
/** IoT 场景联动规则表单 - 主表单组件 */
|
||||||
defineOptions({ name: 'RuleSceneForm' })
|
defineOptions({ name: 'RuleSceneForm' })
|
||||||
|
|
||||||
// TODO @puhui999:是不是融合到 props
|
/** 组件属性定义 */
|
||||||
interface Props {
|
const props = defineProps<{
|
||||||
|
/** 抽屉显示状态 */
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
ruleScene?: IotRuleScene
|
}>()
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @puhui999:Emits 是不是融合到 emit
|
/** 组件事件定义 */
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: boolean): void
|
(e: 'update:modelValue', value: boolean): void
|
||||||
(e: 'success'): void
|
(e: 'success'): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const drawerVisible = useVModel(props, 'modelValue', emit) // 是否可见
|
const drawerVisible = useVModel(props, 'modelValue', emit) // 是否可见
|
||||||
|
|
||||||
// TODO @puhui999:使用 /** 注释风格哈 */
|
|
||||||
|
|
||||||
/** 创建默认的表单数据 */
|
/** 创建默认的表单数据 */
|
||||||
const createDefaultFormData = (): RuleSceneFormData => {
|
const createDefaultFormData = (): RuleSceneFormData => {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
status: CommonStatusEnum.ENABLE, // 默认启用状态
|
status: CommonStatusEnum.ENABLE, // 默认启用状态
|
||||||
trigger: {
|
triggers: [
|
||||||
type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
|
{
|
||||||
productId: undefined,
|
type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
|
||||||
deviceId: undefined,
|
productId: undefined,
|
||||||
identifier: undefined,
|
deviceId: undefined,
|
||||||
operator: undefined,
|
identifier: undefined,
|
||||||
value: undefined,
|
operator: undefined,
|
||||||
cronExpression: undefined,
|
value: undefined,
|
||||||
mainCondition: undefined,
|
cronExpression: undefined,
|
||||||
conditionGroup: undefined
|
conditionGroups: [] // 空的条件组数组
|
||||||
},
|
}
|
||||||
|
],
|
||||||
actions: []
|
actions: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @puhui999:使用 convertFormToVO;下面也是类似哈;
|
|
||||||
/**
|
/**
|
||||||
* 将表单数据转换为 API 请求格式
|
* 将表单数据转换为后端 DO 格式
|
||||||
|
* 由于数据结构已对齐,转换变得非常简单
|
||||||
*/
|
*/
|
||||||
const transformFormToApi = (formData: RuleSceneFormData): IotRuleScene => {
|
const convertFormToVO = (formData: RuleSceneFormData): IotRuleSceneDO => {
|
||||||
return {
|
return {
|
||||||
id: formData.id,
|
id: formData.id,
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
description: formData.description,
|
description: formData.description,
|
||||||
status: Number(formData.status),
|
status: Number(formData.status),
|
||||||
triggers: [
|
triggers: formData.triggers.map((trigger) => ({
|
||||||
{
|
type: trigger.type,
|
||||||
type: formData.trigger.type,
|
productId: trigger.productId,
|
||||||
productKey: formData.trigger.productId
|
deviceId: trigger.deviceId,
|
||||||
? `product_${formData.trigger.productId}`
|
identifier: trigger.identifier,
|
||||||
: undefined,
|
operator: trigger.operator,
|
||||||
deviceNames: formData.trigger.deviceId
|
value: trigger.value,
|
||||||
? [`device_${formData.trigger.deviceId}`]
|
cronExpression: trigger.cronExpression,
|
||||||
: undefined,
|
conditionGroups: trigger.conditionGroups || []
|
||||||
cronExpression: formData.trigger.cronExpression,
|
})),
|
||||||
conditions: [] // TODO: 实现新的条件转换逻辑
|
|
||||||
}
|
|
||||||
],
|
|
||||||
actions:
|
actions:
|
||||||
formData.actions?.map((action) => ({
|
formData.actions?.map((action) => ({
|
||||||
type: action.type,
|
type: action.type,
|
||||||
alertConfigId: action.alertConfigId,
|
productId: action.productId,
|
||||||
deviceControl:
|
deviceId: action.deviceId,
|
||||||
action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
|
params: action.params,
|
||||||
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
|
alertConfigId: action.alertConfigId
|
||||||
? {
|
|
||||||
productKey: action.productId ? `product_${action.productId}` : '',
|
|
||||||
deviceNames: action.deviceId ? [`device_${action.deviceId}`] : [],
|
|
||||||
type: 'property',
|
|
||||||
identifier: 'set',
|
|
||||||
params: action.params || {}
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
})) || []
|
})) || []
|
||||||
} as IotRuleScene
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 API 响应数据转换为表单格式
|
* 将后端 DO 数据转换为表单格式
|
||||||
|
* 由于数据结构已对齐,转换变得非常简单
|
||||||
*/
|
*/
|
||||||
const transformApiToForm = (apiData: IotRuleScene): RuleSceneFormData => {
|
const convertVOToForm = (apiData: IotRuleSceneDO): RuleSceneFormData => {
|
||||||
const firstTrigger = apiData.triggers?.[0]
|
// 转换所有触发器
|
||||||
return {
|
const triggers = apiData.triggers?.length
|
||||||
...apiData,
|
? apiData.triggers.map((trigger: any) => ({
|
||||||
status: Number(apiData.status), // 确保状态为数字类型
|
type: Number(trigger.type),
|
||||||
trigger: firstTrigger
|
productId: trigger.productId,
|
||||||
? {
|
deviceId: trigger.deviceId,
|
||||||
...firstTrigger,
|
identifier: trigger.identifier,
|
||||||
type: Number(firstTrigger.type)
|
operator: trigger.operator,
|
||||||
}
|
value: trigger.value,
|
||||||
: {
|
cronExpression: trigger.cronExpression,
|
||||||
|
conditionGroups: trigger.conditionGroups || []
|
||||||
|
}))
|
||||||
|
: [
|
||||||
|
{
|
||||||
type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
|
type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
|
||||||
productId: undefined,
|
productId: undefined,
|
||||||
deviceId: undefined,
|
deviceId: undefined,
|
||||||
|
|
@ -157,13 +146,23 @@ const transformApiToForm = (apiData: IotRuleScene): RuleSceneFormData => {
|
||||||
operator: undefined,
|
operator: undefined,
|
||||||
value: undefined,
|
value: undefined,
|
||||||
cronExpression: undefined,
|
cronExpression: undefined,
|
||||||
mainCondition: undefined,
|
conditionGroups: []
|
||||||
conditionGroup: undefined
|
}
|
||||||
},
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: apiData.id,
|
||||||
|
name: apiData.name,
|
||||||
|
description: apiData.description,
|
||||||
|
status: Number(apiData.status),
|
||||||
|
triggers,
|
||||||
actions:
|
actions:
|
||||||
apiData.actions?.map((action) => ({
|
apiData.actions?.map((action: any) => ({
|
||||||
...action,
|
|
||||||
type: Number(action.type),
|
type: Number(action.type),
|
||||||
|
productId: action.productId,
|
||||||
|
deviceId: action.deviceId,
|
||||||
|
params: action.params || {},
|
||||||
|
alertConfigId: action.alertConfigId,
|
||||||
// 为每个执行器添加唯一标识符,解决组件索引重用问题
|
// 为每个执行器添加唯一标识符,解决组件索引重用问题
|
||||||
key: generateUUID()
|
key: generateUUID()
|
||||||
})) || []
|
})) || []
|
||||||
|
|
@ -173,7 +172,33 @@ const transformApiToForm = (apiData: IotRuleScene): RuleSceneFormData => {
|
||||||
// 表单数据和状态
|
// 表单数据和状态
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const formData = ref<RuleSceneFormData>(createDefaultFormData())
|
const formData = ref<RuleSceneFormData>(createDefaultFormData())
|
||||||
const formRules = getBaseValidationRules()
|
const formRules = reactive({
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '场景名称不能为空', trigger: 'blur' },
|
||||||
|
{ type: 'string', min: 1, max: 50, message: '场景名称长度应在1-50个字符之间', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
status: [
|
||||||
|
{ required: true, message: '场景状态不能为空', trigger: 'change' },
|
||||||
|
{
|
||||||
|
type: 'enum',
|
||||||
|
enum: [CommonStatusEnum.ENABLE, CommonStatusEnum.DISABLE],
|
||||||
|
message: '状态值必须为启用或禁用',
|
||||||
|
trigger: 'change'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
description: [
|
||||||
|
{ type: 'string', max: 200, message: '场景描述不能超过200个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
triggers: [
|
||||||
|
{ required: true, message: '触发器数组不能为空', trigger: 'change' },
|
||||||
|
{ type: 'array', min: 1, message: '至少需要一个触发器', trigger: 'change' }
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{ required: true, message: '执行器数组不能为空', trigger: 'change' },
|
||||||
|
{ type: 'array', min: 1, message: '至少需要一个执行器', trigger: 'change' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
|
|
||||||
// 验证状态
|
// 验证状态
|
||||||
|
|
@ -181,7 +206,7 @@ const triggerValidation = ref({ valid: true, message: '' })
|
||||||
const actionValidation = ref({ valid: true, message: '' })
|
const actionValidation = ref({ valid: true, message: '' })
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const isEdit = computed(() => !!props.ruleScene?.id)
|
const isEdit = ref(false)
|
||||||
const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' : '新增场景联动规则'))
|
const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' : '新增场景联动规则'))
|
||||||
|
|
||||||
// 事件处理
|
// 事件处理
|
||||||
|
|
@ -211,12 +236,23 @@ const handleSubmit = async () => {
|
||||||
// 提交请求
|
// 提交请求
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
console.log(formData.value)
|
||||||
// 转换数据格式
|
// 转换数据格式
|
||||||
const apiData = transformFormToApi(formData.value)
|
const apiData = convertFormToVO(formData.value)
|
||||||
|
if (true) {
|
||||||
// 这里应该调用API保存数据
|
console.log('转换后', apiData)
|
||||||
// TODO @puhui999:貌似还没接入
|
return
|
||||||
console.log('提交数据:', apiData)
|
}
|
||||||
|
// 调用API保存数据
|
||||||
|
if (isEdit.value) {
|
||||||
|
// 更新场景联动规则
|
||||||
|
// await RuleSceneApi.updateRuleScene(apiData)
|
||||||
|
console.log('更新数据:', apiData)
|
||||||
|
} else {
|
||||||
|
// 创建场景联动规则
|
||||||
|
// await RuleSceneApi.createRuleScene(apiData)
|
||||||
|
console.log('创建数据:', apiData)
|
||||||
|
}
|
||||||
|
|
||||||
// 模拟API调用
|
// 模拟API调用
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
|
@ -224,6 +260,9 @@ const handleSubmit = async () => {
|
||||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||||
drawerVisible.value = false
|
drawerVisible.value = false
|
||||||
emit('success')
|
emit('success')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存失败:', error)
|
||||||
|
ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
|
||||||
} finally {
|
} finally {
|
||||||
submitLoading.value = false
|
submitLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -233,93 +272,30 @@ const handleClose = () => {
|
||||||
drawerVisible.value = false
|
drawerVisible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化表单数据
|
/** 初始化表单数据 */
|
||||||
const initFormData = () => {
|
const initFormData = () => {
|
||||||
if (props.ruleScene) {
|
// TODO @puhui999: 编辑的情况后面实现
|
||||||
formData.value = transformApiToForm(props.ruleScene)
|
formData.value = createDefaultFormData()
|
||||||
} else {
|
|
||||||
formData.value = createDefaultFormData()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听抽屉显示
|
// 监听抽屉显示
|
||||||
watch(drawerVisible, (visible) => {
|
watch(drawerVisible, (visible) => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
initFormData()
|
initFormData()
|
||||||
nextTick(() => {
|
// TODO @puhui999: 重置表单的情况
|
||||||
formRef.value?.clearValidate()
|
// nextTick(() => {
|
||||||
})
|
// formRef.value?.clearValidate()
|
||||||
|
// })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听 props 变化
|
// 监听 props 变化
|
||||||
watch(
|
// watch(
|
||||||
() => props.ruleScene,
|
// () => props.ruleScene,
|
||||||
() => {
|
// () => {
|
||||||
if (drawerVisible.value) {
|
// if (drawerVisible.value) {
|
||||||
initFormData()
|
// initFormData()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
)
|
// )
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- TODO @puhui999:看看下面样式,哪些是必要添加的 -->
|
|
||||||
<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>
|
|
||||||
|
|
|
||||||
|
|
@ -89,24 +89,6 @@
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<!-- 条件预览 -->
|
|
||||||
<!-- TODO puhui999:可以去掉。。。因为表单选择了,可以看懂的呀。 -->
|
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<!-- 当前时间条件配置 -->
|
<!-- 当前时间条件配置 -->
|
||||||
|
|
@ -139,26 +121,24 @@ import DeviceSelector from '../selectors/DeviceSelector.vue'
|
||||||
import PropertySelector from '../selectors/PropertySelector.vue'
|
import PropertySelector from '../selectors/PropertySelector.vue'
|
||||||
import OperatorSelector from '../selectors/OperatorSelector.vue'
|
import OperatorSelector from '../selectors/OperatorSelector.vue'
|
||||||
import ValueInput from '../inputs/ValueInput.vue'
|
import ValueInput from '../inputs/ValueInput.vue'
|
||||||
|
import { TriggerConditionFormData } from '@/api/iot/rule/scene/scene.types'
|
||||||
import {
|
import {
|
||||||
ConditionFormData,
|
IotRuleSceneTriggerConditionTypeEnum,
|
||||||
IotRuleSceneTriggerConditionTypeEnum
|
IotRuleSceneTriggerConditionParameterOperatorEnum
|
||||||
} from '@/api/iot/rule/scene/scene.types'
|
} from '@/views/iot/utils/constants'
|
||||||
|
|
||||||
/** 单个条件配置组件 */
|
/** 单个条件配置组件 */
|
||||||
defineOptions({ name: 'ConditionConfig' })
|
defineOptions({ name: 'ConditionConfig' })
|
||||||
|
|
||||||
interface Props {
|
const props = defineProps<{
|
||||||
modelValue: ConditionFormData
|
modelValue: TriggerConditionFormData
|
||||||
triggerType: number
|
triggerType: number
|
||||||
}
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: ConditionFormData): void
|
(e: 'update:modelValue', value: TriggerConditionFormData): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const condition = useVModel(props, 'modelValue', emit)
|
const condition = useVModel(props, 'modelValue', emit)
|
||||||
|
|
||||||
|
|
@ -172,41 +152,13 @@ const validationMessage = ref('')
|
||||||
const isValid = ref(true)
|
const isValid = ref(true)
|
||||||
const valueValidation = ref({ valid: true, message: '' })
|
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) => {
|
const updateConditionField = (field: keyof TriggerConditionFormData, value: any) => {
|
||||||
;(condition.value as any)[field] = value
|
;(condition.value as any)[field] = value
|
||||||
emit('update:modelValue', condition.value)
|
emit('update:modelValue', condition.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCondition = (newCondition: ConditionFormData) => {
|
const updateCondition = (newCondition: TriggerConditionFormData) => {
|
||||||
condition.value = newCondition
|
condition.value = newCondition
|
||||||
emit('update:modelValue', condition.value)
|
emit('update:modelValue', condition.value)
|
||||||
}
|
}
|
||||||
|
|
@ -215,19 +167,29 @@ const handleConditionTypeChange = (type: number) => {
|
||||||
// 清理不相关的字段
|
// 清理不相关的字段
|
||||||
if (type === ConditionTypeEnum.DEVICE_STATUS) {
|
if (type === ConditionTypeEnum.DEVICE_STATUS) {
|
||||||
condition.value.identifier = undefined
|
condition.value.identifier = undefined
|
||||||
condition.value.timeValue = undefined
|
// 清理时间相关字段(如果存在)
|
||||||
condition.value.timeValue2 = undefined
|
if ('timeValue' in condition.value) {
|
||||||
|
delete (condition.value as any).timeValue
|
||||||
|
}
|
||||||
|
if ('timeValue2' in condition.value) {
|
||||||
|
delete (condition.value as any).timeValue2
|
||||||
|
}
|
||||||
} else if (type === ConditionTypeEnum.CURRENT_TIME) {
|
} else if (type === ConditionTypeEnum.CURRENT_TIME) {
|
||||||
condition.value.identifier = undefined
|
condition.value.identifier = undefined
|
||||||
condition.value.productId = undefined
|
condition.value.productId = undefined
|
||||||
condition.value.deviceId = undefined
|
condition.value.deviceId = undefined
|
||||||
} else if (type === ConditionTypeEnum.DEVICE_PROPERTY) {
|
} else if (type === ConditionTypeEnum.DEVICE_PROPERTY) {
|
||||||
condition.value.timeValue = undefined
|
// 清理时间相关字段(如果存在)
|
||||||
condition.value.timeValue2 = undefined
|
if ('timeValue' in condition.value) {
|
||||||
|
delete (condition.value as any).timeValue
|
||||||
|
}
|
||||||
|
if ('timeValue2' in condition.value) {
|
||||||
|
delete (condition.value as any).timeValue2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置操作符和参数
|
// 重置操作符和参数,使用枚举中的默认值
|
||||||
condition.value.operator = '='
|
condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
|
||||||
condition.value.param = ''
|
condition.value.param = ''
|
||||||
|
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
|
|
@ -239,14 +201,14 @@ const handleValidate = (result: { valid: boolean; message: string }) => {
|
||||||
emit('validate', result)
|
emit('validate', result)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleProductChange = (productId: number) => {
|
const handleProductChange = (_: number) => {
|
||||||
// 产品变化时清空设备和属性
|
// 产品变化时清空设备和属性
|
||||||
condition.value.deviceId = undefined
|
condition.value.deviceId = undefined
|
||||||
condition.value.identifier = ''
|
condition.value.identifier = ''
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeviceChange = (deviceId: number) => {
|
const handleDeviceChange = (_: number) => {
|
||||||
// 设备变化时清空属性
|
// 设备变化时清空属性
|
||||||
condition.value.identifier = ''
|
condition.value.identifier = ''
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
|
|
|
||||||
|
|
@ -1,255 +0,0 @@
|
||||||
<!-- 条件组配置组件 -->
|
|
||||||
<template>
|
|
||||||
<div class="p-16px">
|
|
||||||
<!-- 条件组说明 -->
|
|
||||||
<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="space-y-12px">
|
|
||||||
<div
|
|
||||||
v-for="(condition, index) in group.conditions"
|
|
||||||
:key="`condition-${index}`"
|
|
||||||
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="flex items-center justify-between mb-12px">
|
|
||||||
<div class="flex items-center gap-8px">
|
|
||||||
<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">
|
|
||||||
{{ getConditionTypeName(condition.type) }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
<el-button
|
|
||||||
type="danger"
|
|
||||||
size="small"
|
|
||||||
text
|
|
||||||
@click="removeCondition(index)"
|
|
||||||
v-if="group.conditions!.length > 1"
|
|
||||||
>
|
|
||||||
<Icon icon="ep:delete" />
|
|
||||||
删除
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-12px bg-[var(--el-fill-color-blank)] rounded-4px">
|
|
||||||
<ConditionConfig
|
|
||||||
:model-value="condition"
|
|
||||||
@update:model-value="(value) => updateCondition(index, value)"
|
|
||||||
:trigger-type="triggerType"
|
|
||||||
:product-id="productId"
|
|
||||||
:device-id="deviceId"
|
|
||||||
@validate="(result) => handleConditionValidate(index, result)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 条件间的"且"连接符 -->
|
|
||||||
<div
|
|
||||||
v-if="index < group.conditions!.length - 1"
|
|
||||||
class="flex items-center justify-center py-8px"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-6px">
|
|
||||||
<!-- 连接线 -->
|
|
||||||
<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 v-else class="py-40px text-center">
|
|
||||||
<el-empty description="暂无条件配置" :image-size="80">
|
|
||||||
<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
|
|
||||||
v-if="
|
|
||||||
group.conditions && group.conditions.length > 0 && group.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 {
|
|
||||||
ConditionGroupFormData,
|
|
||||||
ConditionFormData,
|
|
||||||
IotRuleSceneTriggerTypeEnum
|
|
||||||
} from '@/api/iot/rule/scene/scene.types'
|
|
||||||
|
|
||||||
/** 条件组配置组件 */
|
|
||||||
defineOptions({ name: 'ConditionGroupConfig' })
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
modelValue: ConditionGroupFormData
|
|
||||||
triggerType: number
|
|
||||||
productId?: number
|
|
||||||
deviceId?: number
|
|
||||||
maxConditions?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Emits {
|
|
||||||
(e: 'update:modelValue', value: ConditionGroupFormData): void
|
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const group = useVModel(props, 'modelValue', emit)
|
|
||||||
|
|
||||||
// 配置常量
|
|
||||||
const maxConditions = computed(() => props.maxConditions || 3)
|
|
||||||
|
|
||||||
// 验证状态
|
|
||||||
const conditionValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({})
|
|
||||||
const validationMessage = ref('')
|
|
||||||
const isValid = ref(true)
|
|
||||||
|
|
||||||
// 条件类型映射
|
|
||||||
const conditionTypeNames = {
|
|
||||||
[IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST]: '属性条件',
|
|
||||||
[IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST]: '事件条件',
|
|
||||||
[IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE]: '服务条件'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 工具函数
|
|
||||||
const getConditionTypeName = (type: number) => {
|
|
||||||
return conditionTypeNames[type] || '未知条件'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 事件处理
|
|
||||||
const updateCondition = (index: number, condition: ConditionFormData) => {
|
|
||||||
if (group.value.conditions) {
|
|
||||||
group.value.conditions[index] = condition
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addCondition = () => {
|
|
||||||
if (!group.value.conditions) {
|
|
||||||
group.value.conditions = []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group.value.conditions.length >= maxConditions.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newCondition: ConditionFormData = {
|
|
||||||
type: 2, // 默认为设备属性条件
|
|
||||||
productId: props.productId || 0,
|
|
||||||
deviceId: props.deviceId || 0,
|
|
||||||
identifier: '',
|
|
||||||
operator: '=',
|
|
||||||
param: ''
|
|
||||||
}
|
|
||||||
|
|
||||||
group.value.conditions.push(newCondition)
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeCondition = (index: number) => {
|
|
||||||
if (group.value.conditions) {
|
|
||||||
group.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 handleConditionValidate = (index: number, result: { valid: boolean; message: string }) => {
|
|
||||||
conditionValidations.value[index] = result
|
|
||||||
updateValidationResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateValidationResult = () => {
|
|
||||||
if (!group.value.conditions || group.value.conditions.length === 0) {
|
|
||||||
isValid.value = false
|
|
||||||
validationMessage.value = '请至少添加一个条件'
|
|
||||||
emit('validate', { valid: false, message: validationMessage.value })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const validations = Object.values(conditionValidations.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(
|
|
||||||
() => group.value.conditions?.length,
|
|
||||||
() => {
|
|
||||||
updateValidationResult()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
onMounted(() => {
|
|
||||||
if (!group.value.conditions || group.value.conditions.length === 0) {
|
|
||||||
addCondition()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-16px">
|
<div class="flex flex-col gap-16px">
|
||||||
<!-- 条件组容器头部 -->
|
<!-- 条件组容器头部 -->
|
||||||
<!-- TODO @puhui999:这个是不是删除,不然有两个“附件条件组”的 header -->
|
|
||||||
<div
|
<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"
|
class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-8px"
|
||||||
>
|
>
|
||||||
|
|
@ -15,16 +14,14 @@
|
||||||
<span>附加条件组</span>
|
<span>附加条件组</span>
|
||||||
</div>
|
</div>
|
||||||
<el-tag size="small" type="success">与主条件为且关系</el-tag>
|
<el-tag size="small" type="success">与主条件为且关系</el-tag>
|
||||||
<el-tag size="small" type="info">
|
<el-tag size="small" type="info"> {{ modelValue?.length || 0 }}个子条件组 </el-tag>
|
||||||
{{ modelValue.subGroups?.length || 0 }}个子条件组
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-8px">
|
<div class="flex items-center gap-8px">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
@click="addSubGroup"
|
@click="addSubGroup"
|
||||||
:disabled="(modelValue.subGroups?.length || 0) >= maxSubGroups"
|
:disabled="(modelValue?.length || 0) >= maxSubGroups"
|
||||||
>
|
>
|
||||||
<Icon icon="ep:plus" />
|
<Icon icon="ep:plus" />
|
||||||
添加子条件组
|
添加子条件组
|
||||||
|
|
@ -37,21 +34,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 子条件组列表 -->
|
<!-- 子条件组列表 -->
|
||||||
<div v-if="modelValue.subGroups && modelValue.subGroups.length > 0" class="space-y-16px">
|
<div v-if="modelValue && modelValue.length > 0" class="space-y-16px">
|
||||||
<!-- 逻辑关系说明 -->
|
<!-- 逻辑关系说明 -->
|
||||||
<!-- TODO @puhui999:这个可以去掉。。。提示有点太多了。 -->
|
|
||||||
<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 class="relative">
|
||||||
<div
|
<div
|
||||||
v-for="(subGroup, subGroupIndex) in modelValue.subGroups"
|
v-for="(subGroup, subGroupIndex) in modelValue"
|
||||||
:key="`sub-group-${subGroupIndex}`"
|
:key="`sub-group-${subGroupIndex}`"
|
||||||
class="relative"
|
class="relative"
|
||||||
>
|
>
|
||||||
|
|
@ -99,7 +86,7 @@
|
||||||
|
|
||||||
<!-- 子条件组间的"或"连接符 -->
|
<!-- 子条件组间的"或"连接符 -->
|
||||||
<div
|
<div
|
||||||
v-if="subGroupIndex < modelValue.subGroups!.length - 1"
|
v-if="subGroupIndex < modelValue!.length - 1"
|
||||||
class="flex items-center justify-center py-12px"
|
class="flex items-center justify-center py-12px"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-8px">
|
<div class="flex items-center gap-8px">
|
||||||
|
|
@ -136,27 +123,20 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import SubConditionGroupConfig from './SubConditionGroupConfig.vue'
|
import SubConditionGroupConfig from './SubConditionGroupConfig.vue'
|
||||||
import {
|
|
||||||
ConditionGroupContainerFormData,
|
|
||||||
SubConditionGroupFormData
|
|
||||||
} from '@/api/iot/rule/scene/scene.types'
|
|
||||||
|
|
||||||
/** 条件组容器配置组件 */
|
/** 条件组容器配置组件 */
|
||||||
defineOptions({ name: 'ConditionGroupContainerConfig' })
|
defineOptions({ name: 'ConditionGroupContainerConfig' })
|
||||||
|
|
||||||
interface Props {
|
const props = defineProps<{
|
||||||
modelValue: ConditionGroupContainerFormData
|
modelValue: any
|
||||||
triggerType: number
|
triggerType: number
|
||||||
}
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: ConditionGroupContainerFormData): void
|
(e: 'update:modelValue', value: any): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||||
(e: 'remove'): void
|
(e: 'remove'): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const container = useVModel(props, 'modelValue', emit)
|
const container = useVModel(props, 'modelValue', emit)
|
||||||
|
|
||||||
|
|
@ -169,24 +149,20 @@ const subGroupValidations = ref<{ [key: number]: { valid: boolean; message: stri
|
||||||
|
|
||||||
// 事件处理
|
// 事件处理
|
||||||
const addSubGroup = () => {
|
const addSubGroup = () => {
|
||||||
if (!container.value.subGroups) {
|
if (!container.value) {
|
||||||
container.value.subGroups = []
|
container.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (container.value.subGroups.length >= maxSubGroups) {
|
if (container.value.length >= maxSubGroups) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSubGroup: SubConditionGroupFormData = {
|
container.value.push([])
|
||||||
conditions: []
|
|
||||||
}
|
|
||||||
|
|
||||||
container.value.subGroups.push(newSubGroup)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeSubGroup = (index: number) => {
|
const removeSubGroup = (index: number) => {
|
||||||
if (container.value.subGroups) {
|
if (container.value) {
|
||||||
container.value.subGroups.splice(index, 1)
|
container.value.splice(index, 1)
|
||||||
delete subGroupValidations.value[index]
|
delete subGroupValidations.value[index]
|
||||||
|
|
||||||
// 重新索引验证结果
|
// 重新索引验证结果
|
||||||
|
|
@ -205,9 +181,9 @@ const removeSubGroup = (index: number) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateSubGroup = (index: number, subGroup: SubConditionGroupFormData) => {
|
const updateSubGroup = (index: number, subGroup: any) => {
|
||||||
if (container.value.subGroups) {
|
if (container.value) {
|
||||||
container.value.subGroups[index] = subGroup
|
container.value[index] = subGroup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,7 +197,7 @@ const handleSubGroupValidate = (index: number, result: { valid: boolean; message
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateValidationResult = () => {
|
const updateValidationResult = () => {
|
||||||
if (!container.value.subGroups || container.value.subGroups.length === 0) {
|
if (!container.value || container.value.length === 0) {
|
||||||
emit('validate', { valid: true, message: '条件组容器为空,验证通过' })
|
emit('validate', { valid: true, message: '条件组容器为空,验证通过' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -239,7 +215,7 @@ const updateValidationResult = () => {
|
||||||
|
|
||||||
// 监听变化
|
// 监听变化
|
||||||
watch(
|
watch(
|
||||||
() => container.value.subGroups,
|
() => container.value,
|
||||||
() => {
|
() => {
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -89,34 +89,6 @@
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<!-- 条件预览 -->
|
|
||||||
<!-- puhui999:可以去掉。。。因为表单选择了,可以看懂的呀。 -->
|
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -130,17 +102,14 @@ import {
|
||||||
/** 当前时间条件配置组件 */
|
/** 当前时间条件配置组件 */
|
||||||
defineOptions({ name: 'CurrentTimeConditionConfig' })
|
defineOptions({ name: 'CurrentTimeConditionConfig' })
|
||||||
|
|
||||||
interface Props {
|
const props = defineProps<{
|
||||||
modelValue: ConditionFormData
|
modelValue: ConditionFormData
|
||||||
}
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: ConditionFormData): void
|
(e: 'update:modelValue', value: ConditionFormData): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const condition = useVModel(props, 'modelValue', emit)
|
const condition = useVModel(props, 'modelValue', emit)
|
||||||
|
|
||||||
|
|
@ -211,29 +180,6 @@ const needsSecondTimeInput = computed(() => {
|
||||||
return condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value
|
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) => {
|
const updateConditionField = (field: keyof ConditionFormData, value: any) => {
|
||||||
condition.value[field] = value
|
condition.value[field] = value
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,13 @@
|
||||||
<template #default>
|
<template #default>
|
||||||
<div class="space-y-8px">
|
<div class="space-y-8px">
|
||||||
<p class="m-0 text-14px text-[var(--el-text-color-primary)]">属性设置示例:</p>
|
<p class="m-0 text-14px text-[var(--el-text-color-primary)]">属性设置示例:</p>
|
||||||
<pre class="m-0 p-8px bg-[var(--el-fill-color-light)] rounded-4px text-12px text-[var(--el-text-color-regular)] overflow-x-auto"><code>{ "temperature": 25, "power": true }</code></pre>
|
<pre
|
||||||
|
class="m-0 p-8px bg-[var(--el-fill-color-light)] rounded-4px text-12px text-[var(--el-text-color-regular)] overflow-x-auto"
|
||||||
|
><code>{ "temperature": 25, "power": true }</code></pre>
|
||||||
<p class="m-0 text-14px text-[var(--el-text-color-primary)]">服务调用示例:</p>
|
<p class="m-0 text-14px text-[var(--el-text-color-primary)]">服务调用示例:</p>
|
||||||
<pre class="m-0 p-8px bg-[var(--el-fill-color-light)] rounded-4px text-12px text-[var(--el-text-color-regular)] overflow-x-auto"><code>{ "method": "restart", "params": { "delay": 5 } }</code></pre>
|
<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>
|
||||||
|
|
@ -56,17 +60,14 @@ import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
|
||||||
/** 设备控制配置组件 */
|
/** 设备控制配置组件 */
|
||||||
defineOptions({ name: 'DeviceControlConfig' })
|
defineOptions({ name: 'DeviceControlConfig' })
|
||||||
|
|
||||||
interface Props {
|
const props = defineProps<{
|
||||||
modelValue: ActionFormData
|
modelValue: ActionFormData
|
||||||
}
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: ActionFormData): void
|
(e: 'update:modelValue', value: ActionFormData): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const action = useVModel(props, 'modelValue', emit)
|
const action = useVModel(props, 'modelValue', emit)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,31 +26,6 @@
|
||||||
|
|
||||||
<!-- 状态和操作符选择 -->
|
<!-- 状态和操作符选择 -->
|
||||||
<el-row :gutter="16">
|
<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-col :span="12">
|
||||||
<el-form-item label="操作符" required>
|
<el-form-item label="操作符" required>
|
||||||
|
|
@ -76,35 +51,32 @@
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 状态选择 -->
|
||||||
|
<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-row>
|
</el-row>
|
||||||
|
|
||||||
<!-- 条件预览 -->
|
|
||||||
<!-- TODO puhui999:可以去掉。。。因为表单选择了,可以看懂的呀。 -->
|
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -112,22 +84,19 @@
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import ProductSelector from '../selectors/ProductSelector.vue'
|
import ProductSelector from '../selectors/ProductSelector.vue'
|
||||||
import DeviceSelector from '../selectors/DeviceSelector.vue'
|
import DeviceSelector from '../selectors/DeviceSelector.vue'
|
||||||
import { ConditionFormData } from '@/api/iot/rule/scene/scene.types'
|
import { TriggerConditionFormData } from '@/api/iot/rule/scene/scene.types'
|
||||||
|
|
||||||
/** 设备状态条件配置组件 */
|
/** 设备状态条件配置组件 */
|
||||||
defineOptions({ name: 'DeviceStatusConditionConfig' })
|
defineOptions({ name: 'DeviceStatusConditionConfig' })
|
||||||
|
|
||||||
interface Props {
|
const props = defineProps<{
|
||||||
modelValue: ConditionFormData
|
modelValue: TriggerConditionFormData
|
||||||
}
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: ConditionFormData): void
|
(e: 'update:modelValue', value: TriggerConditionFormData): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const condition = useVModel(props, 'modelValue', emit)
|
const condition = useVModel(props, 'modelValue', emit)
|
||||||
|
|
||||||
|
|
@ -169,35 +138,19 @@ const statusOperatorOptions = [
|
||||||
const validationMessage = ref('')
|
const validationMessage = ref('')
|
||||||
const isValid = ref(true)
|
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) => {
|
const updateConditionField = (field: any, value: any) => {
|
||||||
condition.value[field] = value
|
condition.value[field] = value
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleProductChange = (productId: number) => {
|
const handleProductChange = (_: number) => {
|
||||||
// 产品变化时清空设备
|
// 产品变化时清空设备
|
||||||
condition.value.deviceId = undefined
|
condition.value.deviceId = undefined
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeviceChange = (deviceId: number) => {
|
const handleDeviceChange = (_: number) => {
|
||||||
// 设备变化时可以进行其他处理
|
// 设备变化时可以进行其他处理
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,24 @@
|
||||||
<!-- 设备触发配置组件 - 优化版本 -->
|
<!-- 设备触发配置组件 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-16px">
|
<div class="flex flex-col gap-16px">
|
||||||
<!-- 主条件配置 - 默认直接展示 -->
|
<!-- 主条件配置 - 默认直接展示 -->
|
||||||
<div class="space-y-16px">
|
<div class="space-y-16px">
|
||||||
<MainConditionConfig
|
<MainConditionConfig
|
||||||
v-model="trigger.mainCondition"
|
v-model="trigger"
|
||||||
:trigger-type="trigger.type"
|
:trigger-type="trigger.type"
|
||||||
@validate="handleMainConditionValidate"
|
@validate="handleMainConditionValidate"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 条件组配置 -->
|
<!-- 条件组配置 -->
|
||||||
<div v-if="trigger.mainCondition" class="space-y-16px">
|
<div 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
|
<ConditionGroupContainerConfig
|
||||||
v-if="trigger.conditionGroup"
|
v-model="trigger.conditionGroups"
|
||||||
v-model="trigger.conditionGroup"
|
|
||||||
:trigger-type="trigger.type"
|
:trigger-type="trigger.type"
|
||||||
@validate="handleConditionGroupValidate"
|
@validate="handleConditionGroupValidate"
|
||||||
@remove="removeConditionGroup"
|
@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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -62,26 +28,21 @@ import { useVModel } from '@vueuse/core'
|
||||||
|
|
||||||
import MainConditionConfig from './MainConditionConfig.vue'
|
import MainConditionConfig from './MainConditionConfig.vue'
|
||||||
import ConditionGroupContainerConfig from './ConditionGroupContainerConfig.vue'
|
import ConditionGroupContainerConfig from './ConditionGroupContainerConfig.vue'
|
||||||
import {
|
import { TriggerFormData } from '@/api/iot/rule/scene/scene.types'
|
||||||
TriggerFormData,
|
import { IotRuleSceneTriggerTypeEnum as TriggerTypeEnum } from '@/views/iot/utils/constants'
|
||||||
IotRuleSceneTriggerTypeEnum as TriggerTypeEnum
|
|
||||||
} from '@/api/iot/rule/scene/scene.types'
|
|
||||||
|
|
||||||
/** 设备触发配置组件 */
|
/** 设备触发配置组件 */
|
||||||
defineOptions({ name: 'DeviceTriggerConfig' })
|
defineOptions({ name: 'DeviceTriggerConfig' })
|
||||||
|
|
||||||
// TODO @puhui999:下面的 Props、Emits 可以合并到变量那;
|
const props = defineProps<{
|
||||||
interface Props {
|
|
||||||
modelValue: TriggerFormData
|
modelValue: TriggerFormData
|
||||||
}
|
index: number
|
||||||
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: TriggerFormData): void
|
(e: 'update:modelValue', value: TriggerFormData): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
(e: 'validate', value: { valid: boolean; message: string }): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const trigger = useVModel(props, 'modelValue', emit)
|
const trigger = useVModel(props, 'modelValue', emit)
|
||||||
|
|
||||||
|
|
@ -95,16 +56,17 @@ const isValid = ref(true)
|
||||||
|
|
||||||
// 初始化主条件
|
// 初始化主条件
|
||||||
const initMainCondition = () => {
|
const initMainCondition = () => {
|
||||||
if (!trigger.value.mainCondition) {
|
// TODO @puhui999: 等到编辑回显时联调
|
||||||
trigger.value.mainCondition = {
|
// if (!trigger.value.mainCondition) {
|
||||||
type: trigger.value.type, // 使用触发事件类型作为条件类型
|
// trigger.value = {
|
||||||
productId: undefined,
|
// type: trigger.value.type, // 使用触发事件类型作为条件类型
|
||||||
deviceId: undefined,
|
// productId: undefined,
|
||||||
identifier: '',
|
// deviceId: undefined,
|
||||||
operator: '=',
|
// identifier: '',
|
||||||
param: ''
|
// operator: '=',
|
||||||
}
|
// param: ''
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听触发器类型变化,自动初始化主条件
|
// 监听触发器类型变化,自动初始化主条件
|
||||||
|
|
@ -116,28 +78,25 @@ watch(
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// 新的事件处理函数
|
|
||||||
const handleMainConditionValidate = (result: { valid: boolean; message: string }) => {
|
const handleMainConditionValidate = (result: { valid: boolean; message: string }) => {
|
||||||
mainConditionValidation.value = result
|
mainConditionValidation.value = result
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
const addConditionGroup = () => {
|
const addConditionGroup = () => {
|
||||||
if (!trigger.value.conditionGroup) {
|
if (!trigger.value.conditionGroups) {
|
||||||
trigger.value.conditionGroup = {
|
trigger.value.conditionGroups = []
|
||||||
subGroups: []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
trigger.value.conditionGroups.push([])
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件处理
|
// 事件处理
|
||||||
|
|
||||||
const handleConditionGroupValidate = () => {
|
const handleConditionGroupValidate = () => {
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeConditionGroup = () => {
|
const removeConditionGroup = () => {
|
||||||
trigger.value.conditionGroup = undefined
|
trigger.value.conditionGroups = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateValidationResult = () => {
|
const updateValidationResult = () => {
|
||||||
|
|
@ -158,7 +117,7 @@ const updateValidationResult = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主条件验证
|
// 主条件验证
|
||||||
if (!trigger.value.mainCondition) {
|
if (!trigger.value.value) {
|
||||||
isValid.value = false
|
isValid.value = false
|
||||||
validationMessage.value = '请配置主条件'
|
validationMessage.value = '请配置主条件'
|
||||||
emit('validate', { valid: false, message: validationMessage.value })
|
emit('validate', { valid: false, message: validationMessage.value })
|
||||||
|
|
|
||||||
|
|
@ -20,19 +20,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 主条件配置 -->
|
<!-- 主条件配置 -->
|
||||||
<!-- TODO @puhui999:这里可以简化下,主条件是不能删除的。。。 -->
|
|
||||||
<div v-else class="space-y-16px">
|
<div v-else class="space-y-16px">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-8px">
|
<div class="flex items-center gap-8px">
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">主条件</span>
|
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">主条件</span>
|
||||||
<el-tag size="small" type="primary">必须满足</el-tag>
|
<el-tag size="small" type="primary">必须满足</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="danger" size="small" text @click="removeMainCondition">
|
|
||||||
<Icon icon="ep:delete" />
|
|
||||||
删除
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MainConditionInnerConfig
|
<MainConditionInnerConfig
|
||||||
:model-value="modelValue"
|
:model-value="modelValue"
|
||||||
@update:model-value="updateCondition"
|
@update:model-value="updateCondition"
|
||||||
|
|
@ -46,48 +40,45 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import MainConditionInnerConfig from './MainConditionInnerConfig.vue'
|
import MainConditionInnerConfig from './MainConditionInnerConfig.vue'
|
||||||
import {
|
import {
|
||||||
ConditionFormData,
|
IotRuleSceneTriggerConditionTypeEnum,
|
||||||
IotRuleSceneTriggerConditionTypeEnum
|
TriggerFormData
|
||||||
} from '@/api/iot/rule/scene/scene.types'
|
} from '@/api/iot/rule/scene/scene.types'
|
||||||
|
|
||||||
/** 主条件配置组件 */
|
/** 主条件配置组件 */
|
||||||
defineOptions({ name: 'MainConditionConfig' })
|
defineOptions({ name: 'MainConditionConfig' })
|
||||||
|
|
||||||
interface Props {
|
defineProps<{
|
||||||
modelValue?: ConditionFormData
|
modelValue?: TriggerFormData
|
||||||
triggerType: number
|
triggerType: number
|
||||||
}
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value?: ConditionFormData): void
|
(e: 'update:modelValue', value?: TriggerFormData): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
// 事件处理
|
// 事件处理
|
||||||
const addMainCondition = () => {
|
const addMainCondition = () => {
|
||||||
const newCondition: ConditionFormData = {
|
const newCondition: TriggerFormData = {
|
||||||
type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
|
type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
|
||||||
productId: undefined,
|
productId: undefined,
|
||||||
deviceId: undefined,
|
deviceId: undefined,
|
||||||
identifier: '',
|
identifier: '',
|
||||||
operator: '=',
|
operator: '=',
|
||||||
param: ''
|
value: ''
|
||||||
}
|
}
|
||||||
emit('update:modelValue', newCondition)
|
emit('update:modelValue', newCondition)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeMainCondition = () => {
|
const updateCondition = (condition: TriggerFormData) => {
|
||||||
emit('update:modelValue', undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateCondition = (condition: ConditionFormData) => {
|
|
||||||
emit('update:modelValue', condition)
|
emit('update:modelValue', condition)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleValidate = (result: { valid: boolean; message: string }) => {
|
const handleValidate = (result: { valid: boolean; message: string }) => {
|
||||||
emit('validate', result)
|
emit('validate', result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
addMainCondition()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-16px">
|
<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">
|
<div v-if="isDevicePropertyTrigger" class="space-y-16px">
|
||||||
<!-- 产品设备选择 -->
|
<!-- 产品设备选择 -->
|
||||||
|
|
@ -73,14 +67,6 @@
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<!-- 条件预览 -->
|
|
||||||
<!-- TODO puhui999:可以去掉。。。因为表单选择了,可以看懂的呀。 -->
|
|
||||||
<div v-if="conditionPreview" class="mt-12px">
|
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)]">
|
|
||||||
预览:{{ conditionPreview }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 设备状态条件配置 -->
|
<!-- 设备状态条件配置 -->
|
||||||
|
|
@ -112,24 +98,21 @@ import OperatorSelector from '../selectors/OperatorSelector.vue'
|
||||||
import ValueInput from '../inputs/ValueInput.vue'
|
import ValueInput from '../inputs/ValueInput.vue'
|
||||||
import DeviceStatusConditionConfig from './DeviceStatusConditionConfig.vue'
|
import DeviceStatusConditionConfig from './DeviceStatusConditionConfig.vue'
|
||||||
import { ConditionFormData } from '@/api/iot/rule/scene/scene.types'
|
import { ConditionFormData } from '@/api/iot/rule/scene/scene.types'
|
||||||
import { IotRuleSceneTriggerTypeEnum } from '@/api/iot/rule/scene/scene.types'
|
import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
|
|
||||||
/** 主条件内部配置组件 */
|
/** 主条件内部配置组件 */
|
||||||
defineOptions({ name: 'MainConditionInnerConfig' })
|
defineOptions({ name: 'MainConditionInnerConfig' })
|
||||||
|
|
||||||
interface Props {
|
const props = defineProps<{
|
||||||
modelValue: ConditionFormData
|
modelValue: ConditionFormData
|
||||||
triggerType: number
|
triggerType: number
|
||||||
}
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: ConditionFormData): void
|
(e: 'update:modelValue', value: ConditionFormData): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const condition = useVModel(props, 'modelValue', emit)
|
const condition = useVModel(props, 'modelValue', emit)
|
||||||
|
|
@ -152,13 +135,6 @@ const isDeviceStatusTrigger = computed(() => {
|
||||||
return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE
|
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}`
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取触发类型文本
|
// 获取触发类型文本
|
||||||
// TODO @puhui999:是不是有枚举可以服用哈;
|
// TODO @puhui999:是不是有枚举可以服用哈;
|
||||||
const getTriggerTypeText = (type: number) => {
|
const getTriggerTypeText = (type: number) => {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="p-16px">
|
<div class="p-16px">
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div
|
<div v-if="!subGroup || subGroup.length === 0" class="text-center py-24px">
|
||||||
v-if="!subGroup.conditions || subGroup.conditions.length === 0"
|
|
||||||
class="text-center py-24px"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col items-center gap-12px">
|
<div class="flex flex-col items-center gap-12px">
|
||||||
<Icon icon="ep:plus" class="text-32px text-[var(--el-text-color-placeholder)]" />
|
<Icon icon="ep:plus" class="text-32px text-[var(--el-text-color-placeholder)]" />
|
||||||
<div class="text-[var(--el-text-color-secondary)]">
|
<div class="text-[var(--el-text-color-secondary)]">
|
||||||
|
|
@ -21,7 +18,7 @@
|
||||||
<!-- 条件列表 -->
|
<!-- 条件列表 -->
|
||||||
<div v-else class="space-y-16px">
|
<div v-else class="space-y-16px">
|
||||||
<div
|
<div
|
||||||
v-for="(condition, conditionIndex) in subGroup.conditions"
|
v-for="(condition, conditionIndex) in subGroup"
|
||||||
:key="`condition-${conditionIndex}`"
|
:key="`condition-${conditionIndex}`"
|
||||||
class="relative"
|
class="relative"
|
||||||
>
|
>
|
||||||
|
|
@ -47,7 +44,7 @@
|
||||||
size="small"
|
size="small"
|
||||||
text
|
text
|
||||||
@click="removeCondition(conditionIndex)"
|
@click="removeCondition(conditionIndex)"
|
||||||
v-if="subGroup.conditions!.length > 1"
|
v-if="subGroup!.length > 1"
|
||||||
class="hover:bg-red-50"
|
class="hover:bg-red-50"
|
||||||
>
|
>
|
||||||
<Icon icon="ep:delete" />
|
<Icon icon="ep:delete" />
|
||||||
|
|
@ -63,33 +60,11 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 条件间的"且"连接符 -->
|
|
||||||
<!-- TODO @puhu999:建议去掉,有点元素太丰富了。 -->
|
|
||||||
<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>
|
||||||
|
|
||||||
<!-- 添加条件按钮 -->
|
<!-- 添加条件按钮 -->
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="subGroup && subGroup.length > 0 && subGroup.length < maxConditions"
|
||||||
subGroup.conditions &&
|
|
||||||
subGroup.conditions.length > 0 &&
|
|
||||||
subGroup.conditions.length < maxConditions
|
|
||||||
"
|
|
||||||
class="text-center py-16px"
|
class="text-center py-16px"
|
||||||
>
|
>
|
||||||
<el-button type="primary" plain @click="addCondition">
|
<el-button type="primary" plain @click="addCondition">
|
||||||
|
|
@ -108,27 +83,23 @@
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import ConditionConfig from './ConditionConfig.vue'
|
import ConditionConfig from './ConditionConfig.vue'
|
||||||
import {
|
import {
|
||||||
SubConditionGroupFormData,
|
IotRuleSceneTriggerConditionTypeEnum,
|
||||||
ConditionFormData,
|
TriggerConditionFormData
|
||||||
IotRuleSceneTriggerConditionTypeEnum
|
|
||||||
} from '@/api/iot/rule/scene/scene.types'
|
} from '@/api/iot/rule/scene/scene.types'
|
||||||
|
|
||||||
/** 子条件组配置组件 */
|
/** 子条件组配置组件 */
|
||||||
defineOptions({ name: 'SubConditionGroupConfig' })
|
defineOptions({ name: 'SubConditionGroupConfig' })
|
||||||
|
|
||||||
interface Props {
|
const props = defineProps<{
|
||||||
modelValue: SubConditionGroupFormData
|
modelValue: TriggerConditionFormData[]
|
||||||
triggerType: number
|
triggerType: number
|
||||||
maxConditions?: number
|
maxConditions?: number
|
||||||
}
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: SubConditionGroupFormData): void
|
(e: 'update:modelValue', value: TriggerConditionFormData[]): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const subGroup = useVModel(props, 'modelValue', emit)
|
const subGroup = useVModel(props, 'modelValue', emit)
|
||||||
|
|
||||||
|
|
@ -140,15 +111,13 @@ const conditionValidations = ref<{ [key: number]: { valid: boolean; message: str
|
||||||
|
|
||||||
// 事件处理
|
// 事件处理
|
||||||
const addCondition = () => {
|
const addCondition = () => {
|
||||||
if (!subGroup.value.conditions) {
|
if (!subGroup.value) {
|
||||||
subGroup.value.conditions = []
|
subGroup.value = []
|
||||||
}
|
}
|
||||||
|
if (subGroup.value.length >= maxConditions.value) {
|
||||||
if (subGroup.value.conditions.length >= maxConditions.value) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const newCondition: TriggerConditionFormData = {
|
||||||
const newCondition: ConditionFormData = {
|
|
||||||
type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
|
type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
|
||||||
productId: undefined,
|
productId: undefined,
|
||||||
deviceId: undefined,
|
deviceId: undefined,
|
||||||
|
|
@ -156,13 +125,12 @@ const addCondition = () => {
|
||||||
operator: '=',
|
operator: '=',
|
||||||
param: ''
|
param: ''
|
||||||
}
|
}
|
||||||
|
subGroup.value.push(newCondition)
|
||||||
subGroup.value.conditions.push(newCondition)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeCondition = (index: number) => {
|
const removeCondition = (index: number) => {
|
||||||
if (subGroup.value.conditions) {
|
if (subGroup.value) {
|
||||||
subGroup.value.conditions.splice(index, 1)
|
subGroup.value.splice(index, 1)
|
||||||
delete conditionValidations.value[index]
|
delete conditionValidations.value[index]
|
||||||
|
|
||||||
// 重新索引验证结果
|
// 重新索引验证结果
|
||||||
|
|
@ -181,9 +149,9 @@ const removeCondition = (index: number) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCondition = (index: number, condition: ConditionFormData) => {
|
const updateCondition = (index: number, condition: TriggerConditionFormData) => {
|
||||||
if (subGroup.value.conditions) {
|
if (subGroup.value) {
|
||||||
subGroup.value.conditions[index] = condition
|
subGroup.value[index] = condition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,7 +161,7 @@ const handleConditionValidate = (index: number, result: { valid: boolean; messag
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateValidationResult = () => {
|
const updateValidationResult = () => {
|
||||||
if (!subGroup.value.conditions || subGroup.value.conditions.length === 0) {
|
if (!subGroup.value || subGroup.value.length === 0) {
|
||||||
emit('validate', { valid: false, message: '子条件组至少需要一个条件' })
|
emit('validate', { valid: false, message: '子条件组至少需要一个条件' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -211,7 +179,7 @@ const updateValidationResult = () => {
|
||||||
|
|
||||||
// 监听变化
|
// 监听变化
|
||||||
watch(
|
watch(
|
||||||
() => subGroup.value.conditions,
|
() => subGroup.value,
|
||||||
() => {
|
() => {
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -25,18 +25,13 @@ import { Crontab } from '@/components/Crontab'
|
||||||
/** 定时触发配置组件 */
|
/** 定时触发配置组件 */
|
||||||
defineOptions({ name: 'TimerTriggerConfig' })
|
defineOptions({ name: 'TimerTriggerConfig' })
|
||||||
|
|
||||||
// TODO @puhui999:下面的 Props、Emits 可以合并到变量那;
|
const props = defineProps<{
|
||||||
interface Props {
|
|
||||||
modelValue?: string
|
modelValue?: string
|
||||||
}
|
}>()
|
||||||
|
const emit = defineEmits<{
|
||||||
interface Emits {
|
|
||||||
(e: 'update:modelValue', value: string): void
|
(e: 'update:modelValue', value: string): void
|
||||||
(e: 'validate', result: { valid: boolean; message: 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 localValue = useVModel(props, 'modelValue', emit, {
|
||||||
defaultValue: '0 0 12 * * ?'
|
defaultValue: '0 0 12 * * ?'
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,11 @@
|
||||||
|
|
||||||
<!-- 执行器列表 -->
|
<!-- 执行器列表 -->
|
||||||
<div v-else class="space-y-16px">
|
<div v-else class="space-y-16px">
|
||||||
<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
|
||||||
|
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="flex items-center justify-between mb-16px">
|
<div class="flex items-center justify-between mb-16px">
|
||||||
<div class="flex items-center gap-8px">
|
<div class="flex items-center gap-8px">
|
||||||
<Icon icon="ep:setting" class="text-[var(--el-color-success)] text-16px" />
|
<Icon icon="ep:setting" class="text-[var(--el-color-success)] text-16px" />
|
||||||
|
|
@ -92,7 +96,9 @@
|
||||||
<Icon icon="ep:plus" />
|
<Icon icon="ep:plus" />
|
||||||
继续添加执行器
|
继续添加执行器
|
||||||
</el-button>
|
</el-button>
|
||||||
<span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]"> 最多可添加 {{ maxActions }} 个执行器 </span>
|
<span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]">
|
||||||
|
最多可添加 {{ maxActions }} 个执行器
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 验证结果 -->
|
<!-- 验证结果 -->
|
||||||
|
|
@ -113,10 +119,8 @@ import { useVModel } from '@vueuse/core'
|
||||||
import ActionTypeSelector from '../selectors/ActionTypeSelector.vue'
|
import ActionTypeSelector from '../selectors/ActionTypeSelector.vue'
|
||||||
import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
|
import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
|
||||||
import AlertConfig from '../configs/AlertConfig.vue'
|
import AlertConfig from '../configs/AlertConfig.vue'
|
||||||
import {
|
import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
|
||||||
ActionFormData,
|
import { IotRuleSceneActionTypeEnum as ActionTypeEnum } from '@/views/iot/utils/constants'
|
||||||
IotRuleSceneActionTypeEnum as ActionTypeEnum
|
|
||||||
} from '@/api/iot/rule/scene/scene.types'
|
|
||||||
|
|
||||||
/** 执行器配置组件 */
|
/** 执行器配置组件 */
|
||||||
defineOptions({ name: 'ActionSection' })
|
defineOptions({ name: 'ActionSection' })
|
||||||
|
|
@ -173,11 +177,13 @@ const actionTypeTags = {
|
||||||
|
|
||||||
// 工具函数
|
// 工具函数
|
||||||
const isDeviceAction = (type: number) => {
|
const isDeviceAction = (type: number) => {
|
||||||
return [ActionTypeEnum.DEVICE_PROPERTY_SET, ActionTypeEnum.DEVICE_SERVICE_INVOKE].includes(type)
|
return [ActionTypeEnum.DEVICE_PROPERTY_SET, ActionTypeEnum.DEVICE_SERVICE_INVOKE].includes(
|
||||||
|
type as any
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAlertAction = (type: number) => {
|
const isAlertAction = (type: number) => {
|
||||||
return [ActionTypeEnum.ALERT_TRIGGER, ActionTypeEnum.ALERT_RECOVER].includes(type)
|
return [ActionTypeEnum.ALERT_TRIGGER, ActionTypeEnum.ALERT_RECOVER].includes(type as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getActionTypeName = (type: number) => {
|
const getActionTypeName = (type: number) => {
|
||||||
|
|
@ -277,5 +283,3 @@ watch(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<!-- 基础信息配置组件 -->
|
<!-- 基础信息配置组件 -->
|
||||||
<template>
|
<template>
|
||||||
<el-card class="border border-[var(--el-border-color-light)] rounded-8px" shadow="never">
|
<el-card class="border border-[var(--el-border-color-light)] rounded-8px mb-10px" shadow="never">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-8px">
|
<div class="flex items-center gap-8px">
|
||||||
|
|
@ -8,10 +8,7 @@
|
||||||
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">基础信息</span>
|
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">基础信息</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-8px">
|
<div class="flex items-center gap-8px">
|
||||||
<!-- TODO @puhui999:dict-tag 可以哇? -->
|
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData.status" />
|
||||||
<el-tag :type="formData.status === 0 ? 'success' : 'danger'" size="small">
|
|
||||||
{{ formData.status === 0 ? '启用' : '禁用' }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -60,25 +57,19 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||||
import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
|
import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
|
||||||
|
|
||||||
/** 基础信息配置组件 */
|
/** 基础信息配置组件 */
|
||||||
defineOptions({ name: 'BasicInfoSection' })
|
defineOptions({ name: 'BasicInfoSection' })
|
||||||
|
|
||||||
// TODO @puhui999:下面的 Props、Emits 可以合并到变量那;
|
const props = defineProps<{
|
||||||
|
|
||||||
interface Props {
|
|
||||||
modelValue: RuleSceneFormData
|
modelValue: RuleSceneFormData
|
||||||
rules?: any
|
rules?: any
|
||||||
}
|
}>()
|
||||||
|
const emit = defineEmits<{
|
||||||
interface Emits {
|
|
||||||
(e: 'update:modelValue', value: RuleSceneFormData): void
|
(e: 'update:modelValue', value: RuleSceneFormData): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const formData = useVModel(props, 'modelValue', emit)
|
const formData = useVModel(props, 'modelValue', emit)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,98 @@
|
||||||
<template>
|
<template>
|
||||||
<el-card class="border border-[var(--el-border-color-light)] rounded-8px" shadow="never">
|
<el-card class="border border-[var(--el-border-color-light)] rounded-8px mb-10px" shadow="never">
|
||||||
<!-- TODO @puhui999:触发器还是多个。。。每个触发器里面有事件类型 + 附加条件组(最好文案上,和阿里 iot 保持相对一致) -->
|
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center gap-8px">
|
<div class="flex items-center justify-between">
|
||||||
<Icon icon="ep:lightning" class="text-[var(--el-color-primary)] text-18px" />
|
<div class="flex items-center gap-8px">
|
||||||
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">触发器配置</span>
|
<Icon icon="ep:lightning" class="text-[var(--el-color-primary)] text-18px" />
|
||||||
<el-tag size="small" type="info">场景触发器</el-tag>
|
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">触发器配置</span>
|
||||||
|
<el-tag size="small" type="info">{{ triggers.length }} 个触发器</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" size="small" @click="addTrigger">
|
||||||
|
<Icon icon="ep:plus" />
|
||||||
|
添加触发器
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="p-16px space-y-16px">
|
<div class="p-16px space-y-24px">
|
||||||
<!-- 触发事件类型选择 -->
|
<!-- 触发器列表 -->
|
||||||
<el-form-item label="触发事件类型" required>
|
<div v-if="triggers.length > 0" class="space-y-24px">
|
||||||
<el-select
|
<div
|
||||||
:model-value="trigger.type"
|
v-for="(triggerItem, index) in triggers"
|
||||||
@update:model-value="(value) => updateTriggerType(value)"
|
:key="`trigger-${index}`"
|
||||||
@change="onTriggerTypeChange"
|
class="border border-[var(--el-border-color-light)] rounded-8px p-16px relative"
|
||||||
placeholder="请选择触发事件类型"
|
|
||||||
class="w-full"
|
|
||||||
>
|
>
|
||||||
<el-option
|
<!-- 触发器头部 -->
|
||||||
v-for="option in triggerTypeOptions"
|
<div class="flex items-center justify-between mb-16px">
|
||||||
:key="option.value"
|
<div class="flex items-center gap-8px">
|
||||||
:label="option.label"
|
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">
|
||||||
:value="option.value"
|
触发器 {{ index + 1 }}
|
||||||
|
</span>
|
||||||
|
<el-tag size="small" :type="getTriggerTagType(triggerItem.type)">
|
||||||
|
{{ getTriggerTypeLabel(triggerItem.type) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-8px">
|
||||||
|
<el-button
|
||||||
|
v-if="triggers.length > 1"
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
@click="removeTrigger(index)"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:delete" />
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 触发事件类型选择 -->
|
||||||
|
<el-form-item label="触发事件类型" required>
|
||||||
|
<el-select
|
||||||
|
:model-value="triggerItem.type"
|
||||||
|
@update:model-value="(value) => updateTriggerType(index, value)"
|
||||||
|
placeholder="请选择触发事件类型"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="option in triggerTypeOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 设备触发配置 -->
|
||||||
|
<DeviceTriggerConfig
|
||||||
|
v-if="isDeviceTrigger(triggerItem.type)"
|
||||||
|
:model-value="triggerItem"
|
||||||
|
:index="index"
|
||||||
|
@update:model-value="(value) => updateTriggerDeviceConfig(index, value)"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 设备触发配置 -->
|
<!-- 定时触发配置 -->
|
||||||
<DeviceTriggerConfig
|
<TimerTriggerConfig
|
||||||
v-if="isDeviceTrigger(trigger.type)"
|
v-else-if="triggerItem.type === TriggerTypeEnum.TIMER"
|
||||||
:model-value="trigger"
|
:model-value="triggerItem.cronExpression"
|
||||||
@update:model-value="updateTrigger"
|
@update:model-value="(value) => updateTriggerCronConfig(index, value)"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 定时触发配置 -->
|
<!-- 空状态 -->
|
||||||
<!-- TODO @puhui999:这里要不 v-else 好了? -->
|
<div v-else class="py-40px text-center">
|
||||||
<TimerTriggerConfig
|
<el-empty description="暂无触发器">
|
||||||
v-if="trigger.type === TriggerTypeEnum.TIMER"
|
<template #description>
|
||||||
:model-value="trigger.cronExpression"
|
<div class="space-y-8px">
|
||||||
@update:model-value="updateTriggerCronExpression"
|
<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>
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -50,108 +101,92 @@
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue'
|
import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue'
|
||||||
import TimerTriggerConfig from '../configs/TimerTriggerConfig.vue'
|
import TimerTriggerConfig from '../configs/TimerTriggerConfig.vue'
|
||||||
|
import { TriggerFormData } from '@/api/iot/rule/scene/scene.types'
|
||||||
import {
|
import {
|
||||||
TriggerFormData,
|
getTriggerTypeOptions,
|
||||||
IotRuleSceneTriggerTypeEnum as TriggerTypeEnum
|
IotRuleSceneTriggerTypeEnum as TriggerTypeEnum,
|
||||||
} from '@/api/iot/rule/scene/scene.types'
|
IotRuleSceneTriggerTypeEnum,
|
||||||
|
isDeviceTrigger
|
||||||
|
} from '@/views/iot/utils/constants'
|
||||||
|
|
||||||
/** 触发器配置组件 */
|
/** 触发器配置组件 */
|
||||||
defineOptions({ name: 'TriggerSection' })
|
defineOptions({ name: 'TriggerSection' })
|
||||||
|
|
||||||
// TODO @puhui999:下面的 Props、Emits 可以合并到变量那;
|
const props = defineProps<{
|
||||||
interface Props {
|
triggers: TriggerFormData[]
|
||||||
trigger: TriggerFormData
|
}>()
|
||||||
}
|
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:trigger', value: TriggerFormData): void
|
(e: 'update:triggers', value: TriggerFormData[]): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const triggers = useVModel(props, 'triggers', emit)
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const trigger = useVModel(props, 'trigger', emit)
|
// 触发器类型选项(从 constants 中获取)
|
||||||
|
const triggerTypeOptions = getTriggerTypeOptions()
|
||||||
// 触发器类型选项
|
|
||||||
// TODO @puhui999:/Users/yunai/Java/yudao-ui-admin-vue3/src/views/iot/utils/constants.ts
|
|
||||||
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: '定时触发'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 工具函数
|
// 工具函数
|
||||||
// TODO @puhui999:/Users/yunai/Java/yudao-ui-admin-vue3/src/views/iot/utils/constants.ts
|
const getTriggerTypeLabel = (type: number): string => {
|
||||||
const isDeviceTrigger = (type: number) => {
|
const option = triggerTypeOptions.find((opt) => opt.value === type)
|
||||||
const deviceTriggerTypes = [
|
return option?.label || '未知类型'
|
||||||
TriggerTypeEnum.DEVICE_STATE_UPDATE,
|
|
||||||
TriggerTypeEnum.DEVICE_PROPERTY_POST,
|
|
||||||
TriggerTypeEnum.DEVICE_EVENT_POST,
|
|
||||||
TriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
|
||||||
] as number[]
|
|
||||||
return deviceTriggerTypes.includes(type)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件处理
|
const getTriggerTagType = (type: number): string => {
|
||||||
const updateTriggerType = (type: number) => {
|
if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
|
||||||
trigger.value.type = type
|
return 'warning'
|
||||||
onTriggerTypeChange(type)
|
}
|
||||||
|
return isDeviceTrigger(type) ? 'success' : 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @puhui999:updateTriggerDeviceConfig
|
// 事件处理函数
|
||||||
const updateTrigger = (newTrigger: TriggerFormData) => {
|
const addTrigger = () => {
|
||||||
trigger.value = newTrigger
|
const newTrigger: TriggerFormData = {
|
||||||
|
type: TriggerTypeEnum.DEVICE_STATE_UPDATE,
|
||||||
|
productId: undefined,
|
||||||
|
deviceId: undefined,
|
||||||
|
identifier: undefined,
|
||||||
|
operator: undefined,
|
||||||
|
value: undefined,
|
||||||
|
cronExpression: undefined,
|
||||||
|
conditionGroups: [] // 空的条件组数组
|
||||||
|
}
|
||||||
|
triggers.value.push(newTrigger)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @puhui999:updateTriggerCronConfig
|
const removeTrigger = (index: number) => {
|
||||||
const updateTriggerCronExpression = (cronExpression?: string) => {
|
if (triggers.value.length > 1) {
|
||||||
trigger.value.cronExpression = cronExpression
|
triggers.value.splice(index, 1)
|
||||||
}
|
|
||||||
|
|
||||||
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 // 可选的条件组
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateTriggerType = (index: number, type: number) => {
|
||||||
|
triggers.value[index].type = type
|
||||||
|
onTriggerTypeChange(index, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTriggerDeviceConfig = (index: number, newTrigger: TriggerFormData) => {
|
||||||
|
triggers.value[index] = newTrigger
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTriggerCronConfig = (index: number, cronExpression?: string) => {
|
||||||
|
triggers.value[index].cronExpression = cronExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTriggerTypeChange = (index: number, _: number) => {
|
||||||
|
const triggerItem = triggers.value[index]
|
||||||
|
triggerItem.productId = undefined
|
||||||
|
triggerItem.deviceId = undefined
|
||||||
|
triggerItem.identifier = undefined
|
||||||
|
triggerItem.operator = undefined
|
||||||
|
triggerItem.value = undefined
|
||||||
|
triggerItem.cronExpression = undefined
|
||||||
|
triggerItem.conditionGroups = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化:确保至少有一个触发器
|
||||||
|
onMounted(() => {
|
||||||
|
if (triggers.value.length === 0) {
|
||||||
|
addTrigger()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,17 @@
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between w-full py-4px">
|
<div class="flex items-center justify-between w-full py-4px">
|
||||||
<div class="flex items-center gap-12px flex-1">
|
<div class="flex items-center gap-12px flex-1">
|
||||||
<Icon :icon="option.icon" class="text-18px text-[var(--el-color-primary)] flex-shrink-0" />
|
<Icon
|
||||||
|
:icon="option.icon"
|
||||||
|
class="text-18px text-[var(--el-color-primary)] flex-shrink-0"
|
||||||
|
/>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ option.label }}</div>
|
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">{{ option.description }}</div>
|
option.label
|
||||||
|
}}</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">
|
||||||
|
|
@ -35,7 +42,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import { IotRuleSceneActionTypeEnum } from '@/api/iot/rule/scene/scene.types'
|
import { IotRuleSceneActionTypeEnum } from '@/views/iot/utils/constants'
|
||||||
|
|
||||||
/** 执行器类型选择组件 */
|
/** 执行器类型选择组件 */
|
||||||
defineOptions({ name: 'ActionTypeSelector' })
|
defineOptions({ name: 'ActionTypeSelector' })
|
||||||
|
|
|
||||||
|
|
@ -24,22 +24,19 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { IotRuleSceneTriggerConditionTypeEnum } from '@/api/iot/rule/scene/scene.types'
|
import { IotRuleSceneTriggerConditionTypeEnum } from '@/views/iot/utils/constants'
|
||||||
|
|
||||||
/** 条件类型选择器组件 */
|
/** 条件类型选择器组件 */
|
||||||
defineOptions({ name: 'ConditionTypeSelector' })
|
defineOptions({ name: 'ConditionTypeSelector' })
|
||||||
|
|
||||||
interface Props {
|
defineProps<{
|
||||||
modelValue?: number
|
modelValue?: number
|
||||||
}
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: number): void
|
(e: 'update:modelValue', value: number): void
|
||||||
(e: 'change', value: number): void
|
(e: 'change', value: number): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
// 条件类型选项
|
// 条件类型选项
|
||||||
const conditionTypeOptions = [
|
const conditionTypeOptions = [
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,9 @@
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between w-full py-4px">
|
<div class="flex items-center justify-between w-full py-4px">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{
|
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px"
|
||||||
device.deviceName
|
>{{ device.deviceName }}
|
||||||
}}</div>
|
</div>
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ device.deviceKey }}</div>
|
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ device.deviceKey }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4px">
|
<div class="flex items-center gap-4px">
|
||||||
|
|
@ -42,18 +42,15 @@ import { DeviceApi } from '@/api/iot/device/device'
|
||||||
/** 设备选择器组件 */
|
/** 设备选择器组件 */
|
||||||
defineOptions({ name: 'DeviceSelector' })
|
defineOptions({ name: 'DeviceSelector' })
|
||||||
|
|
||||||
interface Props {
|
const props = defineProps<{
|
||||||
modelValue?: number
|
modelValue?: number
|
||||||
productId?: number
|
productId?: number
|
||||||
}
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value?: number): void
|
(e: 'update:modelValue', value?: number): void
|
||||||
(e: 'change', value?: number): void
|
(e: 'change', value?: number): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
const deviceLoading = ref(false)
|
const deviceLoading = ref(false)
|
||||||
|
|
|
||||||
|
|
@ -15,139 +15,140 @@
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between w-full py-4px">
|
<div class="flex items-center justify-between w-full py-4px">
|
||||||
<div class="flex items-center gap-8px">
|
<div class="flex items-center gap-8px">
|
||||||
<div class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ operator.label }}</div>
|
<div class="text-14px font-500 text-[var(--el-text-color-primary)]">
|
||||||
<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>
|
{{ operator.label }}
|
||||||
|
</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 class="text-12px text-[var(--el-text-color-secondary)]">
|
||||||
|
{{ operator.description }}
|
||||||
</div>
|
</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:这个去掉 -->
|
|
||||||
<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="flex items-center gap-6px">
|
|
||||||
<Icon icon="ep:info-filled" class="text-12px text-[var(--el-color-info)]" />
|
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)]">{{ selectedOperator.description }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedOperator.example" class="flex items-center gap-6px mt-4px">
|
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)]">示例:</span>
|
|
||||||
<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>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import { IotRuleSceneTriggerConditionParameterOperatorEnum } from '@/views/iot/utils/constants'
|
||||||
|
|
||||||
/** 操作符选择器组件 */
|
/** 操作符选择器组件 */
|
||||||
defineOptions({ name: 'OperatorSelector' })
|
defineOptions({ name: 'OperatorSelector' })
|
||||||
|
|
||||||
interface Props {
|
const props = defineProps<{
|
||||||
modelValue?: string
|
modelValue?: string
|
||||||
propertyType?: string
|
propertyType?: string
|
||||||
}
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: string): void
|
(e: 'update:modelValue', value: string): void
|
||||||
(e: 'change', value: string): void
|
(e: 'change', value: string): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const localValue = useVModel(props, 'modelValue', emit)
|
const localValue = useVModel(props, 'modelValue', emit)
|
||||||
|
|
||||||
// 所有操作符定义
|
// 基于枚举的操作符定义
|
||||||
const allOperators = [
|
const allOperators = [
|
||||||
{
|
{
|
||||||
value: '=',
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value,
|
||||||
label: '等于',
|
label: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.name,
|
||||||
symbol: '=',
|
symbol: '=',
|
||||||
description: '值完全相等时触发',
|
description: '值完全相等时触发',
|
||||||
example: 'temperature = 25',
|
example: 'temperature = 25',
|
||||||
supportedTypes: ['int', 'float', 'double', 'string', 'bool', 'enum']
|
supportedTypes: ['int', 'float', 'double', 'string', 'bool', 'enum']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '!=',
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.value,
|
||||||
label: '不等于',
|
label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.name,
|
||||||
symbol: '≠',
|
symbol: '≠',
|
||||||
description: '值不相等时触发',
|
description: '值不相等时触发',
|
||||||
example: 'power != false',
|
example: 'power != false',
|
||||||
supportedTypes: ['int', 'float', 'double', 'string', 'bool', 'enum']
|
supportedTypes: ['int', 'float', 'double', 'string', 'bool', 'enum']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '>',
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN.value,
|
||||||
label: '大于',
|
label: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN.name,
|
||||||
symbol: '>',
|
symbol: '>',
|
||||||
description: '值大于指定值时触发',
|
description: '值大于指定值时触发',
|
||||||
example: 'temperature > 30',
|
example: 'temperature > 30',
|
||||||
supportedTypes: ['int', 'float', 'double', 'date']
|
supportedTypes: ['int', 'float', 'double', 'date']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '>=',
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS.value,
|
||||||
label: '大于等于',
|
label: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS.name,
|
||||||
symbol: '≥',
|
symbol: '≥',
|
||||||
description: '值大于或等于指定值时触发',
|
description: '值大于或等于指定值时触发',
|
||||||
example: 'humidity >= 80',
|
example: 'humidity >= 80',
|
||||||
supportedTypes: ['int', 'float', 'double', 'date']
|
supportedTypes: ['int', 'float', 'double', 'date']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '<',
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN.value,
|
||||||
label: '小于',
|
label: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN.name,
|
||||||
symbol: '<',
|
symbol: '<',
|
||||||
description: '值小于指定值时触发',
|
description: '值小于指定值时触发',
|
||||||
example: 'temperature < 10',
|
example: 'temperature < 10',
|
||||||
supportedTypes: ['int', 'float', 'double', 'date']
|
supportedTypes: ['int', 'float', 'double', 'date']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '<=',
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS.value,
|
||||||
label: '小于等于',
|
label: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS.name,
|
||||||
symbol: '≤',
|
symbol: '≤',
|
||||||
description: '值小于或等于指定值时触发',
|
description: '值小于或等于指定值时触发',
|
||||||
example: 'battery <= 20',
|
example: 'battery <= 20',
|
||||||
supportedTypes: ['int', 'float', 'double', 'date']
|
supportedTypes: ['int', 'float', 'double', 'date']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'in',
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.IN.value,
|
||||||
label: '包含于',
|
label: IotRuleSceneTriggerConditionParameterOperatorEnum.IN.name,
|
||||||
symbol: '∈',
|
symbol: '∈',
|
||||||
description: '值在指定列表中时触发',
|
description: '值在指定列表中时触发',
|
||||||
example: 'status in [1,2,3]',
|
example: 'status in [1,2,3]',
|
||||||
supportedTypes: ['int', 'float', 'string', 'enum']
|
supportedTypes: ['int', 'float', 'string', 'enum']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'between',
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN.value,
|
||||||
label: '介于',
|
label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN.name,
|
||||||
|
symbol: '∉',
|
||||||
|
description: '值不在指定列表中时触发',
|
||||||
|
example: 'status not in [1,2,3]',
|
||||||
|
supportedTypes: ['int', 'float', 'string', 'enum']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.value,
|
||||||
|
label: IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.name,
|
||||||
symbol: '⊆',
|
symbol: '⊆',
|
||||||
description: '值在指定范围内时触发',
|
description: '值在指定范围内时触发',
|
||||||
example: 'temperature between 20,30',
|
example: 'temperature between 20,30',
|
||||||
supportedTypes: ['int', 'float', 'double', 'date']
|
supportedTypes: ['int', 'float', 'double', 'date']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'contains',
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN.value,
|
||||||
label: '包含',
|
label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN.name,
|
||||||
symbol: '⊃',
|
symbol: '⊄',
|
||||||
description: '字符串包含指定内容时触发',
|
description: '值不在指定范围内时触发',
|
||||||
example: 'message contains "error"',
|
example: 'temperature not between 20,30',
|
||||||
|
supportedTypes: ['int', 'float', 'double', 'date']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.LIKE.value,
|
||||||
|
label: IotRuleSceneTriggerConditionParameterOperatorEnum.LIKE.name,
|
||||||
|
symbol: '≈',
|
||||||
|
description: '字符串匹配指定模式时触发',
|
||||||
|
example: 'message like "%error%"',
|
||||||
supportedTypes: ['string']
|
supportedTypes: ['string']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'startsWith',
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_NULL.value,
|
||||||
label: '开始于',
|
label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_NULL.name,
|
||||||
symbol: '⊢',
|
symbol: '≠∅',
|
||||||
description: '字符串以指定内容开始时触发',
|
description: '值非空时触发',
|
||||||
example: 'deviceName startsWith "sensor"',
|
example: 'data not null',
|
||||||
supportedTypes: ['string']
|
supportedTypes: ['int', 'float', 'double', 'string', 'bool', 'enum', 'date']
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'endsWith',
|
|
||||||
label: '结束于',
|
|
||||||
symbol: '⊣',
|
|
||||||
description: '字符串以指定内容结束时触发',
|
|
||||||
example: 'fileName endsWith ".log"',
|
|
||||||
supportedTypes: ['string']
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,14 @@
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between w-full py-4px">
|
<div class="flex items-center justify-between w-full py-4px">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ product.name }}</div>
|
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px"
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ product.productKey }}</div>
|
>{{ product.name }}
|
||||||
|
</div>
|
||||||
|
<div class="text-12px text-[var(--el-text-color-secondary)]"
|
||||||
|
>{{ product.productKey }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- TODO @puhui999:是不是用字典 -->
|
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" />
|
||||||
<el-tag size="small" :type="product.status === 0 ? 'success' : 'danger'">
|
|
||||||
{{ product.status === 0 ? '正常' : '禁用' }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
</div>
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
@ -70,8 +71,12 @@
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between w-full py-4px">
|
<div class="flex items-center justify-between w-full py-4px">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ device.deviceName }}</div>
|
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px"
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ device.nickname || '无备注' }}</div>
|
>{{ device.deviceName }}
|
||||||
|
</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,7 +89,10 @@
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<!-- 选择结果展示 -->
|
<!-- 选择结果展示 -->
|
||||||
<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
|
||||||
|
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="flex items-center gap-6px mb-8px">
|
<div class="flex items-center gap-6px mb-8px">
|
||||||
<Icon icon="ep:check" class="text-[var(--el-color-success)] text-16px" />
|
<Icon icon="ep:check" class="text-[var(--el-color-success)] text-16px" />
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">已选择设备</span>
|
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">已选择设备</span>
|
||||||
|
|
@ -92,14 +100,22 @@
|
||||||
<div class="flex flex-col gap-6px ml-22px">
|
<div class="flex flex-col gap-6px ml-22px">
|
||||||
<div class="flex items-center gap-8px">
|
<div class="flex items-center gap-8px">
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-40px">产品:</span>
|
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-40px">产品:</span>
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] font-500">{{ 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="flex items-center gap-8px">
|
<div class="flex items-center gap-8px">
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-40px">设备:</span>
|
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-40px">设备:</span>
|
||||||
<span v-if="deviceSelectionMode === 'all'" class="text-12px text-[var(--el-text-color-primary)] font-500">全部设备</span>
|
<span
|
||||||
<span v-else class="text-12px text-[var(--el-text-color-primary)] font-500">{{ selectedDevice?.deviceName }}</span>
|
v-if="deviceSelectionMode === 'all'"
|
||||||
<el-tag v-if="deviceSelectionMode === 'all'" size="small" type="warning"> 全部 </el-tag>
|
class="text-12px text-[var(--el-text-color-primary)] font-500"
|
||||||
|
>全部设备</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-else size="small" :type="getDeviceStatusTag(selectedDevice?.state)">
|
<el-tag v-else size="small" :type="getDeviceStatusTag(selectedDevice?.state)">
|
||||||
{{ getDeviceStatusText(selectedDevice?.state) }}
|
{{ getDeviceStatusText(selectedDevice?.state) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
|
|
@ -113,6 +129,7 @@
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import { ProductApi } from '@/api/iot/product/product'
|
import { ProductApi } from '@/api/iot/product/product'
|
||||||
import { DeviceApi } from '@/api/iot/device/device'
|
import { DeviceApi } from '@/api/iot/device/device'
|
||||||
|
import { DICT_TYPE } from '@/utils/dict'
|
||||||
|
|
||||||
/** 产品设备选择器组件 */
|
/** 产品设备选择器组件 */
|
||||||
defineOptions({ name: 'ProductDeviceSelector' })
|
defineOptions({ name: 'ProductDeviceSelector' })
|
||||||
|
|
@ -124,7 +141,9 @@ interface Props {
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'update:productId', value?: number): void
|
(e: 'update:productId', value?: number): void
|
||||||
|
|
||||||
(e: 'update:deviceId', value?: number): void
|
(e: 'update:deviceId', value?: number): void
|
||||||
|
|
||||||
(e: 'change', value: { productId?: number; deviceId?: number }): void
|
(e: 'change', value: { productId?: number; deviceId?: number }): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,16 +17,14 @@
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between w-full py-4px">
|
<div class="flex items-center justify-between w-full py-4px">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{
|
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px"
|
||||||
product.name
|
>{{ product.name }}
|
||||||
}}</div>
|
</div>
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)]">{{
|
<div class="text-12px text-[var(--el-text-color-secondary)]"
|
||||||
product.productKey
|
>{{ product.productKey }}
|
||||||
}}</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-tag size="small" :type="product.status === 0 ? 'success' : 'danger'">
|
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" />
|
||||||
{{ product.status === 0 ? '正常' : '禁用' }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
</div>
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
@ -34,21 +32,19 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ProductApi } from '@/api/iot/product/product'
|
import { ProductApi } from '@/api/iot/product/product'
|
||||||
|
import { DICT_TYPE } from '@/utils/dict'
|
||||||
|
|
||||||
/** 产品选择器组件 */
|
/** 产品选择器组件 */
|
||||||
defineOptions({ name: 'ProductSelector' })
|
defineOptions({ name: 'ProductSelector' })
|
||||||
|
|
||||||
interface Props {
|
defineProps<{
|
||||||
modelValue?: number
|
modelValue?: number
|
||||||
}
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value?: number): void
|
(e: 'update:modelValue', value?: number): void
|
||||||
(e: 'change', value?: number): void
|
(e: 'change', value?: number): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
const productLoading = ref(false)
|
const productLoading = ref(false)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
<!-- 属性选择器组件 -->
|
<!-- 属性选择器组件 -->
|
||||||
<!-- TODO @yunai:可能要在 review 下 -->
|
<!-- TODO @yunai:可能要在 review 下 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="flex items-center gap-8px">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="localValue"
|
v-model="localValue"
|
||||||
placeholder="请选择监控项"
|
placeholder="请选择监控项"
|
||||||
filterable
|
filterable
|
||||||
clearable
|
clearable
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
class="w-full"
|
class="!w-150px"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
>
|
>
|
||||||
<el-option-group v-for="group in propertyGroups" :key="group.label" :label="group.label">
|
<el-option-group v-for="group in propertyGroups" :key="group.label" :label="group.label">
|
||||||
|
|
@ -20,8 +20,12 @@
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between w-full py-4px">
|
<div class="flex items-center justify-between w-full py-4px">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ property.name }}</div>
|
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ property.identifier }}</div>
|
{{ property.name }}
|
||||||
|
</div>
|
||||||
|
<div class="text-12px text-[var(--el-text-color-secondary)]">
|
||||||
|
{{ property.identifier }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<el-tag :type="getPropertyTypeTag(property.dataType)" size="small">
|
<el-tag :type="getPropertyTypeTag(property.dataType)" size="small">
|
||||||
|
|
@ -33,61 +37,114 @@
|
||||||
</el-option-group>
|
</el-option-group>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
||||||
<!-- 属性详情 -->
|
<!-- 属性详情触发按钮 -->
|
||||||
<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="relative">
|
||||||
<div class="flex items-center gap-8px mb-12px">
|
<el-button
|
||||||
<Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-16px" />
|
v-if="selectedProperty"
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ selectedProperty.name }}</span>
|
ref="detailTriggerRef"
|
||||||
<el-tag :type="getPropertyTypeTag(selectedProperty.dataType)" size="small">
|
type="info"
|
||||||
{{ getPropertyTypeName(selectedProperty.dataType) }}
|
:icon="InfoFilled"
|
||||||
</el-tag>
|
circle
|
||||||
</div>
|
size="small"
|
||||||
<div class="space-y-8px ml-24px">
|
@click="togglePropertyDetail"
|
||||||
<div class="flex items-start gap-8px">
|
class="flex-shrink-0"
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">标识符:</span>
|
title="查看属性详情"
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.identifier }}</span>
|
/>
|
||||||
|
|
||||||
|
<!-- 属性详情弹出层 -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="showPropertyDetail && selectedProperty"
|
||||||
|
ref="propertyDetailRef"
|
||||||
|
class="property-detail-popover"
|
||||||
|
:style="popoverStyle"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-300px max-w-400px"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-8px mb-12px">
|
||||||
|
<Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-4px" />
|
||||||
|
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">
|
||||||
|
{{ selectedProperty.name }}
|
||||||
|
</span>
|
||||||
|
<el-tag :type="getPropertyTypeTag(selectedProperty.dataType)" size="small">
|
||||||
|
{{ getPropertyTypeName(selectedProperty.dataType) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-8px ml-24px">
|
||||||
|
<div class="flex items-start gap-8px">
|
||||||
|
<span
|
||||||
|
class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
|
||||||
|
>
|
||||||
|
标识符:
|
||||||
|
</span>
|
||||||
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
||||||
|
{{ selectedProperty.identifier }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedProperty.description" class="flex items-start gap-8px">
|
||||||
|
<span
|
||||||
|
class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
|
||||||
|
>
|
||||||
|
描述:
|
||||||
|
</span>
|
||||||
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
||||||
|
{{ selectedProperty.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedProperty.unit" class="flex items-start gap-8px">
|
||||||
|
<span
|
||||||
|
class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
|
||||||
|
>
|
||||||
|
单位:
|
||||||
|
</span>
|
||||||
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
||||||
|
{{ selectedProperty.unit }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedProperty.range" class="flex items-start gap-8px">
|
||||||
|
<span
|
||||||
|
class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
|
||||||
|
>
|
||||||
|
取值范围:
|
||||||
|
</span>
|
||||||
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
||||||
|
{{ selectedProperty.range }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 关闭按钮 -->
|
||||||
|
<div class="flex justify-end mt-12px">
|
||||||
|
<el-button size="small" @click="hidePropertyDetail">关闭</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedProperty.description" class="flex items-start gap-8px">
|
</Teleport>
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">描述:</span>
|
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.description }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedProperty.unit" class="flex items-start gap-8px">
|
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">单位:</span>
|
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.unit }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedProperty.range" class="flex items-start gap-8px">
|
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">取值范围:</span>
|
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.range }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import { IotRuleSceneTriggerTypeEnum } from '@/api/iot/rule/scene/scene.types'
|
import { InfoFilled } from '@element-plus/icons-vue'
|
||||||
|
import { IotRuleSceneTriggerTypeEnum, IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
|
||||||
import { ThingModelApi } from '@/api/iot/thingmodel'
|
import { ThingModelApi } from '@/api/iot/thingmodel'
|
||||||
import { IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
|
import type { IotThingModelTSLRespVO, PropertySelectorItem } from '@/api/iot/rule/scene/scene.types'
|
||||||
import type { IotThingModelTSLRespVO, PropertySelectorItem } from './types'
|
|
||||||
|
|
||||||
/** 属性选择器组件 */
|
/** 属性选择器组件 */
|
||||||
defineOptions({ name: 'PropertySelector' })
|
defineOptions({ name: 'PropertySelector' })
|
||||||
|
|
||||||
interface Props {
|
const props = defineProps<{
|
||||||
modelValue?: string
|
modelValue?: string
|
||||||
triggerType: number
|
triggerType: number
|
||||||
productId?: number
|
productId?: number
|
||||||
deviceId?: number
|
deviceId?: number
|
||||||
}
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: string): void
|
(e: 'update:modelValue', value: string): void
|
||||||
(e: 'change', value: { type: string; config: any }): void
|
(e: 'change', value: { type: string; config: any }): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const localValue = useVModel(props, 'modelValue', emit)
|
const localValue = useVModel(props, 'modelValue', emit)
|
||||||
|
|
||||||
|
|
@ -96,6 +153,25 @@ const loading = ref(false)
|
||||||
const propertyList = ref<PropertySelectorItem[]>([])
|
const propertyList = ref<PropertySelectorItem[]>([])
|
||||||
const thingModelTSL = ref<IotThingModelTSLRespVO | null>(null)
|
const thingModelTSL = ref<IotThingModelTSLRespVO | null>(null)
|
||||||
|
|
||||||
|
// 属性详情弹出层相关状态
|
||||||
|
const showPropertyDetail = ref(false)
|
||||||
|
const detailTriggerRef = ref()
|
||||||
|
const propertyDetailRef = ref()
|
||||||
|
const popoverStyle = ref({})
|
||||||
|
|
||||||
|
// 点击外部关闭弹出层
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
showPropertyDetail.value &&
|
||||||
|
propertyDetailRef.value &&
|
||||||
|
detailTriggerRef.value &&
|
||||||
|
!propertyDetailRef.value.contains(event.target as Node) &&
|
||||||
|
!detailTriggerRef.value.$el.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
hidePropertyDetail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const propertyGroups = computed(() => {
|
const propertyGroups = computed(() => {
|
||||||
const groups: { label: string; options: any[] }[] = []
|
const groups: { label: string; options: any[] }[] = []
|
||||||
|
|
@ -159,6 +235,67 @@ const getPropertyTypeTag = (dataType: string) => {
|
||||||
return tagMap[dataType] || 'info'
|
return tagMap[dataType] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 弹出层控制方法
|
||||||
|
const togglePropertyDetail = () => {
|
||||||
|
if (showPropertyDetail.value) {
|
||||||
|
hidePropertyDetail()
|
||||||
|
} else {
|
||||||
|
showPropertyDetailPopover()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showPropertyDetailPopover = () => {
|
||||||
|
if (!selectedProperty.value || !detailTriggerRef.value) return
|
||||||
|
|
||||||
|
showPropertyDetail.value = true
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
updatePopoverPosition()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hidePropertyDetail = () => {
|
||||||
|
showPropertyDetail.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePopoverPosition = () => {
|
||||||
|
if (!detailTriggerRef.value || !propertyDetailRef.value) return
|
||||||
|
|
||||||
|
const triggerEl = detailTriggerRef.value.$el
|
||||||
|
const triggerRect = triggerEl.getBoundingClientRect()
|
||||||
|
const popoverEl = propertyDetailRef.value
|
||||||
|
|
||||||
|
// 计算弹出层位置
|
||||||
|
const left = triggerRect.left + triggerRect.width + 8
|
||||||
|
const top = triggerRect.top
|
||||||
|
|
||||||
|
// 检查是否超出视窗右边界
|
||||||
|
const popoverWidth = 400 // 最大宽度
|
||||||
|
const viewportWidth = window.innerWidth
|
||||||
|
|
||||||
|
let finalLeft = left
|
||||||
|
if (left + popoverWidth > viewportWidth - 16) {
|
||||||
|
// 如果超出右边界,显示在左侧
|
||||||
|
finalLeft = triggerRect.left - popoverWidth - 8
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否超出视窗下边界
|
||||||
|
let finalTop = top
|
||||||
|
const popoverHeight = popoverEl.offsetHeight || 200
|
||||||
|
const viewportHeight = window.innerHeight
|
||||||
|
|
||||||
|
if (top + popoverHeight > viewportHeight - 16) {
|
||||||
|
finalTop = Math.max(16, viewportHeight - popoverHeight - 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
popoverStyle.value = {
|
||||||
|
position: 'fixed',
|
||||||
|
left: `${finalLeft}px`,
|
||||||
|
top: `${finalTop}px`,
|
||||||
|
zIndex: 9999
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 事件处理
|
// 事件处理
|
||||||
const handleChange = (value: string) => {
|
const handleChange = (value: string) => {
|
||||||
const property = propertyList.value.find((p) => p.identifier === value)
|
const property = propertyList.value.find((p) => p.identifier === value)
|
||||||
|
|
@ -168,6 +305,8 @@ const handleChange = (value: string) => {
|
||||||
config: property
|
config: property
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// 选择变化时隐藏详情弹出层
|
||||||
|
hidePropertyDetail()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取物模型TSL数据
|
// 获取物模型TSL数据
|
||||||
|
|
@ -331,13 +470,74 @@ watch(
|
||||||
() => props.triggerType,
|
() => props.triggerType,
|
||||||
() => {
|
() => {
|
||||||
localValue.value = ''
|
localValue.value = ''
|
||||||
|
hidePropertyDetail()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 监听窗口大小变化,重新计算弹出层位置
|
||||||
|
const handleResize = () => {
|
||||||
|
if (showPropertyDetail.value) {
|
||||||
|
updatePopoverPosition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', handleClickOutside)
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@keyframes fadeInScale {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9) translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-select-dropdown__item) {
|
:deep(.el-select-dropdown__item) {
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 8px 20px;
|
padding: 8px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.property-detail-popover {
|
||||||
|
animation: fadeInScale 0.2s ease-out;
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹出层箭头效果(可选) */
|
||||||
|
.property-detail-popover::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: -8px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 8px solid transparent;
|
||||||
|
border-right: 8px solid var(--el-border-color);
|
||||||
|
border-bottom: 8px solid transparent;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-detail-popover::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: -7px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 8px solid transparent;
|
||||||
|
border-right: 8px solid white;
|
||||||
|
border-bottom: 8px solid transparent;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
// IoT物模型TSL数据类型定义
|
|
||||||
|
|
||||||
// TODO @puhui999:看看这些里面,是不是一些已经有了哈?可以复用下~
|
|
||||||
|
|
||||||
/** 物模型TSL响应数据结构 */
|
|
||||||
export interface IotThingModelTSLRespVO {
|
|
||||||
productId: number
|
|
||||||
productKey: string
|
|
||||||
properties: ThingModelProperty[]
|
|
||||||
events: ThingModelEvent[]
|
|
||||||
services: ThingModelService[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 物模型属性 */
|
|
||||||
export interface ThingModelProperty {
|
|
||||||
identifier: string
|
|
||||||
name: string
|
|
||||||
accessMode: string
|
|
||||||
required?: boolean
|
|
||||||
dataType: string
|
|
||||||
description?: string
|
|
||||||
dataSpecs?: ThingModelDataSpecs
|
|
||||||
dataSpecsList?: ThingModelDataSpecs[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 物模型事件 */
|
|
||||||
export interface ThingModelEvent {
|
|
||||||
identifier: string
|
|
||||||
name: string
|
|
||||||
required?: boolean
|
|
||||||
type: string
|
|
||||||
description?: string
|
|
||||||
outputParams?: ThingModelParam[]
|
|
||||||
method?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 物模型服务 */
|
|
||||||
export interface ThingModelService {
|
|
||||||
identifier: string
|
|
||||||
name: string
|
|
||||||
required?: boolean
|
|
||||||
callType: string
|
|
||||||
description?: string
|
|
||||||
inputParams?: ThingModelParam[]
|
|
||||||
outputParams?: ThingModelParam[]
|
|
||||||
method?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 物模型参数 */
|
|
||||||
export interface ThingModelParam {
|
|
||||||
identifier: string
|
|
||||||
name: string
|
|
||||||
direction: string
|
|
||||||
paraOrder?: number
|
|
||||||
dataType: string
|
|
||||||
dataSpecs?: ThingModelDataSpecs
|
|
||||||
dataSpecsList?: ThingModelDataSpecs[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 数值型数据规范 */
|
|
||||||
export interface ThingModelNumericDataSpec {
|
|
||||||
dataType: 'int' | 'float' | 'double'
|
|
||||||
max: string
|
|
||||||
min: string
|
|
||||||
step: string
|
|
||||||
precise?: string
|
|
||||||
defaultValue?: string
|
|
||||||
unit?: string
|
|
||||||
unitName?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 布尔/枚举型数据规范 */
|
|
||||||
export interface ThingModelBoolOrEnumDataSpecs {
|
|
||||||
dataType: 'bool' | 'enum'
|
|
||||||
name: string
|
|
||||||
value: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 文本/时间型数据规范 */
|
|
||||||
export interface ThingModelDateOrTextDataSpecs {
|
|
||||||
dataType: 'text' | 'date'
|
|
||||||
length?: number
|
|
||||||
defaultValue?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 数组型数据规范 */
|
|
||||||
export interface ThingModelArrayDataSpecs {
|
|
||||||
dataType: 'array'
|
|
||||||
size: number
|
|
||||||
childDataType: string
|
|
||||||
dataSpecsList?: ThingModelDataSpecs[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 结构体型数据规范 */
|
|
||||||
export interface ThingModelStructDataSpecs {
|
|
||||||
dataType: 'struct'
|
|
||||||
identifier: string
|
|
||||||
name: string
|
|
||||||
accessMode: string
|
|
||||||
required?: boolean
|
|
||||||
childDataType: string
|
|
||||||
dataSpecs?: ThingModelDataSpecs
|
|
||||||
dataSpecsList?: ThingModelDataSpecs[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 数据规范联合类型 */
|
|
||||||
export type ThingModelDataSpecs =
|
|
||||||
| ThingModelNumericDataSpec
|
|
||||||
| ThingModelBoolOrEnumDataSpecs
|
|
||||||
| ThingModelDateOrTextDataSpecs
|
|
||||||
| ThingModelArrayDataSpecs
|
|
||||||
| ThingModelStructDataSpecs
|
|
||||||
|
|
||||||
/** 属性选择器内部使用的统一数据结构 */
|
|
||||||
export interface PropertySelectorItem {
|
|
||||||
identifier: string
|
|
||||||
name: string
|
|
||||||
description?: string
|
|
||||||
dataType: string
|
|
||||||
type: number // IoTThingModelTypeEnum
|
|
||||||
accessMode?: string
|
|
||||||
required?: boolean
|
|
||||||
unit?: string
|
|
||||||
range?: string
|
|
||||||
eventType?: string
|
|
||||||
callType?: string
|
|
||||||
inputParams?: ThingModelParam[]
|
|
||||||
outputParams?: ThingModelParam[]
|
|
||||||
property?: ThingModelProperty
|
|
||||||
event?: ThingModelEvent
|
|
||||||
service?: ThingModelService
|
|
||||||
}
|
|
||||||
|
|
@ -270,7 +270,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 表单对话框 -->
|
<!-- 表单对话框 -->
|
||||||
<RuleSceneForm v-model="formVisible" :rule-scene="currentRule" @success="getList" />
|
<RuleSceneForm v-model="formVisible" @success="getList" />
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,188 +0,0 @@
|
||||||
// TODO @puhui999:貌似很多地方,都用不到啦?这个文件
|
|
||||||
/**
|
|
||||||
* IoT 场景联动表单验证工具函数
|
|
||||||
*/
|
|
||||||
import { FormValidationRules, TriggerConfig, ActionConfig } from '@/api/iot/rule/scene/scene.types'
|
|
||||||
import {
|
|
||||||
IotRuleSceneTriggerTypeEnum,
|
|
||||||
IotRuleSceneActionTypeEnum,
|
|
||||||
CommonStatusEnum
|
|
||||||
} from '@/api/iot/rule/scene/scene.types'
|
|
||||||
|
|
||||||
/** 基础表单验证规则 */
|
|
||||||
export const getBaseValidationRules = (): FormValidationRules => ({
|
|
||||||
name: [
|
|
||||||
{ required: true, message: '场景名称不能为空', trigger: 'blur' },
|
|
||||||
{ type: 'string', min: 1, max: 50, message: '场景名称长度应在1-50个字符之间', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
status: [
|
|
||||||
{ required: true, message: '场景状态不能为空', trigger: 'change' },
|
|
||||||
{
|
|
||||||
type: 'enum',
|
|
||||||
enum: [CommonStatusEnum.ENABLE, CommonStatusEnum.DISABLE],
|
|
||||||
message: '状态值必须为启用或禁用',
|
|
||||||
trigger: 'change'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
description: [
|
|
||||||
{ type: 'string', max: 200, message: '场景描述不能超过200个字符', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
triggers: [
|
|
||||||
{ required: true, message: '触发器数组不能为空', trigger: 'change' },
|
|
||||||
{ type: 'array', min: 1, message: '至少需要一个触发器', trigger: 'change' }
|
|
||||||
],
|
|
||||||
actions: [
|
|
||||||
{ required: true, message: '执行器数组不能为空', trigger: 'change' },
|
|
||||||
{ type: 'array', min: 1, message: '至少需要一个执行器', trigger: 'change' }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 验证CRON表达式格式 */
|
|
||||||
// TODO @puhui999:这个可以拿到 cron 组件里哇?
|
|
||||||
export function validateCronExpression(cron: string): boolean {
|
|
||||||
if (!cron || cron.trim().length === 0) return false
|
|
||||||
// 基础的 CRON 表达式正则验证(支持 6 位和 7 位格式)
|
|
||||||
const cronRegex =
|
|
||||||
/^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\*|([0-9]|1[0-9]|2[0-3])|\*\/([0-9]|1[0-9]|2[0-3])) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\*\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\*|([1-9]|1[0-2])|\*\/([1-9]|1[0-2])) (\*|([0-6])|\*\/([0-6]))( (\*|([1-9][0-9]{3})|\*\/([1-9][0-9]{3})))?$/
|
|
||||||
return cronRegex.test(cron.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 验证设备名称数组 */
|
|
||||||
export function validateDeviceNames(deviceNames: string[]): boolean {
|
|
||||||
return (
|
|
||||||
Array.isArray(deviceNames) &&
|
|
||||||
deviceNames.length > 0 &&
|
|
||||||
deviceNames.every((name) => name && name.trim().length > 0)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 验证比较值格式 */
|
|
||||||
export function validateCompareValue(operator: string, value: string): boolean {
|
|
||||||
if (!value || value.trim().length === 0) return false
|
|
||||||
const trimmedValue = value.trim()
|
|
||||||
// TODO @puhui999:这里要用下枚举哇?
|
|
||||||
switch (operator) {
|
|
||||||
case 'between':
|
|
||||||
case 'not between':
|
|
||||||
const betweenValues = trimmedValue.split(',')
|
|
||||||
return (
|
|
||||||
betweenValues.length === 2 &&
|
|
||||||
betweenValues.every((v) => v.trim().length > 0) &&
|
|
||||||
!isNaN(Number(betweenValues[0].trim())) &&
|
|
||||||
!isNaN(Number(betweenValues[1].trim()))
|
|
||||||
)
|
|
||||||
case 'in':
|
|
||||||
case 'not in':
|
|
||||||
const inValues = trimmedValue.split(',')
|
|
||||||
return inValues.length > 0 && inValues.every((v) => v.trim().length > 0)
|
|
||||||
case '>':
|
|
||||||
case '>=':
|
|
||||||
case '<':
|
|
||||||
case '<=':
|
|
||||||
return !isNaN(Number(trimmedValue))
|
|
||||||
case '=':
|
|
||||||
case '!=':
|
|
||||||
case 'like':
|
|
||||||
case 'not null':
|
|
||||||
// TODO @puhui999:这里要不加个 default 抛出异常?
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @puhui999:貌似没用到?
|
|
||||||
/** 验证触发器配置 */
|
|
||||||
export function validateTriggerConfig(trigger: TriggerConfig): {
|
|
||||||
valid: boolean
|
|
||||||
message?: string
|
|
||||||
} {
|
|
||||||
if (!trigger.type) {
|
|
||||||
return { valid: false, message: '触发类型不能为空' }
|
|
||||||
}
|
|
||||||
// 定时触发验证
|
|
||||||
if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
|
|
||||||
if (!trigger.cronExpression) {
|
|
||||||
return { valid: false, message: 'CRON表达式不能为空' }
|
|
||||||
}
|
|
||||||
if (!validateCronExpression(trigger.cronExpression)) {
|
|
||||||
return { valid: false, message: 'CRON表达式格式不正确' }
|
|
||||||
}
|
|
||||||
return { valid: true }
|
|
||||||
}
|
|
||||||
// 设备触发验证
|
|
||||||
if (!trigger.productKey) {
|
|
||||||
return { valid: false, message: '产品标识不能为空' }
|
|
||||||
}
|
|
||||||
if (!trigger.deviceNames || !validateDeviceNames(trigger.deviceNames)) {
|
|
||||||
return { valid: false, message: '设备名称不能为空' }
|
|
||||||
}
|
|
||||||
// 设备状态变更无需额外条件验证
|
|
||||||
if (trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
|
|
||||||
return { valid: true }
|
|
||||||
}
|
|
||||||
// 其他设备触发类型需要验证条件
|
|
||||||
if (!trigger.conditions || trigger.conditions.length === 0) {
|
|
||||||
return { valid: false, message: '触发条件不能为空' }
|
|
||||||
}
|
|
||||||
// 验证每个条件的参数
|
|
||||||
for (const condition of trigger.conditions) {
|
|
||||||
if (!condition.parameters || condition.parameters.length === 0) {
|
|
||||||
return { valid: false, message: '触发条件参数不能为空' }
|
|
||||||
}
|
|
||||||
for (const param of condition.parameters) {
|
|
||||||
if (!param.operator) {
|
|
||||||
return { valid: false, message: '操作符不能为空' }
|
|
||||||
}
|
|
||||||
if (!validateCompareValue(param.operator, param.value)) {
|
|
||||||
return { valid: false, message: `操作符 "${param.operator}" 对应的比较值格式不正确` }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { valid: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @puhui999:貌似没用到?
|
|
||||||
/** 验证执行器配置 */
|
|
||||||
export function validateActionConfig(action: ActionConfig): { valid: boolean; message?: string } {
|
|
||||||
if (!action.type) {
|
|
||||||
return { valid: false, message: '执行类型不能为空' }
|
|
||||||
}
|
|
||||||
// 告警触发/恢复验证
|
|
||||||
if (
|
|
||||||
action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER ||
|
|
||||||
action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER
|
|
||||||
) {
|
|
||||||
if (!action.alertConfigId) {
|
|
||||||
return { valid: false, message: '告警配置ID不能为空' }
|
|
||||||
}
|
|
||||||
return { valid: true }
|
|
||||||
}
|
|
||||||
// 设备控制验证
|
|
||||||
if (
|
|
||||||
action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
|
|
||||||
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
|
|
||||||
) {
|
|
||||||
if (!action.deviceControl) {
|
|
||||||
return { valid: false, message: '设备控制配置不能为空' }
|
|
||||||
}
|
|
||||||
const { deviceControl } = action
|
|
||||||
if (!deviceControl.productKey) {
|
|
||||||
return { valid: false, message: '产品标识不能为空' }
|
|
||||||
}
|
|
||||||
if (!deviceControl.deviceNames || !validateDeviceNames(deviceControl.deviceNames)) {
|
|
||||||
return { valid: false, message: '设备名称不能为空' }
|
|
||||||
}
|
|
||||||
if (!deviceControl.type) {
|
|
||||||
return { valid: false, message: '消息类型不能为空' }
|
|
||||||
}
|
|
||||||
if (!deviceControl.identifier) {
|
|
||||||
return { valid: false, message: '消息标识符不能为空' }
|
|
||||||
}
|
|
||||||
if (!deviceControl.params || Object.keys(deviceControl.params).length === 0) {
|
|
||||||
return { valid: false, message: '参数不能为空' }
|
|
||||||
}
|
|
||||||
return { valid: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { valid: false, message: '未知的执行类型' }
|
|
||||||
}
|
|
||||||
|
|
@ -203,3 +203,100 @@ export const IoTOtaTaskRecordStatusEnum = {
|
||||||
value: 50
|
value: 50
|
||||||
}
|
}
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
// ========== 场景联动规则相关常量 ==========
|
||||||
|
|
||||||
|
/** IoT 场景联动触发器类型枚举 */
|
||||||
|
export const IotRuleSceneTriggerTypeEnum = {
|
||||||
|
DEVICE_STATE_UPDATE: 1, // 设备上下线变更
|
||||||
|
DEVICE_PROPERTY_POST: 2, // 物模型属性上报
|
||||||
|
DEVICE_EVENT_POST: 3, // 设备事件上报
|
||||||
|
DEVICE_SERVICE_INVOKE: 4, // 设备服务调用
|
||||||
|
TIMER: 100 // 定时触发
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/** 触发器类型选项配置 */
|
||||||
|
export const getTriggerTypeOptions = () => [
|
||||||
|
{
|
||||||
|
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 const isDeviceTrigger = (type: number): boolean => {
|
||||||
|
const deviceTriggerTypes = [
|
||||||
|
IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
|
||||||
|
IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
|
||||||
|
IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
|
||||||
|
IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||||
|
] as number[]
|
||||||
|
return deviceTriggerTypes.includes(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 场景联动规则执行器相关常量 ==========
|
||||||
|
|
||||||
|
/** IoT 场景联动执行器类型枚举 */
|
||||||
|
export const IotRuleSceneActionTypeEnum = {
|
||||||
|
DEVICE_PROPERTY_SET: 1, // 设备属性设置
|
||||||
|
DEVICE_SERVICE_INVOKE: 2, // 设备服务调用
|
||||||
|
ALERT_TRIGGER: 100, // 告警触发
|
||||||
|
ALERT_RECOVER: 101 // 告警恢复
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/** IoT 设备消息类型枚举 */
|
||||||
|
export const IotDeviceMessageTypeEnum = {
|
||||||
|
PROPERTY: 'property', // 属性
|
||||||
|
SERVICE: 'service', // 服务
|
||||||
|
EVENT: 'event' // 事件
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/** IoT 场景联动触发条件参数操作符枚举 */
|
||||||
|
export const IotRuleSceneTriggerConditionParameterOperatorEnum = {
|
||||||
|
EQUALS: { name: '等于', value: '=' }, // 等于
|
||||||
|
NOT_EQUALS: { name: '不等于', value: '!=' }, // 不等于
|
||||||
|
GREATER_THAN: { name: '大于', value: '>' }, // 大于
|
||||||
|
GREATER_THAN_OR_EQUALS: { name: '大于等于', value: '>=' }, // 大于等于
|
||||||
|
LESS_THAN: { name: '小于', value: '<' }, // 小于
|
||||||
|
LESS_THAN_OR_EQUALS: { name: '小于等于', value: '<=' }, // 小于等于
|
||||||
|
IN: { name: '在...之中', value: 'in' }, // 在...之中
|
||||||
|
NOT_IN: { name: '不在...之中', value: 'not in' }, // 不在...之中
|
||||||
|
BETWEEN: { name: '在...之间', value: 'between' }, // 在...之间
|
||||||
|
NOT_BETWEEN: { name: '不在...之间', value: 'not between' }, // 不在...之间
|
||||||
|
LIKE: { name: '字符串匹配', value: 'like' }, // 字符串匹配
|
||||||
|
NOT_NULL: { name: '非空', value: 'not null' } // 非空
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/** IoT 场景联动触发条件类型枚举 */
|
||||||
|
export const IotRuleSceneTriggerConditionTypeEnum = {
|
||||||
|
DEVICE_STATUS: 1, // 设备状态
|
||||||
|
DEVICE_PROPERTY: 2, // 设备属性
|
||||||
|
CURRENT_TIME: 3 // 当前时间
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/** IoT 场景联动触发时间操作符枚举 */
|
||||||
|
export 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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue