perf:【IoT 物联网】场景联动触发器优化
parent
929bcb4059
commit
d7b4db9b4e
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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'
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue