소스 검색

master: Fixed 问题修复7.4

gitboyzcf 3 달 전
부모
커밋
bd276835a5
14개의 변경된 파일419개의 추가작업 그리고 223개의 파일을 삭제
  1. 1 0
      .gitignore
  2. 2 2
      package.json
  3. BIN
      public/assets/image/test.jpg
  4. 0 1
      src/components.d.ts
  5. 69 29
      src/components/AddBall.vue
  6. 268 147
      src/components/BallCameraSettings.vue
  7. 2 2
      src/components/HeaderTool.vue
  8. 1 1
      src/layouts/home.vue
  9. 2 6
      src/pages/index.vue
  10. 2 1
      src/shims.d.ts
  11. 51 22
      src/stores/system.ts
  12. 5 0
      src/styles/main.css
  13. 12 12
      uno.config.ts
  14. 4 0
      vite.config.ts

+ 1 - 0
.gitignore

@@ -4,6 +4,7 @@
 *.local
 dist
 dist-ssr
+cmt
 node_modules
 .idea/
 *.log

+ 2 - 2
package.json

@@ -24,13 +24,13 @@
     "nprogress": "catalog:frontend",
     "omnimatrix-video-player": "file:omnimatrix-video-player",
     "pinia": "catalog:frontend",
+    "uuid": "catalog:frontend",
     "vee-validate": "catalog:frontend",
     "viewerjs": "catalog:frontend",
     "vue": "catalog:frontend",
     "vue-i18n": "catalog:frontend",
     "vue-router": "catalog:frontend",
-    "vuetify": "catalog:frontend",
-    "uuid": "catalog:frontend"
+    "vuetify": "catalog:frontend"
   },
   "devDependencies": {
     "@antfu/eslint-config": "catalog:dev",

BIN
public/assets/image/test.jpg


+ 0 - 1
src/components.d.ts

@@ -15,7 +15,6 @@ declare module 'vue' {
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     TheCounter: typeof import('./components/TheCounter.vue')['default']
-    TheFooter: typeof import('./components/TheFooter.vue')['default']
     TheInput: typeof import('./components/TheInput.vue')['default']
   }
 }

+ 69 - 29
src/components/AddBall.vue

@@ -1,6 +1,14 @@
 <script setup lang="ts">
 import axios from 'axios'
 
+const props = withDefaults(defineProps<{
+  [key: string]: any
+  flag?: string
+  cameraId?: number | null
+}>(), {
+  flag: 'add',
+  cameraId: null,
+})
 const dialog = defineModel<boolean>('dialog', { type: Boolean, default: false })
 const system = useSystemStore()
 
@@ -32,7 +40,7 @@ const rules = {
       if (!value) {
         return '请输入球机IP'
       }
-      else if (/\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/.test(value)) {
+      else if (/(?:2(?:5[0-5]|[0-4]\d)|[01]?\d{1,2})(?:\.(?:2(?:5[0-5]|[0-4]\d)|[01]?\d{1,2})){3}/.test(value)) {
         return true
       }
       else {
@@ -90,37 +98,70 @@ async function submit(event: any) {
   loading.value = true
   const results = await event
   if (results.valid) {
-    axios.put(`https://${system.globalConfig.serverUrl}/api/BallCamera/Register`, {
-      Type: typeV,
-      Name: name,
-      Ip: ip,
-      Port: port,
-      User: user,
-      Password: password,
-      RtspUrl: rtspUrl,
-      Channel: 1,
-    }, { headers: { Deviceid: system.currentDevice } }).then((res) => {
-      axios.post(`https://${system.globalConfig.serverUrl}/api/BallCamera/Matrix?CameraId=${res.data.data}`, {
-        P_Start: 0.0,
-        P_Max: 360,
-        p_Positive_Direction: '+',
-        T_Start: 0.0,
-        T_Max: 180,
-        T_Positive_Direction: '+',
-        Matrix: [],
-        InvMatrix: [],
-        PointSet: {},
-      }, { headers: { Deviceid: system.currentDevice } }).then(() => {
+    if (props.flag === 'add') {
+      axios.put(`https://${system.globalConfig.serverUrl}/api/BallCamera/Register`, {
+        Type: typeV.value,
+        Name: name.value,
+        Ip: ip.value,
+        Port: port.value,
+        User: user.value,
+        Password: password.value,
+        RtspUrl: rtspUrl.value || rtspUrlCom.value,
+        Channel: 1,
+      }, { headers: { Deviceid: system.currentDevice } }).then((res) => {
+        axios.post(`https://${system.globalConfig.serverUrl}/api/BallCamera/Matrix?CameraId=${res.data.data.BallCameraId}`, {
+          P_Start: 0.0,
+          P_Max: 360,
+          p_Positive_Direction: '+',
+          T_Start: 0.0,
+          T_Max: 180,
+          T_Positive_Direction: '+',
+          Matrix: [],
+          InvMatrix: [],
+          PointSet: {},
+        }, { headers: { Deviceid: system.currentDevice } }).then(() => {
+          loading.value = false
+          dialog.value = false
+          system.msg('添加成功')
+          system.getBallCameraList()
+        })
+      })
+    }
+    else {
+      axios.post(`https://${system.globalConfig.serverUrl}/api/BallCamera?CameraId=${props.cameraId}`, {
+        Type: typeV,
+        Name: name,
+        Ip: ip,
+        Port: port,
+        User: user,
+        Password: password,
+        RtspUrl: rtspUrl,
+        Channel: 1,
+      }).then(() => {
         loading.value = false
         dialog.value = false
         system.getBallCameraList()
       })
-    })
+    }
   }
   else {
     loading.value = false
   }
 }
+
+watch(dialog, (newV) => {
+  if (newV) {
+    if (props.flag === 'edit') {
+      name.value = system.ballCameraInfo?.Name as string
+      ip.value = system.ballCameraInfo?.Ip as string
+      typeV.value = system.ballCameraInfo?.Type as string
+      port.value = system.ballCameraInfo?.Port as number
+      user.value = system.ballCameraInfo?.User as string
+      password.value = system.ballCameraInfo?.Password as string
+      rtspUrl.value = system.ballCameraInfo?.RtspUrl as string
+    }
+  }
+})
 </script>
 
 <template>
@@ -130,20 +171,19 @@ async function submit(event: any) {
     </template>
 
     <template #default="{ isActive }">
-      <v-card prepend-icon="mdi-plus" title="添加球机">
+      <v-card prepend-icon="mdi-plus" :title="flag === 'add' ? '添加球机' : '修改球机'">
         <v-card-item>
           <v-form validate-on="submit lazy" :disabled="!system.globalConfig.serverUrl" @submit.prevent="submit">
             <v-select
-              v-model="typeV" :items="typeVOptions"
-              :rules="rules.typeV" item-title="label" item-value="value" label="球机类型"
+              v-model="typeV" :items="typeVOptions" :rules="rules.typeV" item-title="label" item-value="value"
+              label="球机类型"
             />
             <v-text-field v-model="name" :rules="rules.name" label="球机名称" />
 
             <v-text-field v-model="ip" :rules="rules.ip" label="球机 IP" />
             <v-number-input
-              v-model="port"
-              :rules="rules.port" :reverse="false"
-              control-variant="stacked" label="球机私有协议端口" :hide-input="false" inset
+              v-model="port" :rules="rules.port" :reverse="false" control-variant="stacked"
+              label="球机私有协议端口" :hide-input="false" inset
             />
             <v-text-field v-model="user" :rules="rules.user" label="SDK 用户名" />
             <v-text-field v-model="password" :rules="rules.password" label="SDK 密码" />

+ 268 - 147
src/components/BallCameraSettings.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import axios from 'axios'
-import { v4 as uuidv4 } from 'uuid';
+import { v4 as uuidv4 } from 'uuid'
 import Viewer from 'viewerjs'
 import 'viewerjs/dist/viewer.css'
 
@@ -8,9 +8,9 @@ const positiveDirectionPOptions = ref([{ label: '+', value: '+' }, { label: '-',
 const positiveDirectionTOptions = ref([{ label: '+', value: '+' }, { label: '-', value: '-' }])
 
 const system = useSystemStore()
-const currentBallCamera = ref('')
 
 const addDialogV = ref(false)
+const updDialogV = ref(false)
 
 const tools = ref([
   {},
@@ -40,9 +40,10 @@ const tools = ref([
   {},
 ])
 
+const isDisabled = ref(true)
 const step = ref(7)
 
-const imgSrc = ref('/assets/image/test.jpg')
+const imgSrc = ref('')
 const imgSrcLoading = ref(true)
 let imgElement: HTMLImageElement | null = null
 const naturalWH = {
@@ -66,96 +67,100 @@ const currentMark = ref<MarkItemType>({
   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);
+const PTXYMap = new Map<string, { P: number, T: number, X: number, Y: number }>()
 
-    // 计算单应性矩阵
-    matrix = findHomography(srcMat, dstMat);
-    console.log(matrix);
+// opencv ==============start
+function calculateInverseMatrix(Matrix: number[]) {
+  // 检查矩阵是否有效
+  if (Matrix.length !== 9) {
+    return []
+  }
 
-    // 清理临时矩阵(OpenCV.js内存管理)
-    srcMat.delete();
-    dstMat.delete();
+  // 提取矩阵元素
+  const a = Matrix[0]
+  const b = Matrix[1]
+  const c = Matrix[2]
+  const d = Matrix[3]
+  const e = Matrix[4]
+  const f = Matrix[5]
+  const g = Matrix[6]
+  const h = Matrix[7]
+  const i = Matrix[8]
+
+  // 计算行列式
+  const det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g)
+
+  // 检查矩阵是否可逆
+  if (det === 0) {
+    return []
   }
 
-  // 应用矩阵到所有点
-  PTXYMap.forEach(function (v: any, key) {
-    if (v.P !== undefined && v.T !== undefined) {
-      matrix2Point(v, matrix);
-    }
-  })
+  // 计算行列式的倒数
+  const invDet = 1.0 / det
+
+  return [
+    (e * i - f * h) * invDet,
+    (c * h - b * i) * invDet,
+    (b * f - c * e) * invDet,
+    (f * g - d * i) * invDet,
+    (a * i - c * g) * invDet,
+    (c * d - a * f) * invDet,
+    (d * h - e * g) * invDet,
+    (b * g - a * h) * invDet,
+    (a * e - b * d) * invDet,
+  ]
 }
 
-// 将点数组转换为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 WarpingPtByHomography(matrix: number[], X: number, Y: number) {
+  let x, y
+  x = matrix[0] * X + matrix[1] * Y + 1.0 * matrix[2]
+  y = matrix[3] * X + matrix[4] * Y + 1.0 * matrix[5]
+  const z = matrix[6] * X + matrix[7] * Y + 1.0 * matrix[8]
 
-// 计算单应性矩阵
-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;
+  x /= z
+  y /= z
+  return { X: x, Y: y }
 }
 
-// 应用矩阵变换到单个点
-function matrix2Point(v:any, matrix:any) {
-  if (!matrix || matrix.empty()) return;
+async function buildMatrix() {
+  if (PTXYMap.size < 4) {
+    system.msg('映射矩阵最少四个')
+    return
+  }
 
-  // 创建输入矩阵(齐次坐标)
-  const src = window.cv.matFromArray(3, 1, window.cv.CV_64FC1, [v.X, v.Y, 1]);
-  const dst = new window.cv.Mat();
+  const PT: any[] = []; const XY: any[] = []
+  PTXYMap.forEach((v: any) => {
+    PT.push(v.P, v.T)
+    XY.push(v.X, v.Y)
+  })
+  // eslint-disable-next-line new-cap
+  const srcPointMat = new window.cv.matFromArray(4, 1, window.cv.CV_64FC2, XY)
+  // eslint-disable-next-line new-cap
+  const dtsPointMat = new window.cv.matFromArray(4, 1, window.cv.CV_64FC2, PT)
 
-  // 执行矩阵变换
-  window.cv.gemm(matrix, src, 1, null, 0, dst);
+  // 计算单应性矩阵
+  const outMatrix = window.cv.findHomography(srcPointMat, dtsPointMat)
+  window.outMatrix = outMatrix.data64F
 
-  // 转换为笛卡尔坐标
-  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;
-  }
+  // 清理临时矩阵(OpenCV.js内存管理)
+  srcPointMat.delete()
+  dtsPointMat.delete()
 
-  // 清理临时矩阵
-  src.delete();
-  dst.delete();
+  // 应用矩阵到所有点
+  PTXYMap.forEach((v: any) => {
+    if (v.P !== undefined && v.T !== undefined) {
+      const out = WarpingPtByHomography(outMatrix.data64F, v.X, v.Y)
+      v.P = out.X
+      v.T = out.Y
+    }
+  })
 }
 
 // opencv =============end
 
-
 function markClick(item: MarkItemType) {
-  console.log(item);
-
+  if (!isDisabled.value)
+    return system.msg('正在装载,请稍后...')
   currentMark.value = item
   colorReset(item.id)
 }
@@ -185,6 +190,7 @@ function colorReset(id: string) {
 function del(id: string) {
   if (!id)
     return
+  PTXYMap.delete(id)
   mark.value = mark.value.filter(v => v.id !== id)
   markClone.value = markClone.value.filter(v => v.id !== id)
   mark.value.length && (currentMark.value = mark.value[mark.value.length - 1])
@@ -202,6 +208,7 @@ function clear() {
     y: 0.0,
     color: '#000',
   }
+  PTXYMap.clear()
 }
 
 /**
@@ -244,13 +251,10 @@ function dbClick(e: MouseEvent) {
   mark.value.push(markV)
   markClone.value.push(markV)
   currentMark.value = mark.value[mark.value.length - 1]
+  PTXYMap.set(currentMark.value.id, { P: 0.0, T: 0.0, X: currentMark.value.x, Y: currentMark.value.y })
   markPointOnImageWithMargin(imgElement as HTMLImageElement)
 }
 
-
-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('请先设置服务器地址')
@@ -264,28 +268,30 @@ function getPTXY(id: string, x: number, y: number) {
     system.msg('请选择球机')
     return
   }
-  axios.get(`https://${system.globalConfig.serverUrl}/api/BallCamera/PtzGet?CameraId=${id}`).then(res => {
+  system.msg('正在装载,请稍后...')
+  isDisabled.value = false
+  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, "")
+        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
       }
     })
+    isDisabled.value = true
+    system.msg('装载完成')
   })
-
 }
 
-const loadOld = () => {
-  if (!system.ballCameraInfo) return
-  console.log(system.ballCameraInfo);
+function loadOld() {
+  if (!system.ballCameraInfo)
+    return
   PTXYMap.clear()
   const ps: any = system.ballCameraInfo.Matrix.PointSet
-  console.log(ps);
 
   for (const key in ps) {
     PTXYMap.set(key, {
@@ -304,33 +310,96 @@ const loadOld = () => {
     })
   }
 
+  if (!mark.value.length)
+    return
   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) => {
+function 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);
+  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=${system.currentBallCamera}&Action=1`, { P: pV, T: tV, Z: 0 }).then((res) => {
+    console.log(res)
   })
 }
 
+function ptByMatrix() {
+  if (!system.globalConfig.serverUrl) {
+    system.msg('请先设置服务器地址')
+    return
+  }
+  const out: any = WarpingPtByHomography(window.outMatrix, currentMark.value.x, currentMark.value.y)
+  PTXYMap.set(currentMark.value.id, { P: out.P, T: out.T, X: currentMark.value.x, Y: currentMark.value.y })
+  mark.value.forEach((v, i) => {
+    if (v.id === currentMark.value.id) {
+      v.p = out.P
+      v.t = out.T
+      markClone.value[i].p = out.P
+      markClone.value[i].t = out.T
+    }
+  })
+}
 
+function updateMatrix(callback?: () => void) {
+  if (!system.globalConfig.serverUrl) {
+    system.msg('请先设置服务器地址')
+    return
+  }
+  if (!system.currentBallCamera) {
+    system.msg('请选择球机')
+    return
+  }
+  const pointSet: any = {}
+  PTXYMap.forEach((v, k) => {
+    pointSet[k] = {
+      P: v.P,
+      T: v.T,
+      X: v.X,
+      Y: v.Y,
+    }
+  })
+  const data = {
+    P_Start: system.positiveConfig.P_Start,
+    P_Max: system.positiveConfig.P_Max,
+    p_Positive_Direction: system.positiveConfig.P_Direction,
+    T_Start: system.positiveConfig.T_Start,
+    T_Max: system.positiveConfig.T_Max,
+    T_Positive_Direction: system.positiveConfig.T_Direction,
+    Matrix: window.outMatrix || system.ballCameraInfo?.Matrix.Matrix || [],
+    InvMatrix: system.ballCameraInfo?.Matrix.InvMatrix || [],
+    PointSet: JSON.stringify(pointSet) === '{}' ? system.ballCameraInfo?.Matrix.PointSet : pointSet,
+  }
+  data.Matrix = Array.isArray(data.Matrix) ? data.Matrix : Array.from(data.Matrix)
+  axios.post(`https://${system.globalConfig.serverUrl}/api/BallCamera/Matrix?CameraId=${system.currentBallCamera}`, data).then(() => {
+    callback && callback()
+  })
+}
 
-let viewer: Viewer
-
-watch(() => system.drawer, () => {
-  console.log(viewer)
-})
+function saveBtn() {
+  if (!window.outMatrix) {
+    system.msg('请先生成映射矩阵')
+    return
+  }
+  const invMatrix = calculateInverseMatrix(window.outMatrix)
+  system.ballCameraInfo!.Matrix.InvMatrix = invMatrix
+  updateMatrix(() => {
+    system.msg('保存成功')
+  })
+}
 
+let viewer: Viewer | null = null
 function initViewer() {
+  if (viewer) {
+    viewer.destroy()
+    viewer = null
+  }
   const updloadImg = document.getElementById('uploadImage') as HTMLImageElement
   viewer = new Viewer(updloadImg, {
     inline: true,
@@ -367,42 +436,68 @@ function initViewer() {
   })
 }
 
-onMounted(() => {
-  initViewer()
+watch(() => system.globalConfig.imgUrl, (file) => {
+  const url = file && URL.createObjectURL(file)
+  if (url) {
+    imgSrc.value = url
+    nextTick(() => {
+      initViewer()
+    })
+  }
 })
+
+watch(() => system.positiveConfig, () => {
+  updateMatrix()
+}, { deep: true })
 </script>
 
 <template>
   <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="system.currentBallCamera" :items="system.ballCameraList" label="球机列表" item-title="name"
+          item-value="id" @update:model-value="system.getBallCameraInfo"
+        >
           <template #append>
-            <add-ball v-model:dialog="addDialogV">
+            <add-ball v-model:dialog="addDialogV" flag="add">
+              <template #activator="{ props: activatorProps }">
+                <v-btn
+                  v-tooltip:top="'添加球机'" size="large" rounded="0" variant="flat" icon="mdi-plus-thick"
+                  v-bind="activatorProps"
+                />
+              </template>
+            </add-ball>
+            <add-ball v-model:dialog="updDialogV" flag="edit" :camera-id="system.currentBallCamera">
               <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-pencil"
+                  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 }">
                 <v-card title="警告" color="#f87171">
                   <v-card-text>
-                    确定删除吗?确定删除吗?
+                    确定删除当前球机吗?
                   </v-card-text>
 
                   <v-card-actions>
                     <v-spacer />
 
-                    <v-btn text="确定" border @click="isActive.value = false" />
+                    <v-btn text="确定" border @click="() => { system.delBallCamera(system.currentBallCamera);isActive.value = false }" />
                   </v-card-actions>
                 </v-card>
               </template>
@@ -412,62 +507,80 @@ 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-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-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_Max" 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_Direction" :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_Direction" :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="system.canvasVideoLoading" title="画面预览"
-            :subtitle="`当前P ${system.PT.p},当前T ${system.PT.t},当前标定点位的个数0`" text="">
+          <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,
+              <v-icon
+                icon="mdi-plus" color="#ef4444" class="absolute left-50% top-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 :disabled="system.canvasVideoLoading" block variant="tonal"
-                @click="system.playBallCamera(currentBallCamera)">
+              <v-btn
+                :disabled="system.canvasVideoLoading" block variant="tonal"
+                @click="system.playBallCamera(system.currentBallCamera)"
+              >
                 播放
               </v-btn>
             </v-card-actions>
@@ -477,9 +590,11 @@ 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"
-                @mousedown="system.ballMove(currentBallCamera, { Speed: step, Direction: tool.value })"
-                @mouseup="system.ballStop(currentBallCamera, { Direction: tool.value })">
+              <v-btn
+                v-else width="60" height="40" variant="tonal" color="blue-darken-2"
+                @mousedown="system.ballMove(system.currentBallCamera, { Speed: step, Direction: tool.value })"
+                @mouseup="system.ballStop(system.currentBallCamera, { Direction: tool.value })"
+              >
                 {{ tool.icon }}
               </v-btn>
             </div>
@@ -487,14 +602,18 @@ 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"
-                @mousedown="system.ballMove(currentBallCamera, { Speed: step, Direction: 9 })"
-                @mouseup="system.ballStop(currentBallCamera, { Direction: 9 })">
+              <v-btn
+                variant="tonal" color="blue-darken-2"
+                @mousedown="system.ballMove(system.currentBallCamera, { Speed: step, Direction: 9 })"
+                @mouseup="system.ballStop(system.currentBallCamera, { Direction: 9 })"
+              >
                 🔍+
               </v-btn>
-              <v-btn variant="tonal" color="blue-darken-2"
-                @mousedown="system.ballMove(currentBallCamera, { Speed: step, Direction: 10 })"
-                @mouseup="system.ballStop(currentBallCamera, { Direction: 10 })">
+              <v-btn
+                variant="tonal" color="blue-darken-2"
+                @mousedown="system.ballMove(system.currentBallCamera, { Speed: step, Direction: 10 })"
+                @mouseup="system.ballStop(system.currentBallCamera, { Direction: 10 })"
+              >
                 🔎-
               </v-btn>
             </div>
@@ -520,13 +639,15 @@ 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>
@@ -537,19 +658,19 @@ onMounted(() => {
             <v-card-text>
               <div class="flex flex-col items-center gap-2">
                 <div>
-                  <v-btn color="blue">
+                  <v-btn color="blue" @click="saveBtn">
                     保存
                   </v-btn>
                 </div>
                 <v-row justify="center">
                   <v-col>
-                    <v-btn width="100%" @click="getPTXY(currentBallCamera, currentMark.x, currentMark.y)">
+                    <v-btn width="100%" @click="getPTXY(system.currentBallCamera, currentMark.x, currentMark.y)">
                       当前PT位置装载PT
                     </v-btn>
                   </v-col>
 
                   <v-col>
-                    <v-btn width="100%">
+                    <v-btn width="100%" @click="ptByMatrix">
                       根据映射矩阵装载PT
                     </v-btn>
                   </v-col>

+ 2 - 2
src/components/HeaderTool.vue

@@ -30,9 +30,9 @@ async function toggleLocales() {
       <div i="carbon-sun dark:carbon-moon" />
     </button>
 
-    <a icon-btn :title="t('button.toggle_langs')" @click="toggleLocales()">
+    <!-- <a icon-btn :title="t('button.toggle_langs')" @click="toggleLocales()">
       <div i-carbon-language />
-    </a>
+    </a> -->
 
     <!-- <RouterLink icon-btn to="/about" :title="t('button.about')" data-test-id="about">
       <div i-carbon-dicom-overlay />

+ 1 - 1
src/layouts/home.vue

@@ -8,7 +8,7 @@ const theme = computed(() => isDark.value ? 'dark' : 'light')
       <v-app-bar-title>CAMERA-MODULE-TOOL</v-app-bar-title>
       <HeaderTool />
     </v-app-bar>
-    <v-main>
+    <v-main scrollable>
       <v-container fluid>
         <RouterView />
       </v-container>

+ 2 - 6
src/pages/index.vue

@@ -20,7 +20,7 @@ const globalConfig = reactive<{
   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 = '敬请期待。。。'
 
 const currentItem = ref(3)
 const tabItems = ref([
@@ -29,10 +29,6 @@ const tabItems = ref([
   { id: 3, label: '球机设置' },
 ])
 
-watch(() => globalConfig.imgUrl, (val) => {
-  console.log(val)
-}, { deep: true })
-
 function applyFn() {
   system.setGlobalConfig(globalConfig)
   system.saveGlobalConfig(() => { system.drawer = false })
@@ -44,7 +40,7 @@ useHead({
 </script>
 
 <template>
-  <v-navigation-drawer v-model="system.drawer" location="right" :width="300">
+  <v-navigation-drawer v-model="system.drawer" location="right" :width="300" temporary>
     <div class="p-4 pb-0">
       <span class="text-5 font-700">全局配置</span>
     </div>

+ 2 - 1
src/shims.d.ts

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

+ 51 - 22
src/stores/system.ts

@@ -2,7 +2,7 @@ import axios from 'axios'
 import useWorker from 'omnimatrix-video-player'
 import { acceptHMRUpdate, defineStore } from 'pinia'
 
-axios.defaults.timeout = 3000
+axios.defaults.timeout = 15000
 axios.interceptors.response.use((response) => {
   return response
 }, (error) => {
@@ -71,6 +71,7 @@ export const useSystemStore = defineStore('system', () => {
     T_Max: 0.0,
   })
   const currentDevice = ref()
+  const currentBallCamera = ref('')
   const canvasVideoLoading = ref(false)
   const snackbar = ref(false)
   const snackbarText = ref('')
@@ -96,8 +97,12 @@ export const useSystemStore = defineStore('system', () => {
     }
   }
 
-  function getBallCameraList() {
-    if (globalConfig.serverUrl) {
+  function getBallCameraList(): Promise<IOptions[]> | undefined {
+    if (!globalConfig.serverUrl) {
+      msg('请先设置服务器地址')
+      return
+    }
+    return new Promise((resolve) => {
       axios.get(`https://${globalConfig.serverUrl}/api/BallCamera/List`, { headers: { Deviceid: currentDevice.value } }).then((res) => {
         ballCameraList.value = []
         Object.keys(res.data.data).forEach((key) => {
@@ -106,8 +111,9 @@ export const useSystemStore = defineStore('system', () => {
             name: res.data.data[key],
           })
         })
+        resolve(ballCameraList.value)
       })
-    }
+    })
   }
 
   function getVersion() {
@@ -189,31 +195,35 @@ export const useSystemStore = defineStore('system', () => {
     }
   }
 
-
   // 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 (direction === '+') {
+      if (method === 'inv') {
         if (value > (max - startV)) {
-          return value - (max - startV);
-        } else {
-          return startV + value; // 映射
+          return value - (max - startV)
+        }
+        else {
+          return startV + value // 映射
         }
-      } else {
+      }
+      else {
         if (value > startV) {
-          return (value - startV);
-        } else {
-          return (max - startV) + value; // 映射
+          return (value - startV)
+        }
+        else {
+          return (max - startV) + value // 映射
         }
       }
-    } else {
+    }
+    else {
       if (value > startV) {
-        return startV + max - value;
-      } else {
-        return startV - value;
+        return startV + max - value
+      }
+      else {
+        return startV - value
       }
     }
   }
@@ -234,9 +244,9 @@ export const useSystemStore = defineStore('system', () => {
       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, "")
+      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)
@@ -255,6 +265,23 @@ export const useSystemStore = defineStore('system', () => {
     axios.put(`https://${globalConfig.serverUrl}/api/BallCamera/Stop?CameraId=${id}`, data)
   }
 
+  function delBallCamera(id: string) {
+    if (!globalConfig.serverUrl) {
+      msg('请先设置服务器地址')
+      return
+    }
+    if (!id) {
+      msg('请选择球机')
+      return
+    }
+    axios.delete(`https://${globalConfig.serverUrl}/api/BallCamera/Delete?CameraId=${id}`).then(() => {
+      getBallCameraList()?.then((res) => {
+        currentBallCamera.value = res[res.length - 1].id
+        getBallCameraInfo(res[res.length - 1].id)
+      })
+      msg('删除成功')
+    })
+  }
 
   onUnmounted(() => {
     workerObj?.close()
@@ -273,7 +300,9 @@ export const useSystemStore = defineStore('system', () => {
     ballCameraInfo,
     getBallCameraInfo,
     getBallCameraList,
+    delBallCamera,
     currentDevice,
+    currentBallCamera,
     playBallCamera,
     canvasVideoLoading,
     snackbar,
@@ -282,7 +311,7 @@ export const useSystemStore = defineStore('system', () => {
     ballMove,
     ballStop,
     PT,
-    mapping
+    mapping,
   }
 })
 

+ 5 - 0
src/styles/main.css

@@ -6,6 +6,7 @@ body,
   height: 100%;
   margin: 0;
   padding: 0;
+  overflow: hidden;
 }
 
 
@@ -13,6 +14,10 @@ html.dark {
   background: #121212;
   color-scheme: dark;
 }
+html.light {
+  background: #ffffff;
+  color-scheme: light;
+}
 
 #nprogress {
   pointer-events: none;

+ 12 - 12
uno.config.ts

@@ -1,13 +1,13 @@
-import {
-  createLocalFontProcessor,
-} from '@unocss/preset-web-fonts/local'
+// import {
+//   createLocalFontProcessor,
+// } from '@unocss/preset-web-fonts/local'
 import {
   defineConfig,
   presetAttributify,
   presetIcons,
   presetTypography,
   presetUno,
-  presetWebFonts,
+  // presetWebFonts,
   transformerDirectives,
   transformerVariantGroup,
 } from 'unocss'
@@ -24,14 +24,14 @@ export default defineConfig({
       scale: 1.2,
     }),
     presetTypography(),
-    presetWebFonts({
-      fonts: {
-        sans: 'DM Sans',
-        serif: 'DM Serif Display',
-        mono: 'DM Mono',
-      },
-      processors: createLocalFontProcessor(),
-    }),
+    // presetWebFonts({
+    //   fonts: {
+    //     sans: 'DM Sans',
+    //     serif: 'DM Serif Display',
+    //     mono: 'DM Mono',
+    //   },
+    //   processors: createLocalFontProcessor(),
+    // }),
   ],
   transformers: [
     transformerDirectives(),

+ 4 - 0
vite.config.ts

@@ -20,11 +20,15 @@ import generateSitemap from 'vite-ssg-sitemap'
 import 'vitest/config'
 
 export default defineConfig({
+  base: '/cmt/',
   resolve: {
     alias: {
       '~/': `${path.resolve(__dirname, 'src')}/`,
     },
   },
+  build: {
+    outDir: 'cmt',
+  },
 
   plugins: [
     basicSsl(),