【功能完善】IoT: 规则场景监听器相关组件

This commit is contained in:
puhui999
2025-03-21 13:30:22 +08:00
parent 07277a6efb
commit 73d2c2005c
6 changed files with 97 additions and 424 deletions

View File

@ -1,12 +1,13 @@
import request from '@/config/axios'
import { IotRuleSceneTriggerConfig } from '@/api/iot/rule/scene/scene.types'
// IoT 规则场景(场景联动) VO
export interface RuleSceneVO {
id?: number // 场景编号
name?: string // 场景名称
name: string // 场景名称
description?: string // 场景描述
status?: number // 场景状态
triggers?: any[] // 触发器数组
status: number // 场景状态
triggers: IotRuleSceneTriggerConfig[] // 触发器数组
actions?: any[] // 执行器数组
}

View File

@ -9,11 +9,11 @@ export interface IotRuleSceneTriggerConfig {
*/
type: number
/** 产品标识 */
productKey?: string
productKey: string
/** 设备名称数组 */
deviceNames?: string[]
deviceNames: string[]
/** 触发条件数组。条件之间是"或"的关系 */
conditions?: IotRuleSceneTriggerCondition[]
conditions: IotRuleSceneTriggerCondition[]
/** CRON 表达式。当 type = 2 时必填 */
cronExpression?: string
}
@ -29,7 +29,7 @@ export interface IotRuleSceneTriggerCondition {
*/
type: string
/** 消息标识符 */
identifier: string
identifier?: string
/** 参数数组。参数之间是"或"的关系 */
parameters: IotRuleSceneTriggerConditionParameter[]
}

View File

@ -61,7 +61,7 @@ export const useAppStore = defineStore('app', {
tagsView: true, // 标签页
tagsViewImmerse: false, // 标签页沉浸
tagsViewIcon: true, // 是否显示标签图标
logo: true, // logo
logo: false, // logo
fixedHeader: true, // 固定toolheader
footer: true, // 显示页脚
greyMode: false, // 是否开始灰色模式,用于特殊悼念日

View File

@ -35,10 +35,19 @@
<el-divider content-position="left">触发器配置</el-divider>
<device-listener
v-for="(trigger, index) in formData.triggers"
:model-value="trigger"
:key="index"
:model-value="trigger"
@update:model-value="(val) => (formData.triggers[index] = val)"
class="mb-10px"
/>
>
<el-button
type="danger"
round
:icon="Delete"
size="small"
@click="removeTrigger(index)"
/>
</device-listener>
<el-text class="ml-10px!" type="primary" @click="addTrigger">添加触发器</el-text>
</el-col>
<el-col :span="24">
@ -59,6 +68,8 @@
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { RuleSceneApi, RuleSceneVO } from '@/api/iot/rule/scene'
import DeviceListener from './components/DeviceListener.vue'
import { Delete } from '@element-plus/icons-vue'
import { IotRuleSceneTriggerConfig } from '@/api/iot/rule/scene/scene.types'
/** IoT 规则场景(场景联动) 表单 */
defineOptions({ name: 'RuleSceneForm' })
@ -72,7 +83,7 @@ const formLoading = ref(false) // 表单的加载中1修改时的数据加
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref<RuleSceneVO>({
status: 0,
triggers: []
triggers: [] as IotRuleSceneTriggerConfig[]
} as RuleSceneVO)
const formRules = reactive({
name: [{ required: true, message: '场景名称不能为空', trigger: 'blur' }],
@ -82,10 +93,26 @@ const formRules = reactive({
})
const formRef = ref() // 表单 Ref
/** 添加触发器 */
const addTrigger = () => {
formData.value.triggers?.push({})
formData.value.triggers.push({
type: 1,
productKey: '',
deviceNames: [],
conditions: [
{
type: 'property',
parameters: []
}
]
})
}
/** 移除触发器 */
const removeTrigger = (index: number) => {
const newTriggers = [...formData.value.triggers]
newTriggers.splice(index, 1)
formData.value.triggers = newTriggers
}
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
@ -132,7 +159,7 @@ const submitForm = async () => {
const resetForm = () => {
formData.value = {
status: 0,
triggers: []
triggers: [] as IotRuleSceneTriggerConfig[]
} as RuleSceneVO
formRef.value?.resetFields()
}

View File

@ -3,7 +3,12 @@
<div class="device-listener-header h-50px flex items-center px-10px">
<div class="flex items-center mr-60px">
<span class="mr-10px">触发条件</span>
<el-select v-model="triggerType" class="!w-240px" clearable placeholder="请选择触发条件">
<el-select
v-model="triggerConfig.type"
class="!w-240px"
clearable
placeholder="请选择触发条件"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_RULE_SCENE_TRIGGER_TYPE_ENUM)"
:key="dict.value"
@ -20,12 +25,19 @@
<span class="mr-10px">设备</span>
<el-button type="primary">选择设备</el-button>
</div>
<!-- 添加规则 -->
<el-button class="device-listener-delete" type="danger" round :icon="Delete" size="small" />
<!-- 删除触发器 -->
<div class="device-listener-delete">
<slot></slot>
</div>
</div>
<div class="device-listener-condition flex p-10px">
<!-- 触发器条件 -->
<div
class="device-listener-condition flex p-10px"
v-for="(condition, index) in triggerConfig.conditions"
:key="index"
>
<div class="flex flex-col items-center justify-center mr-10px h-a">
<el-select v-model="messageType" class="!w-160px" clearable placeholder="">
<el-select v-model="condition.type" class="!w-160px" clearable placeholder="">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.IOT_DEVICE_MESSAGE_TYPE_ENUM)"
:key="dict.value"
@ -36,62 +48,74 @@
</div>
<div class="">
<DeviceListenerCondition
v-for="(conditionParameter, index) in conditionParameters"
:key="index"
:model-value="conditionParameter"
@update:model-value="(val) => (conditionParameters[index] = val)"
v-for="(parameter, index2) in condition.parameters"
:key="index2"
:model-value="parameter"
@update:model-value="(val) => (condition.parameters[index2] = val)"
class="mb-10px last:mb-0"
>
<!-- 添加规则 -->
<!-- 删除规则 -->
<el-button
class="device-listener-delete"
type="danger"
circle
:icon="Delete"
size="small"
@click="removeConditionParameter(index)"
@click="removeConditionParameter(condition.parameters, index2)"
/>
</DeviceListenerCondition>
</div>
<div class="flex flex-1 flex-col items-center justify-center w-a h-a">
<!-- 添加规则 -->
<el-button type="primary" circle :icon="Plus" size="small" @click="addConditionParameter" />
<el-button
type="primary"
circle
:icon="Plus"
size="small"
@click="addConditionParameter(condition.parameters)"
/>
</div>
</div>
<el-text class="ml-10px!" type="primary" @click="addCondition">添加触发条件</el-text>
</div>
</template>
<script setup lang="ts">
import { Delete, Plus } from '@element-plus/icons-vue'
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
import { ref } from 'vue'
import DeviceListenerCondition from './DeviceListenerCondition.vue'
import { IotRuleSceneTriggerConditionParameter } from '@/api/iot/rule/scene/scene.types'
import {
IotRuleSceneTriggerConditionParameter,
IotRuleSceneTriggerConfig
} from '@/api/iot/rule/scene/scene.types'
import { useVModel } from '@vueuse/core'
/** 场景联动之监听器组件 */
defineOptions({ name: 'DeviceListener' })
defineProps<{
modelValue: any
}>()
const props = defineProps<{ modelValue: any }>()
const emits = defineEmits(['update:modelValue'])
const triggerConfig = useVModel(props, 'modelValue', emits) as Ref<IotRuleSceneTriggerConfig>
const emit = defineEmits(['update:modelValue'])
// 添加响应式变量
const triggerType = ref()
const messageType = ref('property')
const conditionParameters = ref<IotRuleSceneTriggerConditionParameter[]>([])
/** 添加触发条件 */
const addConditionParameter = () => {
conditionParameters.value?.push({} as IotRuleSceneTriggerConditionParameter)
const addCondition = () => {
triggerConfig.value.conditions.push({
type: 'property',
parameters: []
})
}
/** 移除触发条件 */
const removeConditionParameter = (index: number) => {
conditionParameters.value?.splice(index, 1)
/** 添加参数 */
const addConditionParameter = (conditionParameters: IotRuleSceneTriggerConditionParameter[]) => {
conditionParameters.push({} as IotRuleSceneTriggerConditionParameter)
}
/** 移除参数 */
const removeConditionParameter = (
conditionParameters: IotRuleSceneTriggerConditionParameter[],
index: number
) => {
conditionParameters.splice(index, 1)
}
onMounted(() => {
addConditionParameter()
})
</script>
<style lang="scss" scoped>

View File

@ -1,379 +0,0 @@
<template>
<div class="trigger-conditions">
<div class="conditions-header mb-3">
<el-button type="primary" @click="addCondition" :disabled="!productKey">
<Icon icon="ep:plus" class="mr-5px" /> 添加条件
</el-button>
<div class="conditions-tips" v-if="modelValue && modelValue.length > 0">
多个条件之间为""关系
</div>
</div>
<el-empty v-if="!modelValue || modelValue.length === 0" description="暂无触发条件" />
<div class="conditions-list" v-else>
<div v-for="(condition, index) in modelValue" :key="index" class="condition-item mb-3">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>条件 {{ index + 1 }}</span>
<el-button type="danger" link @click="removeCondition(index)"> 删除 </el-button>
</div>
</template>
<div class="condition-content">
<el-form label-width="100px" :model="condition">
<el-form-item label="消息类型">
<el-select
v-model="condition.type"
placeholder="请选择消息类型"
@change="handleMessageTypeChange(index)"
>
<el-option label="属性上报" :value="IotDeviceMessageTypeEnum.PROPERTY" />
<el-option label="事件上报" :value="IotDeviceMessageTypeEnum.EVENT" />
</el-select>
</el-form-item>
<el-form-item label="消息标识符">
<el-select
v-model="condition.identifier"
placeholder="请选择消息标识符"
filterable
:loading="thingModelLoading"
@change="handleIdentifierChange(index)"
>
<el-option
v-for="item in getThingModelOptions(condition.type)"
:key="item.identifier"
:label="item.name"
:value="item.identifier"
>
<div class="thing-model-option">
<span>{{ item.name }}</span>
<span class="thing-model-identifier">{{ item.identifier }}</span>
</div>
<div class="thing-model-desc" v-if="item.description">{{
item.description
}}</div>
</el-option>
</el-select>
</el-form-item>
<div class="parameters-area mt-3 mb-2">
<div class="parameters-header">
<div>参数列表多个参数之间为"或"关系</div>
<el-button type="primary" link @click="addParameter(index)"> 添加参数 </el-button>
</div>
<el-empty
v-if="!condition.parameters || condition.parameters.length === 0"
description="暂无参数"
/>
<div class="parameters-list mt-2" v-else>
<div
v-for="(param, pIndex) in condition.parameters"
:key="pIndex"
class="parameter-item mb-2"
>
<el-card shadow="hover">
<div class="parameter-item-header">
<span>参数 {{ pIndex + 1 }}</span>
<el-button type="danger" link @click="removeParameter(index, pIndex)">
删除
</el-button>
</div>
<el-form label-width="90px" :model="param" class="mt-2">
<el-form-item label="标识符">
<el-select
v-model="param.identifier"
placeholder="请选择参数标识符"
filterable
>
<el-option
v-for="item in getParameterOptions(condition)"
:key="item.identifier"
:label="item.name"
:value="item.identifier"
>
<div class="thing-model-option">
<span>{{ item.name }}</span>
<span class="thing-model-identifier">{{ item.identifier }}</span>
</div>
<div class="thing-model-desc" v-if="item.description">{{
item.description
}}</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="条件">
<condition-selector
v-model="param.condition"
:placeholder="'请选择条件'"
:value-placeholder="'请输入比较值'"
/>
</el-form-item>
</el-form>
</el-card>
</div>
</div>
</div>
</el-form>
</div>
</el-card>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineEmits, defineProps, onMounted, ref, watch } from 'vue'
import {
IotDeviceMessageIdentifierEnum,
IotDeviceMessageTypeEnum,
IotRuleSceneTriggerCondition,
IotRuleSceneTriggerConditionParameter
} from '@/api/iot/rule/scene/scene.types'
import { ThingModelApi, ThingModelData } from '@/api/iot/thingmodel'
import ConditionSelector from './ConditionSelector.vue'
const props = defineProps({
modelValue: {
type: Array as () => IotRuleSceneTriggerCondition[],
required: true
},
productKey: {
type: String,
required: true
}
})
const emit = defineEmits(['update:modelValue'])
// 物模型数据
const thingModelList = ref<ThingModelData[]>([])
const thingModelLoading = ref(false)
// 加载物模型数据
const loadThingModelData = async () => {
if (!props.productKey) return
try {
thingModelLoading.value = true
const result = await ThingModelApi.getThingModelListByProductId({
productKey: props.productKey
})
thingModelList.value = result || []
} catch (error) {
console.error('获取物模型数据失败', error)
} finally {
thingModelLoading.value = false
}
}
// 获取物模型选项
const getThingModelOptions = (type: string) => {
if (!thingModelList.value) return []
return thingModelList.value.filter((item) => {
if (type === IotDeviceMessageTypeEnum.PROPERTY) {
return item.property
} else if (type === IotDeviceMessageTypeEnum.EVENT) {
return item.event
}
return false
})
}
// 获取参数选项
const getParameterOptions = (condition: IotRuleSceneTriggerCondition) => {
if (!condition || !condition.identifier) return []
const model = thingModelList.value?.find((item) => item.identifier === condition.identifier)
if (!model) return []
if (condition.type === IotDeviceMessageTypeEnum.PROPERTY) {
return [model] // 属性本身就是参数
} else if (condition.type === IotDeviceMessageTypeEnum.EVENT) {
// TODO: 获取事件的输出参数列表
return []
}
return []
}
// 添加条件
const addCondition = () => {
const newCondition: IotRuleSceneTriggerCondition = {
type: IotDeviceMessageTypeEnum.PROPERTY,
identifier: IotDeviceMessageIdentifierEnum.PROPERTY_REPORT,
parameters: []
}
const newValue = [...(props.modelValue || []), newCondition]
emit('update:modelValue', newValue)
}
// 移除条件
const removeCondition = (index: number) => {
const newValue = [...props.modelValue]
newValue.splice(index, 1)
emit('update:modelValue', newValue)
}
// 消息类型变更
const handleMessageTypeChange = (index: number) => {
const newValue = [...props.modelValue]
// 更新标识符
if (newValue[index].type === IotDeviceMessageTypeEnum.PROPERTY) {
newValue[index].identifier = IotDeviceMessageIdentifierEnum.PROPERTY_REPORT
} else if (newValue[index].type === IotDeviceMessageTypeEnum.EVENT) {
newValue[index].identifier = IotDeviceMessageIdentifierEnum.EVENT_REPORT
}
// 清空参数
newValue[index].parameters = []
emit('update:modelValue', newValue)
}
// 标识符变更
const handleIdentifierChange = (index: number) => {
const newValue = [...props.modelValue]
// 清空参数
newValue[index].parameters = []
emit('update:modelValue', newValue)
}
// 添加参数
const addParameter = (conditionIndex: number) => {
const newValue = [...props.modelValue]
if (!newValue[conditionIndex].parameters) {
newValue[conditionIndex].parameters = []
}
const newParameter: IotRuleSceneTriggerConditionParameter = {
identifier: '',
condition: {
operator: 'eq',
value: ''
}
}
newValue[conditionIndex].parameters.push(newParameter)
emit('update:modelValue', newValue)
}
// 移除参数
const removeParameter = (conditionIndex: number, paramIndex: number) => {
const newValue = [...props.modelValue]
newValue[conditionIndex].parameters.splice(paramIndex, 1)
emit('update:modelValue', newValue)
}
// 监听 productKey 变化
watch(
() => props.productKey,
(newVal) => {
if (!newVal) {
// 清空条件
if (props.modelValue?.length > 0) {
emit('update:modelValue', [])
}
// 清空物模型数据
thingModelList.value = []
} else {
// 加载物模型数据
loadThingModelData()
}
}
)
// 初始化
onMounted(() => {
if (props.productKey) {
loadThingModelData()
}
})
</script>
<style scoped>
.trigger-conditions {
width: 100%;
}
.conditions-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.conditions-tips {
font-size: 12px;
color: #999;
}
.condition-item {
border-radius: 4px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.parameters-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-weight: bold;
}
.parameter-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.value-tips {
margin-top: 5px;
font-size: 12px;
color: #999;
}
.thing-model-option {
display: flex;
justify-content: space-between;
align-items: center;
}
.thing-model-identifier {
font-size: 12px;
color: #999;
}
.thing-model-desc {
margin-top: 4px;
font-size: 12px;
color: #666;
}
.mb-3 {
margin-bottom: 12px;
}
.mb-2 {
margin-bottom: 8px;
}
.mt-3 {
margin-top: 12px;
}
.mt-2 {
margin-top: 8px;
}
.mr-5px {
margin-right: 5px;
}
</style>