Browse Source

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

gitboyzcf 5 months ago
parent
commit
9b999d456a

+ 13 - 0
index.html

@@ -14,6 +14,19 @@
           document.documentElement.classList.toggle('dark', true)
           document.documentElement.classList.toggle('dark', true)
       })()
       })()
     </script>
     </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>
   </head>
   <body class="font-sans">
   <body class="font-sans">
     <div id="app"></div>
     <div id="app"></div>

+ 3 - 1
package.json

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

+ 28 - 0
pnpm-lock.yaml

@@ -76,6 +76,9 @@ catalogs:
     '@mdi/font':
     '@mdi/font':
       specifier: ^7.4.47
       specifier: ^7.4.47
       version: 7.4.47
       version: 7.4.47
+    '@vitejs/plugin-basic-ssl':
+      specifier: ^2.0.0
+      version: 2.0.0
     '@vue-macros/volar':
     '@vue-macros/volar':
       specifier: ^3.0.0-beta.7
       specifier: ^3.0.0-beta.7
       version: 3.0.0-beta.7
       version: 3.0.0-beta.7
@@ -137,6 +140,9 @@ catalogs:
     pinia:
     pinia:
       specifier: ^3.0.1
       specifier: ^3.0.1
       version: 3.0.1
       version: 3.0.1
+    uuid:
+      specifier: ^11.1.0
+      version: 11.1.0
     vee-validate:
     vee-validate:
       specifier: ^4.0.0
       specifier: ^4.0.0
       version: 4.15.0
       version: 4.15.0
@@ -193,6 +199,9 @@ importers:
       pinia:
       pinia:
         specifier: catalog:frontend
         specifier: catalog:frontend
         version: 3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
         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:
       vee-validate:
         specifier: catalog:frontend
         specifier: catalog:frontend
         version: 4.15.0(vue@3.5.13(typescript@5.8.2))
         version: 4.15.0(vue@3.5.13(typescript@5.8.2))
@@ -236,6 +245,9 @@ importers:
       '@unocss/eslint-config':
       '@unocss/eslint-config':
         specifier: catalog:build
         specifier: catalog:build
         version: 66.1.0-beta.7(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)
         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':
       '@vitejs/plugin-vue':
         specifier: catalog:build
         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))
         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]
     cpu: [x64]
     os: [win32]
     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':
   '@vitejs/plugin-vue@5.2.3':
     resolution: {integrity: sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==}
     resolution: {integrity: sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==}
     engines: {node: ^18.0.0 || >=20.0.0}
     engines: {node: ^18.0.0 || >=20.0.0}
@@ -5766,6 +5784,10 @@ packages:
     resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
     resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
     engines: {node: '>= 0.4.0'}
     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:
   uuid@8.3.2:
     resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
     resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
     hasBin: true
     hasBin: true
@@ -8121,6 +8143,10 @@ snapshots:
   '@unrs/resolver-binding-win32-x64-msvc@1.3.2':
   '@unrs/resolver-binding-win32-x64-msvc@1.3.2':
     optional: true
     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))':
   '@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:
     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)
       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: {}
   utils-merge@1.0.1: {}
 
 
+  uuid@11.1.0: {}
+
   uuid@8.3.2: {}
   uuid@8.3.2: {}
 
 
   validate-npm-package-license@3.0.4:
   validate-npm-package-license@3.0.4:

+ 2 - 0
pnpm-workspace.yaml

@@ -31,6 +31,7 @@ catalogs:
     '@antfu/eslint-config': ^4.11.0
     '@antfu/eslint-config': ^4.11.0
     '@iconify-json/carbon': ^1.2.8
     '@iconify-json/carbon': ^1.2.8
     '@mdi/font': ^7.4.47
     '@mdi/font': ^7.4.47
+    '@vitejs/plugin-basic-ssl': ^2.0.0
     '@vue-macros/volar': ^3.0.0-beta.7
     '@vue-macros/volar': ^3.0.0-beta.7
     '@vue/test-utils': ^2.4.6
     '@vue/test-utils': ^2.4.6
     cypress: ^14.2.1
     cypress: ^14.2.1
@@ -59,6 +60,7 @@ catalogs:
     vue-i18n: ^11.1.2
     vue-i18n: ^11.1.2
     vue-router: ^4.5.0
     vue-router: ^4.5.0
     vuetify: ^3.8.0
     vuetify: ^3.8.0
+    uuid: ^11.1.0
 
 
   types:
   types:
     '@types/markdown-it-link-attributes': ^3.0.5
     '@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) {
 async function submit(event: any) {
   loading.value = true
   loading.value = true
   const results = await event
   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="user" :rules="rules.user" label="SDK 用户名" />
             <v-text-field v-model="password" :rules="rules.password" 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 />
             <v-divider />
 
 

+ 261 - 66
src/components/BallCameraSettings.vue

@@ -1,4 +1,6 @@
 <script setup lang="ts">
 <script setup lang="ts">
+import axios from 'axios'
+import { v4 as uuidv4 } from 'uuid';
 import Viewer from 'viewerjs'
 import Viewer from 'viewerjs'
 import 'viewerjs/dist/viewer.css'
 import 'viewerjs/dist/viewer.css'
 
 
@@ -6,7 +8,6 @@ const positiveDirectionPOptions = ref([{ label: '+', value: '+' }, { label: '-',
 const positiveDirectionTOptions = ref([{ label: '+', value: '+' }, { label: '-', value: '-' }])
 const positiveDirectionTOptions = ref([{ label: '+', value: '+' }, { label: '-', value: '-' }])
 
 
 const system = useSystemStore()
 const system = useSystemStore()
-
 const currentBallCamera = ref('')
 const currentBallCamera = ref('')
 
 
 const addDialogV = ref(false)
 const addDialogV = ref(false)
@@ -52,18 +53,109 @@ function imgSrcLoad() {
   imgSrcLoading.value = false
   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[]
 type MarkType = MarkItemType[]
 const mark = ref<MarkType>([])
 const mark = ref<MarkType>([])
 const markClone = ref<MarkType>([])
 const markClone = ref<MarkType>([])
 const currentMark = ref<MarkItemType>({
 const currentMark = ref<MarkItemType>({
-  id: 0,
+  id: '',
+  p: 0.0,
+  t: 0.0,
   x: 0.0,
   x: 0.0,
   y: 0.0,
   y: 0.0,
   color: '#000',
   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) {
 function markClick(item: MarkItemType) {
+  console.log(item);
+
   currentMark.value = item
   currentMark.value = item
   colorReset(item.id)
   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) => {
   mark.value.forEach((v, i) => {
     v.color = '#000'
     v.color = '#000'
     markClone.value[i].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)
   if (!id)
     return
     return
   mark.value = mark.value.filter(v => v.id !== id)
   mark.value = mark.value.filter(v => v.id !== id)
   markClone.value = markClone.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() {
 function clear() {
   mark.value = []
   mark.value = []
   markClone.value = []
   markClone.value = []
   currentMark.value = {
   currentMark.value = {
-    id: 0,
+    id: '',
+    p: 0.0,
+    t: 0.0,
     x: 0.0,
     x: 0.0,
     y: 0.0,
     y: 0.0,
     color: '#000',
     color: '#000',
@@ -125,22 +220,25 @@ function markPointOnImageWithMargin(imgElement: HTMLImageElement) {
       id: v.id,
       id: v.id,
       x: v.x * (scaledWidth / originalWidth) + left - 12,
       x: v.x * (scaledWidth / originalWidth) + left - 12,
       y: v.y * (scaledHeight / originalHeight) + top - 12,
       y: v.y * (scaledHeight / originalHeight) + top - 12,
+      p: v.p,
+      t: v.t,
       color: v.color,
       color: v.color,
     }
     }
   })
   })
   mark.value.length && colorReset(currentMark.value.id)
   mark.value.length && colorReset(currentMark.value.id)
 }
 }
 
 
-let id = 0
 function dbClick(e: MouseEvent) {
 function dbClick(e: MouseEvent) {
   // 获取鼠标点击位置
   // 获取鼠标点击位置
   const { offsetX, offsetY } = e
   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 { originalX, originalY } = calculateOriginalCoords(offsetX, offsetY, (imgElement as HTMLImageElement).naturalWidth, (imgElement as HTMLImageElement).naturalHeight, (imgElement as HTMLImageElement).offsetWidth, (imgElement as HTMLImageElement).offsetHeight)
 
 
   const markV = {
   const markV = {
-    id: ++id,
+    id: uuidv4(),
     x: originalX,
     x: originalX,
     y: originalY,
     y: originalY,
+    p: 0.0,
+    t: 0.0,
     color: '#000',
     color: '#000',
   }
   }
   mark.value.push(markV)
   mark.value.push(markV)
@@ -149,9 +247,92 @@ function dbClick(e: MouseEvent) {
   markPointOnImageWithMargin(imgElement as HTMLImageElement)
   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 updloadImg = document.getElementById('uploadImage') as HTMLImageElement
-  const _ = new Viewer(updloadImg, {
+  viewer = new Viewer(updloadImg, {
     inline: true,
     inline: true,
     backdrop: true,
     backdrop: true,
     navbar: false,
     navbar: false,
@@ -184,6 +365,10 @@ onMounted(() => {
       markPointOnImageWithMargin(imgElement as HTMLImageElement)
       markPointOnImageWithMargin(imgElement as HTMLImageElement)
     },
     },
   })
   })
+}
+
+onMounted(() => {
+  initViewer()
 })
 })
 </script>
 </script>
 
 
@@ -191,30 +376,21 @@ onMounted(() => {
   <v-row>
   <v-row>
     <v-col class="relative" cols="12" md="6">
     <v-col class="relative" cols="12" md="6">
       <div class="flex-1">
       <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>
           <template #append>
             <add-ball v-model:dialog="addDialogV">
             <add-ball v-model:dialog="addDialogV">
               <template #activator="{ props: activatorProps }">
               <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>
               </template>
             </add-ball>
             </add-ball>
             <v-dialog max-width="400">
             <v-dialog max-width="400">
               <template #activator="{ props: activatorProps }">
               <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>
 
 
               <template #default="{ isActive }">
               <template #default="{ isActive }">
@@ -236,47 +412,62 @@ onMounted(() => {
         <v-row>
         <v-row>
           <v-col>
           <v-col>
             <v-sheet>
             <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-sheet>
           </v-col>
           </v-col>
 
 
           <v-col>
           <v-col>
             <v-sheet>
             <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-sheet>
           </v-col>
           </v-col>
         </v-row>
         </v-row>
         <v-row>
         <v-row>
           <v-col>
           <v-col>
             <v-sheet>
             <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-sheet>
           </v-col>
           </v-col>
           <v-col>
           <v-col>
             <v-sheet>
             <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-sheet>
           </v-col>
           </v-col>
         </v-row>
         </v-row>
 
 
         <!-- 视频播放 -->
         <!-- 视频播放 -->
         <div class="p-1">
         <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-card-actions>
-              <v-btn block variant="tonal">
+              <v-btn :disabled="system.canvasVideoLoading" block variant="tonal"
+                @click="system.playBallCamera(currentBallCamera)">
                 播放
                 播放
               </v-btn>
               </v-btn>
             </v-card-actions>
             </v-card-actions>
@@ -286,7 +477,9 @@ onMounted(() => {
           <div class="grid grid-cols-3 grid-rows-3">
           <div class="grid grid-cols-3 grid-rows-3">
             <div v-for="tool in tools" :key="tool.type">
             <div v-for="tool in tools" :key="tool.type">
               <div v-if="!tool.type" class="invisible" />
               <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 }}
                 {{ tool.icon }}
               </v-btn>
               </v-btn>
             </div>
             </div>
@@ -294,10 +487,14 @@ onMounted(() => {
           <v-divider class="mx-2" vertical opacity="8" />
           <v-divider class="mx-2" vertical opacity="8" />
           <div class="flex-1">
           <div class="flex-1">
             <div class="flex justify-around">
             <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>
-              <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>
               </v-btn>
             </div>
             </div>
@@ -308,8 +505,8 @@ onMounted(() => {
               云台转动速度:{{ step }}
               云台转动速度:{{ step }}
             </div>
             </div>
             <div class="mt-1 text-center">
             <div class="mt-1 text-center">
-              <v-btn color="blue">
-                使用旧
+              <v-btn color="blue" @click="loadOld">
+                使用旧
               </v-btn>
               </v-btn>
             </div>
             </div>
           </div>
           </div>
@@ -323,20 +520,18 @@ onMounted(() => {
           <v-skeleton-loader v-if="imgSrcLoading" height="100%" type="image" />
           <v-skeleton-loader v-if="imgSrcLoading" height="100%" type="image" />
           <img id="uploadImage" class="w-full" :src="imgSrc" alt="图片预览" @load="imgSrcLoad">
           <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-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`,
           -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>
         <div class="flex flex-col items-center">
         <div class="flex flex-col items-center">
           <v-chip class="ma-2" color="blue" label>
           <v-chip class="ma-2" color="blue" label>
             <v-icon icon="mdi-map-marker-radius" start />
             <v-icon icon="mdi-map-marker-radius" start />
-            选中的标记 P: {{ currentMark.x }}, T: {{ currentMark.y }}
+            选中的标记 P: {{ currentMark.p }}, T: {{ currentMark.t }}
           </v-chip>
           </v-chip>
           <v-card min-width="90%">
           <v-card min-width="90%">
             <v-card-text>
             <v-card-text>
@@ -348,23 +543,23 @@ onMounted(() => {
                 </div>
                 </div>
                 <v-row justify="center">
                 <v-row justify="center">
                   <v-col>
                   <v-col>
-                    <v-btn width="100%">
-                      当前PT位置装PT
+                    <v-btn width="100%" @click="getPTXY(currentBallCamera, currentMark.x, currentMark.y)">
+                      当前PT位置装PT
                     </v-btn>
                     </v-btn>
                   </v-col>
                   </v-col>
 
 
                   <v-col>
                   <v-col>
                     <v-btn width="100%">
                     <v-btn width="100%">
-                      根据映射矩阵装PT
+                      根据映射矩阵装PT
                     </v-btn>
                     </v-btn>
                   </v-col>
                   </v-col>
                   <v-col>
                   <v-col>
-                    <v-btn width="100%">
+                    <v-btn width="100%" @click="buildMatrix">
                       生成映射矩阵
                       生成映射矩阵
                     </v-btn>
                     </v-btn>
                   </v-col>
                   </v-col>
                   <v-col>
                   <v-col>
-                    <v-btn width="100%">
+                    <v-btn width="100%" @click="jumpToPT(currentMark.p, currentMark.t)">
                       跳转到PT
                       跳转到PT
                     </v-btn>
                     </v-btn>
                   </v-col>
                   </v-col>

+ 11 - 14
src/layouts/home.vue

@@ -3,18 +3,15 @@ const theme = computed(() => isDark.value ? 'dark' : 'light')
 </script>
 </script>
 
 
 <template>
 <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>
 </template>

+ 29 - 29
src/pages/index.vue

@@ -12,7 +12,13 @@ const system = useSystemStore()
 //     router.push(`/hi/${encodeURIComponent(name.value)}`)
 //     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.'
 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: '球机设置' },
   { id: 3, label: '球机设置' },
 ])
 ])
 
 
-const file = ref<File | null>(null)
-
-watch(file, (val) => {
+watch(() => globalConfig.imgUrl, (val) => {
   console.log(val)
   console.log(val)
 }, { deep: true })
 }, { deep: true })
 
 
+function applyFn() {
+  system.setGlobalConfig(globalConfig)
+  system.saveGlobalConfig(() => { system.drawer = false })
+}
+
 useHead({
 useHead({
   title: () => t('button.home'),
   title: () => t('button.home'),
 })
 })
 </script>
 </script>
 
 
 <template>
 <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">
     <div class="p-4 pb-0">
       <span class="text-5 font-700">全局配置</span>
       <span class="text-5 font-700">全局配置</span>
     </div>
     </div>
     <v-divider class="my-2" opacity="8" />
     <v-divider class="my-2" opacity="8" />
     <div class="p-4 pt-0">
     <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>
-    <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>
       </v-btn>
-    </div>
+    </v-sheet>
   </v-navigation-drawer>
   </v-navigation-drawer>
   <div>
   <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>
     <v-tabs-window v-model="currentItem">
     <v-tabs-window v-model="currentItem">
       <v-tabs-window-item :value="1">
       <v-tabs-window-item :value="1">
@@ -90,6 +83,13 @@ useHead({
         </v-card>
         </v-card>
       </v-tabs-window-item>
       </v-tabs-window-item>
     </v-tabs-window>
     </v-tabs-window>
+    <v-snackbar
+      v-model="system.snackbar"
+      content-class="top-15"
+      :timeout="2000"
+    >
+      {{ system.snackbarText }}
+    </v-snackbar>
   </div>
   </div>
 </template>
 </template>
 
 

+ 1 - 0
src/shims.d.ts

@@ -1,5 +1,6 @@
 declare interface Window {
 declare interface Window {
   // extend the window
   // extend the window
+  cv: any
 }
 }
 
 
 // with unplugin-vue-markdown, markdown files can be treated as Vue components
 // 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 axios from 'axios'
+import useWorker from 'omnimatrix-video-player'
 import { acceptHMRUpdate, defineStore } from 'pinia'
 import { acceptHMRUpdate, defineStore } from 'pinia'
 
 
 axios.defaults.timeout = 3000
 axios.defaults.timeout = 3000
@@ -46,28 +47,44 @@ interface IBallCameraInfo {
   }
   }
   Channel: number
   Channel: number
 }
 }
+interface GlobalConfig {
+  imgUrl: File | null
+  serverUrl: string
+}
 
 
 export const useSystemStore = defineStore('system', () => {
 export const useSystemStore = defineStore('system', () => {
-  interface GlobalConfig {
-    imgUrl: string
-    serverUrl: string
-  }
+  const drawer = ref(true)
   const globalConfig = reactive<GlobalConfig>({
   const globalConfig = reactive<GlobalConfig>({
-    imgUrl: '',
-    serverUrl: '192.168.211.3',
+    imgUrl: null,
+    serverUrl: '',
   })
   })
   const ballCameraList = ref<IOptions[]>([])
   const ballCameraList = ref<IOptions[]>([])
   const type = ref<IVersion>()
   const type = ref<IVersion>()
   const deviceList = ref<IDeviceListOptions[]>([])
   const deviceList = ref<IDeviceListOptions[]>([])
   const ballCameraInfo = ref<IBallCameraInfo>()
   const ballCameraInfo = ref<IBallCameraInfo>()
   const positiveConfig = reactive({
   const positiveConfig = reactive({
-    p: '+',
-    t: '+',
+    P_Direction: '+',
+    T_Direction: '+',
     P_Start: 0.0,
     P_Start: 0.0,
     T_Start: 0.0,
     T_Start: 0.0,
+    P_Max: 0.0,
+    T_Max: 0.0,
   })
   })
   const currentDevice = ref()
   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) {
   function setGlobalConfig(config: GlobalConfig) {
     Object.assign(globalConfig, config)
     Object.assign(globalConfig, config)
   }
   }
@@ -81,7 +98,7 @@ export const useSystemStore = defineStore('system', () => {
 
 
   function getBallCameraList() {
   function getBallCameraList() {
     if (globalConfig.serverUrl) {
     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 = []
         ballCameraList.value = []
         Object.keys(res.data.data).forEach((key) => {
         Object.keys(res.data.data).forEach((key) => {
           ballCameraList.value.push({
           ballCameraList.value.push({
@@ -97,9 +114,10 @@ export const useSystemStore = defineStore('system', () => {
     if (globalConfig.serverUrl) {
     if (globalConfig.serverUrl) {
       axios.get(`https://${globalConfig.serverUrl}/Version`).then((res) => {
       axios.get(`https://${globalConfig.serverUrl}/Version`).then((res) => {
         type.value = res.data.data
         type.value = res.data.data
-        if(type.value?.Baseline === 'BoGuan'){
+        if (type.value?.Baseline === 'BoGuan') {
           getDeviceList()
           getDeviceList()
-        }else{
+        }
+        else {
           getBallCameraList()
           getBallCameraList()
         }
         }
       }).catch(() => {
       }).catch(() => {
@@ -128,26 +146,143 @@ export const useSystemStore = defineStore('system', () => {
     if (globalConfig.serverUrl) {
     if (globalConfig.serverUrl) {
       axios.get(`https://${globalConfig.serverUrl}/api/BallCamera/Info?CameraId=${CameraId}`).then((res) => {
       axios.get(`https://${globalConfig.serverUrl}/api/BallCamera/Info?CameraId=${CameraId}`).then((res) => {
         ballCameraInfo.value = res.data.data
         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.P_Start = res.data.data.Matrix.P_Start
         positiveConfig.T_Start = res.data.data.Matrix.T_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 {
   return {
+    drawer,
     globalConfig,
     globalConfig,
     positiveConfig,
     positiveConfig,
     setGlobalConfig,
     setGlobalConfig,
     saveGlobalConfig,
     saveGlobalConfig,
     ballCameraList,
     ballCameraList,
     type,
     type,
+    resetDom,
     deviceList,
     deviceList,
     ballCameraInfo,
     ballCameraInfo,
     getBallCameraInfo,
     getBallCameraInfo,
     getBallCameraList,
     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%;
   height: 100%;
   margin: 0;
   margin: 0;
   padding: 0;
   padding: 0;
-  overflow: hidden;
 }
 }
 
 
+
 html.dark {
 html.dark {
   background: #121212;
   background: #121212;
   color-scheme: dark;
   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 VueI18n from '@intlify/unplugin-vue-i18n/vite'
 import Shiki from '@shikijs/markdown-it'
 import Shiki from '@shikijs/markdown-it'
 import { unheadVueComposablesImports } from '@unhead/vue'
 import { unheadVueComposablesImports } from '@unhead/vue'
+import basicSsl from '@vitejs/plugin-basic-ssl'
 import Vue from '@vitejs/plugin-vue'
 import Vue from '@vitejs/plugin-vue'
 import LinkAttributes from 'markdown-it-link-attributes'
 import LinkAttributes from 'markdown-it-link-attributes'
 import Unocss from 'unocss/vite'
 import Unocss from 'unocss/vite'
@@ -26,6 +27,7 @@ export default defineConfig({
   },
   },
 
 
   plugins: [
   plugins: [
+    basicSsl(),
     VueMacros({
     VueMacros({
       plugins: {
       plugins: {
         vue: Vue({
         vue: Vue({
@@ -148,6 +150,10 @@ export default defineConfig({
     environment: 'jsdom',
     environment: 'jsdom',
   },
   },
 
 
+  optimizeDeps: {
+    exclude: ['omnimatrix-video-player'],
+  },
+
   // https://github.com/antfu/vite-ssg
   // https://github.com/antfu/vite-ssg
   ssgOptions: {
   ssgOptions: {
     script: 'async',
     script: 'async',

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