perf:【IoT 物联网】场景联动触发器优化
parent
c740da02b9
commit
a554bc5309
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:下面的 Props、Emits 可以合并到变量那;
|
// 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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 @puhui999:dict-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:下面的 Props、Emits 可以合并到变量那;
|
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>
|
||||||
|
|
|
||||||
|
|
@ -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:下面的 Props、Emits 可以合并到变量那;
|
// 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 @puhui999:updateTriggerDeviceConfig
|
// 事件处理函数
|
||||||
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 @puhui999:updateTriggerCronConfig
|
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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue