commit
9ca86c292b
|
|
@ -23,6 +23,14 @@ export const RuleSceneApi = {
|
||||||
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) => {
|
||||||
|
return await request.put({ url: `/iot/rule-scene/update-status`, data: {
|
||||||
|
id,
|
||||||
|
status
|
||||||
|
}})
|
||||||
|
},
|
||||||
|
|
||||||
// 删除场景联动
|
// 删除场景联动
|
||||||
deleteRuleScene: async (id: number) => {
|
deleteRuleScene: async (id: number) => {
|
||||||
return await request.delete({ url: `/iot/rule-scene/delete?id=` + id })
|
return await request.delete({ url: `/iot/rule-scene/delete?id=` + id })
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,7 @@ interface ActionFormData {
|
||||||
type: number // 执行类型
|
type: number // 执行类型
|
||||||
productId?: number // 产品编号
|
productId?: number // 产品编号
|
||||||
deviceId?: number // 设备编号
|
deviceId?: number // 设备编号
|
||||||
|
identifier?: string // 物模型标识符(服务调用时使用)
|
||||||
params?: Record<string, any> // 请求参数
|
params?: Record<string, any> // 请求参数
|
||||||
alertConfigId?: number // 告警配置编号
|
alertConfigId?: number // 告警配置编号
|
||||||
}
|
}
|
||||||
|
|
@ -277,6 +278,7 @@ interface ActionDO {
|
||||||
type: number // 执行类型
|
type: number // 执行类型
|
||||||
productId?: number // 产品编号
|
productId?: number // 产品编号
|
||||||
deviceId?: number // 设备编号
|
deviceId?: number // 设备编号
|
||||||
|
identifier?: string // 物模型标识符(服务调用时使用)
|
||||||
params?: Record<string, any> // 请求参数
|
params?: Record<string, any> // 请求参数
|
||||||
alertConfigId?: number // 告警配置编号
|
alertConfigId?: number // 告警配置编号
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@
|
||||||
<!-- 基础信息配置 -->
|
<!-- 基础信息配置 -->
|
||||||
<BasicInfoSection v-model="formData" :rules="formRules" />
|
<BasicInfoSection v-model="formData" :rules="formRules" />
|
||||||
<!-- 触发器配置 -->
|
<!-- 触发器配置 -->
|
||||||
<TriggerSection v-model:triggers="formData.triggers" @validate="handleTriggerValidate" />
|
<TriggerSection v-model:triggers="formData.triggers" />
|
||||||
<!-- 执行器配置 -->
|
<!-- 执行器配置 -->
|
||||||
<ActionSection v-model:actions="formData.actions" @validate="handleActionValidate" />
|
<ActionSection v-model:actions="formData.actions" />
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="drawer-footer">
|
<div class="drawer-footer">
|
||||||
|
|
@ -37,16 +37,14 @@ 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 { IotRuleSceneDO, RuleSceneFormData } from '@/api/iot/rule/scene/scene.types'
|
||||||
import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
|
import { RuleSceneApi } from '@/api/iot/rule/scene'
|
||||||
|
import {
|
||||||
|
IotRuleSceneTriggerTypeEnum,
|
||||||
|
IotRuleSceneActionTypeEnum,
|
||||||
|
isDeviceTrigger
|
||||||
|
} from '@/views/iot/utils/constants'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { generateUUID } from '@/utils'
|
import { CommonStatusEnum } from '@/utils/constants'
|
||||||
|
|
||||||
// 导入全局的 CommonStatusEnum
|
|
||||||
// TODO @puhui999:这里直接复用全局的哈;
|
|
||||||
const CommonStatusEnum = {
|
|
||||||
ENABLE: 0, // 开启
|
|
||||||
DISABLE: 1 // 关闭
|
|
||||||
} as const
|
|
||||||
|
|
||||||
/** IoT 场景联动规则表单 - 主表单组件 */
|
/** IoT 场景联动规则表单 - 主表单组件 */
|
||||||
defineOptions({ name: 'RuleSceneForm' })
|
defineOptions({ name: 'RuleSceneForm' })
|
||||||
|
|
@ -55,6 +53,8 @@ defineOptions({ name: 'RuleSceneForm' })
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** 抽屉显示状态 */
|
/** 抽屉显示状态 */
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
|
/** 编辑的场景联动规则数据 */
|
||||||
|
ruleScene?: IotRuleSceneDO
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
/** 组件事件定义 */
|
/** 组件事件定义 */
|
||||||
|
|
@ -87,90 +87,119 @@ const createDefaultFormData = (): RuleSceneFormData => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 将表单数据转换为后端 DO 格式
|
|
||||||
* 由于数据结构已对齐,转换变得非常简单
|
|
||||||
*/
|
|
||||||
const convertFormToVO = (formData: RuleSceneFormData): IotRuleSceneDO => {
|
|
||||||
return {
|
|
||||||
id: formData.id,
|
|
||||||
name: formData.name,
|
|
||||||
description: formData.description,
|
|
||||||
status: Number(formData.status),
|
|
||||||
triggers: formData.triggers.map((trigger) => ({
|
|
||||||
type: trigger.type,
|
|
||||||
productId: trigger.productId,
|
|
||||||
deviceId: trigger.deviceId,
|
|
||||||
identifier: trigger.identifier,
|
|
||||||
operator: trigger.operator,
|
|
||||||
value: trigger.value,
|
|
||||||
cronExpression: trigger.cronExpression,
|
|
||||||
conditionGroups: trigger.conditionGroups || []
|
|
||||||
})),
|
|
||||||
actions:
|
|
||||||
formData.actions?.map((action) => ({
|
|
||||||
type: action.type,
|
|
||||||
productId: action.productId,
|
|
||||||
deviceId: action.deviceId,
|
|
||||||
params: action.params,
|
|
||||||
alertConfigId: action.alertConfigId
|
|
||||||
})) || []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @puhui999:下面好像没用到?
|
|
||||||
/**
|
|
||||||
* 将后端 DO 数据转换为表单格式
|
|
||||||
* 由于数据结构已对齐,转换变得非常简单
|
|
||||||
*/
|
|
||||||
const convertVOToForm = (apiData: IotRuleSceneDO): RuleSceneFormData => {
|
|
||||||
// 转换所有触发器
|
|
||||||
const triggers = apiData.triggers?.length
|
|
||||||
? apiData.triggers.map((trigger: any) => ({
|
|
||||||
type: Number(trigger.type),
|
|
||||||
productId: trigger.productId,
|
|
||||||
deviceId: trigger.deviceId,
|
|
||||||
identifier: trigger.identifier,
|
|
||||||
operator: trigger.operator,
|
|
||||||
value: trigger.value,
|
|
||||||
cronExpression: trigger.cronExpression,
|
|
||||||
conditionGroups: trigger.conditionGroups || []
|
|
||||||
}))
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
|
|
||||||
productId: undefined,
|
|
||||||
deviceId: undefined,
|
|
||||||
identifier: undefined,
|
|
||||||
operator: undefined,
|
|
||||||
value: undefined,
|
|
||||||
cronExpression: undefined,
|
|
||||||
conditionGroups: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: apiData.id,
|
|
||||||
name: apiData.name,
|
|
||||||
description: apiData.description,
|
|
||||||
status: Number(apiData.status),
|
|
||||||
triggers,
|
|
||||||
actions:
|
|
||||||
apiData.actions?.map((action: any) => ({
|
|
||||||
type: Number(action.type),
|
|
||||||
productId: action.productId,
|
|
||||||
deviceId: action.deviceId,
|
|
||||||
params: action.params || {},
|
|
||||||
alertConfigId: action.alertConfigId,
|
|
||||||
// 为每个执行器添加唯一标识符,解决组件索引重用问题
|
|
||||||
key: generateUUID()
|
|
||||||
})) || []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 表单数据和状态
|
// 表单数据和状态
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const formData = ref<RuleSceneFormData>(createDefaultFormData())
|
const formData = ref<RuleSceneFormData>(createDefaultFormData())
|
||||||
|
// 自定义校验器
|
||||||
|
const validateTriggers = (_rule: any, value: any, callback: any) => {
|
||||||
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||||
|
callback(new Error('至少需要一个触发器'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
const trigger = value[i]
|
||||||
|
|
||||||
|
// 校验触发器类型
|
||||||
|
if (!trigger.type) {
|
||||||
|
callback(new Error(`触发器 ${i + 1}: 触发器类型不能为空`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验设备触发器
|
||||||
|
if (isDeviceTrigger(trigger.type)) {
|
||||||
|
if (!trigger.productId) {
|
||||||
|
callback(new Error(`触发器 ${i + 1}: 产品不能为空`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!trigger.deviceId) {
|
||||||
|
callback(new Error(`触发器 ${i + 1}: 设备不能为空`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!trigger.identifier) {
|
||||||
|
callback(new Error(`触发器 ${i + 1}: 物模型标识符不能为空`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!trigger.operator) {
|
||||||
|
callback(new Error(`触发器 ${i + 1}: 操作符不能为空`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (trigger.value === undefined || trigger.value === null || trigger.value === '') {
|
||||||
|
callback(new Error(`触发器 ${i + 1}: 参数值不能为空`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验定时触发器
|
||||||
|
if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
|
||||||
|
if (!trigger.cronExpression) {
|
||||||
|
callback(new Error(`触发器 ${i + 1}: CRON表达式不能为空`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateActions = (_rule: any, value: any, callback: any) => {
|
||||||
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||||
|
callback(new Error('至少需要一个执行器'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
const action = value[i]
|
||||||
|
|
||||||
|
// 校验执行器类型
|
||||||
|
if (!action.type) {
|
||||||
|
callback(new Error(`执行器 ${i + 1}: 执行器类型不能为空`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验设备控制执行器
|
||||||
|
if (
|
||||||
|
action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
|
||||||
|
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
|
||||||
|
) {
|
||||||
|
if (!action.productId) {
|
||||||
|
callback(new Error(`执行器 ${i + 1}: 产品不能为空`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!action.deviceId) {
|
||||||
|
callback(new Error(`执行器 ${i + 1}: 设备不能为空`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务调用需要验证服务标识符
|
||||||
|
if (action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE) {
|
||||||
|
if (!action.identifier) {
|
||||||
|
callback(new Error(`执行器 ${i + 1}: 服务不能为空`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!action.params || Object.keys(action.params).length === 0) {
|
||||||
|
callback(new Error(`执行器 ${i + 1}: 参数配置不能为空`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验告警执行器
|
||||||
|
if (
|
||||||
|
action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER ||
|
||||||
|
action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER
|
||||||
|
) {
|
||||||
|
if (!action.alertConfigId) {
|
||||||
|
callback(new Error(`执行器 ${i + 1}: 告警配置不能为空`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
|
||||||
const formRules = reactive({
|
const formRules = reactive({
|
||||||
name: [
|
name: [
|
||||||
{ required: true, message: '场景名称不能为空', trigger: 'blur' },
|
{ required: true, message: '场景名称不能为空', trigger: 'blur' },
|
||||||
|
|
@ -188,77 +217,41 @@ const formRules = reactive({
|
||||||
description: [
|
description: [
|
||||||
{ type: 'string', max: 200, message: '场景描述不能超过200个字符', trigger: 'blur' }
|
{ type: 'string', max: 200, message: '场景描述不能超过200个字符', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
triggers: [
|
triggers: [{ required: true, validator: validateTriggers, trigger: 'change' }],
|
||||||
{ required: true, message: '触发器数组不能为空', trigger: 'change' },
|
actions: [{ required: true, validator: validateActions, trigger: 'change' }]
|
||||||
{ type: 'array', min: 1, message: '至少需要一个触发器', trigger: 'change' }
|
|
||||||
],
|
|
||||||
actions: [
|
|
||||||
{ required: true, message: '执行器数组不能为空', trigger: 'change' },
|
|
||||||
{ type: 'array', min: 1, message: '至少需要一个执行器', trigger: 'change' }
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
|
|
||||||
// 验证状态
|
|
||||||
const triggerValidation = ref({ valid: true, message: '' })
|
|
||||||
const actionValidation = ref({ valid: true, message: '' })
|
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const isEdit = ref(false)
|
const isEdit = ref(false)
|
||||||
const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' : '新增场景联动规则'))
|
const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' : '新增场景联动规则'))
|
||||||
|
|
||||||
// TODO @puhui999:方法的注释风格统一;
|
/** 提交表单 */
|
||||||
// 事件处理
|
|
||||||
const handleTriggerValidate = (result: { valid: boolean; message: string }) => {
|
|
||||||
triggerValidation.value = result
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleActionValidate = (result: { valid: boolean; message: string }) => {
|
|
||||||
actionValidation.value = result
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO @puhui999:API 调用
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
// 校验表单
|
// 校验表单
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
const valid = await formRef.value.validate()
|
const valid = await formRef.value.validate()
|
||||||
if (!valid) return
|
if (!valid) return
|
||||||
// 验证触发器和执行器
|
|
||||||
if (!triggerValidation.value.valid) {
|
|
||||||
ElMessage.error(triggerValidation.value.message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!actionValidation.value.valid) {
|
|
||||||
ElMessage.error(actionValidation.value.message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交请求
|
// 提交请求
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
try {
|
try {
|
||||||
console.log(formData.value)
|
// 数据结构已对齐,直接使用表单数据
|
||||||
// 转换数据格式
|
console.log('提交数据:', formData.value)
|
||||||
const apiData = convertFormToVO(formData.value)
|
|
||||||
if (true) {
|
|
||||||
console.log('转换后', apiData)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 调用API保存数据
|
// 调用API保存数据
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
// 更新场景联动规则
|
// 更新场景联动规则
|
||||||
// await RuleSceneApi.updateRuleScene(apiData)
|
await RuleSceneApi.updateRuleScene(formData.value)
|
||||||
console.log('更新数据:', apiData)
|
ElMessage.success('更新成功')
|
||||||
} else {
|
} else {
|
||||||
// 创建场景联动规则
|
// 创建场景联动规则
|
||||||
// await RuleSceneApi.createRuleScene(apiData)
|
await RuleSceneApi.createRuleScene(formData.value)
|
||||||
console.log('创建数据:', apiData)
|
ElMessage.success('创建成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟API调用
|
// 关闭抽屉并触发成功事件
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
||||||
|
|
||||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
|
||||||
drawerVisible.value = false
|
drawerVisible.value = false
|
||||||
emit('success')
|
emit('success')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -275,28 +268,55 @@ const handleClose = () => {
|
||||||
|
|
||||||
/** 初始化表单数据 */
|
/** 初始化表单数据 */
|
||||||
const initFormData = () => {
|
const initFormData = () => {
|
||||||
// TODO @puhui999: 编辑的情况后面实现
|
if (props.ruleScene) {
|
||||||
formData.value = createDefaultFormData()
|
// 编辑模式:数据结构已对齐,直接使用后端数据
|
||||||
|
isEdit.value = true
|
||||||
|
formData.value = {
|
||||||
|
...props.ruleScene,
|
||||||
|
// 确保触发器数组不为空
|
||||||
|
triggers: props.ruleScene.triggers?.length
|
||||||
|
? props.ruleScene.triggers
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
|
||||||
|
productId: undefined,
|
||||||
|
deviceId: undefined,
|
||||||
|
identifier: undefined,
|
||||||
|
operator: undefined,
|
||||||
|
value: undefined,
|
||||||
|
cronExpression: undefined,
|
||||||
|
conditionGroups: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// 确保执行器数组不为空
|
||||||
|
actions: props.ruleScene.actions || []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 新增模式:使用默认数据
|
||||||
|
isEdit.value = false
|
||||||
|
formData.value = createDefaultFormData()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听抽屉显示
|
// 监听抽屉显示
|
||||||
watch(drawerVisible, (visible) => {
|
watch(drawerVisible, (visible) => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
initFormData()
|
initFormData()
|
||||||
// TODO @puhui999: 重置表单的情况
|
// 重置表单验证状态
|
||||||
// nextTick(() => {
|
nextTick(() => {
|
||||||
// formRef.value?.clearValidate()
|
formRef.value?.clearValidate()
|
||||||
// })
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听 props 变化
|
// 监听编辑数据变化
|
||||||
// watch(
|
watch(
|
||||||
// () => props.ruleScene,
|
() => props.ruleScene,
|
||||||
// () => {
|
() => {
|
||||||
// if (drawerVisible.value) {
|
if (drawerVisible.value) {
|
||||||
// initFormData()
|
initFormData()
|
||||||
// }
|
}
|
||||||
// }
|
},
|
||||||
// )
|
{ deep: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,154 @@
|
||||||
<!-- 告警配置组件 -->
|
<!-- 告警配置组件 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<!-- TODO @puhui999:触发告警时,不用选择配置哈; -->
|
<!-- 告警配置选择区域 -->
|
||||||
<el-form-item label="告警配置" required>
|
<div
|
||||||
<el-select
|
class="border border-[var(--el-border-color-light)] rounded-6px p-16px bg-[var(--el-fill-color-blank)]"
|
||||||
v-model="localValue"
|
>
|
||||||
placeholder="请选择告警配置"
|
|
||||||
filterable
|
|
||||||
clearable
|
|
||||||
@change="handleChange"
|
|
||||||
class="w-full"
|
|
||||||
:loading="loading"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="config in alertConfigs"
|
|
||||||
:key="config.id"
|
|
||||||
:label="config.name"
|
|
||||||
:value="config.id"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between w-full py-4px">
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ config.name }}</div>
|
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ config.description }}</div>
|
|
||||||
</div>
|
|
||||||
<el-tag :type="config.enabled ? 'success' : 'danger'" size="small">
|
|
||||||
{{ config.enabled ? '启用' : '禁用' }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</el-option>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 告警配置详情 -->
|
|
||||||
<div v-if="selectedConfig" class="mt-16px p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]">
|
|
||||||
<div class="flex items-center gap-8px mb-12px">
|
<div class="flex items-center gap-8px mb-12px">
|
||||||
<Icon icon="ep:bell" class="text-[var(--el-color-warning)] text-16px" />
|
<Icon icon="ep:bell" class="text-[var(--el-color-warning)] text-16px" />
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ selectedConfig.name }}</span>
|
<span class="text-14px font-600 text-[var(--el-text-color-primary)]">告警配置选择</span>
|
||||||
<el-tag :type="selectedConfig.enabled ? 'success' : 'danger'" size="small">
|
<el-tag size="small" type="warning">必选</el-tag>
|
||||||
{{ selectedConfig.enabled ? '启用' : '禁用' }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-8px">
|
|
||||||
<div class="flex items-start gap-8px">
|
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">描述:</span>
|
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ 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>
|
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ getNotifyTypeName(selectedConfig.notifyType) }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedConfig.receivers" 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.receivers.join(', ') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-form-item label="告警配置" required>
|
||||||
|
<el-select
|
||||||
|
v-model="localValue"
|
||||||
|
placeholder="请选择告警配置"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
@change="handleChange"
|
||||||
|
class="w-full"
|
||||||
|
:loading="loading"
|
||||||
|
>
|
||||||
|
<template #empty>
|
||||||
|
<div class="text-center py-20px">
|
||||||
|
<Icon
|
||||||
|
icon="ep:warning"
|
||||||
|
class="text-24px text-[var(--el-text-color-placeholder)] mb-8px"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
|
||||||
<!-- 验证结果 -->
|
<!-- 告警配置详情 -->
|
||||||
<div v-if="validationMessage" class="mt-16px">
|
<div
|
||||||
<el-alert
|
v-if="selectedConfig"
|
||||||
:title="validationMessage"
|
class="mt-16px border border-[var(--el-border-color-light)] rounded-6px p-16px bg-gradient-to-r from-orange-50 to-yellow-50"
|
||||||
:type="isValid ? 'success' : 'error'"
|
>
|
||||||
:closable="false"
|
<div class="flex items-center gap-8px mb-16px">
|
||||||
show-icon
|
<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 class="pl-22px space-y-8px">
|
||||||
|
<div class="flex items-start gap-8px">
|
||||||
|
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">名称:</span>
|
||||||
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1 font-500">{{
|
||||||
|
selectedConfig.name
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-8px">
|
||||||
|
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">描述:</span>
|
||||||
|
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{
|
||||||
|
selectedConfig.description
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-8px">
|
||||||
|
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px">状态:</span>
|
||||||
|
<el-tag :type="selectedConfig.enabled ? 'success' : 'danger'" size="small">
|
||||||
|
{{ selectedConfig.enabled ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 通知配置 -->
|
||||||
|
<div 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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -80,7 +165,6 @@ interface Props {
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'update:modelValue', value?: number): void
|
(e: 'update:modelValue', value?: number): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
@ -91,8 +175,6 @@ const localValue = useVModel(props, 'modelValue', emit)
|
||||||
// 状态
|
// 状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const alertConfigs = ref<any[]>([])
|
const alertConfigs = ref<any[]>([])
|
||||||
const validationMessage = ref('')
|
|
||||||
const isValid = ref(true)
|
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const selectedConfig = computed(() => {
|
const selectedConfig = computed(() => {
|
||||||
|
|
@ -110,38 +192,20 @@ const getNotifyTypeName = (type: number) => {
|
||||||
return typeMap[type] || '未知'
|
return typeMap[type] || '未知'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件处理
|
const getNotifyTypeTag = (type: number) => {
|
||||||
const handleChange = () => {
|
const tagMap = {
|
||||||
updateValidationResult()
|
1: 'primary', // 邮件
|
||||||
|
2: 'success', // 短信
|
||||||
|
3: 'warning', // 微信
|
||||||
|
4: 'info' // 钉钉
|
||||||
|
}
|
||||||
|
return tagMap[type] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateValidationResult = () => {
|
// 事件处理
|
||||||
if (!localValue.value) {
|
const handleChange = (value?: number) => {
|
||||||
isValid.value = false
|
// 可以在这里添加额外的处理逻辑
|
||||||
validationMessage.value = '请选择告警配置'
|
console.log('告警配置选择变化:', value)
|
||||||
emit('validate', { valid: false, message: validationMessage.value })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = selectedConfig.value
|
|
||||||
if (!config) {
|
|
||||||
isValid.value = false
|
|
||||||
validationMessage.value = '告警配置不存在'
|
|
||||||
emit('validate', { valid: false, message: validationMessage.value })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.enabled) {
|
|
||||||
isValid.value = false
|
|
||||||
validationMessage.value = '选择的告警配置已禁用'
|
|
||||||
emit('validate', { valid: false, message: validationMessage.value })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证通过
|
|
||||||
isValid.value = true
|
|
||||||
validationMessage.value = '告警配置验证通过'
|
|
||||||
emit('validate', { valid: true, message: validationMessage.value })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// API 调用
|
// API 调用
|
||||||
|
|
@ -184,20 +248,9 @@ const getAlertConfigs = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听值变化
|
|
||||||
watch(
|
|
||||||
() => localValue.value,
|
|
||||||
() => {
|
|
||||||
updateValidationResult()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getAlertConfigs()
|
getAlertConfigs()
|
||||||
if (localValue.value) {
|
|
||||||
updateValidationResult()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -154,11 +154,17 @@ const addSubGroup = () => {
|
||||||
container.value = []
|
container.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (container.value.length >= maxSubGroups) {
|
// 检查是否达到最大子组数量限制
|
||||||
|
if (container.value?.length >= maxSubGroups) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
container.value.push([])
|
// 使用 nextTick 确保响应式更新完成后再添加新的子组
|
||||||
|
nextTick(() => {
|
||||||
|
if (container.value) {
|
||||||
|
container.value.push([])
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeSubGroup = (index: number) => {
|
const removeSubGroup = (index: number) => {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -6,7 +6,7 @@
|
||||||
<MainConditionConfig
|
<MainConditionConfig
|
||||||
v-model="trigger"
|
v-model="trigger"
|
||||||
:trigger-type="trigger.type"
|
:trigger-type="trigger.type"
|
||||||
@validate="handleMainConditionValidate"
|
@trigger-type-change="handleTriggerTypeChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
<ConditionGroupContainerConfig
|
<ConditionGroupContainerConfig
|
||||||
v-model="trigger.conditionGroups"
|
v-model="trigger.conditionGroups"
|
||||||
:trigger-type="trigger.type"
|
:trigger-type="trigger.type"
|
||||||
@validate="handleConditionGroupValidate"
|
|
||||||
@remove="removeConditionGroup"
|
@remove="removeConditionGroup"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -42,18 +41,11 @@ const props = defineProps<{
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: TriggerFormData): void
|
(e: 'update:modelValue', value: TriggerFormData): void
|
||||||
(e: 'validate', value: { valid: boolean; message: string }): void
|
(e: 'validate', value: { valid: boolean; message: string }): void
|
||||||
|
(e: 'trigger-type-change', type: number): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const trigger = useVModel(props, 'modelValue', emit)
|
const trigger = useVModel(props, 'modelValue', emit)
|
||||||
|
|
||||||
// 验证状态
|
|
||||||
const mainConditionValidation = ref<{ valid: boolean; message: string }>({
|
|
||||||
valid: true,
|
|
||||||
message: ''
|
|
||||||
})
|
|
||||||
const validationMessage = ref('')
|
|
||||||
const isValid = ref(true)
|
|
||||||
|
|
||||||
// 初始化主条件
|
// 初始化主条件
|
||||||
const initMainCondition = () => {
|
const initMainCondition = () => {
|
||||||
// TODO @puhui999: 等到编辑回显时联调
|
// TODO @puhui999: 等到编辑回显时联调
|
||||||
|
|
@ -78,78 +70,12 @@ watch(
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleMainConditionValidate = (result: { valid: boolean; message: string }) => {
|
const handleTriggerTypeChange = (type: number) => {
|
||||||
mainConditionValidation.value = result
|
trigger.value.type = type
|
||||||
updateValidationResult()
|
emit('trigger-type-change', type)
|
||||||
}
|
|
||||||
|
|
||||||
const addConditionGroup = () => {
|
|
||||||
if (!trigger.value.conditionGroups) {
|
|
||||||
trigger.value.conditionGroups = []
|
|
||||||
}
|
|
||||||
trigger.value.conditionGroups.push([])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 事件处理
|
|
||||||
const handleConditionGroupValidate = () => {
|
|
||||||
updateValidationResult()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeConditionGroup = () => {
|
const removeConditionGroup = () => {
|
||||||
trigger.value.conditionGroups = undefined
|
trigger.value.conditionGroups = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateValidationResult = () => {
|
|
||||||
// 主条件验证
|
|
||||||
if (!mainConditionValidation.value.valid) {
|
|
||||||
isValid.value = false
|
|
||||||
validationMessage.value = mainConditionValidation.value.message
|
|
||||||
emit('validate', { valid: false, message: validationMessage.value })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设备状态变更不需要条件验证
|
|
||||||
if (trigger.value.type === TriggerTypeEnum.DEVICE_STATE_UPDATE) {
|
|
||||||
isValid.value = true
|
|
||||||
validationMessage.value = '设备触发配置验证通过'
|
|
||||||
emit('validate', { valid: true, message: validationMessage.value })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主条件验证
|
|
||||||
if (!trigger.value.value) {
|
|
||||||
isValid.value = false
|
|
||||||
validationMessage.value = '请配置主条件'
|
|
||||||
emit('validate', { valid: false, message: validationMessage.value })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主条件详细验证
|
|
||||||
if (!mainConditionValidation.value.valid) {
|
|
||||||
isValid.value = false
|
|
||||||
validationMessage.value = `主条件配置错误: ${mainConditionValidation.value.message}`
|
|
||||||
emit('validate', { valid: false, message: validationMessage.value })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isValid.value = true
|
|
||||||
validationMessage.value = '设备触发配置验证通过'
|
|
||||||
emit('validate', { valid: isValid.value, message: validationMessage.value })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听触发器类型变化
|
|
||||||
watch(
|
|
||||||
() => trigger.value.type,
|
|
||||||
() => {
|
|
||||||
updateValidationResult()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听产品设备变化
|
|
||||||
watch(
|
|
||||||
() => [trigger.value.productId, trigger.value.deviceId],
|
|
||||||
() => {
|
|
||||||
updateValidationResult()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,33 @@
|
||||||
<!-- 主条件配置组件 -->
|
<!-- 主条件配置组件 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-16px">
|
<div class="flex flex-col gap-16px">
|
||||||
<!-- 条件配置提示 -->
|
|
||||||
<div
|
|
||||||
v-if="!modelValue"
|
|
||||||
class="p-16px border-2 border-dashed border-[var(--el-border-color)] rounded-8px text-center"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col items-center gap-12px">
|
|
||||||
<Icon icon="ep:plus" class="text-32px text-[var(--el-text-color-placeholder)]" />
|
|
||||||
<div class="text-[var(--el-text-color-secondary)]">
|
|
||||||
<p class="text-14px font-500 mb-4px">请配置主条件</p>
|
|
||||||
<p class="text-12px">主条件是触发器的核心条件,必须满足才能触发场景</p>
|
|
||||||
</div>
|
|
||||||
<el-button type="primary" @click="addMainCondition">
|
|
||||||
<Icon icon="ep:plus" />
|
|
||||||
添加主条件
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 主条件配置 -->
|
<!-- 主条件配置 -->
|
||||||
<!-- TODO @puhui999:和“主条件”,是不是和“附加条件组”弄成一个风格,都是占一行;有个绿条; -->
|
<!-- TODO @puhui999:和“主条件”,是不是和“附加条件组”弄成一个风格,都是占一行;有个绿条; -->
|
||||||
<div v-else class="space-y-16px">
|
<div class="space-y-16px">
|
||||||
<div class="flex items-center justify-between">
|
<!-- 主条件头部 - 与附加条件组保持一致的绿色风格 -->
|
||||||
<div class="flex items-center gap-8px">
|
<div
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">主条件</span>
|
class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-8px"
|
||||||
<el-tag size="small" type="primary">必须满足</el-tag>
|
>
|
||||||
|
<div class="flex items-center gap-12px">
|
||||||
|
<div class="flex items-center gap-8px text-16px font-600 text-green-700">
|
||||||
|
<div
|
||||||
|
class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
|
||||||
|
>
|
||||||
|
主
|
||||||
|
</div>
|
||||||
|
<span>主条件</span>
|
||||||
|
</div>
|
||||||
|
<el-tag size="small" type="success">必须满足</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 主条件内容配置 -->
|
||||||
<MainConditionInnerConfig
|
<MainConditionInnerConfig
|
||||||
:model-value="modelValue"
|
:model-value="modelValue"
|
||||||
@update:model-value="updateCondition"
|
@update:model-value="updateCondition"
|
||||||
:trigger-type="triggerType"
|
:trigger-type="triggerType"
|
||||||
@validate="handleValidate"
|
@validate="handleValidate"
|
||||||
|
@trigger-type-change="handleTriggerTypeChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -45,29 +40,18 @@ import { IotRuleSceneTriggerConditionTypeEnum } from '@/views/iot/utils/constant
|
||||||
/** 主条件配置组件 */
|
/** 主条件配置组件 */
|
||||||
defineOptions({ name: 'MainConditionConfig' })
|
defineOptions({ name: 'MainConditionConfig' })
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
modelValue?: TriggerFormData
|
modelValue: TriggerFormData
|
||||||
triggerType: number
|
triggerType: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value?: TriggerFormData): void
|
(e: 'update:modelValue', value: TriggerFormData): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||||
|
(e: 'trigger-type-change', type: number): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// 事件处理
|
// 事件处理
|
||||||
const addMainCondition = () => {
|
|
||||||
const newCondition: TriggerFormData = {
|
|
||||||
type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
|
|
||||||
productId: undefined,
|
|
||||||
deviceId: undefined,
|
|
||||||
identifier: '',
|
|
||||||
operator: '=',
|
|
||||||
value: ''
|
|
||||||
}
|
|
||||||
emit('update:modelValue', newCondition)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateCondition = (condition: TriggerFormData) => {
|
const updateCondition = (condition: TriggerFormData) => {
|
||||||
emit('update:modelValue', condition)
|
emit('update:modelValue', condition)
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +60,7 @@ const handleValidate = (result: { valid: boolean; message: string }) => {
|
||||||
emit('validate', result)
|
emit('validate', result)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
const handleTriggerTypeChange = (type: number) => {
|
||||||
addMainCondition()
|
emit('trigger-type-change', type)
|
||||||
})
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-16px">
|
<div class="space-y-16px">
|
||||||
|
<!-- 触发事件类型选择 -->
|
||||||
|
<el-form-item label="触发事件类型" required>
|
||||||
|
<el-select
|
||||||
|
:model-value="triggerType"
|
||||||
|
@update:model-value="handleTriggerTypeChange"
|
||||||
|
placeholder="请选择触发事件类型"
|
||||||
|
class="w-full"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="option in triggerTypeOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 设备属性条件配置 -->
|
<!-- 设备属性条件配置 -->
|
||||||
<div v-if="isDevicePropertyTrigger" class="space-y-16px">
|
<div v-if="isDevicePropertyTrigger" class="space-y-16px">
|
||||||
<!-- 产品设备选择 -->
|
<!-- 产品设备选择 -->
|
||||||
|
|
@ -41,8 +59,8 @@
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<!-- 操作符选择 -->
|
<!-- 操作符选择 - 服务调用不需要操作符 -->
|
||||||
<el-col :span="6">
|
<el-col v-if="triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE" :span="6">
|
||||||
<el-form-item label="操作符" required>
|
<el-form-item label="操作符" required>
|
||||||
<OperatorSelector
|
<OperatorSelector
|
||||||
:model-value="condition.operator"
|
:model-value="condition.operator"
|
||||||
|
|
@ -54,11 +72,28 @@
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<!-- 值输入 -->
|
<!-- 值输入 -->
|
||||||
<el-col :span="12">
|
<el-col :span="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE ? 18 : 12">
|
||||||
<el-form-item label="比较值" required>
|
<el-form-item
|
||||||
|
:label="
|
||||||
|
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||||
|
? '服务参数'
|
||||||
|
: '比较值'
|
||||||
|
"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<!-- 服务调用参数配置 -->
|
||||||
|
<ServiceParamsInput
|
||||||
|
v-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE"
|
||||||
|
:model-value="condition.value"
|
||||||
|
@update:model-value="(value) => updateConditionField('value', value)"
|
||||||
|
:service-config="propertyConfig"
|
||||||
|
@validate="handleValueValidate"
|
||||||
|
/>
|
||||||
|
<!-- 普通值输入 -->
|
||||||
<ValueInput
|
<ValueInput
|
||||||
:model-value="condition.param"
|
v-else
|
||||||
@update:model-value="(value) => updateConditionField('param', value)"
|
:model-value="condition.value"
|
||||||
|
@update:model-value="(value) => updateConditionField('value', value)"
|
||||||
:property-type="propertyType"
|
:property-type="propertyType"
|
||||||
:operator="condition.operator"
|
:operator="condition.operator"
|
||||||
:property-config="propertyConfig"
|
:property-config="propertyConfig"
|
||||||
|
|
@ -96,22 +131,24 @@ 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 DeviceStatusConditionConfig from './DeviceStatusConditionConfig.vue'
|
import DeviceStatusConditionConfig from './DeviceStatusConditionConfig.vue'
|
||||||
import { ConditionFormData } from '@/api/iot/rule/scene/scene.types'
|
import { TriggerFormData } from '@/api/iot/rule/scene/scene.types'
|
||||||
import { IotRuleSceneTriggerTypeEnum } from '@/views/iot/utils/constants'
|
import { IotRuleSceneTriggerTypeEnum, getTriggerTypeOptions } from '@/views/iot/utils/constants'
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
|
|
||||||
/** 主条件内部配置组件 */
|
/** 主条件内部配置组件 */
|
||||||
defineOptions({ name: 'MainConditionInnerConfig' })
|
defineOptions({ name: 'MainConditionInnerConfig' })
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: ConditionFormData
|
modelValue: TriggerFormData
|
||||||
triggerType: number
|
triggerType: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: ConditionFormData): void
|
(e: 'update:modelValue', value: TriggerFormData): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
(e: 'validate', result: { valid: boolean; message: string }): void
|
||||||
|
(e: 'trigger-type-change', value: number): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
|
|
@ -152,17 +189,24 @@ const getTriggerTypeText = (type: number) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 触发器类型选项
|
||||||
|
const triggerTypeOptions = getTriggerTypeOptions()
|
||||||
|
|
||||||
// 事件处理
|
// 事件处理
|
||||||
const updateConditionField = (field: keyof ConditionFormData, value: any) => {
|
const updateConditionField = (field: keyof TriggerFormData, value: any) => {
|
||||||
condition.value[field] = value
|
;(condition.value as any)[field] = value
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCondition = (value: ConditionFormData) => {
|
const updateCondition = (value: TriggerFormData) => {
|
||||||
emit('update:modelValue', value)
|
emit('update:modelValue', value)
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleTriggerTypeChange = (type: number) => {
|
||||||
|
emit('trigger-type-change', type)
|
||||||
|
}
|
||||||
|
|
||||||
const handleProductChange = () => {
|
const handleProductChange = () => {
|
||||||
// 产品变化时清空设备和属性
|
// 产品变化时清空设备和属性
|
||||||
condition.value.deviceId = undefined
|
condition.value.deviceId = undefined
|
||||||
|
|
@ -188,7 +232,7 @@ const handleOperatorChange = () => {
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleValueValidate = (result: { valid: boolean; message: string }) => {
|
const handleValueValidate = (_result: { valid: boolean; message: string }) => {
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,14 +268,18 @@ const updateValidationResult = () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!condition.value.operator) {
|
// 服务调用不需要操作符
|
||||||
|
if (
|
||||||
|
props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE &&
|
||||||
|
!condition.value.operator
|
||||||
|
) {
|
||||||
isValid.value = false
|
isValid.value = false
|
||||||
validationMessage.value = '请选择操作符'
|
validationMessage.value = '请选择操作符'
|
||||||
emit('validate', { valid: false, message: validationMessage.value })
|
emit('validate', { valid: false, message: validationMessage.value })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!condition.value.param) {
|
if (!condition.value.value) {
|
||||||
isValid.value = false
|
isValid.value = false
|
||||||
validationMessage.value = '请输入比较值'
|
validationMessage.value = '请输入比较值'
|
||||||
emit('validate', { valid: false, message: validationMessage.value })
|
emit('validate', { valid: false, message: validationMessage.value })
|
||||||
|
|
@ -250,8 +298,11 @@ watch(
|
||||||
condition.value.productId,
|
condition.value.productId,
|
||||||
condition.value.deviceId,
|
condition.value.deviceId,
|
||||||
condition.value.identifier,
|
condition.value.identifier,
|
||||||
condition.value.operator,
|
// 服务调用不需要监听操作符
|
||||||
condition.value.param
|
props.triggerType !== IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||||
|
? condition.value.operator
|
||||||
|
: null,
|
||||||
|
condition.value.value
|
||||||
],
|
],
|
||||||
() => {
|
() => {
|
||||||
updateValidationResult()
|
updateValidationResult()
|
||||||
|
|
|
||||||
|
|
@ -80,10 +80,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
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 { TriggerConditionFormData } from '@/api/iot/rule/scene/scene.types'
|
||||||
import { IotRuleSceneTriggerConditionTypeEnum } from '@/views/iot/utils/constants'
|
import {
|
||||||
|
IotRuleSceneTriggerConditionTypeEnum,
|
||||||
|
IotRuleSceneTriggerConditionParameterOperatorEnum
|
||||||
|
} from '@/views/iot/utils/constants'
|
||||||
|
|
||||||
/** 子条件组配置组件 */
|
/** 子条件组配置组件 */
|
||||||
defineOptions({ name: 'SubConditionGroupConfig' })
|
defineOptions({ name: 'SubConditionGroupConfig' })
|
||||||
|
|
@ -109,21 +113,31 @@ const conditionValidations = ref<{ [key: number]: { valid: boolean; message: str
|
||||||
|
|
||||||
// 事件处理
|
// 事件处理
|
||||||
const addCondition = () => {
|
const addCondition = () => {
|
||||||
|
// 确保 subGroup.value 是一个数组
|
||||||
if (!subGroup.value) {
|
if (!subGroup.value) {
|
||||||
subGroup.value = []
|
subGroup.value = []
|
||||||
}
|
}
|
||||||
if (subGroup.value.length >= maxConditions.value) {
|
|
||||||
|
// 检查是否达到最大条件数量限制
|
||||||
|
if (subGroup.value?.length >= maxConditions.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newCondition: TriggerConditionFormData = {
|
const newCondition: TriggerConditionFormData = {
|
||||||
type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
|
type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
|
||||||
productId: undefined,
|
productId: undefined,
|
||||||
deviceId: undefined,
|
deviceId: undefined,
|
||||||
identifier: '',
|
identifier: '',
|
||||||
operator: '=',
|
operator: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value, // 使用枚举默认值
|
||||||
param: ''
|
param: ''
|
||||||
}
|
}
|
||||||
subGroup.value.push(newCondition)
|
|
||||||
|
// 使用 nextTick 确保响应式更新完成后再添加新条件
|
||||||
|
nextTick(() => {
|
||||||
|
if (subGroup.value) {
|
||||||
|
subGroup.value.push(newCondition)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeCondition = (index: number) => {
|
const removeCondition = (index: number) => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,490 @@
|
||||||
|
<!-- 服务参数输入组件 -->
|
||||||
|
<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>
|
||||||
|
<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: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
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 = () => {
|
||||||
|
const exampleData = generateExampleJson()
|
||||||
|
paramsJson.value = exampleData
|
||||||
|
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 (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>
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
<!-- 值输入组件 -->
|
<!-- 值输入组件 -->
|
||||||
<!-- TODO @yunai:这个需要在看看。。。 -->
|
<!-- TODO @yunai:这个需要在看看。。。 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="value-input">
|
<div class="w-full min-w-0">
|
||||||
<!-- 布尔值选择 -->
|
<!-- 布尔值选择 -->
|
||||||
<el-select
|
<el-select
|
||||||
v-if="propertyType === 'bool'"
|
v-if="propertyType === 'bool'"
|
||||||
v-model="localValue"
|
v-model="localValue"
|
||||||
placeholder="请选择布尔值"
|
placeholder="请选择布尔值"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
class="w-full"
|
class="w-full!"
|
||||||
|
style="width: 100% !important"
|
||||||
>
|
>
|
||||||
<el-option label="真 (true)" value="true" />
|
<el-option label="真 (true)" value="true" />
|
||||||
<el-option label="假 (false)" value="false" />
|
<el-option label="假 (false)" value="false" />
|
||||||
|
|
@ -20,7 +21,8 @@
|
||||||
v-model="localValue"
|
v-model="localValue"
|
||||||
placeholder="请选择枚举值"
|
placeholder="请选择枚举值"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
class="w-full"
|
class="w-full!"
|
||||||
|
style="width: 100% !important"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="option in enumOptions"
|
v-for="option in enumOptions"
|
||||||
|
|
@ -31,41 +33,51 @@
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
||||||
<!-- 范围输入 (between 操作符) -->
|
<!-- 范围输入 (between 操作符) -->
|
||||||
<div v-else-if="operator === 'between'" class="range-input">
|
<div
|
||||||
|
v-else-if="operator === 'between'"
|
||||||
|
class="w-full! flex items-center gap-8px"
|
||||||
|
style="width: 100% !important"
|
||||||
|
>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="rangeStart"
|
v-model="rangeStart"
|
||||||
:type="getInputType()"
|
:type="getInputType()"
|
||||||
placeholder="最小值"
|
placeholder="最小值"
|
||||||
@input="handleRangeChange"
|
@input="handleRangeChange"
|
||||||
class="range-start"
|
class="flex-1 min-w-0"
|
||||||
|
style="width: auto !important"
|
||||||
/>
|
/>
|
||||||
<span class="range-separator">至</span>
|
<span class="text-12px text-[var(--el-text-color-secondary)] whitespace-nowrap">至</span>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="rangeEnd"
|
v-model="rangeEnd"
|
||||||
:type="getInputType()"
|
:type="getInputType()"
|
||||||
placeholder="最大值"
|
placeholder="最大值"
|
||||||
@input="handleRangeChange"
|
@input="handleRangeChange"
|
||||||
class="range-end"
|
class="flex-1 min-w-0"
|
||||||
|
style="width: auto !important"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 列表输入 (in 操作符) -->
|
<!-- 列表输入 (in 操作符) -->
|
||||||
<div v-else-if="operator === 'in'" class="list-input">
|
<div v-else-if="operator === 'in'" class="w-full!" style="width: 100% !important">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="localValue"
|
v-model="localValue"
|
||||||
placeholder="请输入值列表,用逗号分隔"
|
placeholder="请输入值列表,用逗号分隔"
|
||||||
@input="handleChange"
|
@input="handleChange"
|
||||||
class="w-full"
|
class="w-full!"
|
||||||
|
style="width: 100% !important"
|
||||||
>
|
>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<el-tooltip content="多个值用逗号分隔,如:1,2,3" placement="top">
|
<el-tooltip content="多个值用逗号分隔,如:1,2,3" placement="top">
|
||||||
<Icon icon="ep:question-filled" class="input-tip" />
|
<Icon
|
||||||
|
icon="ep:question-filled"
|
||||||
|
class="text-[var(--el-text-color-placeholder)] cursor-help"
|
||||||
|
/>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
<div v-if="listPreview.length > 0" class="list-preview">
|
<div v-if="listPreview.length > 0" class="mt-8px flex items-center gap-6px flex-wrap">
|
||||||
<span class="preview-label">解析结果:</span>
|
<span class="text-12px text-[var(--el-text-color-secondary)]">解析结果:</span>
|
||||||
<el-tag v-for="(item, index) in listPreview" :key="index" size="small" class="preview-tag">
|
<el-tag v-for="(item, index) in listPreview" :key="index" size="small" class="m-0">
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -80,7 +92,8 @@
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
@change="handleDateChange"
|
@change="handleDateChange"
|
||||||
class="w-full"
|
class="w-full!"
|
||||||
|
style="width: 100% !important"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 数字输入 -->
|
<!-- 数字输入 -->
|
||||||
|
|
@ -93,7 +106,8 @@
|
||||||
:max="getMax()"
|
:max="getMax()"
|
||||||
placeholder="请输入数值"
|
placeholder="请输入数值"
|
||||||
@change="handleNumberChange"
|
@change="handleNumberChange"
|
||||||
class="w-full"
|
class="w-full!"
|
||||||
|
style="width: 100% !important"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 文本输入 -->
|
<!-- 文本输入 -->
|
||||||
|
|
@ -103,7 +117,8 @@
|
||||||
:type="getInputType()"
|
:type="getInputType()"
|
||||||
:placeholder="getPlaceholder()"
|
:placeholder="getPlaceholder()"
|
||||||
@input="handleChange"
|
@input="handleChange"
|
||||||
class="w-full"
|
class="w-full!"
|
||||||
|
style="width: 100% !important"
|
||||||
>
|
>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
|
|
@ -111,13 +126,15 @@
|
||||||
:content="`单位:${propertyConfig.unit}`"
|
:content="`单位:${propertyConfig.unit}`"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<span class="input-unit">{{ propertyConfig.unit }}</span>
|
<span class="text-12px text-[var(--el-text-color-secondary)] px-4px">{{
|
||||||
|
propertyConfig.unit
|
||||||
|
}}</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
|
|
||||||
<!-- 验证提示 -->
|
<!-- 验证提示 -->
|
||||||
<div v-if="validationMessage" class="validation-message">
|
<div v-if="validationMessage" class="mt-4px">
|
||||||
<el-text :type="isValid ? 'success' : 'danger'" size="small">
|
<el-text :type="isValid ? 'success' : 'danger'" size="small">
|
||||||
<Icon :icon="isValid ? 'ep:check' : 'ep:warning-filled'" />
|
<Icon :icon="isValid ? 'ep:check' : 'ep:warning-filled'" />
|
||||||
{{ validationMessage }}
|
{{ validationMessage }}
|
||||||
|
|
@ -354,62 +371,3 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.value-input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.range-input {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.range-start,
|
|
||||||
.range-end {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.range-separator {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-tip {
|
|
||||||
color: var(--el-text-color-placeholder);
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-unit {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
padding: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-preview {
|
|
||||||
margin-top: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-tag {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.validation-message {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,6 @@
|
||||||
v-if="isDeviceAction(action.type)"
|
v-if="isDeviceAction(action.type)"
|
||||||
:model-value="action"
|
:model-value="action"
|
||||||
@update:model-value="(value) => updateAction(index, value)"
|
@update:model-value="(value) => updateAction(index, value)"
|
||||||
@validate="(result) => handleActionValidate(index, result)"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 告警配置 -->
|
<!-- 告警配置 -->
|
||||||
|
|
@ -84,7 +83,6 @@
|
||||||
v-if="isAlertAction(action.type)"
|
v-if="isAlertAction(action.type)"
|
||||||
:model-value="action.alertConfigId"
|
:model-value="action.alertConfigId"
|
||||||
@update:model-value="(value) => updateActionAlertConfig(index, value)"
|
@update:model-value="(value) => updateActionAlertConfig(index, value)"
|
||||||
@validate="(result) => handleActionValidate(index, result)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -100,16 +98,6 @@
|
||||||
最多可添加 {{ maxActions }} 个执行器
|
最多可添加 {{ maxActions }} 个执行器
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 验证结果 -->
|
|
||||||
<div v-if="validationMessage" class="validation-result">
|
|
||||||
<el-alert
|
|
||||||
:title="validationMessage"
|
|
||||||
:type="isValid ? 'success' : 'error'"
|
|
||||||
:closable="false"
|
|
||||||
show-icon
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -120,7 +108,12 @@ import ActionTypeSelector from '../selectors/ActionTypeSelector.vue'
|
||||||
import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
|
import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
|
||||||
import AlertConfig from '../configs/AlertConfig.vue'
|
import AlertConfig from '../configs/AlertConfig.vue'
|
||||||
import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
|
import { ActionFormData } from '@/api/iot/rule/scene/scene.types'
|
||||||
import { IotRuleSceneActionTypeEnum as ActionTypeEnum } from '@/views/iot/utils/constants'
|
import {
|
||||||
|
IotRuleSceneActionTypeEnum as ActionTypeEnum,
|
||||||
|
isDeviceAction,
|
||||||
|
isAlertAction,
|
||||||
|
getActionTypeLabel
|
||||||
|
} from '@/views/iot/utils/constants'
|
||||||
|
|
||||||
/** 执行器配置组件 */
|
/** 执行器配置组件 */
|
||||||
defineOptions({ name: 'ActionSection' })
|
defineOptions({ name: 'ActionSection' })
|
||||||
|
|
@ -131,7 +124,6 @@ interface Props {
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'update:actions', value: ActionFormData[]): void
|
(e: 'update:actions', value: ActionFormData[]): void
|
||||||
(e: 'validate', result: { valid: boolean; message: string }): void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
@ -147,6 +139,7 @@ const createDefaultActionData = (): ActionFormData => {
|
||||||
type: ActionTypeEnum.DEVICE_PROPERTY_SET, // 默认为设备属性设置
|
type: ActionTypeEnum.DEVICE_PROPERTY_SET, // 默认为设备属性设置
|
||||||
productId: undefined,
|
productId: undefined,
|
||||||
deviceId: undefined,
|
deviceId: undefined,
|
||||||
|
identifier: undefined, // 物模型标识符(服务调用时使用)
|
||||||
params: {},
|
params: {},
|
||||||
alertConfigId: undefined
|
alertConfigId: undefined
|
||||||
}
|
}
|
||||||
|
|
@ -155,42 +148,18 @@ const createDefaultActionData = (): ActionFormData => {
|
||||||
// 配置常量
|
// 配置常量
|
||||||
const maxActions = 5
|
const maxActions = 5
|
||||||
|
|
||||||
// 验证状态
|
|
||||||
const actionValidations = ref<{ [key: number]: { valid: boolean; message: string } }>({})
|
|
||||||
const validationMessage = ref('')
|
|
||||||
const isValid = ref(true)
|
|
||||||
|
|
||||||
// 执行器类型映射
|
|
||||||
const actionTypeNames = {
|
|
||||||
[ActionTypeEnum.DEVICE_PROPERTY_SET]: '属性设置',
|
|
||||||
[ActionTypeEnum.DEVICE_SERVICE_INVOKE]: '服务调用',
|
|
||||||
[ActionTypeEnum.ALERT_TRIGGER]: '触发告警',
|
|
||||||
[ActionTypeEnum.ALERT_RECOVER]: '恢复告警'
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionTypeTags = {
|
|
||||||
[ActionTypeEnum.DEVICE_PROPERTY_SET]: 'primary',
|
|
||||||
[ActionTypeEnum.DEVICE_SERVICE_INVOKE]: 'success',
|
|
||||||
[ActionTypeEnum.ALERT_TRIGGER]: 'danger',
|
|
||||||
[ActionTypeEnum.ALERT_RECOVER]: 'warning'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 工具函数
|
// 工具函数
|
||||||
const isDeviceAction = (type: number) => {
|
|
||||||
return [ActionTypeEnum.DEVICE_PROPERTY_SET, ActionTypeEnum.DEVICE_SERVICE_INVOKE].includes(
|
|
||||||
type as any
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAlertAction = (type: number) => {
|
|
||||||
return [ActionTypeEnum.ALERT_TRIGGER, ActionTypeEnum.ALERT_RECOVER].includes(type as any)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getActionTypeName = (type: number) => {
|
const getActionTypeName = (type: number) => {
|
||||||
return actionTypeNames[type] || '未知类型'
|
return getActionTypeLabel(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getActionTypeTag = (type: number) => {
|
const getActionTypeTag = (type: number) => {
|
||||||
|
const actionTypeTags = {
|
||||||
|
[ActionTypeEnum.DEVICE_PROPERTY_SET]: 'primary',
|
||||||
|
[ActionTypeEnum.DEVICE_SERVICE_INVOKE]: 'success',
|
||||||
|
[ActionTypeEnum.ALERT_TRIGGER]: 'danger',
|
||||||
|
[ActionTypeEnum.ALERT_RECOVER]: 'warning'
|
||||||
|
}
|
||||||
return actionTypeTags[type] || 'info'
|
return actionTypeTags[type] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,21 +175,6 @@ const addAction = () => {
|
||||||
|
|
||||||
const removeAction = (index: number) => {
|
const removeAction = (index: number) => {
|
||||||
actions.value.splice(index, 1)
|
actions.value.splice(index, 1)
|
||||||
delete actionValidations.value[index]
|
|
||||||
|
|
||||||
// 重新索引验证结果
|
|
||||||
const newValidations: { [key: number]: { valid: boolean; message: string } } = {}
|
|
||||||
Object.keys(actionValidations.value).forEach((key) => {
|
|
||||||
const numKey = parseInt(key)
|
|
||||||
if (numKey > index) {
|
|
||||||
newValidations[numKey - 1] = actionValidations.value[numKey]
|
|
||||||
} else if (numKey < index) {
|
|
||||||
newValidations[numKey] = actionValidations.value[numKey]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
actionValidations.value = newValidations
|
|
||||||
|
|
||||||
updateValidationResult()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateActionType = (index: number, type: number) => {
|
const updateActionType = (index: number, type: number) => {
|
||||||
|
|
@ -237,49 +191,28 @@ const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const onActionTypeChange = (action: ActionFormData, type: number) => {
|
const onActionTypeChange = (action: ActionFormData, type: number) => {
|
||||||
// 清理不相关的配置
|
// 清理不相关的配置,确保数据结构干净
|
||||||
if (isDeviceAction(type)) {
|
if (isDeviceAction(type)) {
|
||||||
|
// 设备控制类型:清理告警配置,确保设备参数存在
|
||||||
action.alertConfigId = undefined
|
action.alertConfigId = undefined
|
||||||
if (!action.params) {
|
if (!action.params) {
|
||||||
action.params = {}
|
action.params = {}
|
||||||
}
|
}
|
||||||
|
// 如果从其他类型切换到设备控制类型,清空identifier(让用户重新选择)
|
||||||
|
if (action.identifier && type !== action.type) {
|
||||||
|
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.params = undefined
|
action.params = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 触发重新校验
|
||||||
|
nextTick(() => {
|
||||||
|
// 这里可以添加校验逻辑
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleActionValidate = (index: number, result: { valid: boolean; message: string }) => {
|
|
||||||
actionValidations.value[index] = result
|
|
||||||
updateValidationResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateValidationResult = () => {
|
|
||||||
const validations = Object.values(actionValidations.value)
|
|
||||||
const allValid = validations.every((v) => v.valid)
|
|
||||||
const hasValidations = validations.length > 0
|
|
||||||
|
|
||||||
if (!hasValidations) {
|
|
||||||
isValid.value = true
|
|
||||||
validationMessage.value = ''
|
|
||||||
} else if (allValid) {
|
|
||||||
isValid.value = true
|
|
||||||
validationMessage.value = '所有执行器配置验证通过'
|
|
||||||
} else {
|
|
||||||
isValid.value = false
|
|
||||||
const errorMessages = validations.filter((v) => !v.valid).map((v) => v.message)
|
|
||||||
validationMessage.value = `执行器配置错误: ${errorMessages.join('; ')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('validate', { valid: isValid.value, message: validationMessage.value })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听执行器数量变化
|
|
||||||
watch(
|
|
||||||
() => actions.value.length,
|
|
||||||
() => {
|
|
||||||
updateValidationResult()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -16,20 +16,26 @@
|
||||||
|
|
||||||
<div class="p-16px space-y-24px">
|
<div class="p-16px space-y-24px">
|
||||||
<!-- 触发器列表 -->
|
<!-- 触发器列表 -->
|
||||||
<!-- TODO 每个触发器,有个外框,会不会好点? -->
|
|
||||||
<div v-if="triggers.length > 0" class="space-y-24px">
|
<div v-if="triggers.length > 0" class="space-y-24px">
|
||||||
<div
|
<div
|
||||||
v-for="(triggerItem, index) in triggers"
|
v-for="(triggerItem, index) in triggers"
|
||||||
:key="`trigger-${index}`"
|
:key="`trigger-${index}`"
|
||||||
class="border border-[var(--el-border-color-light)] rounded-8px p-16px relative"
|
class="border-2 border-green-200 rounded-8px bg-green-50 shadow-sm hover:shadow-md transition-shadow"
|
||||||
>
|
>
|
||||||
<!-- 触发器头部 -->
|
<!-- 触发器头部 - 绿色主题 -->
|
||||||
<div class="flex items-center justify-between mb-16px">
|
<div
|
||||||
<div class="flex items-center gap-8px">
|
class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border-b border-green-200 rounded-t-6px"
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">
|
>
|
||||||
触发器 {{ index + 1 }}
|
<div class="flex items-center gap-12px">
|
||||||
</span>
|
<div class="flex items-center gap-8px text-16px font-600 text-green-700">
|
||||||
<el-tag size="small" :type="getTriggerTagType(triggerItem.type)">
|
<div
|
||||||
|
class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
|
||||||
|
>
|
||||||
|
{{ index + 1 }}
|
||||||
|
</div>
|
||||||
|
<span>触发器 {{ index + 1 }}</span>
|
||||||
|
</div>
|
||||||
|
<el-tag size="small" :type="getTriggerTagType(triggerItem.type)" class="font-500">
|
||||||
{{ getTriggerTypeLabel(triggerItem.type) }}
|
{{ getTriggerTypeLabel(triggerItem.type) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -40,6 +46,7 @@
|
||||||
size="small"
|
size="small"
|
||||||
text
|
text
|
||||||
@click="removeTrigger(index)"
|
@click="removeTrigger(index)"
|
||||||
|
class="hover:bg-red-50"
|
||||||
>
|
>
|
||||||
<Icon icon="ep:delete" />
|
<Icon icon="ep:delete" />
|
||||||
删除
|
删除
|
||||||
|
|
@ -47,37 +54,24 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 触发事件类型选择 -->
|
<!-- 触发器内容区域 -->
|
||||||
<el-form-item label="触发事件类型" required>
|
<div class="p-16px space-y-16px">
|
||||||
<el-select
|
<!-- 设备触发配置 -->
|
||||||
:model-value="triggerItem.type"
|
<DeviceTriggerConfig
|
||||||
@update:model-value="(value) => updateTriggerType(index, value)"
|
v-if="isDeviceTrigger(triggerItem.type)"
|
||||||
placeholder="请选择触发事件类型"
|
:model-value="triggerItem"
|
||||||
class="w-full"
|
:index="index"
|
||||||
>
|
@update:model-value="(value) => updateTriggerDeviceConfig(index, value)"
|
||||||
<el-option
|
@trigger-type-change="(type) => updateTriggerType(index, type)"
|
||||||
v-for="option in triggerTypeOptions"
|
/>
|
||||||
:key="option.value"
|
|
||||||
:label="option.label"
|
|
||||||
:value="option.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 设备触发配置 -->
|
<!-- 定时触发配置 -->
|
||||||
<DeviceTriggerConfig
|
<TimerTriggerConfig
|
||||||
v-if="isDeviceTrigger(triggerItem.type)"
|
v-else-if="triggerItem.type === TriggerTypeEnum.TIMER"
|
||||||
:model-value="triggerItem"
|
:model-value="triggerItem.cronExpression"
|
||||||
:index="index"
|
@update:model-value="(value) => updateTriggerCronConfig(index, value)"
|
||||||
@update:model-value="(value) => updateTriggerDeviceConfig(index, value)"
|
/>
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
<!-- 定时触发配置 -->
|
|
||||||
<TimerTriggerConfig
|
|
||||||
v-else-if="triggerItem.type === TriggerTypeEnum.TIMER"
|
|
||||||
:model-value="triggerItem.cronExpression"
|
|
||||||
@update:model-value="(value) => updateTriggerCronConfig(index, value)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,20 @@
|
||||||
<!-- 设备选择模式 -->
|
<!-- 设备选择模式 -->
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="设备选择模式" required>
|
<el-form-item label="设备选择模式" required>
|
||||||
<el-radio-group v-model="deviceSelectionMode" @change="handleDeviceSelectionModeChange">
|
<el-radio-group
|
||||||
|
v-model="deviceSelectionMode"
|
||||||
|
@change="handleDeviceSelectionModeChange"
|
||||||
|
:disabled="!localProductId"
|
||||||
|
>
|
||||||
<el-radio value="all">全部设备</el-radio>
|
<el-radio value="all">全部设备</el-radio>
|
||||||
<el-radio value="specific">选择设备</el-radio>
|
<el-radio value="specific">选择设备</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
|
<div
|
||||||
|
v-if="!localProductId"
|
||||||
|
class="text-12px text-[var(--el-text-color-placeholder)] mt-4px"
|
||||||
|
>
|
||||||
|
请先选择产品
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
@ -50,12 +60,10 @@
|
||||||
<!-- 具体设备选择 -->
|
<!-- 具体设备选择 -->
|
||||||
<el-row v-if="deviceSelectionMode === 'specific'" :gutter="16">
|
<el-row v-if="deviceSelectionMode === 'specific'" :gutter="16">
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<!-- TODO @puhui999:貌似产品选择不上; -->
|
|
||||||
<el-form-item label="选择设备" required>
|
<el-form-item label="选择设备" required>
|
||||||
<!-- TODO @puhui999:请先选择产品,是不是改成请选择设备?然后上面,localProductId 为空(未选择)的时候,禁用 deviceSelectionMode -->
|
|
||||||
<el-select
|
<el-select
|
||||||
v-model="localDeviceId"
|
v-model="localDeviceId"
|
||||||
placeholder="请先选择产品"
|
:placeholder="localProductId ? '请选择设备' : '请先选择产品'"
|
||||||
filterable
|
filterable
|
||||||
clearable
|
clearable
|
||||||
@change="handleDeviceChange"
|
@change="handleDeviceChange"
|
||||||
|
|
@ -152,8 +160,8 @@ const localProductId = useVModel(props, 'productId', emit)
|
||||||
const localDeviceId = useVModel(props, 'deviceId', emit)
|
const localDeviceId = useVModel(props, 'deviceId', emit)
|
||||||
|
|
||||||
// 设备选择模式
|
// 设备选择模式
|
||||||
// TODO @puhui999:默认选中 all
|
// 默认选择具体设备,这样用户可以看到设备选择器
|
||||||
const deviceSelectionMode = ref<'specific' | 'all'>('all')
|
const deviceSelectionMode = ref<'specific' | 'all'>('specific')
|
||||||
|
|
||||||
// 数据状态
|
// 数据状态
|
||||||
const productLoading = ref(false)
|
const productLoading = ref(false)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,460 @@
|
||||||
|
<!-- 服务选择器组件 -->
|
||||||
|
<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
|
||||||
|
}>()
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
@ -164,34 +164,22 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<!-- TODO puhui999:貌似展示不太对劲。。。一个字,一个 tab 哈了。 -->
|
<!-- 触发条件列 -->
|
||||||
<el-table-column label="触发条件" min-width="250">
|
<el-table-column label="触发条件" min-width="250">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="flex flex-wrap gap-4px">
|
<div class="flex flex-wrap gap-4px">
|
||||||
<el-tag
|
<el-tag type="primary" size="small" class="m-0">
|
||||||
v-for="(trigger, index) in getTriggerSummary(row)"
|
{{ getTriggerSummary(row) }}
|
||||||
:key="index"
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
class="m-0"
|
|
||||||
>
|
|
||||||
{{ trigger }}
|
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<!-- TODO puhui999:貌似展示不太对劲。。。一个字,一个 tab 哈了。 -->
|
<!-- 执行动作列 -->
|
||||||
<el-table-column label="执行动作" min-width="250">
|
<el-table-column label="执行动作" min-width="250">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="flex flex-wrap gap-4px">
|
<div class="flex flex-wrap gap-4px">
|
||||||
<el-tag
|
<el-tag type="success" size="small" class="m-0">
|
||||||
v-for="(action, index) in getActionSummary(row)"
|
{{ getActionSummary(row) }}
|
||||||
:key="index"
|
|
||||||
type="success"
|
|
||||||
size="small"
|
|
||||||
class="m-0"
|
|
||||||
>
|
|
||||||
{{ action }}
|
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -222,8 +210,7 @@
|
||||||
@click="handleToggleStatus(row)"
|
@click="handleToggleStatus(row)"
|
||||||
>
|
>
|
||||||
<Icon :icon="row.status === 0 ? 'ep:video-pause' : 'ep:video-play'" />
|
<Icon :icon="row.status === 0 ? 'ep:video-pause' : 'ep:video-play'" />
|
||||||
<!-- TODO @puhui999:字典翻译 -->
|
{{ getDictLabel(DICT_TYPE.COMMON_STATUS, row.status === 0 ? 1 : 0) }}
|
||||||
{{ row.status === 0 ? '禁用' : '启用' }}
|
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="danger" class="!mr-10px" link @click="handleDelete(row.id)">
|
<el-button type="danger" class="!mr-10px" link @click="handleDelete(row.id)">
|
||||||
<Icon icon="ep:delete" />
|
<Icon icon="ep:delete" />
|
||||||
|
|
@ -243,42 +230,23 @@
|
||||||
/>
|
/>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 批量操作 -->
|
|
||||||
<div
|
|
||||||
v-if="selectedRows.length > 0"
|
|
||||||
class="fixed bottom-20px left-1/2 transform -translate-x-1/2 z-1000"
|
|
||||||
>
|
|
||||||
<el-card shadow="always">
|
|
||||||
<div class="flex items-center gap-16px">
|
|
||||||
<span class="font-500 text-[#303133]"> 已选择 {{ selectedRows.length }} 项 </span>
|
|
||||||
<div class="flex gap-8px">
|
|
||||||
<el-button @click="handleBatchEnable">
|
|
||||||
<Icon icon="ep:video-play" />
|
|
||||||
批量启用
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="handleBatchDisable">
|
|
||||||
<Icon icon="ep:video-pause" />
|
|
||||||
批量禁用
|
|
||||||
</el-button>
|
|
||||||
<el-button type="danger" @click="handleBatchDelete">
|
|
||||||
<Icon icon="ep:delete" />
|
|
||||||
批量删除
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 表单对话框 -->
|
<!-- 表单对话框 -->
|
||||||
<RuleSceneForm v-model="formVisible" @success="getList" />
|
<RuleSceneForm v-model="formVisible" :rule-scene="currentRule" @success="getList" />
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DICT_TYPE, getIntDictOptions } 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 { IotRuleScene } from '@/api/iot/rule/scene/scene.types'
|
import { IotRuleSceneDO } from '@/api/iot/rule/scene/scene.types'
|
||||||
|
import { RuleSceneApi } from '@/api/iot/rule/scene'
|
||||||
|
import {
|
||||||
|
IotRuleSceneTriggerTypeEnum,
|
||||||
|
IotRuleSceneActionTypeEnum,
|
||||||
|
getTriggerTypeLabel,
|
||||||
|
getActionTypeLabel
|
||||||
|
} from '@/views/iot/utils/constants'
|
||||||
import { formatDate } from '@/utils/formatTime'
|
import { formatDate } from '@/utils/formatTime'
|
||||||
|
|
||||||
/** 场景联动规则管理页面 */
|
/** 场景联动规则管理页面 */
|
||||||
|
|
@ -287,7 +255,7 @@ defineOptions({ name: 'IoTSceneRule' })
|
||||||
const message = useMessage() // 消息弹窗
|
const message = useMessage() // 消息弹窗
|
||||||
const { t } = useI18n() // 国际化
|
const { t } = useI18n() // 国际化
|
||||||
|
|
||||||
// 查询参数
|
/** 查询参数 */
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
|
|
@ -296,16 +264,16 @@ const queryParams = reactive({
|
||||||
})
|
})
|
||||||
|
|
||||||
const loading = ref(true) // 列表的加载中
|
const loading = ref(true) // 列表的加载中
|
||||||
const list = ref<IotRuleScene[]>([]) // 列表的数据
|
const list = ref<IotRuleSceneDO[]>([]) // 列表的数据
|
||||||
const total = ref(0) // 列表的总页数
|
const total = ref(0) // 列表的总页数
|
||||||
const selectedRows = ref<IotRuleScene[]>([]) // 选中的行数据
|
const selectedRows = ref<IotRuleSceneDO[]>([]) // 选中的行数据
|
||||||
const queryFormRef = ref() // 搜索的表单
|
const queryFormRef = ref() // 搜索的表单
|
||||||
|
|
||||||
// 表单状态
|
/** 表单状态 */
|
||||||
const formVisible = ref(false) // 是否可见
|
const formVisible = ref(false) // 是否可见
|
||||||
const currentRule = ref<IotRuleScene>() // 表单数据
|
const currentRule = ref<IotRuleSceneDO>() // 表单数据
|
||||||
|
|
||||||
// 统计数据
|
/** 统计数据 */
|
||||||
const statistics = ref({
|
const statistics = ref({
|
||||||
total: 0,
|
total: 0,
|
||||||
enabled: 0,
|
enabled: 0,
|
||||||
|
|
@ -314,7 +282,7 @@ const statistics = ref({
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 格式化 CRON 表达式显示 */
|
/** 格式化 CRON 表达式显示 */
|
||||||
// TODO @puhui999:这个能不能 cron 组件里翻译哈;
|
/** 注:后续可考虑将此功能移至 CRON 组件内部 */
|
||||||
const formatCronExpression = (cron: string): string => {
|
const formatCronExpression = (cron: string): string => {
|
||||||
if (!cron) return ''
|
if (!cron) return ''
|
||||||
|
|
||||||
|
|
@ -358,41 +326,86 @@ const formatCronExpression = (cron: string): string => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取规则摘要信息 */
|
/** 获取规则摘要信息 */
|
||||||
const getRuleSceneSummary = (rule: IotRuleScene) => {
|
const getRuleSceneSummary = (rule: IotRuleSceneDO) => {
|
||||||
// TODO @puhui999:是不是可以使用字段,或者枚举?
|
|
||||||
const triggerSummary =
|
const triggerSummary =
|
||||||
rule.triggers?.map((trigger) => {
|
rule.triggers?.map((trigger: any) => {
|
||||||
|
// 构建基础描述
|
||||||
|
let description = ''
|
||||||
|
|
||||||
switch (trigger.type) {
|
switch (trigger.type) {
|
||||||
case 1:
|
case IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE:
|
||||||
return `设备状态变更 (${trigger.deviceNames?.length || 0}个设备)`
|
description = '设备状态变更'
|
||||||
case 2:
|
break
|
||||||
return `属性上报 (${trigger.deviceNames?.length || 0}个设备)`
|
case IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST:
|
||||||
case 3:
|
description = '属性上报'
|
||||||
return `事件上报 (${trigger.deviceNames?.length || 0}个设备)`
|
if (trigger.identifier) {
|
||||||
case 4:
|
description += ` (${trigger.identifier})`
|
||||||
return `服务调用 (${trigger.deviceNames?.length || 0}个设备)`
|
}
|
||||||
case 100:
|
break
|
||||||
return `定时触发 (${formatCronExpression(trigger.cronExpression || '')})`
|
case IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST:
|
||||||
|
description = '事件上报'
|
||||||
|
if (trigger.identifier) {
|
||||||
|
description += ` (${trigger.identifier})`
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE:
|
||||||
|
description = '服务调用'
|
||||||
|
if (trigger.identifier) {
|
||||||
|
description += ` (${trigger.identifier})`
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case IotRuleSceneTriggerTypeEnum.TIMER:
|
||||||
|
description = `定时触发 (${formatCronExpression(trigger.cronExpression || '')})`
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
return '未知触发类型'
|
description = getTriggerTypeLabel(trigger.type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加设备信息(如果有)
|
||||||
|
if (trigger.deviceId) {
|
||||||
|
description += ` [设备ID: ${trigger.deviceId}]`
|
||||||
|
} else if (trigger.productId) {
|
||||||
|
description += ` [产品ID: ${trigger.productId}]`
|
||||||
|
}
|
||||||
|
|
||||||
|
return description
|
||||||
}) || []
|
}) || []
|
||||||
|
|
||||||
// TODO @puhui999:是不是可以使用字段,或者枚举?
|
|
||||||
const actionSummary =
|
const actionSummary =
|
||||||
rule.actions?.map((action) => {
|
rule.actions?.map((action: any) => {
|
||||||
|
// 构建基础描述
|
||||||
|
let description = ''
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 1:
|
case IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET:
|
||||||
return `设备属性设置 (${action.deviceControl?.deviceNames?.length || 0}个设备)`
|
description = '设备属性设置'
|
||||||
case 2:
|
break
|
||||||
return `设备服务调用 (${action.deviceControl?.deviceNames?.length || 0}个设备)`
|
case IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE:
|
||||||
case 100:
|
description = '设备服务调用'
|
||||||
return '发送告警通知'
|
break
|
||||||
case 101:
|
case IotRuleSceneActionTypeEnum.ALERT_TRIGGER:
|
||||||
return '发送邮件通知'
|
description = '发送告警通知'
|
||||||
|
break
|
||||||
|
case IotRuleSceneActionTypeEnum.ALERT_RECOVER:
|
||||||
|
description = '发送恢复通知'
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
return '未知执行类型'
|
description = getActionTypeLabel(action.type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加设备信息(如果有)
|
||||||
|
if (action.deviceId) {
|
||||||
|
description += ` [设备ID: ${action.deviceId}]`
|
||||||
|
} else if (action.productId) {
|
||||||
|
description += ` [产品ID: ${action.productId}]`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加告警配置信息(如果有)
|
||||||
|
if (action.alertConfigId) {
|
||||||
|
description += ` [告警配置ID: ${action.alertConfigId}]`
|
||||||
|
}
|
||||||
|
|
||||||
|
return description
|
||||||
}) || []
|
}) || []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -402,69 +415,26 @@ const getRuleSceneSummary = (rule: IotRuleScene) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询列表 */
|
/** 查询列表 */
|
||||||
// TODO @puhui999:这里使用真实数据;
|
|
||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// 模拟API调用
|
// 调用真实API获取数据
|
||||||
const mockData = {
|
const data = await RuleSceneApi.getRuleScenePage(queryParams)
|
||||||
list: [
|
list.value = data.list
|
||||||
{
|
total.value = data.total
|
||||||
id: 1,
|
|
||||||
name: '温度过高自动降温',
|
|
||||||
description: '当温度超过30度时自动开启空调',
|
|
||||||
status: 0,
|
|
||||||
triggers: [
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
productKey: 'temp_sensor',
|
|
||||||
deviceNames: ['sensor_001'],
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
type: 'property',
|
|
||||||
identifier: 'temperature',
|
|
||||||
parameters: [{ operator: '>', value: '30' }]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
type: 1,
|
|
||||||
deviceControl: {
|
|
||||||
productKey: 'air_conditioner',
|
|
||||||
deviceNames: ['ac_001'],
|
|
||||||
type: 'property',
|
|
||||||
identifier: 'power',
|
|
||||||
params: { power: 1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
lastTriggeredTime: new Date().toISOString(),
|
|
||||||
createTime: new Date().toISOString()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: '设备离线告警',
|
|
||||||
description: '设备离线时发送告警通知',
|
|
||||||
status: 0,
|
|
||||||
triggers: [
|
|
||||||
{ type: 1, productKey: 'smart_device', deviceNames: ['device_001', 'device_002'] }
|
|
||||||
],
|
|
||||||
actions: [{ type: 100, alertConfigId: 1 }],
|
|
||||||
createTime: new Date().toISOString()
|
|
||||||
}
|
|
||||||
],
|
|
||||||
total: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
list.value = mockData.list
|
|
||||||
total.value = mockData.total
|
|
||||||
|
|
||||||
// 更新统计数据
|
// 更新统计数据
|
||||||
updateStatistics()
|
updateStatistics()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取列表失败:', error)
|
console.error('获取列表失败:', error)
|
||||||
|
ElMessage.error('获取列表失败')
|
||||||
|
|
||||||
|
// 清空列表数据
|
||||||
|
list.value = []
|
||||||
|
total.value = 0
|
||||||
|
|
||||||
|
// 更新统计数据
|
||||||
|
updateStatistics()
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -476,18 +446,18 @@ const updateStatistics = () => {
|
||||||
total: list.value.length,
|
total: list.value.length,
|
||||||
enabled: list.value.filter((item) => item.status === 0).length,
|
enabled: list.value.filter((item) => item.status === 0).length,
|
||||||
disabled: list.value.filter((item) => item.status === 1).length,
|
disabled: list.value.filter((item) => item.status === 1).length,
|
||||||
// TODO @puhui999:这里缺了 lastTriggeredTime 定义
|
// 已触发的规则数量 (暂时使用启用状态的规则数量)
|
||||||
triggered: list.value.filter((item) => item.lastTriggeredTime).length
|
triggered: list.value.filter((item) => item.status === 0).length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取触发器摘要
|
/** 获取触发器摘要 */
|
||||||
const getTriggerSummary = (rule: IotRuleScene) => {
|
const getTriggerSummary = (rule: IotRuleSceneDO) => {
|
||||||
return getRuleSceneSummary(rule).triggerSummary
|
return getRuleSceneSummary(rule).triggerSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取执行器摘要
|
/** 获取执行器摘要 */
|
||||||
const getActionSummary = (rule: IotRuleScene) => {
|
const getActionSummary = (rule: IotRuleSceneDO) => {
|
||||||
return getRuleSceneSummary(rule).actionSummary
|
return getRuleSceneSummary(rule).actionSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -511,41 +481,38 @@ const handleAdd = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 修改操作 */
|
/** 修改操作 */
|
||||||
const handleEdit = (row: IotRuleScene) => {
|
const handleEdit = (row: IotRuleSceneDO) => {
|
||||||
currentRule.value = row
|
currentRule.value = row
|
||||||
formVisible.value = true
|
formVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除按钮操作 */
|
/** 删除按钮操作 */
|
||||||
// TODO @puhui999:貌似 id 没用上
|
|
||||||
const handleDelete = async (id: number) => {
|
const handleDelete = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
// 删除的二次确认
|
// 删除的二次确认
|
||||||
await message.delConfirm()
|
await message.delConfirm()
|
||||||
// 发起删除
|
// 发起删除
|
||||||
// await RuleSceneApi.deleteRuleScene(id)
|
await RuleSceneApi.deleteRuleScene(id)
|
||||||
|
|
||||||
// 模拟删除操作
|
|
||||||
message.success(t('common.delSuccess'))
|
message.success(t('common.delSuccess'))
|
||||||
// 刷新列表
|
// 刷新列表
|
||||||
await getList()
|
await getList()
|
||||||
} catch {}
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error)
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 修改状态 */
|
/** 修改状态 */
|
||||||
const handleToggleStatus = async (row: IotRuleScene) => {
|
const handleToggleStatus = async (row: IotRuleSceneDO) => {
|
||||||
try {
|
try {
|
||||||
// 修改状态的二次确认
|
// 修改状态的二次确认
|
||||||
const text = row.status === 0 ? '禁用' : '启用'
|
const text = row.status === 0 ? '禁用' : '启用'
|
||||||
await message.confirm('确认要' + text + '"' + row.name + '"吗?')
|
await message.confirm('确认要' + text + '"' + row.name + '"吗?')
|
||||||
// 发起修改状态
|
// 发起修改状态
|
||||||
// TODO @puhui999:这里缺了
|
await RuleSceneApi.updateRuleSceneStatus(row.id!, row.status === 0 ? 1 : 0)
|
||||||
// await RuleSceneApi.updateRuleSceneStatus(row.id, row.status === 0 ? 1 : 0)
|
|
||||||
|
|
||||||
// 模拟状态切换
|
|
||||||
row.status = row.status === 0 ? 1 : 0
|
|
||||||
message.success(text + '成功')
|
message.success(text + '成功')
|
||||||
// 刷新统计
|
// 刷新
|
||||||
|
await getList()
|
||||||
updateStatistics()
|
updateStatistics()
|
||||||
} catch {
|
} catch {
|
||||||
// 取消后,进行恢复按钮
|
// 取消后,进行恢复按钮
|
||||||
|
|
@ -554,59 +521,11 @@ const handleToggleStatus = async (row: IotRuleScene) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 多选框选中数据 */
|
/** 多选框选中数据 */
|
||||||
const handleSelectionChange = (selection: IotRuleScene[]) => {
|
const handleSelectionChange = (selection: IotRuleSceneDO[]) => {
|
||||||
selectedRows.value = selection
|
selectedRows.value = selection
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 批量启用操作 */
|
/** 初始化 */
|
||||||
const handleBatchEnable = async () => {
|
|
||||||
try {
|
|
||||||
await message.confirm(`确定要启用选中的 ${selectedRows.value.length} 个规则吗?`)
|
|
||||||
// 这里应该调用批量启用API
|
|
||||||
// TODO @puhui999:这里缺了
|
|
||||||
// await RuleSceneApi.updateRuleSceneStatusBatch(selectedRows.value.map(row => row.id), 0)
|
|
||||||
|
|
||||||
// 模拟批量启用
|
|
||||||
selectedRows.value.forEach((row) => {
|
|
||||||
row.status = 0
|
|
||||||
})
|
|
||||||
message.success('批量启用成功')
|
|
||||||
updateStatistics()
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 批量禁用操作 */
|
|
||||||
const handleBatchDisable = async () => {
|
|
||||||
try {
|
|
||||||
await message.confirm(`确定要禁用选中的 ${selectedRows.value.length} 个规则吗?`)
|
|
||||||
// 这里应该调用批量禁用API
|
|
||||||
// TODO @puhui999:这里缺了
|
|
||||||
// await RuleSceneApi.updateRuleSceneStatusBatch(selectedRows.value.map(row => row.id), 1)
|
|
||||||
|
|
||||||
// 模拟批量禁用
|
|
||||||
selectedRows.value.forEach((row) => {
|
|
||||||
row.status = 1
|
|
||||||
})
|
|
||||||
message.success('批量禁用成功')
|
|
||||||
updateStatistics()
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 批量删除操作 */
|
|
||||||
const handleBatchDelete = async () => {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(`确定要删除选中的 ${selectedRows.value.length} 个规则吗?`, '提示', {
|
|
||||||
type: 'warning'
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO @puhui999:这里缺了
|
|
||||||
// 这里应该调用批量删除API
|
|
||||||
message.success('批量删除成功')
|
|
||||||
await getList()
|
|
||||||
} catch (error) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getList()
|
getList()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -260,6 +260,66 @@ export const IotRuleSceneActionTypeEnum = {
|
||||||
ALERT_RECOVER: 101 // 告警恢复
|
ALERT_RECOVER: 101 // 告警恢复
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
/** 执行器类型选项配置 */
|
||||||
|
export const getActionTypeOptions = () => [
|
||||||
|
{
|
||||||
|
value: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
|
||||||
|
label: '设备属性设置',
|
||||||
|
description: '设置目标设备的属性值',
|
||||||
|
icon: 'ep:edit',
|
||||||
|
tag: 'primary',
|
||||||
|
category: '设备控制'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE,
|
||||||
|
label: '设备服务调用',
|
||||||
|
description: '调用目标设备的服务',
|
||||||
|
icon: 'ep:service',
|
||||||
|
tag: 'success',
|
||||||
|
category: '设备控制'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
|
||||||
|
label: '触发告警',
|
||||||
|
description: '触发系统告警通知',
|
||||||
|
icon: 'ep:warning',
|
||||||
|
tag: 'danger',
|
||||||
|
category: '告警通知'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: IotRuleSceneActionTypeEnum.ALERT_RECOVER,
|
||||||
|
label: '恢复告警',
|
||||||
|
description: '恢复已触发的告警',
|
||||||
|
icon: 'ep:circle-check',
|
||||||
|
tag: 'warning',
|
||||||
|
category: '告警通知'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 判断是否为设备执行器类型 */
|
||||||
|
export const isDeviceAction = (type: number): boolean => {
|
||||||
|
const deviceActionTypes = [
|
||||||
|
IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
|
||||||
|
IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
|
||||||
|
] as number[]
|
||||||
|
return deviceActionTypes.includes(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断是否为告警执行器类型 */
|
||||||
|
export const isAlertAction = (type: number): boolean => {
|
||||||
|
const alertActionTypes = [
|
||||||
|
IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
|
||||||
|
IotRuleSceneActionTypeEnum.ALERT_RECOVER
|
||||||
|
] as number[]
|
||||||
|
return alertActionTypes.includes(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取执行器类型标签 */
|
||||||
|
export const getActionTypeLabel = (type: number): string => {
|
||||||
|
const option = getActionTypeOptions().find((opt) => opt.value === type)
|
||||||
|
return option?.label || '未知类型'
|
||||||
|
}
|
||||||
|
|
||||||
/** IoT 设备消息类型枚举 */
|
/** IoT 设备消息类型枚举 */
|
||||||
export const IotDeviceMessageTypeEnum = {
|
export const IotDeviceMessageTypeEnum = {
|
||||||
PROPERTY: 'property', // 属性
|
PROPERTY: 'property', // 属性
|
||||||
|
|
@ -300,3 +360,12 @@ export const IotRuleSceneTriggerTimeOperatorEnum = {
|
||||||
AFTER_TODAY: { name: '在今日之后', value: 'after_today' }, // 在今日之后
|
AFTER_TODAY: { name: '在今日之后', value: 'after_today' }, // 在今日之后
|
||||||
TODAY: { name: '在今日之间', value: 'today' } // 在今日之间
|
TODAY: { name: '在今日之间', value: 'today' } // 在今日之间
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
// ========== 辅助函数 ==========
|
||||||
|
|
||||||
|
/** 获取触发器类型标签 */
|
||||||
|
export const getTriggerTypeLabel = (type: number): string => {
|
||||||
|
const options = getTriggerTypeOptions()
|
||||||
|
const option = options.find((item) => item.value === type)
|
||||||
|
return option?.label || '未知类型'
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue