commit
e53a676bf6
|
|
@ -1,5 +1,5 @@
|
||||||
import request from '@/config/axios'
|
import request from '@/config/axios'
|
||||||
import { IotRuleScene } from './scene.types'
|
import { IotSceneRule } from './scene.types'
|
||||||
|
|
||||||
// IoT 场景联动 API
|
// IoT 场景联动 API
|
||||||
export const RuleSceneApi = {
|
export const RuleSceneApi = {
|
||||||
|
|
@ -14,21 +14,24 @@ export const RuleSceneApi = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// 新增场景联动
|
// 新增场景联动
|
||||||
createRuleScene: async (data: IotRuleScene) => {
|
createRuleScene: async (data: IotSceneRule) => {
|
||||||
return await request.post({ url: `/iot/rule-scene/create`, data })
|
return await request.post({ url: `/iot/rule-scene/create`, data })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 修改场景联动
|
// 修改场景联动
|
||||||
updateRuleScene: async (data: IotRuleScene) => {
|
updateRuleScene: async (data: IotSceneRule) => {
|
||||||
return await request.put({ url: `/iot/rule-scene/update`, data })
|
return await request.put({ url: `/iot/rule-scene/update`, data })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 修改场景联动
|
// 修改场景联动
|
||||||
updateRuleSceneStatus: async (id: number, status: number) => {
|
updateRuleSceneStatus: async (id: number, status: number) => {
|
||||||
return await request.put({ url: `/iot/rule-scene/update-status`, data: {
|
return await request.put({
|
||||||
id,
|
url: `/iot/rule-scene/update-status`,
|
||||||
status
|
data: {
|
||||||
}})
|
id,
|
||||||
|
status
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// 删除场景联动
|
// 删除场景联动
|
||||||
|
|
|
||||||
|
|
@ -137,122 +137,18 @@ export interface PropertySelectorItem {
|
||||||
|
|
||||||
// ========== 场景联动规则相关接口定义 ==========
|
// ========== 场景联动规则相关接口定义 ==========
|
||||||
|
|
||||||
// 基础接口(如果项目中有全局的 BaseDO,可以使用全局的)
|
|
||||||
interface TenantBaseDO {
|
|
||||||
createTime?: Date // 创建时间
|
|
||||||
updateTime?: Date // 更新时间
|
|
||||||
creator?: string // 创建者
|
|
||||||
updater?: string // 更新者
|
|
||||||
deleted?: boolean // 是否删除
|
|
||||||
tenantId?: number // 租户编号
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触发条件参数
|
|
||||||
interface TriggerConditionParameter {
|
|
||||||
identifier0?: string // 标识符(事件、服务)
|
|
||||||
identifier?: string // 标识符(属性)
|
|
||||||
operator: string // 操作符(必填)
|
|
||||||
value: string // 比较值(必填,多值用逗号分隔)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触发条件
|
|
||||||
interface TriggerCondition {
|
|
||||||
type: string // 消息类型
|
|
||||||
identifier: string // 消息标识符
|
|
||||||
parameters: TriggerConditionParameter[] // 参数数组
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触发器配置
|
|
||||||
interface TriggerConfig {
|
|
||||||
key?: string // 组件唯一标识符,用于解决索引重用问题
|
|
||||||
type: number // 触发类型(必填)
|
|
||||||
productKey?: string // 产品标识(设备触发时必填)
|
|
||||||
deviceNames?: string[] // 设备名称数组(设备触发时必填)
|
|
||||||
conditions?: TriggerCondition[] // 触发条件数组(设备触发时必填)
|
|
||||||
cronExpression?: string // CRON表达式(定时触发时必填)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行设备控制
|
|
||||||
interface ActionDeviceControl {
|
|
||||||
productKey: string // 产品标识(必填)
|
|
||||||
deviceNames: string[] // 设备名称数组(必填)
|
|
||||||
type: string // 消息类型(必填)
|
|
||||||
identifier: string // 消息标识符(必填)
|
|
||||||
params: Record<string, any> // 参数对象(必填)- 统一使用 params 字段
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行器配置
|
|
||||||
interface ActionConfig {
|
|
||||||
key?: string // 组件唯一标识符,用于解决索引重用问题
|
|
||||||
type: number // 执行类型(必填)
|
|
||||||
deviceControl?: ActionDeviceControl // 设备控制(设备控制时必填)
|
|
||||||
alertConfigId?: number // 告警配置ID(告警恢复时必填)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 表单数据接口 - 直接对应后端 DO 结构
|
|
||||||
interface RuleSceneFormData {
|
|
||||||
id?: number
|
|
||||||
name: string
|
|
||||||
description?: string
|
|
||||||
status: number
|
|
||||||
triggers: TriggerFormData[] // 支持多个触发器
|
|
||||||
actions: ActionFormData[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触发器表单数据 - 直接对应 TriggerDO
|
|
||||||
interface TriggerFormData {
|
|
||||||
type: number // 触发类型
|
|
||||||
productId?: number // 产品编号
|
|
||||||
deviceId?: number // 设备编号
|
|
||||||
identifier?: string // 物模型标识符
|
|
||||||
operator?: string // 操作符
|
|
||||||
value?: string // 参数值
|
|
||||||
cronExpression?: string // CRON 表达式
|
|
||||||
conditionGroups?: TriggerConditionFormData[][] // 条件组(二维数组)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触发条件表单数据 - 直接对应 TriggerConditionDO
|
|
||||||
interface TriggerConditionFormData {
|
|
||||||
type: number // 条件类型:1-设备状态,2-设备属性,3-当前时间
|
|
||||||
productId?: number // 产品编号
|
|
||||||
deviceId?: number // 设备编号
|
|
||||||
identifier?: string // 标识符
|
|
||||||
operator: string // 操作符
|
|
||||||
param: string // 参数值
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行器表单数据 - 直接对应 ActionDO
|
|
||||||
interface ActionFormData {
|
|
||||||
type: number // 执行类型
|
|
||||||
productId?: number // 产品编号
|
|
||||||
deviceId?: number // 设备编号
|
|
||||||
identifier?: string // 物模型标识符(服务调用时使用)
|
|
||||||
params?: Record<string, any> // 请求参数
|
|
||||||
alertConfigId?: number // 告警配置编号
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主接口 - 原有的 API 接口格式(保持兼容性)
|
|
||||||
interface IotRuleScene extends TenantBaseDO {
|
|
||||||
id?: number // 场景编号(新增时为空)
|
|
||||||
name: string // 场景名称(必填)
|
|
||||||
description?: string // 场景描述(可选)
|
|
||||||
status: number // 场景状态:0-开启,1-关闭
|
|
||||||
triggers: TriggerConfig[] // 触发器数组(必填,至少一个)
|
|
||||||
actions: ActionConfig[] // 执行器数组(必填,至少一个)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 后端 DO 接口 - 匹配后端数据结构
|
// 后端 DO 接口 - 匹配后端数据结构
|
||||||
interface IotRuleSceneDO {
|
interface IotSceneRule {
|
||||||
id?: number // 场景编号
|
id?: number // 场景编号
|
||||||
name: string // 场景名称
|
name: string // 场景名称
|
||||||
description?: string // 场景描述
|
description?: string // 场景描述
|
||||||
status: number // 场景状态:0-开启,1-关闭
|
status: number // 场景状态:0-开启,1-关闭
|
||||||
triggers: TriggerDO[] // 触发器数组
|
triggers: Trigger[] // 触发器数组
|
||||||
actions: ActionDO[] // 执行器数组
|
actions: Action[] // 执行器数组
|
||||||
}
|
}
|
||||||
|
|
||||||
// 触发器 DO 结构
|
// 触发器 DO 结构
|
||||||
interface TriggerDO {
|
interface Trigger {
|
||||||
type: number // 触发类型
|
type: number // 触发类型
|
||||||
productId?: number // 产品编号
|
productId?: number // 产品编号
|
||||||
deviceId?: number // 设备编号
|
deviceId?: number // 设备编号
|
||||||
|
|
@ -260,12 +156,12 @@ interface TriggerDO {
|
||||||
operator?: string // 操作符
|
operator?: string // 操作符
|
||||||
value?: string // 参数值
|
value?: string // 参数值
|
||||||
cronExpression?: string // CRON 表达式
|
cronExpression?: string // CRON 表达式
|
||||||
conditionGroups?: TriggerConditionDO[][] // 条件组(二维数组)
|
conditionGroups?: TriggerCondition[][] // 条件组(二维数组)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 触发条件 DO 结构
|
// 触发条件 DO 结构
|
||||||
interface TriggerConditionDO {
|
interface TriggerCondition {
|
||||||
type: number // 条件类型
|
type: number // 条件类型:1-设备状态,2-设备属性,3-当前时间
|
||||||
productId?: number // 产品编号
|
productId?: number // 产品编号
|
||||||
deviceId?: number // 设备编号
|
deviceId?: number // 设备编号
|
||||||
identifier?: string // 标识符
|
identifier?: string // 标识符
|
||||||
|
|
@ -274,12 +170,12 @@ interface TriggerConditionDO {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行器 DO 结构
|
// 执行器 DO 结构
|
||||||
interface ActionDO {
|
interface Action {
|
||||||
type: number // 执行类型
|
type: number // 执行类型
|
||||||
productId?: number // 产品编号
|
productId?: number // 产品编号
|
||||||
deviceId?: number // 设备编号
|
deviceId?: number // 设备编号
|
||||||
identifier?: string // 物模型标识符(服务调用时使用)
|
identifier?: string // 物模型标识符(服务调用时使用)
|
||||||
params?: Record<string, any> // 请求参数
|
params?: string // 请求参数
|
||||||
alertConfigId?: number // 告警配置编号
|
alertConfigId?: number // 告警配置编号
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,23 +194,9 @@ interface FormValidationRules {
|
||||||
[key: string]: ValidationRule[]
|
[key: string]: ValidationRule[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 表单数据类型别名
|
||||||
|
export type TriggerFormData = Trigger
|
||||||
|
|
||||||
// TODO @puhui999:这个文件,目标最终没有哈,和别的模块一致;
|
// TODO @puhui999:这个文件,目标最终没有哈,和别的模块一致;
|
||||||
|
|
||||||
export {
|
export { IotSceneRule, Trigger, TriggerCondition, Action, ValidationRule, FormValidationRules }
|
||||||
IotRuleScene,
|
|
||||||
IotRuleSceneDO,
|
|
||||||
TriggerDO,
|
|
||||||
TriggerConditionDO,
|
|
||||||
ActionDO,
|
|
||||||
TriggerConfig,
|
|
||||||
TriggerCondition,
|
|
||||||
TriggerConditionParameter,
|
|
||||||
ActionConfig,
|
|
||||||
ActionDeviceControl,
|
|
||||||
RuleSceneFormData,
|
|
||||||
TriggerFormData,
|
|
||||||
TriggerConditionFormData,
|
|
||||||
ActionFormData,
|
|
||||||
ValidationRule,
|
|
||||||
FormValidationRules
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ import { useVModel } from '@vueuse/core'
|
||||||
import BasicInfoSection from './sections/BasicInfoSection.vue'
|
import BasicInfoSection from './sections/BasicInfoSection.vue'
|
||||||
import TriggerSection from './sections/TriggerSection.vue'
|
import TriggerSection from './sections/TriggerSection.vue'
|
||||||
import ActionSection from './sections/ActionSection.vue'
|
import ActionSection from './sections/ActionSection.vue'
|
||||||
import { IotRuleSceneDO, RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
|
import { IotSceneRule } from '@/api/iot/rule/scene/scene.types'
|
||||||
import { RuleSceneApi } from '@/api/iot/rule/scene'
|
import { RuleSceneApi } from '@/api/iot/rule/scene'
|
||||||
import {
|
import {
|
||||||
IotRuleSceneTriggerTypeEnum,
|
IotRuleSceneTriggerTypeEnum,
|
||||||
|
|
@ -54,7 +54,7 @@ const props = defineProps<{
|
||||||
/** 抽屉显示状态 */
|
/** 抽屉显示状态 */
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
/** 编辑的场景联动规则数据 */
|
/** 编辑的场景联动规则数据 */
|
||||||
ruleScene?: IotRuleSceneDO
|
ruleScene?: IotSceneRule
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
/** 组件事件定义 */
|
/** 组件事件定义 */
|
||||||
|
|
@ -66,7 +66,7 @@ const emit = defineEmits<{
|
||||||
const drawerVisible = useVModel(props, 'modelValue', emit) // 是否可见
|
const drawerVisible = useVModel(props, 'modelValue', emit) // 是否可见
|
||||||
|
|
||||||
/** 创建默认的表单数据 */
|
/** 创建默认的表单数据 */
|
||||||
const createDefaultFormData = (): RuleSceneFormData => {
|
const createDefaultFormData = (): IotSceneRule => {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
|
@ -89,7 +89,7 @@ const createDefaultFormData = (): RuleSceneFormData => {
|
||||||
|
|
||||||
// 表单数据和状态
|
// 表单数据和状态
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const formData = ref<RuleSceneFormData>(createDefaultFormData())
|
const formData = ref<IotSceneRule>(createDefaultFormData())
|
||||||
// 自定义校验器
|
// 自定义校验器
|
||||||
const validateTriggers = (_rule: any, value: any, callback: any) => {
|
const validateTriggers = (_rule: any, value: any, callback: any) => {
|
||||||
if (!value || !Array.isArray(value) || value.length === 0) {
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -1,262 +1,81 @@
|
||||||
<!-- 告警配置组件 -->
|
<!-- 告警配置组件 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<!-- 告警配置选择区域 -->
|
<el-form-item label="告警配置" required>
|
||||||
<div
|
<el-select
|
||||||
class="border border-[var(--el-border-color-light)] rounded-6px p-16px bg-[var(--el-fill-color-blank)]"
|
v-model="localValue"
|
||||||
>
|
placeholder="请选择告警配置"
|
||||||
<div class="flex items-center gap-8px mb-12px">
|
filterable
|
||||||
<Icon icon="ep:bell" class="text-[var(--el-color-warning)] text-16px" />
|
clearable
|
||||||
<span class="text-14px font-600 text-[var(--el-text-color-primary)]">告警配置选择</span>
|
@change="handleChange"
|
||||||
<el-tag size="small" type="warning">必选</el-tag>
|
class="w-full"
|
||||||
</div>
|
:loading="loading"
|
||||||
|
>
|
||||||
<el-form-item label="告警配置" required>
|
<el-option
|
||||||
<el-select
|
v-for="config in alertConfigs"
|
||||||
v-model="localValue"
|
:key="config.id"
|
||||||
placeholder="请选择告警配置"
|
:label="config.name"
|
||||||
filterable
|
:value="config.id"
|
||||||
clearable
|
|
||||||
@change="handleChange"
|
|
||||||
class="w-full"
|
|
||||||
:loading="loading"
|
|
||||||
>
|
>
|
||||||
<template #empty>
|
<div class="flex items-center justify-between">
|
||||||
<div class="text-center py-20px">
|
<span>{{ config.name }}</span>
|
||||||
<Icon
|
<el-tag :type="config.enabled ? 'success' : 'danger'" size="small">
|
||||||
icon="ep:warning"
|
{{ config.enabled ? '启用' : '禁用' }}
|
||||||
class="text-24px text-[var(--el-text-color-placeholder)] mb-8px"
|
</el-tag>
|
||||||
/>
|
|
||||||
<p class="text-12px text-[var(--el-text-color-secondary)]">暂无可用的告警配置</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<el-option
|
|
||||||
v-for="config in alertConfigs"
|
|
||||||
:key="config.id"
|
|
||||||
:label="config.name"
|
|
||||||
:value="config.id"
|
|
||||||
:disabled="!config.enabled"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between w-full py-6px">
|
|
||||||
<div class="flex items-center gap-12px flex-1">
|
|
||||||
<Icon
|
|
||||||
:icon="config.enabled ? 'ep:circle-check' : 'ep:circle-close'"
|
|
||||||
:class="
|
|
||||||
config.enabled
|
|
||||||
? 'text-[var(--el-color-success)]'
|
|
||||||
: 'text-[var(--el-color-danger)]'
|
|
||||||
"
|
|
||||||
class="text-16px flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{
|
|
||||||
config.name
|
|
||||||
}}</div>
|
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)] line-clamp-1">{{
|
|
||||||
config.description
|
|
||||||
}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-8px">
|
|
||||||
<el-tag :type="getNotifyTypeTag(config.notifyType)" size="small">
|
|
||||||
{{ getNotifyTypeName(config.notifyType) }}
|
|
||||||
</el-tag>
|
|
||||||
<el-tag :type="config.enabled ? 'success' : 'danger'" size="small">
|
|
||||||
{{ config.enabled ? '启用' : '禁用' }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-option>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 告警配置详情 -->
|
|
||||||
<div
|
|
||||||
v-if="selectedConfig"
|
|
||||||
class="mt-16px border border-[var(--el-border-color-light)] rounded-6px p-16px bg-gradient-to-r from-orange-50 to-yellow-50"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-8px mb-16px">
|
|
||||||
<Icon icon="ep:info-filled" class="text-[var(--el-color-warning)] text-18px" />
|
|
||||||
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">配置详情</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-16px">
|
|
||||||
<!-- 基本信息 -->
|
|
||||||
<div class="space-y-12px">
|
|
||||||
<div class="flex items-center gap-8px">
|
|
||||||
<Icon icon="ep:document" class="text-[var(--el-color-primary)] text-14px" />
|
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">基本信息</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-22px space-y-8px">
|
</el-option>
|
||||||
<div class="flex items-start gap-8px">
|
</el-select>
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">名称:</span>
|
</el-form-item>
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1 font-500">{{
|
|
||||||
selectedConfig.name
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-start gap-8px">
|
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">描述:</span>
|
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{
|
|
||||||
selectedConfig.description
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-start gap-8px">
|
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">状态:</span>
|
|
||||||
<el-tag :type="selectedConfig.enabled ? 'success' : 'danger'" size="small">
|
|
||||||
{{ selectedConfig.enabled ? '启用' : '禁用' }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 通知配置 -->
|
|
||||||
<div class="space-y-12px">
|
|
||||||
<div class="flex items-center gap-8px">
|
|
||||||
<Icon icon="ep:message" class="text-[var(--el-color-success)] text-14px" />
|
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">通知配置</span>
|
|
||||||
</div>
|
|
||||||
<div class="pl-22px space-y-8px">
|
|
||||||
<div class="flex items-start gap-8px">
|
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">方式:</span>
|
|
||||||
<el-tag :type="getNotifyTypeTag(selectedConfig.notifyType)" size="small">
|
|
||||||
{{ getNotifyTypeName(selectedConfig.notifyType) }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="selectedConfig.receivers && selectedConfig.receivers.length > 0"
|
|
||||||
class="flex items-start gap-8px"
|
|
||||||
>
|
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px"
|
|
||||||
>接收人:</span
|
|
||||||
>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="flex flex-wrap gap-4px">
|
|
||||||
<el-tag
|
|
||||||
v-for="receiver in selectedConfig.receivers.slice(0, 3)"
|
|
||||||
:key="receiver"
|
|
||||||
size="small"
|
|
||||||
type="info"
|
|
||||||
>
|
|
||||||
{{ receiver }}
|
|
||||||
</el-tag>
|
|
||||||
<el-tag v-if="selectedConfig.receivers.length > 3" size="small" type="info">
|
|
||||||
+{{ selectedConfig.receivers.length - 3 }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import { AlertConfigApi } from '@/api/iot/alert/config'
|
||||||
|
|
||||||
/** 告警配置组件 */
|
/** 告警配置组件 */
|
||||||
defineOptions({ name: 'AlertConfig' })
|
defineOptions({ name: 'AlertConfig' })
|
||||||
|
|
||||||
interface Props {
|
const props = defineProps<{
|
||||||
modelValue?: number
|
modelValue?: number
|
||||||
}
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value?: number): void
|
(e: 'update:modelValue', value?: number): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const localValue = useVModel(props, 'modelValue', emit)
|
const localValue = useVModel(props, 'modelValue', emit)
|
||||||
|
|
||||||
// 状态
|
const loading = ref(false) // 加载状态
|
||||||
const loading = ref(false)
|
const alertConfigs = ref<any[]>([]) // 告警配置列表
|
||||||
const alertConfigs = ref<any[]>([])
|
|
||||||
|
|
||||||
// 计算属性
|
/**
|
||||||
const selectedConfig = computed(() => {
|
* 处理选择变化事件
|
||||||
return alertConfigs.value.find((config) => config.id === localValue.value)
|
* @param value 选中的值
|
||||||
})
|
*/
|
||||||
|
|
||||||
// 工具函数
|
|
||||||
const getNotifyTypeName = (type: number) => {
|
|
||||||
const typeMap = {
|
|
||||||
1: '邮件通知',
|
|
||||||
2: '短信通知',
|
|
||||||
3: '微信通知',
|
|
||||||
4: '钉钉通知'
|
|
||||||
}
|
|
||||||
return typeMap[type] || '未知'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNotifyTypeTag = (type: number) => {
|
|
||||||
const tagMap = {
|
|
||||||
1: 'primary', // 邮件
|
|
||||||
2: 'success', // 短信
|
|
||||||
3: 'warning', // 微信
|
|
||||||
4: 'info' // 钉钉
|
|
||||||
}
|
|
||||||
return tagMap[type] || 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 事件处理
|
|
||||||
const handleChange = (value?: number) => {
|
const handleChange = (value?: number) => {
|
||||||
// 可以在这里添加额外的处理逻辑
|
emit('update:modelValue', value)
|
||||||
console.log('告警配置选择变化:', value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// API 调用
|
/**
|
||||||
const getAlertConfigs = async () => {
|
* 加载告警配置列表
|
||||||
|
*/
|
||||||
|
const loadAlertConfigs = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// 这里应该调用真实的API获取告警配置
|
const data = await AlertConfigApi.getAlertConfigPage({
|
||||||
// 暂时使用模拟数据
|
pageNo: 1,
|
||||||
// TODO @puhui999:这里是模拟数据
|
pageSize: 100,
|
||||||
alertConfigs.value = [
|
enabled: true // 只加载启用的配置
|
||||||
{
|
})
|
||||||
id: 1,
|
alertConfigs.value = data.list || []
|
||||||
name: '设备异常告警',
|
|
||||||
description: '设备状态异常时发送告警',
|
|
||||||
enabled: true,
|
|
||||||
notifyType: 1,
|
|
||||||
receivers: ['admin@example.com', 'operator@example.com']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: '温度超限告警',
|
|
||||||
description: '温度超过阈值时发送告警',
|
|
||||||
enabled: true,
|
|
||||||
notifyType: 2,
|
|
||||||
receivers: ['13800138000', '13900139000']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: '系统故障告警',
|
|
||||||
description: '系统发生故障时发送告警',
|
|
||||||
enabled: false,
|
|
||||||
notifyType: 3,
|
|
||||||
receivers: ['技术支持群']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取告警配置失败:', error)
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化
|
// 组件挂载时加载数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getAlertConfigs()
|
loadAlertConfigs()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
:deep(.el-select-dropdown__item) {
|
|
||||||
height: auto;
|
|
||||||
padding: 8px 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ import DeviceSelector from '../selectors/DeviceSelector.vue'
|
||||||
import PropertySelector from '../selectors/PropertySelector.vue'
|
import PropertySelector from '../selectors/PropertySelector.vue'
|
||||||
import OperatorSelector from '../selectors/OperatorSelector.vue'
|
import OperatorSelector from '../selectors/OperatorSelector.vue'
|
||||||
import ValueInput from '../inputs/ValueInput.vue'
|
import ValueInput from '../inputs/ValueInput.vue'
|
||||||
import { TriggerConditionFormData } from '@/api/iot/rule/scene/scene.types'
|
import { TriggerCondition } from '@/api/iot/rule/scene/scene.types'
|
||||||
import {
|
import {
|
||||||
IotRuleSceneTriggerConditionTypeEnum,
|
IotRuleSceneTriggerConditionTypeEnum,
|
||||||
IotRuleSceneTriggerConditionParameterOperatorEnum
|
IotRuleSceneTriggerConditionParameterOperatorEnum
|
||||||
|
|
@ -133,12 +133,12 @@ import {
|
||||||
defineOptions({ name: 'ConditionConfig' })
|
defineOptions({ name: 'ConditionConfig' })
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: TriggerConditionFormData
|
modelValue: TriggerCondition
|
||||||
triggerType: number
|
triggerType: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: TriggerConditionFormData): void
|
(e: 'update:modelValue', value: TriggerCondition): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
@ -155,12 +155,12 @@ const isValid = ref(true)
|
||||||
const valueValidation = ref({ valid: true, message: '' })
|
const valueValidation = ref({ valid: true, message: '' })
|
||||||
|
|
||||||
// 事件处理
|
// 事件处理
|
||||||
const updateConditionField = (field: keyof TriggerConditionFormData, value: any) => {
|
const updateConditionField = (field: keyof TriggerCondition, value: any) => {
|
||||||
;(condition.value as any)[field] = value
|
;(condition.value as any)[field] = value
|
||||||
emit('update:modelValue', condition.value)
|
emit('update:modelValue', condition.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCondition = (newCondition: TriggerConditionFormData) => {
|
const updateCondition = (newCondition: TriggerCondition) => {
|
||||||
condition.value = newCondition
|
condition.value = newCondition
|
||||||
emit('update:modelValue', condition.value)
|
emit('update:modelValue', condition.value)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -84,17 +84,17 @@
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import ProductSelector from '../selectors/ProductSelector.vue'
|
import ProductSelector from '../selectors/ProductSelector.vue'
|
||||||
import DeviceSelector from '../selectors/DeviceSelector.vue'
|
import DeviceSelector from '../selectors/DeviceSelector.vue'
|
||||||
import { TriggerConditionFormData } from '@/api/iot/rule/scene/scene.types'
|
import { TriggerCondition } from '@/api/iot/rule/scene/scene.types'
|
||||||
|
|
||||||
/** 设备状态条件配置组件 */
|
/** 设备状态条件配置组件 */
|
||||||
defineOptions({ name: 'DeviceStatusConditionConfig' })
|
defineOptions({ name: 'DeviceStatusConditionConfig' })
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: TriggerConditionFormData
|
modelValue: TriggerCondition
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: TriggerConditionFormData): void
|
(e: 'update:modelValue', value: TriggerCondition): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,14 @@
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<!-- 操作符选择 - 服务调用不需要操作符 -->
|
<!-- 操作符选择 - 服务调用和事件上报不需要操作符 -->
|
||||||
<el-col v-if="triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE" :span="6">
|
<el-col
|
||||||
|
v-if="
|
||||||
|
triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE &&
|
||||||
|
triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
|
||||||
|
"
|
||||||
|
:span="6"
|
||||||
|
>
|
||||||
<el-form-item label="操作符" required>
|
<el-form-item label="操作符" required>
|
||||||
<OperatorSelector
|
<OperatorSelector
|
||||||
:model-value="condition.operator"
|
:model-value="condition.operator"
|
||||||
|
|
@ -72,7 +78,14 @@
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<!-- 值输入 -->
|
<!-- 值输入 -->
|
||||||
<el-col :span="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE ? 18 : 12">
|
<el-col
|
||||||
|
:span="
|
||||||
|
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE ||
|
||||||
|
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
|
||||||
|
? 18
|
||||||
|
: 12
|
||||||
|
"
|
||||||
|
>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
:label="
|
:label="
|
||||||
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||||
|
|
@ -82,11 +95,21 @@
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<!-- 服务调用参数配置 -->
|
<!-- 服务调用参数配置 -->
|
||||||
<ServiceParamsInput
|
<JsonParamsInput
|
||||||
v-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE"
|
v-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE"
|
||||||
:model-value="condition.value"
|
v-model="condition.value"
|
||||||
@update:model-value="(value) => updateConditionField('value', value)"
|
type="service"
|
||||||
:service-config="propertyConfig"
|
:config="serviceConfig"
|
||||||
|
placeholder="请输入JSON格式的服务参数"
|
||||||
|
@validate="handleValueValidate"
|
||||||
|
/>
|
||||||
|
<!-- 事件上报参数配置 -->
|
||||||
|
<JsonParamsInput
|
||||||
|
v-else-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST"
|
||||||
|
v-model="condition.value"
|
||||||
|
type="event"
|
||||||
|
:config="eventConfig"
|
||||||
|
placeholder="请输入JSON格式的事件参数"
|
||||||
@validate="handleValueValidate"
|
@validate="handleValueValidate"
|
||||||
/>
|
/>
|
||||||
<!-- 普通值输入 -->
|
<!-- 普通值输入 -->
|
||||||
|
|
@ -106,11 +129,44 @@
|
||||||
|
|
||||||
<!-- 设备状态条件配置 -->
|
<!-- 设备状态条件配置 -->
|
||||||
<div v-else-if="isDeviceStatusTrigger" class="space-y-16px">
|
<div v-else-if="isDeviceStatusTrigger" class="space-y-16px">
|
||||||
<DeviceStatusConditionConfig
|
<!-- 设备状态触发器使用简化的配置 -->
|
||||||
:model-value="condition"
|
<el-row :gutter="16">
|
||||||
@update:model-value="updateCondition"
|
<el-col :span="12">
|
||||||
@validate="handleValidate"
|
<el-form-item label="产品" required>
|
||||||
/>
|
<ProductSelector
|
||||||
|
:model-value="condition.productId"
|
||||||
|
@update:model-value="(value) => updateConditionField('productId', value)"
|
||||||
|
@change="handleProductChange"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="设备" required>
|
||||||
|
<DeviceSelector
|
||||||
|
:model-value="condition.deviceId"
|
||||||
|
@update:model-value="(value) => updateConditionField('deviceId', value)"
|
||||||
|
:product-id="condition.productId"
|
||||||
|
@change="handleDeviceChange"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="操作符" required>
|
||||||
|
<el-select
|
||||||
|
:model-value="condition.operator"
|
||||||
|
@update:model-value="(value) => updateConditionField('operator', value)"
|
||||||
|
placeholder="请选择操作符"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<el-option label="变为在线" value="online" />
|
||||||
|
<el-option label="变为离线" value="offline" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 其他触发类型的提示 -->
|
<!-- 其他触发类型的提示 -->
|
||||||
|
|
@ -131,8 +187,8 @@ import DeviceSelector from '../selectors/DeviceSelector.vue'
|
||||||
import PropertySelector from '../selectors/PropertySelector.vue'
|
import PropertySelector from '../selectors/PropertySelector.vue'
|
||||||
import OperatorSelector from '../selectors/OperatorSelector.vue'
|
import OperatorSelector from '../selectors/OperatorSelector.vue'
|
||||||
import ValueInput from '../inputs/ValueInput.vue'
|
import ValueInput from '../inputs/ValueInput.vue'
|
||||||
import ServiceParamsInput from '../inputs/ServiceParamsInput.vue'
|
import JsonParamsInput from '../inputs/JsonParamsInput.vue'
|
||||||
import DeviceStatusConditionConfig from './DeviceStatusConditionConfig.vue'
|
|
||||||
import { TriggerFormData } from '@/api/iot/rule/scene/scene.types'
|
import { TriggerFormData } from '@/api/iot/rule/scene/scene.types'
|
||||||
import { IotRuleSceneTriggerTypeEnum, getTriggerTypeOptions } from '@/views/iot/utils/constants'
|
import { IotRuleSceneTriggerTypeEnum, getTriggerTypeOptions } from '@/views/iot/utils/constants'
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
|
|
@ -172,6 +228,35 @@ const isDeviceStatusTrigger = computed(() => {
|
||||||
return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE
|
return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 服务配置 - 用于 JsonParamsInput
|
||||||
|
const serviceConfig = computed(() => {
|
||||||
|
if (
|
||||||
|
propertyConfig.value &&
|
||||||
|
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
service: {
|
||||||
|
name: propertyConfig.value.name || '服务',
|
||||||
|
inputParams: propertyConfig.value.inputParams || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// 事件配置 - 用于 JsonParamsInput
|
||||||
|
const eventConfig = computed(() => {
|
||||||
|
if (propertyConfig.value && props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
|
||||||
|
return {
|
||||||
|
event: {
|
||||||
|
name: propertyConfig.value.name || '事件',
|
||||||
|
outputParams: propertyConfig.value.outputParams || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
// 获取触发类型文本
|
// 获取触发类型文本
|
||||||
// TODO @puhui999:是不是有枚举可以服用哈;
|
// TODO @puhui999:是不是有枚举可以服用哈;
|
||||||
const getTriggerTypeText = (type: number) => {
|
const getTriggerTypeText = (type: number) => {
|
||||||
|
|
@ -198,11 +283,6 @@ const updateConditionField = (field: keyof TriggerFormData, value: any) => {
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCondition = (value: TriggerFormData) => {
|
|
||||||
emit('update:modelValue', value)
|
|
||||||
updateValidationResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTriggerTypeChange = (type: number) => {
|
const handleTriggerTypeChange = (type: number) => {
|
||||||
emit('trigger-type-change', type)
|
emit('trigger-type-change', type)
|
||||||
}
|
}
|
||||||
|
|
@ -224,6 +304,14 @@ const handlePropertyChange = (propertyInfo: any) => {
|
||||||
if (propertyInfo) {
|
if (propertyInfo) {
|
||||||
propertyType.value = propertyInfo.type
|
propertyType.value = propertyInfo.type
|
||||||
propertyConfig.value = propertyInfo.config
|
propertyConfig.value = propertyInfo.config
|
||||||
|
|
||||||
|
// 对于事件上报和服务调用,自动设置操作符为 '='
|
||||||
|
if (
|
||||||
|
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
|
||||||
|
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||||
|
) {
|
||||||
|
condition.value.operator = '='
|
||||||
|
}
|
||||||
}
|
}
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
}
|
}
|
||||||
|
|
@ -232,14 +320,12 @@ const handleOperatorChange = () => {
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleValueValidate = (_result: { valid: boolean; message: string }) => {
|
// 处理参数验证结果
|
||||||
updateValidationResult()
|
const handleValueValidate = (result: { valid: boolean; message: string }) => {
|
||||||
}
|
|
||||||
|
|
||||||
const handleValidate = (result: { valid: boolean; message: string }) => {
|
|
||||||
isValid.value = result.valid
|
isValid.value = result.valid
|
||||||
validationMessage.value = result.message
|
validationMessage.value = result.message
|
||||||
emit('validate', result)
|
emit('validate', result)
|
||||||
|
updateValidationResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证逻辑
|
// 验证逻辑
|
||||||
|
|
@ -268,9 +354,10 @@ const updateValidationResult = () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务调用不需要操作符
|
// 服务调用和事件上报不需要操作符
|
||||||
if (
|
if (
|
||||||
props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE &&
|
props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE &&
|
||||||
|
props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST &&
|
||||||
!condition.value.operator
|
!condition.value.operator
|
||||||
) {
|
) {
|
||||||
isValid.value = false
|
isValid.value = false
|
||||||
|
|
@ -298,8 +385,9 @@ watch(
|
||||||
condition.value.productId,
|
condition.value.productId,
|
||||||
condition.value.deviceId,
|
condition.value.deviceId,
|
||||||
condition.value.identifier,
|
condition.value.identifier,
|
||||||
// 服务调用不需要监听操作符
|
// 服务调用和事件上报不需要监听操作符
|
||||||
props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE &&
|
||||||
|
props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
|
||||||
? condition.value.operator
|
? condition.value.operator
|
||||||
: null,
|
: null,
|
||||||
condition.value.value
|
condition.value.value
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import ConditionConfig from './ConditionConfig.vue'
|
import ConditionConfig from './ConditionConfig.vue'
|
||||||
import { TriggerConditionFormData } from '@/api/iot/rule/scene/scene.types'
|
import { TriggerCondition } from '@/api/iot/rule/scene/scene.types'
|
||||||
import {
|
import {
|
||||||
IotRuleSceneTriggerConditionTypeEnum,
|
IotRuleSceneTriggerConditionTypeEnum,
|
||||||
IotRuleSceneTriggerConditionParameterOperatorEnum
|
IotRuleSceneTriggerConditionParameterOperatorEnum
|
||||||
|
|
@ -93,13 +93,13 @@ import {
|
||||||
defineOptions({ name: 'SubConditionGroupConfig' })
|
defineOptions({ name: 'SubConditionGroupConfig' })
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: TriggerConditionFormData[]
|
modelValue: TriggerCondition[]
|
||||||
triggerType: number
|
triggerType: number
|
||||||
maxConditions?: number
|
maxConditions?: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: TriggerConditionFormData[]): void
|
(e: 'update:modelValue', value: TriggerCondition[]): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
@ -123,7 +123,7 @@ const addCondition = () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCondition: TriggerConditionFormData = {
|
const newCondition: TriggerCondition = {
|
||||||
type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
|
type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
|
||||||
productId: undefined,
|
productId: undefined,
|
||||||
deviceId: undefined,
|
deviceId: undefined,
|
||||||
|
|
@ -161,7 +161,7 @@ const removeCondition = (index: number) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCondition = (index: number, condition: TriggerConditionFormData) => {
|
const updateCondition = (index: number, condition: TriggerCondition) => {
|
||||||
if (subGroup.value) {
|
if (subGroup.value) {
|
||||||
subGroup.value[index] = condition
|
subGroup.value[index] = condition
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,526 @@
|
||||||
|
<!-- JSON参数输入组件 - 通用版本 -->
|
||||||
|
<template>
|
||||||
|
<div class="w-full min-w-0">
|
||||||
|
<!-- 参数配置 -->
|
||||||
|
<div v-if="hasConfig" class="space-y-12px">
|
||||||
|
<!-- JSON 输入框 -->
|
||||||
|
<div class="relative">
|
||||||
|
<el-input
|
||||||
|
v-model="paramsJson"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
@input="handleParamsChange"
|
||||||
|
:class="{ 'is-error': jsonError }"
|
||||||
|
/>
|
||||||
|
<!-- 查看详细示例弹出层 -->
|
||||||
|
<div class="absolute top-8px right-8px">
|
||||||
|
<el-popover
|
||||||
|
placement="left-start"
|
||||||
|
:width="450"
|
||||||
|
trigger="click"
|
||||||
|
:show-arrow="true"
|
||||||
|
:offset="8"
|
||||||
|
popper-class="json-params-detail-popover"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button type="info" :icon="InfoFilled" circle size="small" title="查看参数示例" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 弹出层内容 -->
|
||||||
|
<div class="json-params-detail-content">
|
||||||
|
<div class="flex items-center gap-8px mb-16px">
|
||||||
|
<Icon :icon="titleIcon" class="text-[var(--el-color-primary)] text-18px" />
|
||||||
|
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-16px">
|
||||||
|
<!-- 参数列表 -->
|
||||||
|
<div v-if="paramsList.length > 0">
|
||||||
|
<div class="flex items-center gap-8px mb-8px">
|
||||||
|
<Icon :icon="paramsIcon" class="text-[var(--el-color-primary)] text-14px" />
|
||||||
|
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">
|
||||||
|
{{ paramsLabel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-22px space-y-8px">
|
||||||
|
<div
|
||||||
|
v-for="param in paramsList"
|
||||||
|
:key="param.identifier"
|
||||||
|
class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
|
||||||
|
>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="text-12px font-500 text-[var(--el-text-color-primary)]">
|
||||||
|
{{ param.name }}
|
||||||
|
<el-tag v-if="param.required" size="small" type="danger" class="ml-4px">
|
||||||
|
必填
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="text-11px text-[var(--el-text-color-secondary)]">
|
||||||
|
{{ param.identifier }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-8px">
|
||||||
|
<el-tag :type="getParamTypeTag(param.dataType)" size="small">
|
||||||
|
{{ getParamTypeName(param.dataType) }}
|
||||||
|
</el-tag>
|
||||||
|
<span class="text-11px text-[var(--el-text-color-secondary)]">
|
||||||
|
{{ getExampleValue(param) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-12px ml-22px">
|
||||||
|
<div class="text-12px text-[var(--el-text-color-secondary)] mb-6px">
|
||||||
|
完整 JSON 格式:
|
||||||
|
</div>
|
||||||
|
<pre
|
||||||
|
class="p-12px bg-[var(--el-fill-color-light)] rounded-4px text-11px text-[var(--el-text-color-primary)] overflow-x-auto border-l-3px border-[var(--el-color-primary)]"
|
||||||
|
>
|
||||||
|
<code>{{ generateExampleJson() }}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 无参数提示 -->
|
||||||
|
<div v-else>
|
||||||
|
<div class="text-center py-16px">
|
||||||
|
<p class="text-14px text-[var(--el-text-color-secondary)]">{{
|
||||||
|
emptyMessage
|
||||||
|
}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 验证状态和错误提示 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-8px">
|
||||||
|
<Icon
|
||||||
|
:icon="jsonError ? 'ep:warning' : 'ep:circle-check'"
|
||||||
|
:class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
|
||||||
|
class="text-14px"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
|
||||||
|
class="text-12px"
|
||||||
|
>
|
||||||
|
{{ jsonError || 'JSON 格式正确' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 快速填充按钮 -->
|
||||||
|
<div v-if="paramsList.length > 0" class="flex items-center gap-8px">
|
||||||
|
<span class="text-12px text-[var(--el-text-color-secondary)]">快速填充:</span>
|
||||||
|
<el-button size="small" type="primary" plain @click="fillExampleJson">
|
||||||
|
示例数据
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" type="danger" plain @click="clearParams"> 清空</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 无配置提示 -->
|
||||||
|
<div v-else class="text-center py-20px">
|
||||||
|
<p class="text-14px text-[var(--el-text-color-secondary)]">{{ noConfigMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import { InfoFilled } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
/** JSON参数输入组件 - 通用版本 */
|
||||||
|
defineOptions({ name: 'JsonParamsInput' })
|
||||||
|
|
||||||
|
export interface JsonParamsConfig {
|
||||||
|
// 服务配置
|
||||||
|
service?: {
|
||||||
|
name: string
|
||||||
|
inputParams?: any[]
|
||||||
|
}
|
||||||
|
// 事件配置
|
||||||
|
event?: {
|
||||||
|
name: string
|
||||||
|
outputParams?: any[]
|
||||||
|
}
|
||||||
|
// 属性配置
|
||||||
|
properties?: any[]
|
||||||
|
// 自定义配置
|
||||||
|
custom?: {
|
||||||
|
name: string
|
||||||
|
params: any[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: string
|
||||||
|
config?: JsonParamsConfig
|
||||||
|
type?: 'service' | 'event' | 'property' | 'custom'
|
||||||
|
placeholder?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: string): void
|
||||||
|
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
type: 'service',
|
||||||
|
placeholder: '请输入JSON格式的参数'
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const localValue = useVModel(props, 'modelValue', emit, {
|
||||||
|
defaultValue: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const paramsJson = ref('')
|
||||||
|
const jsonError = ref('')
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const hasConfig = computed(() => {
|
||||||
|
// TODO @puhui999: 后续统一处理
|
||||||
|
console.log(props.config)
|
||||||
|
// return !!(
|
||||||
|
// props.config?.service ||
|
||||||
|
// props.config?.event ||
|
||||||
|
// props.config?.properties ||
|
||||||
|
// props.config?.custom
|
||||||
|
// )
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
const paramsList = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case 'service':
|
||||||
|
return props.config?.service?.inputParams || []
|
||||||
|
case 'event':
|
||||||
|
return props.config?.event?.outputParams || []
|
||||||
|
case 'property':
|
||||||
|
return props.config?.properties || []
|
||||||
|
case 'custom':
|
||||||
|
return props.config?.custom?.params || []
|
||||||
|
default:
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case 'service':
|
||||||
|
return `${props.config?.service?.name || '服务'} - 输入参数示例`
|
||||||
|
case 'event':
|
||||||
|
return `${props.config?.event?.name || '事件'} - 输出参数示例`
|
||||||
|
case 'property':
|
||||||
|
return '属性设置 - 参数示例'
|
||||||
|
case 'custom':
|
||||||
|
return `${props.config?.custom?.name || '自定义'} - 参数示例`
|
||||||
|
default:
|
||||||
|
return '参数示例'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const titleIcon = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case 'service':
|
||||||
|
return 'ep:service'
|
||||||
|
case 'event':
|
||||||
|
return 'ep:bell'
|
||||||
|
case 'property':
|
||||||
|
return 'ep:edit'
|
||||||
|
case 'custom':
|
||||||
|
return 'ep:document'
|
||||||
|
default:
|
||||||
|
return 'ep:document'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const paramsIcon = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case 'service':
|
||||||
|
return 'ep:edit'
|
||||||
|
case 'event':
|
||||||
|
return 'ep:upload'
|
||||||
|
case 'property':
|
||||||
|
return 'ep:setting'
|
||||||
|
case 'custom':
|
||||||
|
return 'ep:list'
|
||||||
|
default:
|
||||||
|
return 'ep:edit'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const paramsLabel = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case 'service':
|
||||||
|
return '输入参数'
|
||||||
|
case 'event':
|
||||||
|
return '输出参数'
|
||||||
|
case 'property':
|
||||||
|
return '属性参数'
|
||||||
|
case 'custom':
|
||||||
|
return '参数列表'
|
||||||
|
default:
|
||||||
|
return '参数'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emptyMessage = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case 'service':
|
||||||
|
return '此服务无需输入参数'
|
||||||
|
case 'event':
|
||||||
|
return '此事件无输出参数'
|
||||||
|
case 'property':
|
||||||
|
return '无可设置的属性'
|
||||||
|
case 'custom':
|
||||||
|
return '无参数配置'
|
||||||
|
default:
|
||||||
|
return '无参数'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const noConfigMessage = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case 'service':
|
||||||
|
return '请先选择服务'
|
||||||
|
case 'event':
|
||||||
|
return '请先选择事件'
|
||||||
|
case 'property':
|
||||||
|
return '请先选择产品'
|
||||||
|
case 'custom':
|
||||||
|
return '请先进行配置'
|
||||||
|
default:
|
||||||
|
return '请先进行配置'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 事件处理
|
||||||
|
const handleParamsChange = () => {
|
||||||
|
try {
|
||||||
|
jsonError.value = '' // 清除之前的错误
|
||||||
|
|
||||||
|
if (paramsJson.value.trim()) {
|
||||||
|
const parsed = JSON.parse(paramsJson.value)
|
||||||
|
localValue.value = paramsJson.value
|
||||||
|
|
||||||
|
// 额外的参数验证
|
||||||
|
if (typeof parsed !== 'object' || parsed === null) {
|
||||||
|
jsonError.value = '参数必须是一个有效的 JSON 对象'
|
||||||
|
emit('validate', { valid: false, message: jsonError.value })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证必填参数
|
||||||
|
for (const param of paramsList.value) {
|
||||||
|
if (param.required && (!parsed[param.identifier] || parsed[param.identifier] === '')) {
|
||||||
|
jsonError.value = `参数 ${param.name} 为必填项`
|
||||||
|
emit('validate', { valid: false, message: jsonError.value })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localValue.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证通过
|
||||||
|
emit('validate', { valid: true, message: 'JSON格式正确' })
|
||||||
|
} catch (error) {
|
||||||
|
jsonError.value = `JSON格式错误: ${error instanceof Error ? error.message : '未知错误'}`
|
||||||
|
emit('validate', { valid: false, message: jsonError.value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快速填充示例数据
|
||||||
|
const fillExampleJson = () => {
|
||||||
|
paramsJson.value = generateExampleJson()
|
||||||
|
handleParamsChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空参数
|
||||||
|
const clearParams = () => {
|
||||||
|
paramsJson.value = ''
|
||||||
|
localValue.value = ''
|
||||||
|
jsonError.value = ''
|
||||||
|
emit('validate', { valid: true, message: '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
const getParamTypeName = (dataType: string) => {
|
||||||
|
const typeMap = {
|
||||||
|
int: '整数',
|
||||||
|
float: '浮点数',
|
||||||
|
double: '双精度',
|
||||||
|
text: '字符串',
|
||||||
|
bool: '布尔值',
|
||||||
|
enum: '枚举',
|
||||||
|
date: '日期',
|
||||||
|
struct: '结构体',
|
||||||
|
array: '数组'
|
||||||
|
}
|
||||||
|
return typeMap[dataType] || dataType
|
||||||
|
}
|
||||||
|
|
||||||
|
const getParamTypeTag = (dataType: string) => {
|
||||||
|
const tagMap = {
|
||||||
|
int: 'primary',
|
||||||
|
float: 'success',
|
||||||
|
double: 'success',
|
||||||
|
text: 'info',
|
||||||
|
bool: 'warning',
|
||||||
|
enum: 'danger',
|
||||||
|
date: 'primary',
|
||||||
|
struct: 'info',
|
||||||
|
array: 'warning'
|
||||||
|
}
|
||||||
|
return tagMap[dataType] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getExampleValue = (param: any) => {
|
||||||
|
switch (param.dataType) {
|
||||||
|
case 'int':
|
||||||
|
return '25'
|
||||||
|
case 'float':
|
||||||
|
case 'double':
|
||||||
|
return '25.5'
|
||||||
|
case 'bool':
|
||||||
|
return 'false'
|
||||||
|
case 'text':
|
||||||
|
return '"auto"'
|
||||||
|
case 'enum':
|
||||||
|
return '"option1"'
|
||||||
|
case 'struct':
|
||||||
|
return '{}'
|
||||||
|
case 'array':
|
||||||
|
return '[]'
|
||||||
|
default:
|
||||||
|
return '""'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateExampleJson = () => {
|
||||||
|
if (paramsList.value.length === 0) {
|
||||||
|
return '{}'
|
||||||
|
}
|
||||||
|
|
||||||
|
const example = {}
|
||||||
|
paramsList.value.forEach((param) => {
|
||||||
|
switch (param.dataType) {
|
||||||
|
case 'int':
|
||||||
|
example[param.identifier] = 25
|
||||||
|
break
|
||||||
|
case 'float':
|
||||||
|
case 'double':
|
||||||
|
example[param.identifier] = 25.5
|
||||||
|
break
|
||||||
|
case 'bool':
|
||||||
|
example[param.identifier] = false
|
||||||
|
break
|
||||||
|
case 'text':
|
||||||
|
example[param.identifier] = 'auto'
|
||||||
|
break
|
||||||
|
case 'struct':
|
||||||
|
example[param.identifier] = {}
|
||||||
|
break
|
||||||
|
case 'array':
|
||||||
|
example[param.identifier] = []
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
example[param.identifier] = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return JSON.stringify(example, null, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理数据回显的函数
|
||||||
|
const handleDataDisplay = (value: string) => {
|
||||||
|
if (!value || !value.trim()) {
|
||||||
|
paramsJson.value = ''
|
||||||
|
jsonError.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 尝试解析JSON,如果成功则格式化
|
||||||
|
const parsed = JSON.parse(value)
|
||||||
|
paramsJson.value = JSON.stringify(parsed, null, 2)
|
||||||
|
jsonError.value = ''
|
||||||
|
} catch {
|
||||||
|
// 如果不是有效的JSON,直接使用原字符串
|
||||||
|
paramsJson.value = value
|
||||||
|
jsonError.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听外部值变化(编辑模式数据回显)
|
||||||
|
watch(
|
||||||
|
() => localValue.value,
|
||||||
|
(newValue, oldValue) => {
|
||||||
|
// 避免循环更新
|
||||||
|
if (newValue === oldValue) return
|
||||||
|
|
||||||
|
// 使用 nextTick 确保在下一个 tick 中处理数据
|
||||||
|
nextTick(() => {
|
||||||
|
handleDataDisplay(newValue || '')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 组件挂载后也尝试处理一次数据回显
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (localValue.value) {
|
||||||
|
handleDataDisplay(localValue.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听配置变化
|
||||||
|
watch(
|
||||||
|
() => props.config,
|
||||||
|
(newConfig, oldConfig) => {
|
||||||
|
// 只有在配置真正变化时才清空数据
|
||||||
|
if (JSON.stringify(newConfig) !== JSON.stringify(oldConfig)) {
|
||||||
|
// 如果没有外部传入的值,才清空数据
|
||||||
|
if (!localValue.value) {
|
||||||
|
paramsJson.value = ''
|
||||||
|
jsonError.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 弹出层内容样式 */
|
||||||
|
.json-params-detail-content {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹出层自定义样式 */
|
||||||
|
:global(.json-params-detail-popover) {
|
||||||
|
max-width: 500px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.json-params-detail-popover .el-popover__content) {
|
||||||
|
padding: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* JSON 代码块样式 */
|
||||||
|
.json-params-detail-content pre {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,495 +0,0 @@
|
||||||
<!-- 服务参数输入组件 -->
|
|
||||||
<template>
|
|
||||||
<div class="w-full min-w-0">
|
|
||||||
<!-- 服务参数配置 -->
|
|
||||||
<div v-if="serviceConfig && serviceConfig.service" class="space-y-12px">
|
|
||||||
<!-- JSON 输入框 -->
|
|
||||||
<div class="relative">
|
|
||||||
<el-input
|
|
||||||
v-model="paramsJson"
|
|
||||||
type="textarea"
|
|
||||||
:rows="4"
|
|
||||||
placeholder="请输入JSON格式的服务参数"
|
|
||||||
@input="handleParamsChange"
|
|
||||||
:class="{ 'is-error': jsonError }"
|
|
||||||
/>
|
|
||||||
<!-- 查看详细示例按钮 -->
|
|
||||||
<div class="absolute top-8px right-8px">
|
|
||||||
<el-button
|
|
||||||
ref="exampleTriggerRef"
|
|
||||||
type="info"
|
|
||||||
:icon="InfoFilled"
|
|
||||||
circle
|
|
||||||
size="small"
|
|
||||||
@click="toggleExampleDetail"
|
|
||||||
title="查看参数示例"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 验证状态和错误提示 -->
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-8px">
|
|
||||||
<Icon
|
|
||||||
:icon="jsonError ? 'ep:warning' : 'ep:circle-check'"
|
|
||||||
:class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
|
|
||||||
class="text-14px"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
:class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
|
|
||||||
class="text-12px"
|
|
||||||
>
|
|
||||||
{{ jsonError || 'JSON 格式正确' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 快速填充按钮 -->
|
|
||||||
<div v-if="inputParams.length > 0" class="flex items-center gap-8px">
|
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)]">快速填充:</span>
|
|
||||||
<el-button size="small" type="primary" plain @click="fillExampleJson">
|
|
||||||
示例数据
|
|
||||||
</el-button>
|
|
||||||
<!-- TODO @puhui999:这里的 type 有告警 -->
|
|
||||||
<el-button size="small" type="default" plain @click="clearParams"> 清空 </el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 详细示例弹出层 -->
|
|
||||||
<Teleport to="body">
|
|
||||||
<div
|
|
||||||
v-if="showExampleDetail"
|
|
||||||
ref="exampleDetailRef"
|
|
||||||
class="example-detail-popover"
|
|
||||||
:style="examplePopoverStyle"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-400px max-w-500px"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-8px mb-16px">
|
|
||||||
<Icon icon="ep:service" class="text-[var(--el-color-primary)] text-18px" />
|
|
||||||
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">
|
|
||||||
{{ serviceConfig.name }} - 参数示例
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-16px">
|
|
||||||
<!-- 服务参数示例 -->
|
|
||||||
<div v-if="inputParams.length > 0">
|
|
||||||
<div class="flex items-center gap-8px mb-8px">
|
|
||||||
<Icon icon="ep:edit" class="text-[var(--el-color-primary)] text-14px" />
|
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">
|
|
||||||
输入参数
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="ml-22px space-y-8px">
|
|
||||||
<div
|
|
||||||
v-for="param in inputParams"
|
|
||||||
:key="param.identifier"
|
|
||||||
class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
|
|
||||||
>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="text-12px font-500 text-[var(--el-text-color-primary)]">
|
|
||||||
{{ param.name }}
|
|
||||||
<el-tag v-if="param.required" size="small" type="danger" class="ml-4px">
|
|
||||||
必填
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
<div class="text-11px text-[var(--el-text-color-secondary)]">
|
|
||||||
{{ param.identifier }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-8px">
|
|
||||||
<el-tag :type="getParamTypeTag(param.dataType)" size="small">
|
|
||||||
{{ getParamTypeName(param.dataType) }}
|
|
||||||
</el-tag>
|
|
||||||
<span class="text-11px text-[var(--el-text-color-secondary)]">
|
|
||||||
{{ getExampleValue(param) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-12px ml-22px">
|
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)] mb-6px">
|
|
||||||
完整 JSON 格式:
|
|
||||||
</div>
|
|
||||||
<pre
|
|
||||||
class="p-12px bg-[var(--el-fill-color-light)] rounded-4px text-11px text-[var(--el-text-color-primary)] overflow-x-auto border-l-3px border-[var(--el-color-primary)]"
|
|
||||||
>
|
|
||||||
<code>{{ generateExampleJson() }}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 无参数提示 -->
|
|
||||||
<div v-else>
|
|
||||||
<div class="text-center py-16px">
|
|
||||||
<p class="text-14px text-[var(--el-text-color-secondary)]">此服务无需输入参数</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 关闭按钮 -->
|
|
||||||
<div class="flex justify-end mt-16px">
|
|
||||||
<el-button size="small" @click="hideExampleDetail">关闭</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 无服务配置提示 -->
|
|
||||||
<div v-else class="text-center py-20px">
|
|
||||||
<p class="text-14px text-[var(--el-text-color-secondary)]">请先选择服务</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import { InfoFilled } from '@element-plus/icons-vue'
|
|
||||||
|
|
||||||
/** 服务参数输入组件 */
|
|
||||||
defineOptions({ name: 'ServiceParamsInput' })
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
modelValue?: string
|
|
||||||
serviceConfig?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Emits {
|
|
||||||
(e: 'update:modelValue', value: string): void
|
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const localValue = useVModel(props, 'modelValue', emit, {
|
|
||||||
defaultValue: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO @puhui999:一些注释风格;
|
|
||||||
|
|
||||||
// 状态
|
|
||||||
const paramsJson = ref('')
|
|
||||||
const jsonError = ref('')
|
|
||||||
|
|
||||||
// 示例弹出层相关状态
|
|
||||||
const showExampleDetail = ref(false)
|
|
||||||
const exampleTriggerRef = ref()
|
|
||||||
const exampleDetailRef = ref()
|
|
||||||
const examplePopoverStyle = ref({})
|
|
||||||
|
|
||||||
// 计算属性
|
|
||||||
const inputParams = computed(() => {
|
|
||||||
return props.serviceConfig?.service?.inputParams || []
|
|
||||||
})
|
|
||||||
|
|
||||||
// 事件处理
|
|
||||||
const handleParamsChange = () => {
|
|
||||||
try {
|
|
||||||
jsonError.value = '' // 清除之前的错误
|
|
||||||
|
|
||||||
if (paramsJson.value.trim()) {
|
|
||||||
const parsed = JSON.parse(paramsJson.value)
|
|
||||||
localValue.value = paramsJson.value
|
|
||||||
|
|
||||||
// 额外的参数验证
|
|
||||||
if (typeof parsed !== 'object' || parsed === null) {
|
|
||||||
jsonError.value = '参数必须是一个有效的 JSON 对象'
|
|
||||||
emit('validate', { valid: false, message: jsonError.value })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证必填参数
|
|
||||||
for (const param of inputParams.value) {
|
|
||||||
if (param.required && (!parsed[param.identifier] || parsed[param.identifier] === '')) {
|
|
||||||
jsonError.value = `参数 ${param.name} 为必填项`
|
|
||||||
emit('validate', { valid: false, message: jsonError.value })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
localValue.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证通过
|
|
||||||
emit('validate', { valid: true, message: 'JSON格式正确' })
|
|
||||||
} catch (error) {
|
|
||||||
jsonError.value = `JSON格式错误: ${error instanceof Error ? error.message : '未知错误'}`
|
|
||||||
emit('validate', { valid: false, message: jsonError.value })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 快速填充示例数据
|
|
||||||
const fillExampleJson = () => {
|
|
||||||
paramsJson.value = generateExampleJson()
|
|
||||||
handleParamsChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清空参数
|
|
||||||
const clearParams = () => {
|
|
||||||
paramsJson.value = ''
|
|
||||||
localValue.value = ''
|
|
||||||
jsonError.value = ''
|
|
||||||
emit('validate', { valid: true, message: '' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 工具函数
|
|
||||||
// TODO @puhui999:这里的复用
|
|
||||||
const getParamTypeName = (dataType: string) => {
|
|
||||||
const typeMap = {
|
|
||||||
int: '整数',
|
|
||||||
float: '浮点数',
|
|
||||||
double: '双精度',
|
|
||||||
text: '字符串',
|
|
||||||
bool: '布尔值',
|
|
||||||
enum: '枚举',
|
|
||||||
date: '日期',
|
|
||||||
struct: '结构体',
|
|
||||||
array: '数组'
|
|
||||||
}
|
|
||||||
return typeMap[dataType] || dataType
|
|
||||||
}
|
|
||||||
|
|
||||||
const getParamTypeTag = (dataType: string) => {
|
|
||||||
const tagMap = {
|
|
||||||
int: 'primary',
|
|
||||||
float: 'success',
|
|
||||||
double: 'success',
|
|
||||||
text: 'info',
|
|
||||||
bool: 'warning',
|
|
||||||
enum: 'danger',
|
|
||||||
date: 'primary',
|
|
||||||
struct: 'info',
|
|
||||||
array: 'warning'
|
|
||||||
}
|
|
||||||
return tagMap[dataType] || 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getExampleValue = (param: any) => {
|
|
||||||
switch (param.dataType) {
|
|
||||||
case 'int':
|
|
||||||
return '25'
|
|
||||||
case 'float':
|
|
||||||
case 'double':
|
|
||||||
return '25.5'
|
|
||||||
case 'bool':
|
|
||||||
return 'false'
|
|
||||||
case 'text':
|
|
||||||
return '"auto"'
|
|
||||||
case 'enum':
|
|
||||||
return '"option1"'
|
|
||||||
case 'struct':
|
|
||||||
return '{}'
|
|
||||||
case 'array':
|
|
||||||
return '[]'
|
|
||||||
default:
|
|
||||||
return '""'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateExampleJson = () => {
|
|
||||||
if (inputParams.value.length === 0) {
|
|
||||||
return '{}'
|
|
||||||
}
|
|
||||||
|
|
||||||
const example = {}
|
|
||||||
inputParams.value.forEach((param) => {
|
|
||||||
switch (param.dataType) {
|
|
||||||
case 'int':
|
|
||||||
example[param.identifier] = 25
|
|
||||||
break
|
|
||||||
case 'float':
|
|
||||||
case 'double':
|
|
||||||
example[param.identifier] = 25.5
|
|
||||||
break
|
|
||||||
case 'bool':
|
|
||||||
example[param.identifier] = false
|
|
||||||
break
|
|
||||||
case 'text':
|
|
||||||
example[param.identifier] = 'auto'
|
|
||||||
break
|
|
||||||
case 'struct':
|
|
||||||
example[param.identifier] = {}
|
|
||||||
break
|
|
||||||
case 'array':
|
|
||||||
example[param.identifier] = []
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
example[param.identifier] = ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return JSON.stringify(example, null, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 示例弹出层控制方法
|
|
||||||
const toggleExampleDetail = () => {
|
|
||||||
if (showExampleDetail.value) {
|
|
||||||
hideExampleDetail()
|
|
||||||
} else {
|
|
||||||
showExampleDetailPopover()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const showExampleDetailPopover = () => {
|
|
||||||
if (!exampleTriggerRef.value) return
|
|
||||||
|
|
||||||
showExampleDetail.value = true
|
|
||||||
|
|
||||||
nextTick(() => {
|
|
||||||
updateExamplePopoverPosition()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideExampleDetail = () => {
|
|
||||||
showExampleDetail.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateExamplePopoverPosition = () => {
|
|
||||||
if (!exampleTriggerRef.value || !exampleDetailRef.value) return
|
|
||||||
|
|
||||||
const triggerEl = exampleTriggerRef.value.$el
|
|
||||||
const triggerRect = triggerEl.getBoundingClientRect()
|
|
||||||
|
|
||||||
// 计算弹出层位置
|
|
||||||
const left = triggerRect.left + triggerRect.width + 8
|
|
||||||
const top = triggerRect.top
|
|
||||||
|
|
||||||
// 检查是否超出视窗右边界
|
|
||||||
const popoverWidth = 500 // 最大宽度
|
|
||||||
const viewportWidth = window.innerWidth
|
|
||||||
|
|
||||||
let finalLeft = left
|
|
||||||
if (left + popoverWidth > viewportWidth - 16) {
|
|
||||||
// 如果超出右边界,显示在左侧
|
|
||||||
finalLeft = triggerRect.left - popoverWidth - 8
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否超出视窗下边界
|
|
||||||
let finalTop = top
|
|
||||||
const popoverHeight = exampleDetailRef.value.offsetHeight || 300
|
|
||||||
const viewportHeight = window.innerHeight
|
|
||||||
|
|
||||||
if (top + popoverHeight > viewportHeight - 16) {
|
|
||||||
finalTop = Math.max(16, viewportHeight - popoverHeight - 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
examplePopoverStyle.value = {
|
|
||||||
position: 'fixed',
|
|
||||||
left: `${finalLeft}px`,
|
|
||||||
top: `${finalTop}px`,
|
|
||||||
zIndex: 9999
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击外部关闭弹出层
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
showExampleDetail.value &&
|
|
||||||
exampleDetailRef.value &&
|
|
||||||
exampleTriggerRef.value &&
|
|
||||||
!exampleDetailRef.value.contains(event.target as Node) &&
|
|
||||||
!exampleTriggerRef.value.$el.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
hideExampleDetail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听窗口大小变化,重新计算弹出层位置
|
|
||||||
const handleResize = () => {
|
|
||||||
if (showExampleDetail.value) {
|
|
||||||
updateExamplePopoverPosition()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
onMounted(() => {
|
|
||||||
if (localValue.value) {
|
|
||||||
try {
|
|
||||||
paramsJson.value = localValue.value
|
|
||||||
jsonError.value = ''
|
|
||||||
} catch (error) {
|
|
||||||
console.error('初始化参数失败:', error)
|
|
||||||
jsonError.value = '初始参数格式错误'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加事件监听器
|
|
||||||
document.addEventListener('click', handleClickOutside)
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 组件卸载时清理事件监听器
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.removeEventListener('click', handleClickOutside)
|
|
||||||
window.removeEventListener('resize', handleResize)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听输入值变化
|
|
||||||
watch(
|
|
||||||
() => localValue.value,
|
|
||||||
(newValue) => {
|
|
||||||
if (newValue !== paramsJson.value) {
|
|
||||||
paramsJson.value = newValue || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听服务配置变化
|
|
||||||
watch(
|
|
||||||
() => props.serviceConfig,
|
|
||||||
() => {
|
|
||||||
// 服务变化时清空参数
|
|
||||||
paramsJson.value = ''
|
|
||||||
localValue.value = ''
|
|
||||||
jsonError.value = ''
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@keyframes fadeInScale {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9) translateY(-4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1) translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-detail-popover {
|
|
||||||
animation: fadeInScale 0.2s ease-out;
|
|
||||||
transform-origin: top left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 弹出层箭头效果 */
|
|
||||||
.example-detail-popover::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: -8px;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-top: 8px solid transparent;
|
|
||||||
border-right: 8px solid var(--el-border-color);
|
|
||||||
border-bottom: 8px solid transparent;
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
.example-detail-popover::after {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: -7px;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-top: 8px solid transparent;
|
|
||||||
border-right: 8px solid white;
|
|
||||||
border-bottom: 8px solid transparent;
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -78,12 +78,27 @@
|
||||||
@update:model-value="(value) => updateAction(index, value)"
|
@update:model-value="(value) => updateAction(index, value)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 告警配置 -->
|
<!-- 告警配置 - 只有恢复告警时才显示 -->
|
||||||
<AlertConfig
|
<AlertConfig
|
||||||
v-if="isAlertAction(action.type)"
|
v-if="action.type === ActionTypeEnum.ALERT_RECOVER"
|
||||||
:model-value="action.alertConfigId"
|
:model-value="action.alertConfigId"
|
||||||
@update:model-value="(value) => updateActionAlertConfig(index, value)"
|
@update:model-value="(value) => updateActionAlertConfig(index, value)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 触发告警提示 - 触发告警时显示 -->
|
||||||
|
<div
|
||||||
|
v-if="action.type === ActionTypeEnum.ALERT_TRIGGER"
|
||||||
|
class="border border-[var(--el-border-color-light)] rounded-6px p-16px bg-[var(--el-fill-color-blank)]"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-8px mb-8px">
|
||||||
|
<Icon icon="ep:warning" class="text-[var(--el-color-warning)] text-16px" />
|
||||||
|
<span class="text-14px font-600 text-[var(--el-text-color-primary)]">触发告警</span>
|
||||||
|
<el-tag size="small" type="warning">自动执行</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">
|
||||||
|
当触发条件满足时,系统将自动发送告警通知,无需额外配置。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -107,7 +122,7 @@ import { useVModel } from '@vueuse/core'
|
||||||
import ActionTypeSelector from '../selectors/ActionTypeSelector.vue'
|
import ActionTypeSelector from '../selectors/ActionTypeSelector.vue'
|
||||||
import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
|
import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
|
||||||
import AlertConfig from '../configs/AlertConfig.vue'
|
import AlertConfig from '../configs/AlertConfig.vue'
|
||||||
import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
|
import { Action } from '@/api/iot/rule/scene/scene.types'
|
||||||
import {
|
import {
|
||||||
IotRuleSceneActionTypeEnum as ActionTypeEnum,
|
IotRuleSceneActionTypeEnum as ActionTypeEnum,
|
||||||
isDeviceAction,
|
isDeviceAction,
|
||||||
|
|
@ -118,23 +133,20 @@ import {
|
||||||
/** 执行器配置组件 */
|
/** 执行器配置组件 */
|
||||||
defineOptions({ name: 'ActionSection' })
|
defineOptions({ name: 'ActionSection' })
|
||||||
|
|
||||||
interface Props {
|
const props = defineProps<{
|
||||||
actions: ActionFormData[]
|
actions: Action[]
|
||||||
}
|
}>()
|
||||||
|
|
||||||
interface Emits {
|
const emit = defineEmits<{
|
||||||
(e: 'update:actions', value: ActionFormData[]): void
|
(e: 'update:actions', value: Action[]): void
|
||||||
}
|
}>()
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const actions = useVModel(props, 'actions', emit)
|
const actions = useVModel(props, 'actions', emit)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建默认的执行器数据
|
* 创建默认的执行器数据
|
||||||
*/
|
*/
|
||||||
const createDefaultActionData = (): ActionFormData => {
|
const createDefaultActionData = (): Action => {
|
||||||
return {
|
return {
|
||||||
type: ActionTypeEnum.DEVICE_PROPERTY_SET, // 默认为设备属性设置
|
type: ActionTypeEnum.DEVICE_PROPERTY_SET, // 默认为设备属性设置
|
||||||
productId: undefined,
|
productId: undefined,
|
||||||
|
|
@ -145,9 +157,7 @@ const createDefaultActionData = (): ActionFormData => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 配置常量
|
const maxActions = 5 // 最大执行器数量
|
||||||
// TODO @puhui999:去掉最大;注释风格改下;
|
|
||||||
const maxActions = 5
|
|
||||||
|
|
||||||
// 工具函数
|
// 工具函数
|
||||||
const getActionTypeName = (type: number) => {
|
const getActionTypeName = (type: number) => {
|
||||||
|
|
@ -164,7 +174,7 @@ const getActionTypeTag = (type: number) => {
|
||||||
return actionTypeTags[type] || 'info'
|
return actionTypeTags[type] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件处理
|
/** 添加执行器 */
|
||||||
const addAction = () => {
|
const addAction = () => {
|
||||||
if (actions.value.length >= maxActions) {
|
if (actions.value.length >= maxActions) {
|
||||||
return
|
return
|
||||||
|
|
@ -174,24 +184,29 @@ const addAction = () => {
|
||||||
actions.value.push(newAction)
|
actions.value.push(newAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 删除执行器 */
|
||||||
const removeAction = (index: number) => {
|
const removeAction = (index: number) => {
|
||||||
actions.value.splice(index, 1)
|
actions.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 更新执行器类型 */
|
||||||
const updateActionType = (index: number, type: number) => {
|
const updateActionType = (index: number, type: number) => {
|
||||||
actions.value[index].type = type
|
actions.value[index].type = type
|
||||||
onActionTypeChange(actions.value[index], type)
|
onActionTypeChange(actions.value[index], type)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateAction = (index: number, action: ActionFormData) => {
|
/** 更新执行器 */
|
||||||
|
const updateAction = (index: number, action: Action) => {
|
||||||
actions.value[index] = action
|
actions.value[index] = action
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 更新告警配置 */
|
||||||
const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
|
const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
|
||||||
actions.value[index].alertConfigId = alertConfigId
|
actions.value[index].alertConfigId = alertConfigId
|
||||||
}
|
}
|
||||||
|
|
||||||
const onActionTypeChange = (action: ActionFormData, type: number) => {
|
/** 监听执行器类型变化 */
|
||||||
|
const onActionTypeChange = (action: Action, type: number) => {
|
||||||
// 清理不相关的配置,确保数据结构干净
|
// 清理不相关的配置,确保数据结构干净
|
||||||
if (isDeviceAction(type)) {
|
if (isDeviceAction(type)) {
|
||||||
// 设备控制类型:清理告警配置,确保设备参数存在
|
// 设备控制类型:清理告警配置,确保设备参数存在
|
||||||
|
|
@ -204,16 +219,11 @@ const onActionTypeChange = (action: ActionFormData, type: number) => {
|
||||||
action.identifier = undefined
|
action.identifier = undefined
|
||||||
}
|
}
|
||||||
} else if (isAlertAction(type)) {
|
} else if (isAlertAction(type)) {
|
||||||
// 告警类型:清理设备配置
|
|
||||||
action.productId = undefined
|
action.productId = undefined
|
||||||
action.deviceId = undefined
|
action.deviceId = undefined
|
||||||
action.identifier = undefined // 清理服务标识符
|
action.identifier = undefined // 清理服务标识符
|
||||||
action.params = undefined
|
action.params = undefined
|
||||||
|
action.alertConfigId = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// 触发重新校验
|
|
||||||
nextTick(() => {
|
|
||||||
// 这里可以添加校验逻辑
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -58,17 +58,17 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||||
import { RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
|
import { IotSceneRule } from '@/api/iot/rule/scene/scene.types'
|
||||||
|
|
||||||
/** 基础信息配置组件 */
|
/** 基础信息配置组件 */
|
||||||
defineOptions({ name: 'BasicInfoSection' })
|
defineOptions({ name: 'BasicInfoSection' })
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: RuleSceneFormData
|
modelValue: IotSceneRule
|
||||||
rules?: any
|
rules?: any
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: RuleSceneFormData): void
|
(e: 'update:modelValue', value: IotSceneRule): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const formData = useVModel(props, 'modelValue', emit)
|
const formData = useVModel(props, 'modelValue', emit)
|
||||||
|
|
|
||||||
|
|
@ -37,90 +37,122 @@
|
||||||
</el-option-group>
|
</el-option-group>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
||||||
<!-- 属性详情触发按钮 -->
|
<!-- 属性详情弹出层 -->
|
||||||
<div class="relative">
|
<el-popover
|
||||||
<el-button
|
v-if="selectedProperty"
|
||||||
v-if="selectedProperty"
|
placement="right-start"
|
||||||
ref="detailTriggerRef"
|
:width="350"
|
||||||
type="info"
|
trigger="click"
|
||||||
:icon="InfoFilled"
|
:show-arrow="true"
|
||||||
circle
|
:offset="8"
|
||||||
size="small"
|
popper-class="property-detail-popover"
|
||||||
@click="togglePropertyDetail"
|
>
|
||||||
class="flex-shrink-0"
|
<template #reference>
|
||||||
title="查看属性详情"
|
<el-button
|
||||||
/>
|
type="info"
|
||||||
|
:icon="InfoFilled"
|
||||||
|
circle
|
||||||
|
size="small"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
title="查看属性详情"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 属性详情弹出层 -->
|
<!-- 弹出层内容 -->
|
||||||
<Teleport to="body">
|
<div class="property-detail-content">
|
||||||
<div
|
<div class="flex items-center gap-8px mb-12px">
|
||||||
v-if="showPropertyDetail && selectedProperty"
|
<Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-16px" />
|
||||||
ref="propertyDetailRef"
|
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">
|
||||||
class="property-detail-popover"
|
{{ selectedProperty.name }}
|
||||||
:style="popoverStyle"
|
</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="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-300px max-w-400px"
|
v-if="
|
||||||
|
selectedProperty.type === IoTThingModelTypeEnum.PROPERTY &&
|
||||||
|
selectedProperty.accessMode
|
||||||
|
"
|
||||||
|
class="flex items-start gap-8px"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-8px mb-12px">
|
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
||||||
<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)]">
|
</span>
|
||||||
{{ selectedProperty.name }}
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
||||||
</span>
|
{{ getAccessModeText(selectedProperty.accessMode) }}
|
||||||
<el-tag :type="getPropertyTypeTag(selectedProperty.dataType)" size="small">
|
</span>
|
||||||
{{ getPropertyTypeName(selectedProperty.dataType) }}
|
</div>
|
||||||
</el-tag>
|
|
||||||
</div>
|
<div
|
||||||
<div class="space-y-8px ml-24px">
|
v-if="
|
||||||
<div class="flex items-start gap-8px">
|
selectedProperty.type === IoTThingModelTypeEnum.EVENT && selectedProperty.eventType
|
||||||
<span
|
"
|
||||||
class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
|
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">
|
</span>
|
||||||
{{ selectedProperty.identifier }}
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
||||||
</span>
|
{{ getEventTypeText(selectedProperty.eventType) }}
|
||||||
</div>
|
</span>
|
||||||
<div v-if="selectedProperty.description" class="flex items-start gap-8px">
|
</div>
|
||||||
<span
|
|
||||||
class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
|
<div
|
||||||
>
|
v-if="
|
||||||
描述:
|
selectedProperty.type === IoTThingModelTypeEnum.SERVICE && selectedProperty.callType
|
||||||
</span>
|
"
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
class="flex items-start gap-8px"
|
||||||
{{ selectedProperty.description }}
|
>
|
||||||
</span>
|
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
||||||
</div>
|
调用类型:
|
||||||
<div v-if="selectedProperty.unit" class="flex items-start gap-8px">
|
</span>
|
||||||
<span
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
||||||
class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0"
|
{{ getCallTypeText(selectedProperty.callType) }}
|
||||||
>
|
</span>
|
||||||
单位:
|
|
||||||
</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>
|
||||||
</Teleport>
|
</div>
|
||||||
</div>
|
</el-popover>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -153,25 +185,6 @@ 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[] }[] = []
|
||||||
|
|
@ -235,65 +248,33 @@ const getPropertyTypeTag = (dataType: string) => {
|
||||||
return tagMap[dataType] || 'info'
|
return tagMap[dataType] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 弹出层控制方法
|
// 工具函数 - 获取访问模式文本
|
||||||
const togglePropertyDetail = () => {
|
const getAccessModeText = (accessMode: string) => {
|
||||||
if (showPropertyDetail.value) {
|
const modeMap = {
|
||||||
hidePropertyDetail()
|
r: '只读',
|
||||||
} else {
|
w: '只写',
|
||||||
showPropertyDetailPopover()
|
rw: '读写'
|
||||||
}
|
}
|
||||||
|
return modeMap[accessMode] || accessMode
|
||||||
}
|
}
|
||||||
|
|
||||||
const showPropertyDetailPopover = () => {
|
// 工具函数 - 获取事件类型文本
|
||||||
if (!selectedProperty.value || !detailTriggerRef.value) return
|
const getEventTypeText = (eventType: string) => {
|
||||||
|
const typeMap = {
|
||||||
showPropertyDetail.value = true
|
info: '信息',
|
||||||
|
alert: '告警',
|
||||||
nextTick(() => {
|
error: '故障'
|
||||||
updatePopoverPosition()
|
}
|
||||||
})
|
return typeMap[eventType] || eventType
|
||||||
}
|
}
|
||||||
|
|
||||||
const hidePropertyDetail = () => {
|
// 工具函数 - 获取调用类型文本
|
||||||
showPropertyDetail.value = false
|
const getCallTypeText = (callType: string) => {
|
||||||
}
|
const typeMap = {
|
||||||
|
sync: '同步',
|
||||||
const updatePopoverPosition = () => {
|
async: '异步'
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
return typeMap[callType] || callType
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件处理
|
// 事件处理
|
||||||
|
|
@ -305,11 +286,11 @@ const handleChange = (value: string) => {
|
||||||
config: property
|
config: property
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 选择变化时隐藏详情弹出层
|
|
||||||
hidePropertyDetail()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取物模型TSL数据
|
/**
|
||||||
|
* 获取物模型TSL数据
|
||||||
|
*/
|
||||||
const getThingModelTSL = async () => {
|
const getThingModelTSL = async () => {
|
||||||
if (!props.productId) {
|
if (!props.productId) {
|
||||||
thingModelTSL.value = null
|
thingModelTSL.value = null
|
||||||
|
|
@ -319,8 +300,15 @@ const getThingModelTSL = async () => {
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(props.productId)
|
const tslData = await ThingModelApi.getThingModelTSLByProductId(props.productId)
|
||||||
parseThingModelData()
|
|
||||||
|
if (tslData) {
|
||||||
|
thingModelTSL.value = tslData
|
||||||
|
parseThingModelData()
|
||||||
|
} else {
|
||||||
|
// 如果TSL获取失败,尝试获取物模型列表
|
||||||
|
await getThingModelList()
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取物模型TSL失败:', error)
|
console.error('获取物模型TSL失败:', error)
|
||||||
// 如果TSL获取失败,尝试获取物模型列表
|
// 如果TSL获取失败,尝试获取物模型列表
|
||||||
|
|
@ -330,7 +318,9 @@ const getThingModelTSL = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取物模型列表(备用方案)
|
/**
|
||||||
|
* 获取物模型列表(备用方案)
|
||||||
|
*/
|
||||||
const getThingModelList = async () => {
|
const getThingModelList = async () => {
|
||||||
if (!props.productId) {
|
if (!props.productId) {
|
||||||
propertyList.value = []
|
propertyList.value = []
|
||||||
|
|
@ -441,21 +431,6 @@ const getPropertyRange = (property: any) => {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取数据范围描述(保留兼容性)
|
|
||||||
const getDataRange = (dataSpecs: any) => {
|
|
||||||
if (!dataSpecs) return undefined
|
|
||||||
|
|
||||||
if (dataSpecs.min !== undefined && dataSpecs.max !== undefined) {
|
|
||||||
return `${dataSpecs.min}~${dataSpecs.max}`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataSpecs.dataSpecsList && Array.isArray(dataSpecs.dataSpecsList)) {
|
|
||||||
return dataSpecs.dataSpecsList.map((item: any) => `${item.name}(${item.value})`).join(', ')
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听产品变化
|
// 监听产品变化
|
||||||
watch(
|
watch(
|
||||||
() => props.productId,
|
() => props.productId,
|
||||||
|
|
@ -470,74 +445,30 @@ watch(
|
||||||
() => props.triggerType,
|
() => props.triggerType,
|
||||||
() => {
|
() => {
|
||||||
localValue.value = ''
|
localValue.value = ''
|
||||||
hidePropertyDetail()
|
// el-popover 会自动关闭,无需手动处理
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 监听窗口大小变化,重新计算弹出层位置
|
|
||||||
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;
|
.property-detail-content {
|
||||||
transform-origin: top left;
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 弹出层箭头效果(可选) */
|
/* 弹出层自定义样式 */
|
||||||
.property-detail-popover::before {
|
:global(.property-detail-popover) {
|
||||||
position: absolute;
|
/* 可以在这里添加全局弹出层样式 */
|
||||||
top: 20px;
|
max-width: 400px !important;
|
||||||
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 {
|
:global(.property-detail-popover .el-popover__content) {
|
||||||
position: absolute;
|
padding: 16px !important;
|
||||||
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>
|
||||||
|
|
|
||||||
|
|
@ -1,462 +0,0 @@
|
||||||
<!-- 服务选择器组件 -->
|
|
||||||
<template>
|
|
||||||
<div class="w-full">
|
|
||||||
<el-select
|
|
||||||
:model-value="modelValue"
|
|
||||||
@update:model-value="handleChange"
|
|
||||||
placeholder="请选择服务"
|
|
||||||
filterable
|
|
||||||
clearable
|
|
||||||
class="w-full"
|
|
||||||
:loading="loading"
|
|
||||||
:disabled="!productId"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="service in serviceList"
|
|
||||||
:key="service.identifier"
|
|
||||||
:label="service.name"
|
|
||||||
:value="service.identifier"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between w-full py-4px">
|
|
||||||
<div class="flex items-center gap-12px flex-1">
|
|
||||||
<Icon
|
|
||||||
icon="ep:service"
|
|
||||||
class="text-18px text-[var(--el-color-success)] flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">
|
|
||||||
{{ service.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">
|
|
||||||
{{ service.identifier }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="service.description"
|
|
||||||
class="text-11px text-[var(--el-text-color-secondary)] mt-2px"
|
|
||||||
>
|
|
||||||
{{ service.description }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-8px">
|
|
||||||
<el-tag :type="getCallTypeTag(service.callType)" size="small">
|
|
||||||
{{ getCallTypeLabel(service.callType) }}
|
|
||||||
</el-tag>
|
|
||||||
<el-button
|
|
||||||
ref="detailTriggerRef"
|
|
||||||
type="info"
|
|
||||||
:icon="InfoFilled"
|
|
||||||
circle
|
|
||||||
size="small"
|
|
||||||
@click.stop="showServiceDetail(service)"
|
|
||||||
title="查看服务详情"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-option>
|
|
||||||
</el-select>
|
|
||||||
|
|
||||||
<!-- 服务详情弹出层 -->
|
|
||||||
<Teleport to="body">
|
|
||||||
<div
|
|
||||||
v-if="showServiceDetailPopover && selectedService"
|
|
||||||
ref="serviceDetailRef"
|
|
||||||
class="service-detail-popover"
|
|
||||||
:style="servicePopoverStyle"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-400px max-w-500px"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-8px mb-16px">
|
|
||||||
<Icon icon="ep:service" class="text-[var(--el-color-success)] text-18px" />
|
|
||||||
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">
|
|
||||||
{{ selectedService.name }}
|
|
||||||
</span>
|
|
||||||
<el-tag :type="getCallTypeTag(selectedService.callType)" size="small">
|
|
||||||
{{ getCallTypeLabel(selectedService.callType) }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-16px">
|
|
||||||
<!-- 基本信息 -->
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center gap-8px mb-8px">
|
|
||||||
<Icon icon="ep:info" class="text-[var(--el-color-info)] text-14px" />
|
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">基本信息</span>
|
|
||||||
</div>
|
|
||||||
<div class="ml-22px space-y-4px">
|
|
||||||
<div class="text-12px">
|
|
||||||
<span class="text-[var(--el-text-color-secondary)]">标识符:</span>
|
|
||||||
<span class="text-[var(--el-text-color-primary)]">{{
|
|
||||||
selectedService.identifier
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedService.description" class="text-12px">
|
|
||||||
<span class="text-[var(--el-text-color-secondary)]">描述:</span>
|
|
||||||
<span class="text-[var(--el-text-color-primary)]">{{
|
|
||||||
selectedService.description
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-12px">
|
|
||||||
<span class="text-[var(--el-text-color-secondary)]">调用方式:</span>
|
|
||||||
<span class="text-[var(--el-text-color-primary)]">{{
|
|
||||||
getCallTypeLabel(selectedService.callType)
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 输入参数 -->
|
|
||||||
<div v-if="selectedService.inputParams && selectedService.inputParams.length > 0">
|
|
||||||
<div class="flex items-center gap-8px mb-8px">
|
|
||||||
<Icon icon="ep:download" class="text-[var(--el-color-primary)] text-14px" />
|
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">输入参数</span>
|
|
||||||
</div>
|
|
||||||
<div class="ml-22px space-y-8px">
|
|
||||||
<div
|
|
||||||
v-for="param in selectedService.inputParams"
|
|
||||||
:key="param.identifier"
|
|
||||||
class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
|
|
||||||
>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="text-12px font-500 text-[var(--el-text-color-primary)]">
|
|
||||||
{{ param.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-11px text-[var(--el-text-color-secondary)]">
|
|
||||||
{{ param.identifier }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-8px">
|
|
||||||
<el-tag :type="getParamTypeTag(param.dataType)" size="small">
|
|
||||||
{{ getParamTypeName(param.dataType) }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 输出参数 -->
|
|
||||||
<div v-if="selectedService.outputParams && selectedService.outputParams.length > 0">
|
|
||||||
<div class="flex items-center gap-8px mb-8px">
|
|
||||||
<Icon icon="ep:upload" class="text-[var(--el-color-warning)] text-14px" />
|
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">输出参数</span>
|
|
||||||
</div>
|
|
||||||
<div class="ml-22px space-y-8px">
|
|
||||||
<div
|
|
||||||
v-for="param in selectedService.outputParams"
|
|
||||||
:key="param.identifier"
|
|
||||||
class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
|
|
||||||
>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="text-12px font-500 text-[var(--el-text-color-primary)]">
|
|
||||||
{{ param.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-11px text-[var(--el-text-color-secondary)]">
|
|
||||||
{{ param.identifier }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-8px">
|
|
||||||
<el-tag :type="getParamTypeTag(param.dataType)" size="small">
|
|
||||||
{{ getParamTypeName(param.dataType) }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 关闭按钮 -->
|
|
||||||
<div class="flex justify-end mt-16px">
|
|
||||||
<el-button size="small" @click="hideServiceDetail">关闭</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import { InfoFilled } from '@element-plus/icons-vue'
|
|
||||||
import { ThingModelApi } from '@/api/iot/thingmodel'
|
|
||||||
import { ThingModelService } from '@/api/iot/rule/scene/scene.types'
|
|
||||||
import { getThingModelServiceCallTypeLabel } from '@/views/iot/utils/constants'
|
|
||||||
|
|
||||||
/** 服务选择器组件 */
|
|
||||||
defineOptions({ name: 'ServiceSelector' })
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue?: string
|
|
||||||
productId?: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value?: string): void
|
|
||||||
(e: 'change', value?: string, service?: ThingModelService): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// TODO @puhui999:这里不用的
|
|
||||||
const localValue = useVModel(props, 'modelValue', emit)
|
|
||||||
|
|
||||||
// 状态
|
|
||||||
const loading = ref(false)
|
|
||||||
const serviceList = ref<ThingModelService[]>([])
|
|
||||||
const showServiceDetailPopover = ref(false)
|
|
||||||
const selectedService = ref<ThingModelService | null>(null)
|
|
||||||
const detailTriggerRef = ref()
|
|
||||||
const serviceDetailRef = ref()
|
|
||||||
const servicePopoverStyle = ref({})
|
|
||||||
|
|
||||||
// 事件处理
|
|
||||||
const handleChange = (value?: string) => {
|
|
||||||
const service = serviceList.value.find((s) => s.identifier === value)
|
|
||||||
emit('change', value, service)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取物模型TSL数据
|
|
||||||
const getThingModelTSL = async () => {
|
|
||||||
if (!props.productId) {
|
|
||||||
serviceList.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const tslData = await ThingModelApi.getThingModelTSLByProductId(props.productId)
|
|
||||||
serviceList.value = tslData?.services || []
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取物模型TSL失败:', error)
|
|
||||||
serviceList.value = []
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 工具函数
|
|
||||||
const getCallTypeLabel = (callType: string) => {
|
|
||||||
return getThingModelServiceCallTypeLabel(callType) || callType
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCallTypeTag = (callType: string) => {
|
|
||||||
return callType === 'sync' ? 'primary' : 'success'
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @puhui999:一些注释风格;
|
|
||||||
const getParamTypeName = (dataType: string) => {
|
|
||||||
const typeMap = {
|
|
||||||
int: '整数',
|
|
||||||
float: '浮点数',
|
|
||||||
double: '双精度',
|
|
||||||
text: '字符串',
|
|
||||||
bool: '布尔值',
|
|
||||||
enum: '枚举',
|
|
||||||
date: '日期',
|
|
||||||
struct: '结构体',
|
|
||||||
array: '数组'
|
|
||||||
}
|
|
||||||
return typeMap[dataType] || dataType
|
|
||||||
}
|
|
||||||
|
|
||||||
const getParamTypeTag = (dataType: string) => {
|
|
||||||
const tagMap = {
|
|
||||||
int: 'primary',
|
|
||||||
float: 'success',
|
|
||||||
double: 'success',
|
|
||||||
text: 'info',
|
|
||||||
bool: 'warning',
|
|
||||||
enum: 'danger',
|
|
||||||
date: 'primary',
|
|
||||||
struct: 'info',
|
|
||||||
array: 'warning'
|
|
||||||
}
|
|
||||||
return tagMap[dataType] || 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 服务详情弹出层控制
|
|
||||||
const showServiceDetail = (service: ThingModelService) => {
|
|
||||||
selectedService.value = service
|
|
||||||
showServiceDetailPopover.value = true
|
|
||||||
|
|
||||||
nextTick(() => {
|
|
||||||
updateServicePopoverPosition()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideServiceDetail = () => {
|
|
||||||
showServiceDetailPopover.value = false
|
|
||||||
selectedService.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateServicePopoverPosition = () => {
|
|
||||||
if (!detailTriggerRef.value || !serviceDetailRef.value) return
|
|
||||||
|
|
||||||
const triggerEl = detailTriggerRef.value.$el
|
|
||||||
const triggerRect = triggerEl.getBoundingClientRect()
|
|
||||||
|
|
||||||
// 计算弹出层位置
|
|
||||||
const left = triggerRect.left + triggerRect.width + 8
|
|
||||||
const top = triggerRect.top
|
|
||||||
|
|
||||||
// 检查是否超出视窗右边界
|
|
||||||
const popoverWidth = 500
|
|
||||||
const viewportWidth = window.innerWidth
|
|
||||||
|
|
||||||
let finalLeft = left
|
|
||||||
if (left + popoverWidth > viewportWidth - 16) {
|
|
||||||
finalLeft = triggerRect.left - popoverWidth - 8
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否超出视窗下边界
|
|
||||||
let finalTop = top
|
|
||||||
const popoverHeight = serviceDetailRef.value.offsetHeight || 300
|
|
||||||
const viewportHeight = window.innerHeight
|
|
||||||
|
|
||||||
if (top + popoverHeight > viewportHeight - 16) {
|
|
||||||
finalTop = Math.max(16, viewportHeight - popoverHeight - 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
servicePopoverStyle.value = {
|
|
||||||
position: 'fixed',
|
|
||||||
left: `${finalLeft}px`,
|
|
||||||
top: `${finalTop}px`,
|
|
||||||
zIndex: 9999
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听产品变化
|
|
||||||
watch(
|
|
||||||
() => props.productId,
|
|
||||||
() => {
|
|
||||||
getThingModelTSL()
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听modelValue变化,处理编辑模式的回显
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(newValue) => {
|
|
||||||
console.log('🔄 ServiceSelector modelValue changed:', {
|
|
||||||
newValue,
|
|
||||||
serviceListLength: serviceList.value.length,
|
|
||||||
serviceList: serviceList.value.map((s) => s.identifier)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (newValue && serviceList.value.length > 0) {
|
|
||||||
// 确保服务列表已加载,然后设置选中的服务
|
|
||||||
const service = serviceList.value.find((s) => s.identifier === newValue)
|
|
||||||
console.log('🎯 ServiceSelector found service:', service)
|
|
||||||
|
|
||||||
if (service) {
|
|
||||||
selectedService.value = service
|
|
||||||
console.log('✅ ServiceSelector service set:', service.name)
|
|
||||||
} else {
|
|
||||||
console.warn('⚠️ ServiceSelector service not found for identifier:', newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听服务列表变化,处理异步加载后的回显
|
|
||||||
watch(
|
|
||||||
() => serviceList.value,
|
|
||||||
(newServiceList) => {
|
|
||||||
console.log('📋 ServiceSelector serviceList changed:', {
|
|
||||||
length: newServiceList.length,
|
|
||||||
services: newServiceList.map((s) => s.identifier),
|
|
||||||
modelValue: props.modelValue
|
|
||||||
})
|
|
||||||
|
|
||||||
if (newServiceList.length > 0 && props.modelValue) {
|
|
||||||
// 服务列表加载完成后,如果有modelValue,设置选中的服务
|
|
||||||
const service = newServiceList.find((s) => s.identifier === props.modelValue)
|
|
||||||
console.log('🎯 ServiceSelector found service in list:', service)
|
|
||||||
|
|
||||||
if (service) {
|
|
||||||
selectedService.value = service
|
|
||||||
console.log('✅ ServiceSelector service set from list:', service.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听窗口大小变化
|
|
||||||
const handleResize = () => {
|
|
||||||
if (showServiceDetailPopover.value) {
|
|
||||||
updateServicePopoverPosition()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击外部关闭弹出层
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
showServiceDetailPopover.value &&
|
|
||||||
serviceDetailRef.value &&
|
|
||||||
detailTriggerRef.value &&
|
|
||||||
!serviceDetailRef.value.contains(event.target as Node) &&
|
|
||||||
!detailTriggerRef.value.$el.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
hideServiceDetail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生命周期
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-detail-popover {
|
|
||||||
animation: fadeInScale 0.2s ease-out;
|
|
||||||
transform-origin: top left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-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: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-detail-popover::after {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: -7px;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-top: 8px solid transparent;
|
|
||||||
border-right: 8px solid white;
|
|
||||||
border-bottom: 8px solid transparent;
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-select-dropdown__item) {
|
|
||||||
height: auto;
|
|
||||||
padding: 8px 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -239,7 +239,7 @@
|
||||||
import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict'
|
import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict'
|
||||||
import { ContentWrap } from '@/components/ContentWrap'
|
import { ContentWrap } from '@/components/ContentWrap'
|
||||||
import RuleSceneForm from './form/RuleSceneForm.vue'
|
import RuleSceneForm from './form/RuleSceneForm.vue'
|
||||||
import { IotRuleSceneDO } from '@/api/iot/rule/scene/scene.types'
|
import { IotSceneRule } from '@/api/iot/rule/scene/scene.types'
|
||||||
import { RuleSceneApi } from '@/api/iot/rule/scene'
|
import { RuleSceneApi } from '@/api/iot/rule/scene'
|
||||||
import {
|
import {
|
||||||
IotRuleSceneTriggerTypeEnum,
|
IotRuleSceneTriggerTypeEnum,
|
||||||
|
|
@ -264,14 +264,14 @@ const queryParams = reactive({
|
||||||
})
|
})
|
||||||
|
|
||||||
const loading = ref(true) // 列表的加载中
|
const loading = ref(true) // 列表的加载中
|
||||||
const list = ref<IotRuleSceneDO[]>([]) // 列表的数据
|
const list = ref<IotSceneRule[]>([]) // 列表的数据
|
||||||
const total = ref(0) // 列表的总页数
|
const total = ref(0) // 列表的总页数
|
||||||
const selectedRows = ref<IotRuleSceneDO[]>([]) // 选中的行数据
|
const selectedRows = ref<IotSceneRule[]>([]) // 选中的行数据
|
||||||
const queryFormRef = ref() // 搜索的表单
|
const queryFormRef = ref() // 搜索的表单
|
||||||
|
|
||||||
/** 表单状态 */
|
/** 表单状态 */
|
||||||
const formVisible = ref(false) // 是否可见
|
const formVisible = ref(false) // 是否可见
|
||||||
const currentRule = ref<IotRuleSceneDO>() // 表单数据
|
const currentRule = ref<IotSceneRule>() // 表单数据
|
||||||
|
|
||||||
/** 统计数据 */
|
/** 统计数据 */
|
||||||
const statistics = ref({
|
const statistics = ref({
|
||||||
|
|
@ -327,7 +327,7 @@ const formatCronExpression = (cron: string): string => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取规则摘要信息 */
|
/** 获取规则摘要信息 */
|
||||||
const getRuleSceneSummary = (rule: IotRuleSceneDO) => {
|
const getRuleSceneSummary = (rule: IotSceneRule) => {
|
||||||
const triggerSummary =
|
const triggerSummary =
|
||||||
rule.triggers?.map((trigger: any) => {
|
rule.triggers?.map((trigger: any) => {
|
||||||
// 构建基础描述
|
// 构建基础描述
|
||||||
|
|
@ -455,12 +455,12 @@ const updateStatistics = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取触发器摘要 */
|
/** 获取触发器摘要 */
|
||||||
const getTriggerSummary = (rule: IotRuleSceneDO) => {
|
const getTriggerSummary = (rule: IotSceneRule) => {
|
||||||
return getRuleSceneSummary(rule).triggerSummary
|
return getRuleSceneSummary(rule).triggerSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取执行器摘要 */
|
/** 获取执行器摘要 */
|
||||||
const getActionSummary = (rule: IotRuleSceneDO) => {
|
const getActionSummary = (rule: IotSceneRule) => {
|
||||||
return getRuleSceneSummary(rule).actionSummary
|
return getRuleSceneSummary(rule).actionSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -484,7 +484,7 @@ const handleAdd = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 修改操作 */
|
/** 修改操作 */
|
||||||
const handleEdit = (row: IotRuleSceneDO) => {
|
const handleEdit = (row: IotSceneRule) => {
|
||||||
currentRule.value = row
|
currentRule.value = row
|
||||||
formVisible.value = true
|
formVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
@ -506,7 +506,7 @@ const handleDelete = async (id: number) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 修改状态 */
|
/** 修改状态 */
|
||||||
const handleToggleStatus = async (row: IotRuleSceneDO) => {
|
const handleToggleStatus = async (row: IotSceneRule) => {
|
||||||
try {
|
try {
|
||||||
// 修改状态的二次确认
|
// 修改状态的二次确认
|
||||||
// TODO @puhui999:status 枚举;
|
// TODO @puhui999:status 枚举;
|
||||||
|
|
@ -525,7 +525,7 @@ const handleToggleStatus = async (row: IotRuleSceneDO) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 多选框选中数据 */
|
/** 多选框选中数据 */
|
||||||
const handleSelectionChange = (selection: IotRuleSceneDO[]) => {
|
const handleSelectionChange = (selection: IotSceneRule[]) => {
|
||||||
selectedRows.value = selection
|
selectedRows.value = selection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,9 +109,19 @@ export const IoTThingModelAccessModeEnum = {
|
||||||
READ_ONLY: {
|
READ_ONLY: {
|
||||||
label: '只读',
|
label: '只读',
|
||||||
value: 'r'
|
value: 'r'
|
||||||
|
},
|
||||||
|
WRITE_ONLY: {
|
||||||
|
label: '只写',
|
||||||
|
value: 'w'
|
||||||
}
|
}
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
/** 获取访问模式标签 */
|
||||||
|
export const getAccessModeLabel = (value: string): string => {
|
||||||
|
const mode = Object.values(IoTThingModelAccessModeEnum).find((mode) => mode.value === value)
|
||||||
|
return mode?.label || value
|
||||||
|
}
|
||||||
|
|
||||||
/** 属性值的数据类型 */
|
/** 属性值的数据类型 */
|
||||||
export const IoTDataSpecsDataTypeEnum = {
|
export const IoTDataSpecsDataTypeEnum = {
|
||||||
INT: 'int',
|
INT: 'int',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue