Bladeren bron

master: Fixed 接口对接完善 有问题

gitboyzcf 2 weken geleden
bovenliggende
commit
9b999d456a
13 gewijzigde bestanden met toevoegingen van 543 en 126 verwijderingen
  1. 13 0
      index.html
  2. 3 1
      package.json
  3. 28 0
      pnpm-lock.yaml
  4. 2 0
      pnpm-workspace.yaml
  5. 29 0
      public/js/opencv.js
  6. 10 1
      src/components/AddBall.vue
  7. 261 66
      src/components/BallCameraSettings.vue
  8. 11 14
      src/layouts/home.vue
  9. 29 29
      src/pages/index.vue
  10. 1 0
      src/shims.d.ts
  11. 149 14
      src/stores/system.ts
  12. 1 1
      src/styles/main.css
  13. 6 0
      vite.config.ts

+ 13 - 0
index.html

@@ -14,6 +14,19 @@
           document.documentElement.classList.toggle('dark', true)
       })()
     </script>
+    <script>
+      var cv = null;
+      function onOpenCvReady() {
+        cv = window.cv;
+        console.log("opencv load succeed.");
+      }
+    </script>
+    <script
+      async
+      src="/js/opencv.js"
+      onload="onOpenCvReady();"
+      type="text/javascript"
+    ></script>
   </head>
   <body class="font-sans">
     <div id="app"></div>

+ 3 - 1
package.json

@@ -29,7 +29,8 @@
     "vue": "catalog:frontend",
     "vue-i18n": "catalog:frontend",
     "vue-router": "catalog:frontend",
-    "vuetify": "catalog:frontend"
+    "vuetify": "catalog:frontend",
+    "uuid": "catalog:frontend"
   },
   "devDependencies": {
     "@antfu/eslint-config": "catalog:dev",
@@ -40,6 +41,7 @@
     "@types/markdown-it-link-attributes": "catalog:types",
     "@types/nprogress": "catalog:types",
     "@unocss/eslint-config": "catalog:build",
+    "@vitejs/plugin-basic-ssl": "catalog:dev",
     "@vitejs/plugin-vue": "catalog:build",
     "@vue-macros/volar": "catalog:dev",
     "@vue/test-utils": "catalog:dev",

+ 28 - 0
pnpm-lock.yaml

@@ -76,6 +76,9 @@ catalogs:
     '@mdi/font':
       specifier: ^7.4.47
       version: 7.4.47
+    '@vitejs/plugin-basic-ssl':
+      specifier: ^2.0.0
+      version: 2.0.0
     '@vue-macros/volar':
       specifier: ^3.0.0-beta.7
       version: 3.0.0-beta.7
@@ -137,6 +140,9 @@ catalogs:
     pinia:
       specifier: ^3.0.1
       version: 3.0.1
+    uuid:
+      specifier: ^11.1.0
+      version: 11.1.0
     vee-validate:
       specifier: ^4.0.0
       version: 4.15.0
@@ -193,6 +199,9 @@ importers:
       pinia:
         specifier: catalog:frontend
         version: 3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
+      uuid:
+        specifier: catalog:frontend
+        version: 11.1.0
       vee-validate:
         specifier: catalog:frontend
         version: 4.15.0(vue@3.5.13(typescript@5.8.2))
@@ -236,6 +245,9 @@ importers:
       '@unocss/eslint-config':
         specifier: catalog:build
         version: 66.1.0-beta.7(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)
+      '@vitejs/plugin-basic-ssl':
+        specifier: catalog:dev
+        version: 2.0.0(vite@6.2.3(@types/node@20.2.3)(jiti@2.4.2)(terser@5.17.6)(tsx@4.19.2)(yaml@2.7.0))
       '@vitejs/plugin-vue':
         specifier: catalog:build
         version: 5.2.3(vite@6.2.3(@types/node@20.2.3)(jiti@2.4.2)(terser@5.17.6)(tsx@4.19.2)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
@@ -2158,6 +2170,12 @@ packages:
     cpu: [x64]
     os: [win32]
 
+  '@vitejs/plugin-basic-ssl@2.0.0':
+    resolution: {integrity: sha512-gc9Tjg8bUxBVSTzeWT3Njc0Cl3PakHFKdNfABnZWiUgbxqmHDEn7uECv3fHVylxoYgNzAcmU7ZrILz+BwSo3sA==}
+    engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+    peerDependencies:
+      vite: ^6.2.3
+
   '@vitejs/plugin-vue@5.2.3':
     resolution: {integrity: sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==}
     engines: {node: ^18.0.0 || >=20.0.0}
@@ -5766,6 +5784,10 @@ packages:
     resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
     engines: {node: '>= 0.4.0'}
 
+  uuid@11.1.0:
+    resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
+    hasBin: true
+
   uuid@8.3.2:
     resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
     hasBin: true
@@ -8121,6 +8143,10 @@ snapshots:
   '@unrs/resolver-binding-win32-x64-msvc@1.3.2':
     optional: true
 
+  '@vitejs/plugin-basic-ssl@2.0.0(vite@6.2.3(@types/node@20.2.3)(jiti@2.4.2)(terser@5.17.6)(tsx@4.19.2)(yaml@2.7.0))':
+    dependencies:
+      vite: 6.2.3(@types/node@20.2.3)(jiti@2.4.2)(terser@5.17.6)(tsx@4.19.2)(yaml@2.7.0)
+
   '@vitejs/plugin-vue@5.2.3(vite@6.2.3(@types/node@20.2.3)(jiti@2.4.2)(terser@5.17.6)(tsx@4.19.2)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))':
     dependencies:
       vite: 6.2.3(@types/node@20.2.3)(jiti@2.4.2)(terser@5.17.6)(tsx@4.19.2)(yaml@2.7.0)
@@ -12461,6 +12487,8 @@ snapshots:
 
   utils-merge@1.0.1: {}
 
+  uuid@11.1.0: {}
+
   uuid@8.3.2: {}
 
   validate-npm-package-license@3.0.4:

+ 2 - 0
pnpm-workspace.yaml

@@ -31,6 +31,7 @@ catalogs:
     '@antfu/eslint-config': ^4.11.0
     '@iconify-json/carbon': ^1.2.8
     '@mdi/font': ^7.4.47
+    '@vitejs/plugin-basic-ssl': ^2.0.0
     '@vue-macros/volar': ^3.0.0-beta.7
     '@vue/test-utils': ^2.4.6
     cypress: ^14.2.1
@@ -59,6 +60,7 @@ catalogs:
     vue-i18n: ^11.1.2
     vue-router: ^4.5.0
     vuetify: ^3.8.0
+    uuid: ^11.1.0
 
   types:
     '@types/markdown-it-link-attributes': ^3.0.5

File diff suppressed because it is too large
+ 29 - 0
public/js/opencv.js


+ 10 - 1
src/components/AddBall.vue

@@ -77,6 +77,15 @@ const rules = {
   ],
 }
 
+const rtspUrlCom = computed({
+  get() {
+    return `rtsp://${user.value}:${password.value}@${ip.value}:${port.value}/0`
+  },
+  set(value: string) {
+    rtspUrl.value = value
+  },
+})
+
 async function submit(event: any) {
   loading.value = true
   const results = await event
@@ -138,7 +147,7 @@ async function submit(event: any) {
             />
             <v-text-field v-model="user" :rules="rules.user" label="SDK 用户名" />
             <v-text-field v-model="password" :rules="rules.password" label="SDK 密码" />
-            <v-text-field v-model="rtspUrl" :rules="rules.rtspUrl" label="RTSP 地址" />
+            <v-text-field v-model="rtspUrlCom" :rules="rules.rtspUrl" label="RTSP 地址" />
 
             <v-divider />
 

+ 261 - 66
src/components/BallCameraSettings.vue

@@ -1,4 +1,6 @@
 <script setup lang="ts">
+import axios from 'axios'
+import { v4 as uuidv4 } from 'uuid';
 import Viewer from 'viewerjs'
 import 'viewerjs/dist/viewer.css'
 
@@ -6,7 +8,6 @@ const positiveDirectionPOptions = ref([{ label: '+', value: '+' }, { label: '-',
 const positiveDirectionTOptions = ref([{ label: '+', value: '+' }, { label: '-', value: '-' }])
 
 const system = useSystemStore()
-
 const currentBallCamera = ref('')
 
 const addDialogV = ref(false)
@@ -52,18 +53,109 @@ function imgSrcLoad() {
   imgSrcLoading.value = false
 }
 
-interface MarkItemType { id: number, x: number, y: number, color: string }
+interface MarkItemType { id: string, p: number, t: number, x: number, y: number, color: string }
 type MarkType = MarkItemType[]
 const mark = ref<MarkType>([])
 const markClone = ref<MarkType>([])
 const currentMark = ref<MarkItemType>({
-  id: 0,
+  id: '',
+  p: 0.0,
+  t: 0.0,
   x: 0.0,
   y: 0.0,
   color: '#000',
 })
 
+// opencv ==============start
+async function buildMatrix() {
+  let srcPoints: any = [];
+  let dstPoints: any = [];
+
+  // 遍历点集收集源点和目标点
+  // Object.values(_imgController.p).forEach(v => {
+  //     if (v.p !== undefined && v.t !== undefined) {
+  //         srcPoints.push([v.x, v.y]);
+  //         dstPoints.push([v.p, v.t]);
+  //     }
+  // });
+  PTXYMap.forEach(function (v: any, key) {
+    if (v.P !== undefined && v.T !== undefined) {
+      srcPoints.push([v.X, v.Y]);
+      dstPoints.push([v.P, v.T]);
+    }
+  })
+
+  let matrix = null;
+
+  if (srcPoints.length >= 4) {
+    // 将点集转换为OpenCV矩阵格式
+    const srcMat = pointsToMat(srcPoints);
+    const dstMat = pointsToMat(dstPoints);
+
+    // 计算单应性矩阵
+    matrix = findHomography(srcMat, dstMat);
+    console.log(matrix);
+
+    // 清理临时矩阵(OpenCV.js内存管理)
+    srcMat.delete();
+    dstMat.delete();
+  }
+
+  // 应用矩阵到所有点
+  PTXYMap.forEach(function (v: any, key) {
+    if (v.P !== undefined && v.T !== undefined) {
+      matrix2Point(v, matrix);
+    }
+  })
+}
+
+// 将点数组转换为OpenCV矩阵(CV_64FC2类型)
+function pointsToMat(points: any) {
+  const data = new Float64Array(points.length * 2);
+  points.forEach(([x, y]:[any, any], i: number) => {
+    data[i * 2] = x;
+    data[i * 2 + 1] = y;
+  });
+  return window.cv.matFromArray(points.length, 1, window.cv.CV_64FC2, data);
+}
+
+// 计算单应性矩阵
+function findHomography(srcPoints:any, dstPoints:any) {
+  const homography = new window.cv.Mat();
+    // 使用基本参数版本:src, dst, method (0), ransacReprojThreshold (3)
+    window.cv.findHomography(srcPoints, dstPoints, homography, 0, 3);
+    return homography;
+}
+
+// 应用矩阵变换到单个点
+function matrix2Point(v:any, matrix:any) {
+  if (!matrix || matrix.empty()) return;
+
+  // 创建输入矩阵(齐次坐标)
+  const src = window.cv.matFromArray(3, 1, window.cv.CV_64FC1, [v.X, v.Y, 1]);
+  const dst = new window.cv.Mat();
+
+  // 执行矩阵变换
+  window.cv.gemm(matrix, src, 1, null, 0, dst);
+
+  // 转换为笛卡尔坐标
+  const w = dst.data64F[2];
+  if (Math.abs(w) > Number.EPSILON) {
+    v.p_transformed = dst.data64F[0] / w;
+    v.t_transformed = dst.data64F[1] / w;
+  }
+
+  // 清理临时矩阵
+  src.delete();
+  dst.delete();
+}
+
+// opencv =============end
+
+
 function markClick(item: MarkItemType) {
+  console.log(item);
+
   currentMark.value = item
   colorReset(item.id)
 }
@@ -76,7 +168,7 @@ function calculateOriginalCoords(x: number, y: number, originalWidth: number, or
 }
 
 // 重置颜色
-function colorReset(id: number) {
+function colorReset(id: string) {
   mark.value.forEach((v, i) => {
     v.color = '#000'
     markClone.value[i].color = '#000'
@@ -90,19 +182,22 @@ function colorReset(id: number) {
 }
 
 // 删除标记
-function del(id: number) {
+function del(id: string) {
   if (!id)
     return
   mark.value = mark.value.filter(v => v.id !== id)
   markClone.value = markClone.value.filter(v => v.id !== id)
-  currentMark.value = mark.value[mark.value.length - 1]
+  mark.value.length && (currentMark.value = mark.value[mark.value.length - 1])
+  mark.value.length && colorReset(currentMark.value.id)
 }
 // 清空标记
 function clear() {
   mark.value = []
   markClone.value = []
   currentMark.value = {
-    id: 0,
+    id: '',
+    p: 0.0,
+    t: 0.0,
     x: 0.0,
     y: 0.0,
     color: '#000',
@@ -125,22 +220,25 @@ function markPointOnImageWithMargin(imgElement: HTMLImageElement) {
       id: v.id,
       x: v.x * (scaledWidth / originalWidth) + left - 12,
       y: v.y * (scaledHeight / originalHeight) + top - 12,
+      p: v.p,
+      t: v.t,
       color: v.color,
     }
   })
   mark.value.length && colorReset(currentMark.value.id)
 }
 
-let id = 0
 function dbClick(e: MouseEvent) {
   // 获取鼠标点击位置
   const { offsetX, offsetY } = e
   const { originalX, originalY } = calculateOriginalCoords(offsetX, offsetY, (imgElement as HTMLImageElement).naturalWidth, (imgElement as HTMLImageElement).naturalHeight, (imgElement as HTMLImageElement).offsetWidth, (imgElement as HTMLImageElement).offsetHeight)
 
   const markV = {
-    id: ++id,
+    id: uuidv4(),
     x: originalX,
     y: originalY,
+    p: 0.0,
+    t: 0.0,
     color: '#000',
   }
   mark.value.push(markV)
@@ -149,9 +247,92 @@ function dbClick(e: MouseEvent) {
   markPointOnImageWithMargin(imgElement as HTMLImageElement)
 }
 
-onMounted(() => {
+
+const PTXYMap = new Map<string, { P: number, T: number, X: number, Y: number }>()
+
+
+function getPTXY(id: string, x: number, y: number) {
+  if (!system.globalConfig.serverUrl) {
+    system.msg('请先设置服务器地址')
+    return
+  }
+  if (!currentMark.value.id) {
+    system.msg('请双击创建标记')
+    return
+  }
+  if (!id) {
+    system.msg('请选择球机')
+    return
+  }
+  axios.get(`https://${system.globalConfig.serverUrl}/api/BallCamera/PtzGet?CameraId=${id}`).then(res => {
+    PTXYMap.set(currentMark.value.id, { P: res.data.data.P, T: res.data.data.T, X: x, Y: y })
+    mark.value.forEach((v, i) => {
+      if (v.id === currentMark.value.id) {
+        const p = system.mapping(system.positiveConfig.P_Start, system.positiveConfig.P_Max, res.data.data.P, system.positiveConfig.P_Direction, "")
+        const t = system.mapping(system.positiveConfig.T_Start, system.positiveConfig.T_Max, res.data.data.T, system.positiveConfig.T_Direction, "")
+        v.p = p
+        v.t = t
+        markClone.value[i].p = p
+        markClone.value[i].t = t
+      }
+    })
+  })
+
+}
+
+const loadOld = () => {
+  if (!system.ballCameraInfo) return
+  console.log(system.ballCameraInfo);
+  PTXYMap.clear()
+  const ps: any = system.ballCameraInfo.Matrix.PointSet
+  console.log(ps);
+
+  for (const key in ps) {
+    PTXYMap.set(key, {
+      P: ps[key].P,
+      T: ps[key].T,
+      X: ps[key].X,
+      Y: ps[key].Y,
+    })
+    mark.value.push({
+      id: key,
+      x: ps[key].X,
+      y: ps[key].Y,
+      p: ps[key].P,
+      t: ps[key].T,
+      color: '#000',
+    })
+  }
+
+  markClone.value = JSON.parse(JSON.stringify(mark.value))
+  colorReset(mark.value[mark.value.length - 1].id)
+  currentMark.value = mark.value[mark.value.length - 1]
+  markPointOnImageWithMargin(imgElement as HTMLImageElement)
+}
+
+const jumpToPT = (p: number, t: number) => {
+  if (!system.globalConfig.serverUrl) {
+    system.msg('请先设置服务器地址')
+    return
+  }
+  const pV = system.mapping(system.positiveConfig.P_Start, system.positiveConfig.P_Max, p, system.positiveConfig.P_Direction, "inv")
+  const tV = system.mapping(system.positiveConfig.T_Start, system.positiveConfig.T_Max, t, system.positiveConfig.T_Direction, "inv")
+  axios.put(`https://${system.globalConfig.serverUrl}/api/BallCamera/PtzSet?CameraId=${currentBallCamera.value}&Action=1`, { P: pV, T: tV, Z: 0 }).then(res => {
+    console.log(res);
+  })
+}
+
+
+
+let viewer: Viewer
+
+watch(() => system.drawer, () => {
+  console.log(viewer)
+})
+
+function initViewer() {
   const updloadImg = document.getElementById('uploadImage') as HTMLImageElement
-  const _ = new Viewer(updloadImg, {
+  viewer = new Viewer(updloadImg, {
     inline: true,
     backdrop: true,
     navbar: false,
@@ -184,6 +365,10 @@ onMounted(() => {
       markPointOnImageWithMargin(imgElement as HTMLImageElement)
     },
   })
+}
+
+onMounted(() => {
+  initViewer()
 })
 </script>
 
@@ -191,30 +376,21 @@ onMounted(() => {
   <v-row>
     <v-col class="relative" cols="12" md="6">
       <div class="flex-1">
-        <v-select
-          v-if="system.type?.Baseline === 'BoGuan'"
-          v-model="system.currentDevice" :items="system.deviceList" item-title="name" item-value="id"
-          label="选择设备"
-          @update:model-value="system.getBallCameraList"
-        />
-        <v-select
-          v-model="currentBallCamera" :items="system.ballCameraList" label="球机列表" item-title="name" item-value="id" @update:model-value="system.getBallCameraInfo"
-        >
+        <v-select v-if="system.type?.Baseline === 'BoGuan'" v-model="system.currentDevice" :items="system.deviceList"
+          item-title="name" item-value="id" label="选择设备" @update:model-value="system.getBallCameraList" />
+        <v-select v-model="currentBallCamera" :items="system.ballCameraList" label="球机列表" item-title="name"
+          item-value="id" @update:model-value="system.getBallCameraInfo">
           <template #append>
             <add-ball v-model:dialog="addDialogV">
               <template #activator="{ props: activatorProps }">
-                <v-btn
-                  v-tooltip:top="'添加球机'" size="large" rounded="0" variant="flat" icon="mdi-plus-thick"
-                  v-bind="activatorProps"
-                />
+                <v-btn v-tooltip:top="'添加球机'" size="large" rounded="0" variant="flat" icon="mdi-plus-thick"
+                  v-bind="activatorProps" />
               </template>
             </add-ball>
             <v-dialog max-width="400">
               <template #activator="{ props: activatorProps }">
-                <v-btn
-                  v-tooltip:top="'删除球机'" size="large" rounded="0" variant="flat" icon="mdi-trash-can"
-                  v-bind="activatorProps"
-                />
+                <v-btn v-tooltip:top="'删除球机'" size="large" rounded="0" variant="flat" icon="mdi-trash-can"
+                  v-bind="activatorProps" />
               </template>
 
               <template #default="{ isActive }">
@@ -236,47 +412,62 @@ onMounted(() => {
         <v-row>
           <v-col>
             <v-sheet>
-              <v-number-input
-                v-model="system.positiveConfig.P_Start" control-variant="stacked" :precision="1"
-                hide-details="auto" label="P 起始值"
-              />
+              <v-number-input v-model="system.positiveConfig.P_Start" control-variant="stacked" :precision="1"
+                hide-details="auto" label="P 起始值" />
+            </v-sheet>
+          </v-col>
+
+          <v-col>
+            <v-sheet>
+              <v-number-input v-model="system.positiveConfig.T_Start" control-variant="stacked" :precision="1"
+                hide-details="auto" label="T 起始值" />
+            </v-sheet>
+          </v-col>
+        </v-row>
+        <v-row>
+          <v-col>
+            <v-sheet>
+              <v-number-input v-model="system.positiveConfig.P_Max" control-variant="stacked" :precision="1"
+                hide-details="auto" label="P 最大值" />
             </v-sheet>
           </v-col>
 
           <v-col>
             <v-sheet>
-              <v-number-input
-                v-model="system.positiveConfig.T_Start" control-variant="stacked" :precision="1"
-                hide-details="auto" label="T 起始值"
-              />
+              <v-number-input v-model="system.positiveConfig.T_Max" control-variant="stacked" :precision="1"
+                hide-details="auto" label="T 最大值" />
             </v-sheet>
           </v-col>
         </v-row>
         <v-row>
           <v-col>
             <v-sheet>
-              <v-select
-                v-model="system.positiveConfig.p" :items="positiveDirectionPOptions" item-title="label"
-                item-value="value" label="P 正方向"
-              />
+              <v-select v-model="system.positiveConfig.P_Direction" :items="positiveDirectionPOptions"
+                item-title="label" item-value="value" label="P 正方向" />
             </v-sheet>
           </v-col>
           <v-col>
             <v-sheet>
-              <v-select
-                v-model="system.positiveConfig.t" :items="positiveDirectionTOptions" item-title="label"
-                item-value="value" label="T 正方向"
-              />
+              <v-select v-model="system.positiveConfig.T_Direction" :items="positiveDirectionTOptions"
+                item-title="label" item-value="value" label="T 正方向" />
             </v-sheet>
           </v-col>
         </v-row>
 
         <!-- 视频播放 -->
         <div class="p-1">
-          <v-card loading title="画面预览" subtitle="当前P0.0,当前T0.0,当前标定点位的个数0" text="">
-            <div class="aspect-ratio-video b-1 b-#ccc b-solid bg-black" />
+          <v-card :loading="system.canvasVideoLoading" title="画面预览"
+            :subtitle="`当前P ${system.PT.p},当前T ${system.PT.t},当前标定点位的个数0`" text="">
+            <div class="relative aspect-ratio-video b-1 b-#ccc b-solid bg-black">
+              <v-icon icon="mdi-plus" color="#ef4444" class="absolute top-50% left-50% transform-translate--50%" :style="{
+                textShadow: `0 -1px #fff, 1px 0px #fff, 0 1px #fff, -1px 0 #fff,
+          -1px -1px #fff, 1px 1px #fff, 1px -1px #fff, -1px 1px #fff`,
+              }" />
+              <canvas v-if="system.resetDom" class="canvas-video h-full w-full" />
+            </div>
             <v-card-actions>
-              <v-btn block variant="tonal">
+              <v-btn :disabled="system.canvasVideoLoading" block variant="tonal"
+                @click="system.playBallCamera(currentBallCamera)">
                 播放
               </v-btn>
             </v-card-actions>
@@ -286,7 +477,9 @@ onMounted(() => {
           <div class="grid grid-cols-3 grid-rows-3">
             <div v-for="tool in tools" :key="tool.type">
               <div v-if="!tool.type" class="invisible" />
-              <v-btn v-else width="60" height="40" variant="tonal" color="blue-darken-2">
+              <v-btn v-else width="60" height="40" variant="tonal" color="blue-darken-2"
+                @mousedown="system.ballMove(currentBallCamera, { Speed: step, Direction: tool.value })"
+                @mouseup="system.ballStop(currentBallCamera, { Direction: tool.value })">
                 {{ tool.icon }}
               </v-btn>
             </div>
@@ -294,10 +487,14 @@ onMounted(() => {
           <v-divider class="mx-2" vertical opacity="8" />
           <div class="flex-1">
             <div class="flex justify-around">
-              <v-btn variant="tonal" color="blue-darken-2">
+              <v-btn variant="tonal" color="blue-darken-2"
+                @mousedown="system.ballMove(currentBallCamera, { Speed: step, Direction: 9 })"
+                @mouseup="system.ballStop(currentBallCamera, { Direction: 9 })">
                 🔍+
               </v-btn>
-              <v-btn variant="tonal" color="blue-darken-2">
+              <v-btn variant="tonal" color="blue-darken-2"
+                @mousedown="system.ballMove(currentBallCamera, { Speed: step, Direction: 10 })"
+                @mouseup="system.ballStop(currentBallCamera, { Direction: 10 })">
                 🔎-
               </v-btn>
             </div>
@@ -308,8 +505,8 @@ onMounted(() => {
               云台转动速度:{{ step }}
             </div>
             <div class="mt-1 text-center">
-              <v-btn color="blue">
-                使用旧
+              <v-btn color="blue" @click="loadOld">
+                使用旧
               </v-btn>
             </div>
           </div>
@@ -323,20 +520,18 @@ onMounted(() => {
           <v-skeleton-loader v-if="imgSrcLoading" height="100%" type="image" />
           <img id="uploadImage" class="w-full" :src="imgSrc" alt="图片预览" @load="imgSrcLoad">
           <v-empty-state v-if="!imgSrcLoading && !imgSrc" icon="mdi-image-broken-variant" title="暂无上传图片" />
-          <v-icon
-            v-for="(item, index) in mark" :key="index" :style="{
-              top: `${item.y}px`,
-              left: `${item.x}px`,
-              textShadow: `0 -1px #fff, 1px 0px #fff, 0 1px #fff, -1px 0 #fff,
+          <v-icon v-for="(item, index) in mark" :key="index" :style="{
+            top: `${item.y}px`,
+            left: `${item.x}px`,
+            textShadow: `0 -1px #fff, 1px 0px #fff, 0 1px #fff, -1px 0 #fff,
           -1px -1px #fff, 1px 1px #fff, 1px -1px #fff, -1px 1px #fff`,
-            }" :color="item.color" icon="mdi-plus-thick" class="absolute bottom-0" variant="tonal"
-            @click="markClick(item)"
-          />
+          }" :color="item.color" icon="mdi-plus-thick" class="absolute bottom-0" variant="tonal"
+            @click="markClick(item)" />
         </div>
         <div class="flex flex-col items-center">
           <v-chip class="ma-2" color="blue" label>
             <v-icon icon="mdi-map-marker-radius" start />
-            选中的标记 P: {{ currentMark.x }}, T: {{ currentMark.y }}
+            选中的标记 P: {{ currentMark.p }}, T: {{ currentMark.t }}
           </v-chip>
           <v-card min-width="90%">
             <v-card-text>
@@ -348,23 +543,23 @@ onMounted(() => {
                 </div>
                 <v-row justify="center">
                   <v-col>
-                    <v-btn width="100%">
-                      当前PT位置装PT
+                    <v-btn width="100%" @click="getPTXY(currentBallCamera, currentMark.x, currentMark.y)">
+                      当前PT位置装PT
                     </v-btn>
                   </v-col>
 
                   <v-col>
                     <v-btn width="100%">
-                      根据映射矩阵装PT
+                      根据映射矩阵装PT
                     </v-btn>
                   </v-col>
                   <v-col>
-                    <v-btn width="100%">
+                    <v-btn width="100%" @click="buildMatrix">
                       生成映射矩阵
                     </v-btn>
                   </v-col>
                   <v-col>
-                    <v-btn width="100%">
+                    <v-btn width="100%" @click="jumpToPT(currentMark.p, currentMark.t)">
                       跳转到PT
                     </v-btn>
                   </v-col>

+ 11 - 14
src/layouts/home.vue

@@ -3,18 +3,15 @@ const theme = computed(() => isDark.value ? 'dark' : 'light')
 </script>
 
 <template>
-  <v-responsive>
-    <v-app :theme="theme">
-      <v-app-bar-nav-icon />
-      <v-app-bar class="px-3">
-        <v-app-bar-title>CAMERA-MODULE-TOOL</v-app-bar-title>
-        <HeaderTool />
-      </v-app-bar>
-      <v-main scrollable>
-        <v-container fluid>
-          <RouterView />
-        </v-container>
-      </v-main>
-    </v-app>
-  </v-responsive>
+  <v-app :theme="theme">
+    <v-app-bar class="px-3">
+      <v-app-bar-title>CAMERA-MODULE-TOOL</v-app-bar-title>
+      <HeaderTool />
+    </v-app-bar>
+    <v-main>
+      <v-container fluid>
+        <RouterView />
+      </v-container>
+    </v-main>
+  </v-app>
 </template>

+ 29 - 29
src/pages/index.vue

@@ -12,7 +12,13 @@ const system = useSystemStore()
 //     router.push(`/hi/${encodeURIComponent(name.value)}`)
 // }
 
-const drawer = ref(true)
+const globalConfig = reactive<{
+  imgUrl: File | null
+  serverUrl: string
+}>({
+  imgUrl: null,
+  serverUrl: '192.168.211.3',
+})
 
 const text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'
 
@@ -23,53 +29,40 @@ const tabItems = ref([
   { id: 3, label: '球机设置' },
 ])
 
-const file = ref<File | null>(null)
-
-watch(file, (val) => {
+watch(() => globalConfig.imgUrl, (val) => {
   console.log(val)
 }, { deep: true })
 
+function applyFn() {
+  system.setGlobalConfig(globalConfig)
+  system.saveGlobalConfig(() => { system.drawer = false })
+}
+
 useHead({
   title: () => t('button.home'),
 })
 </script>
 
 <template>
-  <v-navigation-drawer
-    v-model="drawer" location="right" :width="400" temporary
-  >
+  <v-navigation-drawer v-model="system.drawer" location="right" :width="300">
     <div class="p-4 pb-0">
       <span class="text-5 font-700">全局配置</span>
     </div>
     <v-divider class="my-2" opacity="8" />
     <div class="p-4 pt-0">
-      <v-file-input
-        v-model="file"
-        accept="image/*"
-        prepend-icon="mdi-image"
-        :label="t('intro.qjfwdz')"
-        show-size
-      />
-      <v-text-field v-model="system.globalConfig.serverUrl" clearable label="服务地址" prepend-icon="$vuetify" />
+      <v-file-input v-model="globalConfig.imgUrl" accept="image/*" prepend-icon="mdi-image" :label="t('intro.qjfwdz')" show-size />
+      <v-text-field v-model="globalConfig.serverUrl" clearable label="服务地址" prepend-icon="$vuetify" />
     </div>
-    <div>
-      <v-btn block color="blue" @click="system.saveGlobalConfig(() => { drawer = false })">
+    <v-sheet width="90%" mx-auto>
+      <v-btn block color="blue" @click="applyFn">
         应用
       </v-btn>
-    </div>
+    </v-sheet>
   </v-navigation-drawer>
   <div>
-    <v-fab icon="mdi-cog" app @click="drawer = !drawer" />
-    <v-tabs
-      v-model="currentItem"
-      align-tabs="center"
-    >
-      <v-tab
-        v-for="item in tabItems"
-        :key="item.id"
-        :text="item.label"
-        :value="item.id"
-      />
+    <v-fab icon="mdi-cog" app @click="system.drawer = !system.drawer" />
+    <v-tabs v-model="currentItem" align-tabs="center">
+      <v-tab v-for="item in tabItems" :key="item.id" :text="item.label" :value="item.id" />
     </v-tabs>
     <v-tabs-window v-model="currentItem">
       <v-tabs-window-item :value="1">
@@ -90,6 +83,13 @@ useHead({
         </v-card>
       </v-tabs-window-item>
     </v-tabs-window>
+    <v-snackbar
+      v-model="system.snackbar"
+      content-class="top-15"
+      :timeout="2000"
+    >
+      {{ system.snackbarText }}
+    </v-snackbar>
   </div>
 </template>
 

+ 1 - 0
src/shims.d.ts

@@ -1,5 +1,6 @@
 declare interface Window {
   // extend the window
+  cv: any
 }
 
 // with unplugin-vue-markdown, markdown files can be treated as Vue components

+ 149 - 14
src/stores/system.ts

@@ -1,4 +1,5 @@
 import axios from 'axios'
+import useWorker from 'omnimatrix-video-player'
 import { acceptHMRUpdate, defineStore } from 'pinia'
 
 axios.defaults.timeout = 3000
@@ -46,28 +47,44 @@ interface IBallCameraInfo {
   }
   Channel: number
 }
+interface GlobalConfig {
+  imgUrl: File | null
+  serverUrl: string
+}
 
 export const useSystemStore = defineStore('system', () => {
-  interface GlobalConfig {
-    imgUrl: string
-    serverUrl: string
-  }
+  const drawer = ref(true)
   const globalConfig = reactive<GlobalConfig>({
-    imgUrl: '',
-    serverUrl: '192.168.211.3',
+    imgUrl: null,
+    serverUrl: '',
   })
   const ballCameraList = ref<IOptions[]>([])
   const type = ref<IVersion>()
   const deviceList = ref<IDeviceListOptions[]>([])
   const ballCameraInfo = ref<IBallCameraInfo>()
   const positiveConfig = reactive({
-    p: '+',
-    t: '+',
+    P_Direction: '+',
+    T_Direction: '+',
     P_Start: 0.0,
     T_Start: 0.0,
+    P_Max: 0.0,
+    T_Max: 0.0,
   })
   const currentDevice = ref()
+  const canvasVideoLoading = ref(false)
+  const snackbar = ref(false)
+  const snackbarText = ref('')
+  const resetDom = ref(true)
+  const PTTimer = ref()
+  const PT = reactive({
+    p: 0,
+    t: 0,
+  })
 
+  const msg = (text: string) => {
+    snackbar.value = true
+    snackbarText.value = text
+  }
   function setGlobalConfig(config: GlobalConfig) {
     Object.assign(globalConfig, config)
   }
@@ -81,7 +98,7 @@ export const useSystemStore = defineStore('system', () => {
 
   function getBallCameraList() {
     if (globalConfig.serverUrl) {
-      axios.get(`https://${globalConfig.serverUrl}/api/BallCamera/List`,{ headers:{ 'Deviceid': currentDevice.value }}).then((res) => {
+      axios.get(`https://${globalConfig.serverUrl}/api/BallCamera/List`, { headers: { Deviceid: currentDevice.value } }).then((res) => {
         ballCameraList.value = []
         Object.keys(res.data.data).forEach((key) => {
           ballCameraList.value.push({
@@ -97,9 +114,10 @@ export const useSystemStore = defineStore('system', () => {
     if (globalConfig.serverUrl) {
       axios.get(`https://${globalConfig.serverUrl}/Version`).then((res) => {
         type.value = res.data.data
-        if(type.value?.Baseline === 'BoGuan'){
+        if (type.value?.Baseline === 'BoGuan') {
           getDeviceList()
-        }else{
+        }
+        else {
           getBallCameraList()
         }
       }).catch(() => {
@@ -128,26 +146,143 @@ export const useSystemStore = defineStore('system', () => {
     if (globalConfig.serverUrl) {
       axios.get(`https://${globalConfig.serverUrl}/api/BallCamera/Info?CameraId=${CameraId}`).then((res) => {
         ballCameraInfo.value = res.data.data
-        positiveConfig.p = res.data.data.Matrix.p_Positive_Direction
-        positiveConfig.t = res.data.data.Matrix.T_Positive_Direction
+        positiveConfig.P_Direction = res.data.data.Matrix.p_Positive_Direction
+        positiveConfig.T_Direction = res.data.data.Matrix.T_Positive_Direction
         positiveConfig.P_Start = res.data.data.Matrix.P_Start
         positiveConfig.T_Start = res.data.data.Matrix.T_Start
+        positiveConfig.P_Max = res.data.data.Matrix.P_Max
+        positiveConfig.T_Max = res.data.data.Matrix.T_Max
+      })
+    }
+  }
+
+  let workerObj: any = null
+  function playBallCamera(id: string) {
+    if (!globalConfig.serverUrl) {
+      msg('请先设置服务器地址')
+      return
+    }
+    if (!id) {
+      msg('请选择球机')
+      return
+    }
+    if (workerObj) {
+      resetDom.value = false
+      workerObj.close()
+      workerObj = null
+      setTimeout(() => {
+        resetDom.value = true
+        nextTick(() => {
+          playBallCamera(id)
+        })
+      }, 50)
+    }
+    canvasVideoLoading.value = true
+    const url = `wss://${globalConfig.serverUrl}/api/BallCamera/View?CameraId=${id}`
+    try {
+      workerObj = useWorker(url, '.canvas-video', null, () => {
+        canvasVideoLoading.value = false
+      })
+    }
+    catch (error) {
+      console.error(error)
+    }
+  }
+
+
+  // startV : 以 startV 为 0
+  // max : 原始段最大长度
+  // value : 值
+  // direction (String): 正方向的位置,value +1 的值对应实际的 +/- 方向
+  function mapping(startV: number, max: number, value: number, direction: string, method: string) {
+    if (direction == '+') {
+      if (method == 'inv') {
+        if (value > (max - startV)) {
+          return value - (max - startV);
+        } else {
+          return startV + value; // 映射
+        }
+      } else {
+        if (value > startV) {
+          return (value - startV);
+        } else {
+          return (max - startV) + value; // 映射
+        }
+      }
+    } else {
+      if (value > startV) {
+        return startV + max - value;
+      } else {
+        return startV - value;
+      }
+    }
+  }
+
+  function ballMove(id: string, data: {
+    Speed: number
+    Direction: number
+  }) {
+    if (!globalConfig.serverUrl) {
+      msg('请先设置服务器地址')
+      return
+    }
+    if (!id) {
+      msg('请选择球机')
+      return
+    }
+    if (PTTimer.value) {
+      clearTimeout(PTTimer.value)
+    }
+    PTTimer.value = setTimeout(() => {
+      axios.get(`https://${globalConfig.serverUrl}/api/BallCamera/PtzGet?CameraId=${id}`).then(res => {
+        PT.p = mapping(positiveConfig.P_Start, positiveConfig.P_Max, res.data.data.P, positiveConfig.P_Direction, "")
+        PT.t = mapping(positiveConfig.T_Start, positiveConfig.T_Max, res.data.data.T, positiveConfig.T_Direction, "")
       })
+    }, 3000)
+    axios.put(`https://${globalConfig.serverUrl}/api/BallCamera/Move?CameraId=${id}`, data)
+  }
+  function ballStop(id: string, data: {
+    Direction: number
+  }) {
+    if (!globalConfig.serverUrl) {
+      msg('请先设置服务器地址')
+      return
     }
+    if (!id) {
+      msg('请选择球机')
+      return
+    }
+    axios.put(`https://${globalConfig.serverUrl}/api/BallCamera/Stop?CameraId=${id}`, data)
   }
 
+
+  onUnmounted(() => {
+    workerObj?.close()
+  })
+
   return {
+    drawer,
     globalConfig,
     positiveConfig,
     setGlobalConfig,
     saveGlobalConfig,
     ballCameraList,
     type,
+    resetDom,
     deviceList,
     ballCameraInfo,
     getBallCameraInfo,
     getBallCameraList,
-    currentDevice
+    currentDevice,
+    playBallCamera,
+    canvasVideoLoading,
+    snackbar,
+    snackbarText,
+    msg,
+    ballMove,
+    ballStop,
+    PT,
+    mapping
   }
 })
 

+ 1 - 1
src/styles/main.css

@@ -6,9 +6,9 @@ body,
   height: 100%;
   margin: 0;
   padding: 0;
-  overflow: hidden;
 }
 
+
 html.dark {
   background: #121212;
   color-scheme: dark;

+ 6 - 0
vite.config.ts

@@ -2,6 +2,7 @@ import path from 'node:path'
 import VueI18n from '@intlify/unplugin-vue-i18n/vite'
 import Shiki from '@shikijs/markdown-it'
 import { unheadVueComposablesImports } from '@unhead/vue'
+import basicSsl from '@vitejs/plugin-basic-ssl'
 import Vue from '@vitejs/plugin-vue'
 import LinkAttributes from 'markdown-it-link-attributes'
 import Unocss from 'unocss/vite'
@@ -26,6 +27,7 @@ export default defineConfig({
   },
 
   plugins: [
+    basicSsl(),
     VueMacros({
       plugins: {
         vue: Vue({
@@ -148,6 +150,10 @@ export default defineConfig({
     environment: 'jsdom',
   },
 
+  optimizeDeps: {
+    exclude: ['omnimatrix-video-player'],
+  },
+
   // https://github.com/antfu/vite-ssg
   ssgOptions: {
     script: 'async',

Some files were not shown because too many files changed in this diff