<template>
|
<div class="pointControl">
|
<BaseControl />
|
|
<div class="direction">
|
<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="speed">
|
<el-icon class="btnIcon" @click="speed = speed + 1">
|
<Plus />
|
</el-icon>
|
<div>
|
{{ speed }}
|
<br />
|
m/s
|
</div>
|
<el-icon class="btnIcon" @click="speed = speed - 1">
|
<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.KEY_C)" @mouseup="onMouseUp">C</div>
|
</div>
|
</div>
|
<div class="btnGroupT">
|
<div class="btnItem">
|
<div class="btn" @mousedown="onMouseDown(KeyCode.KEY_Z)" @mouseup="onMouseUp">Z</div>
|
<el-icon class="btnIcon">
|
<Bottom />
|
</el-icon>
|
</div>
|
</div>
|
</div>
|
</div>
|
<!-- 指南针-->
|
<div class="compass">
|
<ControlComPass :options="compassOptions" />
|
</div>
|
|
<div class="ptzControlBox">
|
<div class="boxTitle">
|
云
|
<br />
|
台
|
<br />
|
控
|
<br />
|
制
|
</div>
|
<div class="ptzControlBtnBox">
|
<div class="ptzControlBtn b-r">
|
<div
|
v-for="(item, index) in list5"
|
:style="item.style"
|
class="ptzControlItem"
|
@mousedown="onMouseDown(item.key)"
|
@mouseup="onMouseUp"
|
></div>
|
|
<div
|
class="ptzControlItemIcon"
|
v-for="(item, index) in list5"
|
:style="{ transform: `rotate(${index * 90}deg)` }"
|
>
|
<el-icon>
|
<CaretRight />
|
</el-icon>
|
</div>
|
<div class="circles1 b-r">
|
<div class="circles2 b-r">
|
<div class="circles3 b-r">
|
<div class="circles4 b-r"></div>
|
</div>
|
</div>
|
</div>
|
</div>
|
<div class="videoBox" v-if="valueTime !== '00:00:00'">
|
<div class="videoName">录像</div>
|
<div class="videoPoint"></div>
|
<div class="videoTime">{{ valueTime }}</div>
|
</div>
|
</div>
|
|
<div class="divider"></div>
|
<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 + (item.unit || '') }}</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
<script setup>
|
import ControlComPass from '@/components/CurrentTaskDetails/ControlPanel/ControlComPass/ControlComPass.vue'
|
import { KeyCode, useManualControl } from '@/hooks/controlDrone/useManualControl'
|
import { droneController, exitController, postDrc, returnHome, returnHomeCancel } from '@/api/drc'
|
import { ElMessage } from 'element-plus'
|
import { useStore } from 'vuex'
|
import { UranusMqtt } from '@/mqtt'
|
import {
|
ArrowDown,
|
ArrowLeft,
|
ArrowRight,
|
ArrowUp,
|
Bottom,
|
CaretRight,
|
Minus,
|
Plus,
|
RefreshLeft,
|
RefreshRight,
|
} from '@element-plus/icons-vue'
|
import _ from 'lodash'
|
import BaseControl from '@/components/CurrentTaskDetails/ControlPanel/BaseControl.vue'
|
import EventBus from '@/event-bus'
|
import { getPayloadControlApi } from '@/api/payload'
|
import { directionMap } from '@/const/drc'
|
|
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 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')
|
let timer = null
|
let totalSeconds = 0
|
|
const workspace_id = computed(() => taskDetails?.value?.workspace_id)
|
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 },
|
]
|
|
const speed = ref(5)
|
provide('speed', speed)
|
|
const list5 = [
|
{ name: '上', key: KeyCode.ARROW_UP, operate: 'up', style: { top: '-70%' } },
|
{ name: '右', key: KeyCode.ARROW_RIGHT, operate: 'right', style: { left: '70%' } },
|
{ name: '下', key: KeyCode.ARROW_DOWN, operate: 'down', style: { top: '70%' } },
|
{ name: '左', key: KeyCode.ARROW_LEFT, operate: 'left', style: { left: '-70%' } },
|
]
|
|
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: zoom_factor },
|
{ name: '俯仰角度', value: pitchAngle.value.angle, unit: '°' },
|
{ name: '横向角度', value: yawAngle.value.angle, unit: '°' },
|
],
|
[
|
{ name: '储存', value: usedStorageGB, unit: 'G' },
|
{ name: '方向', value: pitchAngle.value.direction },
|
{ name: '方向', value: yawAngle.value.direction },
|
],
|
]
|
})
|
|
const pitchAngle = computed(() => {
|
const { payloads } = device_osd_host?.value || {}
|
const gimbal_pitch = payloads?.[0]?.gimbal_pitch || 0
|
let direction = ''
|
if (gimbal_pitch > -2 && gimbal_pitch < 2) {
|
direction = '正前'
|
} else if (gimbal_pitch >= 2 && gimbal_pitch < 90) {
|
direction = '斜上'
|
} else if (gimbal_pitch === 90) {
|
direction = '正上'
|
} else if (gimbal_pitch <= -2 && gimbal_pitch > -90) {
|
direction = '斜下'
|
} else if (gimbal_pitch === -90 || gimbal_pitch < -90) {
|
direction = '正下'
|
}
|
return {
|
angle: _.round(gimbal_pitch || 0, 1),
|
direction,
|
}
|
})
|
|
const yawAngle = computed(() => {
|
let { payloads, attitude_head } = device_osd_host?.value || {}
|
const gimbal_yaw = payloads?.[0]?.gimbal_yaw || 0
|
attitude_head = attitude_head || 0
|
let yaw = ''
|
if (gimbal_yaw > 180) {
|
yaw = gimbal_yaw
|
} else {
|
yaw = gimbal_yaw - attitude_head
|
}
|
let result = 0
|
if (yaw < 0) {
|
result = yaw + 360
|
}
|
if (yaw > 0) {
|
result = yaw
|
}
|
if ((yaw > -2 && yaw < 2) || parseInt(attitude_head) === parseInt(gimbal_yaw)) {
|
result = attitude_head < 0 ? 180 + (180 + attitude_head) : attitude_head
|
}
|
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),
|
direction,
|
}
|
})
|
|
const deviceTopicInfo = ref({
|
pubTopic: '',
|
subTopic: '',
|
})
|
const flightController = ref(false)
|
// 控制对象
|
let manualControl = {}
|
const isAutoControl = inject('isAutoControl')
|
|
const timeStart = () => {
|
stop() // 避免重复启动
|
timer = setInterval(() => {
|
totalSeconds++
|
const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, '0')
|
const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, '0')
|
const secs = String(totalSeconds % 60).padStart(2, '0')
|
valueTime.value = `${hours}:${minutes}:${secs}`
|
}, 1000)
|
}
|
|
const timeStop = () => {
|
if (timer) {
|
clearInterval(timer)
|
timer = null
|
totalSeconds = 0
|
valueTime.value = '00:00:00'
|
}
|
}
|
|
// 按下操作
|
function onMouseDown(type) {
|
manualControl?.handleKeyup(type)
|
}
|
|
// 弹起操作
|
function onMouseUp() {
|
manualControl?.resetControlState()
|
}
|
|
// 取消手动控制
|
function cancelControl() {
|
exitController({ client_id: client_id.value, dock_sn: dockSn.value })
|
.then(res => {
|
flightController.value = false
|
deviceTopicInfo.value.subTopic = ''
|
deviceTopicInfo.value.pubTopic = ''
|
ElMessage.success('退出飞行控制成功')
|
})
|
.catch(e => {})
|
}
|
|
// 获得有效载荷控制
|
function getPayloadControl() {
|
getPayloadControlApi({ sn: dockSn.value }).then(res => {
|
ElMessage.success('成功获得有效载荷控制')
|
})
|
}
|
|
// 手动控制
|
function control() {
|
if (!client_id.value) return ElMessage.error('无人机不在空中,不能进入指挥飞行模式。')
|
if (!dockSn.value) return ElMessage.error('系统错误,未获取到dock_sn')
|
droneController({ client_id: client_id.value, dock_sn: dockSn.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('控制成功')
|
getPayloadControl()
|
isAutoControl.value = false
|
})
|
}
|
|
// 返航
|
function onBackDock() {
|
returnHome(dockSn?.value).then(res => {
|
ElMessage.success('返航操作成功')
|
})
|
}
|
|
// 取消返航
|
function cancelBackDock() {
|
returnHomeCancel(dockSn?.value).then(res => {
|
ElMessage.success('取消返航成功')
|
})
|
}
|
|
// 创建mqtt连接
|
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 = ''
|
}
|
}
|
|
// 返航或取消返航
|
const returnOrCancelReturn = () => {
|
if (device_osd_host?.value?.mode_code === 9) {
|
cancelBackDock()
|
} else {
|
onBackDock()
|
}
|
}
|
|
// useManualControl里面用的参数
|
const paramsRef = computed(() => ({
|
droneSn: droneSn.value,
|
dockSn: dockSn.value,
|
speed: speed.value,
|
}))
|
|
watch(
|
() => workspace_id.value,
|
async () => {
|
if (workspace_id.value) {
|
await createConnect()
|
// 使用控制
|
manualControl = useManualControl(mqttState, deviceTopicInfo.value, flightController, paramsRef)
|
}
|
}
|
)
|
|
onMounted(async () => {
|
EventBus.on('controlPanel-control', control)
|
EventBus.on('controlPanel-cancelControl', cancelControl)
|
EventBus.on('controlPanel-returnOrCancelReturn', returnOrCancelReturn)
|
EventBus.on('controlPanel-onMouseDown', onMouseDown)
|
EventBus.on('controlPanel-timeStart', timeStart)
|
EventBus.on('controlPanel-timeStop', timeStop)
|
EventBus.on('controlPanel-getPayloadControl', getPayloadControl)
|
})
|
|
onBeforeUnmount(() => {
|
EventBus.off('controlPanel-control', control)
|
EventBus.off('controlPanel-cancelControl', cancelControl)
|
EventBus.off('controlPanel-returnOrCancelReturn', returnOrCancelReturn)
|
EventBus.off('controlPanel-onMouseDown', onMouseDown)
|
EventBus.off('controlPanel-timeStart', timeStart)
|
EventBus.off('controlPanel-timeStop', timeStop)
|
EventBus.off('controlPanel-getPayloadControl', getPayloadControl)
|
destroyConnect()
|
})
|
</script>
|
|
<style scoped lang="scss">
|
.f-c {
|
display: flex;
|
align-items: center;
|
}
|
|
.boxTitle {
|
font-family: Segoe UI, Segoe UI;
|
font-weight: 400;
|
font-size: 14px;
|
color: #d2e8fa;
|
}
|
|
.b-r {
|
border-radius: 50%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.pointControl {
|
position: absolute;
|
bottom: 0;
|
right: 0;
|
width: 1400px;
|
height: 217px;
|
background: linear-gradient(196deg, rgba(23, 23, 23, 0.11) 0%, rgba(6, 6, 6, 0.11) 100%);
|
backdrop-filter: blur(5px);
|
border-radius: 40px 0px 40px 40px;
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
color: white;
|
gap: 0 10px;
|
pointer-events: all;
|
|
.direction {
|
width: 400px;
|
height: 188px;
|
background: rgb(0, 0, 0, 0.4); /* 半透明背景 */
|
border-radius: 40px 40px 40px 40px;
|
display: flex;
|
align-items: center;
|
justify-content: space-evenly;
|
|
.btnGroup {
|
display: flex;
|
flex-direction: column;
|
gap: 10px 0;
|
|
.btnGroupT,
|
.btnGroupB {
|
width: 180px;
|
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;
|
}
|
}
|
}
|
|
.ptzControlBox {
|
width: 386px;
|
height: 188px;
|
background: rgb(0, 0, 0, 0.4); /* 半透明背景 */
|
border-radius: 40px 40px 40px 40px;
|
display: flex;
|
align-items: center;
|
justify-content: space-evenly;
|
|
.ptzControlBtnBox {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
|
.ptzControlBtn {
|
position: relative;
|
overflow: hidden;
|
width: 154px;
|
height: 154px;
|
background: linear-gradient(180deg, #818181 0%, #222222 100%);
|
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
|
|
.ptzControlItemIcon {
|
pointer-events: none;
|
width: 75px;
|
display: flex;
|
justify-content: right;
|
font-size: 18px;
|
position: absolute;
|
}
|
|
.ptzControlItem {
|
position: absolute;
|
width: 100%;
|
height: 100%;
|
transform: rotate(45deg);
|
|
&:hover {
|
cursor: pointer;
|
box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.3);
|
}
|
}
|
|
.circles1 {
|
width: 130px;
|
height: 130px;
|
background: linear-gradient(360deg, #282828 23%, #3a3a3a 70%, #3c3a3a 95%);
|
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
|
|
.circles2 {
|
width: 79px;
|
height: 79px;
|
background: linear-gradient(180deg, #484848 0%, #3f3f3f 100%);
|
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25), inset -1px -2px 4px 0px rgba(255, 255, 255, 0.25);
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
.circles3 {
|
width: 42px;
|
height: 42px;
|
background: rgba(0, 0, 0, 0.31);
|
z-index: 1;
|
|
.circles4 {
|
width: 23px;
|
height: 23px;
|
background: #ffffff;
|
box-shadow: 2px 4px 6px 0px rgba(35, 37, 39, 0.26);
|
}
|
}
|
}
|
}
|
}
|
|
.videoBox {
|
display: flex;
|
align-items: center;
|
font-family: Segoe UI, Segoe UI;
|
margin-top: 5px;
|
|
.videoName {
|
font-weight: 400;
|
font-size: 12px;
|
color: rgba(210, 232, 250, 0.57);
|
}
|
|
.videoPoint {
|
width: 3px;
|
height: 3px;
|
background: #12ff7f;
|
border-radius: 50%;
|
margin: 0 5px;
|
}
|
|
.videoTime {
|
font-weight: 400;
|
font-size: 16px;
|
color: #ffffff;
|
line-height: 15px;
|
}
|
}
|
}
|
|
.divider {
|
position: absolute;
|
transform: translateX(90px);
|
width: 0;
|
height: 137px;
|
border: 1px solid rgba(255, 255, 255, 0.07);
|
}
|
|
.info {
|
display: flex;
|
flex-direction: column;
|
gap: 7px 0;
|
width: 60px;
|
|
.infoName {
|
height: 25px;
|
font-family: Segoe UI, Segoe UI, serif;
|
font-weight: 400;
|
font-size: 11px;
|
color: rgba(210, 232, 250, 0.57);
|
line-height: 15px;
|
}
|
|
.infoValue {
|
height: 25px;
|
font-family: Segoe UI, Segoe UI, serif;
|
font-weight: 400;
|
font-size: 16px;
|
color: #ffffff;
|
line-height: 15px;
|
}
|
}
|
}
|
|
.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: space-evenly;
|
|
.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>
|