forked from drone/command-center-dashboard

chenyao
2025-04-19 76eb78c9874ddb6b4153341dce021bc5195426ba
Merge branch 'master' of http://139.196.74.78:10010/r/drone/command-center-dashboard
10 files modified
1 files added
7962 ■■■■ changed files
Dockerfile.prod 7 ●●●●● patch | view | raw | blame | history
docker-compose-prod.yml 2 ●●● patch | view | raw | blame | history
pnpm-lock.yaml 7045 ●●●● patch | view | raw | blame | history
src/components/CurrentTaskDetails/ControlPanel/ControlComPass/ControlComPass.vue 560 ●●●●● patch | view | raw | blame | history
src/components/CurrentTaskDetails/ControlPanel/ControlPanel.vue 97 ●●●●● patch | view | raw | blame | history
src/components/CurrentTaskDetails/CurrentTaskDetails.vue 136 ●●●●● patch | view | raw | blame | history
src/components/CurrentTaskDetails/TaskDetailsHead.vue 12 ●●●●● patch | view | raw | blame | history
src/components/CurrentTaskDetails/TaskDetailsRight.vue 33 ●●●●● patch | view | raw | blame | history
src/const/drc.js 19 ●●●●● patch | view | raw | blame | history
src/hooks/controlDrone/useManualControl.js 4 ●●●● patch | view | raw | blame | history
src/hooks/useDroneWS.js 47 ●●●●● patch | view | raw | blame | history
Dockerfile.prod
@@ -1,17 +1,16 @@
# 使用官方的 Node.js 20.10.0 版本作为基础镜像
FROM node:20-alpine AS build-env
FROM node:20.18.3-alpine AS build-env
# 设置工作目录
WORKDIR /app
# 将当前目录(Vue 项目目录)的内容复制到 /app 目录中
COPY . /app
COPY . .
# 安装项目依赖
RUN rm -rf node_modules package-lock.json && npm install --registry=https://registry.npmmirror.com
# 构建项目
RUN npm run build:dev
RUN npm run build:test
# 查看
RUN ls /app/dist
docker-compose-prod.yml
@@ -1,7 +1,7 @@
version: '3.1'
services:
  piloth5:
    image: 172.21.81.239:7666/drone-web/command-center-dashboard:SNAPSHOT-$BUILD_NUMBER
    image: 172.21.81.239:7666/drone-web-prod/command-center-dashboard:SNAPSHOT-$BUILD_NUMBER
    container_name: command-center-dashboard
    ports:
      - "8711:80"
pnpm-lock.yaml
Diff too large
src/components/CurrentTaskDetails/ControlPanel/ControlComPass/ControlComPass.vue
@@ -1,392 +1,218 @@
<template>
  <div class="instrument-content">
    <div class="left-img" :data-text="`${attitude_pitch}°`">
      <div class="scaleImg">
        <p class="scale" :style="{ top: 45 + ScaleTop + 'px' }"></p>
        <img src="../../../../assets/images/rightmapidentification.png" />
      </div>
    </div>
    <div class="instrument-center">
      <div class="compass-box" :data-text="`${prevRotate?.toFixed(2)}°`">
        <div v-for="(item, index) in str" :key="index" class="scale"
          :style="{ '--rotate': 30 * index - prevRotate + 'deg' }">
          <span class="text">{{ item }}</span>
        </div>
      </div>
      <div class="center-show">
        <img src="../../../../assets/images/mapidentification.png" />
      </div>
      <div class="rotat-btn"></div>
    </div>
    <div class="right-img" :data-text="`${height}m`">
      <div class="ident-arrow">
        <img src="../../../../assets/images/leftmapidentification.png" />
        <div class="arrow-box" :style="{ bottom: realHeight }">
          <div class="arrow"></div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import _ from 'lodash';
export default {
  data () {
    return {
      str: ['W', 30, 33, 'N', 3, 6, 'E', 12, 15, 'S', 21, 24],
      // 俯仰角度数
      attitude_pitch: 0,
      // 真空高度
      height: 0,
      // 旋转方向角度
      prevRotate: 0,
      dockHeight: 0,
    }
  },
  watch: {
    roamPoint: {
      handler (val) {
        // this.prevRotate = val.arrowHeading
        // this.attitude_pitch = val.roll
        // this.height = _.round(val.altitude,1)
      },
    },
  },
  computed: {
    // roamPoint: vuexStateSimplify('pointsWayLine', 'roamPoint'),
    ScaleTop () {
      return (-this.attitude_pitch * 30) / 90
    },
    realHeight () {
            return 0
      // 无人机高度
      const maxHeight = 240
      // 真空高度
      const vacuumHeight = 120
      // 机场高度
      const dockHeightConfig = {
        'e3dea0f5-37f2-4d79-ae58-490af3228069': 14.7,
        '4a574db8-4ad3-48f7-9f16-3edbcd8056e1': 54,
        // 'f47ac10b-58cc-4372-a567-0e02b2c3d479': 81,
      }
      const workspaceId =
        this.$store.state.drone.selectedWorkSpaceId ||
        window.localStorage.getItem('bs_workspace_id')
      const dockHeight = dockHeightConfig?.[workspaceId] || 1
    <div class="instrument-content">
        <div class="left-img">
            <div class="valueBox">{{ props?.options?.pitchAngle || 0 }}°</div>
            <img src="@/assets/images/rightmapidentification.png" alt="" />
            <div class="triangle" :style="pitchAngleStyle"></div>
            <div class="nameBox">俯仰角度</div>
        </div>
      let proportion = 0
      // 处于真空高度和最大高度之间
      if (this.height < maxHeight && this.height >= vacuumHeight) {
        const height = this.height - vacuumHeight
        const proport = height / maxHeight
        proportion = Math.round(proport * 50 + 50)
      }
      // 小于等于真空高度计算
      if (this.height < vacuumHeight && this.height >= dockHeight) {
        const height = this.height - dockHeight
        const copyVacuumHeight = vacuumHeight - dockHeight
        const proport = height / copyVacuumHeight
        proportion = Math.round(proport * 25 + 25)
      }
      // 小于机场高度计算
      if (this.height < dockHeight && this.height >= 0) {
        const proport = this.height / dockHeight
        proportion = Math.round(proport * 25)
      }
      // 大于最大高度计算
      if (this.height >= maxHeight) {
        proportion = 95
      }
      return proportion + '%'
      // const proportion = Math.ceil((this.height / maxHeight) * 100) || 50;
    },
  },
  methods: {
    getAngle (currentLngLat, targetLngLat) {
      const { longitude: lng_a, latitude: lat_a } = currentLngLat
      const { longitude: lng_b, latitude: lat_b } = targetLngLat
      var a = ((90 - lat_b) * Math.PI) / 180
      var b = ((90 - lat_a) * Math.PI) / 180
      var AOC_BOC = ((lng_b - lng_a) * Math.PI) / 180
      var cosc =
        Math.cos(a) * Math.cos(b) +
        Math.sin(a) * Math.sin(b) * Math.cos(AOC_BOC)
      var sinc = Math.sqrt(1 - cosc * cosc)
      var sinA = (Math.sin(a) * Math.sin(AOC_BOC)) / sinc
      var A = (Math.asin(sinA) * 180) / Math.PI
      var res = 0
      if (lng_b > lng_a && lat_b > lat_a) res = A
      else if (lng_b > lng_a && lat_b < lat_a) res = 180 - A
      else if (lng_b < lng_a && lat_b < lat_a) res = 180 - A
      else if (lng_b < lng_a && lat_b > lat_a) res = 360 + A
      else if (lng_b > lng_a && lat_b == lat_a) res = 90
      else if (lng_b < lng_a && lat_b == lat_a) res = 270
      else if (lng_b == lng_a && lat_b > lat_a) res = 0
      else if (lng_b == lng_a && lat_b < lat_a) res = 180
      return res
    },
  },
}
        <div class="instrument-center">
            <div class="compass-box" :style="{ transform: `rotate(${props?.options?.yawAngle || 0}deg)` }">
                <div v-for="(item, index) in str" :key="index" class="scale" :style="{ '--rotate': 30 * index + 'deg' }">
                    <span class="text">{{ item }}</span>
                </div>
            </div>
            <div class="center-show">
                <img src="@/assets/images/mapidentification.png" alt="" />
            </div>
        </div>
        <div class="right-img">
            <div class="valueBox">{{ props.options?.trueAltitude }}m</div>
            <img src="@/assets/images/leftmapidentification.png" alt="" />
            <div class="rightTriangle" :style="trueAltitudeStyle"></div>
            <div class="nameBox">真空高度</div>
        </div>
    </div>
</template>
<script setup>
const str = ['W', 30, 33, 'N', 3, 6, 'E', 12, 15, 'S', 21, 24]
const props = defineProps(['options'])
const pitchAngleStyle = computed(() => {
    const pitchAngle = props?.options?.pitchAngle || 0
    // 将 [-90, 90] 映射到 [0%, 100%]
    const percentage = (((pitchAngle + 90) / 180) * 100).toFixed(2)
    return {
        bottom: `${percentage}%`,
    }
})
const trueAltitudeStyle = computed(() => {
    const trueAltitude = props?.options?.trueAltitude || 0
    // 将 [-240,240] 映射到 [0%, 100%]
    const percentage = (((trueAltitude + 240) / 480) * 100).toFixed(2)
    return {
        bottom: `${percentage}%`,
    }
})
</script>
<style lang="scss" scoped>
.instrument-content {
  height: 100%;
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  position: relative;
  transform: translateY(20px);
    height: 100%;
    width: 100%;
    display: flex;
    gap: 0 30px;
    justify-content: center;
    align-items: center;
    position: relative;
  .left-img,
  .right-img {
    position: relative;
    height: 100%;
    display: flex;
    align-items: center;
    width: 40px;
    .left-img,
    .right-img {
        position: relative;
        width: 9px;
        height: 127px;
    img {
      height: 100px;
    }
  }
        img {
            width: 100%;
            height: 100%;
        }
  .left-img {
    .text {
      position: absolute;
      left: 0px;
      top: 50px;
      width: 60px;
      text-align: center;
    }
        .triangle {
            width: 0px;
            height: 0px;
            margin: auto;
            border: 6px solid transparent;
            border-left-color: #0fff7b;
            position: absolute;
            left: -10px;
    .scaleImg {
      position: relative;
      width: 100%;
      height: 100px;
            transform: translateY(50%);
        }
      img {
        margin-left: 25px;
      }
        .rightTriangle {
            width: 0px;
            height: 0px;
            margin: auto;
            border: 6px solid transparent;
            border-left-color: #0fff7b;
            position: absolute;
            right: -10px;
            transform: translateY(50%) rotate(180deg);
        }
      .scale {
        width: 10px;
        height: 10px;
        background-color: #1fa3f6;
        position: absolute;
        margin-left: 10px;
        .valueBox {
            position: absolute;
            top: -20px;
            left: 50%;
            transform: translateX(-50%);
        }
        &::before {
          content: '';
          position: absolute;
          width: 0;
          height: 0;
          top: 0px;
          left: 10px;
          border-top: solid 5px transparent;
          border-left: solid 5px #1fa3f6;
          border-bottom: solid 5px transparent;
        }
      }
    }
        .nameBox {
            position: absolute;
            bottom: -20px;
            left: 50%;
            width: auto;
            transform: translateX(-50%);
            font-family: Segoe UI, Segoe UI;
            font-weight: 400;
            font-size: 12px;
            white-space: nowrap;
            color: #ffffff;
        }
    }
    &::before {
      content: attr(data-text);
      position: absolute;
      right: 0px;
      top: 50px;
      font-size: 14px;
      font-weight: bolder;
      font-family: none;
    }
    .instrument-center {
        position: relative;
    &::after {
      content: '俯仰角度';
      position: absolute;
      right: 0px;
      text-align: right;
      bottom: 30px;
      font-size: 14px;
      font-weight: bolder;
      font-family: none;
    }
  }
        .compass-box {
            width: 180px;
            height: 180px;
            border-radius: 50%;
            position: relative;
            border: 30px solid rgba($color: #323931, $alpha: 0.5);
            box-shadow: 0 2px 12px 0 #158aff;
            user-select: none;
  .right-img {
    &::before {
      content: attr(data-text);
      position: absolute;
      left: 0px;
      top: 50px;
      font-size: 14px;
      font-weight: bolder;
      font-family: none;
    }
            .scale {
                width: 135%;
                position: absolute;
                top: 50%;
                left: 50%;
                font-weight: bold;
                color: #c1c3c4;
                text-align: left;
                transform: translate(-50%, -50%) rotate(var(--rotate));
    &::after {
      content: '真空高度';
      position: absolute;
      left: 0px;
      bottom: 30px;
      font-size: 14px;
      font-weight: bolder;
      font-family: none;
    }
                &:nth-child(3n - 2) {
                    color: #fff;
                    font-weight: bolder;
                    font-size: 20px;
                }
            }
    .ident-arrow {
      position: relative;
            .scale {
                .text {
                    display: inline-block;
                    // rotate: -90deg;
                    transform: rotate(-90deg);
                    -ms-transform: rotate(-90deg);
                    -moz-transform: rotate(-90deg);
                    -webkit-transform: rotate(-90deg);
                    -o-transform: rotate(-90deg);
                }
            }
        }
      .arrow-box {
        position: absolute;
        bottom: 0;
        left: 20px;
        .rotat-btn {
            width: 16px;
            height: 16px;
            background-color: rgba($color: #1fa3f6, $alpha: 1);
            position: absolute;
            top: 10px;
            left: 50%;
            transform: translateX(-50%);
        .arrow {
          position: relative;
          width: 10px;
          height: 10px;
          background-color: #1fa3f6;
            &::before {
                content: '';
                display: block;
                width: 0;
                height: 0;
                border-left: 8px solid transparent;
                border-right: 8px solid transparent;
                border-bottom: 8px solid #1fa3f6;
                position: absolute;
                bottom: 100%;
            }
        }
          &::before {
            content: '';
            position: absolute;
            left: -9px;
            top: 0;
        .center-show {
            width: 30px;
            height: 40px;
            position: absolute;
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
            border: 5px solid transparent {
              right: solid 5px #1fa3f6;
            }
          }
        }
      }
    }
  }
            img {
                width: 100%;
                height: 100%;
                transform: rotate(var(--rotate));
                transition: all 0.5s linear;
            }
        }
  .instrument-center {
    position: relative;
        &::after {
            content: '';
            position: absolute;
            width: 40px;
            top: 50%;
            left: 0;
            z-index: 99;
        }
    .compass-box {
      width: 180px;
      height: 180px;
      border-radius: 50%;
      position: relative;
      border: 30px solid rgba($color: #323931, $alpha: 0.5);
      box-shadow: 0 2px 12px 0 #158aff;
      user-select: none;
      transform: none !important;
      .scale {
        width: 135%;
        position: absolute;
        top: 50%;
        left: 50%;
        font-weight: bold;
        color: #c1c3c4;
        text-align: left;
        transform: translate(-50%, -50%) rotate(var(--rotate));
        &:nth-child(3n - 2) {
          color: #fff;
          font-weight: bolder;
          font-size: 20px;
        }
      }
      .scale {
        .text {
          display: inline-block;
          // rotate: -90deg;
          transform: rotate(-90deg);
          -ms-transform: rotate(-90deg);
          -moz-transform: rotate(-90deg);
          -webkit-transform: rotate(-90deg);
          -o-transform: rotate(-90deg);
        }
      }
      &::before {
        content: '';
        position: absolute;
        top: -45px;
        left: 50%;
        transform: translateX(-50%);
        width: 7px;
        height: 15px;
        background: #00ee8b;
        margin: 0 auto;
        box-shadow: 0 0 4px rgba(0, 0, 0, 0.5), -1px -1px 0 rgba(0, 0, 0, 0.5),
          1px -1px 0 rgba(0, 0, 0, 0.5), -1px 1px 0 rgba(0, 0, 0, 0.5),
          1px 1px 0 rgba(0, 0, 0, 0.5);
      }
      &::after {
        content: attr(data-text);
        position: absolute;
        top: -60px;
        font-size: 16px;
        line-height: 16px;
        font-weight: 600;
        color: #00ee8b;
        left: 50%;
        transform: translateX(-50%);
      }
    }
    .rotat-btn {
      width: 16px;
      height: 16px;
      background-color: rgba($color: #1fa3f6, $alpha: 1);
      position: absolute;
      top: 10px;
      left: 50%;
      transform: translateX(-50%);
      &::before {
        content: '';
        display: block;
        width: 0;
        height: 0;
        border-left: 8px solid transparent;
        border-right: 8px solid transparent;
        border-bottom: 8px solid #1fa3f6;
        position: absolute;
        bottom: 100%;
      }
    }
    .center-show {
      width: 30px;
      height: 40px;
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      img {
        width: 100%;
        height: 100%;
        transform: rotate(var(--rotate));
        transition: all 0.5s linear;
      }
    }
    &::after {
      content: '';
      position: absolute;
      width: 40px;
      top: 50%;
      left: 0;
      z-index: 99;
    }
    &::before {
      content: '';
      position: absolute;
      width: 40px;
      top: 50%;
      right: 0;
      z-index: 99;
    }
  }
        &::before {
            content: '';
            position: absolute;
            width: 40px;
            top: 50%;
            right: 0;
            z-index: 99;
        }
    }
}
</style>
src/components/CurrentTaskDetails/ControlPanel/ControlPanel.vue
@@ -68,7 +68,7 @@
        </div>
        <!--     指南针-->
        <div class="compass">
            <ControlComPass />
            <ControlComPass :options="compassOptions" />
        </div>
        <div class="ptzControlBox">
@@ -116,10 +116,10 @@
            </div>
            <div class="divider"></div>
            <div v-for="arr in list4" class="info">
            <div v-for="arr in baseInfo" class="info">
                <div v-for="item in arr" class="infoItem">
                    <div class="infoName">{{ item.name }}</div>
                    <div class="infoValue">{{ item.value }}</div>
                    <div class="infoValue">{{ item.value + (item.unit || '') }}</div>
                </div>
            </div>
        </div>
@@ -147,15 +147,26 @@
import _ from 'lodash'
import BaseControl from '@/components/CurrentTaskDetails/ControlPanel/BaseControl.vue'
import EventBus from '@/event-bus'
import { getPayloadControlApi, ptzControlApi } from '@/api/payload'
import { getPayloadControlApi } from '@/api/payload'
import { directionMap } from '@/const/drc'
const deviceOsdInfo = inject('deviceOsdInfo')
const host = computed(() => deviceOsdInfo?.value?.data?.host || {})
const wsInfo = inject('wsInfo')
const device_osd_host = computed(() => wsInfo?.value?.device_osd?.data?.host || {})
const dock_osd_host = computed(() => wsInfo?.value?.dock_osd?.data?.host || {})
const taskDetails = inject('taskDetails')
const dockSn = inject('dockSn')
const droneSn = inject('droneSn')
const store = useStore()
const trueAltitude = inject('trueAltitude')
const compassOptions = computed(() => {
    return {
        pitchAngle: pitchAngle.value.angle,
        trueAltitude: trueAltitude.value,
        yawAngle: yawAngle.value.angle,
    }
})
let mqttState = null
const client_id = ref('')
const valueTime = ref('00:00:00')
@@ -184,24 +195,26 @@
    { name: '左', key: KeyCode.ARROW_LEFT, operate: 'left', style: { left: '-70%' } },
]
const list4 = computed(() => {
    const { longitude, latitude, height, payloads } = host?.value || {}
    const { gimbal_pitch } = payloads?.[0] || {} //俯仰角度
const baseInfo = computed(() => {
    const usedStorage = dock_osd_host.value?.storage?.used || 0
    const zoom_factor = device_osd_host.value?.cameras?.[0]?.zoom_factor || 0
    const usedStorageGB = _.round(usedStorage / 1024 / 1024, 2)
    return [
        [
            { name: '焦距倍数', value: '0' },
            { name: '俯仰角度', value: pitchAngle.value.angle },
            { name: '横向角度', value: yawAngle.value.angle },
            { name: '焦距倍数', value: zoom_factor },
            { name: '俯仰角度', value: pitchAngle.value.angle, unit: '°' },
            { name: '横向角度', value: yawAngle.value.angle, unit: '°' },
        ],
        [
            { name: '储存', value: '64.5G' },
            { name: '储存', value: usedStorageGB, unit: 'G' },
            { name: '方向', value: pitchAngle.value.direction },
            { name: '方向', value: yawAngle.value.direction },
        ],
    ]
})
const pitchAngle = computed(() => {
    const { longitude, latitude, height, payloads } = host?.value || {}
    const { payloads } = device_osd_host?.value || {}
    const gimbal_pitch = payloads?.[0]?.gimbal_pitch || 0
    let direction = ''
    if (gimbal_pitch > -2 && gimbal_pitch < 2) {
@@ -216,14 +229,13 @@
        direction = '正下'
    }
    return {
        angle: _.round(gimbal_pitch || 0, 1) + '°',
        angle: _.round(gimbal_pitch || 0, 1),
        direction,
    }
})
const yawAngle = computed(() => {
    let { longitude, latitude, height, payloads, attitude_head } = host?.value || {}
    const gimbal_pitch = payloads?.[0]?.gimbal_pitch || 0
    let { payloads, attitude_head } = device_osd_host?.value || {}
    const gimbal_yaw = payloads?.[0]?.gimbal_yaw || 0
    attitude_head = attitude_head || 0
    let yaw = ''
@@ -242,45 +254,16 @@
    if ((yaw > -2 && yaw < 2) || parseInt(attitude_head) === parseInt(gimbal_yaw)) {
        result = attitude_head < 0 ? 180 + (180 + attitude_head) : attitude_head
    }
    let direction = ''
    const roundResult = Math.round(result)
    if (roundResult === 0) {
        direction = '正北'
    } else if (roundResult > 0 && roundResult < 45) {
        direction = '北偏东'
    } else if (roundResult === 45) {
        direction = '东北'
    } else if (roundResult > 45 && roundResult < 90) {
        direction = '北偏东'
    } else if (roundResult === 90) {
        direction = '正东'
    } else if (roundResult > 90 && roundResult < 135) {
        direction = '东偏南'
    } else if (roundResult === 135) {
        direction = '东南'
    } else if (roundResult > 135 && roundResult < 180) {
        direction = '南偏东'
    } else if (roundResult === 180) {
        direction = '正南'
    } else if (roundResult > 180 && roundResult < 225) {
        direction = '南偏西'
    } else if (roundResult === 225) {
        direction = '西南'
    } else if (roundResult > 225 && roundResult < 270) {
        direction = '西偏南'
    } else if (roundResult === 270) {
        direction = '正西'
    } else if (roundResult > 270 && roundResult < 315) {
        direction = '西偏北'
    } else if (roundResult === 315) {
        direction = '西北'
    } else if (roundResult > 315 && roundResult < 360) {
        direction = '北偏西'
    } else if (roundResult === 360) {
        direction = '正北'
    const roundResult = Math.round(result);
    let direction = '';
    for (const item of directionMap) {
        if (roundResult >= item.min && roundResult <= item.max) {
            direction = item.value;
            break;
        }
    }
    return {
        angle: _.round(result, 1) + '°',
        angle: _.round(result, 1),
        direction,
    }
})
@@ -398,7 +381,7 @@
// 返航或取消返航
const returnOrCancelReturn = () => {
    if (deviceOsdInfo.value?.data?.host?.mode_code === 9) {
    if (device_osd_host?.value?.mode_code === 9) {
        cancelBackDock()
    } else {
        onBackDock()
@@ -415,7 +398,7 @@
watch(
    () => workspace_id.value,
    async () => {
        if (workspace_id.value) {
        if (workspace_id.value && mqttState === null && client_id.value === '') {
            await createConnect()
            // 使用控制
            manualControl = useManualControl(mqttState, deviceTopicInfo.value, flightController, paramsRef)
src/components/CurrentTaskDetails/CurrentTaskDetails.vue
@@ -17,12 +17,12 @@
            <RealTimeMap :class="`${isMaxMap ? 'maxBox' : 'minBox'}`" />
            <TaskDetailsRight v-if="isAutoControl" />
            <template v-else>
            </template>
            <TaskDetailsHead />
                <TaskDetailsHead />
                <TaskDetailsLeft />
            </template>
            <!--    控制面板,里面有方法需要立即执行,不可用v-if        -->
            <!--            <ControlPanel />-->
            <ControlPanel v-show="!isAutoControl" />
            <img alt="" :src="amplifyImg" class="amplify" @click="isMaxMap = !isMaxMap" />
        </div>
@@ -36,9 +36,6 @@
import { getJobDetails } from '@/api/home/task'
import RealTimeMap from '@/components/CurrentTaskDetails/RealTimeMap.vue'
import { getWebsocketUrl } from '@/websocket/util/config'
import { useConnectWebSocket } from '@/utils/websocket/connect-websocket'
import { EBizCode } from '@/utils/staticData/enums'
import ControlPanel from '@/components/CurrentTaskDetails/ControlPanel/ControlPanel.vue'
import TaskDetailsHead from '@/components/CurrentTaskDetails/TaskDetailsHead.vue'
import TaskDetailsLeft from '@/components/CurrentTaskDetails/TaskDetailsLeft.vue'
@@ -49,37 +46,47 @@
import { updateDroneQualityApi } from '@/api/drc'
import { getLiveAiLinkApi } from '@/api/payload'
import { CURRENT_CONFIG } from '@/utils/http/config'
import { useDroneWS } from '@/hooks/useDroneWS'
const isAutoControl = ref(true)
const isAutoControl = ref(true) //是否自动控制
const lineQuality = ref(1) //1流畅,2标清
provide('isAutoControl', isAutoControl)
provide('lineQuality', lineQuality)
const taskDetailsViewer = ref(null)
provide('taskDetailsViewer', taskDetailsViewer)
let taskDetails = ref({})
provide('taskDetails', taskDetails)
const deviceOsdInfo = ref({})
provide('deviceOsdInfo', deviceOsdInfo)
const taskDetailsViewer = ref(null) //地图实例
let taskDetails = ref({}) //任务详情
const deviceOsdInfo = computed(() => wsInfo.value?.device_osd)
const dockSn = computed(() => taskDetails?.value?.device_sns?.[0])
const droneSn = computed(() => deviceOsdInfo?.value?.data?.sn)
const trueAltitude = ref('') // 真实高度
const isAiLive = ref(false) // 是ai直播
const video_id = ref('') // 直播视频id
const isShow = defineModel('show') // 是否显示当前任务详情
const props = defineProps(['id'])
const currentLiveUrl = ref('') // 当前直播地址
const isTakeOff = ref(false) // 是在飞行中
const isMaxMap = ref(false) //是大地图
const workspace_id = ref('')
let wsInfo = useDroneWS(workspace_id) //ws信息,是一个ref对象
watch(wsInfo, () => {
    // wsInfo 变化触发
    setCurrentLiveUrl()
}, { deep: true })
provide('wsInfo', wsInfo)
provide('isAutoControl', isAutoControl)
provide('lineQuality', lineQuality)
provide('taskDetailsViewer', taskDetailsViewer)
provide('taskDetails', taskDetails)
provide('deviceOsdInfo', deviceOsdInfo)
provide('dockOsdInfo', wsInfo?.value?.dock_osd)
provide('dockSn', dockSn)
provide('droneSn', droneSn)
const isAiLive = ref(false)
provide('trueAltitude', trueAltitude)
provide('isAiLive', isAiLive)
const isShow = defineModel('show')
const props = defineProps(['id'])
const currentLiveUrl = ref('')
const isTakeOff = ref(false)
const isMaxMap = ref(false)
let droneWebSocket //WS实例
const video_id = ref('')
provide('video_id', video_id)
// 获取机巢直播
const getDeviceLiveUrl = async () => {
@@ -87,9 +94,13 @@
    currentLiveUrl.value = res.data.data.rtcs_url
}
const getAiLiveUrl = ()=>{
    const res = getLiveAiLinkApi({ original_stream_url: `${CURRENT_CONFIG.rtmpURL}${video_id.value}`, video_id:video_id.value })
const getAiLiveUrl = async () => {
    const res = await getLiveAiLinkApi({
        original_stream_url: `${CURRENT_CONFIG.rtmpURL}${video_id.value.replace(/\//g, '-')}`,
        video_id: video_id.value,
    })
    currentLiveUrl.value = res.data.data.rtcs_url
    ElMessage.success('开启成功')
    isAiLive.value = true
}
@@ -109,15 +120,14 @@
// 设置当前直播地址
const setCurrentLiveUrl = async () => {
    const data = deviceOsdInfo.value?.data
    const deviceInfo = data?.host
    const currentIsTakeOff = ![14, 0].includes(deviceInfo.mode_code)
    const deviceInfo = deviceOsdInfo.value?.data?.host
  if (!deviceInfo) return
    const currentIsTakeOff = ![14, 0].includes(deviceInfo?.mode_code)
    // 如果还是之前的状态,不切换
    if (isTakeOff.value === currentIsTakeOff) return
    isTakeOff.value = currentIsTakeOff
    isTakeOff.value ? await getDroneLiveUrl() : await getDeviceLiveUrl()
}
// 获取任务详情获取航线文件
const getTaskDetails = () => {
    if (!props.id) ElMessage.warning('请检查是否传入id')
@@ -125,59 +135,8 @@
        taskDetails.value = res.data.data
        await getDeviceLiveUrl()
        taskDetails.value.workspace_id = taskDetails.value.way_lines[0]?.workspace_id
        createWsConnect()
        workspace_id.value = taskDetails.value.workspace_id
    })
}
const dockOsdInfo = ref({})
provide('dockOsdInfo', dockOsdInfo)
const wsInfo = ref({})
provide('wsInfo', wsInfo)
// websocket 的消息回调
const messageHandler = result => {
    let payload = JSON.parse(result)
    wsInfo.value[payload.biz_code] = payload
    switch (payload.biz_code) {
        // 无人机
        case EBizCode.DeviceOsd: {
            deviceOsdInfo.value = payload
            setCurrentLiveUrl()
            console.log(payload, 'DeviceOsd--信息')
            break
        }
        // 遥控器
        case EBizCode.GatewayOsd: {
            console.log(payload, 'GatewayOsd--信息')
            break
        }
        // 机巢
        case EBizCode.DockOsd: {
            console.log(payload, 'DockOsd--信息')
            break
        }
        // PsdkWidgetValues
        case EBizCode.PsdkWidgetValues: {
            console.log(payload, 'PsdkWidgetValues--信息')
            break
        }
        // VideoSurveillance
        case EBizCode.VideoSurveillance: {
            console.log(payload, 'VideoSurveillance--信息')
            break
        }
        default:
            break
    }
}
// 创建ws连接
const createWsConnect = () => {
    const workspaceId = taskDetails.value.workspace_id
    if (!workspaceId) return
    let webSocketUrl = getWebsocketUrl() + '&workspace-id=' + workspaceId
    // 监听ws 消息
    droneWebSocket = useConnectWebSocket(messageHandler, webSocketUrl)
}
onMounted(() => {
@@ -188,9 +147,6 @@
})
onBeforeUnmount(() => {
    droneWebSocket?.close()
    deviceOsdInfo.value = {}
    droneWebSocket = null
    EventBus.off('CurrentTaskDetails-timeStop', changeLineQuality)
    EventBus.off('CurrentTaskDetails-getAiLiveUrl', getAiLiveUrl)
    EventBus.off('CurrentTaskDetails-getDroneLiveUrl', getDroneLiveUrl)
src/components/CurrentTaskDetails/TaskDetailsHead.vue
@@ -1,6 +1,6 @@
<template>
    <div class="detailsHead">
        <div class="droneName" :title="taskDetails.name">{{ taskDetails.name }}</div>
        <div class="droneName" :title="taskDetails.device_names">{{ taskDetails.device_names }}</div>
        <div class="infoListBox">
            <div v-for="item in infoList">
                <div class="infoValue" :title="item.value">{{ item.value }}{{ item.unit }}</div>
@@ -61,7 +61,7 @@
    }
}
function refreshLive(){
function refreshLive() {
    EventBus.emit('CurrentTaskDetails-getDroneLiveUrl')
}
@@ -72,6 +72,9 @@
    })
}
const trueAltitude = inject('trueAltitude')
// 获取真实高度
function getRealTimeReallyHigh() {
    if (!taskDetailsViewer?.value) return
    const device_osd_host = wsInfo?.value?.device_osd?.data?.host || {}
@@ -79,7 +82,8 @@
    if (!latitude) return
    getLnglatAltitude(longitude, latitude, taskDetailsViewer.value).then(res => {
        const last = height - res?.height
        infoList.value[0].value = last ? infoList.value[0].value : _.round(height - res?.height, 1)
        infoList.value[0].value = last ? _.round(height - res?.height, 1) : infoList.value[0].value
        trueAltitude.value = infoList.value[0].value
    })
}
@@ -88,7 +92,6 @@
    const dock_osd_host = wsInfo?.value?.dock_osd?.data?.host || {}
    const { longitude, latitude, height, horizontal_speed, vertical_speed, wind_speed, battery } = device_osd_host
    const { longitude: dockLon, latitude: dockLat, wireless_link } = dock_osd_host
    let dist = infoList.value[11].value
    if (longitude && latitude && dockLon && dockLat) {
        dist = _.round(getLnglatDist(longitude, latitude, dockLon, dockLat), 0)
@@ -187,7 +190,6 @@
                color: #ffffff;
                line-height: 18px;
                margin-bottom: 10px;
            }
            .infoTitle {
src/components/CurrentTaskDetails/TaskDetailsRight.vue
@@ -1,6 +1,6 @@
<template>
    <div class="task-details-right-container">
        <div  class="titleImg">
        <div class="titleImg">
            <img :src="droneImg" alt="" />
        </div>
@@ -11,7 +11,7 @@
            </div>
        </div>
        <BaseControl v-if="taskDetails.workspace_id"/>
        <BaseControl v-if="taskDetails.workspace_id" />
    </div>
</template>
<script setup>
@@ -19,12 +19,6 @@
import BaseControl from '@/components/CurrentTaskDetails/ControlPanel/BaseControl.vue'
const taskDetails = inject('taskDetails')
watch(taskDetails, () => {
    list.value.forEach(item => {
        item.value = taskDetails?.value?.[item.field] || ''
    })
})
const list = ref([
    { name: '任务编号', value: '', field: 'job_info_num' },
@@ -37,6 +31,21 @@
    { name: '关联算法', value: '', field: 'ai_type_str' },
    { name: '任务描述', value: '', field: 'remark' },
])
watch(
    taskDetails,
    () => {
        list.value.forEach(item => {
            item.value = taskDetails?.value?.[item.field] || ''
        })
    },
    {
        immediate: true,
        deep: true,
    }
)
</script>
<style scoped lang="scss">
@@ -54,7 +63,7 @@
    gap: 25px 0;
    align-items: center;
    .manualControl{
    .manualControl {
        background: transparent !important;
    }
@@ -68,7 +77,8 @@
        display: flex;
        align-items: center;
        justify-content: center;
        img{
        img {
            width: 100px;
            height: 68px;
        }
@@ -86,11 +96,12 @@
            display: flex;
            font-family: Segoe UI, Segoe UI;
            font-size: 14px;
            .itemName {
                flex-shrink: 0;
                font-weight: 400;
                color: #D3D3D3;
                color: #d3d3d3;
            }
            .itemValue {
src/const/drc.js
@@ -10,3 +10,22 @@
export const fourGQuality = { 0: '无信号', 1: '差', 2: '较差', 3: '一般', 4: '较好', 5: '好' }
export const SDRQuality = { 0: '无信号', 1: '差', 2: '较差', 3: '一般', 4: '较好', 5: '好' }
export const directionMap = [
    { min: 0, max: 0, value: '正北' },
    { min: 0, max: 45, value: '北偏东' },
    { min: 45, max: 45, value: '东北' },
    { min: 45, max: 90, value: '北偏东' },
    { min: 90, max: 90, value: '正东' },
    { min: 90, max: 135, value: '东偏南' },
    { min: 135, max: 135, value: '东南' },
    { min: 135, max: 180, value: '南偏东' },
    { min: 180, max: 180, value: '正南' },
    { min: 180, max: 225, value: '南偏西' },
    { min: 225, max: 225, value: '西南' },
    { min: 225, max: 270, value: '西偏南' },
    { min: 270, max: 270, value: '正西' },
    { min: 270, max: 315, value: '西偏北' },
    { min: 315, max: 315, value: '西北' },
    { min: 315, max: 360, value: '北偏西' },
    { min: 360, max: 360, value: '正北' }
];
src/hooks/controlDrone/useManualControl.js
@@ -184,24 +184,28 @@
            case 'ArrowUp':
                if (activeCodeKey === keyCode) return
                clearInterval(myInterval)
                ptzControl('up')
                myInterval = setInterval(()=>ptzControl('up'), 200)
                activeCodeKey = keyCode
                break
            case 'ArrowDown':
                if (activeCodeKey === keyCode) return
                clearInterval(myInterval)
                ptzControl('down')
                myInterval = setInterval(()=>ptzControl('down'), 200)
                activeCodeKey = keyCode
                break
            case 'ArrowLeft':
                if (activeCodeKey === keyCode) return
                clearInterval(myInterval)
                ptzControl('left')
                myInterval = setInterval(()=>ptzControl('left'), 200)
                activeCodeKey = keyCode
                break
            case 'ArrowRight':
                if (activeCodeKey === keyCode) return
                clearInterval(myInterval)
                ptzControl('right')
                myInterval = setInterval(()=>ptzControl('right'), 200)
                activeCodeKey = keyCode
                break
src/hooks/useDroneWS.js
New file
@@ -0,0 +1,47 @@
import { getWebsocketUrl } from '@/websocket/util/config'
import { useConnectWebSocket } from '@/utils/websocket/connect-websocket'
/**
 * 使用无人机WebSocket连接,根据工作区ID动态管理WebSocket连接。
 *
 * @param {Ref<string>} workspaceIdRef - 工作区ID的引用,用于动态更新WebSocket连接。
 * @returns {Ref<Object>} - 返回一个包含WebSocket消息的响应式对象,键为业务代码,值为对应的消息负载。
 */
export const useDroneWS = workspaceIdRef => {
    const wsInfo = ref({})
    let droneWebSocket = null
    // ws消息钩子 todo后面加节流参数处理
    const messageHandler = result => {
        let payload = JSON.parse(result)
        if (payload.biz_code) {
            wsInfo.value[payload.biz_code] = payload
        }
    }
    function init() {
        let webSocketUrl = getWebsocketUrl() + '&workspace-id=' + workspaceIdRef.value
        // 监听ws 消息
        droneWebSocket = useConnectWebSocket(messageHandler, webSocketUrl)
    }
    watch(workspaceIdRef, (newValue, oldValue) => {
        removeWS()
        if (workspaceIdRef.value) {
            init()
        }
    })
    // 移除ws 事件
    function removeWS() {
        droneWebSocket?.close()
        droneWebSocket = null
        wsInfo.value = {}
    }
    onBeforeUnmount(() => {
        removeWS()
    })
    return wsInfo
}