!802 perf:【IoT 物联网】场景联动优化数据结构对齐后端

Merge pull request !802 from puhui999/feature/iot
pull/803/MERGE
芋道源码 2025-08-02 02:53:35 +00:00 committed by Gitee
commit 11e78b78a7
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
27 changed files with 1172 additions and 1651 deletions

View File

@ -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
}

View File

@ -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 @puhui999Emits 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>

View File

@ -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()

View File

@ -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>

View File

@ -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()
},

View File

@ -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

View File

@ -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)

View File

@ -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()
}

View File

@ -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 PropsEmits
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 })

View File

@ -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>

View File

@ -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) => {

View File

@ -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()
},

View File

@ -25,18 +25,13 @@ import { Crontab } from '@/components/Crontab'
/** 定时触发配置组件 */
defineOptions({ name: 'TimerTriggerConfig' })
// TODO @puhui999 PropsEmits
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 * * ?'

View File

@ -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>

View File

@ -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 @puhui999dict-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 PropsEmits
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>

View File

@ -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 PropsEmits
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 @puhui999updateTriggerDeviceConfig
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 @puhui999updateTriggerCronConfig
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>

View File

@ -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' })

View File

@ -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 = [

View File

@ -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)

View File

@ -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']
}
]

View File

@ -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
}

View File

@ -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)

View File

@ -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>

View File

@ -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
}

View File

@ -270,7 +270,7 @@
</div>
<!-- 表单对话框 -->
<RuleSceneForm v-model="formVisible" :rule-scene="currentRule" @success="getList" />
<RuleSceneForm v-model="formVisible" @success="getList" />
</ContentWrap>
</template>

View File

@ -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: '未知的执行类型' }
}

View File

@ -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