!880 fix: 修复场景联动配置中所有必填字段缺少了实际的验证

Merge pull request !880 from 熊猫大侠/master-iotscene
master
芋道源码 2026-05-30 15:13:58 +00:00 committed by Gitee
commit 8405a07dd0
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
13 changed files with 1001 additions and 262 deletions

View File

@ -4,7 +4,7 @@
:title="drawerTitle"
size="80%"
direction="rtl"
:close-on-click-modal="false"
:close-on-click-modal="true"
:close-on-press-escape="false"
@close="handleClose"
>
@ -12,9 +12,9 @@
<!-- 基础信息配置 -->
<BasicInfoSection v-model="formData" :rules="formRules" />
<!-- 触发器配置 -->
<TriggerSection v-model:triggers="formData.triggers" />
<TriggerSection ref="triggerSectionRef" v-model:triggers="formData.triggers" />
<!-- 执行器配置 -->
<ActionSection v-model:actions="formData.actions" />
<ActionSection ref="actionSectionRef" v-model:actions="formData.actions" />
</el-form>
<template #footer>
<div class="drawer-footer">
@ -38,11 +38,10 @@ import TriggerSection from './sections/TriggerSection.vue'
import ActionSection from './sections/ActionSection.vue'
import { IotSceneRule } from '@/api/iot/rule/scene'
import { RuleSceneApi } from '@/api/iot/rule/scene'
import {
IotRuleSceneTriggerTypeEnum,
IotRuleSceneActionTypeEnum,
isDeviceTrigger
} from '@/views/iot/utils/constants'
import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
import { validateTriggerItem } from './utils/triggerConditionRules'
import { validateActionItem } from './utils/actionRules'
import type { Trigger, Action } from '@/api/iot/rule/scene'
import { ElMessage } from 'element-plus'
import { CommonStatusEnum } from '@/utils/constants'
@ -91,67 +90,34 @@ const createDefaultFormData = (): IotSceneRule => {
}
const formRef = ref() //
const triggerSectionRef = ref<{
validateAllTriggers: () => Promise<boolean>
clearAllTriggerValidate: () => void
}>()
const actionSectionRef = ref<{
validateAllActions: () => Promise<boolean>
clearAllActionValidate: () => void
}>()
const formData = ref<IotSceneRule>(createDefaultFormData()) //
/**
* 触发器校验器
* 触发器校验器兜底与主条件 UI 规则一致
* @param _rule 校验规则未使用
* @param value 校验值
* @param callback 回调函数
*/
const validateTriggers = (_rule: any, value: any, callback: any) => {
const validateTriggers = (_rule: any, value: Trigger[], callback: any) => {
if (!value || !Array.isArray(value) || value.length === 0) {
callback(new Error('至少需要一个触发器'))
return
}
for (let i = 0; i < value.length; i++) {
const trigger = value[i]
//
if (!trigger.type) {
callback(new Error(`触发器 ${i + 1}: 触发器类型不能为空`))
const error = validateTriggerItem(value[i], i)
if (error) {
callback(new Error(error))
return
}
//
if (isDeviceTrigger(trigger.type)) {
if (!trigger.productId) {
callback(new Error(`触发器 ${i + 1}: 产品不能为空`))
return
}
if (!trigger.deviceId) {
callback(new Error(`触发器 ${i + 1}: 设备不能为空`))
return
}
const isStateUpdate = trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE
if (!isStateUpdate && !trigger.identifier) {
callback(new Error(`触发器 ${i + 1}: 物模型标识符不能为空`))
return
}
// / operator '='" / "
const isEventOrService =
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
if (!isEventOrService) {
if (!trigger.operator) {
callback(new Error(`触发器 ${i + 1}: 操作符不能为空`))
return
}
if (trigger.value === undefined || trigger.value === null || trigger.value === '') {
callback(new Error(`触发器 ${i + 1}: 参数值不能为空`))
return
}
}
}
//
if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
if (!trigger.cronExpression) {
callback(new Error(`触发器 ${i + 1}: CRON表达式不能为空`))
return
}
}
}
callback()
@ -163,59 +129,18 @@ const validateTriggers = (_rule: any, value: any, callback: any) => {
* @param value 校验值
* @param callback 回调函数
*/
const validateActions = (_rule: any, value: any, callback: any) => {
const validateActions = (_rule: any, value: Action[], callback: any) => {
if (!value || !Array.isArray(value) || value.length === 0) {
callback(new Error('至少需要一个执行器'))
return
}
for (let i = 0; i < value.length; i++) {
const action = value[i]
//
if (!action.type) {
callback(new Error(`执行器 ${i + 1}: 执行器类型不能为空`))
const error = validateActionItem(value[i], i)
if (error) {
callback(new Error(error))
return
}
//
if (
action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
) {
if (!action.productId) {
callback(new Error(`执行器 ${i + 1}: 产品不能为空`))
return
}
if (!action.deviceId) {
callback(new Error(`执行器 ${i + 1}: 设备不能为空`))
return
}
//
if (action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE) {
if (!action.identifier) {
callback(new Error(`执行器 ${i + 1}: 服务不能为空`))
return
}
}
if (!action.params || Object.keys(action.params).length === 0) {
callback(new Error(`执行器 ${i + 1}: 参数配置不能为空`))
return
}
}
//
if (
action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER ||
action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER
) {
if (!action.alertConfigId) {
callback(new Error(`执行器 ${i + 1}: 告警配置不能为空`))
return
}
}
}
callback()
@ -248,10 +173,22 @@ const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' :
/** 提交表单 */
const handleSubmit = async () => {
//
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
try {
await formRef.value.validate()
} catch {
return
}
const mainConditionValid = await triggerSectionRef.value?.validateAllTriggers?.()
if (mainConditionValid === false) {
return
}
const actionValid = await actionSectionRef.value?.validateAllActions?.()
if (actionValid === false) {
return
}
//
submitLoading.value = true
@ -321,6 +258,8 @@ watch(drawerVisible, async (visible) => {
//
await nextTick()
formRef.value?.clearValidate()
triggerSectionRef.value?.clearAllTriggerValidate?.()
actionSectionRef.value?.clearAllActionValidate?.()
}
})

View File

@ -1,7 +1,7 @@
<!-- 告警配置组件 -->
<template>
<div class="w-full">
<el-form-item label="告警配置" required>
<el-form ref="innerFormRef" :model="formModel" :rules="formRules" label-width="110px" class="w-full">
<el-form-item label="告警配置" prop="alertConfigId" required>
<el-select
v-model="localValue"
placeholder="请选择告警配置"
@ -16,15 +16,24 @@
:key="config.id"
:label="config.name"
:value="config.id"
/>
>
<div class="flex items-center justify-between">
<span>{{ config.name }}</span>
<el-tag :type="config.enabled ? 'success' : 'danger'" size="small">
{{ config.enabled ? '启用' : '禁用' }}
</el-tag>
</div>
</el-option>
</el-select>
</el-form-item>
</div>
</el-form>
</template>
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { useVModel } from '@vueuse/core'
import { AlertConfigApi } from '@/api/iot/alert/config'
import { buildAlertConfigRules } from '../utils/actionRules'
/** 告警配置组件 */
defineOptions({ name: 'AlertConfig' })
@ -38,9 +47,15 @@ const emit = defineEmits<{
}>()
const localValue = useVModel(props, 'modelValue', emit)
const innerFormRef = ref<FormInstance>()
const formRules = buildAlertConfigRules()
const loading = ref(false) //
const alertConfigs = ref<any[]>([]) //
const formModel = computed(() => ({
alertConfigId: localValue.value
}))
const loading = ref(false)
const alertConfigs = ref<any[]>([])
/**
* 处理选择变化事件
@ -48,19 +63,44 @@ const alertConfigs = ref<any[]>([]) // 告警配置列表
*/
const handleChange = (value?: number) => {
emit('update:modelValue', value)
nextTick(() => {
innerFormRef.value?.validateField('alertConfigId').catch(() => {})
})
}
/** 加载告警配置列表 */
const loadAlertConfigs = async () => {
loading.value = true
try {
alertConfigs.value = (await AlertConfigApi.getSimpleAlertConfigList()) || []
const data = await AlertConfigApi.getAlertConfigPage({
pageNo: 1,
pageSize: 100,
enabled: true
})
alertConfigs.value = data.list || []
} finally {
loading.value = false
}
}
//
const validate = async (): Promise<boolean> => {
if (!innerFormRef.value) {
return true
}
try {
await innerFormRef.value.validate()
return true
} catch {
return false
}
}
const clearValidate = () => {
innerFormRef.value?.clearValidate()
}
defineExpose({ validate, clearValidate })
onMounted(() => {
loadAlertConfigs()
})

View File

@ -1,10 +1,16 @@
<!-- 单个条件配置组件 -->
<template>
<div class="flex flex-col gap-16px">
<el-form
ref="innerFormRef"
:model="condition"
:rules="conditionRules"
label-width="110px"
class="flex flex-col gap-16px"
>
<!-- 条件类型选择 -->
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="条件类型" required>
<el-form-item label="条件类型" prop="type" required>
<el-select
:model-value="condition.type"
@update:model-value="(value) => updateConditionField('type', value)"
@ -26,7 +32,7 @@
<!-- 产品设备选择 - 设备相关条件的公共部分 -->
<el-row v-if="isDeviceCondition" :gutter="16">
<el-col :span="12">
<el-form-item label="产品" required>
<el-form-item label="产品" prop="productId" required>
<ProductSelector
:model-value="condition.productId"
@update:model-value="(value) => updateConditionField('productId', value)"
@ -35,7 +41,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备" required>
<el-form-item label="设备" prop="deviceId" required>
<DeviceSelector
:model-value="condition.deviceId"
@update:model-value="(value) => updateConditionField('deviceId', value)"
@ -51,11 +57,9 @@
v-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS"
class="flex flex-col gap-16px"
>
<!-- 状态和操作符选择 -->
<el-row :gutter="16">
<!-- 操作符选择 -->
<el-col :span="12">
<el-form-item label="操作符" required>
<el-form-item label="操作符" prop="operator" required>
<el-select
:model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)"
@ -72,9 +76,8 @@
</el-form-item>
</el-col>
<!-- 状态选择 -->
<el-col :span="12">
<el-form-item label="设备状态" required>
<el-form-item label="设备状态" prop="param" required>
<el-select
:model-value="condition.param"
@update:model-value="(value) => updateConditionField('param', value)"
@ -98,11 +101,9 @@
v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY"
class="space-y-16px"
>
<!-- 属性配置 -->
<el-row :gutter="16">
<!-- 属性/事件/服务选择 -->
<el-col :span="6">
<el-form-item label="监控项" required>
<el-form-item label="监控项" prop="identifier" required>
<PropertySelector
:model-value="condition.identifier"
@update:model-value="(value) => updateConditionField('identifier', value)"
@ -114,9 +115,8 @@
</el-form-item>
</el-col>
<!-- 操作符选择 -->
<el-col :span="6">
<el-form-item label="操作符" required>
<el-form-item label="操作符" prop="operator" required>
<OperatorSelector
:model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)"
@ -126,9 +126,8 @@
</el-form-item>
</el-col>
<!-- 值输入 -->
<el-col :span="12">
<el-form-item label="比较值" required>
<el-form-item label="比较值" prop="param" required>
<ValueInput
:model-value="condition.param"
@update:model-value="(value) => updateConditionField('param', value)"
@ -146,11 +145,13 @@
v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME"
:model-value="condition"
@update:model-value="updateCondition"
@field-change="handleCurrentTimeFieldChange"
/>
</div>
</el-form>
</template>
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { useVModel } from '@vueuse/core'
import CurrentTimeConditionConfig from './CurrentTimeConditionConfig.vue'
import ProductSelector from '../selectors/ProductSelector.vue'
@ -165,6 +166,7 @@ import {
getConditionTypeOptions,
IoTDeviceStatusEnum
} from '@/views/iot/utils/constants'
import { buildSubConditionRules } from '../utils/triggerConditionRules'
/** 单个条件配置组件 */
defineOptions({ name: 'ConditionConfig' })
@ -203,24 +205,32 @@ const statusOperatorOptions = [
]
const condition = useVModel(props, 'modelValue', emit)
const innerFormRef = ref<FormInstance>()
const propertyType = ref<string>('string')
const propertyConfig = ref<any>(null)
const propertyType = ref<string>('string') //
const propertyConfig = ref<any>(null) //
const isDeviceCondition = computed(() => {
return (
condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS ||
condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY
)
}) //
})
const conditionRules = computed(() =>
buildSubConditionRules(condition.value.type, () => condition.value.operator)
)
/**
* 更新条件字段
* @param field 字段名
* @param value 字段值
*/
const updateConditionField = (field: any, value: any) => {
const updateConditionField = (field: keyof TriggerCondition, value: any) => {
;(condition.value as any)[field] = value
emit('update:modelValue', condition.value)
nextTick(() => {
innerFormRef.value?.validateField(field as string).catch(() => {})
})
}
/**
@ -232,46 +242,56 @@ const updateCondition = (newCondition: TriggerCondition) => {
emit('update:modelValue', condition.value)
}
/** 当前时间子组件字段变更后触发校验 */
const handleCurrentTimeFieldChange = (field: string) => {
nextTick(() => {
innerFormRef.value?.validateField(field).catch(() => {})
})
}
/**
* 处理条件类型变化事件
* @param type 条件类型
*/
const handleConditionTypeChange = (type: number) => {
//
const isCurrentTime = type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME
const isDeviceStatus = type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS
//
if (isCurrentTime || isDeviceStatus) {
condition.value.identifier = undefined
}
//
if (isCurrentTime) {
condition.value.productId = undefined
condition.value.deviceId = undefined
}
//
condition.value.operator = isCurrentTime
? 'at_time'
: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
//
condition.value.param = ''
emit('update:modelValue', condition.value)
nextTick(() => clearValidate())
}
/** 处理产品变化事件 */
const handleProductChange = (_: number) => {
//
const handleProductChange = () => {
condition.value.deviceId = undefined
condition.value.identifier = ''
emit('update:modelValue', condition.value)
nextTick(() => {
innerFormRef.value?.clearValidate(['deviceId', 'identifier'])
})
}
/** 处理设备变化事件 */
const handleDeviceChange = (_: number) => {
//
const handleDeviceChange = () => {
condition.value.identifier = ''
emit('update:modelValue', condition.value)
nextTick(() => {
innerFormRef.value?.clearValidate('identifier')
})
}
/**
@ -281,17 +301,37 @@ const handleDeviceChange = (_: number) => {
const handlePropertyChange = (propertyInfo: { type: string; config: any }) => {
propertyType.value = propertyInfo.type
propertyConfig.value = propertyInfo.config
//
condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
condition.value.param = ''
emit('update:modelValue', condition.value)
}
/** 处理操作符变化事件 */
const handleOperatorChange = () => {
//
condition.value.param = ''
emit('update:modelValue', condition.value)
nextTick(() => {
innerFormRef.value?.validateField('param').catch(() => {})
})
}
const validate = async (): Promise<boolean> => {
if (!innerFormRef.value) {
return true
}
try {
await innerFormRef.value.validate()
return true
} catch {
return false
}
}
const clearValidate = () => {
innerFormRef.value?.clearValidate()
}
defineExpose({ validate, clearValidate })
</script>
<style scoped>

View File

@ -4,7 +4,7 @@
<el-row :gutter="16">
<!-- 时间操作符选择 -->
<el-col :span="8">
<el-form-item label="时间条件" required>
<el-form-item label="时间条件" prop="operator" required>
<el-select
:model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)"
@ -31,7 +31,7 @@
<!-- 时间值输入 -->
<el-col :span="8">
<el-form-item label="时间值" required>
<el-form-item label="时间值" prop="param" required>
<el-time-picker
v-if="needsTimeInput"
:model-value="timeValue"
@ -59,7 +59,7 @@
<!-- 第二个时间值范围条件 -->
<el-col :span="8" v-if="needsSecondTimeInput">
<el-form-item label="结束时间" required>
<el-form-item label="结束时间" prop="param" required>
<el-time-picker
v-if="needsTimeInput"
:model-value="timeValue2"
@ -99,6 +99,7 @@ const props = defineProps<{
const emit = defineEmits<{
(e: 'update:modelValue', value: TriggerCondition): void
(e: 'field-change', field: string): void
}>()
const condition = useVModel(props, 'modelValue', emit)
@ -187,8 +188,9 @@ const timeValue2 = computed(() => {
* @param field 字段名
* @param value 字段值
*/
const updateConditionField = (field: any, value: any) => {
const updateConditionField = (field: keyof TriggerCondition, value: any) => {
condition.value[field] = value
emit('field-change', field)
}
/**
@ -199,12 +201,12 @@ const handleTimeValueChange = (value: string) => {
const currentParams = condition.value.param ? condition.value.param.split(',') : []
currentParams[0] = value || ''
//
if (needsSecondTimeInput.value) {
condition.value.param = currentParams.slice(0, 2).join(',')
} else {
condition.value.param = currentParams[0]
}
emit('field-change', 'param')
}
/**
@ -215,6 +217,7 @@ const handleTimeValue2Change = (value: string) => {
const currentParams = condition.value.param ? condition.value.param.split(',') : ['']
currentParams[1] = value || ''
condition.value.param = currentParams.slice(0, 2).join(',')
emit('field-change', 'param')
}
/** 监听操作符变化,清理不相关的时间值 */
@ -222,13 +225,12 @@ watch(
() => condition.value.operator,
(newOperator) => {
if (newOperator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
//
condition.value.param = ''
} else if (!needsSecondTimeInput.value) {
//
const currentParams = condition.value.param ? condition.value.param.split(',') : []
condition.value.param = currentParams[0] || ''
}
emit('field-change', 'param')
}
)
</script>

View File

@ -1,15 +1,20 @@
<!-- 设备控制配置组件 -->
<template>
<div class="flex flex-col gap-16px">
<!-- 产品和设备选择 - 与触发器保持一致的分离式选择器 -->
<el-form
ref="innerFormRef"
:model="action"
:rules="formRules"
label-width="110px"
class="flex flex-col gap-16px"
>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="产品" required>
<el-form-item label="产品" prop="productId" required>
<ProductSelector v-model="action.productId" @change="handleProductChange" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备" required>
<el-form-item label="设备" prop="deviceId" required>
<DeviceSelector
v-model="action.deviceId"
:product-id="action.productId"
@ -21,7 +26,7 @@
<!-- 服务选择 - 服务调用类型时显示 -->
<div v-if="action.productId && isServiceInvokeAction" class="space-y-16px">
<el-form-item label="服务" required>
<el-form-item label="服务" prop="identifier" required>
<el-select
v-model="action.identifier"
placeholder="请选择服务"
@ -47,9 +52,8 @@
</el-select>
</el-form-item>
<!-- 服务参数配置 -->
<div v-if="action.identifier" class="space-y-16px">
<el-form-item label="服务参数" required>
<el-form-item label="服务参数" prop="params" required>
<JsonParamsInput
v-model="paramsValue"
type="service"
@ -62,8 +66,7 @@
<!-- 控制参数配置 - 属性设置类型时显示 -->
<div v-if="action.productId && isPropertySetAction" class="space-y-16px">
<!-- 参数配置 -->
<el-form-item label="参数" required>
<el-form-item label="参数" prop="params" required>
<JsonParamsInput
v-model="paramsValue"
type="property"
@ -72,10 +75,11 @@
/>
</el-form-item>
</div>
</div>
</el-form>
</template>
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { useVModel } from '@vueuse/core'
import ProductSelector from '../selectors/ProductSelector.vue'
import DeviceSelector from '../selectors/DeviceSelector.vue'
@ -88,6 +92,7 @@ import {
IoTDataSpecsDataTypeEnum
} from '@/views/iot/utils/constants'
import { ThingModelApi } from '@/api/iot/thingmodel'
import { buildDeviceControlRules } from '../utils/actionRules'
/** 设备控制配置组件 */
defineOptions({ name: 'DeviceControlConfig' })
@ -101,54 +106,71 @@ const emit = defineEmits<{
}>()
const action = useVModel(props, 'modelValue', emit)
const innerFormRef = ref<FormInstance>()
const thingModelProperties = ref<ThingModelProperty[]>([]) //
const loadingThingModel = ref(false) //
const selectedService = ref<ThingModelService | null>(null) //
const serviceList = ref<ThingModelService[]>([]) //
const loadingServices = ref(false) //
const formRules = computed(() => {
const rules = buildDeviceControlRules(action.value.type)
if (isServiceInvokeAction.value) {
if (!action.value.productId) {
delete rules.identifier
delete rules.params
} else if (!action.value.identifier) {
delete rules.params
}
}
if (isPropertySetAction.value && !action.value.productId) {
delete rules.params
}
return rules
})
const thingModelProperties = ref<ThingModelProperty[]>([])
const loadingThingModel = ref(false)
const selectedService = ref<ThingModelService | null>(null)
const serviceList = ref<ThingModelService[]>([])
const loadingServices = ref(false)
//
const paramsValue = computed({
get: () => {
// params JSON
if (action.value.params && typeof action.value.params === 'object') {
return JSON.stringify(action.value.params, null, 2)
}
// params
return action.value.params || ''
},
set: (value: string) => {
// JSON
action.value.params = value.trim() || ''
nextTick(() => {
innerFormRef.value?.validateField('params').catch(() => {})
})
}
})
//
const isPropertySetAction = computed(() => {
return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
})
//
const isServiceInvokeAction = computed(() => {
return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
})
/**
* 处理产品变化事件
* @param productId 产品 ID
*/
const validateField = (field: string) => {
nextTick(() => {
innerFormRef.value?.validateField(field).catch(() => {})
})
}
const handleProductChange = (productId?: number) => {
//
if (action.value.productId !== productId) {
action.value.deviceId = undefined
action.value.identifier = undefined //
action.value.params = '' //
selectedService.value = null //
serviceList.value = [] //
action.value.identifier = undefined
action.value.params = ''
selectedService.value = null
serviceList.value = []
}
//
if (productId) {
if (isPropertySetAction.value) {
loadThingModelProperties(productId)
@ -156,47 +178,37 @@ const handleProductChange = (productId?: number) => {
loadServiceList(productId)
}
}
validateField('productId')
nextTick(() => {
innerFormRef.value?.clearValidate(['deviceId', 'identifier', 'params'])
})
}
/**
* 处理设备变化事件
* @param deviceId 设备 ID
*/
const handleDeviceChange = (deviceId?: number) => {
//
if (action.value.deviceId !== deviceId) {
action.value.params = '' //
action.value.params = ''
}
validateField('deviceId')
}
/**
* 处理服务变化事件
* @param serviceIdentifier 服务标识符
*/
const handleServiceChange = (serviceIdentifier?: string) => {
//
const service = serviceList.value.find((s) => s.identifier === serviceIdentifier) || null
selectedService.value = service
//
action.value.params = ''
//
if (service && service.inputParams && service.inputParams.length > 0) {
const defaultParams = {}
const defaultParams: Record<string, unknown> = {}
service.inputParams.forEach((param) => {
defaultParams[param.identifier] = getDefaultValueForParam(param)
})
// JSON
action.value.params = JSON.stringify(defaultParams, null, 2)
}
validateField('identifier')
validateField('params')
}
/**
* 获取物模型TSL数据
* @param productId 产品ID
* @returns 物模型TSL数据
*/
const getThingModelTSL = async (productId: number) => {
if (!productId) return null
@ -208,10 +220,6 @@ const getThingModelTSL = async (productId: number) => {
}
}
/**
* 加载物模型属性可写属性
* @param productId 产品ID
*/
const loadThingModelProperties = async (productId: number) => {
if (!productId) {
thingModelProperties.value = []
@ -227,7 +235,6 @@ const loadThingModelProperties = async (productId: number) => {
return
}
// accessMode 'w'
thingModelProperties.value = tslData.properties.filter(
(property: ThingModelProperty) =>
property.accessMode &&
@ -242,10 +249,6 @@ const loadThingModelProperties = async (productId: number) => {
}
}
/**
* 加载服务列表
* @param productId 产品ID
*/
const loadServiceList = async (productId: number) => {
if (!productId) {
serviceList.value = []
@ -270,27 +273,14 @@ const loadServiceList = async (productId: number) => {
}
}
/**
* 从TSL加载服务信息用于编辑模式回显
* @param productId 产品ID
* @param serviceIdentifier 服务标识符
*/
const loadServiceFromTSL = async (productId: number, serviceIdentifier: string) => {
//
await loadServiceList(productId)
//
const service = serviceList.value.find((s: any) => s.identifier === serviceIdentifier)
const service = serviceList.value.find((s) => s.identifier === serviceIdentifier)
if (service) {
selectedService.value = service
}
}
/**
* 根据参数类型获取默认值
* @param param 参数对象
* @returns 默认值
*/
const getDefaultValueForParam = (param: any) => {
switch (param.dataType) {
case IoTDataSpecsDataTypeEnum.INT:
@ -303,7 +293,6 @@ const getDefaultValueForParam = (param: any) => {
case IoTDataSpecsDataTypeEnum.TEXT:
return ''
case IoTDataSpecsDataTypeEnum.ENUM:
// 使
if (param.dataSpecs?.dataSpecsList && param.dataSpecs.dataSpecsList.length > 0) {
return param.dataSpecs.dataSpecsList[0].value
}
@ -313,44 +302,52 @@ const getDefaultValueForParam = (param: any) => {
}
}
const isInitialized = ref(false) //
const isInitialized = ref(false)
/**
* 初始化组件数据
*/
const initializeComponent = async () => {
if (isInitialized.value) return
const currentAction = action.value
if (!currentAction) return
//
if (currentAction.productId && isPropertySetAction.value) {
await loadThingModelProperties(currentAction.productId)
}
//
if (currentAction.productId && isServiceInvokeAction.value && currentAction.identifier) {
// TSL
await loadServiceFromTSL(currentAction.productId, currentAction.identifier)
}
isInitialized.value = true
}
/** 组件初始化 */
const validate = async (): Promise<boolean> => {
if (!innerFormRef.value) {
return true
}
try {
await innerFormRef.value.validate()
return true
} catch {
return false
}
}
const clearValidate = () => {
innerFormRef.value?.clearValidate()
}
defineExpose({ validate, clearValidate })
onMounted(() => {
initializeComponent()
})
/** 监听关键字段的变化,避免深度监听导致的性能问题 */
watch(
() => [action.value.productId, action.value.type, action.value.identifier],
async ([newProductId, , newIdentifier], [oldProductId, , oldIdentifier]) => {
//
if (!isInitialized.value) return
//
if (newProductId !== oldProductId) {
if (newProductId && isPropertySetAction.value) {
await loadThingModelProperties(newProductId as number)
@ -359,18 +356,22 @@ watch(
}
}
//
if (
newIdentifier !== oldIdentifier &&
newProductId &&
isServiceInvokeAction.value &&
newIdentifier
) {
const service = serviceList.value.find((s: any) => s.identifier === newIdentifier)
const service = serviceList.value.find((s) => s.identifier === newIdentifier)
if (service) {
selectedService.value = service
}
}
}
)
watch(
() => action.value.type,
() => nextTick(() => clearValidate())
)
</script>

View File

@ -26,6 +26,7 @@
<!-- 主条件内容配置 -->
<MainConditionInnerConfig
ref="mainConditionRef"
:model-value="trigger"
@update:model-value="updateCondition"
:trigger-type="trigger.type"
@ -118,6 +119,7 @@
</div>
<SubConditionGroupConfig
:ref="(el) => setSubGroupRef(el, subGroupIndex)"
:model-value="subGroup"
@update:model-value="(value) => updateSubGroup(subGroupIndex, value)"
:trigger-type="trigger.type"
@ -184,6 +186,22 @@ const emit = defineEmits<{
}>()
const trigger = useVModel(props, 'modelValue', emit)
const mainConditionRef = ref<InstanceType<typeof MainConditionInnerConfig>>()
type SubConditionGroupExpose = {
validate: () => Promise<boolean>
clearValidate: () => void
}
const subGroupRefs = ref<Record<number, SubConditionGroupExpose>>({})
const setSubGroupRef = (el: unknown, index: number) => {
if (el) {
subGroupRefs.value[index] = el as SubConditionGroupExpose
} else {
delete subGroupRefs.value[index]
}
}
const maxSubGroups = 3 // 3
const maxConditionsPerGroup = 3 // 3
@ -248,4 +266,35 @@ const updateSubGroup = (index: number, subGroup: any) => {
const removeConditionGroup = () => {
trigger.value.conditionGroups = undefined
}
/** 校验主条件及附加子条件组 */
const validate = async (): Promise<boolean> => {
const mainValid = (await mainConditionRef.value?.validate()) ?? true
if (!mainValid) {
return false
}
const groups = trigger.value.conditionGroups
if (!groups?.length) {
return true
}
for (let i = 0; i < groups.length; i++) {
const subGroupRef = subGroupRefs.value[i]
if (subGroupRef?.validate) {
const valid = await subGroupRef.validate()
if (!valid) {
return false
}
}
}
return true
}
const clearValidate = () => {
mainConditionRef.value?.clearValidate()
Object.values(subGroupRefs.value).forEach((ref) => ref.clearValidate?.())
}
defineExpose({ validate, clearValidate })
</script>

View File

@ -1,5 +1,11 @@
<template>
<div class="space-y-16px">
<el-form
ref="innerFormRef"
:model="condition"
:rules="conditionRules"
label-width="110px"
class="space-y-16px"
>
<!-- 触发事件类型选择 -->
<el-form-item label="触发事件类型" required>
<el-select
@ -22,7 +28,7 @@
<!-- 产品设备选择 -->
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="产品" required>
<el-form-item label="产品" prop="productId" required>
<ProductSelector
:model-value="condition.productId"
@update:model-value="(value) => updateConditionField('productId', value)"
@ -31,7 +37,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备" required>
<el-form-item label="设备" prop="deviceId" required>
<DeviceSelector
:model-value="condition.deviceId"
@update:model-value="(value) => updateConditionField('deviceId', value)"
@ -46,7 +52,7 @@
<el-row :gutter="16">
<!-- 属性/事件/服务选择 -->
<el-col :span="6">
<el-form-item label="监控项" required>
<el-form-item label="监控项" prop="identifier" required>
<PropertySelector
:model-value="condition.identifier"
@update:model-value="(value) => updateConditionField('identifier', value)"
@ -60,7 +66,7 @@
<!-- 操作符选择 - 服务调用和事件上报不需要操作符 -->
<el-col v-if="needsOperatorSelector" :span="6">
<el-form-item label="操作符" required>
<el-form-item label="操作符" prop="operator" required>
<OperatorSelector
:model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)"
@ -71,7 +77,7 @@
<!-- 值输入 -->
<el-col :span="isWideValueColumn ? 18 : 12">
<el-form-item :label="valueInputLabel" required>
<el-form-item :label="valueInputLabel" prop="value" :required="needsValueRequired">
<!-- 服务调用参数配置 -->
<JsonParamsInput
v-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE"
@ -113,7 +119,7 @@
<!-- 设备状态触发器使用简化的配置 -->
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="产品" required>
<el-form-item label="产品" prop="productId" required>
<ProductSelector
:model-value="condition.productId"
@update:model-value="(value) => updateConditionField('productId', value)"
@ -122,7 +128,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备" required>
<el-form-item label="设备" prop="deviceId" required>
<DeviceSelector
:model-value="condition.deviceId"
@update:model-value="(value) => updateConditionField('deviceId', value)"
@ -134,7 +140,7 @@
</el-row>
<el-row :gutter="16">
<el-col :span="6">
<el-form-item label="操作符" required>
<el-form-item label="操作符" prop="operator" required>
<el-select
:model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)"
@ -149,11 +155,11 @@
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="参数" required>
<el-form-item label="参数" prop="value" required>
<el-select
:model-value="condition.value"
@update:model-value="(value) => updateConditionField('value', value)"
placeholder="请选择操作符"
placeholder="请选择设备状态"
class="w-full"
>
<el-option
@ -177,10 +183,11 @@
此触发类型暂不需要配置额外条件
</p>
</div>
</div>
</el-form>
</template>
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import ProductSelector from '../selectors/ProductSelector.vue'
import DeviceSelector from '../selectors/DeviceSelector.vue'
import PropertySelector from '../selectors/PropertySelector.vue'
@ -196,6 +203,7 @@ import {
IotRuleSceneTriggerConditionParameterOperatorEnum,
IoTDeviceStatusEnum
} from '@/views/iot/utils/constants'
import { buildMainConditionRules } from '../utils/triggerConditionRules'
import { useVModel } from '@vueuse/core'
/** 主条件内部配置组件 */
@ -224,9 +232,12 @@ const deviceStatusChangeOptions = [
]
const condition = useVModel(props, 'modelValue', emit)
const innerFormRef = ref<FormInstance>()
const propertyType = ref('') //
const propertyConfig = ref<any>(null) //
const conditionRules = computed(() => buildMainConditionRules(props.triggerType))
//
const isDevicePropertyTrigger = computed(() => {
return (
@ -250,6 +261,11 @@ const needsOperatorSelector = computed(() => {
return !noOperatorTriggerTypes.includes(props.triggerType)
})
// /
const needsValueRequired = computed(() => {
return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST
})
//
const isWideValueColumn = computed(() => {
const wideColumnTriggerTypes = [
@ -282,13 +298,26 @@ const serviceConfig = computed(() => {
return undefined
})
/** 设备状态触发器默认操作符为「等于」 */
const ensureDeviceStatusDefaults = () => {
if (props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
return
}
if (!condition.value.operator) {
condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
}
}
/**
* 更新条件字段
* @param field 字段名
* @param value 字段值
*/
const updateConditionField = (field: any, value: any) => {
const updateConditionField = (field: keyof Trigger, value: any) => {
condition.value[field] = value
nextTick(() => {
innerFormRef.value?.validateField(field as string).catch(() => {})
})
}
/**
@ -301,15 +330,19 @@ const handleTriggerTypeChange = (type: number) => {
/** 处理产品变化事件 */
const handleProductChange = () => {
//
condition.value.deviceId = undefined
condition.value.identifier = ''
nextTick(() => {
innerFormRef.value?.clearValidate(['deviceId', 'identifier'])
})
}
/** 处理设备变化事件 */
const handleDeviceChange = () => {
//
condition.value.identifier = ''
nextTick(() => {
innerFormRef.value?.clearValidate('identifier')
})
}
/**
@ -321,7 +354,6 @@ const handlePropertyChange = (propertyInfo: any) => {
propertyType.value = propertyInfo.type
propertyConfig.value = propertyInfo.config
// '='
if (
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
@ -330,4 +362,36 @@ const handlePropertyChange = (propertyInfo: any) => {
}
}
}
/** 校验主条件表单 */
const validate = async (): Promise<boolean> => {
if (!innerFormRef.value || Object.keys(conditionRules.value).length === 0) {
return true
}
try {
await innerFormRef.value.validate()
return true
} catch {
return false
}
}
const clearValidate = () => {
innerFormRef.value?.clearValidate()
}
defineExpose({ validate, clearValidate })
watch(
() => props.triggerType,
() => {
ensureDeviceStatusDefaults()
nextTick(() => clearValidate())
},
{ immediate: true }
)
onMounted(() => {
ensureDeviceStatusDefaults()
})
</script>

View File

@ -53,6 +53,7 @@
<div class="p-12px">
<ConditionConfig
:ref="(el) => setConditionRef(el, conditionIndex)"
:model-value="condition"
@update:model-value="(value) => updateCondition(conditionIndex, value)"
:trigger-type="triggerType"
@ -105,6 +106,44 @@ const subGroup = useVModel(props, 'modelValue', emit)
const maxConditions = computed(() => props.maxConditions || 3) //
type ConditionConfigExpose = {
validate: () => Promise<boolean>
clearValidate: () => void
}
const conditionRefs = ref<Record<number, ConditionConfigExpose>>({})
const setConditionRef = (el: unknown, index: number) => {
if (el) {
conditionRefs.value[index] = el as ConditionConfigExpose
} else {
delete conditionRefs.value[index]
}
}
/** 校验组内所有子条件 */
const validate = async (): Promise<boolean> => {
if (!subGroup.value?.length) {
return false
}
for (let i = 0; i < subGroup.value.length; i++) {
const conditionRef = conditionRefs.value[i]
if (conditionRef?.validate) {
const valid = await conditionRef.validate()
if (!valid) {
return false
}
}
}
return true
}
const clearValidate = () => {
Object.values(conditionRefs.value).forEach((ref) => ref.clearValidate?.())
}
defineExpose({ validate, clearValidate })
/** 添加条件 */
const addCondition = async () => {
// subGroup.value

View File

@ -67,6 +67,7 @@
</div>
<SubConditionGroupConfig
:ref="(el) => setSubGroupRef(el, groupIndex)"
:model-value="group"
@update:model-value="(value) => updateConditionGroup(groupIndex, value)"
:trigger-type="IotRuleSceneTriggerTypeEnum.TIMER"
@ -131,6 +132,44 @@ const conditionGroups = useVModel(props, 'modelValue', emit)
const maxGroups = 3 // 3
const maxConditionsPerGroup = 3 // 3
type SubConditionGroupExpose = {
validate: () => Promise<boolean>
clearValidate: () => void
}
const subGroupRefs = ref<Record<number, SubConditionGroupExpose>>({})
const setSubGroupRef = (el: unknown, index: number) => {
if (el) {
subGroupRefs.value[index] = el as SubConditionGroupExpose
} else {
delete subGroupRefs.value[index]
}
}
/** 校验所有附加条件组 */
const validate = async (): Promise<boolean> => {
if (!conditionGroups.value?.length) {
return true
}
for (let i = 0; i < conditionGroups.value.length; i++) {
const subGroupRef = subGroupRefs.value[i]
if (subGroupRef?.validate) {
const valid = await subGroupRef.validate()
if (!valid) {
return false
}
}
}
return true
}
const clearValidate = () => {
Object.values(subGroupRefs.value).forEach((ref) => ref.clearValidate?.())
}
defineExpose({ validate, clearValidate })
/** 添加条件组 */
const addConditionGroup = async () => {
if (!conditionGroups.value) {

View File

@ -75,7 +75,7 @@
<el-select
:model-value="action.type"
@update:model-value="(value) => updateActionType(index, value)"
@change="(value) => onActionTypeChange(action, value)"
@change="(value) => onActionTypeChange(action, value, index)"
placeholder="请选择执行类型"
class="w-full"
>
@ -92,6 +92,7 @@
<!-- 设备控制配置 -->
<DeviceControlConfig
v-if="isDeviceAction(action.type)"
:ref="(el) => setDeviceControlRef(el, index)"
:model-value="action"
@update:model-value="(value) => updateAction(index, value)"
/>
@ -99,6 +100,7 @@
<!-- 告警配置 - 只有恢复告警时才显示 -->
<AlertConfig
v-if="action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER"
:ref="(el) => setAlertConfigRef(el, index)"
:model-value="action.alertConfigId"
@update:model-value="(value) => updateActionAlertConfig(index, value)"
/>
@ -156,6 +158,66 @@ const emit = defineEmits<{
const actions = useVModel(props, 'actions', emit)
type ConfigExpose = {
validate: () => Promise<boolean>
clearValidate: () => void
}
const deviceControlRefs = ref<Record<number, ConfigExpose>>({})
const alertConfigRefs = ref<Record<number, ConfigExpose>>({})
const setDeviceControlRef = (el: unknown, index: number) => {
if (el) {
deviceControlRefs.value[index] = el as ConfigExpose
} else {
delete deviceControlRefs.value[index]
}
}
const setAlertConfigRef = (el: unknown, index: number) => {
if (el) {
alertConfigRefs.value[index] = el as ConfigExpose
} else {
delete alertConfigRefs.value[index]
}
}
/** 校验所有执行器配置 */
const validateAllActions = async (): Promise<boolean> => {
for (let i = 0; i < actions.value.length; i++) {
const action = actions.value[i]
if (isDeviceAction(action.type)) {
const deviceRef = deviceControlRefs.value[i]
if (deviceRef?.validate) {
const valid = await deviceRef.validate()
if (!valid) {
return false
}
}
continue
}
if (action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER) {
const alertRef = alertConfigRefs.value[i]
if (alertRef?.validate) {
const valid = await alertRef.validate()
if (!valid) {
return false
}
}
}
}
return true
}
const clearAllActionValidate = () => {
Object.values(deviceControlRefs.value).forEach((ref) => ref.clearValidate?.())
Object.values(alertConfigRefs.value).forEach((ref) => ref.clearValidate?.())
}
defineExpose({ validateAllActions, clearAllActionValidate })
/** 获取执行器标签类型(用于 el-tag 的 type 属性) */
const getActionTypeTag = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
const actionTypeTags = {
@ -222,9 +284,8 @@ const removeAction = (index: number) => {
* @param type 执行器类型
*/
const updateActionType = (index: number, type: number) => {
const action = actions.value[index]
onActionTypeChange(action, type) // action.type
action.type = type
actions.value[index].type = type
onActionTypeChange(actions.value[index], type, index)
}
/**
@ -250,7 +311,7 @@ const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
* @param action 执行器对象
* @param type 执行器类型
*/
const onActionTypeChange = (action: Action, type: number) => {
const onActionTypeChange = (action: Action, type: number, index: number) => {
//
if (isDeviceAction(type)) {
//
@ -269,5 +330,10 @@ const onActionTypeChange = (action: Action, type: number) => {
action.params = undefined
action.alertConfigId = undefined
}
nextTick(() => {
deviceControlRefs.value[index]?.clearValidate?.()
alertConfigRefs.value[index]?.clearValidate?.()
})
}
</script>

View File

@ -59,6 +59,7 @@
<!-- 设备触发配置 -->
<DeviceTriggerConfig
v-if="isDeviceTrigger(triggerItem.type)"
:ref="(el) => setDeviceTriggerRef(el, index)"
:model-value="triggerItem"
:index="index"
@update:model-value="(value) => updateTriggerDeviceConfig(index, value)"
@ -93,6 +94,7 @@
<!-- 附加条件组配置 -->
<TimerConditionGroupConfig
:ref="(el) => setTimerConditionRef(el, index)"
:model-value="triggerItem.conditionGroups"
@update:model-value="(value) => updateTriggerConditionGroups(index, value)"
/>
@ -127,6 +129,7 @@ import type { Trigger, TriggerCondition } from '@/api/iot/rule/scene'
import {
getTriggerTypeLabel,
IotRuleSceneTriggerTypeEnum,
IotRuleSceneTriggerConditionParameterOperatorEnum,
isDeviceTrigger
} from '@/views/iot/utils/constants'
@ -143,6 +146,72 @@ const emit = defineEmits<{
const triggers = useVModel(props, 'triggers', emit)
type DeviceTriggerConfigExpose = {
validate: () => Promise<boolean>
clearValidate: () => void
}
const deviceTriggerRefs = ref<Record<number, DeviceTriggerConfigExpose>>({})
type TimerConditionGroupExpose = {
validate: () => Promise<boolean>
clearValidate: () => void
}
const timerConditionRefs = ref<Record<number, TimerConditionGroupExpose>>({})
const setDeviceTriggerRef = (el: unknown, index: number) => {
if (el) {
deviceTriggerRefs.value[index] = el as DeviceTriggerConfigExpose
} else {
delete deviceTriggerRefs.value[index]
}
}
const setTimerConditionRef = (el: unknown, index: number) => {
if (el) {
timerConditionRefs.value[index] = el as TimerConditionGroupExpose
} else {
delete timerConditionRefs.value[index]
}
}
/** 校验所有触发器(主条件 + 附加子条件组) */
const validateAllTriggers = async (): Promise<boolean> => {
for (let i = 0; i < triggers.value.length; i++) {
const triggerItem = triggers.value[i]
if (isDeviceTrigger(triggerItem.type)) {
const deviceConfig = deviceTriggerRefs.value[i]
if (deviceConfig?.validate) {
const valid = await deviceConfig.validate()
if (!valid) {
return false
}
}
continue
}
if (triggerItem.type === IotRuleSceneTriggerTypeEnum.TIMER) {
const timerConfig = timerConditionRefs.value[i]
if (timerConfig?.validate) {
const valid = await timerConfig.validate()
if (!valid) {
return false
}
}
}
}
return true
}
const clearAllTriggerValidate = () => {
Object.values(deviceTriggerRefs.value).forEach((ref) => ref.clearValidate?.())
Object.values(timerConditionRefs.value).forEach((ref) => ref.clearValidate?.())
}
defineExpose({ validateAllTriggers, clearAllTriggerValidate })
/** 获取触发器标签类型(用于 el-tag 的 type 属性) */
const getTriggerTagType = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
@ -158,7 +227,7 @@ const addTrigger = () => {
productId: undefined,
deviceId: undefined,
identifier: undefined,
operator: undefined,
operator: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value,
value: undefined,
cronExpression: undefined,
conditionGroups: [] //
@ -218,7 +287,7 @@ const updateTriggerConditionGroups = (index: number, conditionGroups: TriggerCon
* @param index 触发器索引
* @param _ 触发器类型未使用
*/
const onTriggerTypeChange = (index: number, _: number) => {
const onTriggerTypeChange = (index: number, type: number) => {
const triggerItem = triggers.value[index]
triggerItem.productId = undefined
triggerItem.deviceId = undefined
@ -227,6 +296,10 @@ const onTriggerTypeChange = (index: number, _: number) => {
triggerItem.value = undefined
triggerItem.cronExpression = undefined
triggerItem.conditionGroups = []
if (type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
triggerItem.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
}
nextTick(() => deviceTriggerRefs.value[index]?.clearValidate?.())
}
/** 初始化:确保至少有一个触发器 */

View File

@ -0,0 +1,110 @@
import type { FormItemRule } from 'element-plus'
import type { Action } from '@/api/iot/rule/scene'
import { IotRuleSceneActionTypeEnum } from '@/views/iot/utils/constants'
const requiredRule = (message: string): FormItemRule => ({
required: true,
message,
trigger: ['change', 'blur']
})
/** 判断执行器参数是否为空 */
export const isActionParamsEmpty = (params?: string): boolean => {
if (!params || !String(params).trim()) {
return true
}
try {
const parsed = JSON.parse(String(params))
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
return Object.keys(parsed).length === 0
}
} catch {
return false
}
return false
}
/** 设备控制执行器表单项校验规则 */
export function buildDeviceControlRules(actionType: number): Record<string, FormItemRule[]> {
const rules: Record<string, FormItemRule[]> = {
productId: [requiredRule('请选择产品')],
deviceId: [requiredRule('请选择设备')],
params: [createParamsRule()]
}
if (actionType === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE) {
rules.identifier = [requiredRule('请选择服务')]
}
return rules
}
/** 告警配置表单项校验规则 */
export function buildAlertConfigRules(): Record<string, FormItemRule[]> {
return {
alertConfigId: [requiredRule('请选择告警配置')]
}
}
function createParamsRule(): FormItemRule {
return {
validator: (_rule, value, callback) => {
if (isActionParamsEmpty(value)) {
callback(new Error('请填写参数配置'))
return
}
try {
JSON.parse(String(value))
} catch {
callback(new Error('参数格式须为合法 JSON'))
return
}
callback()
},
trigger: ['change', 'blur']
}
}
/**
* UI
* @returns null
*/
export function validateActionItem(action: Action, index: number): string | null {
const prefix = `执行器 ${index + 1}`
if (!action.type) {
return `${prefix}: 执行器类型不能为空`
}
if (
action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
) {
if (!action.productId) {
return `${prefix}: 产品不能为空`
}
if (!action.deviceId) {
return `${prefix}: 设备不能为空`
}
if (action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE && !action.identifier) {
return `${prefix}: 服务不能为空`
}
if (isActionParamsEmpty(action.params)) {
return `${prefix}: 参数配置不能为空`
}
try {
JSON.parse(String(action.params))
} catch {
return `${prefix}: 参数格式须为合法 JSON`
}
return null
}
if (action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER) {
if (!action.alertConfigId) {
return `${prefix}: 告警配置不能为空`
}
}
return null
}

View File

@ -0,0 +1,277 @@
import type { FormItemRule } from 'element-plus'
import type { Trigger, TriggerCondition } from '@/api/iot/rule/scene'
import {
IotRuleSceneTriggerTypeEnum,
IotRuleSceneTriggerConditionTypeEnum,
IotRuleSceneTriggerTimeOperatorEnum,
isDeviceTrigger
} from '@/views/iot/utils/constants'
const requiredRule = (message: string): FormItemRule => ({
required: true,
message,
trigger: ['change', 'blur']
})
const isEmpty = (val: unknown) => val === undefined || val === null || val === ''
/** 主条件表单项校验规则(与 MainConditionInnerConfig 展示字段对齐) */
export function buildMainConditionRules(triggerType: number): Record<string, FormItemRule[]> {
const base: Record<string, FormItemRule[]> = {
productId: [requiredRule('请选择产品')],
deviceId: [requiredRule('请选择设备')]
}
if (triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
return {
...base,
operator: [requiredRule('请选择操作符')],
value: [requiredRule('请选择设备状态')]
}
}
if (
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST ||
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
) {
const rules: Record<string, FormItemRule[]> = {
...base,
identifier: [requiredRule('请选择监控项')]
}
const isEventOrService =
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
if (!isEventOrService) {
rules.operator = [requiredRule('请选择操作符')]
rules.value = [requiredRule('请填写比较值')]
}
return rules
}
return {}
}
/**
* UI RuleSceneForm
* @returns null
*/
export function validateTriggerItem(trigger: Trigger, index: number): string | null {
if (!trigger.type) {
return `触发器 ${index + 1}: 触发器类型不能为空`
}
if (isDeviceTrigger(trigger.type)) {
if (!trigger.productId) {
return `触发器 ${index + 1}: 产品不能为空`
}
if (!trigger.deviceId) {
return `触发器 ${index + 1}: 设备不能为空`
}
if (trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
if (!trigger.operator) {
return `触发器 ${index + 1}: 操作符不能为空`
}
if (isEmpty(trigger.value)) {
return `触发器 ${index + 1}: 设备状态不能为空`
}
return null
}
if (!trigger.identifier) {
return `触发器 ${index + 1}: 物模型标识符不能为空`
}
const isEventOrService =
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
if (!isEventOrService) {
if (!trigger.operator) {
return `触发器 ${index + 1}: 操作符不能为空`
}
if (isEmpty(trigger.value)) {
return `触发器 ${index + 1}: 参数值不能为空`
}
}
return null
}
if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
if (!trigger.cronExpression) {
return `触发器 ${index + 1}: CRON表达式不能为空`
}
}
const groupError = validateTriggerConditionGroups(trigger.conditionGroups, index)
if (groupError) {
return groupError
}
return null
}
/** 子条件表单项校验规则(与 ConditionConfig 展示字段对齐) */
export function buildSubConditionRules(
conditionType?: number,
getOperator?: () => string
): Record<string, FormItemRule[]> {
const rules: Record<string, FormItemRule[]> = {
type: [requiredRule('请选择条件类型')]
}
if (!conditionType) {
return rules
}
if (
conditionType === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS ||
conditionType === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY
) {
rules.productId = [requiredRule('请选择产品')]
rules.deviceId = [requiredRule('请选择设备')]
}
if (conditionType === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS) {
rules.operator = [requiredRule('请选择操作符')]
rules.param = [requiredRule('请选择设备状态')]
}
if (conditionType === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY) {
rules.identifier = [requiredRule('请选择监控项')]
rules.operator = [requiredRule('请选择操作符')]
rules.param = [requiredRule('请填写比较值')]
}
if (conditionType === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME) {
rules.operator = [requiredRule('请选择时间条件')]
rules.param = [createCurrentTimeParamRule(getOperator ?? (() => ''))]
}
return rules
}
/** 当前时间条件的 param 校验 */
function createCurrentTimeParamRule(getOperator: () => string): FormItemRule {
return {
validator: (_rule, value, callback) => {
const operator = getOperator()
if (operator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
callback()
return
}
if (isEmpty(value)) {
callback(new Error('请填写时间值'))
return
}
if (operator === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value) {
const parts = String(value).split(',')
if (!parts[0]?.trim() || !parts[1]?.trim()) {
callback(new Error('请填写开始和结束时间'))
return
}
}
callback()
},
trigger: ['change', 'blur']
}
}
/**
*
* @returns null
*/
export function validateTriggerCondition(
condition: TriggerCondition,
path: string
): string | null {
if (!condition.type) {
return `${path}: 条件类型不能为空`
}
if (
condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS ||
condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY
) {
if (!condition.productId) {
return `${path}: 产品不能为空`
}
if (!condition.deviceId) {
return `${path}: 设备不能为空`
}
}
if (condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS) {
if (!condition.operator) {
return `${path}: 操作符不能为空`
}
if (isEmpty(condition.param)) {
return `${path}: 设备状态不能为空`
}
return null
}
if (condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY) {
if (!condition.identifier) {
return `${path}: 监控项不能为空`
}
if (!condition.operator) {
return `${path}: 操作符不能为空`
}
if (isEmpty(condition.param)) {
return `${path}: 比较值不能为空`
}
return null
}
if (condition.type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME) {
if (!condition.operator) {
return `${path}: 时间条件不能为空`
}
if (condition.operator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
return null
}
if (isEmpty(condition.param)) {
return `${path}: 时间值不能为空`
}
if (condition.operator === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value) {
const parts = String(condition.param).split(',')
if (!parts[0]?.trim() || !parts[1]?.trim()) {
return `${path}: 开始和结束时间不能为空`
}
}
return null
}
return null
}
/** 校验触发器的附加条件组 */
export function validateTriggerConditionGroups(
groups: TriggerCondition[][] | undefined,
triggerIndex: number
): string | null {
if (!groups?.length) {
return null
}
for (let g = 0; g < groups.length; g++) {
const group = groups[g]
if (!group?.length) {
return `触发器 ${triggerIndex + 1} 子条件组 ${g + 1}: 至少需要一个条件`
}
for (let c = 0; c < group.length; c++) {
const error = validateTriggerCondition(
group[c],
`触发器 ${triggerIndex + 1} 子条件组 ${g + 1} 条件 ${c + 1}`
)
if (error) {
return error
}
}
}
return null
}