Browse Source

feat: 自定义地图点完绑定

gitboyzcf 2 weeks ago
parent
commit
64b7070f99

+ 17 - 0
src/api/modules/mr.js

@@ -12,6 +12,15 @@ export default {
       data
     })
   },
+  // 模拟IV设备查询接口
+  API_DEVICES_GET(params = {}) {
+    return request({
+      baseURL: noMr,
+      url: '/api/devices',
+      method: 'get',
+      params
+    })
+  },
   // 获取相机
   API_MR_CAMERA_GET(data = {}) {
     return request({
@@ -20,6 +29,14 @@ export default {
       data
     })
   },
+  // 添加相机
+  API_MR_CAMERA_POST(data = {}) {
+    return request({
+      url: `/api/Camera`,
+      method: 'post',
+      data
+    })
+  },
   // 获取视频播放地址列表
   API_MR_VIDEO_LIST_GET(data = {}) {
     return request({

+ 103 - 0
src/layout/Three3D/components/addDevice.vue

@@ -0,0 +1,103 @@
+<template>
+  <n-data-table :columns="columns" :data="data" :pagination="pagination" bordered />
+</template>
+
+<script setup>
+  import { NDataTable, NButton, NInput, NSelect } from 'naive-ui'
+  const { API_MR_CAMERA_POST, API_DEVICES_GET } = useRequest()
+  const props = defineProps({
+    intersection: Object,
+    addSprite: Function
+  })
+  const emits = defineEmits(['closeAddTagModal'])
+
+  function createColumns({ handleSubmit }) {
+    return [
+      {
+        title: '设备序列号',
+        key: 'sn'
+      },
+      {
+        title: '设备名称',
+        key: 'name'
+      },
+      {
+        title: '设备状态',
+        key: 'status',
+        render(row) {
+          return h(
+            'span',
+            { class: row.status === 1 ? 'color-green' : 'color-red' },
+            row.status === 1 ? '在线' : '离线'
+          )
+        }
+      },
+      {
+        title: '设备版本',
+        key: 'version'
+      },
+      {
+        title: '设备IP',
+        key: 'ip_address'
+      },
+      {
+        title: '设备端口',
+        key: 'port'
+      },
+      {
+        title: '操作',
+        key: 'actions',
+        render(row) {
+          return h(
+            NButton,
+            {
+              strong: true,
+              tertiary: true,
+              size: 'small',
+              onClick: () => handleSubmit(row)
+            },
+            { default: () => '绑定' }
+          )
+        }
+      }
+    ]
+  }
+  const data = ref([])
+  const columns = createColumns({
+    handleSubmit(row) {
+      const params = {
+        X: props.intersection.x, //必传
+        Y: props.intersection.y, //必传
+        Z: props.intersection.z, //必传
+        Type: 'FallView', //必传
+        RtspMain: row.sn
+      }
+      API_MR_CAMERA_POST(params).then(() => {
+        props.addSprite([{ x: params.X, y: params.Y, z: params.Z, RtspMain: params.RtspMain }])
+        emits('closeAddTagModal')
+      })
+    }
+  })
+  const pagination = reactive({
+    page: 1,
+    pageSize: 10,
+    showSizePicker: true,
+    pageSizes: [10, 50, 100],
+    onChange: (page) => {
+      pagination.page = page
+    },
+    onUpdatePageSize: (pageSize) => {
+      pagination.pageSize = pageSize
+      pagination.page = 1
+    }
+  })
+
+  const init = async () => {
+    const res = await API_DEVICES_GET()
+    data.value = res?.items || []
+    console.log(res)
+  }
+  onMounted(() => {
+    init()
+  })
+</script>

+ 56 - 0
src/layout/Three3D/components/rightClick.vue

@@ -0,0 +1,56 @@
+<template>
+  <ul
+    v-if="show"
+    :style="{ top: y + 'px', left: x + 'px' }"
+    class="three-create-tag absolute c-#fff right-box py-1 flex flex-col items-center justify-center bg-#2262acb3"
+  >
+    <li
+      class="px-4 py-1 cursor-pointer hover:bg-#2262ac mb-2 last:mb-0"
+      @click="item.handler"
+      v-for="(item, i) in menu"
+      :key="i"
+      >{{ item.label }}</li
+    >
+  </ul>
+</template>
+<script setup>
+  import { useModal } from 'naive-ui'
+  import AddDevice from './addDevice.vue'
+
+  const addSprite = inject('addSprite')
+  const show = defineModel()
+  const modal = useModal()
+  const props = defineProps({
+    x: {
+      type: Number,
+      default: 0
+    },
+    y: {
+      type: Number,
+      default: 0
+    },
+    intersection: Object
+  })
+  const menu = [
+    {
+      label: '创建点位',
+      handler: () => {
+        show.value = false
+        modal.create({
+          title: '新建点位',
+          preset: 'card',
+          maskClosable: false,
+          style: {
+            marginTop: '10%'
+          },
+          content: () =>
+            h(AddDevice, {
+              intersection: props.intersection,
+              addSprite,
+              onCloseAddTagModal: () => modal.destroyAll()
+            })
+        })
+      }
+    }
+  ]
+</script>

+ 61 - 4
src/layout/Three3D/index.vue

@@ -9,6 +9,12 @@
       :percentage="percentage"
       :fill-border-radius="0"
     />
+    <RightClick
+      v-model="rightClickD.show"
+      :x="rightClickD.x"
+      :y="rightClickD.y"
+      :intersection="rightClickD.intersection"
+    />
   </div>
 </template>
 
@@ -21,12 +27,20 @@
   import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
   import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
   import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'
-  import { onMounted, onUnmounted } from 'vue'
+  import { onMounted, onUnmounted, provide, ref } from 'vue'
   import { useOutsideHomeStore } from '@/stores/modules/home'
   import { useOutsideSystemStore } from '@/stores/modules/system'
+  import { getCorrectMousePosition } from '@/utils'
   import storage from '@/utils/storage'
+  import RightClick from './components/rightClick.vue'
 
   const emits = defineEmits(['onGetData'])
+  const rightClickD = reactive({
+    show: false,
+    x: 0,
+    y: 0,
+    intersection: null
+  })
 
   const { API_MR_CAMERA_GET, API_MR_VIDEO_LIST_GET } = useRequest()
   const useHomeStore = useOutsideHomeStore()
@@ -79,15 +93,18 @@
       }
       sprite.position.set(x, y, z)
       sprite.scale.set(0.4, 0.45, 0.5)
+      // 设置精灵的锚点为底部中央
+      sprite.center.set(0.5, 0)
       scene.add(sprite)
       sprites.push(sprite)
     })
   }
 
-  const addEvent = (dom = window) => {
-    const raycaster = new THREE.Raycaster()
-    const pointer = new THREE.Vector2()
+  provide('addSprite', addSprite)
 
+  const raycaster = new THREE.Raycaster()
+  const pointer = new THREE.Vector2()
+  const addEvent = (dom = window) => {
     dom.addEventListener('click', (event) => {
       if (useHomeStore.temp === 'video') return
       pointer.x = (event.clientX / window.innerWidth) * 2 - 1
@@ -108,7 +125,46 @@
         })
       }
     })
+
+    dom.addEventListener('contextmenu', onRightClick)
+  }
+  function onRightClick(event) {
+    event.preventDefault()
+
+    // 隐藏之前的菜单
+    // document.getElementById('context-menu').style.display = 'none'
+
+    // 计算鼠标位置
+    pointer.x = (event.clientX / window.innerWidth) * 2 - 1
+    pointer.y = -(event.clientY / window.innerHeight) * 2 + 1
+
+    // 更新射线投射
+    raycaster.setFromCamera(pointer, camera)
+
+    // 检测与模型的交点
+    const intersects = raycaster.intersectObjects(scene.children, true)
+
+    if (intersects.length > 0) {
+      if (intersects[0].object.type === 'Sprite') return
+      // 显示右键菜单
+      const scale = document.body.style.transform.split('(')[2].split(')')[0]
+
+      rightClickD.show = true
+      rightClickD.x = event.clientX / scale
+      rightClickD.y = event.clientY / scale
+
+      // 存储交点信息
+      rightClickD.intersection = intersects[0].point
+    } else {
+      // 如果没有点击到物体,隐藏菜单
+      rightClickD.show = false
+    }
+  }
+
+  const destroyedMenu = () => {
+    rightClickD.show = false
   }
+  document.addEventListener('click', destroyedMenu)
 
   // 创建场景
   const scene = new THREE.Scene()
@@ -253,6 +309,7 @@
 
   onUnmounted(() => {
     window.removeEventListener('resize', resize)
+    document.removeEventListener('click', destroyedMenu)
     cleanupThreeJS()
   })
 </script>

+ 0 - 2
src/stores/modules/system.js

@@ -88,8 +88,6 @@ export const useSystemStore = defineStore('systemStore', {
     },
     setRatio() {
       const pD = document.querySelector('.pub-video')
-      // const scale = document.body.style.transform.split('(')[1].split(')')[0]
-      // const [x, y] = [pD.offset * parseFloat(scale), event.offsetY * parseFloat(scale)]
       const { width, height } = pD.getBoundingClientRect()
 
       this.ratio = { wR: width / pD.videoWidth, hR: height / pD.videoHeight }

+ 2 - 2
src/stores/modules/tag.js

@@ -60,8 +60,8 @@ export const useTagStore = defineStore('tag', {
       const useSystemStore = useOutsideSystemStore()
       const showTag = this.getTrueTag().filter((item) => item.isShow)
       const scale = document.body.style.transform.split('(')[2].split(')')[0]
-      console.log(scale);
-      console.log(useSystemStore.ratio);
+      console.log(scale)
+      console.log(useSystemStore.ratio)
 
       const tagList = showTag.map((item) => ({
         ...item,

+ 20 - 1
src/utils/index.js

@@ -85,4 +85,23 @@ function getContainerTransform(container) {
   return { scaleX: matrix.a, scaleY: matrix.d }
 }
 
-export { getStaticResource, msg, modulesHandle, $mitt, uuid, getContainerTransform, dialogFn }
+// 然后在 getCorrectMousePosition 中使用:
+function getCorrectMousePosition(event, container) {
+  const containerRect = container.getBoundingClientRect()
+  const transform = getContainerTransform(container)
+
+  return {
+    x: (event.clientX - containerRect.left) * transform.scaleX,
+    y: (event.clientY - containerRect.top) * transform.scaleY
+  }
+}
+export {
+  getStaticResource,
+  msg,
+  modulesHandle,
+  $mitt,
+  uuid,
+  getContainerTransform,
+  getCorrectMousePosition,
+  dialogFn
+}

+ 0 - 0
src/views/VideoBox/components/rightBox.vue → src/views/VideoBox/components/rightClick.vue


+ 1 - 1
src/views/VideoBox/index.vue

@@ -88,7 +88,7 @@
 
   import { UseFullscreen } from '@vueuse/components'
   import { $mitt, uuid } from '@/utils'
-  import RightClick from './components/rightBox.vue'
+  import RightClick from './components/rightClick.vue'
   import TopTools from './components/topTools.vue'
   import VuePhotoZoomPro from 'vue-photo-zoom-pro'
   import 'vue-photo-zoom-pro/dist/style/vue-photo-zoom-pro.css'