forked from drone/command-center-dashboard

罗广辉
2025-04-15 2abd8e08f2148331e14bf1332e05f5d6302f82de
feat: 当前任务详情35%
6 files modified
1 files deleted
1 files added
479 ■■■■ changed files
src/hooks/controlDrone/useConnectDrone.js 54 ●●●●● patch | view | raw | blame | history
src/hooks/controlDrone/useManualControl.js 4 ●●●● patch | view | raw | blame | history
src/hooks/controlDrone/useMqtt.js 19 ●●●●● 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 299 ●●●● patch | view | raw | blame | history
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/CurrentTaskDetails.vue 9 ●●●● patch | view | raw | blame | history
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/TaskDetailsHead.vue 4 ●●●● patch | view | raw | blame | history
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/TaskDetailsLeft.vue 86 ●●●●● patch | view | raw | blame | history
src/hooks/controlDrone/useConnectDrone.js
File was deleted
src/hooks/controlDrone/useManualControl.js
@@ -15,10 +15,10 @@
    ARROW_DOWN: 'ArrowDown',
}
export function useManualControl(deviceTopicInfo, isCurrentFlightController) {
export function useManualControl(mqttState,deviceTopicInfo, isCurrentFlightController) {
    const activeCodeKey = ref(null)
    const mqttHooks = useMqtt(deviceTopicInfo)
    const mqttHooks = useMqtt(mqttState,deviceTopicInfo)
    let seq = 0
    function handlePublish(params) {
src/hooks/controlDrone/useMqtt.js
@@ -6,23 +6,18 @@
export function useMqtt (deviceTopicInfo) {
export function useMqtt (mqttState,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)
    mqttState?.publishMqtt(topic, JSON.stringify(body), ots)
  }
  function subscribeMqtt (topic, handleMessageMqtt) {
    mqttState.value?.subscribeMqtt(topic)
    mqttState?.subscribeMqtt(topic)
    const handler = handleMessageMqtt || onMessageMqtt
    mqttState.value?.on('onMessageMqtt', handler)
    mqttState?.on('onMessageMqtt', handler)
    cacheSubscribeArr.push({
      topic,
      callback: handler,
@@ -52,8 +47,8 @@
  function unsubscribeDrc () {
    // 销毁已订阅事件
    cacheSubscribeArr.forEach(item => {
      mqttState.value?.off('onMessageMqtt', item.callback)
      mqttState.value?.unsubscribeMqtt(item.topic)
      mqttState?.off('onMessageMqtt', item.callback)
      mqttState?.unsubscribeMqtt(item.topic)
    })
    cacheSubscribeArr = []
  }
@@ -87,7 +82,7 @@
      },
    }
    const pingInterval = setInterval(() => {
      if (!mqttState.value) return
      if (!mqttState) return
      heartBeatSeq.value += 1
      body.data.ts = new Date().getTime()
      body.data.seq = heartBeatSeq.value
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,32 +1,124 @@
<template>
    <div class="pointControl">
        <div class="manualControl"></div>
        <div class="direction">
            <div class="blackBg directionUp">
                <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 class="boxTitle">
                飞
                <br />
                行
                <br />
                控
                <br />
                制
                <br />
                器
            </div>
            <div class="blackBg directionDown"></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="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="ptzControl">
            <div>
                云
                <br />
                台
                <br />
                控
                <br />
                制
            </div>
            <div></div>
        </div>
    </div>
</template>
<script setup>
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 { 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 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 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 mqttState = null
const client_id = ref('')
const deviceTopicInfo = ref({
    sn: deviceOsdInfo.value?.data?.sn,
@@ -36,29 +128,21 @@
const flightController = ref(false)
console.log('控制面板')
// 连接无人机mqtt 成功获得有效控制
useConnectDrone()
// 订阅消息
useMqtt(deviceTopicInfo.value)
// 使用手动控制
const { handleKeyup, handleEmergencyStop, resetControlState } = useManualControl(
    deviceTopicInfo.value,
    flightController
)
// 控制对象
let manualControl = {}
function onMouseDown(type) {
    console.log('anxia')
    handleKeyup(type)
    manualControl?.handleKeyup(type)
}
const store = useStore()
const clientId = computed(() => store.state.common.clientId)
const dock_sn = computed(() => taskDetails.value.device_sns[0])
function onMouseUp() {
    console.log('弹起')
    manualControl?.resetControlState()
}
// 取消手动控制
function cancelControl() {
    exitController({ client_id: clientId.value, dock_sn:dock_sn.value })
    exitController({ client_id: client_id.value, dock_sn: dock_sn.value })
        .then(res => {
            flightController.value = false
            deviceTopicInfo.value.subTopic = ''
@@ -68,11 +152,11 @@
        .catch(e => {})
}
// 控制
// 手动控制
function control() {
    if (!clientId.value) return ElMessage.error('无人机不在空中,不能进入指挥飞行模式。')
    if (!client_id.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 => {
    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) {
@@ -85,10 +169,35 @@
    })
}
function onMouseUp() {
    console.log('弹起')
    resetControlState()
// 创建连接
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
    }
}
// 销毁连接
const destroyConnect = () => {
    if (mqttState) {
        mqttState?.destroyed()
        mqttState = null
        client_id.value = ''
    }
}
onMounted(async () => {
    await createConnect()
    // 使用控制
    manualControl = useManualControl(mqttState, deviceTopicInfo.value, flightController)
})
onBeforeUnmount(() => {
    destroyConnect()
})
</script>
<style scoped lang="scss">
@@ -97,19 +206,133 @@
    align-items: center;
}
.pointControl {
    position: absolute;
    bottom: 0;
    right: 0;
    width: 1540px;
    height: 217px;
    background: rgba(255, 255, 255, 0.3);
    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;
    .direction {
        width: 476px;
        height: 188px;
        background: rgb(0, 0, 0, 0.4); /* 半透明背景 */
        border-radius: 40px 40px 40px 40px;
        display: flex;
        align-items: center;
        justify-content: space-around;
        .boxTitle{
            font-family: Segoe UI, Segoe UI;
            font-weight: 400;
            font-size: 14px;
            color: #D2E8FA;
        }
        .btnGroup {
            display: flex;
            flex-direction: column;
            gap: 10px 0;
            .btnGroupT,
            .btnGroupB {
                width: 238px;
                height: 73px;
            }
        }
        .upAndDown{
            display: flex;
            flex-direction: column;
            gap: 10px 0;
            .btnGroupT,
            .btnGroupB {
                width: 58px;
                height: 73px;
            }
        }
        .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;
                }
            }
        }
    }
}
</style>
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/CurrentTaskDetails.vue
@@ -10,14 +10,16 @@
    >
        <div class="content-container" v-if="isShow">
            <TaskDetailsHead/>
            <TaskDetailsLeft/>
            <!-- 视频直播 -->
            <div class="video-container">
                <LiveVideo :videoUrl="currentLiveUrl" />
            </div>
            <!-- 展示地图 -->
            <RealTimeMap class="realTimeMap" />
            <ControlPanel />
<!--            <ControlPanel v-if="deviceOsdInfo?.data?.sn" />-->
        </div>
        <ControlPanel v-if="deviceOsdInfo?.data?.sn" />
    </el-dialog>
</template>
@@ -34,6 +36,7 @@
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({
@@ -135,6 +138,7 @@
    justify-content: space-between;
    .el-dialog {
        border-radius: 40px;
        position: relative;
        margin-top: 38px;
        width: 1782px;
@@ -167,6 +171,8 @@
.content-container {
    height: 100%;
    width: 100%;
    border-radius: 4rem;
    overflow: hidden;
    .video-container {
        width: 100%;
@@ -183,6 +189,7 @@
        height: 217px;
        border-radius: 0px 20px 0px 40px;
        border: 1px solid #62a1ff;
        overflow: hidden;
    }
}
</style>
src/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/TaskDetailsHead.vue
@@ -48,8 +48,8 @@
    z-index: 5;
    width: 100%;
    height: 68px;
    background: rgba(255, 255, 255, 0.1); /* 半透明背景 */
    backdrop-filter: blur(3px);
    background: rgb(0,0,0,.4); /* 半透明背景 */
    backdrop-filter: blur(5px);
    padding: 0 31px;
    display: flex;
    align-items: center;
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>