Merge branch 'master' of http://139.196.74.78:10010/r/drone/command-center-dashboard
10 files modified
1 files added
| | |
| | | # 使用官方的 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 |
| | |
| | | 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" |
| | |
| | | <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> |
| | |
| | | </div> |
| | | <!-- 指南针--> |
| | | <div class="compass"> |
| | | <ControlComPass /> |
| | | <ControlComPass :options="compassOptions" /> |
| | | </div> |
| | | |
| | | <div class="ptzControlBox"> |
| | |
| | | </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> |
| | |
| | | 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') |
| | |
| | | { 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) { |
| | |
| | | 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 = '' |
| | |
| | | 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, |
| | | } |
| | | }) |
| | |
| | | |
| | | // 返航或取消返航 |
| | | const returnOrCancelReturn = () => { |
| | | if (deviceOsdInfo.value?.data?.host?.mode_code === 9) { |
| | | if (device_osd_host?.value?.mode_code === 9) { |
| | | cancelBackDock() |
| | | } else { |
| | | onBackDock() |
| | |
| | | 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) |
| | |
| | | <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> |
| | |
| | | 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' |
| | |
| | | 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 () => { |
| | |
| | | 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 |
| | | } |
| | | |
| | |
| | | |
| | | // 设置当前直播地址 |
| | | 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') |
| | |
| | | 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(() => { |
| | |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | droneWebSocket?.close() |
| | | deviceOsdInfo.value = {} |
| | | droneWebSocket = null |
| | | EventBus.off('CurrentTaskDetails-timeStop', changeLineQuality) |
| | | EventBus.off('CurrentTaskDetails-getAiLiveUrl', getAiLiveUrl) |
| | | EventBus.off('CurrentTaskDetails-getDroneLiveUrl', getDroneLiveUrl) |
| | |
| | | <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> |
| | |
| | | } |
| | | } |
| | | |
| | | function refreshLive(){ |
| | | function refreshLive() { |
| | | EventBus.emit('CurrentTaskDetails-getDroneLiveUrl') |
| | | } |
| | | |
| | |
| | | }) |
| | | } |
| | | |
| | | const trueAltitude = inject('trueAltitude') |
| | | |
| | | // 获取真实高度 |
| | | function getRealTimeReallyHigh() { |
| | | if (!taskDetailsViewer?.value) return |
| | | const device_osd_host = wsInfo?.value?.device_osd?.data?.host || {} |
| | |
| | | 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 |
| | | }) |
| | | } |
| | | |
| | |
| | | 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) |
| | |
| | | color: #ffffff; |
| | | line-height: 18px; |
| | | margin-bottom: 10px; |
| | | |
| | | } |
| | | |
| | | .infoTitle { |
| | |
| | | <template> |
| | | <div class="task-details-right-container"> |
| | | <div class="titleImg"> |
| | | <div class="titleImg"> |
| | | <img :src="droneImg" alt="" /> |
| | | </div> |
| | | |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <BaseControl v-if="taskDetails.workspace_id"/> |
| | | <BaseControl v-if="taskDetails.workspace_id" /> |
| | | </div> |
| | | </template> |
| | | <script setup> |
| | |
| | | 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' }, |
| | |
| | | { 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"> |
| | |
| | | gap: 25px 0; |
| | | align-items: center; |
| | | |
| | | .manualControl{ |
| | | .manualControl { |
| | | background: transparent !important; |
| | | } |
| | | |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | img{ |
| | | |
| | | img { |
| | | width: 100px; |
| | | height: 68px; |
| | | } |
| | |
| | | display: flex; |
| | | font-family: Segoe UI, Segoe UI; |
| | | font-size: 14px; |
| | | |
| | | .itemName { |
| | | flex-shrink: 0; |
| | | font-weight: 400; |
| | | |
| | | color: #D3D3D3; |
| | | color: #d3d3d3; |
| | | } |
| | | |
| | | .itemValue { |
| | |
| | | |
| | | 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: '正北' } |
| | | ]; |
| | |
| | | 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 |
| New file |
| | |
| | | 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 |
| | | } |