perf:【IoT 物联网】场景联动目录结构优化
This commit is contained in:
@ -26,13 +26,6 @@
|
||||
|
||||
<!-- 执行器配置 -->
|
||||
<ActionSection v-model:actions="formData.actions" @validate="handleActionValidate" />
|
||||
|
||||
<!-- 预览区域 -->
|
||||
<PreviewSection
|
||||
:form-data="formData"
|
||||
:validation-result="validationResult"
|
||||
@validate="handleValidate"
|
||||
/>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
@ -48,11 +41,16 @@ import { useVModel } from '@vueuse/core'
|
||||
import BasicInfoSection from './sections/BasicInfoSection.vue'
|
||||
import TriggerSection from './sections/TriggerSection.vue'
|
||||
import ActionSection from './sections/ActionSection.vue'
|
||||
import PreviewSection from './sections/PreviewSection.vue'
|
||||
import { RuleSceneFormData, IotRuleScene } from '@/api/iot/rule/scene/scene.types'
|
||||
import {
|
||||
RuleSceneFormData,
|
||||
IotRuleScene,
|
||||
IotRuleSceneTriggerTypeEnum,
|
||||
IotRuleSceneActionTypeEnum,
|
||||
CommonStatusEnum
|
||||
} from '@/api/iot/rule/scene/scene.types'
|
||||
import { getBaseValidationRules } from '../utils/validation'
|
||||
import { transformFormToApi, transformApiToForm, createDefaultFormData } from '../utils/transform'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { generateUUID } from '@/utils'
|
||||
|
||||
/** IoT 场景联动规则表单 - 主表单组件 */
|
||||
defineOptions({ name: 'RuleSceneForm' })
|
||||
@ -72,12 +70,93 @@ const emit = defineEmits<Emits>()
|
||||
|
||||
const drawerVisible = useVModel(props, 'modelValue', emit)
|
||||
|
||||
/**
|
||||
* 创建默认的表单数据
|
||||
*/
|
||||
const createDefaultFormData = (): RuleSceneFormData => {
|
||||
return {
|
||||
name: '',
|
||||
description: '',
|
||||
status: CommonStatusEnum.ENABLE, // 默认启用状态
|
||||
triggers: [],
|
||||
actions: []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将表单数据转换为API请求格式
|
||||
*/
|
||||
const transformFormToApi = (formData: RuleSceneFormData): IotRuleScene => {
|
||||
return {
|
||||
id: formData.id,
|
||||
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
|
||||
}))
|
||||
})) || []
|
||||
})) || [],
|
||||
actions:
|
||||
formData.actions?.map((action) => ({
|
||||
type: action.type,
|
||||
alertConfigId: action.alertConfigId,
|
||||
deviceControl:
|
||||
action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
|
||||
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
|
||||
? {
|
||||
productKey: action.productId ? `product_${action.productId}` : '',
|
||||
deviceNames: action.deviceId ? [`device_${action.deviceId}`] : [],
|
||||
type: 'property',
|
||||
identifier: 'set',
|
||||
params: action.params || {}
|
||||
}
|
||||
: undefined
|
||||
})) || []
|
||||
} as IotRuleScene
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 API 响应数据转换为表单格式
|
||||
*/
|
||||
const transformApiToForm = (apiData: IotRuleScene): RuleSceneFormData => {
|
||||
return {
|
||||
...apiData,
|
||||
status: Number(apiData.status), // 确保状态为数字类型
|
||||
triggers:
|
||||
apiData.triggers?.map((trigger) => ({
|
||||
...trigger,
|
||||
type: Number(trigger.type),
|
||||
// 为每个触发器添加唯一标识符,解决组件索引重用问题
|
||||
key: generateUUID()
|
||||
})) || [],
|
||||
actions:
|
||||
apiData.actions?.map((action) => ({
|
||||
...action,
|
||||
type: Number(action.type),
|
||||
// 为每个执行器添加唯一标识符,解决组件索引重用问题
|
||||
key: generateUUID()
|
||||
})) || []
|
||||
}
|
||||
}
|
||||
|
||||
// 表单数据和状态
|
||||
const formRef = ref()
|
||||
const formData = ref<RuleSceneFormData>(createDefaultFormData())
|
||||
const formRules = getBaseValidationRules()
|
||||
const submitLoading = ref(false)
|
||||
const validationResult = ref<{ valid: boolean; message?: string } | null>(null)
|
||||
|
||||
// 验证状态
|
||||
const triggerValidation = ref({ valid: true, message: '' })
|
||||
@ -87,16 +166,6 @@ const actionValidation = ref({ valid: true, message: '' })
|
||||
const isEdit = computed(() => !!props.ruleScene?.id)
|
||||
const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' : '新增场景联动规则'))
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
return (
|
||||
formData.value.name &&
|
||||
formData.value.triggers.length > 0 &&
|
||||
formData.value.actions.length > 0 &&
|
||||
triggerValidation.value.valid &&
|
||||
actionValidation.value.valid
|
||||
)
|
||||
})
|
||||
|
||||
// 事件处理
|
||||
const handleTriggerValidate = (result: { valid: boolean; message: string }) => {
|
||||
triggerValidation.value = result
|
||||
@ -106,29 +175,6 @@ const handleActionValidate = (result: { valid: boolean; message: string }) => {
|
||||
actionValidation.value = result
|
||||
}
|
||||
|
||||
const handleValidate = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
|
||||
if (!triggerValidation.value.valid) {
|
||||
throw new Error(triggerValidation.value.message)
|
||||
}
|
||||
|
||||
if (!actionValidation.value.valid) {
|
||||
throw new Error(actionValidation.value.message)
|
||||
}
|
||||
|
||||
validationResult.value = { valid: true, message: '验证通过' }
|
||||
ElMessage.success('规则验证通过')
|
||||
return true
|
||||
} catch (error: any) {
|
||||
const message = error.message || '表单验证失败'
|
||||
validationResult.value = { valid: false, message }
|
||||
ElMessage.error(message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 校验表单
|
||||
if (!formRef.value) return
|
||||
@ -167,7 +213,6 @@ const handleSubmit = async () => {
|
||||
|
||||
const handleClose = () => {
|
||||
drawerVisible.value = false
|
||||
validationResult.value = null
|
||||
}
|
||||
|
||||
// 初始化表单数据
|
||||
|
||||
@ -1,76 +0,0 @@
|
||||
<!-- 执行器预览组件 -->
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div v-if="actions.length === 0" class="text-center py-20px">
|
||||
<el-text type="info" size="small">暂无执行器配置</el-text>
|
||||
</div>
|
||||
<div v-else class="space-y-12px">
|
||||
<div
|
||||
v-for="(action, index) in actions"
|
||||
:key="index"
|
||||
class="p-12px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]"
|
||||
>
|
||||
<div class="flex items-center gap-8px mb-8px">
|
||||
<Icon icon="ep:setting" class="text-[var(--el-color-success)] text-16px" />
|
||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">执行器 {{ index + 1 }}</span>
|
||||
<el-tag :type="getActionTypeTag(action.type)" size="small">
|
||||
{{ getActionTypeName(action.type) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="pl-24px">
|
||||
<div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">
|
||||
{{ getActionSummary(action) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ActionFormData, IotRuleSceneActionTypeEnum } from '@/api/iot/rule/scene/scene.types'
|
||||
|
||||
/** 执行器预览组件 */
|
||||
defineOptions({ name: 'ActionPreview' })
|
||||
|
||||
interface Props {
|
||||
actions: ActionFormData[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// 执行器类型映射
|
||||
const actionTypeNames = {
|
||||
[IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET]: '属性设置',
|
||||
[IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE]: '服务调用',
|
||||
[IotRuleSceneActionTypeEnum.ALERT_TRIGGER]: '触发告警',
|
||||
[IotRuleSceneActionTypeEnum.ALERT_RECOVER]: '恢复告警'
|
||||
}
|
||||
|
||||
const actionTypeTags = {
|
||||
[IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET]: 'primary',
|
||||
[IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE]: 'success',
|
||||
[IotRuleSceneActionTypeEnum.ALERT_TRIGGER]: 'danger',
|
||||
[IotRuleSceneActionTypeEnum.ALERT_RECOVER]: 'warning'
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
const getActionTypeName = (type: number) => {
|
||||
return actionTypeNames[type] || '未知类型'
|
||||
}
|
||||
|
||||
const getActionTypeTag = (type: number) => {
|
||||
return actionTypeTags[type] || 'info'
|
||||
}
|
||||
|
||||
const getActionSummary = (action: ActionFormData) => {
|
||||
if (action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER || action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER) {
|
||||
return `告警配置: ${action.alertConfigId ? `配置ID ${action.alertConfigId}` : '未选择'}`
|
||||
} else {
|
||||
const paramsCount = action.params ? Object.keys(action.params).length : 0
|
||||
return `设备控制: 产品${action.productId || '未选择'} 设备${action.deviceId || '未选择'} (${paramsCount}个参数)`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
<!-- 配置预览组件 -->
|
||||
<!-- TODO @puhui999:应该暂时不用预览哈 -->
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="space-y-8px">
|
||||
<div class="flex items-start gap-8px">
|
||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">场景名称:</span>
|
||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ formData.name || '未设置' }}</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-8px">
|
||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">场景状态:</span>
|
||||
<el-tag :type="formData.status === 0 ? 'success' : 'danger'" size="small">
|
||||
{{ formData.status === 0 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-if="formData.description" class="flex items-start gap-8px">
|
||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">场景描述:</span>
|
||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ formData.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
|
||||
|
||||
/** 配置预览组件 */
|
||||
defineOptions({ name: 'ConfigPreview' })
|
||||
|
||||
interface Props {
|
||||
formData: RuleSceneFormData
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,226 +0,0 @@
|
||||
<!-- 下次执行时间预览组件 -->
|
||||
<template>
|
||||
<div class="next-execution-preview">
|
||||
<div class="preview-header">
|
||||
<Icon icon="ep:timer" class="preview-icon" />
|
||||
<span class="preview-title">执行时间预览</span>
|
||||
</div>
|
||||
|
||||
<div v-if="isValidCron" class="preview-content">
|
||||
<div class="current-expression">
|
||||
<span class="expression-label">CRON表达式:</span>
|
||||
<code class="expression-code">{{ cronExpression }}</code>
|
||||
</div>
|
||||
|
||||
<div class="description">
|
||||
<span class="description-label">执行规律:</span>
|
||||
<span class="description-text">{{ cronDescription }}</span>
|
||||
</div>
|
||||
|
||||
<div class="next-times">
|
||||
<span class="times-label">接下来5次执行时间:</span>
|
||||
<div class="times-list">
|
||||
<div
|
||||
v-for="(time, index) in nextExecutionTimes"
|
||||
:key="index"
|
||||
class="time-item"
|
||||
>
|
||||
<Icon icon="ep:clock" class="time-icon" />
|
||||
<span class="time-text">{{ time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="preview-error">
|
||||
<el-alert
|
||||
title="CRON表达式无效"
|
||||
description="请检查CRON表达式格式是否正确"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { validateCronExpression } from '../../utils/validation'
|
||||
|
||||
/** 下次执行时间预览组件 */
|
||||
defineOptions({ name: 'NextExecutionPreview' })
|
||||
|
||||
interface Props {
|
||||
cronExpression?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// 计算属性
|
||||
const isValidCron = computed(() => {
|
||||
return props.cronExpression ? validateCronExpression(props.cronExpression) : false
|
||||
})
|
||||
|
||||
const cronDescription = computed(() => {
|
||||
if (!isValidCron.value) return ''
|
||||
|
||||
// 简单的CRON描述生成
|
||||
const parts = props.cronExpression?.split(' ') || []
|
||||
if (parts.length < 6) return '无法解析'
|
||||
|
||||
const [second, minute, hour, day, month, week] = parts
|
||||
|
||||
// 生成描述
|
||||
let description = ''
|
||||
|
||||
if (second === '0' && minute === '0' && hour === '12' && day === '*' && month === '*' && week === '?') {
|
||||
description = '每天中午12点执行'
|
||||
} else if (second === '0' && minute === '*' && hour === '*' && day === '*' && month === '*' && week === '?') {
|
||||
description = '每分钟执行一次'
|
||||
} else if (second === '0' && minute === '0' && hour === '*' && day === '*' && month === '*' && week === '?') {
|
||||
description = '每小时执行一次'
|
||||
} else {
|
||||
description = '按自定义时间规律执行'
|
||||
}
|
||||
|
||||
return description
|
||||
})
|
||||
|
||||
const nextExecutionTimes = computed(() => {
|
||||
if (!isValidCron.value) return []
|
||||
|
||||
// 模拟生成下次执行时间
|
||||
const now = new Date()
|
||||
const times = []
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
// 这里应该使用真实的CRON解析库来计算
|
||||
// 暂时生成模拟时间
|
||||
const nextTime = new Date(now.getTime() + i * 60 * 60 * 1000)
|
||||
times.push(nextTime.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}))
|
||||
}
|
||||
|
||||
return times
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.next-execution-preview {
|
||||
margin-top: 16px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 6px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.preview-icon {
|
||||
color: var(--el-color-primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.current-expression {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.expression-label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.expression-code {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: var(--el-fill-color-light);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.description-label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.next-times {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.times-label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.times-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.time-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.time-icon {
|
||||
color: var(--el-color-success);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.time-text {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-primary);
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.preview-error {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
@ -1,80 +0,0 @@
|
||||
<!-- 触发器预览组件 -->
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div v-if="triggers.length === 0" class="text-center py-20px">
|
||||
<el-text type="info" size="small">暂无触发器配置</el-text>
|
||||
</div>
|
||||
<div v-else class="space-y-12px">
|
||||
<div
|
||||
v-for="(trigger, index) in triggers"
|
||||
:key="index"
|
||||
class="p-12px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]"
|
||||
>
|
||||
<div class="flex items-center gap-8px mb-8px">
|
||||
<Icon icon="ep:lightning" class="text-[var(--el-color-warning)] text-16px" />
|
||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">触发器 {{ index + 1 }}</span>
|
||||
<el-tag :type="getTriggerTypeTag(trigger.type)" size="small">
|
||||
{{ getTriggerTypeName(trigger.type) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="pl-24px">
|
||||
<div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">
|
||||
{{ getTriggerSummary(trigger) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TriggerFormData, IotRuleSceneTriggerTypeEnum } from '@/api/iot/rule/scene/scene.types'
|
||||
|
||||
/** 触发器预览组件 */
|
||||
defineOptions({ name: 'TriggerPreview' })
|
||||
|
||||
interface Props {
|
||||
triggers: TriggerFormData[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// 触发器类型映射
|
||||
const triggerTypeNames = {
|
||||
[IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE]: '设备状态变更',
|
||||
[IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST]: '属性上报',
|
||||
[IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST]: '事件上报',
|
||||
[IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE]: '服务调用',
|
||||
[IotRuleSceneTriggerTypeEnum.TIMER]: '定时触发'
|
||||
}
|
||||
|
||||
const triggerTypeTags = {
|
||||
[IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE]: 'warning',
|
||||
[IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST]: 'primary',
|
||||
[IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST]: 'success',
|
||||
[IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE]: 'info',
|
||||
[IotRuleSceneTriggerTypeEnum.TIMER]: 'danger'
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
const getTriggerTypeName = (type: number) => {
|
||||
return triggerTypeNames[type] || '未知类型'
|
||||
}
|
||||
|
||||
const getTriggerTypeTag = (type: number) => {
|
||||
return triggerTypeTags[type] || 'info'
|
||||
}
|
||||
|
||||
const getTriggerSummary = (trigger: TriggerFormData) => {
|
||||
if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
|
||||
return `定时执行: ${trigger.cronExpression || '未配置'}`
|
||||
} else if (trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) {
|
||||
return `设备状态变更: 产品${trigger.productId || '未选择'} 设备${trigger.deviceId || '未选择'}`
|
||||
} else {
|
||||
const conditionCount = trigger.conditionGroups?.reduce((total, group) => total + (group.conditions?.length || 0), 0) || 0
|
||||
return `设备监控: 产品${trigger.productId || '未选择'} 设备${trigger.deviceId || '未选择'} (${conditionCount}个条件)`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,120 +0,0 @@
|
||||
<!-- 验证结果组件 -->
|
||||
<template>
|
||||
<div class="validation-result">
|
||||
<div v-if="!validationResult" class="no-validation">
|
||||
<el-text type="info" size="small">
|
||||
<Icon icon="ep:info-filled" />
|
||||
点击"验证配置"按钮检查规则配置
|
||||
</el-text>
|
||||
</div>
|
||||
<div v-else class="validation-content">
|
||||
<el-alert
|
||||
:title="validationResult.valid ? '配置验证通过' : '配置验证失败'"
|
||||
:description="validationResult.message"
|
||||
:type="validationResult.valid ? 'success' : 'error'"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<template #default>
|
||||
<div v-if="validationResult.valid" class="success-content">
|
||||
<p>{{ validationResult.message || '所有配置项验证通过,规则可以正常运行' }}</p>
|
||||
<div class="success-tips">
|
||||
<Icon icon="ep:check" class="tip-icon" />
|
||||
<span class="tip-text">规则配置完整且有效</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="error-content">
|
||||
<p>{{ validationResult.message || '配置验证失败,请检查以下问题' }}</p>
|
||||
<div class="error-tips">
|
||||
<div class="tip-item">
|
||||
<Icon icon="ep:warning-filled" class="tip-icon error" />
|
||||
<span class="tip-text">请确保所有必填项都已配置</span>
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<Icon icon="ep:warning-filled" class="tip-icon error" />
|
||||
<span class="tip-text">请检查触发器和执行器配置是否正确</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/** 验证结果组件 */
|
||||
defineOptions({ name: 'ValidationResult' })
|
||||
|
||||
interface Props {
|
||||
validationResult?: { valid: boolean; message?: string } | null
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.validation-result {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.no-validation {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.validation-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.success-content,
|
||||
.error-content {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.success-content p,
|
||||
.error-content p {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.success-tips,
|
||||
.error-tips {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tip-icon {
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tip-icon:not(.error) {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.tip-icon.error {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.success-tips .tip-text {
|
||||
color: var(--el-color-success-dark-2);
|
||||
}
|
||||
|
||||
.error-tips .tip-text {
|
||||
color: var(--el-color-danger-dark-2);
|
||||
}
|
||||
</style>
|
||||
@ -117,7 +117,6 @@ import {
|
||||
ActionFormData,
|
||||
IotRuleSceneActionTypeEnum as ActionTypeEnum
|
||||
} from '@/api/iot/rule/scene/scene.types'
|
||||
import { createDefaultActionData } from '../../utils/transform'
|
||||
|
||||
/** 执行器配置组件 */
|
||||
defineOptions({ name: 'ActionSection' })
|
||||
@ -136,6 +135,19 @@ const emit = defineEmits<Emits>()
|
||||
|
||||
const actions = useVModel(props, 'actions', emit)
|
||||
|
||||
/**
|
||||
* 创建默认的执行器数据
|
||||
*/
|
||||
const createDefaultActionData = (): ActionFormData => {
|
||||
return {
|
||||
type: ActionTypeEnum.DEVICE_PROPERTY_SET, // 默认为设备属性设置
|
||||
productId: undefined,
|
||||
deviceId: undefined,
|
||||
params: {},
|
||||
alertConfigId: undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 配置常量
|
||||
const maxActions = 5
|
||||
|
||||
|
||||
@ -1,108 +0,0 @@
|
||||
<!-- 预览区域组件 -->
|
||||
<!-- 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>
|
||||
|
||||
|
||||
@ -119,7 +119,6 @@ import {
|
||||
TriggerFormData,
|
||||
IotRuleSceneTriggerTypeEnum as TriggerTypeEnum
|
||||
} from '@/api/iot/rule/scene/scene.types'
|
||||
import { createDefaultTriggerData } from '../../utils/transform'
|
||||
|
||||
/** 触发器配置组件 */
|
||||
defineOptions({ name: 'TriggerSection' })
|
||||
@ -138,6 +137,22 @@ 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 maxTriggers = 5
|
||||
|
||||
|
||||
Reference in New Issue
Block a user