forked from drone/command-center-dashboard

shuishen
2025-04-15 01f329228c55cdbe3c1391b427c7a54c891efada
Merge branch 'master' of http://139.196.74.78:10010/r/drone/command-center-dashboard
7 files modified
9 files added
8178 ■■■■■ changed files
package.json 3 ●●●●● patch | view | raw | blame | history
pnpm-lock.yaml 7016 ●●●●● patch | view | raw | blame | history
src/api/drc.js 42 ●●●●● patch | view | raw | blame | history
src/const/drc.js 8 ●●●●● patch | view | raw | blame | history
src/event-bus/index.js 5 ●●●●● patch | view | raw | blame | history
src/hooks/controlDrone/useManualControl.js 159 ●●●●● patch | view | raw | blame | history
src/hooks/controlDrone/useMqtt.js 106 ●●●●● patch | view | raw | blame | history
src/mqtt/config.js 8 ●●●●● patch | view | raw | blame | history
src/mqtt/index.js 98 ●●●●● patch | view | raw | blame | history
src/store/modules/common.js 8 ●●●●● patch | view | raw | blame | history
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/ControlComPass/ControlComPass.vue 4 ●●●● patch | view | raw | blame | history
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/ControlPanel.vue 412 ●●●●● patch | view | raw | blame | history
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/CurrentTaskDetails.vue 85 ●●●● patch | view | raw | blame | history
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/RealTimeMap.vue 3 ●●●●● patch | view | raw | blame | history
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/TaskDetailsHead.vue 135 ●●●●● patch | view | raw | blame | history
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/TaskDetailsLeft.vue 86 ●●●●● patch | view | raw | blame | history
package.json
@@ -27,12 +27,15 @@
    "disable-devtool": "^0.3.8",
    "echarts": "^5.6.0",
    "element-plus": "^2.9.3",
    "eventemitter3": "^5.0.1",
    "highlight.js": "^11.9.0",
    "js-base64": "^3.7.4",
    "js-cookie": "^3.0.0",
    "js-md5": "^0.7.3",
    "jszip": "^3.10.1",
    "lodash": "^4.17.21",
    "mitt": "^3.0.1",
    "mqtt": "^5.11.0",
    "nprogress": "^0.2.0",
    "postcss-pxtorem": "^6.1.0",
    "reconnecting-websocket": "^4.4.0",
pnpm-lock.yaml
Diff too large
src/api/drc.js
New file
@@ -0,0 +1,42 @@
import request from '@/axios'
// DRC 链路
const DRC_API_PREFIX = '/drone-device-core/control/api/v1'
// 获取 mqtt 连接认证
export async function postDrc (body,workspaceId) {
  const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/connect`, body)
  return resp.data
}
// /control/api/v1/workspaces/82f6008b-2068-448c-9094-881e212f31c3/drc/connect
// 进入飞行控制 (建立drc连接&获取云控控制权)
export async function postDrcEnter (body,workspaceId) {
  const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/enter`, body)
  return resp.data
}
// 退出飞行控制 (退出drc连接&退出云控控制权)
export async function postDrcExit (body,workspaceId) {
  const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/exit`, body)
  return resp.data
}
// 无人控制
export async function droneController(data) {
  return request({
    url: '/drone-device-core/dp/home/drc/droneController',
    method: 'post',
    data,
  })
}
// 无人机退出控制
export async function exitController(data) {
  return request({
    url: '/drone-device-core/dp/home/drc/exitController',
    method: 'post',
    data,
  })
}
src/const/drc.js
New file
@@ -0,0 +1,8 @@
export const DRC_METHOD = {
    HEART_BEAT: 'heart_beat',
    DRONE_CONTROL: 'drone_control', // 飞行控制-虚拟摇杆
    DRONE_EMERGENCY_STOP: 'drone_emergency_stop', // 急停
    OSD_INFO_PUSH: 'osd_info_push', // 高频osd信息上报
    HSI_INFO_PUSH: 'hsi_info_push', // 避障信息上报
    DELAY_TIME_INFO_PUSH: 'delay_info_push', // 图传链路延时信息上报
}
src/event-bus/index.js
New file
@@ -0,0 +1,5 @@
import mitt from 'mitt'
const emitter = mitt()
export default emitter
src/hooks/controlDrone/useManualControl.js
New file
@@ -0,0 +1,159 @@
import { DRC_METHOD } from '@/const/drc.js'
import { useMqtt } from '@/hooks/controlDrone/useMqtt'
import { ElMessage } from 'element-plus'
let myInterval
export const KeyCode = {
    KEY_W: 'KeyW',
    KEY_A: 'KeyA',
    KEY_S: 'KeyS',
    KEY_D: 'KeyD',
    KEY_Q: 'KeyQ',
    KEY_E: 'KeyE',
    ARROW_UP: 'ArrowUp',
    ARROW_DOWN: 'ArrowDown',
}
export function useManualControl(mqttState,deviceTopicInfo, isCurrentFlightController) {
    const activeCodeKey = ref(null)
    const mqttHooks = useMqtt(mqttState,deviceTopicInfo)
    let seq = 0
    function handlePublish(params) {
        const body = {
            method: DRC_METHOD.DRONE_CONTROL,
            data: params,
        }
        handleClearInterval()
        myInterval = setInterval(() => {
            body.data.seq = seq++
            seq++
            window.console.log('keyCode>>>>', activeCodeKey.value, body)
            mqttHooks?.publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 0 })
        }, 50)
    }
    function handleKeyup(keyCode) {
        if (!deviceTopicInfo.pubTopic) {
            ElMessage.error('请确保已经建立DRC链路')
            return
        }
        const SPEED = 5 //  check
        const HEIGHT = 5 //  check
        const W_SPEED = 20 // 机头角速度
        seq = 0
        switch (keyCode) {
            case 'KeyA':
                if (activeCodeKey.value === keyCode) return
                handlePublish({ y: -SPEED })
                activeCodeKey.value = keyCode
                break
            case 'KeyW':
                if (activeCodeKey.value === keyCode) return
                handlePublish({ x: SPEED })
                activeCodeKey.value = keyCode
                break
            case 'KeyS':
                if (activeCodeKey.value === keyCode) return
                handlePublish({ x: -SPEED })
                activeCodeKey.value = keyCode
                break
            case 'KeyD':
                if (activeCodeKey.value === keyCode) return
                handlePublish({ y: SPEED })
                activeCodeKey.value = keyCode
                break
            case 'ArrowUp':
                if (activeCodeKey.value === keyCode) return
                handlePublish({ h: HEIGHT })
                activeCodeKey.value = keyCode
                break
            case 'ArrowDown':
                if (activeCodeKey.value === keyCode) return
                handlePublish({ h: -HEIGHT })
                activeCodeKey.value = keyCode
                break
            case 'KeyQ':
                if (activeCodeKey.value === keyCode) return
                handlePublish({ w: -W_SPEED })
                activeCodeKey.value = keyCode
                break
            case 'KeyE':
                if (activeCodeKey.value === keyCode) return
                handlePublish({ w: W_SPEED })
                activeCodeKey.value = keyCode
                break
            default:
                break
        }
    }
    function handleClearInterval() {
        clearInterval(myInterval)
        myInterval = undefined
    }
    function resetControlState() {
        activeCodeKey.value = null
        seq = 0
        handleClearInterval()
    }
    function onKeyup() {
        resetControlState()
    }
    function onKeydown(e) {
        handleKeyup(e.code)
    }
    function startKeyboardManualControl() {
        window.addEventListener('keydown', onKeydown)
        window.addEventListener('keyup', onKeyup)
    }
    function closeKeyboardManualControl() {
        resetControlState()
        window.removeEventListener('keydown', onKeydown)
        window.removeEventListener('keyup', onKeyup)
    }
    watch(
        () => isCurrentFlightController.value,
        val => {
            if (val && deviceTopicInfo.pubTopic) {
                startKeyboardManualControl()
            } else {
                closeKeyboardManualControl()
            }
        },
        { immediate: true }
    )
    onUnmounted(() => {
        closeKeyboardManualControl()
    })
    function handleEmergencyStop() {
        if (!deviceTopicInfo.pubTopic) {
            ElMessage.error('请确保已经建立DRC链路')
            return
        }
        const body = {
            method: DRC_METHOD.DRONE_EMERGENCY_STOP,
            data: {},
        }
        resetControlState()
        window.console.log('handleEmergencyStop>>>>', deviceTopicInfo.pubTopic, body)
        mqttHooks?.publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 1 })
    }
    return {
        activeCodeKey,
        handleKeyup,
        handleEmergencyStop,
        resetControlState,
    }
}
src/hooks/controlDrone/useMqtt.js
New file
@@ -0,0 +1,106 @@
import {
  DRC_METHOD,
} from '@/const/drc.js'
import EventBus from '@/event-bus'
import { useStore } from 'vuex'
export function useMqtt (mqttState,deviceTopicInfo) {
  let cacheSubscribeArr= []
  function publishMqtt (topic, body, ots) {
    mqttState?.publishMqtt(topic, JSON.stringify(body), ots)
  }
  function subscribeMqtt (topic, handleMessageMqtt) {
    mqttState?.subscribeMqtt(topic)
    const handler = handleMessageMqtt || onMessageMqtt
    mqttState?.on('onMessageMqtt', handler)
    cacheSubscribeArr.push({
      topic,
      callback: handler,
    })
  }
  function onMessageMqtt (message) {
    if (cacheSubscribeArr.findIndex(item => item.topic === message?.topic) !== -1) {
      const payloadStr = new TextDecoder('utf-8').decode(message?.payload)
      const payloadObj = JSON.parse(payloadStr)
      switch (payloadObj?.method) {
        case DRC_METHOD.HEART_BEAT:
          break
        case DRC_METHOD.DELAY_TIME_INFO_PUSH:
        case DRC_METHOD.HSI_INFO_PUSH:
        case DRC_METHOD.OSD_INFO_PUSH:
        case DRC_METHOD.DRONE_CONTROL:
        case DRC_METHOD.DRONE_EMERGENCY_STOP:
          EventBus.emit('droneControlMqttInfo', payloadObj)
          break
        default:
          break
      }
    }
  }
  function unsubscribeDrc () {
    // 销毁已订阅事件
    cacheSubscribeArr.forEach(item => {
      mqttState?.off('onMessageMqtt', item.callback)
      mqttState?.unsubscribeMqtt(item.topic)
    })
    cacheSubscribeArr = []
  }
  // 心跳
  const heartBeatSeq = ref(0)
  const state = reactive({
    heartState: new Map(),
  })
  // 监听云控控制权
  watch(() => deviceTopicInfo, (val, oldVal) => {
    if (val.subTopic !== '') {
      // 1.订阅topic
      subscribeMqtt(deviceTopicInfo.subTopic)
      // 2.发心跳
      publishDrcPing(deviceTopicInfo.sn)
    } else {
      clearInterval(state.heartState.get(deviceTopicInfo.sn)?.pingInterval)
      state.heartState.delete(deviceTopicInfo.sn)
      heartBeatSeq.value = 0
    }
  }, { immediate: true, deep: true })
  function publishDrcPing (sn) {
    const body = {
      method: DRC_METHOD.HEART_BEAT,
      data: {
        ts: new Date().getTime(),
        seq: heartBeatSeq.value,
      },
    }
    const pingInterval = setInterval(() => {
      if (!mqttState) return
      heartBeatSeq.value += 1
      body.data.ts = new Date().getTime()
      body.data.seq = heartBeatSeq.value
      publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 0 })
    }, 1000)
    state.heartState.set(sn, {
      pingInterval,
    })
  }
  onUnmounted(() => {
    unsubscribeDrc()
    heartBeatSeq.value = 0
  })
  return {
    mqttState,
    publishMqtt,
    subscribeMqtt,
  }
}
src/mqtt/config.js
New file
@@ -0,0 +1,8 @@
export const OPTIONS = {
  clean: true, // true: 清除会话, false: 保留会话
  connectTimeout: 10000, // mqtt 超时时间
  resubscribe: true, // 断开重连后,再次订阅原订阅
  reconnectPeriod: 10000, // 重连间隔时间: 5s
  keepalive: 1, // 心跳间隔时间:1s
}
src/mqtt/index.js
New file
@@ -0,0 +1,98 @@
import EventEmitter from 'eventemitter3'
import { OPTIONS } from './config'
import mqtt from 'mqtt'
export class UranusMqtt extends EventEmitter {
    constructor(url, options) {
        super()
        this._url = url || ''
        this._options = options
        this._client = null
        this._hasInit = false
    }
    initMqtt = () => {
        // 仅初始化一次
        if (this._hasInit) return
        // 建立连接
        this._client = mqtt.connect(this._url, {
            ...OPTIONS,
            ...this._options,
        })
        this._hasInit = true
        if (this._client) {
            this._client.on('reconnect', this._onReconnect)
            // 消息监听
            this._client.on('message', this._onMessage)
            // 连接关闭
            this._client.on('close', this._onClose)
            // 连接异常
            this._client.on('error', this._onError)
        }
    }
    // 发布
    publishMqtt = (topic, body, opts) => {
        if (!this._client?.connected) {
            this.initMqtt()
        }
        this._client?.publish(topic, body, opts || {}, (error, packet) => {
            if (error) {
                window.console.error('mqtt publish error,', error, packet)
            }
        })
    }
    // 订阅
    subscribeMqtt = topic => {
        if (!this._client?.connected) {
            this.initMqtt()
        }
        window.console.log('subscribeMqtt>>>>>', topic)
        this._client?.subscribe(topic, (error, granted) => {
            window.console.log('mqtt subscribe,', error, granted)
        })
    }
    // 取消订阅
    unsubscribeMqtt = topic => {
        window.console.log('mqtt unsubscribeMqtt,', topic)
        this._client?.unsubscribe(topic)
    }
    // 关闭 mqtt 客户端
    destroyed = () => {
        window.console.log('mqtt destroyed')
        this._client?.end()
    }
    _onReconnect = () => {
        if (this._client) {
            window.console.error('mqtt reconnect,')
        }
    }
    _onMessage = (topic, payload, packet) => {
        this.emit('onMessageMqtt', { topic, payload, packet })
    }
    _onClose = () => {
        // 连接异常关闭会自动重连
        window.console.error('mqtt close,')
        this.emit('onStatus', {
            status: 'close',
        })
    }
    _onError = error => {
        // 连接错误会自动重连
        window.console.error('mqtt error,', error)
        this.emit('onStatus', {
            status: 'error',
            data: error,
        })
    }
}
src/store/modules/common.js
@@ -21,6 +21,8 @@
    isMenu: true,
    isSearch: false,
    isRefresh: true,
    mqttState: null, // mqtt 实例
    clientId: null, // 客户端ID实例
    isLock: getStore({ name: 'isLock' }),
    colorName: getStore({ name: 'colorName' }) || '#2C77F1',
    themeName: getStore({ name: 'themeName' }) || 'theme-go',
@@ -40,6 +42,12 @@
      state.mapSetting = data
      loadLAYER()
    },
    SET_MQTT_STATE (state, mqttState) {
      state.mqttState = mqttState
    },
    SET_CLIENT_ID (state, id) {
      state.clientId = id
    },
    SET_LANGUAGE: (state, language) => {
      state.language = language
      setStore({
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/ControlComPass/ControlComPass.vue
@@ -130,8 +130,8 @@
</script>
<style lang="scss" scoped>
.instrument-content {
  height: 245px;
  width: 280px;
  height: 100%;
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/ControlPanel.vue
@@ -1,86 +1,202 @@
<template>
    <div class="pointControl">
        <div class="manualControl"></div>
        <div class="direction">
            <div class="blackBg directionUp">
                <div v-for="item in upperRowButton" :key="item.text">
                    <el-icon :class="item.icon"></el-icon>
                    <div
                        :class="{ hotkeyBtn: true, activeKey: currentKey === item.text }"
                        @click="() => mousedown(item.text)"
                        v-hold="() => mousedown(item.text)"
                    >
                        {{ item.text }}
            <div class="boxTitle">
                飞
                <br />
                行
                <br />
                控
                <br />
                制
                <br />
                器
            </div>
            <div class="btnGroup">
                <div class="btnGroupT">
                    <div class="btnItem" v-for="item in list1">
                        <el-icon class="btnIcon">
                            <component :is="item.icon" />
                        </el-icon>
                        <div class="btn" @mousedown="onMouseDown(item.key)" @mouseup="onMouseUp">{{ item.text }}</div>
                    </div>
                </div>
                <div class="btnGroupB">
                    <div class="btnItem" v-for="item in list2">
                        <div class="btn" @mousedown="onMouseDown(item.key)" @mouseup="onMouseUp">{{ item.text }}</div>
                        <el-icon class="btnIcon">
                            <component :is="item.icon" />
                        </el-icon>
                    </div>
                </div>
            </div>
            <div class="blackBg directionDown">
                <div v-for="item in lowerRowButton" :key="item.text">
                    <el-icon :class="item.icon"></el-icon>
                    <div
                        :class="{ hotkeyBtn: true, activeKey: currentKey === item.text }"
                        @click="() => mousedown(item.text)"
                        v-hold="() => mousedown(item.text)"
                    >
                        {{ item.text }}
            <div class="speed">
                <el-icon class="btnIcon">
                    <Plus />
                </el-icon>
                <div>5<br>m/s</div>
                <el-icon class="btnIcon">
                    <Minus />
                </el-icon>
            </div>
            <div class="upAndDown">
                <div class="btnGroupT">
                    <div class="btnItem">
                        <el-icon class="btnIcon">
                            <Top />
                        </el-icon>
                        <div class="btn" @mousedown="onMouseDown(KeyCode.ARROW_UP)" @mouseup="onMouseUp">C</div>
                    </div>
                </div>
                <div class="btnGroupT">
                    <div class="btnItem">
                        <div class="btn" @mousedown="onMouseDown(KeyCode.ARROW_DOWN)" @mouseup="onMouseUp">Z</div>
                        <el-icon class="btnIcon">
                            <Bottom />
                        </el-icon>
                    </div>
                </div>
            </div>
        </div>
        <ControlComPass />
        <div class="compass">
            <ControlComPass />
        </div>
        <div class="height-direction">
            <div class="blackBg height-direction-btn">
                <el-icon class="el-icon-top" />
                <div :class="{ hotkeyBtn: true, activeKey: currentKey === 'C' }" v-hold="() => mousedown('C')">C</div>
        <div class="ptzControl">
            <div>
                云
                <br />
                台
                <br />
                控
                <br />
                制
            </div>
            <div class="blackBg height-direction-btn">
                <div :class="{ hotkeyBtn: true, activeKey: currentKey === 'Z' }" v-hold="() => mousedown('Z')">Z</div>
                <el-icon class="el-icon-bottom" />
            </div>
            <div></div>
        </div>
    </div>
</template>
<script setup>
import vHold from '@/directive/hold'
import ControlComPass from './ControlComPass/ControlComPass.vue'
import { KeyCode, useManualControl } from '@/hooks/controlDrone/useManualControl'
import { droneController, exitController, postDrc, postDrcExit } from '@/api/drc'
import { ElMessage } from 'element-plus'
import { useStore } from 'vuex'
import { UranusMqtt } from '@/mqtt'
import {
    ArrowDown,
    ArrowLeft,
    ArrowRight,
    ArrowUp,
    Bottom, Minus,
    Plus,
    RefreshLeft,
    RefreshRight,
} from '@element-plus/icons-vue'
const lowerRowButton = [
    { icon: 'el-icon-arrow-left', text: 'A' },
    { icon: 'el-icon-arrow-down', text: 'S' },
    { icon: 'el-icon-arrow-right', text: 'D' },
const deviceOsdInfo = inject('deviceOsdInfo')
const taskDetails = inject('taskDetails')
const store = useStore()
const workspace_id = computed(() => taskDetails.value.way_lines[0].workspace_id)
const dock_sn = computed(() => taskDetails.value.device_sns[0])
const list1 = [
    { key: KeyCode.KEY_Q, text: 'Q', icon: RefreshLeft },
    { key: KeyCode.KEY_W, text: 'W', icon: ArrowUp },
    { key: KeyCode.KEY_E, text: 'E', icon: RefreshRight },
]
const upperRowButton = [
    { icon: 'el-icon-refresh-left', text: 'Q' },
    { icon: 'el-icon-arrow-up', text: 'W' },
    { icon: 'el-icon-refresh-right', text: 'E' },
const list2 = [
    { key: KeyCode.KEY_A, text: 'A', icon: ArrowLeft },
    { key: KeyCode.KEY_S, text: 'S', icon: ArrowDown },
    { key: KeyCode.KEY_D, text: 'D', icon: ArrowRight },
]
let currentKey = ref('')
let keyReset = null
let visible = false
function mousedown(key) {
    if (key === 'Q') {
        console.log(6666666666)
let mqttState = null
const client_id = ref('')
const deviceTopicInfo = ref({
    sn: deviceOsdInfo.value?.data?.sn,
    pubTopic: '',
    subTopic: '',
})
const flightController = ref(false)
console.log('控制面板')
// 控制对象
let manualControl = {}
function onMouseDown(type) {
    manualControl?.handleKeyup(type)
}
function onMouseUp() {
    console.log('弹起')
    manualControl?.resetControlState()
}
// 取消手动控制
function cancelControl() {
    exitController({ client_id: client_id.value, dock_sn: dock_sn.value })
        .then(res => {
            flightController.value = false
            deviceTopicInfo.value.subTopic = ''
            deviceTopicInfo.value.pubTopic = ''
            ElMessage.success('退出飞行控制成功')
        })
        .catch(e => {})
}
// 手动控制
function control() {
    if (!client_id.value) return ElMessage.error('无人机不在空中,不能进入指挥飞行模式。')
    if (!dock_sn.value) return ElMessage.error('系统错误,未获取到dock_sn')
    droneController({ client_id: client_id.value, dock_sn: dock_sn.value }).then(res => {
        flightController.value = true
        const { data } = res.data
        if (data.sub && data.sub?.length > 0) {
            deviceTopicInfo.value.subTopic = data.sub[0]
        }
        if (data.pub && data.pub?.length > 0) {
            deviceTopicInfo.value.pubTopic = data.pub[0]
        }
        ElMessage.success('控制成功')
    })
}
// 创建连接
const createConnect = async () => {
    const result = await postDrc({}, workspace_id.value)
    if (result?.code === 0) {
        const { address, client_id: clientId, username, password, expire_time } = result.data
        mqttState = new UranusMqtt(address, { clientId, username, password })
        mqttState?.initMqtt()
        client_id.value = clientId
    }
}
function handleKeydown(e) {
    currentKey.value = e.key.toUpperCase()
    mousedown(e.key.toUpperCase())
// 销毁连接
const destroyConnect = () => {
    if (mqttState) {
        mqttState?.destroyed()
        mqttState = null
        client_id.value = ''
    }
}
function handleKeyup() {
    currentKey.value = null
}
onMounted(() => {
    window.addEventListener('keydown', handleKeydown)
    window.addEventListener('keyup', handleKeyup)
onMounted(async () => {
    await createConnect()
    // 使用控制
    manualControl = useManualControl(mqttState, deviceTopicInfo.value, flightController)
})
onBeforeUnmount(() => {
    window.removeEventListener('keydown', handleKeydown)
    window.removeEventListener('keyup', handleKeyup)
    destroyConnect()
})
</script>
@@ -90,110 +206,132 @@
    align-items: center;
}
//变量
$FSColor: #00ee8b;
.hotkeyBtn {
    background: #3c3c3c;
    border-radius: 2px;
    width: 24px;
    height: 24px;
    font-size: 14px;
    line-height: 24px;
    text-align: center;
    color: #fff;
    cursor: pointer;
    -webkit-user-select: none;
    user-select: none;
    &:hover {
        background: $FSColor;
    }
}
.pointControl {
    position: absolute;
    bottom: 0;
    right: 0;
    width: 1540px;
    height: 217px;
    background: rgb(0, 0, 0, 0.4); /* 半透明背景 */
    backdrop-filter: blur(5px);
    border-radius: 40px 0px 40px 40px;
    display: flex;
    justify-content: center;
    align-items: flex-end;
    align-items: center;
    color: white;
    gap: 0 10px;
    pointer-events: all;
    width: 100%;
    height: 200px;
    .activeKey {
        background: $FSColor;
    }
    .blackBg {
        background: rgba(0, 0, 0, 0.65);
        padding: 5px 5px;
        border-radius: 5px;
    }
    .direction {
        width: 476px;
        height: 188px;
        background: rgb(0, 0, 0, 0.4); /* 半透明背景 */
        border-radius: 40px 40px 40px 40px;
        display: flex;
        flex-direction: column;
        width: 100px;
        .speedBox {
            color: $FSColor;
            font-size: 16px;
            font-weight: 600;
            margin: 5px 0;
            gap: 0 5px;
        align-items: center;
        justify-content: space-around;
        .boxTitle{
            font-family: Segoe UI, Segoe UI;
            font-weight: 400;
            font-size: 14px;
            color: #D2E8FA;
        }
        .editBox {
        .btnGroup {
            display: flex;
            justify-content: end;
            gap: 0 5px;
            margin-bottom: 5px;
            .current-point {
                font-size: 20px;
                display: flex;
                align-items: center;
                color: $FSColor;
            }
            .editPointBtn {
                &:hover {
                    cursor: pointer;
                    background: $FSColor;
                }
            flex-direction: column;
            gap: 10px 0;
            .btnGroupT,
            .btnGroupB {
                width: 238px;
                height: 73px;
            }
        }
        .directionUp,
        .directionDown {
        .upAndDown{
            display: flex;
            justify-content: space-around;
            text-align: center;
            height: 55px;
            > div {
                display: flex;
                flex-direction: column;
                justify-content: space-between;
            flex-direction: column;
            gap: 10px 0;
            .btnGroupT,
            .btnGroupB {
                width: 58px;
                height: 73px;
            }
        }
    }
    .height-direction {
        width: 100px;
        display: flex;
        flex-direction: column;
        align-items: end;
        gap: 5px 0;
        .height-direction-btn {
            width: 35px;
            height: 55px;
            text-align: center;
        .speed{
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            align-items: center;
            width: 58px;
            height: 155px;
            background: #37393F;
            box-shadow: 2px 4px 6px 0px rgba(0,13,26,0.42);
            border-radius: 8px 8px 8px 8px;
            text-align: center;
            padding: 5px 0;
            .btnIcon{
                font-size: 20px;
            }
        }
    }
    .manualControl {
        width: 188px;
        height: 188px;
        background: rgb(0, 0, 0, 0.4); /* 半透明背景 */
        border-radius: 40px 40px 40px 40px;
    }
    .ptzControl {
        width: 406px;
        height: 188px;
        background: rgb(0, 0, 0, 0.4); /* 半透明背景 */
        border-radius: 40px 40px 40px 40px;
    }
    .compass {
        width: 356px;
        height: 188px;
        background: rgba(157, 173, 189, 0.11);
        background: rgb(0, 0, 0, 0.4); /* 半透明背景 */
        border-radius: 40px 40px 40px 40px;
    }
    .btnGroupT,
    .btnGroupB {
        background: #37393f;
        box-shadow: 2px 4px 6px 0px rgba(0, 13, 26, 0.42);
        border-radius: 8px 8px 8px 8px;
        display: flex;
        align-items: center;
        text-align: center;
        justify-content: center;
        gap: 0 45px;
        .btnItem {
            .btnIcon {
                font-size: 20px;
                &:first-child {
                    margin-bottom: 5px;
                }
            }
            .btn {
                width: 35px;
                height: 35px;
                background: #222324;
                line-height: 35px;
                border-radius: 5px;
                cursor: pointer;
                &:first-child {
                    margin-bottom: 5px;
                }
            }
        }
    }
}
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/CurrentTaskDetails.vue
@@ -9,14 +9,17 @@
        :destroy-on-close="true"
    >
        <div class="content-container" v-if="isShow">
            <TaskDetailsHead/>
            <TaskDetailsLeft/>
            <!-- 视频直播 -->
            <div class="video-container">
                <LiveVideo :videoUrl="currentLiveUrl" />
            </div>
            <!-- 展示地图 -->
            <RealTimeMap />
            <RealTimeMap class="realTimeMap" />
            <ControlPanel />
<!--            <ControlPanel v-if="deviceOsdInfo?.data?.sn" />-->
        </div>
        <ControlPanel/>
    </el-dialog>
</template>
@@ -31,6 +34,9 @@
import { useConnectWebSocket } from '@/utils/websocket/connect-websocket'
import { EBizCode } from '@/utils/staticData/enums'
import ControlPanel from '@/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/ControlPanel.vue'
import { KeyCode } from '@/hooks/controlDrone/useManualControl'
import TaskDetailsHead from '@/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/TaskDetailsHead.vue'
import TaskDetailsLeft from '@/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/TaskDetailsLeft.vue'
const isShow = defineModel('show')
const props = defineProps({
@@ -43,7 +49,8 @@
let taskDetails = ref({})
const currentLiveUrl = ref('')
const machineNestUrl = ref('')
const dockLiveUrl = ref('')
const droneLiveUrl = ref('')
provide('taskDetails', taskDetails)
const deviceOsdInfo = ref({})
@@ -52,17 +59,17 @@
// 机巢直播
const getDeviceLiveUrl = async () => {
    if (machineNestUrl.value) return machineNestUrl.value
    const res = await liveStart(taskDetails.value.device_sns[0],'')
    const res = await liveStart(taskDetails.value.device_sns[0], '')
    machineNestUrl.value = res.data.data.rtcs_url
    return machineNestUrl.value
}
// 无人机直播
const getDockLiveUrl = async dockSn => {
    if (dockLiveUrl.value) return dockLiveUrl.value
    const res = await liveStart(dockSn,'')
    dockLiveUrl.value = res.data.data.rtcs_url
    return dockLiveUrl.value
    if (droneLiveUrl.value) return droneLiveUrl.value
    const res = await liveStart(dockSn, '')
    droneLiveUrl.value = res.data.data.rtcs_url
    return droneLiveUrl.value
}
// 设置当前直播地址
@@ -125,20 +132,64 @@
})
</script>
<style lang="scss" scoped>
<style lang="scss">
.current-task-details {
    display: flex;
    justify-content: space-between;
    .content-container {
        display: flex;
        // gap: 20px;
        height: 600px;
        .video-container {
            width: 50%;
            padding-right: 10px;
    .el-dialog {
        border-radius: 40px;
        position: relative;
        margin-top: 38px;
        width: 1782px;
        height: 1002px;
        padding: 0;
        .el-dialog__body {
            width: 100%;
            height: 100%;
        }
        .el-dialog__header {
            height: 0;
            overflow: hidden;
            padding: 0;
            .el-dialog__headerbtn {
                position: absolute;
                right: -40px;
                top: -40px;
                .el-dialog__close {
                    font-size: 30px;
                }
            }
        }
    }
}
</style>
<style lang="scss" scoped>
.content-container {
    height: 100%;
    width: 100%;
    border-radius: 4rem;
    overflow: hidden;
    .video-container {
        width: 100%;
        height: 100%;
    }
    .realTimeMap {
        position: absolute;
        left: -1px;
        bottom: -1px;
        width: 218px;
        height: 217px;
        border-radius: 0px 20px 0px 40px;
        border: 1px solid #62a1ff;
        overflow: hidden;
    }
}
</style>
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/RealTimeMap.vue
@@ -161,9 +161,6 @@
</script>
<style scoped lang="scss">
#currentTaskMap {
    width: 50%;
    padding-left: 10px;
    height: 100%;
    :deep() {
        .cesium-viewer {
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/TaskDetailsHead.vue
New file
@@ -0,0 +1,135 @@
<template>
    <div class="detailsHead">
        <div class="droneName">小蓝工业园</div>
        <div class="infoListBox">
            <div v-for="item in infoList">
                <div class="infoValue">{{ item.value }}</div>
                <div class="infoTitle">{{ item.title }}</div>
            </div>
        </div>
        <div class="controlBtn">
            <el-icon class="refresh"><Refresh /></el-icon>
            <div class="switchBtn" @click="switchBtn">
                <div :class="{ open: open }">NO</div>
                <div :class="{ open: !open }">OFF</div>
            </div>
        </div>
    </div>
</template>
<script setup>
import { Refresh } from '@element-plus/icons-vue'
const open = ref(true)
const switchBtn = () => {
    open.value = !open.value
}
const infoList = [
    { title: '实时真高', value: '0' },
    { title: '绝对高度', value: '0' },
    { title: '水平速度', value: '0' },
    { title: '垂直速度', value: '0' },
    { title: '经度', value: '0' },
    { title: '纬度', value: '0' },
    { title: '4G信号', value: '0' },
    { title: 'SDR信号', value: '0' },
    { title: 'GPS搜星数', value: '0' },
    { title: 'RTK搜星数', value: '0' },
    { title: '距离机场', value: '0' },
    { title: '飞行时长', value: '0' },
    { title: '电池电量', value: '0' },
]
</script>
<style scoped lang="scss">
.detailsHead {
    position: absolute;
    top: 0;
    z-index: 5;
    width: 100%;
    height: 68px;
    background: rgb(0,0,0,.4); /* 半透明背景 */
    backdrop-filter: blur(5px);
    padding: 0 31px;
    display: flex;
    align-items: center;
    .droneName {
        width: 132px;
        height: 42px;
        background: rgba(74, 72, 72, 0.54);
        box-shadow: 0px 4px 72px 0px rgba(0, 0, 0, 0.25);
        border-radius: 8px 8px 8px 8px;
        border: 1px solid rgba(255, 255, 255, 0.99);
        font-family: Segoe UI, Segoe UI;
        font-weight: normal;
        font-size: 18px;
        color: #ededed;
        text-align: center;
        line-height: 42px;
    }
    .infoListBox {
        width: 0;
        flex: 1;
        display: flex;
        align-items: center;
        justify-content: space-around;
        font-family: Segoe UI, Segoe UI;
        font-weight: 400;
        .infoValue {
            font-size: 20px;
            color: #ffffff;
            line-height: 15px;
            margin-bottom: 10px;
        }
        .infoTitle {
            font-size: 12px;
            color: #d2e8fa;
            line-height: 15px;
        }
    }
    .controlBtn {
        width: 130px;
        display: flex;
        align-items: center;
        justify-content: space-between;
        .refresh{
            color: white;
            font-size: 30px;
            cursor: pointer;
        }
        .switchBtn {
            width: 70px;
            height: 30px;
            box-shadow: 2px 4px 20px 0px rgba(0, 13, 26, 0.23);
            border-radius: 4px 4px 4px 4px;
            border: 1px solid #ffffff;
            display: flex;
            align-items: center;
            justify-content: space-between;
            font-family: Segoe UI, Segoe UI;
            font-weight: bold;
            font-size: 14px;
            line-height: 30px;
            text-align: center;
            color: #ffffff;
            cursor: pointer;
            > div {
                width: 50%;
            }
            .open {
                background: #ffffff;
                color: #242424;
            }
        }
    }
}
</style>
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/TaskDetailsLeft.vue
New file
@@ -0,0 +1,86 @@
<template>
    <div class="taskDetailsLeft">
        <div class="title">负载控制</div>
        <div class="singleCol">广角相机</div>
        <div class="singleCol">变焦相机</div>
        <div class="singleCol">红外相机</div>
        <div class="multiCol">
            <div>云台回中</div>
            <div>云台朝下</div>
        </div>
        <div class="singleCol">画质</div>
        <div class="multiCol">
            <div>拍照</div>
            <div>录像</div>
        </div>
        <div class="multiCol">
            <div>喊话</div>
            <div>广播</div>
        </div>
    </div>
</template>
<script setup></script>
<style scoped lang="scss">
.taskDetailsLeft {
    z-index: 2;
    position: absolute;
    left: 18px;
    top: 50%;
    transform: translateY(-60%);
    width: 178px;
    height: 409px;
    background: rgba(0, 0, 0, 0.5);
    backdrop-filter: blur(5px);
    border-radius: 20px 20px 20px 20px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    font-family: Segoe UI, Segoe UI;
    font-weight: 400;
    font-size: 14px;
    color: #FFFFFF;
    line-height: 18px;
    text-align: center;
    align-items: center;
    gap: 12px;
    .singleCol{
        width: 146px;
        height: 40px;
        box-shadow: 0px 4px 72px 0px rgba(0,0,0,0.25);
        border-radius: 8px 8px 8px 8px;
        line-height: 40px;
        cursor: pointer;
        background: rgba(74,72,72,0.67);
        &:hover{
            background: rgba(255,255,255,0.78);
            color: #3D3B3B;
            border: 1px solid rgba(255,255,255,0.99);
        }
    }
    .multiCol{
        display: flex;
        gap: 12px;
        >div{
            cursor: pointer;
            width: 67px;
            height: 40px;
            background: rgba(74,72,72,0.67);
            box-shadow: 0px 4px 72px 0px rgba(0,0,0,0.25);
            border-radius: 8px 8px 8px 8px;
            line-height: 40px;
            &:hover{
                background: rgba(255,255,255,0.78);
                color: #3D3B3B;
                border: 1px solid rgba(255,255,255,0.99);
            }
        }
    }
}
</style>