perf:【IoT 物联网】场景联动触发器优化

pull/802/head
puhui999 2025-08-01 16:14:11 +08:00
parent c740da02b9
commit a554bc5309
11 changed files with 551 additions and 315 deletions

View File

@ -2,15 +2,7 @@
* IoT * IoT
*/ */
// TODO @puhui999枚举挪到 views/iot/utils/constants.ts 里 // 枚举定义已迁移到 constants.ts这里不再重复导出
// 枚举定义
const IotRuleSceneTriggerTypeEnum = {
DEVICE_STATE_UPDATE: 1, // 设备上下线变更
DEVICE_PROPERTY_POST: 2, // 物模型属性上报
DEVICE_EVENT_POST: 3, // 设备事件上报
DEVICE_SERVICE_INVOKE: 4, // 设备服务调用
TIMER: 100 // 定时触发
} as const
const IotRuleSceneActionTypeEnum = { const IotRuleSceneActionTypeEnum = {
DEVICE_PROPERTY_SET: 1, // 设备属性设置, DEVICE_PROPERTY_SET: 1, // 设备属性设置,
@ -25,11 +17,7 @@ const IotDeviceMessageTypeEnum = {
EVENT: 'event' // 事件 EVENT: 'event' // 事件
} as const } as const
// TODO @puhui999这个貌似可以不要 // 已删除不需要的 IotDeviceMessageIdentifierEnum
const IotDeviceMessageIdentifierEnum = {
PROPERTY_SET: 'set', // 属性设置
SERVICE_INVOKE: '${identifier}' // 服务调用
} as const
const IotRuleSceneTriggerConditionParameterOperatorEnum = { const IotRuleSceneTriggerConditionParameterOperatorEnum = {
EQUALS: { name: '等于', value: '=' }, // 等于 EQUALS: { name: '等于', value: '=' }, // 等于
@ -64,29 +52,10 @@ const IotRuleSceneTriggerTimeOperatorEnum = {
TODAY: { name: '在今日之间', value: 'today' } // 在今日之间 TODAY: { name: '在今日之间', value: 'today' } // 在今日之间
} as const } as const
// TODO @puhui999下面 IotAlertConfigReceiveTypeEnum、DeviceStateEnum 没用到,貌似可以删除下? // 已删除未使用的枚举IotAlertConfigReceiveTypeEnum、DeviceStateEnum
const IotAlertConfigReceiveTypeEnum = { // CommonStatusEnum 已在全局定义,这里不再重复定义
SMS: 1, // 短信
MAIL: 2, // 邮箱
NOTIFY: 3 // 通知
} as const
// 设备状态枚举 // 基础接口(如果项目中有全局的 BaseDO可以使用全局的
const DeviceStateEnum = {
INACTIVE: 0, // 未激活
ONLINE: 1, // 在线
OFFLINE: 2 // 离线
} as const
// TODO @puhui999这个全局已经有啦
// 通用状态枚举
const CommonStatusEnum = {
ENABLE: 0, // 开启
DISABLE: 1 // 关闭
} as const
// 基础接口
// TODO @puhui999这个貌似可以不要
interface TenantBaseDO { interface TenantBaseDO {
createTime?: Date // 创建时间 createTime?: Date // 创建时间
updateTime?: Date // 更新时间 updateTime?: Date // 更新时间
@ -144,7 +113,7 @@ interface RuleSceneFormData {
name: string name: string
description?: string description?: string
status: number status: number
trigger: TriggerFormData triggers: TriggerFormData[] // 支持多个触发器
actions: ActionFormData[] actions: ActionFormData[]
} }
@ -209,8 +178,7 @@ interface IotRuleScene extends TenantBaseDO {
} }
// 工具类型 - 从枚举中提取类型 // 工具类型 - 从枚举中提取类型
export type TriggerType = // TriggerType 现在从 constants.ts 中的枚举提取
(typeof IotRuleSceneTriggerTypeEnum)[keyof typeof IotRuleSceneTriggerTypeEnum]
export type ActionType = export type ActionType =
(typeof IotRuleSceneActionTypeEnum)[keyof typeof IotRuleSceneActionTypeEnum] (typeof IotRuleSceneActionTypeEnum)[keyof typeof IotRuleSceneActionTypeEnum]
export type MessageType = (typeof IotDeviceMessageTypeEnum)[keyof typeof IotDeviceMessageTypeEnum] export type MessageType = (typeof IotDeviceMessageTypeEnum)[keyof typeof IotDeviceMessageTypeEnum]
@ -246,16 +214,11 @@ export {
ConditionGroupContainerFormData, ConditionGroupContainerFormData,
SubConditionGroupFormData, SubConditionGroupFormData,
ConditionFormData, ConditionFormData,
IotRuleSceneTriggerTypeEnum,
IotRuleSceneActionTypeEnum, IotRuleSceneActionTypeEnum,
IotDeviceMessageTypeEnum, IotDeviceMessageTypeEnum,
IotDeviceMessageIdentifierEnum,
IotRuleSceneTriggerConditionParameterOperatorEnum, IotRuleSceneTriggerConditionParameterOperatorEnum,
IotRuleSceneTriggerConditionTypeEnum, IotRuleSceneTriggerConditionTypeEnum,
IotRuleSceneTriggerTimeOperatorEnum, IotRuleSceneTriggerTimeOperatorEnum,
IotAlertConfigReceiveTypeEnum,
DeviceStateEnum,
CommonStatusEnum,
ValidationRule, ValidationRule,
FormValidationRules FormValidationRules
} }

View File

@ -9,12 +9,12 @@
:close-on-press-escape="false" :close-on-press-escape="false"
@close="handleClose" @close="handleClose"
> >
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px"> <el-form ref="formRef" :model="formData" :rules="formRules" label-width="110px">
<!-- 基础信息配置 --> <!-- 基础信息配置 -->
<BasicInfoSection v-model="formData" :rules="formRules" /> <BasicInfoSection v-model="formData" :rules="formRules" />
<!-- 触发器配置 --> <!-- 触发器配置 -->
<TriggerSection v-model:trigger="formData.trigger" @validate="handleTriggerValidate" /> <TriggerSection v-model:triggers="formData.triggers" @validate="handleTriggerValidate" />
<!-- 执行器配置 --> <!-- 执行器配置 -->
<ActionSection v-model:actions="formData.actions" @validate="handleActionValidate" /> <ActionSection v-model:actions="formData.actions" @validate="handleActionValidate" />
@ -40,15 +40,21 @@ 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 { import {
CommonStatusEnum,
IotRuleScene, IotRuleScene,
IotRuleSceneActionTypeEnum, IotRuleSceneActionTypeEnum,
IotRuleSceneTriggerTypeEnum, RuleSceneFormData,
RuleSceneFormData TriggerFormData
} from '@/api/iot/rule/scene/scene.types' } from '@/api/iot/rule/scene/scene.types'
import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { generateUUID } from '@/utils' import { generateUUID } from '@/utils'
// CommonStatusEnum
const CommonStatusEnum = {
ENABLE: 0, //
DISABLE: 1 //
} as const
/** IoT 场景联动规则表单 - 主表单组件 */ /** IoT 场景联动规则表单 - 主表单组件 */
defineOptions({ name: 'RuleSceneForm' }) defineOptions({ name: 'RuleSceneForm' })
@ -76,17 +82,19 @@ const createDefaultFormData = (): RuleSceneFormData => {
name: '', name: '',
description: '', description: '',
status: CommonStatusEnum.ENABLE, // status: CommonStatusEnum.ENABLE, //
trigger: { triggers: [
type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST, {
productId: undefined, type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
deviceId: undefined, productId: undefined,
identifier: undefined, deviceId: undefined,
operator: undefined, identifier: undefined,
value: undefined, operator: undefined,
cronExpression: undefined, value: undefined,
mainCondition: undefined, cronExpression: undefined,
conditionGroup: undefined mainCondition: undefined,
}, conditionGroup: undefined
}
],
actions: [] actions: []
} }
} }
@ -95,13 +103,13 @@ const createDefaultFormData = (): RuleSceneFormData => {
* 将表单数据转换为 API 请求格式 * 将表单数据转换为 API 请求格式
*/ */
const convertFormToVO = (formData: RuleSceneFormData): IotRuleScene => { const convertFormToVO = (formData: RuleSceneFormData): IotRuleScene => {
// //
const buildTriggerConditions = () => { const buildTriggerConditions = (trigger: TriggerFormData) => {
const conditions: any[] = [] const conditions: any[] = []
// //
if (formData.trigger.mainCondition) { if (trigger.mainCondition) {
const mainCondition = formData.trigger.mainCondition const mainCondition = trigger.mainCondition
conditions.push({ conditions.push({
type: mainCondition.type === 2 ? 'property' : 'event', type: mainCondition.type === 2 ? 'property' : 'event',
identifier: mainCondition.identifier || '', identifier: mainCondition.identifier || '',
@ -115,8 +123,8 @@ const convertFormToVO = (formData: RuleSceneFormData): IotRuleScene => {
} }
// //
if (formData.trigger.conditionGroup?.subGroups) { if (trigger.conditionGroup?.subGroups) {
formData.trigger.conditionGroup.subGroups.forEach((subGroup) => { trigger.conditionGroup.subGroups.forEach((subGroup) => {
subGroup.conditions.forEach((condition) => { subGroup.conditions.forEach((condition) => {
conditions.push({ conditions.push({
type: condition.type === 2 ? 'property' : 'event', type: condition.type === 2 ? 'property' : 'event',
@ -140,19 +148,13 @@ const convertFormToVO = (formData: RuleSceneFormData): IotRuleScene => {
name: formData.name, name: formData.name,
description: formData.description, description: formData.description,
status: Number(formData.status), status: Number(formData.status),
triggers: [ triggers: formData.triggers.map((trigger) => ({
{ type: trigger.type,
type: formData.trigger.type, productKey: trigger.productId ? `product_${trigger.productId}` : undefined,
productKey: formData.trigger.productId deviceNames: trigger.deviceId ? [`device_${trigger.deviceId}`] : undefined,
? `product_${formData.trigger.productId}` cronExpression: trigger.cronExpression,
: undefined, conditions: buildTriggerConditions(trigger)
deviceNames: formData.trigger.deviceId })),
? [`device_${formData.trigger.deviceId}`]
: undefined,
cronExpression: formData.trigger.cronExpression,
conditions: buildTriggerConditions()
}
],
actions: actions:
formData.actions?.map((action) => ({ formData.actions?.map((action) => ({
type: action.type, type: action.type,
@ -180,9 +182,7 @@ const convertFormToVO = (formData: RuleSceneFormData): IotRuleScene => {
* API 响应数据转换为表单格式 * API 响应数据转换为表单格式
*/ */
const convertVOToForm = (apiData: IotRuleScene): RuleSceneFormData => { const convertVOToForm = (apiData: IotRuleScene): RuleSceneFormData => {
const firstTrigger = apiData.triggers?.[0] //
//
const parseConditions = (trigger: any) => { const parseConditions = (trigger: any) => {
if (!trigger?.conditions?.length) { if (!trigger?.conditions?.length) {
return { return {
@ -208,28 +208,23 @@ const convertVOToForm = (apiData: IotRuleScene): RuleSceneFormData => {
} }
} }
const conditionData = firstTrigger //
? parseConditions(firstTrigger) const triggers = apiData.triggers?.length
: { ? apiData.triggers.map((trigger) => {
mainCondition: undefined, const conditionData = parseConditions(trigger)
conditionGroup: undefined return {
} type: Number(trigger.type),
return {
...apiData,
status: Number(apiData.status),
trigger: firstTrigger
? {
type: Number(firstTrigger.type),
productId: undefined, // productKey productId: undefined, // productKey
deviceId: undefined, // deviceNames deviceId: undefined, // deviceNames
identifier: undefined, identifier: undefined,
operator: undefined, operator: undefined,
value: undefined, value: undefined,
cronExpression: firstTrigger.cronExpression, cronExpression: trigger.cronExpression,
...conditionData ...conditionData
} }
: { })
: [
{
type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST, type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
productId: undefined, productId: undefined,
deviceId: undefined, deviceId: undefined,
@ -239,7 +234,13 @@ const convertVOToForm = (apiData: IotRuleScene): RuleSceneFormData => {
cronExpression: undefined, cronExpression: undefined,
mainCondition: undefined, mainCondition: undefined,
conditionGroup: undefined conditionGroup: undefined
}, }
]
return {
...apiData,
status: Number(apiData.status),
triggers,
actions: actions:
apiData.actions?.map((action) => ({ apiData.actions?.map((action) => ({
...action, ...action,

View File

@ -114,11 +114,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import ConditionConfig from './ConditionConfig.vue' import ConditionConfig from './ConditionConfig.vue'
import { import { ConditionFormData, ConditionGroupFormData } from '@/api/iot/rule/scene/scene.types'
ConditionGroupFormData, import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
ConditionFormData,
IotRuleSceneTriggerTypeEnum
} from '@/api/iot/rule/scene/scene.types'
/** 条件组配置组件 */ /** 条件组配置组件 */
defineOptions({ name: 'ConditionGroupConfig' }) defineOptions({ name: 'ConditionGroupConfig' })
@ -133,6 +130,7 @@ interface Props {
interface Emits { interface Emits {
(e: 'update:modelValue', value: ConditionGroupFormData): void (e: 'update:modelValue', value: ConditionGroupFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void (e: 'validate', result: { valid: boolean; message: string }): void
} }

View File

@ -62,26 +62,21 @@ import { useVModel } from '@vueuse/core'
import MainConditionConfig from './MainConditionConfig.vue' import MainConditionConfig from './MainConditionConfig.vue'
import ConditionGroupContainerConfig from './ConditionGroupContainerConfig.vue' import ConditionGroupContainerConfig from './ConditionGroupContainerConfig.vue'
import { import { TriggerFormData } from '@/api/iot/rule/scene/scene.types'
TriggerFormData, import { IotRuleSceneTriggerTypeEnum as TriggerTypeEnum } from '@/views/iot/utils/constants'
IotRuleSceneTriggerTypeEnum as TriggerTypeEnum
} from '@/api/iot/rule/scene/scene.types'
/** 设备触发配置组件 */ /** 设备触发配置组件 */
defineOptions({ name: 'DeviceTriggerConfig' }) defineOptions({ name: 'DeviceTriggerConfig' })
// TODO @puhui999 PropsEmits // Props Emits
interface Props { const props = defineProps<{
modelValue: TriggerFormData modelValue: TriggerFormData
} }>()
interface Emits { const emit = defineEmits<{
(e: 'update:modelValue', value: TriggerFormData): void 'update:modelValue': [value: TriggerFormData]
(e: 'validate', result: { valid: boolean; message: string }): void validate: [result: { valid: boolean; message: string }]
} }>()
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const trigger = useVModel(props, 'modelValue', emit) const trigger = useVModel(props, 'modelValue', emit)

View File

@ -20,19 +20,13 @@
</div> </div>
<!-- 主条件配置 --> <!-- 主条件配置 -->
<!-- TODO @puhui999这里可以简化下主条件是不能删除的 -->
<div v-else class="space-y-16px"> <div v-else class="space-y-16px">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-8px"> <div class="flex items-center gap-8px">
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">主条件</span> <span class="text-14px font-500 text-[var(--el-text-color-primary)]">主条件</span>
<el-tag size="small" type="primary">必须满足</el-tag> <el-tag size="small" type="primary">必须满足</el-tag>
</div> </div>
<el-button type="danger" size="small" text @click="removeMainCondition">
<Icon icon="ep:delete" />
删除
</el-button>
</div> </div>
<MainConditionInnerConfig <MainConditionInnerConfig
:model-value="modelValue" :model-value="modelValue"
@update:model-value="updateCondition" @update:model-value="updateCondition"
@ -53,18 +47,14 @@ import {
/** 主条件配置组件 */ /** 主条件配置组件 */
defineOptions({ name: 'MainConditionConfig' }) defineOptions({ name: 'MainConditionConfig' })
interface Props { defineProps<{
modelValue?: ConditionFormData modelValue?: ConditionFormData
triggerType: number triggerType: number
} }>()
const emit = defineEmits<{
interface Emits {
(e: 'update:modelValue', value?: ConditionFormData): void (e: 'update:modelValue', value?: ConditionFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void (e: 'validate', result: { valid: boolean; message: string }): void
} }>()
defineProps<Props>()
const emit = defineEmits<Emits>()
// //
const addMainCondition = () => { const addMainCondition = () => {
@ -79,10 +69,6 @@ const addMainCondition = () => {
emit('update:modelValue', newCondition) emit('update:modelValue', newCondition)
} }
const removeMainCondition = () => {
emit('update:modelValue', undefined)
}
const updateCondition = (condition: ConditionFormData) => { const updateCondition = (condition: ConditionFormData) => {
emit('update:modelValue', condition) emit('update:modelValue', condition)
} }
@ -90,4 +76,8 @@ const updateCondition = (condition: ConditionFormData) => {
const handleValidate = (result: { valid: boolean; message: string }) => { const handleValidate = (result: { valid: boolean; message: string }) => {
emit('validate', result) emit('validate', result)
} }
onMounted(() => {
addMainCondition()
})
</script> </script>

View File

@ -1,11 +1,5 @@
<template> <template>
<div class="space-y-16px"> <div class="space-y-16px">
<!-- 触发事件类型显示 -->
<div class="flex items-center gap-8px mb-16px">
<span class="text-14px text-[var(--el-text-color-regular)]">触发事件类型</span>
<el-tag size="small" type="primary">{{ getTriggerTypeText(triggerType) }}</el-tag>
</div>
<!-- 设备属性条件配置 --> <!-- 设备属性条件配置 -->
<div v-if="isDevicePropertyTrigger" class="space-y-16px"> <div v-if="isDevicePropertyTrigger" class="space-y-16px">
<!-- 产品设备选择 --> <!-- 产品设备选择 -->
@ -112,7 +106,7 @@ import OperatorSelector from '../selectors/OperatorSelector.vue'
import ValueInput from '../inputs/ValueInput.vue' import ValueInput from '../inputs/ValueInput.vue'
import DeviceStatusConditionConfig from './DeviceStatusConditionConfig.vue' import DeviceStatusConditionConfig from './DeviceStatusConditionConfig.vue'
import { ConditionFormData } from '@/api/iot/rule/scene/scene.types' import { ConditionFormData } from '@/api/iot/rule/scene/scene.types'
import { IotRuleSceneTriggerTypeEnum } from '@/api/iot/rule/scene/scene.types' import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
/** 主条件内部配置组件 */ /** 主条件内部配置组件 */
@ -125,6 +119,7 @@ interface Props {
interface Emits { interface Emits {
(e: 'update:modelValue', value: ConditionFormData): void (e: 'update:modelValue', value: ConditionFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void (e: 'validate', result: { valid: boolean; message: string }): void
} }

View File

@ -1,6 +1,6 @@
<!-- 基础信息配置组件 --> <!-- 基础信息配置组件 -->
<template> <template>
<el-card class="border border-[var(--el-border-color-light)] rounded-8px" shadow="never"> <el-card class="border border-[var(--el-border-color-light)] rounded-8px mb-10px" shadow="never">
<template #header> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-8px"> <div class="flex items-center gap-8px">
@ -8,10 +8,7 @@
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">基础信息</span> <span class="text-16px font-600 text-[var(--el-text-color-primary)]">基础信息</span>
</div> </div>
<div class="flex items-center gap-8px"> <div class="flex items-center gap-8px">
<!-- TODO @puhui999dict-tag 可以哇 --> <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData.status" />
<el-tag :type="formData.status === 0 ? 'success' : 'danger'" size="small">
{{ formData.status === 0 ? '启用' : '禁用' }}
</el-tag>
</div> </div>
</div> </div>
</template> </template>
@ -60,25 +57,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types' import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
/** 基础信息配置组件 */ /** 基础信息配置组件 */
defineOptions({ name: 'BasicInfoSection' }) defineOptions({ name: 'BasicInfoSection' })
// TODO @puhui999 PropsEmits const props = defineProps<{
interface Props {
modelValue: RuleSceneFormData modelValue: RuleSceneFormData
rules?: any rules?: any
} }>()
const emit = defineEmits<{
interface Emits {
(e: 'update:modelValue', value: RuleSceneFormData): void (e: 'update:modelValue', value: RuleSceneFormData): void
} }>()
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const formData = useVModel(props, 'modelValue', emit) const formData = useVModel(props, 'modelValue', emit)
</script> </script>

View File

@ -1,47 +1,97 @@
<template> <template>
<el-card class="border border-[var(--el-border-color-light)] rounded-8px" shadow="never"> <el-card class="border border-[var(--el-border-color-light)] rounded-8px mb-10px" shadow="never">
<!-- TODO @puhui999触发器还是多个每个触发器里面有事件类型 + 附加条件组最好文案上和阿里 iot 保持相对一致 -->
<template #header> <template #header>
<div class="flex items-center gap-8px"> <div class="flex items-center justify-between">
<Icon icon="ep:lightning" class="text-[var(--el-color-primary)] text-18px" /> <div class="flex items-center gap-8px">
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">触发器配置</span> <Icon icon="ep:lightning" class="text-[var(--el-color-primary)] text-18px" />
<el-tag size="small" type="info">场景触发器</el-tag> <span class="text-16px font-600 text-[var(--el-text-color-primary)]">触发器配置</span>
<el-tag size="small" type="info">{{ triggers.length }} 个触发器</el-tag>
</div>
<el-button type="primary" size="small" @click="addTrigger">
<Icon icon="ep:plus" />
添加触发器
</el-button>
</div> </div>
</template> </template>
<div class="p-16px space-y-16px"> <div class="p-16px space-y-24px">
<!-- 触发事件类型选择 --> <!-- 触发器列表 -->
<el-form-item label="触发事件类型" required> <div v-if="triggers.length > 0" class="space-y-24px">
<el-select <div
:model-value="trigger.type" v-for="(triggerItem, index) in triggers"
@update:model-value="(value) => updateTriggerType(value)" :key="`trigger-${index}`"
@change="onTriggerTypeChange" class="border border-[var(--el-border-color-light)] rounded-8px p-16px relative"
placeholder="请选择触发事件类型"
class="w-full"
> >
<el-option <!-- 触发器头部 -->
v-for="option in triggerTypeOptions" <div class="flex items-center justify-between mb-16px">
:key="option.value" <div class="flex items-center gap-8px">
:label="option.label" <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
:value="option.value" 触发器 {{ index + 1 }}
</span>
<el-tag size="small" :type="getTriggerTagType(triggerItem.type)">
{{ getTriggerTypeLabel(triggerItem.type) }}
</el-tag>
</div>
<div class="flex items-center gap-8px">
<el-button
v-if="triggers.length > 1"
type="danger"
size="small"
text
@click="removeTrigger(index)"
>
<Icon icon="ep:delete" />
删除
</el-button>
</div>
</div>
<!-- 触发事件类型选择 -->
<el-form-item label="触发事件类型" required>
<el-select
:model-value="triggerItem.type"
@update:model-value="(value) => updateTriggerType(index, value)"
placeholder="请选择触发事件类型"
class="w-full"
>
<el-option
v-for="option in triggerTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
<!-- 设备触发配置 -->
<DeviceTriggerConfig
v-if="isDeviceTrigger(triggerItem.type)"
:model-value="triggerItem"
@update:model-value="(value) => updateTriggerDeviceConfig(index, value)"
/> />
</el-select>
</el-form-item>
<!-- 设备触发配置 --> <!-- 定时触发配置 -->
<DeviceTriggerConfig <TimerTriggerConfig
v-if="isDeviceTrigger(trigger.type)" v-else-if="triggerItem.type === TriggerTypeEnum.TIMER"
:model-value="trigger" :model-value="triggerItem.cronExpression"
@update:model-value="updateTrigger" @update:model-value="(value) => updateTriggerCronConfig(index, value)"
/> />
</div>
</div>
<!-- 定时触发配置 --> <!-- 空状态 -->
<!-- TODO @puhui999这里要不 v-else 好了 --> <div v-else class="py-40px text-center">
<TimerTriggerConfig <el-empty description="暂无触发器">
v-if="trigger.type === TriggerTypeEnum.TIMER" <template #description>
:model-value="trigger.cronExpression" <div class="space-y-8px">
@update:model-value="updateTriggerCronExpression" <p class="text-[var(--el-text-color-secondary)]">暂无触发器配置</p>
/> <p class="text-12px text-[var(--el-text-color-placeholder)]">
请使用上方的"添加触发器"按钮来设置触发规则
</p>
</div>
</template>
</el-empty>
</div>
</div> </div>
</el-card> </el-card>
</template> </template>
@ -50,108 +100,115 @@
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue' import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue'
import TimerTriggerConfig from '../configs/TimerTriggerConfig.vue' import TimerTriggerConfig from '../configs/TimerTriggerConfig.vue'
import { TriggerFormData } from '@/api/iot/rule/scene/scene.types'
import { import {
TriggerFormData, getTriggerTypeOptions,
IotRuleSceneTriggerTypeEnum as TriggerTypeEnum IotRuleSceneTriggerTypeEnum as TriggerTypeEnum,
} from '@/api/iot/rule/scene/scene.types' IotRuleSceneTriggerTypeEnum,
isDeviceTrigger
} from '@/views/iot/utils/constants'
/** 触发器配置组件 */ /** 触发器配置组件 */
defineOptions({ name: 'TriggerSection' }) defineOptions({ name: 'TriggerSection' })
// TODO @puhui999 PropsEmits // Props Emits
interface Props { const props = defineProps<{
trigger: TriggerFormData triggers: TriggerFormData[]
} }>()
interface Emits { const emit = defineEmits<{
(e: 'update:trigger', value: TriggerFormData): void 'update:triggers': [value: TriggerFormData[]]
} }>()
const props = defineProps<Props>() const triggers = useVModel(props, 'triggers', emit)
const emit = defineEmits<Emits>()
const trigger = useVModel(props, 'trigger', emit) // constants
const triggerTypeOptions = getTriggerTypeOptions()
//
// TODO @puhui999/Users/yunai/Java/yudao-ui-admin-vue3/src/views/iot/utils/constants.ts
const triggerTypeOptions = [
{
value: TriggerTypeEnum.DEVICE_STATE_UPDATE,
label: '设备状态变更'
},
{
value: TriggerTypeEnum.DEVICE_PROPERTY_POST,
label: '设备属性上报'
},
{
value: TriggerTypeEnum.DEVICE_EVENT_POST,
label: '设备事件上报'
},
{
value: TriggerTypeEnum.DEVICE_SERVICE_INVOKE,
label: '设备服务调用'
},
{
value: TriggerTypeEnum.TIMER,
label: '定时触发'
}
]
// //
// TODO @puhui999/Users/yunai/Java/yudao-ui-admin-vue3/src/views/iot/utils/constants.ts const getTriggerTypeLabel = (type: number): string => {
const isDeviceTrigger = (type: number) => { const option = triggerTypeOptions.find((opt) => opt.value === type)
const deviceTriggerTypes = [ return option?.label || '未知类型'
TriggerTypeEnum.DEVICE_STATE_UPDATE,
TriggerTypeEnum.DEVICE_PROPERTY_POST,
TriggerTypeEnum.DEVICE_EVENT_POST,
TriggerTypeEnum.DEVICE_SERVICE_INVOKE
] as number[]
return deviceTriggerTypes.includes(type)
} }
// const getTriggerTagType = (type: number): string => {
const updateTriggerType = (type: number) => { if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
trigger.value.type = type return 'warning'
onTriggerTypeChange(type) }
return isDeviceTrigger(type) ? 'success' : 'info'
} }
// TODO @puhui999updateTriggerDeviceConfig //
const updateTrigger = (newTrigger: TriggerFormData) => { const addTrigger = () => {
trigger.value = newTrigger const newTrigger: TriggerFormData = {
type: TriggerTypeEnum.DEVICE_STATE_UPDATE,
productId: undefined,
deviceId: undefined,
identifier: undefined,
operator: undefined,
value: undefined,
cronExpression: undefined,
mainCondition: undefined,
conditionGroup: undefined
}
triggers.value.push(newTrigger)
} }
// TODO @puhui999updateTriggerCronConfig const removeTrigger = (index: number) => {
const updateTriggerCronExpression = (cronExpression?: string) => { if (triggers.value.length > 1) {
trigger.value.cronExpression = cronExpression triggers.value.splice(index, 1)
}
} }
const onTriggerTypeChange = (type: number) => { const updateTriggerType = (index: number, type: number) => {
triggers.value[index].type = type
onTriggerTypeChange(index, type)
}
const updateTriggerDeviceConfig = (index: number, newTrigger: TriggerFormData) => {
triggers.value[index] = newTrigger
}
const updateTriggerCronConfig = (index: number, cronExpression?: string) => {
triggers.value[index].cronExpression = cronExpression
}
const onTriggerTypeChange = (index: number, type: number) => {
const triggerItem = triggers.value[index]
// //
if (type === TriggerTypeEnum.TIMER) { if (type === TriggerTypeEnum.TIMER) {
trigger.value.productId = undefined triggerItem.productId = undefined
trigger.value.deviceId = undefined triggerItem.deviceId = undefined
trigger.value.identifier = undefined triggerItem.identifier = undefined
trigger.value.operator = undefined triggerItem.operator = undefined
trigger.value.value = undefined triggerItem.value = undefined
trigger.value.mainCondition = undefined triggerItem.mainCondition = undefined
trigger.value.conditionGroup = undefined triggerItem.conditionGroup = undefined
if (!trigger.value.cronExpression) { if (!triggerItem.cronExpression) {
trigger.value.cronExpression = '0 0 12 * * ?' triggerItem.cronExpression = '0 0 12 * * ?'
} }
} else { } else {
trigger.value.cronExpression = undefined triggerItem.cronExpression = undefined
if (type === TriggerTypeEnum.DEVICE_STATE_UPDATE) { if (type === TriggerTypeEnum.DEVICE_STATE_UPDATE) {
trigger.value.mainCondition = undefined triggerItem.mainCondition = undefined
trigger.value.conditionGroup = undefined triggerItem.conditionGroup = undefined
} else { } else {
// //
if (!trigger.value.mainCondition) { if (!triggerItem.mainCondition) {
trigger.value.mainCondition = undefined // triggerItem.mainCondition = undefined //
} }
if (!trigger.value.conditionGroup) { if (!triggerItem.conditionGroup) {
trigger.value.conditionGroup = undefined // triggerItem.conditionGroup = undefined //
} }
} }
} }
} }
//
onMounted(() => {
if (triggers.value.length === 0) {
addTrigger()
}
})
</script> </script>

View File

@ -15,26 +15,21 @@
> >
<div class="flex items-center justify-between w-full py-4px"> <div class="flex items-center justify-between w-full py-4px">
<div class="flex items-center gap-8px"> <div class="flex items-center gap-8px">
<div class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ operator.label }}</div> <div class="text-14px font-500 text-[var(--el-text-color-primary)]">
<div class="text-12px text-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)] px-6px py-2px rounded-4px font-mono">{{ operator.symbol }}</div> {{ operator.label }}
</div>
<div
class="text-12px text-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)] px-6px py-2px rounded-4px font-mono"
>
{{ operator.symbol }}
</div>
</div>
<div class="text-12px text-[var(--el-text-color-secondary)]">
{{ operator.description }}
</div> </div>
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ operator.description }}</div>
</div> </div>
</el-option> </el-option>
</el-select> </el-select>
<!-- 操作符说明 -->
<!-- TODO @puhui999这个去掉 -->
<div v-if="selectedOperator" class="mt-8px p-8px bg-[var(--el-fill-color-light)] rounded-4px border border-[var(--el-border-color-lighter)]">
<div class="flex items-center gap-6px">
<Icon icon="ep:info-filled" class="text-12px text-[var(--el-color-info)]" />
<span class="text-12px text-[var(--el-text-color-secondary)]">{{ selectedOperator.description }}</span>
</div>
<div v-if="selectedOperator.example" class="flex items-center gap-6px mt-4px">
<span class="text-12px text-[var(--el-text-color-secondary)]">示例</span>
<code class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] px-4px py-2px rounded-2px font-mono">{{ selectedOperator.example }}</code>
</div>
</div>
</div> </div>
</template> </template>
@ -51,6 +46,7 @@ interface Props {
interface Emits { interface Emits {
(e: 'update:modelValue', value: string): void (e: 'update:modelValue', value: string): void
(e: 'change', value: string): void (e: 'change', value: string): void
} }

View File

@ -1,14 +1,14 @@
<!-- 属性选择器组件 --> <!-- 属性选择器组件 -->
<!-- TODO @yunai可能要在 review --> <!-- TODO @yunai可能要在 review -->
<template> <template>
<div class="w-full"> <div class="flex items-center gap-8px">
<el-select <el-select
v-model="localValue" v-model="localValue"
placeholder="请选择监控项" placeholder="请选择监控项"
filterable filterable
clearable clearable
@change="handleChange" @change="handleChange"
class="w-full" class="!w-150px"
:loading="loading" :loading="loading"
> >
<el-option-group v-for="group in propertyGroups" :key="group.label" :label="group.label"> <el-option-group v-for="group in propertyGroups" :key="group.label" :label="group.label">
@ -20,8 +20,12 @@
> >
<div class="flex items-center justify-between w-full py-4px"> <div class="flex items-center justify-between w-full py-4px">
<div class="flex-1"> <div class="flex-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ property.name }}</div> <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ property.identifier }}</div> {{ property.name }}
</div>
<div class="text-12px text-[var(--el-text-color-secondary)]">
{{ property.identifier }}
</div>
</div> </div>
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<el-tag :type="getPropertyTypeTag(property.dataType)" size="small"> <el-tag :type="getPropertyTypeTag(property.dataType)" size="small">
@ -33,42 +37,98 @@
</el-option-group> </el-option-group>
</el-select> </el-select>
<!-- 属性详情 --> <!-- 属性详情触发按钮 -->
<div v-if="selectedProperty" class="mt-16px p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"> <div class="relative">
<div class="flex items-center gap-8px mb-12px"> <el-button
<Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-16px" /> v-if="selectedProperty"
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ selectedProperty.name }}</span> ref="detailTriggerRef"
<el-tag :type="getPropertyTypeTag(selectedProperty.dataType)" size="small"> type="info"
{{ getPropertyTypeName(selectedProperty.dataType) }} :icon="InfoFilled"
</el-tag> circle
</div> size="small"
<div class="space-y-8px ml-24px"> @click="togglePropertyDetail"
<div class="flex items-start gap-8px"> class="flex-shrink-0"
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">标识符</span> title="查看属性详情"
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.identifier }}</span> />
<!-- 属性详情弹出层 -->
<Teleport to="body">
<div
v-if="showPropertyDetail && selectedProperty"
ref="propertyDetailRef"
class="property-detail-popover"
:style="popoverStyle"
>
<div
class="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-300px max-w-400px"
>
<div class="flex items-center gap-8px mb-12px">
<Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-4px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">
{{ selectedProperty.name }}
</span>
<el-tag :type="getPropertyTypeTag(selectedProperty.dataType)" size="small">
{{ getPropertyTypeName(selectedProperty.dataType) }}
</el-tag>
</div>
<div class="space-y-8px ml-24px">
<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">
{{ selectedProperty.identifier }}
</span>
</div>
<div v-if="selectedProperty.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">
{{ selectedProperty.description }}
</span>
</div>
<div v-if="selectedProperty.unit" 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">
{{ selectedProperty.unit }}
</span>
</div>
<div v-if="selectedProperty.range" 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">
{{ selectedProperty.range }}
</span>
</div>
</div>
<!-- 关闭按钮 -->
<div class="flex justify-end mt-12px">
<el-button size="small" @click="hidePropertyDetail"></el-button>
</div>
</div>
</div> </div>
<div v-if="selectedProperty.description" class="flex items-start gap-8px"> </Teleport>
<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">{{ selectedProperty.description }}</span>
</div>
<div v-if="selectedProperty.unit" 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">{{ selectedProperty.unit }}</span>
</div>
<div v-if="selectedProperty.range" 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">{{ selectedProperty.range }}</span>
</div>
</div>
</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 { IotRuleSceneTriggerTypeEnum } from '@/api/iot/rule/scene/scene.types' import { InfoFilled } from '@element-plus/icons-vue'
import { IotRuleSceneTriggerTypeEnum, IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
import { ThingModelApi } from '@/api/iot/thingmodel' import { ThingModelApi } from '@/api/iot/thingmodel'
import { IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
import type { IotThingModelTSLRespVO, PropertySelectorItem } from './types' import type { IotThingModelTSLRespVO, PropertySelectorItem } from './types'
/** 属性选择器组件 */ /** 属性选择器组件 */
@ -83,6 +143,7 @@ interface Props {
interface Emits { interface Emits {
(e: 'update:modelValue', value: string): void (e: 'update:modelValue', value: string): void
(e: 'change', value: { type: string; config: any }): void (e: 'change', value: { type: string; config: any }): void
} }
@ -96,6 +157,25 @@ const loading = ref(false)
const propertyList = ref<PropertySelectorItem[]>([]) const propertyList = ref<PropertySelectorItem[]>([])
const thingModelTSL = ref<IotThingModelTSLRespVO | null>(null) const thingModelTSL = ref<IotThingModelTSLRespVO | null>(null)
//
const showPropertyDetail = ref(false)
const detailTriggerRef = ref()
const propertyDetailRef = ref()
const popoverStyle = ref({})
//
const handleClickOutside = (event: MouseEvent) => {
if (
showPropertyDetail.value &&
propertyDetailRef.value &&
detailTriggerRef.value &&
!propertyDetailRef.value.contains(event.target as Node) &&
!detailTriggerRef.value.$el.contains(event.target as Node)
) {
hidePropertyDetail()
}
}
// //
const propertyGroups = computed(() => { const propertyGroups = computed(() => {
const groups: { label: string; options: any[] }[] = [] const groups: { label: string; options: any[] }[] = []
@ -159,6 +239,67 @@ const getPropertyTypeTag = (dataType: string) => {
return tagMap[dataType] || 'info' return tagMap[dataType] || 'info'
} }
//
const togglePropertyDetail = () => {
if (showPropertyDetail.value) {
hidePropertyDetail()
} else {
showPropertyDetailPopover()
}
}
const showPropertyDetailPopover = () => {
if (!selectedProperty.value || !detailTriggerRef.value) return
showPropertyDetail.value = true
nextTick(() => {
updatePopoverPosition()
})
}
const hidePropertyDetail = () => {
showPropertyDetail.value = false
}
const updatePopoverPosition = () => {
if (!detailTriggerRef.value || !propertyDetailRef.value) return
const triggerEl = detailTriggerRef.value.$el
const triggerRect = triggerEl.getBoundingClientRect()
const popoverEl = propertyDetailRef.value
//
const left = triggerRect.left + triggerRect.width + 8
const top = triggerRect.top
//
const popoverWidth = 400 //
const viewportWidth = window.innerWidth
let finalLeft = left
if (left + popoverWidth > viewportWidth - 16) {
//
finalLeft = triggerRect.left - popoverWidth - 8
}
//
let finalTop = top
const popoverHeight = popoverEl.offsetHeight || 200
const viewportHeight = window.innerHeight
if (top + popoverHeight > viewportHeight - 16) {
finalTop = Math.max(16, viewportHeight - popoverHeight - 16)
}
popoverStyle.value = {
position: 'fixed',
left: `${finalLeft}px`,
top: `${finalTop}px`,
zIndex: 9999
}
}
// //
const handleChange = (value: string) => { const handleChange = (value: string) => {
const property = propertyList.value.find((p) => p.identifier === value) const property = propertyList.value.find((p) => p.identifier === value)
@ -168,6 +309,8 @@ const handleChange = (value: string) => {
config: property config: property
}) })
} }
//
hidePropertyDetail()
} }
// TSL // TSL
@ -331,13 +474,74 @@ watch(
() => props.triggerType, () => props.triggerType,
() => { () => {
localValue.value = '' localValue.value = ''
hidePropertyDetail()
} }
) )
//
const handleResize = () => {
if (showPropertyDetail.value) {
updatePopoverPosition()
}
}
//
onMounted(() => {
document.addEventListener('click', handleClickOutside)
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
window.removeEventListener('resize', handleResize)
})
</script> </script>
<style scoped> <style scoped>
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.9) translateY(-4px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
:deep(.el-select-dropdown__item) { :deep(.el-select-dropdown__item) {
height: auto; height: auto;
padding: 8px 20px; padding: 8px 20px;
} }
.property-detail-popover {
animation: fadeInScale 0.2s ease-out;
transform-origin: top left;
}
/* 弹出层箭头效果(可选) */
.property-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: '';
}
.property-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: '';
}
</style> </style>

View File

@ -203,3 +203,49 @@ export const IoTOtaTaskRecordStatusEnum = {
value: 50 value: 50
} }
} as const } as const
// ========== 场景联动规则相关常量 ==========
/** IoT 场景联动触发器类型枚举 */
export const IotRuleSceneTriggerTypeEnum = {
DEVICE_STATE_UPDATE: 1, // 设备上下线变更
DEVICE_PROPERTY_POST: 2, // 物模型属性上报
DEVICE_EVENT_POST: 3, // 设备事件上报
DEVICE_SERVICE_INVOKE: 4, // 设备服务调用
TIMER: 100 // 定时触发
} as const
/** 触发器类型选项配置 */
export const getTriggerTypeOptions = () => [
{
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 const isDeviceTrigger = (type: number): boolean => {
const deviceTriggerTypes = [
IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
] as number[]
return deviceTriggerTypes.includes(type)
}