【功能完善】IoT: 场景联动执行器配置
This commit is contained in:
parent
fe905721bd
commit
eadf26dc3b
@ -247,5 +247,6 @@ export enum DICT_TYPE {
|
|||||||
IOT_DATA_BRIDGE_DIRECTION_ENUM = 'iot_data_bridge_direction_enum', // 桥梁方向
|
IOT_DATA_BRIDGE_DIRECTION_ENUM = 'iot_data_bridge_direction_enum', // 桥梁方向
|
||||||
IOT_DATA_BRIDGE_TYPE_ENUM = 'iot_data_bridge_type_enum', // 桥梁类型
|
IOT_DATA_BRIDGE_TYPE_ENUM = 'iot_data_bridge_type_enum', // 桥梁类型
|
||||||
IOT_DEVICE_MESSAGE_TYPE_ENUM = 'iot_device_message_type_enum', // IoT 设备消息类型枚举
|
IOT_DEVICE_MESSAGE_TYPE_ENUM = 'iot_device_message_type_enum', // IoT 设备消息类型枚举
|
||||||
IOT_RULE_SCENE_TRIGGER_TYPE_ENUM = 'iot_rule_scene_trigger_type_enum' // IoT 场景流转的触发类型枚举
|
IOT_RULE_SCENE_TRIGGER_TYPE_ENUM = 'iot_rule_scene_trigger_type_enum', // IoT 场景流转的触发类型枚举
|
||||||
|
IOT_RULE_SCENE_ACTION_TYPE_ENUM = 'iot_rule_scene_action_type_enum' // IoT 规则场景的触发类型枚举
|
||||||
}
|
}
|
||||||
|
@ -49,10 +49,21 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-divider content-position="left">执行动作配置</el-divider>
|
<el-divider content-position="left">执行器配置</el-divider>
|
||||||
<el-form-item label="执行器数组" prop="actionConfigs">
|
<action-executor
|
||||||
<!-- <el-input v-model="formData.actions" placeholder="请输入执行器数组" />-->
|
v-for="(action, index) in formData.actions"
|
||||||
</el-form-item>
|
:key="index"
|
||||||
|
:model-value="action"
|
||||||
|
@update:model-value="(val) => (formData.actions[index] = val)"
|
||||||
|
class="mb-10px"
|
||||||
|
>
|
||||||
|
<el-button type="danger" round size="small" @click="removeAction(index)">
|
||||||
|
<Icon icon="ep:delete" />
|
||||||
|
</el-button>
|
||||||
|
</action-executor>
|
||||||
|
<el-button class="ml-10px!" type="primary" size="small" @click="addAction">
|
||||||
|
添加执行器
|
||||||
|
</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
@ -65,15 +76,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||||
import { RuleSceneApi } from '@/api/iot/rule/scene'
|
import { RuleSceneApi } from '@/api/iot/rule/scene'
|
||||||
import DeviceListener from './components/DeviceListener.vue'
|
import DeviceListener from './components/listener/DeviceListener.vue'
|
||||||
import { CommonStatusEnum } from '@/utils/constants'
|
import { CommonStatusEnum } from '@/utils/constants'
|
||||||
import {
|
import {
|
||||||
|
ActionConfig,
|
||||||
IotDeviceMessageIdentifierEnum,
|
IotDeviceMessageIdentifierEnum,
|
||||||
IotDeviceMessageTypeEnum,
|
IotDeviceMessageTypeEnum,
|
||||||
IotRuleScene,
|
IotRuleScene,
|
||||||
|
IotRuleSceneActionTypeEnum,
|
||||||
IotRuleSceneTriggerTypeEnum,
|
IotRuleSceneTriggerTypeEnum,
|
||||||
TriggerConfig
|
TriggerConfig
|
||||||
} from '@/api/iot/rule/scene/scene.types'
|
} from '@/api/iot/rule/scene/scene.types'
|
||||||
|
import ActionExecutor from './components/action/ActionExecutor.vue'
|
||||||
|
|
||||||
/** IoT 场景联动表单 */
|
/** IoT 场景联动表单 */
|
||||||
defineOptions({ name: 'IotRuleSceneForm' })
|
defineOptions({ name: 'IotRuleSceneForm' })
|
||||||
@ -87,7 +101,8 @@ const formLoading = ref(false) // 表单的加载中:1)修改时的数据加
|
|||||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||||
const formData = ref<IotRuleScene>({
|
const formData = ref<IotRuleScene>({
|
||||||
status: CommonStatusEnum.ENABLE,
|
status: CommonStatusEnum.ENABLE,
|
||||||
triggers: [] as TriggerConfig[]
|
triggers: [] as TriggerConfig[],
|
||||||
|
actions: [] as ActionConfig[]
|
||||||
} as IotRuleScene)
|
} as IotRuleScene)
|
||||||
const formRules = reactive({
|
const formRules = reactive({
|
||||||
name: [{ required: true, message: '场景名称不能为空', trigger: 'blur' }],
|
name: [{ required: true, message: '场景名称不能为空', trigger: 'blur' }],
|
||||||
@ -119,6 +134,19 @@ const removeTrigger = (index: number) => {
|
|||||||
formData.value.triggers = newTriggers
|
formData.value.triggers = newTriggers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 添加执行器 */
|
||||||
|
const addAction = () => {
|
||||||
|
formData.value.actions.push({
|
||||||
|
type: IotRuleSceneActionTypeEnum.DEVICE_CONTROL
|
||||||
|
} as ActionConfig)
|
||||||
|
}
|
||||||
|
/** 移除执行器 */
|
||||||
|
const removeAction = (index: number) => {
|
||||||
|
const newActions = [...formData.value.actions]
|
||||||
|
newActions.splice(index, 1)
|
||||||
|
formData.value.actions = newActions
|
||||||
|
}
|
||||||
|
|
||||||
/** 打开弹窗 */
|
/** 打开弹窗 */
|
||||||
const open = async (type: string, id?: number) => {
|
const open = async (type: string, id?: number) => {
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
@ -165,7 +193,8 @@ const submitForm = async () => {
|
|||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
formData.value = {
|
formData.value = {
|
||||||
status: CommonStatusEnum.ENABLE,
|
status: CommonStatusEnum.ENABLE,
|
||||||
triggers: [] as TriggerConfig[]
|
triggers: [] as TriggerConfig[],
|
||||||
|
actions: [] as ActionConfig[]
|
||||||
} as IotRuleScene
|
} as IotRuleScene
|
||||||
formRef.value?.resetFields()
|
formRef.value?.resetFields()
|
||||||
}
|
}
|
||||||
|
180
src/views/iot/rule/scene/components/action/ActionExecutor.vue
Normal file
180
src/views/iot/rule/scene/components/action/ActionExecutor.vue
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<div class="m-10px">
|
||||||
|
<div class="relative bg-[#eff3f7] h-50px flex items-center px-10px">
|
||||||
|
<div class="flex items-center mr-60px">
|
||||||
|
<span class="mr-10px">执行动作</span>
|
||||||
|
<el-select
|
||||||
|
v-model="actionConfig.type"
|
||||||
|
class="!w-240px"
|
||||||
|
clearable
|
||||||
|
placeholder="请选择执行类型"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_RULE_SCENE_ACTION_TYPE_ENUM)"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="actionConfig.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL"
|
||||||
|
class="flex items-center mr-60px"
|
||||||
|
>
|
||||||
|
<span class="mr-10px">产品</span>
|
||||||
|
<el-button type="primary" @click="productTableSelectRef?.open()" size="small" plain>
|
||||||
|
{{ product ? product.name : '选择产品' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="actionConfig.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL"
|
||||||
|
class="flex items-center mr-60px"
|
||||||
|
>
|
||||||
|
<span class="mr-10px">设备</span>
|
||||||
|
<el-button type="primary" @click="openDeviceSelect" size="small" plain>
|
||||||
|
{{ isEmpty(deviceList) ? '选择设备' : actionConfig.deviceControl?.deviceNames.join(',') }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<!-- 删除执行器 -->
|
||||||
|
<div class="absolute top-auto right-16px bottom-auto">
|
||||||
|
<el-tooltip content="删除执行器" placement="top">
|
||||||
|
<slot></slot>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 设备控制执行器 -->
|
||||||
|
<DeviceControlAction
|
||||||
|
v-if="actionConfig.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL"
|
||||||
|
:model-value="actionConfig.deviceControl"
|
||||||
|
@update:model-value="(val) => (actionConfig.deviceControl = val)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 告警执行器 -->
|
||||||
|
<AlertAction
|
||||||
|
v-else-if="actionConfig.type === IotRuleSceneActionTypeEnum.ALERT"
|
||||||
|
:model-value="actionConfig.alert"
|
||||||
|
@update:model-value="(val) => (actionConfig.alert = val)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 数据桥接执行器 -->
|
||||||
|
<DataBridgeAction
|
||||||
|
v-else-if="actionConfig.type === IotRuleSceneActionTypeEnum.DATA_BRIDGE"
|
||||||
|
:model-value="actionConfig.dataBridgeId"
|
||||||
|
@update:model-value="(val) => (actionConfig.dataBridgeId = val)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 产品、设备的选择 -->
|
||||||
|
<ProductTableSelect ref="productTableSelectRef" @success="handleProductSelect" />
|
||||||
|
<DeviceTableSelect
|
||||||
|
ref="deviceTableSelectRef"
|
||||||
|
multiple
|
||||||
|
:product-id="product?.id"
|
||||||
|
@success="handleDeviceSelect"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import { isEmpty } from '@/utils/is'
|
||||||
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||||
|
import ProductTableSelect from '@/views/iot/product/product/components/ProductTableSelect.vue'
|
||||||
|
import DeviceTableSelect from '@/views/iot/device/device/components/DeviceTableSelect.vue'
|
||||||
|
import DeviceControlAction from './DeviceControlAction.vue'
|
||||||
|
import AlertAction from './AlertAction.vue'
|
||||||
|
import DataBridgeAction from './DataBridgeAction.vue'
|
||||||
|
import { ProductVO } from '@/api/iot/product/product'
|
||||||
|
import { DeviceVO } from '@/api/iot/device/device'
|
||||||
|
import { ThingModelApi } from '@/api/iot/thingmodel'
|
||||||
|
import {
|
||||||
|
ActionAlert,
|
||||||
|
ActionConfig,
|
||||||
|
ActionDeviceControl,
|
||||||
|
IotRuleSceneActionTypeEnum
|
||||||
|
} from '@/api/iot/rule/scene/scene.types'
|
||||||
|
|
||||||
|
/** 场景联动之执行器组件 */
|
||||||
|
defineOptions({ name: 'ActionExecutor' })
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>()
|
||||||
|
const emits = defineEmits(['update:modelValue'])
|
||||||
|
const actionConfig = useVModel(props, 'modelValue', emits) as Ref<ActionConfig>
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 初始化执行器结构
|
||||||
|
const initActionConfig = () => {
|
||||||
|
if (!actionConfig.value) {
|
||||||
|
actionConfig.value = { type: IotRuleSceneActionTypeEnum.DEVICE_CONTROL } as ActionConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设备控制执行器初始化
|
||||||
|
if (
|
||||||
|
actionConfig.value.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL &&
|
||||||
|
!actionConfig.value.deviceControl
|
||||||
|
) {
|
||||||
|
actionConfig.value.deviceControl = {} as ActionDeviceControl
|
||||||
|
}
|
||||||
|
|
||||||
|
// 告警执行器初始化
|
||||||
|
if (actionConfig.value.type === IotRuleSceneActionTypeEnum.ALERT && !actionConfig.value.alert) {
|
||||||
|
actionConfig.value.alert = {} as ActionAlert
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据桥接执行器初始化
|
||||||
|
if (
|
||||||
|
actionConfig.value.type === IotRuleSceneActionTypeEnum.DATA_BRIDGE &&
|
||||||
|
!actionConfig.value.dataBridgeId
|
||||||
|
) {
|
||||||
|
actionConfig.value.dataBridgeId = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听执行类型变化,初始化对应配置
|
||||||
|
watch(
|
||||||
|
() => actionConfig.value.type,
|
||||||
|
() => {
|
||||||
|
initActionConfig()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 产品和设备选择
|
||||||
|
const productTableSelectRef = ref<InstanceType<typeof ProductTableSelect>>()
|
||||||
|
const deviceTableSelectRef = ref<InstanceType<typeof DeviceTableSelect>>()
|
||||||
|
const product = ref<ProductVO>()
|
||||||
|
const deviceList = ref<DeviceVO[]>([])
|
||||||
|
|
||||||
|
/** 处理产品选择 */
|
||||||
|
const handleProductSelect = (val: ProductVO) => {
|
||||||
|
product.value = val
|
||||||
|
actionConfig.value.deviceControl!.productKey = val.productKey
|
||||||
|
deviceList.value = []
|
||||||
|
getThingModelTSL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理设备选择 */
|
||||||
|
const handleDeviceSelect = (val: DeviceVO[]) => {
|
||||||
|
deviceList.value = val
|
||||||
|
actionConfig.value.deviceControl!.deviceNames = val.map((item) => item.deviceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开设备选择器 */
|
||||||
|
const openDeviceSelect = () => {
|
||||||
|
if (!product.value) {
|
||||||
|
message.warning('请先选择一个产品')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
deviceTableSelectRef.value?.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取产品物模型 */
|
||||||
|
const thingModelTSL = ref<any>()
|
||||||
|
const getThingModelTSL = async () => {
|
||||||
|
if (!product.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(product.value.id)
|
||||||
|
}
|
||||||
|
</script>
|
83
src/views/iot/rule/scene/components/action/AlertAction.vue
Normal file
83
src/views/iot/rule/scene/components/action/AlertAction.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-[#dbe5f6] p-10px">
|
||||||
|
<div class="flex items-center mb-10px">
|
||||||
|
<span class="mr-10px w-80px">接收方式</span>
|
||||||
|
<el-select
|
||||||
|
v-model="alertConfig.receiveType"
|
||||||
|
class="!w-160px"
|
||||||
|
clearable
|
||||||
|
placeholder="选择接收方式"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="(value, key) in IotAlertConfigReceiveTypeEnum"
|
||||||
|
:key="value"
|
||||||
|
:label="key === 'SMS' ? '短信' : key === 'MAIL' ? '邮箱' : '通知'"
|
||||||
|
:value="value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div v-if="alertConfig.receiveType === IotAlertConfigReceiveTypeEnum.SMS" class="flex items-center mb-10px">
|
||||||
|
<span class="mr-10px w-80px">手机号码</span>
|
||||||
|
<el-select
|
||||||
|
v-model="alertConfig.phoneNumbers"
|
||||||
|
class="!w-360px"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
placeholder="请输入手机号码"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="alertConfig.receiveType === IotAlertConfigReceiveTypeEnum.MAIL" class="flex items-center mb-10px">
|
||||||
|
<span class="mr-10px w-80px">邮箱地址</span>
|
||||||
|
<el-select
|
||||||
|
v-model="alertConfig.emails"
|
||||||
|
class="!w-360px"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
placeholder="请输入邮箱地址"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="mr-10px w-80px align-self-start">通知内容</span>
|
||||||
|
<el-input
|
||||||
|
v-model="alertConfig.content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
class="!w-360px"
|
||||||
|
placeholder="请输入通知内容"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import { ActionAlert, IotAlertConfigReceiveTypeEnum } from '@/api/iot/rule/scene/scene.types'
|
||||||
|
|
||||||
|
/** 告警执行器组件 */
|
||||||
|
defineOptions({ name: 'AlertAction' })
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>()
|
||||||
|
const emits = defineEmits(['update:modelValue'])
|
||||||
|
const alertConfig = useVModel(props, 'modelValue', emits) as Ref<ActionAlert>
|
||||||
|
|
||||||
|
// 初始化告警执行器结构
|
||||||
|
const initAlertConfig = () => {
|
||||||
|
if (!alertConfig.value) {
|
||||||
|
alertConfig.value = {
|
||||||
|
receiveType: IotAlertConfigReceiveTypeEnum.NOTIFY,
|
||||||
|
phoneNumbers: [],
|
||||||
|
emails: [],
|
||||||
|
content: ''
|
||||||
|
} as ActionAlert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
initAlertConfig()
|
||||||
|
})
|
||||||
|
</script>
|
@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-[#dbe5f6] p-10px">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="mr-10px w-80px">数据桥接</span>
|
||||||
|
<el-select
|
||||||
|
v-model="dataBridgeId"
|
||||||
|
class="!w-240px"
|
||||||
|
clearable
|
||||||
|
placeholder="选择数据桥接"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="bridge in dataBridgeList"
|
||||||
|
:key="bridge.id"
|
||||||
|
:label="bridge.name"
|
||||||
|
:value="bridge.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core'
|
||||||
|
|
||||||
|
/** 数据桥接执行器组件 */
|
||||||
|
defineOptions({ name: 'DataBridgeAction' })
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>()
|
||||||
|
const emits = defineEmits(['update:modelValue'])
|
||||||
|
const dataBridgeId = useVModel(props, 'modelValue', emits)
|
||||||
|
|
||||||
|
// 模拟数据桥接列表,实际项目中应该从API获取
|
||||||
|
const dataBridgeList = ref([
|
||||||
|
{ id: 1, name: '数据桥接1' },
|
||||||
|
{ id: 2, name: '数据桥接2' },
|
||||||
|
{ id: 3, name: '数据桥接3' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 实际项目中,应该从API获取数据桥接列表
|
||||||
|
// const getDataBridgeList = async () => {
|
||||||
|
// try {
|
||||||
|
// dataBridgeList.value = await DataBridgeApi.getSimpleList()
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('获取数据桥接列表失败', error)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// onMounted(() => {
|
||||||
|
// getDataBridgeList()
|
||||||
|
// })
|
||||||
|
</script>
|
@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<div class="device-action-control">
|
||||||
|
<!-- 属性设置 -->
|
||||||
|
<template v-if="messageType === IotDeviceMessageTypeEnum.PROPERTY">
|
||||||
|
<div class="flex flex-wrap gap-10px">
|
||||||
|
<div v-for="model in thingModels" :key="model.identifier" class="flex items-center mb-10px">
|
||||||
|
<span class="mr-10px inline-block min-w-80px">{{ model.name }}</span>
|
||||||
|
<!-- 根据属性类型渲染不同的输入控件 -->
|
||||||
|
<PropertyValueInput
|
||||||
|
:model-value="propertyData[model.identifier]"
|
||||||
|
:data-specs="model.dataSpecs"
|
||||||
|
@update:model-value="(val) => updatePropertyValue(model.identifier, val)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 服务调用 -->
|
||||||
|
<template v-else-if="messageType === IotDeviceMessageTypeEnum.SERVICE">
|
||||||
|
<div class="flex flex-wrap gap-10px">
|
||||||
|
<template v-if="thingModels && thingModels.inputParams">
|
||||||
|
<div
|
||||||
|
v-for="param in thingModels.inputParams"
|
||||||
|
:key="param.identifier"
|
||||||
|
class="flex items-center mb-10px"
|
||||||
|
>
|
||||||
|
<span class="mr-10px inline-block min-w-80px">{{ param.name }}</span>
|
||||||
|
<!-- 根据服务输入参数类型渲染不同的输入控件 -->
|
||||||
|
<PropertyValueInput
|
||||||
|
:model-value="serviceData[param.identifier]"
|
||||||
|
:data-specs="param.dataSpecs"
|
||||||
|
@update:model-value="(val) => updateServiceValue(param.identifier, val)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="text-gray-500">该服务没有输入参数</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import PropertyValueInput from './PropertyValueInput.vue'
|
||||||
|
import { IotDeviceMessageTypeEnum } from '@/api/iot/rule/scene/scene.types'
|
||||||
|
|
||||||
|
/** 设备控制执行器的参数配置组件 */
|
||||||
|
defineOptions({ name: 'DeviceActionControl' })
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: any
|
||||||
|
thingModels: any
|
||||||
|
messageType: string
|
||||||
|
identifier: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emits = defineEmits(['update:modelValue'])
|
||||||
|
const actionData = useVModel(props, 'modelValue', emits)
|
||||||
|
|
||||||
|
// 属性数据
|
||||||
|
const propertyData = computed({
|
||||||
|
get: () => {
|
||||||
|
return actionData.value || {}
|
||||||
|
},
|
||||||
|
set: (value) => {
|
||||||
|
actionData.value = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 服务数据
|
||||||
|
const serviceData = computed({
|
||||||
|
get: () => {
|
||||||
|
return actionData.value || {}
|
||||||
|
},
|
||||||
|
set: (value) => {
|
||||||
|
actionData.value = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新属性值
|
||||||
|
const updatePropertyValue = (identifier: string, value: any) => {
|
||||||
|
const newData = { ...propertyData.value }
|
||||||
|
newData[identifier] = value
|
||||||
|
propertyData.value = newData
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新服务参数值
|
||||||
|
const updateServiceValue = (identifier: string, value: any) => {
|
||||||
|
const newData = { ...serviceData.value }
|
||||||
|
newData[identifier] = value
|
||||||
|
serviceData.value = newData
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-[#dbe5f6] p-10px">
|
||||||
|
<div class="flex items-center mb-10px">
|
||||||
|
<span class="mr-10px w-60px">产品</span>
|
||||||
|
<el-button type="primary" @click="productTableSelectRef?.open()" size="small" plain>
|
||||||
|
{{ product ? product.name : '选择产品' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-10px">
|
||||||
|
<span class="mr-10px w-60px">设备</span>
|
||||||
|
<el-button type="primary" @click="openDeviceSelect" size="small" plain>
|
||||||
|
{{ isEmpty(deviceList) ? '选择设备' : deviceControlConfig.deviceNames.join(',') }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-10px">
|
||||||
|
<span class="mr-10px w-60px">消息类型</span>
|
||||||
|
<el-select
|
||||||
|
v-model="deviceControlConfig.type"
|
||||||
|
class="!w-160px"
|
||||||
|
clearable
|
||||||
|
placeholder="选择消息类型"
|
||||||
|
>
|
||||||
|
<el-option label="属性" :value="IotDeviceMessageTypeEnum.PROPERTY" />
|
||||||
|
<el-option label="服务" :value="IotDeviceMessageTypeEnum.SERVICE" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div v-if="deviceControlConfig.type" class="flex items-center mb-10px">
|
||||||
|
<span class="mr-10px w-60px">具体操作</span>
|
||||||
|
<el-select
|
||||||
|
v-model="deviceControlConfig.identifier"
|
||||||
|
class="!w-240px"
|
||||||
|
clearable
|
||||||
|
placeholder="选择操作项"
|
||||||
|
>
|
||||||
|
<template v-if="deviceControlConfig.type === IotDeviceMessageTypeEnum.PROPERTY">
|
||||||
|
<el-option :value="IotDeviceMessageIdentifierEnum.PROPERTY_SET" label="属性设置" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="deviceControlConfig.type === IotDeviceMessageTypeEnum.SERVICE">
|
||||||
|
<el-option
|
||||||
|
v-for="model in thingModelTSL?.services"
|
||||||
|
:key="model.identifier"
|
||||||
|
:label="model.name"
|
||||||
|
:value="model.identifier"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<DeviceActionControl
|
||||||
|
v-if="deviceControlConfig.identifier && deviceControlConfig.type"
|
||||||
|
:model-value="deviceControlConfig.data"
|
||||||
|
:thing-models="getThingModels()"
|
||||||
|
:message-type="deviceControlConfig.type"
|
||||||
|
:identifier="deviceControlConfig.identifier"
|
||||||
|
@update:model-value="(val) => (deviceControlConfig.data = val)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 产品、设备的选择 -->
|
||||||
|
<ProductTableSelect ref="productTableSelectRef" @success="handleProductSelect" />
|
||||||
|
<DeviceTableSelect
|
||||||
|
ref="deviceTableSelectRef"
|
||||||
|
multiple
|
||||||
|
:product-id="product?.id"
|
||||||
|
@success="handleDeviceSelect"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import { isEmpty } from '@/utils/is'
|
||||||
|
import ProductTableSelect from '@/views/iot/product/product/components/ProductTableSelect.vue'
|
||||||
|
import DeviceTableSelect from '@/views/iot/device/device/components/DeviceTableSelect.vue'
|
||||||
|
import DeviceActionControl from './DeviceActionControl.vue'
|
||||||
|
import { ProductVO } from '@/api/iot/product/product'
|
||||||
|
import { DeviceVO } from '@/api/iot/device/device'
|
||||||
|
import { ThingModelApi } from '@/api/iot/thingmodel'
|
||||||
|
import {
|
||||||
|
ActionDeviceControl,
|
||||||
|
IotDeviceMessageIdentifierEnum,
|
||||||
|
IotDeviceMessageTypeEnum
|
||||||
|
} from '@/api/iot/rule/scene/scene.types'
|
||||||
|
|
||||||
|
/** 设备控制执行器组件 */
|
||||||
|
defineOptions({ name: 'DeviceControlAction' })
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>()
|
||||||
|
const emits = defineEmits(['update:modelValue'])
|
||||||
|
const deviceControlConfig = useVModel(props, 'modelValue', emits) as Ref<ActionDeviceControl>
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 初始化设备控制执行器结构
|
||||||
|
const initDeviceControlConfig = () => {
|
||||||
|
if (!deviceControlConfig.value) {
|
||||||
|
deviceControlConfig.value = {
|
||||||
|
productKey: '',
|
||||||
|
deviceNames: [],
|
||||||
|
type: IotDeviceMessageTypeEnum.PROPERTY,
|
||||||
|
identifier: IotDeviceMessageIdentifierEnum.PROPERTY_SET,
|
||||||
|
data: {}
|
||||||
|
} as ActionDeviceControl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
initDeviceControlConfig()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 产品和设备选择
|
||||||
|
const productTableSelectRef = ref<InstanceType<typeof ProductTableSelect>>()
|
||||||
|
const deviceTableSelectRef = ref<InstanceType<typeof DeviceTableSelect>>()
|
||||||
|
const product = ref<ProductVO>()
|
||||||
|
const deviceList = ref<DeviceVO[]>([])
|
||||||
|
|
||||||
|
/** 处理产品选择 */
|
||||||
|
const handleProductSelect = (val: ProductVO) => {
|
||||||
|
product.value = val
|
||||||
|
deviceControlConfig.value.productKey = val.productKey
|
||||||
|
deviceList.value = []
|
||||||
|
getThingModelTSL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理设备选择 */
|
||||||
|
const handleDeviceSelect = (val: DeviceVO[]) => {
|
||||||
|
deviceList.value = val
|
||||||
|
deviceControlConfig.value.deviceNames = val.map((item) => item.deviceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开设备选择器 */
|
||||||
|
const openDeviceSelect = () => {
|
||||||
|
if (!product.value) {
|
||||||
|
message.warning('请先选择一个产品')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
deviceTableSelectRef.value?.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取产品物模型 */
|
||||||
|
const thingModelTSL = ref<any>()
|
||||||
|
const getThingModelTSL = async () => {
|
||||||
|
if (!product.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(product.value.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据消息类型和标识符获取对应的物模型 */
|
||||||
|
const getThingModels = () => {
|
||||||
|
if (!deviceControlConfig.value) return []
|
||||||
|
|
||||||
|
switch (deviceControlConfig.value.type) {
|
||||||
|
case IotDeviceMessageTypeEnum.PROPERTY:
|
||||||
|
return thingModelTSL.value?.properties || []
|
||||||
|
case IotDeviceMessageTypeEnum.SERVICE:
|
||||||
|
return thingModelTSL.value?.services.find(
|
||||||
|
(s: any) => s.identifier === deviceControlConfig.value.identifier
|
||||||
|
) || {}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,238 @@
|
|||||||
|
<template>
|
||||||
|
<div class="property-value-input">
|
||||||
|
<!-- 文本类型 -->
|
||||||
|
<template v-if="dataType === 'text'">
|
||||||
|
<el-input
|
||||||
|
v-model="inputValue"
|
||||||
|
class="!w-240px"
|
||||||
|
:placeholder="`请输入${dataSpecs?.name || '值'}`"
|
||||||
|
>
|
||||||
|
<template v-if="dataSpecs?.unitName" #append>{{ dataSpecs.unitName }}</template>
|
||||||
|
</el-input>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 数值类型 -->
|
||||||
|
<template v-else-if="['int', 'float', 'double'].includes(dataType)">
|
||||||
|
<el-input-number
|
||||||
|
v-model="inputValue"
|
||||||
|
class="!w-240px"
|
||||||
|
:min="dataSpecs?.min"
|
||||||
|
:max="dataSpecs?.max"
|
||||||
|
:precision="dataType === 'int' ? 0 : 2"
|
||||||
|
:step="dataType === 'int' ? 1 : 0.1"
|
||||||
|
:placeholder="`请输入${dataSpecs?.name || '值'}`"
|
||||||
|
/>
|
||||||
|
<span v-if="dataSpecs?.unitName" class="ml-5px">{{ dataSpecs.unitName }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 枚举类型 -->
|
||||||
|
<template v-else-if="dataType === 'enum'">
|
||||||
|
<el-select
|
||||||
|
v-model="inputValue"
|
||||||
|
class="!w-240px"
|
||||||
|
clearable
|
||||||
|
:placeholder="`请选择${dataSpecs?.name || '值'}`"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in dataSpecs?.enumList"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.text"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 布尔类型 -->
|
||||||
|
<template v-else-if="dataType === 'bool'">
|
||||||
|
<el-radio-group v-model="inputValue">
|
||||||
|
<el-radio
|
||||||
|
v-for="item in [
|
||||||
|
{ value: true, label: dataSpecs?.trueText || '是' },
|
||||||
|
{ value: false, label: dataSpecs?.falseText || '否' }
|
||||||
|
]"
|
||||||
|
:key="String(item.value)"
|
||||||
|
:label="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 日期类型 -->
|
||||||
|
<template v-else-if="dataType === 'date'">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="inputValue"
|
||||||
|
class="!w-240px"
|
||||||
|
type="datetime"
|
||||||
|
:placeholder="`请选择${dataSpecs?.name || '日期'}`"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 结构体类型 -->
|
||||||
|
<template v-else-if="dataType === 'struct'">
|
||||||
|
<el-collapse class="w-full">
|
||||||
|
<el-collapse-item title="结构体参数">
|
||||||
|
<div
|
||||||
|
v-for="field in dataSpecs?.specs"
|
||||||
|
:key="field.identifier"
|
||||||
|
class="flex items-center mb-10px"
|
||||||
|
>
|
||||||
|
<span class="mr-10px inline-block min-w-80px">{{ field.name }}</span>
|
||||||
|
<PropertyValueInput
|
||||||
|
:model-value="getStructFieldValue(field.identifier)"
|
||||||
|
:data-specs="field"
|
||||||
|
@update:model-value="(val) => updateStructField(field.identifier, val)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 数组类型 -->
|
||||||
|
<template v-else-if="dataType === 'array'">
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div v-for="(item, index) in arrayValue" :key="index" class="flex items-center mb-10px">
|
||||||
|
<PropertyValueInput
|
||||||
|
:model-value="item"
|
||||||
|
:data-specs="dataSpecs?.arrayInfo"
|
||||||
|
@update:model-value="(val) => updateArrayItem(index, val)"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
circle
|
||||||
|
class="ml-10px"
|
||||||
|
@click="removeArrayItem(index)"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:delete" />
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" size="small" plain @click="addArrayItem">添加项</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 默认文本输入 -->
|
||||||
|
<template v-else>
|
||||||
|
<el-input
|
||||||
|
v-model="inputValue"
|
||||||
|
class="!w-240px"
|
||||||
|
:placeholder="`请输入${dataSpecs?.name || '值'}`"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core'
|
||||||
|
|
||||||
|
/** 属性值输入组件 */
|
||||||
|
defineOptions({ name: 'PropertyValueInput' })
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: any
|
||||||
|
dataSpecs: any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emits = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
// 获取数据类型
|
||||||
|
const dataType = computed(() => {
|
||||||
|
return props.dataSpecs?.dataType
|
||||||
|
})
|
||||||
|
|
||||||
|
// 基本类型的输入值
|
||||||
|
const inputValue = computed({
|
||||||
|
get: () => {
|
||||||
|
// 对于日期类型,如果是字符串需要转换为日期对象
|
||||||
|
if (dataType.value === 'date' && typeof props.modelValue === 'string') {
|
||||||
|
return new Date(props.modelValue)
|
||||||
|
}
|
||||||
|
return props.modelValue
|
||||||
|
},
|
||||||
|
set: (value) => {
|
||||||
|
// 对于日期类型,需要转换为ISO字符串
|
||||||
|
if (dataType.value === 'date' && value instanceof Date) {
|
||||||
|
emits('update:modelValue', value.toISOString())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emits('update:modelValue', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 结构体类型处理
|
||||||
|
const structValue = computed({
|
||||||
|
get: () => {
|
||||||
|
return props.modelValue || {}
|
||||||
|
},
|
||||||
|
set: (value) => {
|
||||||
|
emits('update:modelValue', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取结构体字段值
|
||||||
|
const getStructFieldValue = (identifier: string) => {
|
||||||
|
return structValue.value[identifier]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新结构体字段
|
||||||
|
const updateStructField = (identifier: string, value: any) => {
|
||||||
|
const newStruct = { ...structValue.value }
|
||||||
|
newStruct[identifier] = value
|
||||||
|
structValue.value = newStruct
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数组类型处理
|
||||||
|
const arrayValue = computed({
|
||||||
|
get: () => {
|
||||||
|
return Array.isArray(props.modelValue) ? props.modelValue : []
|
||||||
|
},
|
||||||
|
set: (value) => {
|
||||||
|
emits('update:modelValue', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加数组项
|
||||||
|
const addArrayItem = () => {
|
||||||
|
const newArray = [...arrayValue.value]
|
||||||
|
// 根据数组元素类型创建默认值
|
||||||
|
const arrayItemType = props.dataSpecs?.arrayInfo?.dataType
|
||||||
|
let defaultValue = null
|
||||||
|
switch (arrayItemType) {
|
||||||
|
case 'int':
|
||||||
|
case 'float':
|
||||||
|
case 'double':
|
||||||
|
defaultValue = 0
|
||||||
|
break
|
||||||
|
case 'bool':
|
||||||
|
defaultValue = false
|
||||||
|
break
|
||||||
|
case 'text':
|
||||||
|
defaultValue = ''
|
||||||
|
break
|
||||||
|
case 'struct':
|
||||||
|
defaultValue = {}
|
||||||
|
break
|
||||||
|
case 'array':
|
||||||
|
defaultValue = []
|
||||||
|
break
|
||||||
|
}
|
||||||
|
newArray.push(defaultValue)
|
||||||
|
arrayValue.value = newArray
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新数组项
|
||||||
|
const updateArrayItem = (index: number, value: any) => {
|
||||||
|
const newArray = [...arrayValue.value]
|
||||||
|
newArray[index] = value
|
||||||
|
arrayValue.value = newArray
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除数组项
|
||||||
|
const removeArrayItem = (index: number) => {
|
||||||
|
const newArray = [...arrayValue.value]
|
||||||
|
newArray.splice(index, 1)
|
||||||
|
arrayValue.value = newArray
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
Loading…
Reference in New Issue
Block a user