Merge branch 'master' of http://139.196.74.78:10010/r/drone/command-center-dashboard
7 files modified
9 files added
| | |
| | | "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", |
| New file |
| | |
| | | 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, |
| | | }) |
| | | } |
| New file |
| | |
| | | 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', // 图传链路延时信息上报 |
| | | } |
| New file |
| | |
| | | import mitt from 'mitt' |
| | | |
| | | const emitter = mitt() |
| | | |
| | | export default emitter |
| New file |
| | |
| | | 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, |
| | | } |
| | | } |
| New file |
| | |
| | | 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, |
| | | } |
| | | } |
| New file |
| | |
| | | |
| | | export const OPTIONS = { |
| | | clean: true, // true: 清除会话, false: 保留会话 |
| | | connectTimeout: 10000, // mqtt 超时时间 |
| | | resubscribe: true, // 断开重连后,再次订阅原订阅 |
| | | reconnectPeriod: 10000, // 重连间隔时间: 5s |
| | | keepalive: 1, // 心跳间隔时间:1s |
| | | } |
| New file |
| | |
| | | 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, |
| | | }) |
| | | } |
| | | } |
| | |
| | | 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', |
| | |
| | | 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({ |
| | |
| | | </script> |
| | | <style lang="scss" scoped> |
| | | .instrument-content { |
| | | height: 245px; |
| | | width: 280px; |
| | | height: 100%; |
| | | width: 100%; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | |
| | | <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> |
| | | |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | :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> |
| | | |
| | |
| | | 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({ |
| | |
| | | let taskDetails = ref({}) |
| | | const currentLiveUrl = ref('') |
| | | const machineNestUrl = ref('') |
| | | const dockLiveUrl = ref('') |
| | | |
| | | const droneLiveUrl = ref('') |
| | | provide('taskDetails', taskDetails) |
| | | |
| | | const deviceOsdInfo = ref({}) |
| | |
| | | // 机巢直播 |
| | | 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 |
| | | } |
| | | |
| | | // 设置当前直播地址 |
| | |
| | | }) |
| | | </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> |
| | |
| | | </script> |
| | | <style scoped lang="scss"> |
| | | #currentTaskMap { |
| | | width: 50%; |
| | | padding-left: 10px; |
| | | height: 100%; |
| | | |
| | | :deep() { |
| | | .cesium-viewer { |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | <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> |