|
|
@@ -0,0 +1,251 @@
|
|
|
+<template>
|
|
|
+ <div class="w-1/1">
|
|
|
+ <el-button class="w-1/1" @click="openDrawer">点击配置</el-button>
|
|
|
+
|
|
|
+ <el-drawer v-model="drawerVisible" title="配置设备绑定" direction="ltr" size="520px">
|
|
|
+ <el-empty v-if="!devices.length" description="当前组态项目未绑定设备" />
|
|
|
+
|
|
|
+ <template v-else>
|
|
|
+ <el-form ref="formRef" :model="formModel" label-position="top" size="small">
|
|
|
+ <div
|
|
|
+ v-for="(bindItem, index) in formModel.deviceBinds"
|
|
|
+ :key="bindItem.id"
|
|
|
+ class="mb-12px">
|
|
|
+ <el-form-item
|
|
|
+ label="选择设备"
|
|
|
+ :prop="`deviceBinds.${index}.deviceId`"
|
|
|
+ :rules="rules.deviceId">
|
|
|
+ <el-select
|
|
|
+ v-model="bindItem.deviceId"
|
|
|
+ filterable
|
|
|
+ clearable
|
|
|
+ class="w-1/1"
|
|
|
+ placeholder="请选择设备"
|
|
|
+ :disabled="!bindItem.editing"
|
|
|
+ @change="onDeviceChange(bindItem)">
|
|
|
+ <el-option
|
|
|
+ v-for="device in devices"
|
|
|
+ :key="device.id"
|
|
|
+ :label="formatDeviceLabel(device)"
|
|
|
+ :value="device.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item
|
|
|
+ label="设备属性"
|
|
|
+ :prop="`deviceBinds.${index}.deviceProp`"
|
|
|
+ :rules="rules.deviceProp">
|
|
|
+ <el-select
|
|
|
+ v-model="bindItem.deviceProp"
|
|
|
+ filterable
|
|
|
+ clearable
|
|
|
+ class="w-1/1"
|
|
|
+ placeholder="请选择设备属性"
|
|
|
+ :disabled="!bindItem.editing || !bindItem.deviceId"
|
|
|
+ :loading="isDevicePropsLoading(bindItem.deviceId)">
|
|
|
+ <el-option
|
|
|
+ v-for="deviceProp in getDeviceProps(bindItem.deviceId)"
|
|
|
+ :key="deviceProp.value"
|
|
|
+ :label="deviceProp.label"
|
|
|
+ :value="deviceProp.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item
|
|
|
+ label="图形属性"
|
|
|
+ :prop="`deviceBinds.${index}.nodeProp`"
|
|
|
+ :rules="rules.nodeProp">
|
|
|
+ <el-select
|
|
|
+ v-model="bindItem.nodeProp"
|
|
|
+ filterable
|
|
|
+ clearable
|
|
|
+ class="w-1/1"
|
|
|
+ placeholder="请选择图形属性"
|
|
|
+ :disabled="!bindItem.editing">
|
|
|
+ <el-option
|
|
|
+ v-for="nodeProp in nodePropOptions"
|
|
|
+ :key="nodeProp.value"
|
|
|
+ :label="nodeProp.label"
|
|
|
+ :value="nodeProp.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-button
|
|
|
+ v-if="bindItem.editing"
|
|
|
+ type="success"
|
|
|
+ size="small"
|
|
|
+ class="w-1/1"
|
|
|
+ @click="saveBind(index)">
|
|
|
+ 绑定
|
|
|
+ </el-button>
|
|
|
+ <el-button v-else type="danger" size="small" class="w-1/1" @click="removeBind(index)">
|
|
|
+ 解绑
|
|
|
+ </el-button>
|
|
|
+
|
|
|
+ <el-divider />
|
|
|
+ </div>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <el-button type="primary" size="small" class="w-1/1" @click="addBind">新增</el-button>
|
|
|
+ </template>
|
|
|
+ </el-drawer>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { IotDeviceApi } from '@/api/pms/device'
|
|
|
+import type { WebtopoProjectLinkedDeviceVO } from '@/api/pms/maotu'
|
|
|
+import type { IDoneJson, IDoneJsonDeviceBind } from '@/components/mt-edit/store/types'
|
|
|
+import { ElMessage, type FormInstance, type FormItemRule } from 'element-plus'
|
|
|
+import { computed, reactive, ref } from 'vue'
|
|
|
+
|
|
|
+type DeviceBindRow = IDoneJsonDeviceBind & {
|
|
|
+ editing?: boolean
|
|
|
+}
|
|
|
+
|
|
|
+type DevicePropOption = {
|
|
|
+ label: string
|
|
|
+ value: string
|
|
|
+}
|
|
|
+
|
|
|
+type DevicePropResponseItem = {
|
|
|
+ modelName?: string
|
|
|
+ identifier?: string
|
|
|
+}
|
|
|
+
|
|
|
+type Props = {
|
|
|
+ item: IDoneJson
|
|
|
+ devices?: WebtopoProjectLinkedDeviceVO[]
|
|
|
+}
|
|
|
+
|
|
|
+const props = withDefaults(defineProps<Props>(), {
|
|
|
+ devices: () => []
|
|
|
+})
|
|
|
+
|
|
|
+const drawerVisible = ref(false)
|
|
|
+const formRef = ref<FormInstance>()
|
|
|
+const formModel = reactive({
|
|
|
+ deviceBinds: [] as DeviceBindRow[]
|
|
|
+})
|
|
|
+const devicePropsMap = ref<Record<number, DevicePropOption[]>>({})
|
|
|
+const devicePropsLoading = ref<number[]>([])
|
|
|
+
|
|
|
+const rules: Record<string, FormItemRule | FormItemRule[]> = {
|
|
|
+ deviceId: [{ required: true, message: '请选择设备', trigger: 'change' }],
|
|
|
+ deviceProp: [{ required: true, message: '请选择设备属性', trigger: 'change' }],
|
|
|
+ nodeProp: [{ required: true, message: '请选择图形属性', trigger: 'change' }]
|
|
|
+}
|
|
|
+
|
|
|
+const nodePropOptions = computed(() => {
|
|
|
+ return Object.entries(props.item.props || {})
|
|
|
+ .filter(([, prop]) => !prop.disabled)
|
|
|
+ .map(([key, prop]) => ({
|
|
|
+ label: prop.title,
|
|
|
+ value: `props.${key}.val`
|
|
|
+ }))
|
|
|
+})
|
|
|
+
|
|
|
+function randomId() {
|
|
|
+ return `device-bind-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
|
+}
|
|
|
+
|
|
|
+function getChineseName(value?: string) {
|
|
|
+ return (value || '').split('~~')[0]
|
|
|
+}
|
|
|
+
|
|
|
+function formatDeviceLabel(device: WebtopoProjectLinkedDeviceVO) {
|
|
|
+ return [device.deviceCode, getChineseName(device.deviceName)].filter(Boolean).join(' - ')
|
|
|
+}
|
|
|
+
|
|
|
+function updateItemDeviceBinds(item: IDoneJson, deviceBinds: IDoneJsonDeviceBind[]) {
|
|
|
+ item.deviceBinds = deviceBinds
|
|
|
+}
|
|
|
+
|
|
|
+function syncDeviceBinds() {
|
|
|
+ updateItemDeviceBinds(
|
|
|
+ props.item,
|
|
|
+ formModel.deviceBinds.map(({ editing, ...item }) => item)
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+async function openDrawer() {
|
|
|
+ formModel.deviceBinds = (props.item.deviceBinds || []).map((item) => ({
|
|
|
+ ...item,
|
|
|
+ editing: false
|
|
|
+ }))
|
|
|
+ drawerVisible.value = true
|
|
|
+
|
|
|
+ await Promise.all(
|
|
|
+ formModel.deviceBinds
|
|
|
+ .filter((item) => item.deviceId)
|
|
|
+ .map((item) => loadDeviceProps(item.deviceId!))
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function addBind() {
|
|
|
+ if (formModel.deviceBinds.some((item) => item.editing)) {
|
|
|
+ ElMessage.error('请先完成当前绑定')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ formModel.deviceBinds.push({
|
|
|
+ id: randomId(),
|
|
|
+ deviceId: undefined,
|
|
|
+ deviceProp: '',
|
|
|
+ nodeProp: '',
|
|
|
+ editing: true
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+async function onDeviceChange(bindItem: DeviceBindRow) {
|
|
|
+ bindItem.deviceProp = ''
|
|
|
+ if (bindItem.deviceId) {
|
|
|
+ await loadDeviceProps(bindItem.deviceId)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function loadDeviceProps(deviceId: number) {
|
|
|
+ if (devicePropsMap.value[deviceId] || devicePropsLoading.value.includes(deviceId)) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ devicePropsLoading.value.push(deviceId)
|
|
|
+ try {
|
|
|
+ const data = ((await IotDeviceApi.getIotDeviceTds(deviceId)) || []) as DevicePropResponseItem[]
|
|
|
+ devicePropsMap.value[deviceId] = data
|
|
|
+ .filter((item) => item.identifier)
|
|
|
+ .map((item) => ({
|
|
|
+ label: item.modelName || item.identifier!,
|
|
|
+ value: item.identifier!
|
|
|
+ }))
|
|
|
+ } finally {
|
|
|
+ devicePropsLoading.value = devicePropsLoading.value.filter((id) => id !== deviceId)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function getDeviceProps(deviceId?: number) {
|
|
|
+ return deviceId ? devicePropsMap.value[deviceId] || [] : []
|
|
|
+}
|
|
|
+
|
|
|
+function isDevicePropsLoading(deviceId?: number) {
|
|
|
+ return !!deviceId && devicePropsLoading.value.includes(deviceId)
|
|
|
+}
|
|
|
+
|
|
|
+async function saveBind(index: number) {
|
|
|
+ await formRef.value?.validateField([
|
|
|
+ `deviceBinds.${index}.deviceId`,
|
|
|
+ `deviceBinds.${index}.deviceProp`,
|
|
|
+ `deviceBinds.${index}.nodeProp`
|
|
|
+ ])
|
|
|
+
|
|
|
+ formModel.deviceBinds[index].editing = false
|
|
|
+ syncDeviceBinds()
|
|
|
+ ElMessage.success('绑定成功')
|
|
|
+}
|
|
|
+
|
|
|
+function removeBind(index: number) {
|
|
|
+ formModel.deviceBinds.splice(index, 1)
|
|
|
+ syncDeviceBinds()
|
|
|
+ ElMessage.success('解绑成功')
|
|
|
+}
|
|
|
+</script>
|