forked from drone/command-center-dashboard

罗广辉
2025-04-15 2888173145d91e465a25bea276acaa41f9a7b124
feat: 控制联调10%
6 files modified
9 files added
7977 ■■■■■ 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/useConnectDrone.js 54 ●●●●● patch | view | raw | blame | history
src/hooks/controlDrone/useManualControl.js 159 ●●●●● patch | view | raw | blame | history
src/hooks/controlDrone/useMqtt.js 111 ●●●●● 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/ControlPanel.vue 249 ●●●●● patch | view | raw | blame | history
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/CurrentTaskDetails.vue 78 ●●●● 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
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/useConnectDrone.js
New file
@@ -0,0 +1,54 @@
import { useStore } from 'vuex'
import { postDrc } from '@/api/drc'
import { UranusMqtt } from '@/mqtt'
export function useConnectDrone() {
    const store = useStore()
    const deviceOsdInfo = inject('deviceOsdInfo')
    const taskDetails = inject('taskDetails')
    const mqttState = ref(null)
    const client_id = ref(null)
    onMounted(async ()=>{
        if (mqttState.value) return
        const workspace_id = taskDetails.value.way_lines[0].workspace_id
        const result = await postDrc({},workspace_id)
        if (result?.code === 0) {
            const { address, client_id, username, password, expire_time } = result.data
            // @TODO: 校验 expire_time
            mqttState.value = new UranusMqtt(address, {
                clientId: client_id,
                username,
                password,
            })
            mqttState.value?.initMqtt()
            mqttState.value?.on('onStatus', statusOptions => {
                // @TODO: 异常case
            })
            store.commit('SET_MQTT_STATE', mqttState.value)
            store.commit('SET_CLIENT_ID', client_id)
        }
        // @TODO: 认证失败case
    })
    onBeforeUnmount(() => {
        if (mqttState?.value) {
            mqttState.value?.destroyed()
            mqttState.value = null
            store.commit('SET_MQTT_STATE', null)
            store.commit('SET_CLIENT_ID', '')
        }
    })
    return {
        mqttState,
        client_id
    }
}
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(deviceTopicInfo, isCurrentFlightController) {
    const activeCodeKey = ref(null)
    const mqttHooks = useMqtt(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,111 @@
import {
  DRC_METHOD,
} from '@/const/drc.js'
import EventBus from '@/event-bus'
import { useStore } from 'vuex'
export function useMqtt (deviceTopicInfo) {
  let cacheSubscribeArr= []
  const store = useStore()
  const mqttState = computed(() => {
    return store.state.common.mqttState
  })
  function publishMqtt (topic, body, ots) {
    mqttState.value?.publishMqtt(topic, JSON.stringify(body), ots)
  }
  function subscribeMqtt (topic, handleMessageMqtt) {
    mqttState.value?.subscribeMqtt(topic)
    const handler = handleMessageMqtt || onMessageMqtt
    mqttState.value?.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.value?.off('onMessageMqtt', item.callback)
      mqttState.value?.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.value) 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/ControlPanel.vue
@@ -2,86 +2,93 @@
    <div class="pointControl">
        <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>
                </div>
                <el-button type="primary" @click="control">控制</el-button>
                <el-button type="primary" @click="cancelControl">取消控制</el-button>
                <el-button type="primary" ghost @mousedown="onMouseDown(KeyCode.KEY_Q)" @mouseup="onMouseUp">q</el-button>
            </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>
                </div>
            </div>
            <div class="blackBg directionDown"></div>
        </div>
        <ControlComPass />
        <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>
            <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>
</template>
<script setup>
import vHold from '@/directive/hold'
import ControlComPass from './ControlComPass/ControlComPass.vue'
import {
    KeyCode,
    useManualControl,
} from '@/hooks/controlDrone/useManualControl'
import { useMqtt } from '@/hooks/controlDrone/useMqtt'
import { useConnectDrone } from '@/hooks/controlDrone/useConnectDrone'
import { droneController, exitController, postDrcExit } from '@/api/drc'
import { ElMessage } from 'element-plus'
import { useStore } from 'vuex'
const lowerRowButton = [
    { icon: 'el-icon-arrow-left', text: 'A' },
    { icon: 'el-icon-arrow-down', text: 'S' },
    { icon: 'el-icon-arrow-right', text: 'D' },
]
const upperRowButton = [
    { icon: 'el-icon-refresh-left', text: 'Q' },
    { icon: 'el-icon-arrow-up', text: 'W' },
    { icon: 'el-icon-refresh-right', text: 'E' },
]
let currentKey = ref('')
let keyReset = null
let visible = false
const deviceOsdInfo = inject('deviceOsdInfo')
const taskDetails = inject('taskDetails')
function mousedown(key) {
    if (key === 'Q') {
        console.log(6666666666)
    }
}
function handleKeydown(e) {
    currentKey.value = e.key.toUpperCase()
    mousedown(e.key.toUpperCase())
}
function handleKeyup() {
    currentKey.value = null
}
onMounted(() => {
    window.addEventListener('keydown', handleKeydown)
    window.addEventListener('keyup', handleKeyup)
const deviceTopicInfo = ref({
    sn: deviceOsdInfo.value?.data?.sn,
    pubTopic: '',
    subTopic: '',
})
onBeforeUnmount(() => {
    window.removeEventListener('keydown', handleKeydown)
    window.removeEventListener('keyup', handleKeyup)
})
const flightController = ref(false)
console.log('控制面板')
// 连接无人机mqtt 成功获得有效控制
useConnectDrone()
// 订阅消息
useMqtt(deviceTopicInfo.value)
// 使用手动控制
const { handleKeyup, handleEmergencyStop, resetControlState } = useManualControl(
    deviceTopicInfo.value,
    flightController
)
function onMouseDown(type) {
    console.log('anxia')
    handleKeyup(type)
}
const store = useStore()
const clientId = computed(() => store.state.common.clientId)
const dock_sn = computed(() => taskDetails.value.device_sns[0])
function cancelControl() {
    exitController({ client_id: clientId.value, dock_sn:dock_sn.value })
        .then(res => {
            flightController.value = false
            deviceTopicInfo.value.subTopic = ''
            deviceTopicInfo.value.pubTopic = ''
            ElMessage.success('退出飞行控制成功')
        })
        .catch(e => {})
}
// 控制
function control() {
    if (!clientId.value) return ElMessage.error('无人机不在空中,不能进入指挥飞行模式。')
    if (!dock_sn.value) return ElMessage.error('系统错误,未获取到dock_sn')
    droneController({ client_id: clientId.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('控制成功')
    })
}
function onMouseUp() {
    console.log('弹起')
    resetControlState()
}
</script>
<style scoped lang="scss">
@@ -90,111 +97,19 @@
    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: rgba(255, 255, 255, 0.3);
    border-radius: 40px 0px 40px 40px;
    display: flex;
    justify-content: center;
    align-items: flex-end;
    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 {
        display: flex;
        flex-direction: column;
        width: 100px;
        .speedBox {
            color: $FSColor;
            font-size: 16px;
            font-weight: 600;
            margin: 5px 0;
            gap: 0 5px;
        }
        .editBox {
            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;
                }
            }
        }
        .directionUp,
        .directionDown {
            display: flex;
            justify-content: space-around;
            text-align: center;
            height: 55px;
            > div {
                display: flex;
                flex-direction: column;
                justify-content: space-between;
            }
        }
    }
    .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;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
        }
    }
}
</style>
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/CurrentTaskDetails.vue
@@ -9,14 +9,15 @@
        :destroy-on-close="true"
    >
        <div class="content-container" v-if="isShow">
            <TaskDetailsHead/>
            <!-- 视频直播 -->
            <div class="video-container">
                <LiveVideo :videoUrl="currentLiveUrl" />
            </div>
            <!-- 展示地图 -->
            <RealTimeMap />
            <RealTimeMap class="realTimeMap" />
        </div>
        <ControlPanel/>
        <ControlPanel v-if="deviceOsdInfo?.data?.sn" />
    </el-dialog>
</template>
@@ -31,6 +32,8 @@
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'
const isShow = defineModel('show')
const props = defineProps({
@@ -43,7 +46,8 @@
let taskDetails = ref({})
const currentLiveUrl = ref('')
const machineNestUrl = ref('')
const dockLiveUrl = ref('')
const droneLiveUrl = ref('')
provide('taskDetails', taskDetails)
const deviceOsdInfo = ref({})
@@ -52,17 +56,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 +129,60 @@
})
</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 {
        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%;
    .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;
    }
}
</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: rgba(255, 255, 255, 0.1); /* 半透明背景 */
    backdrop-filter: blur(3px);
    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>