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