<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.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>
|
|
<div class="compass">
|
<ControlComPass />
|
</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"></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 list4" class="info">
|
<div v-for="item in arr" class="infoItem">
|
<div class="infoName">{{ item.name }}</div>
|
<div class="infoValue">{{ item.value }}</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
<script setup>
|
import ControlComPass from '../ControlComPass/ControlComPass.vue'
|
import { KeyCode, useManualControl } from '@/hooks/controlDrone/useManualControl'
|
import { droneController, exitController, postDrc, postDrcExit, 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,
|
CaretTop,
|
Minus,
|
Plus,
|
RefreshLeft,
|
RefreshRight,
|
} from '@element-plus/icons-vue'
|
|
import controlCenterImg from '@/assets/images/taskManagement/taskIntermediateContent/controlCenter.png'
|
import BaseControl from '@/components/CurrentTaskDetails/ControlPanel/BaseControl.vue'
|
import EventBus from '@/event-bus'
|
import dayjs from 'dayjs'
|
|
const deviceOsdInfo = inject('deviceOsdInfo')
|
const taskDetails = inject('taskDetails')
|
const dockSn = inject('dockSn')
|
const droneSn = inject('droneSn')
|
|
|
const store = useStore()
|
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: '上', style: { top: '-70%' }, imgStyle: { top: '20%', left: '50%' } },
|
{ name: '右', style: { left: '70%' }, imgStyle: { right: '0', top: '50%' } },
|
{ name: '下', style: { top: '70%' }, imgStyle: { bottom: '0', left: '50%' } },
|
{ name: '左', style: { left: '-70%' }, imgStyle: { left: '20%', top: '50%' } },
|
]
|
|
const list4 = [
|
[
|
{ name: '焦距倍数', value: '0' },
|
{ name: '俯仰角度', value: '0.0°' },
|
{ name: '横向角度', value: '0.0°' },
|
],
|
[
|
{ name: '储存', value: '64.5G' },
|
{ name: '方向', value: '正北' },
|
{ name: '方向', value: '正北' },
|
],
|
]
|
|
|
|
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 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('控制成功')
|
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 (deviceOsdInfo.value?.data?.host?.mode_code === 9) {
|
cancelBackDock()
|
} else {
|
onBackDock()
|
}
|
}
|
|
// useManualControl里面用的参数
|
const paramsRef = computed(()=>({
|
droneSn:droneSn.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)
|
})
|
|
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)
|
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>
|