perf:【IoT 物联网】场景联动目录结构优化

This commit is contained in:
puhui999
2025-07-27 22:44:06 +08:00
parent 8549399ae8
commit d3d6f8f8ab
13 changed files with 275 additions and 1819 deletions

View File

@@ -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
}
// 初始化表单数据

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -35,7 +35,6 @@
class="!w-240px"
/>
</el-form-item>
<!-- TODO @puhui999字典 -->
<el-form-item label="规则状态">
<el-select
v-model="queryParams.status"
@@ -43,8 +42,12 @@
clearable
class="!w-240px"
>
<el-option label="启用" :value="0" />
<el-option label="禁用" :value="1" />
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
@@ -61,7 +64,6 @@
</el-card>
<!-- 统计卡片 -->
<!-- TODO @puhui999这种需要服用的 stats-contentstats-info 的属性到底 unocss 还是现有的 style css ~ -->
<el-row :gutter="16" class="mb-16px">
<el-col :span="6">
<el-card class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px" shadow="hover">
@@ -125,14 +127,7 @@
<template #default="{ row }">
<div class="flex items-center gap-8px">
<span class="font-500 text-[#303133]">{{ row.name }}</span>
<!-- TODO @puhui999字典 -->
<el-tag
:type="row.status === 0 ? 'success' : 'danger'"
size="small"
class="flex-shrink-0"
>
{{ row.status === 0 ? '启用' : '禁用' }}
</el-tag>
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
</div>
<div v-if="row.description" class="text-12px text-[#909399] mt-4px">
{{ row.description }}
@@ -169,7 +164,6 @@
</div>
</template>
</el-table-column>
<!-- TODO @puhui999貌似要新增一个字段 -->
<el-table-column label="最近触发" prop="lastTriggeredTime" width="180">
<template #default="{ row }">
<span v-if="row.lastTriggeredTime">
@@ -185,7 +179,6 @@
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<!-- TODO @puhui999间隙大了点 -->
<div class="flex gap-8px">
<el-button type="primary" link @click="handleEdit(row)">
<Icon icon="ep:edit" />
@@ -197,10 +190,9 @@
@click="handleToggleStatus(row)"
>
<Icon :icon="row.status === 0 ? 'ep:video-pause' : 'ep:video-play'" />
<!-- TODO @puhui999翻译字典 -->
{{ row.status === 0 ? '禁用' : '启用' }}
</el-button>
<el-button type="danger" link @click="handleDelete(row)">
<el-button type="danger" link @click="handleDelete(row.id)">
<Icon icon="ep:delete" />
删除
</el-button>
@@ -247,17 +239,17 @@
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { ContentWrap } from '@/components/ContentWrap'
import RuleSceneForm from './form/RuleSceneForm.vue'
import { IotRuleScene } from '@/api/iot/rule/scene/scene.types'
import { getRuleSceneSummary } from './utils/transform'
import { formatDate } from '@/utils/formatTime'
/** 场景联动规则管理页面 */
defineOptions({ name: 'IoTSceneRule' })
const message = useMessage()
// const { t } = useI18n() // TODO @puhui999可以删除
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
// 查询参数
const queryParams = reactive({
@@ -267,12 +259,11 @@ const queryParams = reactive({
status: undefined as number | undefined
})
// 数据状态
// TODO @puhui999变量名和别的页面保持一致哈
const loading = ref(true)
const list = ref<IotRuleScene[]>([])
const total = ref(0)
const selectedRows = ref<IotRuleScene[]>([])
const loading = ref(true) // 列表的加载中
const list = ref<IotRuleScene[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const selectedRows = ref<IotRuleScene[]>([]) // 选中的行数据
const queryFormRef = ref() // 搜索的表单
// 表单状态
const formVisible = ref(false)
@@ -286,8 +277,96 @@ const statistics = ref({
triggered: 0
})
// 获取列表数据
// TODO @puhui999接入
/**
* 格式化CRON表达式显示
*/
const formatCronExpression = (cron: string): string => {
if (!cron) return ''
// 简单的CRON表达式解析和格式化
const parts = cron.trim().split(' ')
if (parts.length < 5) return cron
const [second, minute, hour] = parts
// 构建可读的描述
let description = ''
if (second === '0' && minute === '0') {
if (hour === '*') {
description = '每小时'
} else if (hour.includes('/')) {
const interval = hour.split('/')[1]
description = `${interval}小时`
} else {
description = `每天${hour}`
}
} else if (second === '0') {
if (minute === '*') {
description = '每分钟'
} else if (minute.includes('/')) {
const interval = minute.split('/')[1]
description = `${interval}分钟`
} else {
description = `每小时第${minute}分钟`
}
} else {
if (second === '*') {
description = '每秒'
} else if (second.includes('/')) {
const interval = second.split('/')[1]
description = `${interval}`
}
}
return description || cron
}
/**
* 获取规则摘要信息
*/
const getRuleSceneSummary = (rule: IotRuleScene) => {
const triggerSummary =
rule.triggers?.map((trigger) => {
switch (trigger.type) {
case 1:
return `设备状态变更 (${trigger.deviceNames?.length || 0}个设备)`
case 2:
return `属性上报 (${trigger.deviceNames?.length || 0}个设备)`
case 3:
return `事件上报 (${trigger.deviceNames?.length || 0}个设备)`
case 4:
return `服务调用 (${trigger.deviceNames?.length || 0}个设备)`
case 100:
return `定时触发 (${formatCronExpression(trigger.cronExpression || '')})`
default:
return '未知触发类型'
}
}) || []
const actionSummary =
rule.actions?.map((action) => {
switch (action.type) {
case 1:
return `设备属性设置 (${action.deviceControl?.deviceNames?.length || 0}个设备)`
case 2:
return `设备服务调用 (${action.deviceControl?.deviceNames?.length || 0}个设备)`
case 100:
return '发送告警通知'
case 101:
return '发送邮件通知'
default:
return '未知执行类型'
}
}) || []
return {
triggerSummary: triggerSummary.join(', ') || '无触发器',
actionSummary: actionSummary.join(', ') || '无执行器'
}
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
@@ -355,9 +434,7 @@ const getList = async () => {
}
}
// TODO @puhui999方法注释使用 /** */ 风格
// 更新统计数据
/** 更新统计数据 */
const updateStatistics = () => {
statistics.value = {
total: list.value.length,
@@ -377,19 +454,20 @@ const getActionSummary = (rule: IotRuleScene) => {
return getRuleSceneSummary(rule).actionSummary
}
// 事件处理
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryParams.name = ''
queryParams.status = undefined
handleQuery()
}
// TODO @puhui999这个要不还是使用 open 方式,只是弹出的右侧;
/** 添加/修改操作 */
const handleAdd = () => {
currentRule.value = undefined
formVisible.value = true
@@ -400,78 +478,76 @@ const handleEdit = (row: IotRuleScene) => {
formVisible.value = true
}
// TODO @puhui999handleDelete、handleToggleStatus 保持和别的模块一致哇?
const handleDelete = async (row: IotRuleScene) => {
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
await ElMessageBox.confirm('确定要删除这个规则吗?', '提示', {
type: 'warning'
})
// 删除的二次确认
await message.delConfirm()
// 发起删除
// await RuleSceneApi.deleteRuleScene(id)
// 这里应该调用删除API
message.success('删除成功')
getList()
} catch (error) {
// 用户取消删除
}
// 模拟删除操作
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 修改状态 */
const handleToggleStatus = async (row: IotRuleScene) => {
try {
const newStatus = row.status === 0 ? 1 : 0
const action = newStatus === 0 ? '用' : '用'
// 修改状态的二次确认
const text = row.status === 0 ? '用' : '用'
await message.confirm('确认要' + text + '"' + row.name + '"吗?')
// 发起修改状态
// await RuleSceneApi.updateRuleSceneStatus(row.id, row.status === 0 ? 1 : 0)
await ElMessageBox.confirm(`确定要${action}这个规则吗?`, '提示', {
type: 'warning'
})
// 这里应该调用状态切换API
row.status = newStatus
message.success(`${action}成功`)
// 模拟状态切换
row.status = row.status === 0 ? 1 : 0
message.success(text + '成功')
// 刷新统计
updateStatistics()
} catch (error) {
// 用户取消操作
} catch {
// 取消后,进行恢复按钮
row.status = row.status === 0 ? 1 : 0
}
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: IotRuleScene[]) => {
selectedRows.value = selection
}
// TODO @puhui999batch 操作的逻辑,要不和其它 UI 界面保持一致,或者相对一致哈;
/** 批量启用操作 */
const handleBatchEnable = async () => {
try {
await ElMessageBox.confirm(`确定要启用选中的 ${selectedRows.value.length} 个规则吗?`, '提示', {
type: 'warning'
})
await message.confirm(`确定要启用选中的 ${selectedRows.value.length} 个规则吗?`)
// 这里应该调用批量启用API
// await RuleSceneApi.updateRuleSceneStatusBatch(selectedRows.value.map(row => row.id), 0)
// 模拟批量启用
selectedRows.value.forEach((row) => {
row.status = 0
})
message.success('批量启用成功')
updateStatistics()
} catch (error) {
// 用户取消操作
}
} catch {}
}
/** 批量禁用操作 */
const handleBatchDisable = async () => {
try {
await ElMessageBox.confirm(`确定要禁用选中的 ${selectedRows.value.length} 个规则吗?`, '提示', {
type: 'warning'
})
await message.confirm(`确定要禁用选中的 ${selectedRows.value.length} 个规则吗?`)
// 这里应该调用批量禁用API
// await RuleSceneApi.updateRuleSceneStatusBatch(selectedRows.value.map(row => row.id), 1)
// 模拟批量禁用
selectedRows.value.forEach((row) => {
row.status = 1
})
message.success('批量禁用成功')
updateStatistics()
} catch (error) {
// 用户取消操作
}
} catch {}
}
const handleBatchDelete = async () => {

View File

@@ -1,550 +0,0 @@
/**
* IoT 场景联动错误处理和用户反馈工具
*/
// TODO @puhui999这个貌似用不到
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
// 错误类型枚举
export enum ErrorType {
VALIDATION = 'validation',
NETWORK = 'network',
BUSINESS = 'business',
SYSTEM = 'system',
PERMISSION = 'permission'
}
// 错误级别枚举
export enum ErrorLevel {
INFO = 'info',
WARNING = 'warning',
ERROR = 'error',
CRITICAL = 'critical'
}
// 错误信息接口
export interface ErrorInfo {
type: ErrorType
level: ErrorLevel
code?: string
message: string
details?: any
timestamp?: Date
context?: string
}
// 用户反馈选项
export interface FeedbackOptions {
showMessage?: boolean
showNotification?: boolean
showDialog?: boolean
autoClose?: boolean
duration?: number
confirmText?: string
cancelText?: string
}
/**
* 错误处理器类
*/
export class SceneRuleErrorHandler {
private static instance: SceneRuleErrorHandler
private errorLog: ErrorInfo[] = []
private maxLogSize = 100
private constructor() {}
static getInstance(): SceneRuleErrorHandler {
if (!SceneRuleErrorHandler.instance) {
SceneRuleErrorHandler.instance = new SceneRuleErrorHandler()
}
return SceneRuleErrorHandler.instance
}
/**
* 处理错误
*/
handleError(error: ErrorInfo, options: FeedbackOptions = {}): Promise<boolean> {
// 记录错误日志
this.logError(error)
// 根据错误类型和级别选择处理方式
return this.processError(error, options)
}
/**
* 记录错误日志
*/
private logError(error: ErrorInfo): void {
const errorWithTimestamp = {
...error,
timestamp: new Date()
}
this.errorLog.unshift(errorWithTimestamp)
// 限制日志大小
if (this.errorLog.length > this.maxLogSize) {
this.errorLog = this.errorLog.slice(0, this.maxLogSize)
}
// 开发环境下打印到控制台
if (import.meta.env.DEV) {
console.error('[SceneRule Error]', errorWithTimestamp)
}
}
/**
* 处理错误
*/
private async processError(error: ErrorInfo, options: FeedbackOptions): Promise<boolean> {
const defaultOptions: FeedbackOptions = {
showMessage: true,
showNotification: false,
showDialog: false,
autoClose: true,
duration: 3000,
confirmText: '确定',
cancelText: '取消'
}
const finalOptions = { ...defaultOptions, ...options }
try {
// 根据错误级别决定反馈方式
switch (error.level) {
case ErrorLevel.INFO:
return this.handleInfoError(error, finalOptions)
case ErrorLevel.WARNING:
return this.handleWarningError(error, finalOptions)
case ErrorLevel.ERROR:
return this.handleNormalError(error, finalOptions)
case ErrorLevel.CRITICAL:
return this.handleCriticalError(error, finalOptions)
default:
return this.handleNormalError(error, finalOptions)
}
} catch (e) {
console.error('Error handler failed:', e)
return false
}
}
/**
* 处理信息级错误
*/
private async handleInfoError(error: ErrorInfo, options: FeedbackOptions): Promise<boolean> {
if (options.showMessage) {
ElMessage.info({
message: error.message,
duration: options.duration,
showClose: !options.autoClose
})
}
return true
}
/**
* 处理警告级错误
*/
private async handleWarningError(error: ErrorInfo, options: FeedbackOptions): Promise<boolean> {
if (options.showNotification) {
ElNotification.warning({
title: '警告',
message: error.message,
duration: options.duration
})
} else if (options.showMessage) {
ElMessage.warning({
message: error.message,
duration: options.duration,
showClose: !options.autoClose
})
}
return true
}
/**
* 处理普通错误
*/
private async handleNormalError(error: ErrorInfo, options: FeedbackOptions): Promise<boolean> {
if (options.showDialog) {
try {
await ElMessageBox.alert(error.message, '错误', {
type: 'error',
confirmButtonText: options.confirmText
})
return true
} catch (e) {
return false
}
} else if (options.showNotification) {
ElNotification.error({
title: '错误',
message: error.message,
duration: options.duration
})
} else if (options.showMessage) {
ElMessage.error({
message: error.message,
duration: options.duration,
showClose: !options.autoClose
})
}
return true
}
/**
* 处理严重错误
*/
private async handleCriticalError(error: ErrorInfo, _: FeedbackOptions): Promise<boolean> {
try {
await ElMessageBox.confirm(`${error.message}\n\n是否重新加载页面`, '严重错误', {
type: 'error',
confirmButtonText: '重新加载',
cancelButtonText: '继续使用'
})
// 用户选择重新加载
window.location.reload()
return true
} catch (e) {
// 用户选择继续使用
return false
}
}
/**
* 获取错误日志
*/
getErrorLog(): ErrorInfo[] {
return [...this.errorLog]
}
/**
* 清空错误日志
*/
clearErrorLog(): void {
this.errorLog = []
}
/**
* 导出错误日志
*/
exportErrorLog(): string {
return JSON.stringify(this.errorLog, null, 2)
}
}
/**
* 预定义的错误处理函数
*/
export const errorHandler = SceneRuleErrorHandler.getInstance()
/**
* 验证错误处理
*/
export function handleValidationError(message: string, context?: string): Promise<boolean> {
return errorHandler.handleError(
{
type: ErrorType.VALIDATION,
level: ErrorLevel.WARNING,
message,
context
},
{
showMessage: true,
duration: 4000
}
)
}
/**
* 网络错误处理
*/
export function handleNetworkError(error: any, context?: string): Promise<boolean> {
let message = '网络请求失败'
if (error?.response?.status) {
switch (error.response.status) {
case 400:
message = '请求参数错误'
break
case 401:
message = '未授权,请重新登录'
break
case 403:
message = '权限不足'
break
case 404:
message = '请求的资源不存在'
break
case 500:
message = '服务器内部错误'
break
case 502:
message = '网关错误'
break
case 503:
message = '服务暂不可用'
break
default:
message = `网络错误 (${error.response.status})`
}
} else if (error?.message) {
message = error.message
}
return errorHandler.handleError(
{
type: ErrorType.NETWORK,
level: ErrorLevel.ERROR,
code: error?.response?.status?.toString(),
message,
details: error,
context
},
{
showMessage: true,
duration: 5000
}
)
}
/**
* 业务逻辑错误处理
*/
export function handleBusinessError(
message: string,
code?: string,
context?: string
): Promise<boolean> {
return errorHandler.handleError(
{
type: ErrorType.BUSINESS,
level: ErrorLevel.ERROR,
code,
message,
context
},
{
showMessage: true,
duration: 4000
}
)
}
/**
* 系统错误处理
*/
export function handleSystemError(error: any, context?: string): Promise<boolean> {
const message = error?.message || '系统发生未知错误'
return errorHandler.handleError(
{
type: ErrorType.SYSTEM,
level: ErrorLevel.CRITICAL,
message,
details: error,
context
},
{
showDialog: true
}
)
}
/**
* 权限错误处理
*/
export function handlePermissionError(
message: string = '权限不足',
context?: string
): Promise<boolean> {
return errorHandler.handleError(
{
type: ErrorType.PERMISSION,
level: ErrorLevel.WARNING,
message,
context
},
{
showNotification: true,
duration: 5000
}
)
}
/**
* 成功反馈
*/
export function showSuccess(message: string, duration: number = 3000): void {
ElMessage.success({
message,
duration,
showClose: false
})
}
/**
* 信息反馈
*/
export function showInfo(message: string, duration: number = 3000): void {
ElMessage.info({
message,
duration,
showClose: false
})
}
/**
* 警告反馈
*/
export function showWarning(message: string, duration: number = 4000): void {
ElMessage.warning({
message,
duration,
showClose: true
})
}
/**
* 确认对话框
*/
export function showConfirm(
message: string,
title: string = '确认',
options: {
type?: 'info' | 'success' | 'warning' | 'error'
confirmText?: string
cancelText?: string
} = {}
): Promise<boolean> {
const defaultOptions = {
type: 'warning' as const,
confirmText: '确定',
cancelText: '取消'
}
const finalOptions = { ...defaultOptions, ...options }
return ElMessageBox.confirm(message, title, {
type: finalOptions.type,
confirmButtonText: finalOptions.confirmText,
cancelButtonText: finalOptions.cancelText
})
.then(() => true)
.catch(() => false)
}
/**
* 加载状态管理
*/
export class LoadingManager {
private loadingStates = new Map<string, boolean>()
private loadingInstances = new Map<string, any>()
/**
* 开始加载
*/
startLoading(key: string, _: string = '加载中...'): void {
if (this.loadingStates.get(key)) {
return // 已经在加载中
}
this.loadingStates.set(key, true)
// 这里可以根据需要创建全局加载实例
// const loading = ElLoading.service({
// lock: true,
// text,
// background: 'rgba(0, 0, 0, 0.7)'
// })
// this.loadingInstances.set(key, loading)
}
/**
* 结束加载
*/
stopLoading(key: string): void {
this.loadingStates.set(key, false)
const loading = this.loadingInstances.get(key)
if (loading) {
loading.close()
this.loadingInstances.delete(key)
}
}
/**
* 检查是否在加载中
*/
isLoading(key: string): boolean {
return this.loadingStates.get(key) || false
}
/**
* 清空所有加载状态
*/
clearAll(): void {
this.loadingInstances.forEach((loading) => loading.close())
this.loadingStates.clear()
this.loadingInstances.clear()
}
}
export const loadingManager = new LoadingManager()
/**
* 异步操作包装器,自动处理错误和加载状态
*/
export async function withErrorHandling<T>(
operation: () => Promise<T>,
options: {
loadingKey?: string
loadingText?: string
context?: string
showSuccess?: boolean
successMessage?: string
errorHandler?: (error: any) => Promise<boolean>
} = {}
): Promise<T | null> {
const {
loadingKey,
loadingText = '处理中...',
context,
showSuccess = false,
// successMessage = '操作成功',
errorHandler: customErrorHandler
} = options
try {
// 开始加载
if (loadingKey) {
loadingManager.startLoading(loadingKey, loadingText)
}
// 执行操作
const result = await operation()
// 显示成功消息
if (showSuccess) {
// showSuccess(successMessage)
}
return result
} catch (error) {
// 使用自定义错误处理器或默认处理器
if (customErrorHandler) {
await customErrorHandler(error)
} else {
await handleNetworkError(error, context)
}
return null
} finally {
// 结束加载
if (loadingKey) {
loadingManager.stopLoading(loadingKey)
}
}
}

View File

@@ -1,413 +0,0 @@
/**
* IoT 场景联动数据转换工具函数
*/
import {
IotRuleScene,
TriggerConfig,
ActionConfig,
RuleSceneFormData,
TriggerFormData,
ActionFormData
} from '@/api/iot/rule/scene/scene.types'
import { generateUUID } from '@/utils'
// TODO @puhui999这些是不是放到对应的界面会好一丢丢哈
/**
* 创建默认的表单数据
*/
export function createDefaultFormData(): RuleSceneFormData {
return {
name: '',
description: '',
status: 0, // TODO @puhui999枚举值
triggers: [],
actions: []
}
}
/**
* 创建默认的触发器数据
*/
export function createDefaultTriggerData(): TriggerFormData {
return {
type: 2, // 默认为属性上报 TODO @puhui999枚举值
productId: undefined,
deviceId: undefined,
identifier: undefined,
operator: undefined,
value: undefined,
cronExpression: undefined,
conditionGroups: []
}
}
/**
* 创建默认的执行器数据
*/
export function createDefaultActionData(): ActionFormData {
return {
type: 1, // 默认为属性设置 TODO @puhui999枚举值
productId: undefined,
deviceId: undefined,
params: {},
alertConfigId: undefined
}
}
/**
* 将表单数据转换为API请求格式
*/
export function transformFormToApi(formData: RuleSceneFormData): IotRuleScene {
// TODO @puhui999这个关注下
// 这里需要根据实际 API 结构进行转换
// 暂时返回基本结构
return {
id: formData.id,
name: formData.name,
description: formData.description,
status: Number(formData.status),
triggers: [], // 需要根据实际API结构转换
actions: [] // 需要根据实际API结构转换
} as IotRuleScene
}
/**
* 将 API 响应数据转换为表单格式
*/
export function 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()
})) || []
}
}
// TODO @puhui999貌似没用到
/**
* 创建默认的触发器配置
*/
export function createDefaultTriggerConfig(type?: number): TriggerConfig {
const baseConfig: TriggerConfig = {
key: generateUUID(),
type: type || 2, // 默认为物模型属性上报
productKey: '',
deviceNames: [],
conditions: []
}
// 定时触发的默认配置
if (type === 100) {
return {
...baseConfig,
cronExpression: '0 0 12 * * ?', // 默认每天中午12点
productKey: undefined,
deviceNames: undefined,
conditions: undefined
}
}
// 设备状态变更的默认配置
if (type === 1) {
return {
...baseConfig,
conditions: undefined // 设备状态变更不需要条件
}
}
// 其他设备触发类型的默认配置
return {
...baseConfig,
conditions: [
{
type: 'property',
identifier: 'set',
parameters: [
{
identifier: '',
operator: '=',
value: ''
}
]
}
]
}
}
// TODO @puhui999貌似没用到
/**
* 创建默认的执行器配置
*/
export function createDefaultActionConfig(type?: number): ActionConfig {
const baseConfig: ActionConfig = {
key: generateUUID(),
type: type || 1 // 默认为设备属性设置
}
// 告警相关的默认配置
if (type === 100 || type === 101) {
return {
...baseConfig,
alertConfigId: undefined
}
}
// 设备控制的默认配置
return {
...baseConfig,
deviceControl: {
productKey: '',
deviceNames: [],
type: 'property',
identifier: 'set',
params: {}
}
}
}
// TODO @puhui999全局已经有类似的
/**
* 深度克隆对象(用于避免引用问题)
*/
export function deepClone<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') {
return obj
}
if (obj instanceof Date) {
return new Date(obj.getTime()) as unknown as T
}
if (obj instanceof Array) {
return obj.map((item) => deepClone(item)) as unknown as T
}
if (typeof obj === 'object') {
const clonedObj = {} as T
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key])
}
}
return clonedObj
}
return obj
}
// TODO @puhui999貌似没用到
/**
* 清理空值和无效数据
*/
export function cleanFormData(data: IotRuleScene): IotRuleScene {
const cleaned = deepClone(data)
// 清理触发器数据
cleaned.triggers =
cleaned.triggers?.filter((trigger) => {
// 移除类型为空的触发器
if (!trigger.type) return false
// 定时触发器必须有CRON表达式
if (trigger.type === 100 && !trigger.cronExpression) return false
// 设备触发器必须有产品和设备
if (trigger.type !== 100 && (!trigger.productKey || !trigger.deviceNames?.length))
return false
return true
}) || []
// 清理执行器数据
cleaned.actions =
cleaned.actions?.filter((action) => {
// 移除类型为空的执行器
if (!action.type) return false
// 告警类型必须有告警配置ID
if ((action.type === 100 || action.type === 101) && !action.alertConfigId) return false
// 设备控制类型必须有完整的设备控制配置
if (
(action.type === 1 || action.type === 2) &&
(!action.deviceControl?.productKey ||
!action.deviceControl?.deviceNames?.length ||
!action.deviceControl?.identifier ||
!action.deviceControl?.params ||
Object.keys(action.deviceControl.params).length === 0)
) {
return false
}
return true
}) || []
return cleaned
}
/**
* 格式化CRON表达式显示
*/
export function formatCronExpression(cron: string): string {
if (!cron) return ''
// 简单的CRON表达式解析和格式化
const parts = cron.trim().split(' ')
if (parts.length < 5) return cron
const [second, minute, hour] = parts
// 构建可读的描述
let description = ''
if (second === '0' && minute === '0') {
if (hour === '*') {
description = '每小时'
} else if (hour.includes('/')) {
const interval = hour.split('/')[1]
description = `${interval}小时`
} else {
description = `每天${hour}`
}
} else if (second === '0') {
if (minute === '*') {
description = '每分钟'
} else if (minute.includes('/')) {
const interval = minute.split('/')[1]
description = `${interval}分钟`
} else {
description = `每小时第${minute}分钟`
}
} else {
if (second === '*') {
description = '每秒'
} else if (second.includes('/')) {
const interval = second.split('/')[1]
description = `${interval}`
}
}
return description || cron
}
// TODO @puhui999貌似没用到
/**
* 验证并修复数据结构
*/
export function validateAndFixData(data: IotRuleScene): IotRuleScene {
const fixed = deepClone(data)
// 确保必要字段存在
if (!fixed.triggers) fixed.triggers = []
if (!fixed.actions) fixed.actions = []
// 修复触发器数据
fixed.triggers = fixed.triggers.map((trigger) => {
const fixedTrigger = { ...trigger }
// 确保有key
if (!fixedTrigger.key) {
fixedTrigger.key = generateUUID()
}
// 定时触发器不需要产品和设备信息
if (fixedTrigger.type === 100) {
fixedTrigger.productKey = undefined
fixedTrigger.deviceNames = undefined
fixedTrigger.conditions = undefined
}
return fixedTrigger
})
// 修复执行器数据
fixed.actions = fixed.actions.map((action) => {
const fixedAction = { ...action }
// 确保有key
if (!fixedAction.key) {
fixedAction.key = generateUUID()
}
// 确保类型为数字
if (typeof fixedAction.type === 'string') {
fixedAction.type = Number(fixedAction.type)
}
// 修复设备控制参数字段名
if (fixedAction.deviceControl && 'data' in fixedAction.deviceControl) {
fixedAction.deviceControl.params = (fixedAction.deviceControl as any).data
delete (fixedAction.deviceControl as any).data
}
return fixedAction
})
return fixed
}
// TODO @puhui999貌似没用到
/**
* 比较两个场景联动规则是否相等忽略key字段
*/
export function isRuleSceneEqual(a: IotRuleScene, b: IotRuleScene): boolean {
const cleanA = transformFormToApi(a)
const cleanB = transformFormToApi(b)
return JSON.stringify(cleanA) === JSON.stringify(cleanB)
}
/**
* 获取场景联动规则的摘要信息
*/
export function getRuleSceneSummary(ruleScene: IotRuleScene): {
triggerSummary: string[]
actionSummary: string[]
} {
const triggerSummary =
ruleScene.triggers?.map((trigger) => {
switch (trigger.type) {
case 1:
return `设备状态变更 (${trigger.deviceNames?.length || 0}个设备)`
case 2:
return `属性上报 (${trigger.deviceNames?.length || 0}个设备)`
case 3:
return `事件上报 (${trigger.deviceNames?.length || 0}个设备)`
case 4:
return `服务调用 (${trigger.deviceNames?.length || 0}个设备)`
case 100:
return `定时触发 (${formatCronExpression(trigger.cronExpression || '')})`
default:
return '未知触发类型'
}
}) || []
const actionSummary =
ruleScene.actions?.map((action) => {
switch (action.type) {
case 1:
return `属性设置 (${action.deviceControl?.deviceNames?.length || 0}个设备)`
case 2:
return `服务调用 (${action.deviceControl?.deviceNames?.length || 0}个设备)`
case 100:
return '告警触发'
case 101:
return '告警恢复'
default:
return '未知执行类型'
}
}) || []
return { triggerSummary, actionSummary }
}

View File

@@ -1,15 +1,11 @@
/**
* IoT 场景联动表单验证工具函数
*/
import {
FormValidationRules,
IotRuleScene,
TriggerConfig,
ActionConfig
} from '@/api/iot/rule/scene/scene.types'
import { FormValidationRules, TriggerConfig, ActionConfig } from '@/api/iot/rule/scene/scene.types'
import {
IotRuleSceneTriggerTypeEnum,
IotRuleSceneActionTypeEnum
IotRuleSceneActionTypeEnum,
CommonStatusEnum
} from '@/api/iot/rule/scene/scene.types'
/** 基础表单验证规则 */
@@ -20,7 +16,12 @@ export const getBaseValidationRules = (): FormValidationRules => ({
],
status: [
{ required: true, message: '场景状态不能为空', trigger: 'change' },
{ type: 'enum', enum: [0, 1], message: '状态值必须为0或1', trigger: 'change' }
{
type: 'enum',
enum: [CommonStatusEnum.ENABLE, CommonStatusEnum.DISABLE],
message: '状态值必须为启用或禁用',
trigger: 'change'
}
],
description: [
{ type: 'string', max: 200, message: '场景描述不能超过200个字符', trigger: 'blur' }
@@ -179,86 +180,3 @@ export function validateActionConfig(action: ActionConfig): { valid: boolean; me
return { valid: false, message: '未知的执行类型' }
}
// TODO @puhui999貌似没用到
/** 验证完整的场景联动规则 */
export function validateRuleScene(ruleScene: IotRuleScene): { valid: boolean; message?: string } {
// 基础字段验证
if (!ruleScene.name || ruleScene.name.trim().length === 0) {
return { valid: false, message: '场景名称不能为空' }
}
if (ruleScene.status !== 0 && ruleScene.status !== 1) {
return { valid: false, message: '场景状态必须为0或1' }
}
if (!ruleScene.triggers || ruleScene.triggers.length === 0) {
return { valid: false, message: '至少需要一个触发器' }
}
if (!ruleScene.actions || ruleScene.actions.length === 0) {
return { valid: false, message: '至少需要一个执行器' }
}
// 验证每个触发器
for (let i = 0; i < ruleScene.triggers.length; i++) {
const triggerResult = validateTriggerConfig(ruleScene.triggers[i])
if (!triggerResult.valid) {
return { valid: false, message: `触发器${i + 1}: ${triggerResult.message}` }
}
}
// 验证每个执行器
for (let i = 0; i < ruleScene.actions.length; i++) {
const actionResult = validateActionConfig(ruleScene.actions[i])
if (!actionResult.valid) {
return { valid: false, message: `执行器${i + 1}: ${actionResult.message}` }
}
}
return { valid: true }
}
// TODO @puhui999下面 getOperatorOptions、getTriggerTypeOptions、getActionTypeOptions 三个貌似没用到?如果用到的话,要不放到 yudao-ui-admin-vue3/src/views/iot/utils/constants.ts 里
/**
* 获取操作符选项
*/
export function getOperatorOptions() {
// TODO @puhui999这个能不能从枚举计算出来减少后续添加枚举的维护
return [
{ value: '=', label: '等于' },
{ value: '!=', label: '不等于' },
{ value: '>', label: '大于' },
{ value: '>=', label: '大于等于' },
{ value: '<', label: '小于' },
{ value: '<=', label: '小于等于' },
{ value: 'in', label: '包含' },
{ value: 'not in', label: '不包含' },
{ value: 'between', label: '介于之间' },
{ value: 'not between', label: '不在之间' },
{ value: 'like', label: '字符串匹配' },
{ value: 'not null', label: '非空' }
]
}
/**
* 获取触发类型选项
*/
export function getTriggerTypeOptions() {
// TODO @puhui999这个能不能从枚举计算出来减少后续添加枚举的维护
return [
{ value: IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE, label: '设备上下线变更' },
{ value: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST, label: '物模型属性上报' },
{ value: IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST, label: '设备事件上报' },
{ value: IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE, label: '设备服务调用' },
{ value: IotRuleSceneTriggerTypeEnum.TIMER, label: '定时触发' }
]
}
/**
* 获取执行类型选项
*/
export function getActionTypeOptions() {
// TODO @puhui999这个能不能从枚举计算出来减少后续添加枚举的维护
return [
{ value: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET, label: '设备属性设置' },
{ value: IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE, label: '设备服务调用' },
{ value: IotRuleSceneActionTypeEnum.ALERT_TRIGGER, label: '告警触发' },
{ value: IotRuleSceneActionTypeEnum.ALERT_RECOVER, label: '告警恢复' }
]
}