perf:【IoT 物联网】场景联动触发器优化

pull/800/head
puhui999 2025-07-28 21:38:27 +08:00
parent 929bcb4059
commit d7b4db9b4e
15 changed files with 1815 additions and 453 deletions

View File

@ -46,6 +46,24 @@ const IotRuleSceneTriggerConditionParameterOperatorEnum = {
NOT_NULL: { name: '非空', value: 'not null' } // 非空
} as const
// 条件类型枚举
const IotRuleSceneTriggerConditionTypeEnum = {
DEVICE_STATUS: 1, // 设备状态
DEVICE_PROPERTY: 2, // 设备属性
CURRENT_TIME: 3 // 当前时间
} as const
// 时间运算符枚举
const IotRuleSceneTriggerTimeOperatorEnum = {
BEFORE_TIME: { name: '在时间之前', value: 'before_time' }, // 在时间之前
AFTER_TIME: { name: '在时间之后', value: 'after_time' }, // 在时间之后
BETWEEN_TIME: { name: '在时间之间', value: 'between_time' }, // 在时间之间
AT_TIME: { name: '在指定时间', value: 'at_time' }, // 在指定时间
BEFORE_TODAY: { name: '在今日之前', value: 'before_today' }, // 在今日之前
AFTER_TODAY: { name: '在今日之后', value: 'after_today' }, // 在今日之后
TODAY: { name: '在今日之间', value: 'today' } // 在今日之间
} as const
// TODO @puhui999下面 IotAlertConfigReceiveTypeEnum、DeviceStateEnum 没用到,貌似可以删除下?
const IotAlertConfigReceiveTypeEnum = {
SMS: 1, // 短信
@ -126,7 +144,7 @@ interface RuleSceneFormData {
name: string
description?: string
status: number
triggers: TriggerFormData[]
trigger: TriggerFormData // 改为单个触发器
actions: ActionFormData[]
}
@ -138,7 +156,9 @@ interface TriggerFormData {
operator?: string
value?: string
cronExpression?: string
conditionGroups?: ConditionGroupFormData[]
// 新的条件结构
mainCondition?: ConditionFormData // 主条件(必须满足)
conditionGroup?: ConditionGroupContainerFormData // 条件组容器(可选,与主条件为且关系)
}
interface ActionFormData {
@ -149,6 +169,17 @@ interface ActionFormData {
alertConfigId?: number
}
// 条件组容器(包含多个子条件组,子条件组间为或关系)
interface ConditionGroupContainerFormData {
subGroups: SubConditionGroupFormData[] // 子条件组数组,子条件组间为或关系
}
// 子条件组(内部条件为且关系)
interface SubConditionGroupFormData {
conditions: ConditionFormData[] // 条件数组,条件间为且关系
}
// 保留原有接口用于兼容性
interface ConditionGroupFormData {
conditions: ConditionFormData[]
// 注意:条件组内部的条件固定为"且"关系,条件组之间固定为"或"关系
@ -157,12 +188,14 @@ interface ConditionGroupFormData {
}
interface ConditionFormData {
type: number
productId: number
deviceId: number
identifier: string
operator: string
param: string
type: number // 条件类型1-设备状态2-设备属性3-当前时间
productId?: number // 产品ID设备状态和设备属性时必填
deviceId?: number // 设备ID设备状态和设备属性时必填
identifier?: string // 标识符(设备属性时必填)
operator: string // 操作符
param: string // 参数值
timeValue?: string // 时间值(当前时间条件时使用)
timeValue2?: string // 第二个时间值(时间范围条件时使用)
}
// 主接口
@ -210,12 +243,16 @@ export {
TriggerFormData,
ActionFormData,
ConditionGroupFormData,
ConditionGroupContainerFormData,
SubConditionGroupFormData,
ConditionFormData,
IotRuleSceneTriggerTypeEnum,
IotRuleSceneActionTypeEnum,
IotDeviceMessageTypeEnum,
IotDeviceMessageIdentifierEnum,
IotRuleSceneTriggerConditionParameterOperatorEnum,
IotRuleSceneTriggerConditionTypeEnum,
IotRuleSceneTriggerTimeOperatorEnum,
IotAlertConfigReceiveTypeEnum,
DeviceStateEnum,
CommonStatusEnum,

View File

@ -22,7 +22,7 @@
<BasicInfoSection v-model="formData" :rules="formRules" />
<!-- 触发器配置 -->
<TriggerSection v-model:triggers="formData.triggers" @validate="handleTriggerValidate" />
<TriggerSection v-model:trigger="formData.trigger" @validate="handleTriggerValidate" />
<!-- 执行器配置 -->
<ActionSection v-model:actions="formData.actions" @validate="handleActionValidate" />
@ -45,6 +45,7 @@ import {
RuleSceneFormData,
IotRuleScene,
IotRuleSceneActionTypeEnum,
IotRuleSceneTriggerTypeEnum,
CommonStatusEnum
} from '@/api/iot/rule/scene/scene.types'
import { getBaseValidationRules } from '../utils/validation'
@ -77,7 +78,17 @@ const createDefaultFormData = (): RuleSceneFormData => {
name: '',
description: '',
status: CommonStatusEnum.ENABLE, //
triggers: [],
trigger: {
type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
productId: undefined,
deviceId: undefined,
identifier: undefined,
operator: undefined,
value: undefined,
cronExpression: undefined,
mainCondition: undefined,
conditionGroup: undefined
},
actions: []
}
}
@ -91,23 +102,19 @@ const transformFormToApi = (formData: RuleSceneFormData): IotRuleScene => {
name: formData.name,
description: formData.description,
status: Number(formData.status),
triggers:
formData.triggers?.map((trigger) => ({
type: trigger.type,
productKey: trigger.productId ? `product_${trigger.productId}` : undefined,
deviceNames: trigger.deviceId ? [`device_${trigger.deviceId}`] : undefined,
cronExpression: trigger.cronExpression,
conditions:
trigger.conditionGroups?.map((group) => ({
type: 'property',
identifier: trigger.identifier || '',
parameters: group.conditions.map((condition) => ({
identifier: condition.identifier,
operator: condition.operator,
value: condition.param
}))
})) || []
})) || [],
triggers: [
{
type: formData.trigger.type,
productKey: formData.trigger.productId
? `product_${formData.trigger.productId}`
: undefined,
deviceNames: formData.trigger.deviceId
? [`device_${formData.trigger.deviceId}`]
: undefined,
cronExpression: formData.trigger.cronExpression,
conditions: [] // TODO:
}
],
actions:
formData.actions?.map((action) => ({
type: action.type,
@ -131,16 +138,26 @@ const transformFormToApi = (formData: RuleSceneFormData): IotRuleScene => {
* API 响应数据转换为表单格式
*/
const transformApiToForm = (apiData: IotRuleScene): RuleSceneFormData => {
const firstTrigger = apiData.triggers?.[0]
return {
...apiData,
status: Number(apiData.status), //
triggers:
apiData.triggers?.map((trigger) => ({
...trigger,
type: Number(trigger.type),
//
key: generateUUID()
})) || [],
trigger: firstTrigger
? {
...firstTrigger,
type: Number(firstTrigger.type)
}
: {
type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
productId: undefined,
deviceId: undefined,
identifier: undefined,
operator: undefined,
value: undefined,
cronExpression: undefined,
mainCondition: undefined,
conditionGroup: undefined
},
actions:
apiData.actions?.map((action) => ({
...action,

View File

@ -1,60 +1,121 @@
<!-- 单个条件配置组件 -->
<!-- TODO @puhui999这里需要在对下阿里云 IoT不太对它是条件类型然后选择产品设备接着选条件类型对应的比较 -->
<template>
<div class="flex flex-col gap-16px">
<!-- 条件类型选择 -->
<el-row :gutter="16">
<!-- 属性/事件/服务选择 -->
<el-col :span="8">
<el-form-item label="监控项" required>
<PropertySelector
:model-value="condition.identifier"
@update:model-value="(value) => updateConditionField('identifier', value)"
:trigger-type="triggerType"
:product-id="productId"
:device-id="deviceId"
@change="handlePropertyChange"
/>
</el-form-item>
</el-col>
<!-- 操作符选择 -->
<el-col :span="6">
<el-form-item label="操作符" required>
<OperatorSelector
:model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)"
:property-type="propertyType"
@change="handleOperatorChange"
/>
</el-form-item>
</el-col>
<!-- 值输入 -->
<el-col :span="10">
<el-form-item label="比较值" required>
<ValueInput
:model-value="condition.param"
@update:model-value="(value) => updateConditionField('param', value)"
:property-type="propertyType"
:operator="condition.operator"
:property-config="propertyConfig"
@validate="handleValueValidate"
<el-form-item label="条件类型" required>
<ConditionTypeSelector
:model-value="condition.type"
@update:model-value="(value) => updateConditionField('type', value)"
@change="handleConditionTypeChange"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 条件预览 -->
<div v-if="conditionPreview" class="p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]">
<div class="flex items-center gap-8px mb-8px">
<Icon icon="ep:view" class="text-[var(--el-color-info)] text-16px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">条件预览</span>
</div>
<div class="pl-24px">
<code class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] p-8px rounded-4px font-mono">{{ conditionPreview }}</code>
<!-- 设备状态条件配置 -->
<DeviceStatusConditionConfig
v-if="condition.type === ConditionTypeEnum.DEVICE_STATUS"
:model-value="condition"
@update:model-value="updateCondition"
@validate="handleValidate"
/>
<!-- 设备属性条件配置 -->
<div v-else-if="condition.type === ConditionTypeEnum.DEVICE_PROPERTY" class="space-y-16px">
<!-- 产品设备选择 -->
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="产品" required>
<ProductSelector
:model-value="condition.productId"
@update:model-value="(value) => updateConditionField('productId', value)"
@change="handleProductChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备" required>
<DeviceSelector
:model-value="condition.deviceId"
@update:model-value="(value) => updateConditionField('deviceId', value)"
:product-id="condition.productId"
@change="handleDeviceChange"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 属性配置 -->
<el-row :gutter="16">
<!-- 属性/事件/服务选择 -->
<el-col :span="6">
<el-form-item label="监控项" required>
<PropertySelector
:model-value="condition.identifier"
@update:model-value="(value) => updateConditionField('identifier', value)"
:trigger-type="triggerType"
:product-id="condition.productId"
:device-id="condition.deviceId"
@change="handlePropertyChange"
/>
</el-form-item>
</el-col>
<!-- 操作符选择 -->
<el-col :span="6">
<el-form-item label="操作符" required>
<OperatorSelector
:model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)"
:property-type="propertyType"
@change="handleOperatorChange"
/>
</el-form-item>
</el-col>
<!-- 值输入 -->
<el-col :span="12">
<el-form-item label="比较值" required>
<ValueInput
:model-value="condition.param"
@update:model-value="(value) => updateConditionField('param', value)"
:property-type="propertyType"
:operator="condition.operator"
:property-config="propertyConfig"
@validate="handleValueValidate"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 条件预览 -->
<div
v-if="conditionPreview"
class="p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"
>
<div class="flex items-center gap-8px mb-8px">
<Icon icon="ep:view" class="text-[var(--el-color-info)] text-16px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">条件预览</span>
</div>
<div class="pl-24px">
<code
class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] p-8px rounded-4px font-mono"
>{{ conditionPreview }}</code
>
</div>
</div>
</div>
<!-- 当前时间条件配置 -->
<CurrentTimeConditionConfig
v-else-if="condition.type === ConditionTypeEnum.CURRENT_TIME"
:model-value="condition"
@update:model-value="updateCondition"
@validate="handleValidate"
/>
<!-- 验证结果 -->
<div v-if="validationMessage" class="mt-8px">
<el-alert
@ -69,10 +130,18 @@
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import ConditionTypeSelector from '../selectors/ConditionTypeSelector.vue'
import DeviceStatusConditionConfig from './DeviceStatusConditionConfig.vue'
import CurrentTimeConditionConfig from './CurrentTimeConditionConfig.vue'
import ProductSelector from '../selectors/ProductSelector.vue'
import DeviceSelector from '../selectors/DeviceSelector.vue'
import PropertySelector from '../selectors/PropertySelector.vue'
import OperatorSelector from '../selectors/OperatorSelector.vue'
import ValueInput from '../inputs/ValueInput.vue'
import { ConditionFormData } from '@/api/iot/rule/scene/scene.types'
import {
ConditionFormData,
IotRuleSceneTriggerConditionTypeEnum
} from '@/api/iot/rule/scene/scene.types'
/** 单个条件配置组件 */
defineOptions({ name: 'ConditionConfig' })
@ -80,8 +149,6 @@ defineOptions({ name: 'ConditionConfig' })
interface Props {
modelValue: ConditionFormData
triggerType: number
productId?: number
deviceId?: number
}
interface Emits {
@ -94,6 +161,9 @@ const emit = defineEmits<Emits>()
const condition = useVModel(props, 'modelValue', emit)
//
const ConditionTypeEnum = IotRuleSceneTriggerConditionTypeEnum
//
const propertyType = ref<string>('string')
const propertyConfig = ref<any>(null)
@ -131,10 +201,56 @@ const getOperatorText = (operator: string) => {
//
const updateConditionField = (field: keyof ConditionFormData, value: any) => {
condition.value[field] = value
;(condition.value as any)[field] = value
emit('update:modelValue', condition.value)
}
const updateCondition = (newCondition: ConditionFormData) => {
condition.value = newCondition
emit('update:modelValue', condition.value)
}
const handleConditionTypeChange = (type: number) => {
//
if (type === ConditionTypeEnum.DEVICE_STATUS) {
condition.value.identifier = undefined
condition.value.timeValue = undefined
condition.value.timeValue2 = undefined
} else if (type === ConditionTypeEnum.CURRENT_TIME) {
condition.value.identifier = undefined
condition.value.productId = undefined
condition.value.deviceId = undefined
} else if (type === ConditionTypeEnum.DEVICE_PROPERTY) {
condition.value.timeValue = undefined
condition.value.timeValue2 = undefined
}
//
condition.value.operator = '='
condition.value.param = ''
updateValidationResult()
}
const handleValidate = (result: { valid: boolean; message: string }) => {
isValid.value = result.valid
validationMessage.value = result.message
emit('validate', result)
}
const handleProductChange = (productId: number) => {
//
condition.value.deviceId = undefined
condition.value.identifier = ''
updateValidationResult()
}
const handleDeviceChange = (deviceId: number) => {
//
condition.value.identifier = ''
updateValidationResult()
}
const handlePropertyChange = (propertyInfo: { type: string; config: any }) => {
propertyType.value = propertyInfo.type
propertyConfig.value = propertyInfo.config

View File

@ -128,6 +128,7 @@ interface Props {
triggerType: number
productId?: number
deviceId?: number
maxConditions?: number
}
interface Emits {
@ -141,7 +142,7 @@ const emit = defineEmits<Emits>()
const group = useVModel(props, 'modelValue', emit)
//
const maxConditions = 5
const maxConditions = computed(() => props.maxConditions || 3)
//
const conditionValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({})
@ -172,12 +173,12 @@ const addCondition = () => {
group.value.conditions = []
}
if (group.value.conditions.length >= maxConditions) {
if (group.value.conditions.length >= maxConditions.value) {
return
}
const newCondition: ConditionFormData = {
type: props.triggerType,
type: 2, //
productId: props.productId || 0,
deviceId: props.deviceId || 0,
identifier: '',

View File

@ -0,0 +1,247 @@
<!-- 条件组容器配置组件 -->
<template>
<div class="flex flex-col gap-16px">
<!-- 条件组容器头部 -->
<div
class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-8px"
>
<div class="flex items-center gap-12px">
<div class="flex items-center gap-8px text-16px font-600 text-green-700">
<div
class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
>
</div>
<span>附加条件组</span>
</div>
<el-tag size="small" type="success">与主条件为且关系</el-tag>
<el-tag size="small" type="info">
{{ modelValue.subGroups?.length || 0 }}个子条件组
</el-tag>
</div>
<div class="flex items-center gap-8px">
<el-button
type="primary"
size="small"
@click="addSubGroup"
:disabled="(modelValue.subGroups?.length || 0) >= maxSubGroups"
>
<Icon icon="ep:plus" />
添加子条件组
</el-button>
<el-button type="danger" size="small" text @click="removeContainer">
<Icon icon="ep:delete" />
删除条件组
</el-button>
</div>
</div>
<!-- 子条件组列表 -->
<div v-if="modelValue.subGroups && modelValue.subGroups.length > 0" class="space-y-16px">
<!-- 逻辑关系说明 -->
<div v-if="modelValue.subGroups.length > 1" class="flex items-center justify-center">
<div
class="flex items-center gap-8px px-12px py-6px bg-orange-50 border border-orange-200 rounded-full text-12px text-orange-600"
>
<Icon icon="ep:info-filled" />
<span>子条件组之间为"或"关系满足任意一组即可触发</span>
</div>
</div>
<div class="relative">
<div
v-for="(subGroup, subGroupIndex) in modelValue.subGroups"
:key="`sub-group-${subGroupIndex}`"
class="relative"
>
<!-- 子条件组容器 -->
<div
class="border-2 border-orange-200 rounded-8px bg-orange-50 shadow-sm hover:shadow-md transition-shadow"
>
<div
class="flex items-center justify-between p-16px bg-gradient-to-r from-orange-50 to-yellow-50 border-b border-orange-200 rounded-t-6px"
>
<div class="flex items-center gap-12px">
<div class="flex items-center gap-8px text-16px font-600 text-orange-700">
<div
class="w-24px h-24px bg-orange-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
>
{{ subGroupIndex + 1 }}
</div>
<span>子条件组 {{ subGroupIndex + 1 }}</span>
</div>
<el-tag size="small" type="warning" class="font-500">组内条件为"且"关系</el-tag>
<el-tag size="small" type="info">
{{ subGroup.conditions?.length || 0 }}个条件
</el-tag>
</div>
<el-button
type="danger"
size="small"
text
@click="removeSubGroup(subGroupIndex)"
class="hover:bg-red-50"
>
<Icon icon="ep:delete" />
删除组
</el-button>
</div>
<SubConditionGroupConfig
:model-value="subGroup"
@update:model-value="(value) => updateSubGroup(subGroupIndex, value)"
:trigger-type="triggerType"
:max-conditions="maxConditionsPerGroup"
@validate="(result) => handleSubGroupValidate(subGroupIndex, result)"
/>
</div>
<!-- 子条件组间的"或"连接符 -->
<div
v-if="subGroupIndex < modelValue.subGroups!.length - 1"
class="flex items-center justify-center py-12px"
>
<div class="flex items-center gap-8px">
<!-- 连接线 -->
<div class="w-32px h-1px bg-orange-300"></div>
<!-- 或标签 -->
<div class="px-16px py-6px bg-orange-100 border-2 border-orange-300 rounded-full">
<span class="text-14px font-600 text-orange-600"></span>
</div>
<!-- 连接线 -->
<div class="w-32px h-1px bg-orange-300"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div
v-else
class="p-24px border-2 border-dashed border-orange-200 rounded-8px text-center bg-orange-50"
>
<div class="flex flex-col items-center gap-12px">
<Icon icon="ep:plus" class="text-32px text-orange-400" />
<div class="text-orange-600">
<p class="text-14px font-500 mb-4px">暂无子条件组</p>
<p class="text-12px">点击上方"添加子条件组"按钮开始配置</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import SubConditionGroupConfig from './SubConditionGroupConfig.vue'
import {
ConditionGroupContainerFormData,
SubConditionGroupFormData
} from '@/api/iot/rule/scene/scene.types'
/** 条件组容器配置组件 */
defineOptions({ name: 'ConditionGroupContainerConfig' })
interface Props {
modelValue: ConditionGroupContainerFormData
triggerType: number
}
interface Emits {
(e: 'update:modelValue', value: ConditionGroupContainerFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void
(e: 'remove'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const container = useVModel(props, 'modelValue', emit)
//
const maxSubGroups = 3 // 3
const maxConditionsPerGroup = 3 // 3
//
const subGroupValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({})
//
const addSubGroup = () => {
if (!container.value.subGroups) {
container.value.subGroups = []
}
if (container.value.subGroups.length >= maxSubGroups) {
return
}
const newSubGroup: SubConditionGroupFormData = {
conditions: []
}
container.value.subGroups.push(newSubGroup)
}
const removeSubGroup = (index: number) => {
if (container.value.subGroups) {
container.value.subGroups.splice(index, 1)
delete subGroupValidations.value[index]
//
const newValidations: { [key: number]: { valid: boolean; message: string } } = {}
Object.keys(subGroupValidations.value).forEach((key) => {
const numKey = parseInt(key)
if (numKey > index) {
newValidations[numKey - 1] = subGroupValidations.value[numKey]
} else if (numKey < index) {
newValidations[numKey] = subGroupValidations.value[numKey]
}
})
subGroupValidations.value = newValidations
updateValidationResult()
}
}
const updateSubGroup = (index: number, subGroup: SubConditionGroupFormData) => {
if (container.value.subGroups) {
container.value.subGroups[index] = subGroup
}
}
const removeContainer = () => {
emit('remove')
}
const handleSubGroupValidate = (index: number, result: { valid: boolean; message: string }) => {
subGroupValidations.value[index] = result
updateValidationResult()
}
const updateValidationResult = () => {
if (!container.value.subGroups || container.value.subGroups.length === 0) {
emit('validate', { valid: true, message: '条件组容器为空,验证通过' })
return
}
const validations = Object.values(subGroupValidations.value)
const allValid = validations.every((v: any) => v.valid)
if (allValid) {
emit('validate', { valid: true, message: '条件组容器配置验证通过' })
} else {
const errorMessages = validations.filter((v: any) => !v.valid).map((v: any) => v.message)
emit('validate', { valid: false, message: `子条件组配置错误: ${errorMessages.join('; ')}` })
}
}
//
watch(
() => container.value.subGroups,
() => {
updateValidationResult()
},
{ deep: true, immediate: true }
)
</script>

View File

@ -0,0 +1,287 @@
<!-- 当前时间条件配置组件 -->
<template>
<div class="flex flex-col gap-16px">
<div class="flex items-center gap-8px p-12px px-16px bg-orange-50 rounded-6px border border-orange-200">
<Icon icon="ep:timer" class="text-orange-500 text-18px" />
<span class="text-14px font-500 text-orange-700">当前时间条件配置</span>
</div>
<el-row :gutter="16">
<!-- 时间操作符选择 -->
<el-col :span="8">
<el-form-item label="时间条件" required>
<el-select
:model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)"
placeholder="请选择时间条件"
class="w-full"
>
<el-option
v-for="option in timeOperatorOptions"
:key="option.value"
:label="option.label"
:value="option.value"
>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-8px">
<Icon :icon="option.icon" :class="option.iconClass" />
<span>{{ option.label }}</span>
</div>
<el-tag :type="option.tag" size="small">{{ option.category }}</el-tag>
</div>
</el-option>
</el-select>
</el-form-item>
</el-col>
<!-- 时间值输入 -->
<el-col :span="8">
<el-form-item label="时间值" required>
<el-time-picker
v-if="needsTimeInput"
:model-value="condition.timeValue"
@update:model-value="(value) => updateConditionField('timeValue', value)"
placeholder="请选择时间"
format="HH:mm:ss"
value-format="HH:mm:ss"
class="w-full"
/>
<el-date-picker
v-else-if="needsDateInput"
:model-value="condition.timeValue"
@update:model-value="(value) => updateConditionField('timeValue', value)"
type="datetime"
placeholder="请选择日期时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
class="w-full"
/>
<div v-else class="text-[var(--el-text-color-placeholder)] text-14px">
无需设置时间值
</div>
</el-form-item>
</el-col>
<!-- 第二个时间值范围条件 -->
<el-col :span="8" v-if="needsSecondTimeInput">
<el-form-item label="结束时间" required>
<el-time-picker
v-if="needsTimeInput"
:model-value="condition.timeValue2"
@update:model-value="(value) => updateConditionField('timeValue2', value)"
placeholder="请选择结束时间"
format="HH:mm:ss"
value-format="HH:mm:ss"
class="w-full"
/>
<el-date-picker
v-else
:model-value="condition.timeValue2"
@update:model-value="(value) => updateConditionField('timeValue2', value)"
type="datetime"
placeholder="请选择结束日期时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
class="w-full"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 条件预览 -->
<div v-if="conditionPreview" class="p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]">
<div class="flex items-center gap-8px mb-8px">
<Icon icon="ep:view" class="text-[var(--el-color-info)] text-16px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">条件预览</span>
</div>
<div class="pl-24px">
<code class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] p-8px rounded-4px font-mono">{{ conditionPreview }}</code>
</div>
</div>
<!-- 验证结果 -->
<div v-if="validationMessage" class="mt-8px">
<el-alert
:title="validationMessage"
:type="isValid ? 'success' : 'error'"
:closable="false"
show-icon
/>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { ConditionFormData, IotRuleSceneTriggerTimeOperatorEnum } from '@/api/iot/rule/scene/scene.types'
/** 当前时间条件配置组件 */
defineOptions({ name: 'CurrentTimeConditionConfig' })
interface Props {
modelValue: ConditionFormData
}
interface Emits {
(e: 'update:modelValue', value: ConditionFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const condition = useVModel(props, 'modelValue', emit)
//
const timeOperatorOptions = [
{
value: IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.value,
label: IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.name,
icon: 'ep:arrow-left',
iconClass: 'text-blue-500',
tag: 'primary',
category: '时间点'
},
{
value: IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.value,
label: IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.name,
icon: 'ep:arrow-right',
iconClass: 'text-green-500',
tag: 'success',
category: '时间点'
},
{
value: IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value,
label: IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.name,
icon: 'ep:sort',
iconClass: 'text-orange-500',
tag: 'warning',
category: '时间段'
},
{
value: IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.value,
label: IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.name,
icon: 'ep:position',
iconClass: 'text-purple-500',
tag: 'info',
category: '时间点'
},
{
value: IotRuleSceneTriggerTimeOperatorEnum.TODAY.value,
label: IotRuleSceneTriggerTimeOperatorEnum.TODAY.name,
icon: 'ep:calendar',
iconClass: 'text-red-500',
tag: 'danger',
category: '日期'
}
]
//
const validationMessage = ref('')
const isValid = ref(true)
//
const needsTimeInput = computed(() => {
const timeOnlyOperators = [
IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.value,
IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.value,
IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value,
IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.value
]
return timeOnlyOperators.includes(condition.value.operator)
})
const needsDateInput = computed(() => {
return false //
})
const needsSecondTimeInput = computed(() => {
return condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value
})
const conditionPreview = computed(() => {
if (!condition.value.operator) {
return ''
}
const operatorOption = timeOperatorOptions.find(opt => opt.value === condition.value.operator)
const operatorLabel = operatorOption?.label || condition.value.operator
if (condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
return `当前时间 ${operatorLabel}`
}
if (!condition.value.timeValue) {
return `当前时间 ${operatorLabel} [未设置时间]`
}
if (needsSecondTimeInput.value && condition.value.timeValue2) {
return `当前时间 ${operatorLabel} ${condition.value.timeValue}${condition.value.timeValue2}`
}
return `当前时间 ${operatorLabel} ${condition.value.timeValue}`
})
//
const updateConditionField = (field: keyof ConditionFormData, value: any) => {
condition.value[field] = value
updateValidationResult()
}
const updateValidationResult = () => {
if (!condition.value.operator) {
isValid.value = false
validationMessage.value = '请选择时间条件'
emit('validate', { valid: false, message: validationMessage.value })
return
}
//
if (condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
isValid.value = true
validationMessage.value = '当前时间条件配置验证通过'
emit('validate', { valid: true, message: validationMessage.value })
return
}
if (needsTimeInput.value && !condition.value.timeValue) {
isValid.value = false
validationMessage.value = '请设置时间值'
emit('validate', { valid: false, message: validationMessage.value })
return
}
if (needsSecondTimeInput.value && !condition.value.timeValue2) {
isValid.value = false
validationMessage.value = '请设置结束时间'
emit('validate', { valid: false, message: validationMessage.value })
return
}
isValid.value = true
validationMessage.value = '当前时间条件配置验证通过'
emit('validate', { valid: true, message: validationMessage.value })
}
//
watch(
() => [condition.value.operator, condition.value.timeValue, condition.value.timeValue2],
() => {
updateValidationResult()
},
{ immediate: true }
)
//
watch(
() => condition.value.operator,
(newOperator) => {
if (newOperator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
condition.value.timeValue = undefined
condition.value.timeValue2 = undefined
} else if (!needsSecondTimeInput.value) {
condition.value.timeValue2 = undefined
}
}
)
</script>

View File

@ -0,0 +1,258 @@
<!-- 设备状态条件配置组件 -->
<template>
<div class="flex flex-col gap-16px">
<div
class="flex items-center gap-8px p-12px px-16px bg-blue-50 rounded-6px border border-blue-200"
>
<Icon icon="ep:connection" class="text-blue-500 text-18px" />
<span class="text-14px font-500 text-blue-700">设备状态条件配置</span>
</div>
<!-- 产品设备选择 -->
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="产品" required>
<ProductSelector
:model-value="condition.productId"
@update:model-value="(value) => updateConditionField('productId', value)"
@change="handleProductChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备" required>
<DeviceSelector
:model-value="condition.deviceId"
@update:model-value="(value) => updateConditionField('deviceId', value)"
:product-id="condition.productId"
@change="handleDeviceChange"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 状态和操作符选择 -->
<el-row :gutter="16">
<!-- 状态选择 -->
<el-col :span="12">
<el-form-item label="设备状态" required>
<el-select
:model-value="condition.param"
@update:model-value="(value) => updateConditionField('param', value)"
placeholder="请选择设备状态"
class="w-full"
>
<el-option
v-for="option in deviceStatusOptions"
:key="option.value"
:label="option.label"
:value="option.value"
>
<div class="flex items-center gap-8px">
<Icon :icon="option.icon" :class="option.iconClass" />
<span>{{ option.label }}</span>
<el-tag :type="option.tag" size="small">{{ option.description }}</el-tag>
</div>
</el-option>
</el-select>
</el-form-item>
</el-col>
<!-- 操作符选择 -->
<el-col :span="12">
<el-form-item label="操作符" required>
<el-select
:model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)"
placeholder="请选择操作符"
class="w-full"
>
<el-option
v-for="option in statusOperatorOptions"
:key="option.value"
:label="option.label"
:value="option.value"
>
<div class="flex items-center justify-between w-full">
<span>{{ option.label }}</span>
<span class="text-12px text-[var(--el-text-color-secondary)]">{{
option.description
}}</span>
</div>
</el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 条件预览 -->
<div
v-if="conditionPreview"
class="p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"
>
<div class="flex items-center gap-8px mb-8px">
<Icon icon="ep:view" class="text-[var(--el-color-info)] text-16px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">条件预览</span>
</div>
<div class="pl-24px">
<code
class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] p-8px rounded-4px font-mono"
>{{ conditionPreview }}</code
>
</div>
</div>
<!-- 验证结果 -->
<div v-if="validationMessage" class="mt-8px">
<el-alert
:title="validationMessage"
:type="isValid ? 'success' : 'error'"
:closable="false"
show-icon
/>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import ProductSelector from '../selectors/ProductSelector.vue'
import DeviceSelector from '../selectors/DeviceSelector.vue'
import { ConditionFormData } from '@/api/iot/rule/scene/scene.types'
/** 设备状态条件配置组件 */
defineOptions({ name: 'DeviceStatusConditionConfig' })
interface Props {
modelValue: ConditionFormData
}
interface Emits {
(e: 'update:modelValue', value: ConditionFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const condition = useVModel(props, 'modelValue', emit)
//
const deviceStatusOptions = [
{
value: 'online',
label: '在线',
description: '设备已连接',
icon: 'ep:circle-check',
iconClass: 'text-green-500',
tag: 'success'
},
{
value: 'offline',
label: '离线',
description: '设备已断开',
icon: 'ep:circle-close',
iconClass: 'text-red-500',
tag: 'danger'
}
]
//
const statusOperatorOptions = [
{
value: '=',
label: '等于',
description: '状态完全匹配时触发'
},
{
value: '!=',
label: '不等于',
description: '状态不匹配时触发'
}
]
//
const validationMessage = ref('')
const isValid = ref(true)
//
const conditionPreview = computed(() => {
if (!condition.value.param || !condition.value.operator) {
return ''
}
const statusLabel =
deviceStatusOptions.find((opt) => opt.value === condition.value.param)?.label ||
condition.value.param
const operatorLabel =
statusOperatorOptions.find((opt) => opt.value === condition.value.operator)?.label ||
condition.value.operator
return `设备状态 ${operatorLabel} ${statusLabel}`
})
//
const updateConditionField = (field: keyof ConditionFormData, value: any) => {
condition.value[field] = value
updateValidationResult()
}
const handleProductChange = (productId: number) => {
//
condition.value.deviceId = undefined
updateValidationResult()
}
const handleDeviceChange = (deviceId: number) => {
//
updateValidationResult()
}
const updateValidationResult = () => {
if (!condition.value.productId) {
isValid.value = false
validationMessage.value = '请选择产品'
emit('validate', { valid: false, message: validationMessage.value })
return
}
if (!condition.value.deviceId) {
isValid.value = false
validationMessage.value = '请选择设备'
emit('validate', { valid: false, message: validationMessage.value })
return
}
if (!condition.value.param) {
isValid.value = false
validationMessage.value = '请选择设备状态'
emit('validate', { valid: false, message: validationMessage.value })
return
}
if (!condition.value.operator) {
isValid.value = false
validationMessage.value = '请选择操作符'
emit('validate', { valid: false, message: validationMessage.value })
return
}
isValid.value = true
validationMessage.value = '设备状态条件配置验证通过'
emit('validate', { valid: true, message: validationMessage.value })
}
//
watch(
() => [
condition.value.productId,
condition.value.deviceId,
condition.value.param,
condition.value.operator
],
() => {
updateValidationResult()
},
{ immediate: true }
)
</script>

View File

@ -8,118 +8,56 @@
@change="handleDeviceChange"
/>
<!-- 条件组配置 -->
<div v-if="needsConditions" class="space-y-12px">
<div class="flex items-center justify-between mb-12px">
<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="info">
{{ trigger.conditionGroups?.length || 0 }}个条件组
</el-tag>
<el-tooltip
content="条件组之间为'或'关系,满足任意一组即可触发;每个条件组内的条件为'且'关系,需要全部满足"
placement="top"
>
<Icon icon="ep:question-filled" class="text-[var(--el-color-info)] cursor-help" />
</el-tooltip>
</div>
<div class="flex items-center gap-8px">
<el-button
type="primary"
size="small"
@click="addConditionGroup"
:disabled="(trigger.conditionGroups?.length || 0) >= maxConditionGroups"
>
<Icon icon="ep:plus" />
添加条件组
</el-button>
</div>
</div>
<!-- 条件组列表 -->
<!-- 主条件配置 -->
<div v-if="needsConditions" class="space-y-16px">
<div
v-if="trigger.conditionGroups && trigger.conditionGroups.length > 0"
class="space-y-16px"
class="flex items-center gap-8px p-12px px-16px bg-blue-50 rounded-6px border border-blue-200"
>
<!-- 逻辑关系说明 -->
<div v-if="trigger.conditionGroups.length > 1" class="flex items-center justify-center">
<div
class="flex items-center gap-8px px-12px py-6px bg-blue-50 border border-blue-200 rounded-full text-12px text-blue-600"
>
<Icon icon="ep:info-filled" />
<span>条件组之间为"或"关系满足任意一组即可触发</span>
</div>
</div>
<div class="relative">
<div
v-for="(group, groupIndex) in trigger.conditionGroups"
:key="`group-${groupIndex}`"
class="relative"
>
<!-- 条件组容器 -->
<div
class="border-2 border-[var(--el-border-color-lighter)] rounded-8px bg-[var(--el-fill-color-blank)] shadow-sm hover:shadow-md transition-shadow"
>
<div
class="flex items-center justify-between p-16px bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-[var(--el-border-color-lighter)] rounded-t-6px"
>
<div class="flex items-center gap-12px">
<div
class="flex items-center gap-8px text-16px font-600 text-[var(--el-text-color-primary)]"
>
<div
class="w-24px h-24px bg-blue-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
>
{{ groupIndex + 1 }}
</div>
<span>条件组</span>
</div>
<el-tag size="small" type="success" class="font-500"> 组内条件为"且"关系 </el-tag>
</div>
<el-button
type="danger"
size="small"
text
@click="removeConditionGroup(groupIndex)"
v-if="trigger.conditionGroups!.length > 1"
class="hover:bg-red-50"
>
<Icon icon="ep:delete" />
删除组
</el-button>
</div>
<ConditionGroupConfig
:model-value="group"
@update:model-value="(value) => updateConditionGroup(groupIndex, value)"
:trigger-type="trigger.type"
:product-id="trigger.productId"
:device-id="trigger.deviceId"
@validate="(result) => handleGroupValidate(groupIndex, result)"
/>
</div>
<!-- 条件组间的"或"连接符 -->
<div
v-if="groupIndex < trigger.conditionGroups!.length - 1"
class="flex items-center justify-center py-12px"
>
<div class="flex items-center gap-8px">
<!-- 连接线 -->
<div class="w-32px h-1px bg-orange-300"></div>
<!-- 或标签 -->
<div class="px-16px py-6px bg-orange-100 border-2 border-orange-300 rounded-full">
<span class="text-14px font-600 text-orange-600"></span>
</div>
<!-- 连接线 -->
<div class="w-32px h-1px bg-orange-300"></div>
</div>
</div>
</div>
</div>
<Icon icon="ep:star-filled" class="text-blue-500 text-18px" />
<span class="text-14px font-600 text-blue-700">主条件配置</span>
<el-tag size="small" type="primary">必须满足</el-tag>
</div>
<MainConditionConfig
v-model="trigger.mainCondition"
:trigger-type="trigger.type"
@validate="handleMainConditionValidate"
/>
</div>
<!-- 条件组配置 -->
<div v-if="needsConditions && trigger.mainCondition" class="space-y-16px">
<div class="flex items-center justify-between">
<div
class="flex items-center gap-8px p-12px px-16px bg-green-50 rounded-6px border border-green-200"
>
<Icon icon="ep:connection" class="text-green-500 text-18px" />
<span class="text-14px font-600 text-green-700">附加条件组</span>
<el-tag size="small" type="success">与主条件为且关系</el-tag>
<el-tag size="small" type="info">
{{ trigger.conditionGroup?.subGroups?.length || 0 }}个子条件组
</el-tag>
</div>
<el-button
type="primary"
size="small"
@click="addConditionGroup"
v-if="!trigger.conditionGroup"
>
<Icon icon="ep:plus" />
添加条件组
</el-button>
</div>
<!-- 条件组配置 -->
<ConditionGroupContainerConfig
v-if="trigger.conditionGroup"
v-model="trigger.conditionGroup"
:trigger-type="trigger.type"
@validate="handleConditionGroupValidate"
@remove="removeConditionGroup"
/>
<!-- 空状态 -->
<div v-else class="py-40px text-center">
<el-empty description="暂无触发条件">
@ -140,10 +78,10 @@
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import ProductDeviceSelector from '../selectors/ProductDeviceSelector.vue'
import ConditionGroupConfig from './ConditionGroupConfig.vue'
import MainConditionConfig from './MainConditionConfig.vue'
import ConditionGroupContainerConfig from './ConditionGroupContainerConfig.vue'
import {
TriggerFormData,
ConditionGroupFormData,
IotRuleSceneTriggerTypeEnum as TriggerTypeEnum
} from '@/api/iot/rule/scene/scene.types'
@ -164,11 +102,11 @@ const emit = defineEmits<Emits>()
const trigger = useVModel(props, 'modelValue', emit)
//
const maxConditionGroups = 3
//
const groupValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({})
const mainConditionValidation = ref<{ valid: boolean; message: string }>({
valid: true,
message: ''
})
const validationMessage = ref('')
const isValid = ref(true)
@ -177,62 +115,35 @@ const needsConditions = computed(() => {
return trigger.value.type !== TriggerTypeEnum.DEVICE_STATE_UPDATE
})
//
const updateConditionGroup = (index: number, group: ConditionGroupFormData) => {
if (trigger.value.conditionGroups) {
trigger.value.conditionGroups[index] = group
//
const handleMainConditionValidate = (result: { valid: boolean; message: string }) => {
mainConditionValidation.value = result
updateValidationResult()
}
const addConditionGroup = () => {
if (!trigger.value.conditionGroup) {
trigger.value.conditionGroup = {
subGroups: []
}
}
}
//
const handleDeviceChange = ({ productId, deviceId }: { productId?: number; deviceId?: number }) => {
trigger.value.productId = productId
trigger.value.deviceId = deviceId
updateValidationResult()
}
const addConditionGroup = () => {
if (!trigger.value.conditionGroups) {
trigger.value.conditionGroups = []
}
if (trigger.value.conditionGroups.length >= maxConditionGroups) {
return
}
const newGroup: ConditionGroupFormData = {
conditions: [],
logicOperator: 'AND' // AND""
}
trigger.value.conditionGroups.push(newGroup)
}
const removeConditionGroup = (index: number) => {
if (trigger.value.conditionGroups) {
trigger.value.conditionGroups.splice(index, 1)
delete groupValidations.value[index]
//
const newValidations: { [key: number]: { valid: boolean; message: string } } = {}
Object.keys(groupValidations.value).forEach((key) => {
const numKey = parseInt(key)
if (numKey > index) {
newValidations[numKey - 1] = groupValidations.value[numKey]
} else if (numKey < index) {
newValidations[numKey] = groupValidations.value[numKey]
}
})
groupValidations.value = newValidations
updateValidationResult()
}
}
const handleGroupValidate = (index: number, result: { valid: boolean; message: string }) => {
groupValidations.value[index] = result
const handleConditionGroupValidate = (result: { valid: boolean; message: string }) => {
updateValidationResult()
}
const removeConditionGroup = () => {
trigger.value.conditionGroup = undefined
}
const updateValidationResult = () => {
//
if (!trigger.value.productId || !trigger.value.deviceId) {
@ -250,26 +161,24 @@ const updateValidationResult = () => {
return
}
//
if (!trigger.value.conditionGroups || trigger.value.conditionGroups.length === 0) {
//
if (!trigger.value.mainCondition) {
isValid.value = false
validationMessage.value = '请至少添加一个触发条件组'
validationMessage.value = '请配置主条件'
emit('validate', { valid: false, message: validationMessage.value })
return
}
const validations = Object.values(groupValidations.value)
const allValid = validations.every((v) => v.valid)
if (allValid) {
isValid.value = true
validationMessage.value = '设备触发配置验证通过'
} else {
//
if (!mainConditionValidation.value.valid) {
isValid.value = false
const errorMessages = validations.filter((v) => !v.valid).map((v) => v.message)
validationMessage.value = `条件组配置错误: ${errorMessages.join('; ')}`
validationMessage.value = `主条件配置错误: ${mainConditionValidation.value.message}`
emit('validate', { valid: false, message: validationMessage.value })
return
}
isValid.value = true
validationMessage.value = '设备触发配置验证通过'
emit('validate', { valid: isValid.value, message: validationMessage.value })
}

View File

@ -0,0 +1,114 @@
<!-- 主条件配置组件 -->
<template>
<div class="flex flex-col gap-16px">
<!-- 条件配置提示 -->
<div
v-if="!modelValue"
class="p-16px border-2 border-dashed border-[var(--el-border-color)] rounded-8px text-center"
>
<div class="flex flex-col items-center gap-12px">
<Icon icon="ep:plus" class="text-32px text-[var(--el-text-color-placeholder)]" />
<div class="text-[var(--el-text-color-secondary)]">
<p class="text-14px font-500 mb-4px">请配置主条件</p>
<p class="text-12px">主条件是触发器的核心条件必须满足才能触发场景</p>
</div>
<el-button type="primary" @click="addMainCondition">
<Icon icon="ep:plus" />
添加主条件
</el-button>
</div>
</div>
<!-- 主条件配置 -->
<div
v-else
class="border border-[var(--el-border-color-lighter)] rounded-8px bg-[var(--el-fill-color-blank)] shadow-sm"
>
<div
class="flex items-center justify-between p-16px bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-[var(--el-border-color-lighter)] rounded-t-6px"
>
<div class="flex items-center gap-12px">
<div
class="flex items-center gap-8px text-16px font-600 text-[var(--el-text-color-primary)]"
>
<div
class="w-24px h-24px bg-blue-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
>
</div>
<span>主条件</span>
</div>
<el-tag size="small" type="primary" class="font-500">必须满足</el-tag>
</div>
<el-button
type="danger"
size="small"
text
@click="removeMainCondition"
class="hover:bg-red-50"
>
<Icon icon="ep:delete" />
删除
</el-button>
</div>
<div class="p-16px">
<ConditionConfig
:model-value="modelValue"
@update:model-value="updateCondition"
:trigger-type="triggerType"
@validate="handleValidate"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import ConditionConfig from './ConditionConfig.vue'
import {
ConditionFormData,
IotRuleSceneTriggerConditionTypeEnum
} from '@/api/iot/rule/scene/scene.types'
/** 主条件配置组件 */
defineOptions({ name: 'MainConditionConfig' })
interface Props {
modelValue?: ConditionFormData
triggerType: number
}
interface Emits {
(e: 'update:modelValue', value?: ConditionFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
//
const addMainCondition = () => {
const newCondition: ConditionFormData = {
type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, //
productId: undefined,
deviceId: undefined,
identifier: '',
operator: '=',
param: ''
}
emit('update:modelValue', newCondition)
}
const removeMainCondition = () => {
emit('update:modelValue', undefined)
}
const updateCondition = (condition: ConditionFormData) => {
emit('update:modelValue', condition)
}
const handleValidate = (result: { valid: boolean; message: string }) => {
emit('validate', result)
}
</script>

View File

@ -0,0 +1,220 @@
<!-- 子条件组配置组件 -->
<template>
<div class="p-16px">
<!-- 空状态 -->
<div
v-if="!subGroup.conditions || subGroup.conditions.length === 0"
class="text-center py-24px"
>
<div class="flex flex-col items-center gap-12px">
<Icon icon="ep:plus" class="text-32px text-[var(--el-text-color-placeholder)]" />
<div class="text-[var(--el-text-color-secondary)]">
<p class="text-14px font-500 mb-4px">暂无条件</p>
<p class="text-12px">点击下方按钮添加第一个条件</p>
</div>
<el-button type="primary" @click="addCondition">
<Icon icon="ep:plus" />
添加条件
</el-button>
</div>
</div>
<!-- 条件列表 -->
<div v-else class="space-y-16px">
<div
v-for="(condition, conditionIndex) in subGroup.conditions"
:key="`condition-${conditionIndex}`"
class="relative"
>
<!-- 条件配置 -->
<div
class="border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)] shadow-sm"
>
<div
class="flex items-center justify-between p-12px bg-[var(--el-fill-color-light)] border-b border-[var(--el-border-color-lighter)] rounded-t-4px"
>
<div class="flex items-center gap-8px">
<div
class="w-20px h-20px bg-blue-500 text-white rounded-full flex items-center justify-center text-10px font-bold"
>
{{ conditionIndex + 1 }}
</div>
<span class="text-12px font-500 text-[var(--el-text-color-primary)]"
>条件 {{ conditionIndex + 1 }}</span
>
</div>
<el-button
type="danger"
size="small"
text
@click="removeCondition(conditionIndex)"
v-if="subGroup.conditions!.length > 1"
class="hover:bg-red-50"
>
<Icon icon="ep:delete" />
</el-button>
</div>
<div class="p-12px">
<ConditionConfig
:model-value="condition"
@update:model-value="(value) => updateCondition(conditionIndex, value)"
:trigger-type="triggerType"
@validate="(result) => handleConditionValidate(conditionIndex, result)"
/>
</div>
</div>
<!-- 条件间的"且"连接符 -->
<div
v-if="conditionIndex < subGroup.conditions!.length - 1"
class="flex items-center justify-center py-8px"
>
<div class="flex items-center gap-8px">
<!-- 连接线 -->
<div class="w-24px h-1px bg-green-300"></div>
<!-- 且标签 -->
<div class="px-12px py-4px bg-green-100 border border-green-300 rounded-full">
<span class="text-12px font-600 text-green-600"></span>
</div>
<!-- 连接线 -->
<div class="w-24px h-1px bg-green-300"></div>
</div>
</div>
</div>
<!-- 添加条件按钮 -->
<div
v-if="
subGroup.conditions &&
subGroup.conditions.length > 0 &&
subGroup.conditions.length < maxConditions
"
class="text-center py-16px"
>
<el-button type="primary" plain @click="addCondition">
<Icon icon="ep:plus" />
继续添加条件
</el-button>
<span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]">
最多可添加 {{ maxConditions }} 个条件
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import ConditionConfig from './ConditionConfig.vue'
import {
SubConditionGroupFormData,
ConditionFormData,
IotRuleSceneTriggerConditionTypeEnum
} from '@/api/iot/rule/scene/scene.types'
/** 子条件组配置组件 */
defineOptions({ name: 'SubConditionGroupConfig' })
interface Props {
modelValue: SubConditionGroupFormData
triggerType: number
maxConditions?: number
}
interface Emits {
(e: 'update:modelValue', value: SubConditionGroupFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const subGroup = useVModel(props, 'modelValue', emit)
//
const maxConditions = computed(() => props.maxConditions || 3)
//
const conditionValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({})
//
const addCondition = () => {
if (!subGroup.value.conditions) {
subGroup.value.conditions = []
}
if (subGroup.value.conditions.length >= maxConditions.value) {
return
}
const newCondition: ConditionFormData = {
type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, //
productId: undefined,
deviceId: undefined,
identifier: '',
operator: '=',
param: ''
}
subGroup.value.conditions.push(newCondition)
}
const removeCondition = (index: number) => {
if (subGroup.value.conditions) {
subGroup.value.conditions.splice(index, 1)
delete conditionValidations.value[index]
//
const newValidations: { [key: number]: { valid: boolean; message: string } } = {}
Object.keys(conditionValidations.value).forEach((key) => {
const numKey = parseInt(key)
if (numKey > index) {
newValidations[numKey - 1] = conditionValidations.value[numKey]
} else if (numKey < index) {
newValidations[numKey] = conditionValidations.value[numKey]
}
})
conditionValidations.value = newValidations
updateValidationResult()
}
}
const updateCondition = (index: number, condition: ConditionFormData) => {
if (subGroup.value.conditions) {
subGroup.value.conditions[index] = condition
}
}
const handleConditionValidate = (index: number, result: { valid: boolean; message: string }) => {
conditionValidations.value[index] = result
updateValidationResult()
}
const updateValidationResult = () => {
if (!subGroup.value.conditions || subGroup.value.conditions.length === 0) {
emit('validate', { valid: false, message: '子条件组至少需要一个条件' })
return
}
const validations = Object.values(conditionValidations.value)
const allValid = validations.every((v: any) => v.valid)
if (allValid) {
emit('validate', { valid: true, message: '子条件组配置验证通过' })
} else {
const errorMessages = validations.filter((v: any) => !v.valid).map((v: any) => v.message)
emit('validate', { valid: false, message: `条件配置错误: ${errorMessages.join('; ')}` })
}
}
//
watch(
() => subGroup.value.conditions,
() => {
updateValidationResult()
},
{ deep: true, immediate: true }
)
</script>

View File

@ -1,92 +1,46 @@
<!-- 触发器配置组件 -->
<!-- 场景触发器配置组件 -->
<template>
<el-card class="border border-[var(--el-border-color-light)] rounded-8px" shadow="never">
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center gap-8px">
<Icon icon="ep: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>
<div class="flex items-center gap-8px">
<el-button
type="primary"
size="small"
@click="addTrigger"
>
<Icon icon="ep:plus" />
添加触发器
</el-button>
</div>
<div class="flex items-center gap-8px">
<Icon icon="ep:lightning" class="text-[var(--el-color-primary)] text-18px" />
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">触发器配置</span>
<el-tag size="small" type="info">场景触发器</el-tag>
</div>
</template>
<div class="p-0">
<!-- 空状态 -->
<div v-if="triggers.length === 0">
<el-empty description="暂无触发器配置,请点击右上角添加触发器按钮开始配置" />
</div>
<div class="p-16px space-y-16px">
<!-- 触发事件类型选择 -->
<el-form-item label="触发事件类型" required>
<el-select
:model-value="trigger.type"
@update:model-value="(value) => updateTriggerType(value)"
@change="onTriggerTypeChange"
placeholder="请选择触发事件类型"
class="w-full"
>
<el-option
v-for="option in triggerTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
<!-- 触发器列表 -->
<div v-else class="space-y-16px">
<div v-for="(trigger, index) in triggers" :key="`trigger-${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:lightning" class="text-[var(--el-color-warning)] text-16px" />
<span>触发器 {{ index + 1 }}</span>
<el-tag :type="getTriggerTypeTag(trigger.type)" size="small">
{{ getTriggerTypeName(trigger.type) }}
</el-tag>
</div>
<div>
<el-button
type="danger"
size="small"
text
@click="removeTrigger(index)"
v-if="triggers.length > 1"
>
<Icon icon="ep:delete" />
删除
</el-button>
</div>
</div>
<!-- 设备触发配置 -->
<DeviceTriggerConfig
v-if="isDeviceTrigger(trigger.type)"
:model-value="trigger"
@update:model-value="updateTrigger"
/>
<div class="space-y-16px">
<!-- 触发类型选择 -->
<el-form-item label="触发类型" required>
<el-select
:model-value="trigger.type"
@update:model-value="(value) => updateTriggerType(index, value)"
@change="onTriggerTypeChange(trigger, $event)"
placeholder="请选择触发类型"
class="w-full"
>
<el-option
v-for="option in triggerTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
<!-- 设备触发配置 -->
<DeviceTriggerConfig
v-if="isDeviceTrigger(trigger.type)"
:model-value="trigger"
@update:model-value="(value) => updateTrigger(index, value)"
/>
<!-- 定时触发配置 -->
<TimerTriggerConfig
v-if="trigger.type === TriggerTypeEnum.TIMER"
:model-value="trigger.cronExpression"
@update:model-value="(value) => updateTriggerCronExpression(index, value)"
/>
</div>
</div>
</div>
<!-- 定时触发配置 -->
<TimerTriggerConfig
v-if="trigger.type === TriggerTypeEnum.TIMER"
:model-value="trigger.cronExpression"
@update:model-value="updateTriggerCronExpression"
/>
</div>
</el-card>
</template>
@ -104,37 +58,17 @@ import {
defineOptions({ name: 'TriggerSection' })
interface Props {
triggers: TriggerFormData[]
trigger: TriggerFormData
}
interface Emits {
(e: 'update:triggers', value: TriggerFormData[]): void
(e: 'update:trigger', value: TriggerFormData): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const triggers = useVModel(props, 'triggers', emit)
/**
* 创建默认的触发器数据
*/
const createDefaultTriggerData = (): TriggerFormData => {
return {
type: TriggerTypeEnum.DEVICE_PROPERTY_POST, //
productId: undefined,
deviceId: undefined,
identifier: undefined,
operator: undefined,
value: undefined,
cronExpression: undefined,
conditionGroups: []
}
}
const trigger = useVModel(props, 'trigger', emit)
//
const triggerTypeOptions = [
@ -160,23 +94,6 @@ const triggerTypeOptions = [
}
]
//
const triggerTypeNames = {
[TriggerTypeEnum.DEVICE_STATE_UPDATE]: '设备状态变更',
[TriggerTypeEnum.DEVICE_PROPERTY_POST]: '属性上报',
[TriggerTypeEnum.DEVICE_EVENT_POST]: '事件上报',
[TriggerTypeEnum.DEVICE_SERVICE_INVOKE]: '服务调用',
[TriggerTypeEnum.TIMER]: '定时触发'
}
const triggerTypeTags = {
[TriggerTypeEnum.DEVICE_STATE_UPDATE]: 'warning',
[TriggerTypeEnum.DEVICE_PROPERTY_POST]: 'primary',
[TriggerTypeEnum.DEVICE_EVENT_POST]: 'success',
[TriggerTypeEnum.DEVICE_SERVICE_INVOKE]: 'info',
[TriggerTypeEnum.TIMER]: 'danger'
}
//
const isDeviceTrigger = (type: number) => {
const deviceTriggerTypes = [
@ -188,58 +105,47 @@ const isDeviceTrigger = (type: number) => {
return deviceTriggerTypes.includes(type)
}
const getTriggerTypeName = (type: number) => {
return triggerTypeNames[type] || '未知类型'
}
const getTriggerTypeTag = (type: number) => {
return triggerTypeTags[type] || 'info'
}
//
const addTrigger = () => {
const newTrigger = createDefaultTriggerData()
triggers.value.push(newTrigger)
const updateTriggerType = (type: number) => {
trigger.value.type = type
onTriggerTypeChange(type)
}
const removeTrigger = (index: number) => {
triggers.value.splice(index, 1)
const updateTrigger = (newTrigger: TriggerFormData) => {
trigger.value = newTrigger
}
const updateTriggerType = (index: number, type: number) => {
triggers.value[index].type = type
onTriggerTypeChange(triggers.value[index], type)
const updateTriggerCronExpression = (cronExpression?: string) => {
trigger.value.cronExpression = cronExpression
}
const updateTrigger = (index: number, trigger: TriggerFormData) => {
triggers.value[index] = trigger
}
const updateTriggerCronExpression = (index: number, cronExpression?: string) => {
triggers.value[index].cronExpression = cronExpression
}
const onTriggerTypeChange = (trigger: TriggerFormData, type: number) => {
const onTriggerTypeChange = (type: number) => {
//
if (type === TriggerTypeEnum.TIMER) {
trigger.productId = undefined
trigger.deviceId = undefined
trigger.identifier = undefined
trigger.operator = undefined
trigger.value = undefined
trigger.conditionGroups = undefined
if (!trigger.cronExpression) {
trigger.cronExpression = '0 0 12 * * ?'
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.cronExpression = undefined
trigger.value.cronExpression = undefined
if (type === TriggerTypeEnum.DEVICE_STATE_UPDATE) {
trigger.conditionGroups = undefined
} else if (!trigger.conditionGroups) {
trigger.conditionGroups = []
trigger.value.mainCondition = undefined
trigger.value.conditionGroup = undefined
} else {
//
if (!trigger.value.mainCondition) {
trigger.value.mainCondition = undefined //
}
if (!trigger.value.conditionGroup) {
trigger.value.conditionGroup = undefined //
}
}
}
}
</script>

View File

@ -0,0 +1,80 @@
<!-- 条件类型选择器组件 -->
<template>
<el-select
:model-value="modelValue"
@update:model-value="handleChange"
placeholder="请选择条件类型"
class="w-full"
>
<el-option
v-for="option in conditionTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-8px">
<Icon :icon="option.icon" :class="option.iconClass" />
<span>{{ option.label }}</span>
</div>
<el-tag :type="option.tag" size="small">{{ option.category }}</el-tag>
</div>
</el-option>
</el-select>
</template>
<script setup lang="ts">
import { IotRuleSceneTriggerConditionTypeEnum } from '@/api/iot/rule/scene/scene.types'
/** 条件类型选择器组件 */
defineOptions({ name: 'ConditionTypeSelector' })
interface Props {
modelValue?: number
}
interface Emits {
(e: 'update:modelValue', value: number): void
(e: 'change', value: number): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
//
const conditionTypeOptions = [
{
value: IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS,
label: '设备状态',
description: '监控设备的在线/离线状态变化',
icon: 'ep:connection',
iconClass: 'text-blue-500',
tag: 'primary',
category: '设备'
},
{
value: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY,
label: '设备属性',
description: '监控设备属性值的变化',
icon: 'ep:data-analysis',
iconClass: 'text-green-500',
tag: 'success',
category: '属性'
},
{
value: IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME,
label: '当前时间',
description: '基于当前时间的条件判断',
icon: 'ep:timer',
iconClass: 'text-orange-500',
tag: 'warning',
category: '时间'
}
]
//
const handleChange = (value: number) => {
emit('update:modelValue', value)
emit('change', value)
}
</script>

View File

@ -0,0 +1,127 @@
<!-- 设备选择器组件 -->
<template>
<el-select
:model-value="modelValue"
@update:model-value="handleChange"
placeholder="请选择设备"
filterable
clearable
class="w-full"
:loading="deviceLoading"
:disabled="!productId"
>
<el-option
v-for="device in deviceList"
:key="device.id"
:label="device.deviceName"
:value="device.id"
>
<div class="flex items-center justify-between w-full py-4px">
<div class="flex-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{
device.deviceName
}}</div>
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ device.deviceKey }}</div>
</div>
<div class="flex items-center gap-4px">
<el-tag size="small" :type="getStatusType(device.status)">
{{ getStatusText(device.status) }}
</el-tag>
<el-tag size="small" :type="device.activeTime ? 'success' : 'info'">
{{ device.activeTime ? '已激活' : '未激活' }}
</el-tag>
</div>
</div>
</el-option>
</el-select>
</template>
<script setup lang="ts">
import { DeviceApi } from '@/api/iot/device/device'
/** 设备选择器组件 */
defineOptions({ name: 'DeviceSelector' })
interface Props {
modelValue?: number
productId?: number
}
interface Emits {
(e: 'update:modelValue', value?: number): void
(e: 'change', value?: number): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
//
const deviceLoading = ref(false)
const deviceList = ref<any[]>([])
//
const handleChange = (value?: number) => {
emit('update:modelValue', value)
emit('change', value)
}
//
const getDeviceList = async () => {
if (!props.productId) {
deviceList.value = []
return
}
try {
deviceLoading.value = true
const res = await DeviceApi.getDeviceListByProductId(props.productId)
deviceList.value = res || []
} catch (error) {
console.error('获取设备列表失败:', error)
deviceList.value = []
} finally {
deviceLoading.value = false
}
}
//
const getStatusType = (status: number) => {
switch (status) {
case 0:
return 'success' //
case 1:
return 'danger' //
default:
return 'info'
}
}
const getStatusText = (status: number) => {
switch (status) {
case 0:
return '正常'
case 1:
return '禁用'
default:
return '未知'
}
}
//
watch(
() => props.productId,
(newProductId) => {
if (newProductId) {
getDeviceList()
} else {
deviceList.value = []
//
if (props.modelValue) {
emit('update:modelValue', undefined)
emit('change', undefined)
}
}
},
{ immediate: true }
)
</script>

View File

@ -0,0 +1,81 @@
<!-- 产品选择器组件 -->
<template>
<el-select
:model-value="modelValue"
@update:model-value="handleChange"
placeholder="请选择产品"
filterable
clearable
class="w-full"
:loading="productLoading"
>
<el-option
v-for="product in productList"
:key="product.id"
:label="product.name"
:value="product.id"
>
<div class="flex items-center justify-between w-full py-4px">
<div class="flex-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{
product.name
}}</div>
<div class="text-12px text-[var(--el-text-color-secondary)]">{{
product.productKey
}}</div>
</div>
<el-tag size="small" :type="product.status === 0 ? 'success' : 'danger'">
{{ product.status === 0 ? '正常' : '禁用' }}
</el-tag>
</div>
</el-option>
</el-select>
</template>
<script setup lang="ts">
import { ProductApi } from '@/api/iot/product/product'
/** 产品选择器组件 */
defineOptions({ name: 'ProductSelector' })
interface Props {
modelValue?: number
}
interface Emits {
(e: 'update:modelValue', value?: number): void
(e: 'change', value?: number): void
}
defineProps<Props>()
const emit = defineEmits<Emits>()
//
const productLoading = ref(false)
const productList = ref<any[]>([])
//
const handleChange = (value?: number) => {
emit('update:modelValue', value)
emit('change', value)
}
//
const getProductList = async () => {
try {
productLoading.value = true
const res = await ProductApi.getSimpleProductList()
productList.value = res || []
} catch (error) {
console.error('获取产品列表失败:', error)
productList.value = []
} finally {
productLoading.value = false
}
}
//
onMounted(() => {
getProductList()
})
</script>

View File

@ -130,41 +130,3 @@ export interface PropertySelectorItem {
event?: ThingModelEvent
service?: ThingModelService
}
/** 数据类型枚举 */
export enum DataTypeEnum {
INT = 'int',
FLOAT = 'float',
DOUBLE = 'double',
ENUM = 'enum',
BOOL = 'bool',
TEXT = 'text',
DATE = 'date',
STRUCT = 'struct',
ARRAY = 'array'
}
/** 访问模式枚举 */
export enum AccessModeEnum {
READ = 'r',
READ_write = 'rw'
}
/** 事件类型枚举 */
export enum EventTypeEnum {
INFO = 'info',
ALERT = 'alert',
ERROR = 'error'
}
/** 调用类型枚举 */
export enum CallTypeEnum {
ASYNC = 'async',
SYNC = 'sync'
}
/** 参数方向枚举 */
export enum ParamDirectionEnum {
INPUT = 'input',
OUTPUT = 'output'
}