mirror of
https://github.com/yudaocode/yudao-ui-admin-vue3.git
synced 2026-04-21 11:08:37 +00:00
perf:【IoT 物联网】场景联动触发器优化
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
<!-- 条件组容器配置组件 -->
|
||||
<template>
|
||||
<div class="flex flex-col gap-16px">
|
||||
<!-- 条件组容器头部 -->
|
||||
<div
|
||||
class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-8px"
|
||||
>
|
||||
<div class="flex items-center gap-12px">
|
||||
<div class="flex items-center gap-8px text-16px font-600 text-green-700">
|
||||
<div
|
||||
class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
|
||||
>
|
||||
组
|
||||
</div>
|
||||
<span>附加条件组</span>
|
||||
</div>
|
||||
<el-tag size="small" type="success">与主条件为且关系</el-tag>
|
||||
<el-tag size="small" type="info">
|
||||
{{ modelValue.subGroups?.length || 0 }}个子条件组
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="flex items-center gap-8px">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="addSubGroup"
|
||||
:disabled="(modelValue.subGroups?.length || 0) >= maxSubGroups"
|
||||
>
|
||||
<Icon icon="ep:plus" />
|
||||
添加子条件组
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" text @click="removeContainer">
|
||||
<Icon icon="ep:delete" />
|
||||
删除条件组
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 子条件组列表 -->
|
||||
<div v-if="modelValue.subGroups && modelValue.subGroups.length > 0" class="space-y-16px">
|
||||
<!-- 逻辑关系说明 -->
|
||||
<div v-if="modelValue.subGroups.length > 1" class="flex items-center justify-center">
|
||||
<div
|
||||
class="flex items-center gap-8px px-12px py-6px bg-orange-50 border border-orange-200 rounded-full text-12px text-orange-600"
|
||||
>
|
||||
<Icon icon="ep:info-filled" />
|
||||
<span>子条件组之间为"或"关系,满足任意一组即可触发</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div
|
||||
v-for="(subGroup, subGroupIndex) in modelValue.subGroups"
|
||||
:key="`sub-group-${subGroupIndex}`"
|
||||
class="relative"
|
||||
>
|
||||
<!-- 子条件组容器 -->
|
||||
<div
|
||||
class="border-2 border-orange-200 rounded-8px bg-orange-50 shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between p-16px bg-gradient-to-r from-orange-50 to-yellow-50 border-b border-orange-200 rounded-t-6px"
|
||||
>
|
||||
<div class="flex items-center gap-12px">
|
||||
<div class="flex items-center gap-8px text-16px font-600 text-orange-700">
|
||||
<div
|
||||
class="w-24px h-24px bg-orange-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
|
||||
>
|
||||
{{ subGroupIndex + 1 }}
|
||||
</div>
|
||||
<span>子条件组 {{ subGroupIndex + 1 }}</span>
|
||||
</div>
|
||||
<el-tag size="small" type="warning" class="font-500">组内条件为"且"关系</el-tag>
|
||||
<el-tag size="small" type="info">
|
||||
{{ subGroup.conditions?.length || 0 }}个条件
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
text
|
||||
@click="removeSubGroup(subGroupIndex)"
|
||||
class="hover:bg-red-50"
|
||||
>
|
||||
<Icon icon="ep:delete" />
|
||||
删除组
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<SubConditionGroupConfig
|
||||
:model-value="subGroup"
|
||||
@update:model-value="(value) => updateSubGroup(subGroupIndex, value)"
|
||||
:trigger-type="triggerType"
|
||||
:max-conditions="maxConditionsPerGroup"
|
||||
@validate="(result) => handleSubGroupValidate(subGroupIndex, result)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 子条件组间的"或"连接符 -->
|
||||
<div
|
||||
v-if="subGroupIndex < modelValue.subGroups!.length - 1"
|
||||
class="flex items-center justify-center py-12px"
|
||||
>
|
||||
<div class="flex items-center gap-8px">
|
||||
<!-- 连接线 -->
|
||||
<div class="w-32px h-1px bg-orange-300"></div>
|
||||
<!-- 或标签 -->
|
||||
<div class="px-16px py-6px bg-orange-100 border-2 border-orange-300 rounded-full">
|
||||
<span class="text-14px font-600 text-orange-600">或</span>
|
||||
</div>
|
||||
<!-- 连接线 -->
|
||||
<div class="w-32px h-1px bg-orange-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-else
|
||||
class="p-24px border-2 border-dashed border-orange-200 rounded-8px text-center bg-orange-50"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-12px">
|
||||
<Icon icon="ep:plus" class="text-32px text-orange-400" />
|
||||
<div class="text-orange-600">
|
||||
<p class="text-14px font-500 mb-4px">暂无子条件组</p>
|
||||
<p class="text-12px">点击上方"添加子条件组"按钮开始配置</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import SubConditionGroupConfig from './SubConditionGroupConfig.vue'
|
||||
import {
|
||||
ConditionGroupContainerFormData,
|
||||
SubConditionGroupFormData
|
||||
} from '@/api/iot/rule/scene/scene.types'
|
||||
|
||||
/** 条件组容器配置组件 */
|
||||
defineOptions({ name: 'ConditionGroupContainerConfig' })
|
||||
|
||||
interface Props {
|
||||
modelValue: ConditionGroupContainerFormData
|
||||
triggerType: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: ConditionGroupContainerFormData): void
|
||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||
(e: 'remove'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const container = useVModel(props, 'modelValue', emit)
|
||||
|
||||
// 配置常量
|
||||
const maxSubGroups = 3 // 最多3个子条件组
|
||||
const maxConditionsPerGroup = 3 // 每组最多3个条件
|
||||
|
||||
// 验证状态
|
||||
const subGroupValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({})
|
||||
|
||||
// 事件处理
|
||||
const addSubGroup = () => {
|
||||
if (!container.value.subGroups) {
|
||||
container.value.subGroups = []
|
||||
}
|
||||
|
||||
if (container.value.subGroups.length >= maxSubGroups) {
|
||||
return
|
||||
}
|
||||
|
||||
const newSubGroup: SubConditionGroupFormData = {
|
||||
conditions: []
|
||||
}
|
||||
|
||||
container.value.subGroups.push(newSubGroup)
|
||||
}
|
||||
|
||||
const removeSubGroup = (index: number) => {
|
||||
if (container.value.subGroups) {
|
||||
container.value.subGroups.splice(index, 1)
|
||||
delete subGroupValidations.value[index]
|
||||
|
||||
// 重新索引验证结果
|
||||
const newValidations: { [key: number]: { valid: boolean; message: string } } = {}
|
||||
Object.keys(subGroupValidations.value).forEach((key) => {
|
||||
const numKey = parseInt(key)
|
||||
if (numKey > index) {
|
||||
newValidations[numKey - 1] = subGroupValidations.value[numKey]
|
||||
} else if (numKey < index) {
|
||||
newValidations[numKey] = subGroupValidations.value[numKey]
|
||||
}
|
||||
})
|
||||
subGroupValidations.value = newValidations
|
||||
|
||||
updateValidationResult()
|
||||
}
|
||||
}
|
||||
|
||||
const updateSubGroup = (index: number, subGroup: SubConditionGroupFormData) => {
|
||||
if (container.value.subGroups) {
|
||||
container.value.subGroups[index] = subGroup
|
||||
}
|
||||
}
|
||||
|
||||
const removeContainer = () => {
|
||||
emit('remove')
|
||||
}
|
||||
|
||||
const handleSubGroupValidate = (index: number, result: { valid: boolean; message: string }) => {
|
||||
subGroupValidations.value[index] = result
|
||||
updateValidationResult()
|
||||
}
|
||||
|
||||
const updateValidationResult = () => {
|
||||
if (!container.value.subGroups || container.value.subGroups.length === 0) {
|
||||
emit('validate', { valid: true, message: '条件组容器为空,验证通过' })
|
||||
return
|
||||
}
|
||||
|
||||
const validations = Object.values(subGroupValidations.value)
|
||||
const allValid = validations.every((v: any) => v.valid)
|
||||
|
||||
if (allValid) {
|
||||
emit('validate', { valid: true, message: '条件组容器配置验证通过' })
|
||||
} else {
|
||||
const errorMessages = validations.filter((v: any) => !v.valid).map((v: any) => v.message)
|
||||
emit('validate', { valid: false, message: `子条件组配置错误: ${errorMessages.join('; ')}` })
|
||||
}
|
||||
}
|
||||
|
||||
// 监听变化
|
||||
watch(
|
||||
() => container.value.subGroups,
|
||||
() => {
|
||||
updateValidationResult()
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,287 @@
|
||||
<!-- 当前时间条件配置组件 -->
|
||||
<template>
|
||||
<div class="flex flex-col gap-16px">
|
||||
<div class="flex items-center gap-8px p-12px px-16px bg-orange-50 rounded-6px border border-orange-200">
|
||||
<Icon icon="ep:timer" class="text-orange-500 text-18px" />
|
||||
<span class="text-14px font-500 text-orange-700">当前时间条件配置</span>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<!-- 时间操作符选择 -->
|
||||
<el-col :span="8">
|
||||
<el-form-item label="时间条件" required>
|
||||
<el-select
|
||||
:model-value="condition.operator"
|
||||
@update:model-value="(value) => updateConditionField('operator', value)"
|
||||
placeholder="请选择时间条件"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in timeOperatorOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex items-center gap-8px">
|
||||
<Icon :icon="option.icon" :class="option.iconClass" />
|
||||
<span>{{ option.label }}</span>
|
||||
</div>
|
||||
<el-tag :type="option.tag" size="small">{{ option.category }}</el-tag>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<!-- 时间值输入 -->
|
||||
<el-col :span="8">
|
||||
<el-form-item label="时间值" required>
|
||||
<el-time-picker
|
||||
v-if="needsTimeInput"
|
||||
:model-value="condition.timeValue"
|
||||
@update:model-value="(value) => updateConditionField('timeValue', value)"
|
||||
placeholder="请选择时间"
|
||||
format="HH:mm:ss"
|
||||
value-format="HH:mm:ss"
|
||||
class="w-full"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-else-if="needsDateInput"
|
||||
:model-value="condition.timeValue"
|
||||
@update:model-value="(value) => updateConditionField('timeValue', value)"
|
||||
type="datetime"
|
||||
placeholder="请选择日期时间"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
class="w-full"
|
||||
/>
|
||||
<div v-else class="text-[var(--el-text-color-placeholder)] text-14px">
|
||||
无需设置时间值
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<!-- 第二个时间值(范围条件) -->
|
||||
<el-col :span="8" v-if="needsSecondTimeInput">
|
||||
<el-form-item label="结束时间" required>
|
||||
<el-time-picker
|
||||
v-if="needsTimeInput"
|
||||
:model-value="condition.timeValue2"
|
||||
@update:model-value="(value) => updateConditionField('timeValue2', value)"
|
||||
placeholder="请选择结束时间"
|
||||
format="HH:mm:ss"
|
||||
value-format="HH:mm:ss"
|
||||
class="w-full"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-else
|
||||
:model-value="condition.timeValue2"
|
||||
@update:model-value="(value) => updateConditionField('timeValue2', value)"
|
||||
type="datetime"
|
||||
placeholder="请选择结束日期时间"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 条件预览 -->
|
||||
<div v-if="conditionPreview" class="p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]">
|
||||
<div class="flex items-center gap-8px mb-8px">
|
||||
<Icon icon="ep:view" class="text-[var(--el-color-info)] text-16px" />
|
||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">条件预览</span>
|
||||
</div>
|
||||
<div class="pl-24px">
|
||||
<code class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] p-8px rounded-4px font-mono">{{ conditionPreview }}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证结果 -->
|
||||
<div v-if="validationMessage" class="mt-8px">
|
||||
<el-alert
|
||||
:title="validationMessage"
|
||||
:type="isValid ? 'success' : 'error'"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { ConditionFormData, IotRuleSceneTriggerTimeOperatorEnum } from '@/api/iot/rule/scene/scene.types'
|
||||
|
||||
/** 当前时间条件配置组件 */
|
||||
defineOptions({ name: 'CurrentTimeConditionConfig' })
|
||||
|
||||
interface Props {
|
||||
modelValue: ConditionFormData
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: ConditionFormData): void
|
||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const condition = useVModel(props, 'modelValue', emit)
|
||||
|
||||
// 时间操作符选项
|
||||
const timeOperatorOptions = [
|
||||
{
|
||||
value: IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.value,
|
||||
label: IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.name,
|
||||
icon: 'ep:arrow-left',
|
||||
iconClass: 'text-blue-500',
|
||||
tag: 'primary',
|
||||
category: '时间点'
|
||||
},
|
||||
{
|
||||
value: IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.value,
|
||||
label: IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.name,
|
||||
icon: 'ep:arrow-right',
|
||||
iconClass: 'text-green-500',
|
||||
tag: 'success',
|
||||
category: '时间点'
|
||||
},
|
||||
{
|
||||
value: IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value,
|
||||
label: IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.name,
|
||||
icon: 'ep:sort',
|
||||
iconClass: 'text-orange-500',
|
||||
tag: 'warning',
|
||||
category: '时间段'
|
||||
},
|
||||
{
|
||||
value: IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.value,
|
||||
label: IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.name,
|
||||
icon: 'ep:position',
|
||||
iconClass: 'text-purple-500',
|
||||
tag: 'info',
|
||||
category: '时间点'
|
||||
},
|
||||
{
|
||||
value: IotRuleSceneTriggerTimeOperatorEnum.TODAY.value,
|
||||
label: IotRuleSceneTriggerTimeOperatorEnum.TODAY.name,
|
||||
icon: 'ep:calendar',
|
||||
iconClass: 'text-red-500',
|
||||
tag: 'danger',
|
||||
category: '日期'
|
||||
}
|
||||
]
|
||||
|
||||
// 状态
|
||||
const validationMessage = ref('')
|
||||
const isValid = ref(true)
|
||||
|
||||
// 计算属性
|
||||
const needsTimeInput = computed(() => {
|
||||
const timeOnlyOperators = [
|
||||
IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.value,
|
||||
IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.value,
|
||||
IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value,
|
||||
IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.value
|
||||
]
|
||||
return timeOnlyOperators.includes(condition.value.operator)
|
||||
})
|
||||
|
||||
const needsDateInput = computed(() => {
|
||||
return false // 暂时不支持日期输入,只支持时间
|
||||
})
|
||||
|
||||
const needsSecondTimeInput = computed(() => {
|
||||
return condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value
|
||||
})
|
||||
|
||||
const conditionPreview = computed(() => {
|
||||
if (!condition.value.operator) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const operatorOption = timeOperatorOptions.find(opt => opt.value === condition.value.operator)
|
||||
const operatorLabel = operatorOption?.label || condition.value.operator
|
||||
|
||||
if (condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
|
||||
return `当前时间 ${operatorLabel}`
|
||||
}
|
||||
|
||||
if (!condition.value.timeValue) {
|
||||
return `当前时间 ${operatorLabel} [未设置时间]`
|
||||
}
|
||||
|
||||
if (needsSecondTimeInput.value && condition.value.timeValue2) {
|
||||
return `当前时间 ${operatorLabel} ${condition.value.timeValue} 和 ${condition.value.timeValue2}`
|
||||
}
|
||||
|
||||
return `当前时间 ${operatorLabel} ${condition.value.timeValue}`
|
||||
})
|
||||
|
||||
// 事件处理
|
||||
const updateConditionField = (field: keyof ConditionFormData, value: any) => {
|
||||
condition.value[field] = value
|
||||
updateValidationResult()
|
||||
}
|
||||
|
||||
const updateValidationResult = () => {
|
||||
if (!condition.value.operator) {
|
||||
isValid.value = false
|
||||
validationMessage.value = '请选择时间条件'
|
||||
emit('validate', { valid: false, message: validationMessage.value })
|
||||
return
|
||||
}
|
||||
|
||||
// 今日条件不需要时间值
|
||||
if (condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
|
||||
isValid.value = true
|
||||
validationMessage.value = '当前时间条件配置验证通过'
|
||||
emit('validate', { valid: true, message: validationMessage.value })
|
||||
return
|
||||
}
|
||||
|
||||
if (needsTimeInput.value && !condition.value.timeValue) {
|
||||
isValid.value = false
|
||||
validationMessage.value = '请设置时间值'
|
||||
emit('validate', { valid: false, message: validationMessage.value })
|
||||
return
|
||||
}
|
||||
|
||||
if (needsSecondTimeInput.value && !condition.value.timeValue2) {
|
||||
isValid.value = false
|
||||
validationMessage.value = '请设置结束时间'
|
||||
emit('validate', { valid: false, message: validationMessage.value })
|
||||
return
|
||||
}
|
||||
|
||||
isValid.value = true
|
||||
validationMessage.value = '当前时间条件配置验证通过'
|
||||
emit('validate', { valid: true, message: validationMessage.value })
|
||||
}
|
||||
|
||||
// 监听变化
|
||||
watch(
|
||||
() => [condition.value.operator, condition.value.timeValue, condition.value.timeValue2],
|
||||
() => {
|
||||
updateValidationResult()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 监听操作符变化,清理不相关的时间值
|
||||
watch(
|
||||
() => condition.value.operator,
|
||||
(newOperator) => {
|
||||
if (newOperator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
|
||||
condition.value.timeValue = undefined
|
||||
condition.value.timeValue2 = undefined
|
||||
} else if (!needsSecondTimeInput.value) {
|
||||
condition.value.timeValue2 = undefined
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
114
src/views/iot/rule/scene/form/configs/MainConditionConfig.vue
Normal file
114
src/views/iot/rule/scene/form/configs/MainConditionConfig.vue
Normal 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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user