perf:【IoT 物联网】场景联动目录结构优化
This commit is contained in:
269
src/views/iot/rule/scene/form/sections/ActionSection.vue
Normal file
269
src/views/iot/rule/scene/form/sections/ActionSection.vue
Normal file
@ -0,0 +1,269 @@
|
||||
<!-- 执行器配置组件 -->
|
||||
<!-- todo @puhui999:参考“触发器配置”,简化下。 -->
|
||||
<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:setting" 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">{{ actions.length }}/{{ maxActions }}</el-tag>
|
||||
</div>
|
||||
<div class="flex items-center gap-8px">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="addAction"
|
||||
:disabled="actions.length >= maxActions"
|
||||
>
|
||||
<Icon icon="ep:plus" />
|
||||
添加执行器
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="p-0">
|
||||
<!-- 空状态 -->
|
||||
<div v-if="actions.length === 0">
|
||||
<el-empty description="暂无执行器配置">
|
||||
<el-button type="primary" @click="addAction">
|
||||
<Icon icon="ep:plus" />
|
||||
添加第一个执行器
|
||||
</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
|
||||
<!-- 执行器列表 -->
|
||||
<div v-else class="space-y-16px">
|
||||
<div v-for="(action, index) in actions" :key="`action-${index}`" class="p-16px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]">
|
||||
<div class="flex items-center justify-between mb-16px">
|
||||
<div class="flex items-center gap-8px">
|
||||
<Icon icon="ep:setting" class="text-[var(--el-color-success)] text-16px" />
|
||||
<span>执行器 {{ index + 1 }}</span>
|
||||
<el-tag :type="getActionTypeTag(action.type)" size="small">
|
||||
{{ getActionTypeName(action.type) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
text
|
||||
@click="removeAction(index)"
|
||||
v-if="actions.length > 1"
|
||||
>
|
||||
<Icon icon="ep:delete" />
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-16px">
|
||||
<!-- 执行类型选择 -->
|
||||
<ActionTypeSelector
|
||||
:model-value="action.type"
|
||||
@update:model-value="(value) => updateActionType(index, value)"
|
||||
@change="onActionTypeChange(action, $event)"
|
||||
/>
|
||||
|
||||
<!-- 设备控制配置 -->
|
||||
<DeviceControlConfig
|
||||
v-if="isDeviceAction(action.type)"
|
||||
:model-value="action"
|
||||
@update:model-value="(value) => updateAction(index, value)"
|
||||
@validate="(result) => handleActionValidate(index, result)"
|
||||
/>
|
||||
|
||||
<!-- 告警配置 -->
|
||||
<AlertConfig
|
||||
v-if="isAlertAction(action.type)"
|
||||
:model-value="action.alertConfigId"
|
||||
@update:model-value="(value) => updateActionAlertConfig(index, value)"
|
||||
@validate="(result) => handleActionValidate(index, result)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加提示 -->
|
||||
<div v-if="actions.length > 0 && actions.length < maxActions" class="text-center py-16px">
|
||||
<el-button type="primary" plain @click="addAction">
|
||||
<Icon icon="ep:plus" />
|
||||
继续添加执行器
|
||||
</el-button>
|
||||
<span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]"> 最多可添加 {{ maxActions }} 个执行器 </span>
|
||||
</div>
|
||||
|
||||
<!-- 验证结果 -->
|
||||
<div v-if="validationMessage" class="validation-result">
|
||||
<el-alert
|
||||
:title="validationMessage"
|
||||
:type="isValid ? 'success' : 'error'"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import ActionTypeSelector from '../selectors/ActionTypeSelector.vue'
|
||||
import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
|
||||
import AlertConfig from '../configs/AlertConfig.vue'
|
||||
import {
|
||||
ActionFormData,
|
||||
IotRuleSceneActionTypeEnum as ActionTypeEnum
|
||||
} from '@/api/iot/rule/scene/scene.types'
|
||||
import { createDefaultActionData } from '../../utils/transform'
|
||||
|
||||
/** 执行器配置组件 */
|
||||
defineOptions({ name: 'ActionSection' })
|
||||
|
||||
interface Props {
|
||||
actions: ActionFormData[]
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:actions', value: ActionFormData[]): void
|
||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const actions = useVModel(props, 'actions', emit)
|
||||
|
||||
// 配置常量
|
||||
const maxActions = 5
|
||||
|
||||
// 验证状态
|
||||
const actionValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({})
|
||||
const validationMessage = ref('')
|
||||
const isValid = ref(true)
|
||||
|
||||
// 执行器类型映射
|
||||
const actionTypeNames = {
|
||||
[ActionTypeEnum.DEVICE_PROPERTY_SET]: '属性设置',
|
||||
[ActionTypeEnum.DEVICE_SERVICE_INVOKE]: '服务调用',
|
||||
[ActionTypeEnum.ALERT_TRIGGER]: '触发告警',
|
||||
[ActionTypeEnum.ALERT_RECOVER]: '恢复告警'
|
||||
}
|
||||
|
||||
const actionTypeTags = {
|
||||
[ActionTypeEnum.DEVICE_PROPERTY_SET]: 'primary',
|
||||
[ActionTypeEnum.DEVICE_SERVICE_INVOKE]: 'success',
|
||||
[ActionTypeEnum.ALERT_TRIGGER]: 'danger',
|
||||
[ActionTypeEnum.ALERT_RECOVER]: 'warning'
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
const isDeviceAction = (type: number) => {
|
||||
return [ActionTypeEnum.DEVICE_PROPERTY_SET, ActionTypeEnum.DEVICE_SERVICE_INVOKE].includes(type)
|
||||
}
|
||||
|
||||
const isAlertAction = (type: number) => {
|
||||
return [ActionTypeEnum.ALERT_TRIGGER, ActionTypeEnum.ALERT_RECOVER].includes(type)
|
||||
}
|
||||
|
||||
const getActionTypeName = (type: number) => {
|
||||
return actionTypeNames[type] || '未知类型'
|
||||
}
|
||||
|
||||
const getActionTypeTag = (type: number) => {
|
||||
return actionTypeTags[type] || 'info'
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
const addAction = () => {
|
||||
if (actions.value.length >= maxActions) {
|
||||
return
|
||||
}
|
||||
|
||||
const newAction = createDefaultActionData()
|
||||
actions.value.push(newAction)
|
||||
}
|
||||
|
||||
const removeAction = (index: number) => {
|
||||
actions.value.splice(index, 1)
|
||||
delete actionValidations.value[index]
|
||||
|
||||
// 重新索引验证结果
|
||||
const newValidations: { [key: number]: { valid: boolean; message: string } } = {}
|
||||
Object.keys(actionValidations.value).forEach((key) => {
|
||||
const numKey = parseInt(key)
|
||||
if (numKey > index) {
|
||||
newValidations[numKey - 1] = actionValidations.value[numKey]
|
||||
} else if (numKey < index) {
|
||||
newValidations[numKey] = actionValidations.value[numKey]
|
||||
}
|
||||
})
|
||||
actionValidations.value = newValidations
|
||||
|
||||
updateValidationResult()
|
||||
}
|
||||
|
||||
const updateActionType = (index: number, type: number) => {
|
||||
actions.value[index].type = type
|
||||
onActionTypeChange(actions.value[index], type)
|
||||
}
|
||||
|
||||
const updateAction = (index: number, action: ActionFormData) => {
|
||||
actions.value[index] = action
|
||||
}
|
||||
|
||||
const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
|
||||
actions.value[index].alertConfigId = alertConfigId
|
||||
}
|
||||
|
||||
const onActionTypeChange = (action: ActionFormData, type: number) => {
|
||||
// 清理不相关的配置
|
||||
if (isDeviceAction(type)) {
|
||||
action.alertConfigId = undefined
|
||||
if (!action.params) {
|
||||
action.params = {}
|
||||
}
|
||||
} else if (isAlertAction(type)) {
|
||||
action.productId = undefined
|
||||
action.deviceId = undefined
|
||||
action.params = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const handleActionValidate = (index: number, result: { valid: boolean; message: string }) => {
|
||||
actionValidations.value[index] = result
|
||||
updateValidationResult()
|
||||
}
|
||||
|
||||
const updateValidationResult = () => {
|
||||
const validations = Object.values(actionValidations.value)
|
||||
const allValid = validations.every((v) => v.valid)
|
||||
const hasValidations = validations.length > 0
|
||||
|
||||
if (!hasValidations) {
|
||||
isValid.value = true
|
||||
validationMessage.value = ''
|
||||
} else if (allValid) {
|
||||
isValid.value = true
|
||||
validationMessage.value = '所有执行器配置验证通过'
|
||||
} else {
|
||||
isValid.value = false
|
||||
const errorMessages = validations.filter((v) => !v.valid).map((v) => v.message)
|
||||
validationMessage.value = `执行器配置错误: ${errorMessages.join('; ')}`
|
||||
}
|
||||
|
||||
emit('validate', { valid: isValid.value, message: validationMessage.value })
|
||||
}
|
||||
|
||||
// 监听执行器数量变化
|
||||
watch(
|
||||
() => actions.value.length,
|
||||
() => {
|
||||
updateValidationResult()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
75
src/views/iot/rule/scene/form/sections/BasicInfoSection.vue
Normal file
75
src/views/iot/rule/scene/form/sections/BasicInfoSection.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<!-- 基础信息配置组件 -->
|
||||
<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:info-filled" class="text-[var(--el-color-primary)] text-18px" />
|
||||
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">基础信息</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-8px">
|
||||
<el-tag :type="formData.status === 0 ? 'success' : 'danger'" size="small">
|
||||
{{ formData.status === 0 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="p-0">
|
||||
<el-row :gutter="24">
|
||||
<!-- TODO @puhui999:NameInput、StatusRadio、DescriptionInput 是不是直接写在当前界面哈。有点散; -->
|
||||
<el-col :span="12">
|
||||
<el-form-item label="场景名称" prop="name" required>
|
||||
<NameInput v-model="formData.name" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- TODO @puhui999:每个一行会好点? -->
|
||||
<el-col :span="12">
|
||||
<el-form-item label="场景状态" prop="status" required>
|
||||
<StatusRadio v-model="formData.status" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="场景描述" prop="description">
|
||||
<DescriptionInput v-model="formData.description" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import NameInput from '../inputs/NameInput.vue'
|
||||
import DescriptionInput from '../inputs/DescriptionInput.vue'
|
||||
import StatusRadio from '../inputs/StatusRadio.vue'
|
||||
import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
|
||||
|
||||
/** 基础信息配置组件 */
|
||||
defineOptions({ name: 'BasicInfoSection' })
|
||||
|
||||
interface Props {
|
||||
modelValue: RuleSceneFormData
|
||||
rules?: any
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: RuleSceneFormData): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formData = useVModel(props, 'modelValue', emit)
|
||||
// TODO @puhui999:看看能不能 unocss
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
108
src/views/iot/rule/scene/form/sections/PreviewSection.vue
Normal file
108
src/views/iot/rule/scene/form/sections/PreviewSection.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<!-- 预览区域组件 -->
|
||||
<!-- TODO @puhui999:是不是不用这个哈? -->
|
||||
<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:view" class="text-[var(--el-color-primary)] text-18px" />
|
||||
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">配置预览</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-8px">
|
||||
<el-button type="primary" size="small" @click="handleValidate" :loading="validating">
|
||||
<Icon icon="ep:check" />
|
||||
验证配置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="p-0">
|
||||
<!-- 基础信息预览 -->
|
||||
<div class="mb-20px">
|
||||
<div class="flex items-center gap-8px mb-12px">
|
||||
<Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-16px" />
|
||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">基础信息</span>
|
||||
</div>
|
||||
<div class="p-12px bg-[var(--el-fill-color-light)] rounded-6px">
|
||||
<ConfigPreview :form-data="formData" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 触发器预览 -->
|
||||
<div class="mb-20px">
|
||||
<div class="flex items-center gap-8px mb-12px">
|
||||
<Icon icon="ep:lightning" class="text-[var(--el-color-warning)] text-16px" />
|
||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">触发器配置</span>
|
||||
<el-tag size="small" type="primary">{{ formData.triggers.length }}</el-tag>
|
||||
</div>
|
||||
<div class="p-12px bg-[var(--el-fill-color-light)] rounded-6px">
|
||||
<TriggerPreview :triggers="formData.triggers" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 执行器预览 -->
|
||||
<div class="mb-20px">
|
||||
<div class="flex items-center gap-8px mb-12px">
|
||||
<Icon icon="ep:setting" class="text-[var(--el-color-success)] text-16px" />
|
||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">执行器配置</span>
|
||||
<el-tag size="small" type="success">{{ formData.actions.length }}</el-tag>
|
||||
</div>
|
||||
<div class="p-12px bg-[var(--el-fill-color-light)] rounded-6px">
|
||||
<ActionPreview :actions="formData.actions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证结果 -->
|
||||
<div class="mb-20px">
|
||||
<div class="flex items-center gap-8px mb-12px">
|
||||
<Icon icon="ep:circle-check" class="text-[var(--el-color-primary)] text-16px" />
|
||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">验证结果</span>
|
||||
</div>
|
||||
<div class="p-12px bg-[var(--el-fill-color-light)] rounded-6px">
|
||||
<ValidationResult :validation-result="validationResult" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ConfigPreview from '../previews/ConfigPreview.vue'
|
||||
import TriggerPreview from '../previews/TriggerPreview.vue'
|
||||
import ActionPreview from '../previews/ActionPreview.vue'
|
||||
import ValidationResult from '../previews/ValidationResult.vue'
|
||||
import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
|
||||
|
||||
/** 预览区域组件 */
|
||||
defineOptions({ name: 'PreviewSection' })
|
||||
|
||||
interface Props {
|
||||
formData: RuleSceneFormData
|
||||
validationResult?: { valid: boolean; message?: string } | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'validate'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 状态
|
||||
const validating = ref(false)
|
||||
|
||||
// 事件处理
|
||||
const handleValidate = async () => {
|
||||
validating.value = true
|
||||
try {
|
||||
// 延迟一下模拟验证过程
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
emit('validate')
|
||||
} finally {
|
||||
validating.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
282
src/views/iot/rule/scene/form/sections/TriggerSection.vue
Normal file
282
src/views/iot/rule/scene/form/sections/TriggerSection.vue
Normal file
@ -0,0 +1,282 @@
|
||||
<!-- 触发器配置组件 -->
|
||||
<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>
|
||||
<!-- TODO @puhui999:是不是去掉 maxTriggers;计数 -->
|
||||
<el-tag size="small" type="info">{{ triggers.length }}/{{ maxTriggers }}</el-tag>
|
||||
</div>
|
||||
<div class="flex items-center gap-8px">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="addTrigger"
|
||||
:disabled="triggers.length >= maxTriggers"
|
||||
>
|
||||
<Icon icon="ep:plus" />
|
||||
添加触发器
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="p-0">
|
||||
<!-- 空状态 -->
|
||||
<div v-if="triggers.length === 0">
|
||||
<el-empty description="暂无触发器配置">
|
||||
<!-- TODO @puhui999:这个要不要去掉哈;入口统一点 -->
|
||||
<el-button type="primary" @click="addTrigger">
|
||||
<Icon icon="ep:plus" />
|
||||
添加第一个触发器
|
||||
</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
|
||||
<!-- 触发器列表 -->
|
||||
<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>
|
||||
|
||||
<div class="space-y-16px">
|
||||
<!-- 触发类型选择 -->
|
||||
<TriggerTypeSelector
|
||||
:model-value="trigger.type"
|
||||
@update:model-value="(value) => updateTriggerType(index, value)"
|
||||
@change="onTriggerTypeChange(trigger, $event)"
|
||||
/>
|
||||
|
||||
<!-- 设备触发配置 -->
|
||||
<DeviceTriggerConfig
|
||||
v-if="isDeviceTrigger(trigger.type)"
|
||||
:model-value="trigger"
|
||||
@update:model-value="(value) => updateTrigger(index, value)"
|
||||
@validate="(result) => handleTriggerValidate(index, result)"
|
||||
/>
|
||||
|
||||
<!-- 定时触发配置 -->
|
||||
<TimerTriggerConfig
|
||||
v-if="trigger.type === TriggerTypeEnum.TIMER"
|
||||
:model-value="trigger.cronExpression"
|
||||
@update:model-value="(value) => updateTriggerCronExpression(index, value)"
|
||||
@validate="(result) => handleTriggerValidate(index, result)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加提示 -->
|
||||
<!-- TODO @puhui999:这个要不要去掉哈;入口统一点 -->
|
||||
<div v-if="triggers.length > 0 && triggers.length < maxTriggers" class="text-center py-16px">
|
||||
<el-button type="primary" plain @click="addTrigger">
|
||||
<Icon icon="ep:plus" />
|
||||
继续添加触发器
|
||||
</el-button>
|
||||
<span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]"> 最多可添加 {{ maxTriggers }} 个触发器 </span>
|
||||
</div>
|
||||
|
||||
<!-- 验证结果 -->
|
||||
<div v-if="validationMessage" class="validation-result">
|
||||
<el-alert
|
||||
:title="validationMessage"
|
||||
:type="isValid ? 'success' : 'error'"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import TriggerTypeSelector from '../selectors/TriggerTypeSelector.vue'
|
||||
import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue'
|
||||
import TimerTriggerConfig from '../configs/TimerTriggerConfig.vue'
|
||||
import {
|
||||
TriggerFormData,
|
||||
IotRuleSceneTriggerTypeEnum as TriggerTypeEnum
|
||||
} from '@/api/iot/rule/scene/scene.types'
|
||||
import { createDefaultTriggerData } from '../../utils/transform'
|
||||
|
||||
/** 触发器配置组件 */
|
||||
defineOptions({ name: 'TriggerSection' })
|
||||
|
||||
interface Props {
|
||||
triggers: TriggerFormData[]
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:triggers', value: TriggerFormData[]): void
|
||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const triggers = useVModel(props, 'triggers', emit)
|
||||
|
||||
// 配置常量
|
||||
const maxTriggers = 5
|
||||
|
||||
// 验证状态
|
||||
const triggerValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({})
|
||||
const validationMessage = ref('')
|
||||
const isValid = ref(true)
|
||||
|
||||
// 触发器类型映射
|
||||
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) => {
|
||||
return [
|
||||
TriggerTypeEnum.DEVICE_STATE_UPDATE,
|
||||
TriggerTypeEnum.DEVICE_PROPERTY_POST,
|
||||
TriggerTypeEnum.DEVICE_EVENT_POST,
|
||||
TriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||
].includes(type)
|
||||
}
|
||||
|
||||
const getTriggerTypeName = (type: number) => {
|
||||
return triggerTypeNames[type] || '未知类型'
|
||||
}
|
||||
|
||||
const getTriggerTypeTag = (type: number) => {
|
||||
return triggerTypeTags[type] || 'info'
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
const addTrigger = () => {
|
||||
if (triggers.value.length >= maxTriggers) {
|
||||
return
|
||||
}
|
||||
|
||||
const newTrigger = createDefaultTriggerData()
|
||||
triggers.value.push(newTrigger)
|
||||
}
|
||||
|
||||
const removeTrigger = (index: number) => {
|
||||
triggers.value.splice(index, 1)
|
||||
delete triggerValidations.value[index]
|
||||
|
||||
// 重新索引验证结果
|
||||
const newValidations: { [key: number]: { valid: boolean; message: string } } = {}
|
||||
Object.keys(triggerValidations.value).forEach((key) => {
|
||||
const numKey = parseInt(key)
|
||||
if (numKey > index) {
|
||||
newValidations[numKey - 1] = triggerValidations.value[numKey]
|
||||
} else if (numKey < index) {
|
||||
newValidations[numKey] = triggerValidations.value[numKey]
|
||||
}
|
||||
})
|
||||
triggerValidations.value = newValidations
|
||||
|
||||
updateValidationResult()
|
||||
}
|
||||
|
||||
const updateTriggerType = (index: number, type: number) => {
|
||||
triggers.value[index].type = type
|
||||
onTriggerTypeChange(triggers.value[index], type)
|
||||
}
|
||||
|
||||
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) => {
|
||||
// 清理不相关的配置
|
||||
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 * * ?'
|
||||
}
|
||||
} else {
|
||||
trigger.cronExpression = undefined
|
||||
if (type === TriggerTypeEnum.DEVICE_STATE_UPDATE) {
|
||||
trigger.conditionGroups = undefined
|
||||
} else if (!trigger.conditionGroups) {
|
||||
trigger.conditionGroups = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleTriggerValidate = (index: number, result: { valid: boolean; message: string }) => {
|
||||
triggerValidations.value[index] = result
|
||||
updateValidationResult()
|
||||
}
|
||||
|
||||
const updateValidationResult = () => {
|
||||
const validations = Object.values(triggerValidations.value)
|
||||
const allValid = validations.every((v) => v.valid)
|
||||
const hasValidations = validations.length > 0
|
||||
|
||||
if (!hasValidations) {
|
||||
isValid.value = true
|
||||
validationMessage.value = ''
|
||||
} else if (allValid) {
|
||||
isValid.value = true
|
||||
validationMessage.value = '所有触发器配置验证通过'
|
||||
} else {
|
||||
isValid.value = false
|
||||
const errorMessages = validations.filter((v) => !v.valid).map((v) => v.message)
|
||||
validationMessage.value = `触发器配置错误: ${errorMessages.join('; ')}`
|
||||
}
|
||||
|
||||
emit('validate', { valid: isValid.value, message: validationMessage.value })
|
||||
}
|
||||
|
||||
// 监听触发器数量变化
|
||||
watch(
|
||||
() => triggers.value.length,
|
||||
() => {
|
||||
updateValidationResult()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user