Zimo 1 неделя назад
Родитель
Сommit
0b0b84f1e7

+ 8 - 1
build/vite/index.ts

@@ -74,7 +74,14 @@ export function createVitePlugins() {
       dts: 'src/types/auto-components.d.ts',
       // 自定义组件的解析器
       resolvers: [ElementPlusResolver()],
-      globs: ['src/components/**/**.{vue, md}', '!src/components/DiyEditor/components/mobile/**']
+      globs: [
+        'src/components/**/**.{vue, md}',
+        '!src/components/DiyEditor/components/mobile/**',
+        '!src/components/mt-dzr/**',
+        '!src/components/mt-preview/**',
+        '!src/components/mt-edit/**',
+        '!src/components/custom-components/**'
+      ]
     }),
     EslintPlugin({
       cache: false,

+ 11 - 2
src/api/pms/maotu/index.ts

@@ -6,11 +6,20 @@ export type WebtopoProjectVO = {
   deptId?: number
   thumbnail?: string
   createTime?: number | string
-  linkedDevices?: number[]
+  linkedDevices?: WebtopoProjectLinkedDeviceVO[]
+  linkedDeviceIds?: number[]
   remark?: string
   updateTime?: string
 }
 
+export type WebtopoProjectLinkedDeviceVO = {
+  id: number
+  deviceCode?: string
+  deviceName?: string
+  deviceStatus?: string | null
+  pointParams?: unknown[]
+}
+
 export type WebtopoProjectDetailVO = WebtopoProjectVO & {
   dataModel?: unknown
 }
@@ -24,7 +33,7 @@ export type WebtopoProjectPageReqVO = {
 
 export type WebtopoProjectCreateReqVO = {
   projectName: string
-  linkedDevices?: number[]
+  linkedDeviceIds?: number[]
   remark?: string
 }
 

+ 7 - 0
src/components/mt-edit/store/types.ts

@@ -108,6 +108,12 @@ export interface IDoneJsonEventList {
     value?: any //期望值
   }
 }
+export interface IDoneJsonDeviceBind {
+  id: string
+  deviceId?: number
+  deviceProp?: string
+  nodeProp?: string
+}
 export interface IDoneJson {
   id: string //必须唯一
   title: string //标题
@@ -126,6 +132,7 @@ export interface IDoneJson {
   tag?: string
   thumbnail?: string
   events: IDoneJsonEventList[]
+  deviceBinds?: IDoneJsonDeviceBind[]
 }
 //图形边界信息
 export interface IDoneJsonBinfo {

+ 251 - 0
src/views/maotu/components/DeviceBindPanel.vue

@@ -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>

+ 11 - 2
src/views/maotu/edit.vue

@@ -12,6 +12,7 @@ import html2canvas from 'html2canvas'
 import { ElMessage } from 'element-plus'
 import { nextTick, onMounted, ref } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
+import DeviceBindPanel from './components/DeviceBindPanel.vue'
 const route = useRoute()
 const router = useRouter()
 const MtEditRef = ref<InstanceType<typeof MtEdit>>()
@@ -23,6 +24,10 @@ const getProjectId = () => {
   return Number.isFinite(id) && id > 0 ? id : undefined
 }
 
+const getLinkedDeviceIds = (detail: WebtopoProjectDetailVO) => {
+  return detail.linkedDeviceIds || detail.linkedDevices?.map((item) => item.id) || []
+}
+
 const loadProject = async () => {
   const id = getProjectId()
   if (!id) {
@@ -111,7 +116,7 @@ const onSaveClick = async (dataModel: IExportJson) => {
     const data: WebtopoProjectUpdateReqVO = {
       id: detail.id,
       projectName: detail.projectName,
-      linkedDevices: detail.linkedDevices || [],
+      linkedDeviceIds: getLinkedDeviceIds(detail),
       remark: detail.remark,
       thumbnail,
       dataModel: JSON.stringify(dataModel)
@@ -148,7 +153,11 @@ onMounted(() => {
       @on-preview-click="onPreviewClick"
       @on-return-click="onReturnClick"
       @on-save-click="onSaveClick"
-      @on-thumbnail-click="onThumbnailClick" />
+      @on-thumbnail-click="onThumbnailClick">
+      <template #deviceBind="{ item }">
+        <DeviceBindPanel :item="item" :devices="projectDetail?.linkedDevices || []" />
+      </template>
+    </mt-edit>
   </div>
 </template>
 

+ 13 - 8
src/views/maotu/index.vue

@@ -48,7 +48,7 @@ const formMode = ref<FormMode>('create')
 const deviceOptions = ref<DeviceOption[]>([])
 const createForm = ref<ProjectForm>({
   projectName: '',
-  linkedDevices: [],
+  linkedDeviceIds: [],
   remark: ''
 })
 
@@ -83,6 +83,10 @@ function formatDeviceLabel(item: any) {
   return [code, name].filter(Boolean).join(' - ')
 }
 
+function getLinkedDeviceIds(row: WebtopoProjectVO) {
+  return row.linkedDeviceIds || row.linkedDevices?.map((item) => item.id) || []
+}
+
 async function loadDeviceOptions(deptId?: number) {
   if (!deptId) {
     deviceOptions.value = []
@@ -128,7 +132,7 @@ async function openCreate() {
   createForm.value = {
     id: undefined,
     projectName: '',
-    linkedDevices: [],
+    linkedDeviceIds: [],
     remark: ''
   }
   deviceOptions.value = []
@@ -143,7 +147,7 @@ async function openConfig(row: WebtopoProjectVO) {
   createForm.value = {
     id: row.id,
     projectName: row.projectName,
-    linkedDevices: row.linkedDevices || [],
+    linkedDeviceIds: getLinkedDeviceIds(row),
     remark: row.remark
   }
   createVisible.value = true
@@ -164,7 +168,7 @@ async function submitCreate() {
     await formRef.value.validate()
     const data: WebtopoProjectCreateReqVO = {
       projectName: createForm.value.projectName!,
-      linkedDevices: createForm.value.linkedDevices || [],
+      linkedDeviceIds: createForm.value.linkedDeviceIds || [],
       remark: createForm.value.remark
     }
     const res = await WebtopoProjectApi.createWebtopoProject(data)
@@ -215,7 +219,7 @@ function formatCreateTime(value?: number | string) {
 }
 
 function getLinkedDeviceCount(row: WebtopoProjectVO) {
-  return row.linkedDevices?.length || 0
+  return getLinkedDeviceIds(row).length
 }
 
 function handleEdit(row: WebtopoProjectVO) {
@@ -251,6 +255,7 @@ watch(
 
     <el-form
       size="default"
+      @submit.prevent="handleQuery()"
       class="filter-form bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 gap-8 flex items-center justify-between">
       <div class="flex items-center gap-8">
         <el-form-item label="组态名称">
@@ -259,7 +264,7 @@ watch(
             clearable
             class="!w-240px"
             placeholder="请输入组态名称"
-            @keyup.enter="handleQuery()" />
+            @keydown.enter.prevent="handleQuery()" />
         </el-form-item>
       </div>
       <el-form-item>
@@ -406,9 +411,9 @@ watch(
         <el-input v-model="createForm.projectName" clearable placeholder="请输入项目名称" />
       </el-form-item>
 
-      <el-form-item label="绑定设备" prop="linkedDevices">
+      <el-form-item label="绑定设备" prop="linkedDeviceIds">
         <el-select
-          v-model="createForm.linkedDevices"
+          v-model="createForm.linkedDeviceIds"
           multiple
           filterable
           clearable