perf:【IoT 物联网】场景联动执行器优化

This commit is contained in:
puhui999
2025-08-04 00:21:19 +08:00
parent 00e193d3e2
commit 38ad857c33
6 changed files with 881 additions and 158 deletions

View File

@@ -36,11 +36,12 @@ import { useVModel } from '@vueuse/core'
import BasicInfoSection from './sections/BasicInfoSection.vue'
import TriggerSection from './sections/TriggerSection.vue'
import ActionSection from './sections/ActionSection.vue'
import { IotRuleSceneDO, RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
import { IotRuleScene, IotRuleSceneDO, RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
import { RuleSceneApi } from '@/api/iot/rule/scene'
import {
IotRuleSceneTriggerTypeEnum,
IotRuleSceneActionTypeEnum,
IotDeviceMessageTypeEnum,
isDeviceTrigger
} from '@/views/iot/utils/constants'
import { ElMessage } from 'element-plus'
@@ -53,6 +54,57 @@ const CommonStatusEnum = {
DISABLE: 1 // 关闭
} as const
// 工具函数:根据触发器类型获取消息类型
const getMessageTypeByTriggerType = (triggerType: number): string => {
switch (triggerType) {
case IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST:
return IotDeviceMessageTypeEnum.PROPERTY
case IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST:
return IotDeviceMessageTypeEnum.EVENT
case IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE:
return IotDeviceMessageTypeEnum.SERVICE
default:
return IotDeviceMessageTypeEnum.PROPERTY
}
}
// 工具函数:根据执行器类型获取消息类型
const getMessageTypeByActionType = (actionType: number): string => {
switch (actionType) {
case IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET:
return IotDeviceMessageTypeEnum.PROPERTY
case IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE:
return IotDeviceMessageTypeEnum.SERVICE
default:
return IotDeviceMessageTypeEnum.PROPERTY
}
}
// 工具函数:根据执行器类型和参数获取标识符
const getIdentifierByActionType = (actionType: number, params?: Record<string, any>): string => {
if (!params) return ''
switch (actionType) {
case IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET:
// 属性设置:取第一个属性名作为标识符
return Object.keys(params)[0] || ''
case IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE:
// 服务调用:取 method 字段作为标识符
return params.method || ''
default:
return ''
}
}
// 工具函数:判断是否为设备执行器
const isDeviceAction = (type: number): boolean => {
const deviceActionTypes = [
IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
] as number[]
return deviceActionTypes.includes(type)
}
/** IoT 场景联动规则表单 - 主表单组件 */
defineOptions({ name: 'RuleSceneForm' })
@@ -95,31 +147,50 @@ const createDefaultFormData = (): RuleSceneFormData => {
}
/**
* 将表单数据转换为后端 DO 格式
* 由于数据结构已对齐,转换变得非常简单
* 将表单数据转换为后端 API 格式
* 转换为 IotRuleScene 格式,与后端 API 接口对齐
*/
const convertFormToVO = (formData: RuleSceneFormData): IotRuleSceneDO => {
const convertFormToVO = (formData: RuleSceneFormData): IotRuleScene => {
return {
id: formData.id,
name: formData.name,
description: formData.description,
status: Number(formData.status),
triggers: formData.triggers.map((trigger) => ({
key: generateUUID(), // 为每个触发器生成唯一标识
type: trigger.type,
productId: trigger.productId,
deviceId: trigger.deviceId,
identifier: trigger.identifier,
operator: trigger.operator,
value: trigger.value,
cronExpression: trigger.cronExpression,
conditionGroups: trigger.conditionGroups || []
productKey: trigger.productId ? `product_${trigger.productId}` : undefined, // 转换为产品标识
deviceNames: trigger.deviceId ? [`device_${trigger.deviceId}`] : undefined, // 转换为设备名称数组
conditions: trigger.identifier
? [
{
type: getMessageTypeByTriggerType(trigger.type),
identifier: trigger.identifier,
parameters: [
{
identifier: trigger.identifier,
operator: trigger.operator || '=',
value: trigger.value || ''
}
]
}
]
: undefined,
cronExpression: trigger.cronExpression
})),
actions:
formData.actions?.map((action) => ({
key: generateUUID(), // 为每个执行器生成唯一标识
type: action.type,
productId: action.productId,
deviceId: action.deviceId,
params: action.params,
deviceControl: isDeviceAction(action.type)
? {
productKey: action.productId ? `product_${action.productId}` : '',
deviceNames: action.deviceId ? [`device_${action.deviceId}`] : [],
type: getMessageTypeByActionType(action.type),
identifier: getIdentifierByActionType(action.type, action.params),
params: action.params || {}
}
: undefined,
alertConfigId: action.alertConfigId
})) || []
}

View File

@@ -1,72 +1,152 @@
<!-- 告警配置组件 -->
<template>
<div class="w-full">
<!-- TODO @puhui999触发告警时不用选择配置哈 -->
<el-form-item label="告警配置" required>
<el-select
v-model="localValue"
placeholder="请选择告警配置"
filterable
clearable
@change="handleChange"
class="w-full"
:loading="loading"
>
<el-option
v-for="config in alertConfigs"
:key="config.id"
:label="config.name"
:value="config.id"
<!-- 告警配置选择区域 -->
<div
class="border border-[var(--el-border-color-light)] rounded-6px p-16px bg-[var(--el-fill-color-blank)]"
>
<div class="flex items-center gap-8px mb-12px">
<Icon icon="ep:bell" class="text-[var(--el-color-warning)] text-16px" />
<span class="text-14px font-600 text-[var(--el-text-color-primary)]">告警配置选择</span>
<el-tag size="small" type="warning">必选</el-tag>
</div>
<el-form-item label="告警配置" required>
<el-select
v-model="localValue"
placeholder="请选择告警配置"
filterable
clearable
@change="handleChange"
class="w-full"
:loading="loading"
>
<div class="flex items-center justify-between w-full py-4px">
<div class="flex-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{
config.name
}}</div>
<div class="text-12px text-[var(--el-text-color-secondary)]">{{
config.description
}}</div>
<template #empty>
<div class="text-center py-20px">
<Icon
icon="ep:warning"
class="text-24px text-[var(--el-text-color-placeholder)] mb-8px"
/>
<p class="text-12px text-[var(--el-text-color-secondary)]">暂无可用的告警配置</p>
</div>
<el-tag :type="config.enabled ? 'success' : 'danger'" size="small">
{{ config.enabled ? '启用' : '禁用' }}
</el-tag>
</div>
</el-option>
</el-select>
</el-form-item>
</template>
<el-option
v-for="config in alertConfigs"
:key="config.id"
:label="config.name"
:value="config.id"
:disabled="!config.enabled"
>
<div class="flex items-center justify-between w-full py-6px">
<div class="flex items-center gap-12px flex-1">
<Icon
:icon="config.enabled ? 'ep:circle-check' : 'ep:circle-close'"
:class="
config.enabled
? 'text-[var(--el-color-success)]'
: 'text-[var(--el-color-danger)]'
"
class="text-16px flex-shrink-0"
/>
<div class="flex-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{
config.name
}}</div>
<div class="text-12px text-[var(--el-text-color-secondary)] line-clamp-1">{{
config.description
}}</div>
</div>
</div>
<div class="flex items-center gap-8px">
<el-tag :type="getNotifyTypeTag(config.notifyType)" size="small">
{{ getNotifyTypeName(config.notifyType) }}
</el-tag>
<el-tag :type="config.enabled ? 'success' : 'danger'" size="small">
{{ config.enabled ? '启用' : '禁用' }}
</el-tag>
</div>
</div>
</el-option>
</el-select>
</el-form-item>
</div>
<!-- 告警配置详情 -->
<div
v-if="selectedConfig"
class="mt-16px p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"
class="mt-16px border border-[var(--el-border-color-light)] rounded-6px p-16px bg-gradient-to-r from-orange-50 to-yellow-50"
>
<div class="flex items-center gap-8px mb-12px">
<Icon icon="ep:bell" class="text-[var(--el-color-warning)] text-16px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">{{
selectedConfig.name
}}</span>
<el-tag :type="selectedConfig.enabled ? 'success' : 'danger'" size="small">
{{ selectedConfig.enabled ? '启用' : '禁用' }}
</el-tag>
<div class="flex items-center gap-8px mb-16px">
<Icon icon="ep:info-filled" class="text-[var(--el-color-warning)] text-18px" />
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">配置详情</span>
</div>
<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">描述</span>
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{
selectedConfig.description
}}</span>
<div class="grid grid-cols-1 md:grid-cols-2 gap-16px">
<!-- 基本信息 -->
<div class="space-y-12px">
<div class="flex items-center gap-8px">
<Icon icon="ep:document" class="text-[var(--el-color-primary)] text-14px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">基本信息</span>
</div>
<div class="pl-22px space-y-8px">
<div class="flex items-start gap-8px">
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">名称</span>
<span class="text-12px text-[var(--el-text-color-primary)] flex-1 font-500">{{
selectedConfig.name
}}</span>
</div>
<div class="flex items-start gap-8px">
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">描述</span>
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{
selectedConfig.description
}}</span>
</div>
<div class="flex items-start gap-8px">
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">状态</span>
<el-tag :type="selectedConfig.enabled ? 'success' : 'danger'" size="small">
{{ selectedConfig.enabled ? '启用' : '禁用' }}
</el-tag>
</div>
</div>
</div>
<div class="flex items-start gap-8px">
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">通知方式</span>
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{
getNotifyTypeName(selectedConfig.notifyType)
}}</span>
</div>
<div v-if="selectedConfig.receivers" class="flex items-start gap-8px">
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">接收人</span>
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{
selectedConfig.receivers.join(', ')
}}</span>
<!-- 通知配置 -->
<div class="space-y-12px">
<div class="flex items-center gap-8px">
<Icon icon="ep:message" class="text-[var(--el-color-success)] text-14px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">通知配置</span>
</div>
<div class="pl-22px space-y-8px">
<div class="flex items-start gap-8px">
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">方式</span>
<el-tag :type="getNotifyTypeTag(selectedConfig.notifyType)" size="small">
{{ getNotifyTypeName(selectedConfig.notifyType) }}
</el-tag>
</div>
<div
v-if="selectedConfig.receivers && selectedConfig.receivers.length > 0"
class="flex items-start gap-8px"
>
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px"
>接收人</span
>
<div class="flex-1">
<div class="flex flex-wrap gap-4px">
<el-tag
v-for="receiver in selectedConfig.receivers.slice(0, 3)"
:key="receiver"
size="small"
type="info"
>
{{ receiver }}
</el-tag>
<el-tag v-if="selectedConfig.receivers.length > 3" size="small" type="info">
+{{ selectedConfig.receivers.length - 3 }}
</el-tag>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -112,6 +192,22 @@ const getNotifyTypeName = (type: number) => {
return typeMap[type] || '未知'
}
const getNotifyTypeTag = (type: number) => {
const tagMap = {
1: 'primary', // 邮件
2: 'success', // 短信
3: 'warning', // 微信
4: 'info' // 钉钉
}
return tagMap[type] || 'info'
}
// 事件处理
const handleChange = (value?: number) => {
// 可以在这里添加额外的处理逻辑
console.log('告警配置选择变化:', value)
}
// API 调用
const getAlertConfigs = async () => {
loading.value = true

View File

@@ -2,50 +2,190 @@
<!-- TODO @puhui999貌似没生效~~~ -->
<template>
<div class="flex flex-col gap-16px">
<!-- 产品和设备选择 -->
<ProductDeviceSelector
v-model:product-id="action.productId"
v-model:device-id="action.deviceId"
@change="handleDeviceChange"
/>
<!-- 产品和设备选择 - 与触发器保持一致的分离式选择器 -->
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="产品" required>
<ProductSelector v-model="action.productId" @change="handleProductChange" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备" required>
<DeviceSelector
v-model="action.deviceId"
:product-id="action.productId"
@change="handleDeviceChange"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 控制参数配置 -->
<div v-if="action.productId && action.deviceId" class="space-y-16px">
<el-form-item label="控制参数" required>
<el-input
v-model="paramsJson"
type="textarea"
:rows="4"
placeholder="请输入JSON格式的控制参数"
@input="handleParamsChange"
/>
<!-- 控制参数配置 - 只要选择了产品就显示支持全部设备和单独设备 -->
<div v-if="action.productId && isPropertySetAction" class="space-y-16px">
<!-- 参数配置 -->
<el-form-item label="参数" required>
<div class="w-full space-y-8px">
<!-- JSON 输入框 -->
<div class="relative">
<el-input
v-model="paramsJson"
type="textarea"
:rows="6"
placeholder="请输入JSON格式的控制参数"
@input="handleParamsChange"
:class="{ 'is-error': jsonError }"
/>
<!-- 查看详细示例按钮 -->
<div class="absolute top-8px right-8px">
<el-button
ref="exampleTriggerRef"
type="info"
:icon="InfoFilled"
circle
size="small"
@click="toggleExampleDetail"
title="查看详细示例"
/>
</div>
</div>
<!-- 验证状态和错误提示 -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-8px">
<Icon
:icon="jsonError ? 'ep:warning' : 'ep:circle-check'"
:class="
jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'
"
class="text-14px"
/>
<span
:class="
jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'
"
class="text-12px"
>
{{ jsonError || 'JSON格式正确' }}
</span>
</div>
<!-- 快速填充按钮 -->
<div v-if="thingModelProperties.length > 0" class="flex items-center gap-8px">
<span class="text-12px text-[var(--el-text-color-secondary)]">快速填充</span>
<el-button size="small" type="primary" plain @click="fillExampleJson">
示例数据
</el-button>
<el-button size="small" type="default" plain @click="clearParams"> 清空 </el-button>
</div>
</div>
</div>
</el-form-item>
<!-- 参数示例 -->
<div class="mt-12px">
<el-alert title="参数格式示例" type="info" :closable="false" show-icon>
<template #default>
<div class="space-y-8px">
<p class="m-0 text-14px text-[var(--el-text-color-primary)]">属性设置示例</p>
<pre
class="m-0 p-8px bg-[var(--el-fill-color-light)] rounded-4px text-12px text-[var(--el-text-color-regular)] overflow-x-auto"
><code>{ "temperature": 25, "power": true }</code></pre>
<p class="m-0 text-14px text-[var(--el-text-color-primary)]">服务调用示例</p>
<pre
class="m-0 p-8px bg-[var(--el-fill-color-light)] rounded-4px text-12px text-[var(--el-text-color-regular)] overflow-x-auto"
><code>{ "method": "restart", "params": { "delay": 5 } }</code></pre>
<!-- 详细示例弹出层 -->
<Teleport to="body">
<div
v-if="showExampleDetail"
ref="exampleDetailRef"
class="example-detail-popover"
:style="examplePopoverStyle"
>
<div
class="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-400px max-w-500px"
>
<div class="flex items-center gap-8px mb-16px">
<Icon icon="ep:document" class="text-[var(--el-color-info)] text-18px" />
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">
参数配置详细示例
</span>
</div>
</template>
</el-alert>
</div>
<div class="space-y-16px">
<!-- 物模型属性示例 -->
<div v-if="thingModelProperties.length > 0">
<div class="flex items-center gap-8px mb-8px">
<Icon icon="ep:edit" class="text-[var(--el-color-primary)] text-14px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">
当前物模型属性
</span>
</div>
<div class="ml-22px space-y-8px">
<div
v-for="property in thingModelProperties.slice(0, 4)"
:key="property.identifier"
class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
>
<div class="flex-1">
<div class="text-12px font-500 text-[var(--el-text-color-primary)]">
{{ property.name }}
</div>
<div class="text-11px text-[var(--el-text-color-secondary)]">
{{ property.identifier }}
</div>
</div>
<div class="flex items-center gap-8px">
<el-tag :type="getPropertyTypeTag(property.dataType)" size="small">
{{ getPropertyTypeName(property.dataType) }}
</el-tag>
<span class="text-11px text-[var(--el-text-color-secondary)]">
{{ getExampleValue(property) }}
</span>
</div>
</div>
</div>
<div class="mt-12px ml-22px">
<div class="text-12px text-[var(--el-text-color-secondary)] mb-6px">
完整JSON格式
</div>
<pre
class="p-12px bg-[var(--el-fill-color-light)] rounded-4px text-11px text-[var(--el-text-color-primary)] overflow-x-auto border-l-3px border-[var(--el-color-primary)]"
><code>{{ generateExampleJson() }}</code></pre>
</div>
</div>
<!-- 通用示例 -->
<div>
<div class="flex items-center gap-8px mb-8px">
<Icon icon="ep:service" class="text-[var(--el-color-success)] text-14px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">
通用格式示例
</span>
</div>
<div class="ml-22px space-y-8px">
<div class="text-12px text-[var(--el-text-color-secondary)]">
服务调用格式
</div>
<pre
class="p-12px bg-[var(--el-fill-color-light)] rounded-4px text-11px text-[var(--el-text-color-primary)] overflow-x-auto border-l-3px border-[var(--el-color-success)]"
><code>{
"method": "restart",
"params": {
"delay": 5,
"force": false
}
}</code></pre>
</div>
</div>
</div>
<!-- 关闭按钮 -->
<div class="flex justify-end mt-16px">
<el-button size="small" @click="hideExampleDetail">关闭</el-button>
</div>
</div>
</div>
</Teleport>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import ProductDeviceSelector from '../selectors/ProductDeviceSelector.vue'
import { InfoFilled } from '@element-plus/icons-vue'
import ProductSelector from '../selectors/ProductSelector.vue'
import DeviceSelector from '../selectors/DeviceSelector.vue'
import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
import { IotRuleSceneActionTypeEnum } from '@/views/iot/utils/constants'
/** 设备控制配置组件 */
defineOptions({ name: 'DeviceControlConfig' })
@@ -62,38 +202,362 @@ const action = useVModel(props, 'modelValue', emit)
// 状态
const paramsJson = ref('')
const jsonError = ref('')
const thingModelProperties = ref<any[]>([])
const loadingThingModel = ref(false)
const propertyValues = ref<Record<string, any>>({})
// 示例弹出层相关状态
const showExampleDetail = ref(false)
const exampleTriggerRef = ref()
const exampleDetailRef = ref()
const examplePopoverStyle = ref({})
// 计算属性
const isPropertySetAction = computed(() => {
return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
})
// 事件处理
const handleDeviceChange = ({ productId, deviceId }: { productId?: number; deviceId?: number }) => {
action.value.productId = productId
action.value.deviceId = deviceId
const handleProductChange = (productId?: number) => {
// 当产品变化时,清空设备选择和参数配置
if (action.value.productId !== productId) {
action.value.deviceId = undefined
action.value.params = {}
paramsJson.value = ''
jsonError.value = ''
propertyValues.value = {}
}
// 加载新产品的物模型属性
if (productId && isPropertySetAction.value) {
loadThingModelProperties(productId)
}
}
const handleDeviceChange = (deviceId?: number) => {
// 当设备变化时,清空参数配置
if (action.value.deviceId !== deviceId) {
action.value.params = {}
paramsJson.value = ''
jsonError.value = ''
}
}
// 快速填充示例数据
const fillExampleJson = () => {
const exampleData = generateExampleJson()
paramsJson.value = exampleData
handleParamsChange()
}
// 清空参数
const clearParams = () => {
paramsJson.value = ''
action.value.params = {}
propertyValues.value = {}
jsonError.value = ''
}
// 更新属性值(保留但不在模板中使用)
const updatePropertyValue = (identifier: string, value: any) => {
propertyValues.value[identifier] = value
// 同步更新到 action.params
action.value.params = { ...propertyValues.value }
// 同步更新 JSON 显示
paramsJson.value = JSON.stringify(action.value.params, null, 2)
jsonError.value = ''
}
// 加载物模型属性
const loadThingModelProperties = async (productId: number) => {
if (!productId) {
thingModelProperties.value = []
return
}
try {
loadingThingModel.value = true
// TODO: 这里需要调用实际的物模型API
// const response = await ProductApi.getThingModel(productId)
// 暂时使用模拟数据
thingModelProperties.value = [
{
identifier: 'BatteryLevel',
name: '电池电量',
dataType: 'int',
description: '设备电池电量百分比'
},
{
identifier: 'WaterLeachState',
name: '漏水状态',
dataType: 'bool',
description: '设备漏水检测状态'
},
{
identifier: 'Temperature',
name: '温度',
dataType: 'float',
description: '环境温度值'
},
{
identifier: 'Humidity',
name: '湿度',
dataType: 'float',
description: '环境湿度值'
}
]
// 初始化属性值
thingModelProperties.value.forEach((property) => {
if (!(property.identifier in propertyValues.value)) {
propertyValues.value[property.identifier] = ''
}
})
} catch (error) {
console.error('加载物模型失败:', error)
thingModelProperties.value = []
} finally {
loadingThingModel.value = false
}
}
const handleParamsChange = () => {
try {
jsonError.value = '' // 清除之前的错误
if (paramsJson.value.trim()) {
action.value.params = JSON.parse(paramsJson.value)
const parsed = JSON.parse(paramsJson.value)
action.value.params = parsed
// 同步更新到属性值
propertyValues.value = { ...parsed }
// 额外的参数验证
if (typeof parsed !== 'object' || parsed === null) {
jsonError.value = '参数必须是一个有效的JSON对象'
return
}
} else {
action.value.params = {}
propertyValues.value = {}
}
} catch (error) {
jsonError.value = `JSON格式错误: ${error instanceof Error ? error.message : '未知错误'}`
console.error('JSON格式错误:', error)
}
}
// 工具函数 - 参考 PropertySelector 的设计
const getPropertyTypeName = (dataType: string) => {
const typeMap = {
int: '整数',
float: '浮点数',
double: '双精度',
text: '字符串',
bool: '布尔值',
enum: '枚举',
date: '日期',
struct: '结构体',
array: '数组'
}
return typeMap[dataType] || dataType
}
const getPropertyTypeTag = (dataType: string) => {
const tagMap = {
int: 'primary',
float: 'success',
double: 'success',
text: 'info',
bool: 'warning',
enum: 'danger',
date: 'primary',
struct: 'info',
array: 'warning'
}
return tagMap[dataType] || 'info'
}
const getExampleValue = (property: any) => {
switch (property.dataType) {
case 'int':
return property.identifier === 'BatteryLevel' ? '85' : '25'
case 'float':
case 'double':
return property.identifier === 'Temperature' ? '25.5' : '60.0'
case 'bool':
return 'false'
case 'text':
return '"auto"'
case 'enum':
return '"option1"'
default:
return '""'
}
}
const generateExampleJson = () => {
if (thingModelProperties.value.length === 0) {
return JSON.stringify(
{
BatteryLevel: '',
WaterLeachState: ''
},
null,
2
)
}
const example = {}
thingModelProperties.value.forEach((property) => {
switch (property.dataType) {
case 'int':
example[property.identifier] = property.identifier === 'BatteryLevel' ? 85 : 25
break
case 'float':
case 'double':
example[property.identifier] = property.identifier === 'Temperature' ? 25.5 : 60.0
break
case 'bool':
example[property.identifier] = false
break
case 'text':
example[property.identifier] = 'auto'
break
default:
example[property.identifier] = ''
}
})
return JSON.stringify(example, null, 2)
}
// 示例弹出层控制方法 - 参考 PropertySelector 的设计
const toggleExampleDetail = () => {
if (showExampleDetail.value) {
hideExampleDetail()
} else {
showExampleDetailPopover()
}
}
const showExampleDetailPopover = () => {
if (!exampleTriggerRef.value) return
showExampleDetail.value = true
nextTick(() => {
updateExamplePopoverPosition()
})
}
const hideExampleDetail = () => {
showExampleDetail.value = false
}
const updateExamplePopoverPosition = () => {
if (!exampleTriggerRef.value || !exampleDetailRef.value) return
const triggerEl = exampleTriggerRef.value.$el
const triggerRect = triggerEl.getBoundingClientRect()
// 计算弹出层位置
const left = triggerRect.left + triggerRect.width + 8
const top = triggerRect.top
// 检查是否超出视窗右边界
const popoverWidth = 500 // 最大宽度
const viewportWidth = window.innerWidth
let finalLeft = left
if (left + popoverWidth > viewportWidth - 16) {
// 如果超出右边界,显示在左侧
finalLeft = triggerRect.left - popoverWidth - 8
}
// 检查是否超出视窗下边界
let finalTop = top
const popoverHeight = exampleDetailRef.value.offsetHeight || 300
const viewportHeight = window.innerHeight
if (top + popoverHeight > viewportHeight - 16) {
finalTop = Math.max(16, viewportHeight - popoverHeight - 16)
}
examplePopoverStyle.value = {
position: 'fixed',
left: `${finalLeft}px`,
top: `${finalTop}px`,
zIndex: 9999
}
}
// 点击外部关闭弹出层
const handleClickOutside = (event: MouseEvent) => {
if (
showExampleDetail.value &&
exampleDetailRef.value &&
exampleTriggerRef.value &&
!exampleDetailRef.value.contains(event.target as Node) &&
!exampleTriggerRef.value.$el.contains(event.target as Node)
) {
hideExampleDetail()
}
}
// 监听窗口大小变化,重新计算弹出层位置
const handleResize = () => {
if (showExampleDetail.value) {
updateExamplePopoverPosition()
}
}
// 初始化
onMounted(() => {
if (action.value.params) {
paramsJson.value = JSON.stringify(action.value.params, null, 2)
if (action.value.params && Object.keys(action.value.params).length > 0) {
try {
paramsJson.value = JSON.stringify(action.value.params, null, 2)
propertyValues.value = { ...action.value.params }
jsonError.value = '' // 清除错误状态
} catch (error) {
console.error('初始化参数格式化失败:', error)
jsonError.value = '初始参数格式错误'
}
}
// 如果已经选择了产品且是属性设置类型,加载物模型
if (action.value.productId && isPropertySetAction.value) {
loadThingModelProperties(action.value.productId)
}
// 添加事件监听器
document.addEventListener('click', handleClickOutside)
window.addEventListener('resize', handleResize)
})
// 组件卸载时清理事件监听器
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
window.removeEventListener('resize', handleResize)
})
// 监听参数变化
watch(
() => action.value.params,
(newParams) => {
if (newParams && typeof newParams === 'object') {
paramsJson.value = JSON.stringify(newParams, null, 2)
if (newParams && typeof newParams === 'object' && Object.keys(newParams).length > 0) {
try {
const newJsonString = JSON.stringify(newParams, null, 2)
// 只有当JSON字符串真正改变时才更新避免循环更新
if (newJsonString !== paramsJson.value) {
paramsJson.value = newJsonString
jsonError.value = ''
}
} catch (error) {
console.error('参数格式化失败:', error)
jsonError.value = '参数格式化失败'
}
}
},
{ deep: true }
@@ -101,6 +565,49 @@ watch(
</script>
<style scoped>
/* 参考 PropertySelector 的弹出层样式 */
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.9) translateY(-4px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.example-detail-popover {
animation: fadeInScale 0.2s ease-out;
transform-origin: top left;
}
/* 弹出层箭头效果 */
.example-detail-popover::before {
position: absolute;
top: 20px;
left: -8px;
width: 0;
height: 0;
border-top: 8px solid transparent;
border-right: 8px solid var(--el-border-color);
border-bottom: 8px solid transparent;
content: '';
}
.example-detail-popover::after {
position: absolute;
top: 20px;
left: -7px;
width: 0;
height: 0;
border-top: 8px solid transparent;
border-right: 8px solid white;
border-bottom: 8px solid transparent;
content: '';
}
:deep(.example-content code) {
font-family: 'Courier New', monospace;
color: var(--el-color-primary);

View File

@@ -108,7 +108,12 @@ import ActionTypeSelector from '../selectors/ActionTypeSelector.vue'
import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
import AlertConfig from '../configs/AlertConfig.vue'
import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
import { IotRuleSceneActionTypeEnum as ActionTypeEnum } from '@/views/iot/utils/constants'
import {
IotRuleSceneActionTypeEnum as ActionTypeEnum,
isDeviceAction,
isAlertAction,
getActionTypeLabel
} from '@/views/iot/utils/constants'
/** 执行器配置组件 */
defineOptions({ name: 'ActionSection' })
@@ -142,37 +147,18 @@ const createDefaultActionData = (): ActionFormData => {
// 配置常量
const maxActions = 5
// 执行器类型映射
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 as any
)
}
const isAlertAction = (type: number) => {
return [ActionTypeEnum.ALERT_TRIGGER, ActionTypeEnum.ALERT_RECOVER].includes(type as any)
}
const getActionTypeName = (type: number) => {
return actionTypeNames[type] || '未知类型'
return getActionTypeLabel(type)
}
const getActionTypeTag = (type: number) => {
const actionTypeTags = {
[ActionTypeEnum.DEVICE_PROPERTY_SET]: 'primary',
[ActionTypeEnum.DEVICE_SERVICE_INVOKE]: 'success',
[ActionTypeEnum.ALERT_TRIGGER]: 'danger',
[ActionTypeEnum.ALERT_RECOVER]: 'warning'
}
return actionTypeTags[type] || 'info'
}
@@ -204,16 +190,23 @@ const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
}
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
}
// 触发重新校验
nextTick(() => {
// 这里可以添加校验逻辑
})
}
</script>

View File

@@ -39,10 +39,20 @@
<!-- 设备选择模式 -->
<el-col :span="12">
<el-form-item label="设备选择模式" required>
<el-radio-group v-model="deviceSelectionMode" @change="handleDeviceSelectionModeChange">
<el-radio-group
v-model="deviceSelectionMode"
@change="handleDeviceSelectionModeChange"
:disabled="!localProductId"
>
<el-radio value="all">全部设备</el-radio>
<el-radio value="specific">选择设备</el-radio>
</el-radio-group>
<div
v-if="!localProductId"
class="text-12px text-[var(--el-text-color-placeholder)] mt-4px"
>
请先选择产品
</div>
</el-form-item>
</el-col>
</el-row>
@@ -50,12 +60,10 @@
<!-- 具体设备选择 -->
<el-row v-if="deviceSelectionMode === 'specific'" :gutter="16">
<el-col :span="24">
<!-- TODO @puhui999貌似产品选择不上 -->
<el-form-item label="选择设备" required>
<!-- TODO @puhui999请先选择产品是不是改成请选择设备然后上面localProductId 为空未选择的时候禁用 deviceSelectionMode -->
<el-select
v-model="localDeviceId"
placeholder="请先选择产品"
:placeholder="localProductId ? '请选择设备' : '请先选择产品'"
filterable
clearable
@change="handleDeviceChange"
@@ -152,8 +160,8 @@ const localProductId = useVModel(props, 'productId', emit)
const localDeviceId = useVModel(props, 'deviceId', emit)
// 设备选择模式
// TODO @puhui999默认选中 all
const deviceSelectionMode = ref<'specific' | 'all'>('all')
// 默认选择具体设备,这样用户可以看到设备选择器
const deviceSelectionMode = ref<'specific' | 'all'>('specific')
// 数据状态
const productLoading = ref(false)

View File

@@ -260,6 +260,66 @@ export const IotRuleSceneActionTypeEnum = {
ALERT_RECOVER: 101 // 告警恢复
} as const
/** 执行器类型选项配置 */
export const getActionTypeOptions = () => [
{
value: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
label: '设备属性设置',
description: '设置目标设备的属性值',
icon: 'ep:edit',
tag: 'primary',
category: '设备控制'
},
{
value: IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE,
label: '设备服务调用',
description: '调用目标设备的服务',
icon: 'ep:service',
tag: 'success',
category: '设备控制'
},
{
value: IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
label: '触发告警',
description: '触发系统告警通知',
icon: 'ep:warning',
tag: 'danger',
category: '告警通知'
},
{
value: IotRuleSceneActionTypeEnum.ALERT_RECOVER,
label: '恢复告警',
description: '恢复已触发的告警',
icon: 'ep:circle-check',
tag: 'warning',
category: '告警通知'
}
]
/** 判断是否为设备执行器类型 */
export const isDeviceAction = (type: number): boolean => {
const deviceActionTypes = [
IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
] as number[]
return deviceActionTypes.includes(type)
}
/** 判断是否为告警执行器类型 */
export const isAlertAction = (type: number): boolean => {
const alertActionTypes = [
IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
IotRuleSceneActionTypeEnum.ALERT_RECOVER
] as number[]
return alertActionTypes.includes(type)
}
/** 获取执行器类型标签 */
export const getActionTypeLabel = (type: number): string => {
const option = getActionTypeOptions().find((opt) => opt.value === type)
return option?.label || '未知类型'
}
/** IoT 设备消息类型枚举 */
export const IotDeviceMessageTypeEnum = {
PROPERTY: 'property', // 属性
@@ -309,15 +369,3 @@ export const getTriggerTypeLabel = (type: number): string => {
const option = options.find((item) => item.value === type)
return option?.label || '未知类型'
}
/** 获取执行器类型标签 */
export const getActionTypeLabel = (type: number): string => {
const actionTypeOptions = [
{ label: '设备属性设置', value: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET },
{ label: '设备服务调用', value: IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE },
{ label: '告警触发', value: IotRuleSceneActionTypeEnum.ALERT_TRIGGER },
{ label: '告警恢复', value: IotRuleSceneActionTypeEnum.ALERT_RECOVER }
]
const option = actionTypeOptions.find((item) => item.value === type)
return option?.label || '未知类型'
}