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

View File

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

View File

@ -114,11 +114,8 @@
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import ConditionConfig from './ConditionConfig.vue'
import {
ConditionGroupFormData,
ConditionFormData,
IotRuleSceneTriggerTypeEnum
} from '@/api/iot/rule/scene/scene.types'
import { ConditionFormData, ConditionGroupFormData } from '@/api/iot/rule/scene/scene.types'
import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
/** 条件组配置组件 */
defineOptions({ name: 'ConditionGroupConfig' })
@ -133,6 +130,7 @@ interface Props {
interface Emits {
(e: 'update:modelValue', value: ConditionGroupFormData): 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 ConditionGroupContainerConfig from './ConditionGroupContainerConfig.vue'
import {
TriggerFormData,
IotRuleSceneTriggerTypeEnum as TriggerTypeEnum
} from '@/api/iot/rule/scene/scene.types'
import { TriggerFormData } from '@/api/iot/rule/scene/scene.types'
import { IotRuleSceneTriggerTypeEnum as TriggerTypeEnum } from '@/views/iot/utils/constants'
/** 设备触发配置组件 */
defineOptions({ name: 'DeviceTriggerConfig' })
// TODO @puhui999 PropsEmits
interface Props {
// Props Emits
const props = defineProps<{
modelValue: TriggerFormData
}
}>()
interface Emits {
(e: 'update:modelValue', value: TriggerFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const emit = defineEmits<{
'update:modelValue': [value: TriggerFormData]
validate: [result: { valid: boolean; message: string }]
}>()
const trigger = useVModel(props, 'modelValue', emit)

View File

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

View File

@ -1,11 +1,5 @@
<template>
<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">
<!-- 产品设备选择 -->
@ -112,7 +106,7 @@ import OperatorSelector from '../selectors/OperatorSelector.vue'
import ValueInput from '../inputs/ValueInput.vue'
import DeviceStatusConditionConfig from './DeviceStatusConditionConfig.vue'
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'
/** 主条件内部配置组件 */
@ -125,6 +119,7 @@ interface Props {
interface Emits {
(e: 'update:modelValue', value: ConditionFormData): void
(e: 'validate', result: { valid: boolean; message: string }): void
}

View File

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

View File

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

View File

@ -15,26 +15,21 @@
>
<div class="flex items-center justify-between w-full py-4px">
<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-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 class="text-14px font-500 text-[var(--el-text-color-primary)]">
{{ 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 class="text-12px text-[var(--el-text-color-secondary)]">{{ operator.description }}</div>
</div>
</el-option>
</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>
</template>
@ -51,6 +46,7 @@ interface Props {
interface Emits {
(e: 'update:modelValue', value: string): void
(e: 'change', value: string): void
}

View File

@ -1,14 +1,14 @@
<!-- 属性选择器组件 -->
<!-- TODO @yunai可能要在 review -->
<template>
<div class="w-full">
<div class="flex items-center gap-8px">
<el-select
v-model="localValue"
placeholder="请选择监控项"
filterable
clearable
@change="handleChange"
class="w-full"
class="!w-150px"
:loading="loading"
>
<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-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ property.name }}</div>
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ property.identifier }}</div>
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">
{{ property.name }}
</div>
<div class="text-12px text-[var(--el-text-color-secondary)]">
{{ property.identifier }}
</div>
</div>
<div class="flex-shrink-0">
<el-tag :type="getPropertyTypeTag(property.dataType)" size="small">
@ -33,42 +37,98 @@
</el-option-group>
</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="flex items-center gap-8px mb-12px">
<Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-16px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ 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 class="relative">
<el-button
v-if="selectedProperty"
ref="detailTriggerRef"
type="info"
:icon="InfoFilled"
circle
size="small"
@click="togglePropertyDetail"
class="flex-shrink-0"
title="查看属性详情"
/>
<!-- 属性详情弹出层 -->
<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 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>
</Teleport>
</div>
</div>
</template>
<script setup lang="ts">
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 { IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
import type { IotThingModelTSLRespVO, PropertySelectorItem } from './types'
/** 属性选择器组件 */
@ -83,6 +143,7 @@ interface Props {
interface Emits {
(e: 'update:modelValue', value: string): void
(e: 'change', value: { type: string; config: any }): void
}
@ -96,6 +157,25 @@ const loading = ref(false)
const propertyList = ref<PropertySelectorItem[]>([])
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 groups: { label: string; options: any[] }[] = []
@ -159,6 +239,67 @@ const getPropertyTypeTag = (dataType: string) => {
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 property = propertyList.value.find((p) => p.identifier === value)
@ -168,6 +309,8 @@ const handleChange = (value: string) => {
config: property
})
}
//
hidePropertyDetail()
}
// TSL
@ -331,13 +474,74 @@ watch(
() => props.triggerType,
() => {
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>
<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) {
height: auto;
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>

View File

@ -203,3 +203,49 @@ export const IoTOtaTaskRecordStatusEnum = {
value: 50
}
} 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)
}