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

pull/805/head
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 BasicInfoSection from './sections/BasicInfoSection.vue'
import TriggerSection from './sections/TriggerSection.vue' import TriggerSection from './sections/TriggerSection.vue'
import ActionSection from './sections/ActionSection.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 { RuleSceneApi } from '@/api/iot/rule/scene'
import { import {
IotRuleSceneTriggerTypeEnum, IotRuleSceneTriggerTypeEnum,
IotRuleSceneActionTypeEnum, IotRuleSceneActionTypeEnum,
IotDeviceMessageTypeEnum,
isDeviceTrigger isDeviceTrigger
} from '@/views/iot/utils/constants' } from '@/views/iot/utils/constants'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
@ -53,6 +54,57 @@ const CommonStatusEnum = {
DISABLE: 1 // DISABLE: 1 //
} as const } 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 场景联动规则表单 - 主表单组件 */ /** IoT 场景联动规则表单 - 主表单组件 */
defineOptions({ name: 'RuleSceneForm' }) 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 { return {
id: formData.id, id: formData.id,
name: formData.name, name: formData.name,
description: formData.description, description: formData.description,
status: Number(formData.status), status: Number(formData.status),
triggers: formData.triggers.map((trigger) => ({ triggers: formData.triggers.map((trigger) => ({
key: generateUUID(), //
type: trigger.type, type: trigger.type,
productId: trigger.productId, productKey: trigger.productId ? `product_${trigger.productId}` : undefined, //
deviceId: trigger.deviceId, deviceNames: trigger.deviceId ? [`device_${trigger.deviceId}`] : undefined, //
identifier: trigger.identifier, conditions: trigger.identifier
operator: trigger.operator, ? [
value: trigger.value, {
cronExpression: trigger.cronExpression, type: getMessageTypeByTriggerType(trigger.type),
conditionGroups: trigger.conditionGroups || [] identifier: trigger.identifier,
parameters: [
{
identifier: trigger.identifier,
operator: trigger.operator || '=',
value: trigger.value || ''
}
]
}
]
: undefined,
cronExpression: trigger.cronExpression
})), })),
actions: actions:
formData.actions?.map((action) => ({ formData.actions?.map((action) => ({
key: generateUUID(), //
type: action.type, type: action.type,
productId: action.productId, deviceControl: isDeviceAction(action.type)
deviceId: action.deviceId, ? {
params: action.params, 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 alertConfigId: action.alertConfigId
})) || [] })) || []
} }

View File

@ -1,72 +1,152 @@
<!-- 告警配置组件 --> <!-- 告警配置组件 -->
<template> <template>
<div class="w-full"> <div class="w-full">
<!-- TODO @puhui999触发告警时不用选择配置哈 --> <!-- 告警配置选择区域 -->
<el-form-item label="告警配置" required> <div
<el-select class="border border-[var(--el-border-color-light)] rounded-6px p-16px bg-[var(--el-fill-color-blank)]"
v-model="localValue" >
placeholder="请选择告警配置" <div class="flex items-center gap-8px mb-12px">
filterable <Icon icon="ep:bell" class="text-[var(--el-color-warning)] text-16px" />
clearable <span class="text-14px font-600 text-[var(--el-text-color-primary)]">告警配置选择</span>
@change="handleChange" <el-tag size="small" type="warning">必选</el-tag>
class="w-full" </div>
:loading="loading"
> <el-form-item label="告警配置" required>
<el-option <el-select
v-for="config in alertConfigs" v-model="localValue"
:key="config.id" placeholder="请选择告警配置"
:label="config.name" filterable
:value="config.id" clearable
@change="handleChange"
class="w-full"
:loading="loading"
> >
<div class="flex items-center justify-between w-full py-4px"> <template #empty>
<div class="flex-1"> <div class="text-center py-20px">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ <Icon
config.name icon="ep:warning"
}}</div> class="text-24px text-[var(--el-text-color-placeholder)] mb-8px"
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ />
config.description <p class="text-12px text-[var(--el-text-color-secondary)]">暂无可用的告警配置</p>
}}</div>
</div> </div>
<el-tag :type="config.enabled ? 'success' : 'danger'" size="small"> </template>
{{ config.enabled ? '启用' : '禁用' }} <el-option
</el-tag> v-for="config in alertConfigs"
</div> :key="config.id"
</el-option> :label="config.name"
</el-select> :value="config.id"
</el-form-item> :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 <div
v-if="selectedConfig" 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"> <div class="flex items-center gap-8px mb-16px">
<Icon icon="ep:bell" class="text-[var(--el-color-warning)] text-16px" /> <Icon icon="ep:info-filled" class="text-[var(--el-color-warning)] text-18px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ <span class="text-16px font-600 text-[var(--el-text-color-primary)]">配置详情</span>
selectedConfig.name
}}</span>
<el-tag :type="selectedConfig.enabled ? 'success' : 'danger'" size="small">
{{ selectedConfig.enabled ? '启用' : '禁用' }}
</el-tag>
</div> </div>
<div class="space-y-8px">
<div class="flex items-start gap-8px"> <div class="grid grid-cols-1 md:grid-cols-2 gap-16px">
<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">{{ <div class="space-y-12px">
selectedConfig.description <div class="flex items-center gap-8px">
}}</span> <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>
<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">{{ <div class="space-y-12px">
getNotifyTypeName(selectedConfig.notifyType) <div class="flex items-center gap-8px">
}}</span> <Icon icon="ep:message" class="text-[var(--el-color-success)] text-14px" />
</div> <span class="text-14px font-500 text-[var(--el-text-color-primary)]">通知配置</span>
<div v-if="selectedConfig.receivers" class="flex items-start gap-8px"> </div>
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">接收人</span> <div class="pl-22px space-y-8px">
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ <div class="flex items-start gap-8px">
selectedConfig.receivers.join(', ') <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">方式</span>
}}</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> </div>
</div> </div>
@ -112,6 +192,22 @@ const getNotifyTypeName = (type: number) => {
return typeMap[type] || '未知' 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 // API
const getAlertConfigs = async () => { const getAlertConfigs = async () => {
loading.value = true loading.value = true

View File

@ -2,50 +2,190 @@
<!-- TODO @puhui999貌似没生效~~~ --> <!-- TODO @puhui999貌似没生效~~~ -->
<template> <template>
<div class="flex flex-col gap-16px"> <div class="flex flex-col gap-16px">
<!-- 产品和设备选择 --> <!-- 产品和设备选择 - 与触发器保持一致的分离式选择器 -->
<ProductDeviceSelector <el-row :gutter="16">
v-model:product-id="action.productId" <el-col :span="12">
v-model:device-id="action.deviceId" <el-form-item label="产品" required>
@change="handleDeviceChange" <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"> <div v-if="action.productId && isPropertySetAction" class="space-y-16px">
<el-form-item label="控制参数" required> <!-- 参数配置 -->
<el-input <el-form-item label="参数" required>
v-model="paramsJson" <div class="w-full space-y-8px">
type="textarea" <!-- JSON 输入框 -->
:rows="4" <div class="relative">
placeholder="请输入JSON格式的控制参数" <el-input
@input="handleParamsChange" 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> </el-form-item>
<!-- 参数示例 --> <!-- 详细示例弹出层 -->
<div class="mt-12px"> <Teleport to="body">
<el-alert title="参数格式示例" type="info" :closable="false" show-icon> <div
<template #default> v-if="showExampleDetail"
<div class="space-y-8px"> ref="exampleDetailRef"
<p class="m-0 text-14px text-[var(--el-text-color-primary)]">属性设置示例</p> class="example-detail-popover"
<pre :style="examplePopoverStyle"
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> <div
<p class="m-0 text-14px text-[var(--el-text-color-primary)]">服务调用示例</p> class="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-400px max-w-500px"
<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" <div class="flex items-center gap-8px mb-16px">
><code>{ "method": "restart", "params": { "delay": 5 } }</code></pre> <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> </div>
</template>
</el-alert> <div class="space-y-16px">
</div> <!-- 物模型属性示例 -->
<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>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useVModel } from '@vueuse/core' 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 { ActionFormData } from '@/api/iot/rule/scene/scene.types'
import { IotRuleSceneActionTypeEnum } from '@/views/iot/utils/constants'
/** 设备控制配置组件 */ /** 设备控制配置组件 */
defineOptions({ name: 'DeviceControlConfig' }) defineOptions({ name: 'DeviceControlConfig' })
@ -62,38 +202,362 @@ const action = useVModel(props, 'modelValue', emit)
// //
const paramsJson = ref('') 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 }) => { const handleProductChange = (productId?: number) => {
action.value.productId = productId //
action.value.deviceId = deviceId 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 = () => { const handleParamsChange = () => {
try { try {
jsonError.value = '' //
if (paramsJson.value.trim()) { 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 { } else {
action.value.params = {} action.value.params = {}
propertyValues.value = {}
} }
} catch (error) { } catch (error) {
jsonError.value = `JSON格式错误: ${error instanceof Error ? error.message : '未知错误'}`
console.error('JSON格式错误:', error) 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(() => { onMounted(() => {
if (action.value.params) { if (action.value.params && Object.keys(action.value.params).length > 0) {
paramsJson.value = JSON.stringify(action.value.params, null, 2) 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( watch(
() => action.value.params, () => action.value.params,
(newParams) => { (newParams) => {
if (newParams && typeof newParams === 'object') { if (newParams && typeof newParams === 'object' && Object.keys(newParams).length > 0) {
paramsJson.value = JSON.stringify(newParams, null, 2) 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 } { deep: true }
@ -101,6 +565,49 @@ watch(
</script> </script>
<style scoped> <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) { :deep(.example-content code) {
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
color: var(--el-color-primary); color: var(--el-color-primary);

View File

@ -108,7 +108,12 @@ import ActionTypeSelector from '../selectors/ActionTypeSelector.vue'
import DeviceControlConfig from '../configs/DeviceControlConfig.vue' import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
import AlertConfig from '../configs/AlertConfig.vue' import AlertConfig from '../configs/AlertConfig.vue'
import { ActionFormData } from '@/api/iot/rule/scene/scene.types' 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' }) defineOptions({ name: 'ActionSection' })
@ -142,37 +147,18 @@ const createDefaultActionData = (): ActionFormData => {
// //
const maxActions = 5 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) => { const getActionTypeName = (type: number) => {
return actionTypeNames[type] || '未知类型' return getActionTypeLabel(type)
} }
const getActionTypeTag = (type: number) => { 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' return actionTypeTags[type] || 'info'
} }
@ -204,16 +190,23 @@ const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
} }
const onActionTypeChange = (action: ActionFormData, type: number) => { const onActionTypeChange = (action: ActionFormData, type: number) => {
// //
if (isDeviceAction(type)) { if (isDeviceAction(type)) {
//
action.alertConfigId = undefined action.alertConfigId = undefined
if (!action.params) { if (!action.params) {
action.params = {} action.params = {}
} }
} else if (isAlertAction(type)) { } else if (isAlertAction(type)) {
//
action.productId = undefined action.productId = undefined
action.deviceId = undefined action.deviceId = undefined
action.params = undefined action.params = undefined
} }
//
nextTick(() => {
//
})
} }
</script> </script>

View File

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

View File

@ -260,6 +260,66 @@ export const IotRuleSceneActionTypeEnum = {
ALERT_RECOVER: 101 // 告警恢复 ALERT_RECOVER: 101 // 告警恢复
} as const } 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 设备消息类型枚举 */ /** IoT 设备消息类型枚举 */
export const IotDeviceMessageTypeEnum = { export const IotDeviceMessageTypeEnum = {
PROPERTY: 'property', // 属性 PROPERTY: 'property', // 属性
@ -309,15 +369,3 @@ export const getTriggerTypeLabel = (type: number): string => {
const option = options.find((item) => item.value === type) const option = options.find((item) => item.value === type)
return option?.label || '未知类型' 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 || '未知类型'
}