1412 lines
39 KiB
Vue
1412 lines
39 KiB
Vue
<template>
|
||
<!-- 飞行控制主容器 -->
|
||
<div class="flight-control">
|
||
<!-- 顶部状态栏 -->
|
||
<div class="flight-control-top">
|
||
<!-- 机场选择组件 -->
|
||
<SelectComponent @selectChange="changeSelect" />
|
||
<!-- 机场名称 -->
|
||
<div class="airport-name">
|
||
<img src="@/assets/images/flightcontrol/logo.png" alt="机场logo" />
|
||
{{ airportName }}
|
||
</div>
|
||
<!-- 机场状态 -->
|
||
<span class="airport-status" v-if="airporOsdVal.flighttask_step_code">
|
||
<template v-if="airporOsdVal.flighttask_step_code == 0">作业准备中</template>
|
||
<template v-else-if="airporOsdVal.flighttask_step_code == 1">飞行作业中</template>
|
||
<template v-else-if="airporOsdVal.flighttask_step_code == 2">作业后状态恢复</template>
|
||
<template v-else-if="airporOsdVal.flighttask_step_code == 3">自定义飞行区更新中</template>
|
||
<template v-else-if="airporOsdVal.flighttask_step_code == 4">地形障碍物更新中</template>
|
||
<template v-else-if="airporOsdVal.flighttask_step_code == 5">任务空闲</template>
|
||
<template v-else-if="airporOsdVal.flighttask_step_code == 255">飞行器异常</template>
|
||
<template v-else-if="airporOsdVal.flighttask_step_code == 256">未知状态</template>
|
||
</span>
|
||
<!-- 机场直播按钮 -->
|
||
<span class="airport-live" @click="getRtmpList">机场直播</span>
|
||
<!-- 机场环境信息 -->
|
||
<div class="airport-info">
|
||
<img src="@/assets/images/flightcontrol/temperature.png" alt="温度图标" />
|
||
<span class="airport-info-title">舱外温度</span>
|
||
<span>{{ airporOsdVal.temperature }}℃</span>
|
||
</div>
|
||
<div class="airport-info">
|
||
<img src="@/assets/images/flightcontrol/rainfall.png" alt="雨量图标" />
|
||
<span class="airport-info-title">雨量</span>
|
||
<span>
|
||
<template v-if="airporOsdVal.rainfall == 0">无雨</template>
|
||
<template v-else-if="airporOsdVal.rainfall == 1">小雨</template>
|
||
<template v-else-if="airporOsdVal.rainfall == 2">中雨</template>
|
||
<template v-else-if="airporOsdVal.rainfall == 3">大雨</template>
|
||
</span>
|
||
</div>
|
||
<div class="airport-info">
|
||
<img src="@/assets/images/flightcontrol/wind_speed.png" alt="风速图标" />
|
||
<span class="airport-info-title">风速</span>
|
||
<span>{{ airporOsdVal.wind_speed }}m/s</span>
|
||
</div>
|
||
<div class="airport-info">
|
||
<img src="@/assets/images/flightcontrol/rate.png" alt="网络图标" />
|
||
<span class="airport-info-title">网络</span>
|
||
<span>{{ airporOsdVal.rate }}KB/s</span>
|
||
</div>
|
||
</div>
|
||
<!-- 主内容区域 -->
|
||
<div class="flight-contain">
|
||
<!-- 飞行器控制盒打开按钮 -->
|
||
<div class="uavbox-open" @click="openUavBox" v-if="!uavBoxVisible">
|
||
<img src="@/assets/images/flightcontrol/uavbox-open.png" alt="打开飞行器控制盒" />
|
||
</div>
|
||
<!-- 地图容器 -->
|
||
<div class="map-container" :style="uavBoxVisible
|
||
? 'width:38vw;height:' + mapHeight + 'px'
|
||
: 'width:90vw;height:' + mapHeight + 'px'
|
||
">
|
||
<div class="map-container-content">
|
||
<!-- 地图组件 -->
|
||
<Map @flyToThere="flyToThere" :airPort="airPort" :uavTrack="uavTrack" :allAreaDataList="allAreaDataList"
|
||
@takeOff="takeOff" />
|
||
</div>
|
||
</div>
|
||
<!-- 飞行器控制盒 -->
|
||
<!-- <div class="uav-box" ref="uavBox" v-if="uavBoxVisible"> -->
|
||
<div class="uav-box" ref="uavBox" v-if="1">
|
||
<UavBox @closeUavBox="uavBoxVisible = false" @flyLog="logVisible = true" @flyTo="flyTobtn" @patrol="patrolbtn"
|
||
@taskSubmitted="handleTaskSubmitted" />
|
||
</div>
|
||
<!-- 飞行日志面板 -->
|
||
<div class="fly-log" v-if="logVisible">
|
||
<div class="fly-log-title">
|
||
<span>飞行日志</span>
|
||
<span @click="logVisible = false">
|
||
<CloseCircleOutlined />
|
||
</span>
|
||
</div>
|
||
<div class="fly-log-content">
|
||
<div>检查项</div>
|
||
<div class="fly-status">所有检查项均通过,飞行器将立即执行飞行任务。</div>
|
||
<div class="log-opstions" ref="myDiv">
|
||
<template v-for="(item, index) in flyOptions" :key="index">
|
||
<div class="log-item MinusCircleFilled" v-if="item.type == 'info'">
|
||
<MinusCircleFilled />
|
||
<span class="item-desc">{{ item.title }}:{{ item.content }}</span>
|
||
</div>
|
||
<div class="log-item CheckCircleFilled" v-else-if="item.type == 'success'">
|
||
<CheckCircleFilled />
|
||
<span class="item-desc">{{ item.title }}:{{ item.content }}</span>
|
||
</div>
|
||
<div class="log-item CloseCircleFilled" v-else-if="item.type == 'error'">
|
||
<CloseCircleFilled />
|
||
<span class="item-desc">{{ item.title }}:{{ item.content }}</span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 指点飞行面板 -->
|
||
<div class="fly-to-form" v-if="flyToVisible">
|
||
<div class="fly-to-title">
|
||
<span class="title-name">指点飞行</span>
|
||
<span class="title-icon" @click="flyToVisible = false">
|
||
<CloseOutlined />
|
||
</span>
|
||
</div>
|
||
<span class="form-desc">点击地图选择目标点,点击开始执行,飞行器将飞到指定地点。绘制路线仅供参考。</span>
|
||
<div class="form-content">
|
||
<div class="form-item">
|
||
<span>经度</span>
|
||
<div>
|
||
<a-input v-model:value="flyToData.points[0].longitude" />
|
||
</div>
|
||
</div>
|
||
<div class="form-item">
|
||
<span>纬度</span>
|
||
<div>
|
||
<a-input v-model:value="flyToData.points[0].latitude" />
|
||
</div>
|
||
</div>
|
||
<div class="form-item">
|
||
<span>高度</span>
|
||
<div>
|
||
<a-input v-model:value="flyToData.points[0].height" />
|
||
m
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="fly-to-button">
|
||
<span @click="stop">结束执行</span>
|
||
<span @click="execute">开始执行</span>
|
||
</div>
|
||
</div>
|
||
<!-- 机场直播面板 -->
|
||
<div class="airport-live-contain" v-show="liveVisible">
|
||
<div class="live-title">
|
||
<span title="刷新">
|
||
<img src="@/assets/images/flightcontrol/refresh.png" alt="刷新" @click="reloadLive" />
|
||
</span>
|
||
<span class="关闭">
|
||
<img src="@/assets/images/flightcontrol/close.png" alt="关闭" @click="closeLive" />
|
||
</span>
|
||
</div>
|
||
<div class="player">
|
||
<video id="player-container-id" :width="width" :height="height" preload="auto" playsinline webkit-playsinline>
|
||
</video>
|
||
</div>
|
||
</div>
|
||
<!-- 智能巡检面板 -->
|
||
<div class="patrol-btn" v-if="patrolVisible">
|
||
<Patrol @changePatrol="changePatrol" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<script lang="ts" setup>
|
||
// 导入Vue核心模块
|
||
import { ref, computed, onMounted, watch, reactive, nextTick, onUnmounted } from 'vue';
|
||
// 导入自定义组件
|
||
import { SelectComponent, UavBox, Map, Patrol } from './index';
|
||
// 导入状态管理
|
||
import { airPortStore } from '@/store/modules/airport';
|
||
import { useUserStore } from '@/store/modules/user';
|
||
// 导入MQTT工具
|
||
import { getClient, createConnection, clientSubscribe, destroyConnection } from '@/utils/mqtt';
|
||
// 导入事件总线
|
||
import { EventBus } from '@/utils/eventBus';
|
||
// 导入API接口
|
||
import { GetWorkAreaList } from '@/api/demo/mediaLibrary';
|
||
import {
|
||
getDronePortInfoFromRedis,
|
||
saveHandFlyTask,
|
||
getLastHandFlyTask,
|
||
} from '@/api/workmanagement/droneDock';
|
||
import { getOperationLogList, isCanFly } from '@/api/workmanagement/airportMaintenance';
|
||
// 导入工具函数
|
||
import { WktToGeojson } from '@/components/MapboxMaps/src/WktGeojsonTransform';
|
||
import { buildGUID, uuid } from '@/utils/uuid';
|
||
import {
|
||
servicesTopic,
|
||
services_replyTopic,
|
||
errorName,
|
||
unServices_replyTopic,
|
||
} from '@/utils/debugging/remote';
|
||
import { servicesTopicReize } from '@/utils/debugging/events';
|
||
// 导入UI组件
|
||
import {
|
||
CloseCircleOutlined,
|
||
MinusCircleFilled,
|
||
CheckCircleFilled,
|
||
CloseCircleFilled,
|
||
CloseOutlined,
|
||
} from '@ant-design/icons-vue';
|
||
import { useMessage } from '@/hooks/web/useMessage';
|
||
// 导入第三方库
|
||
import axios from 'axios';
|
||
import TCPlayer from 'tcplayer.js';
|
||
import 'tcplayer.js/dist/tcplayer.min.css'; //引入插件和样式文件
|
||
import { io, Socket } from 'socket.io-client';
|
||
|
||
// 消息提示实例
|
||
const { createConfirm, createMessage } = useMessage();
|
||
// 飞行器控制盒引用
|
||
const uavBox = ref();
|
||
|
||
// 地图高度
|
||
const mapHeight = ref(document.documentElement.clientHeight - 124);
|
||
// 机场状态存储
|
||
const airPortStoreVal = airPortStore();
|
||
const airPortInfo = airPortStoreVal.getAirport;
|
||
const live_info = airPortStoreVal.getLiveInfo;
|
||
const uavInfo = airPortStoreVal.getUAV;
|
||
const typeId = airPortStoreVal.getTypeId;
|
||
// 直播相关
|
||
const liveCode = ref(airPortInfo.sn);
|
||
const airportName = ref(airPortInfo.name);
|
||
// 用户信息
|
||
const userStore = useUserStore();
|
||
const userInfo = userStore.getUserInfo;
|
||
|
||
// 机场OSD信息
|
||
const airporOsdVal = ref({
|
||
flighttask_step_code: 256, // 飞行任务步骤代码
|
||
temperature: 0, // 温度
|
||
rainfall: 0, // 雨量
|
||
wind_speed: 0, // 风速
|
||
rate: 0, // 网络速率
|
||
});
|
||
|
||
// 位置信息
|
||
const locationVal: any = ref({});
|
||
// 机场位置信息
|
||
const airPort = ref({
|
||
latitude: null, // 纬度
|
||
longitude: null, // 经度
|
||
height: null, // 高度
|
||
});
|
||
|
||
// 无人机轨迹
|
||
const uavTrack = ref({});
|
||
// 工作区域数据
|
||
const allAreaDataList: any = ref([]);
|
||
// 选择值
|
||
const changeSelectValue = ref();
|
||
// 飞行器控制盒可见性
|
||
const uavBoxVisible = ref(false);
|
||
// 飞行日志可见性
|
||
const logVisible = ref(false);
|
||
// 飞行日志选项
|
||
const flyOptions = ref(airPortStoreVal.getflyLog);
|
||
// 指点飞行可见性
|
||
const flyToVisible = ref(false);
|
||
|
||
// Socket相关
|
||
const socket = ref<Socket | null>(null);
|
||
const socketConnected = ref(false);
|
||
const socketUrl = ref('http://localhost:5000'); // Socket服务器地址
|
||
|
||
// 连接Socket服务
|
||
const connectSocket = () => {
|
||
try {
|
||
if (socket.value) {
|
||
socket.value.disconnect();
|
||
}
|
||
|
||
socket.value = io(socketUrl.value, {
|
||
transports: ['websocket'],
|
||
reconnection: true,
|
||
reconnectionAttempts: 5,
|
||
reconnectionDelay: 1000
|
||
});
|
||
|
||
socket.value.on('connect', () => {
|
||
console.log('Socket连接成功');
|
||
socketConnected.value = true;
|
||
airPortStoreVal.setflyLog({
|
||
type: 'info',
|
||
title: 'Socket连接',
|
||
content: 'Socket服务连接成功'
|
||
});
|
||
});
|
||
|
||
socket.value.on('disconnect', () => {
|
||
console.log('Socket断开连接');
|
||
socketConnected.value = false;
|
||
airPortStoreVal.setflyLog({
|
||
type: 'info',
|
||
title: 'Socket连接',
|
||
content: 'Socket服务断开连接'
|
||
});
|
||
});
|
||
|
||
socket.value.on('task_status_update', (data) => {
|
||
console.log('任务状态更新:', data);
|
||
airPortStoreVal.setflyLog({
|
||
type: 'info',
|
||
title: '任务状态更新',
|
||
content: `任务 ${data.task_id} 状态: ${data.status}`
|
||
});
|
||
});
|
||
|
||
socket.value.on('inference_result', (data) => {
|
||
console.log('推理结果:', data);
|
||
airPortStoreVal.setflyLog({
|
||
type: 'info',
|
||
title: '推理结果',
|
||
content: `任务 ${data.task_id} 模型 ${data.model_id} 推理完成`
|
||
});
|
||
});
|
||
|
||
socket.value.on('system_notification', (data) => {
|
||
console.log('系统通知:', data);
|
||
airPortStoreVal.setflyLog({
|
||
type: data.type === 'error' ? 'error' : 'info',
|
||
title: '系统通知',
|
||
content: data.message
|
||
});
|
||
});
|
||
|
||
socket.value.on('connect_error', (error) => {
|
||
console.error('Socket连接错误:', error);
|
||
socketConnected.value = false;
|
||
airPortStoreVal.setflyLog({
|
||
type: 'error',
|
||
title: 'Socket连接',
|
||
content: `Socket服务连接失败: ${error.message}`
|
||
});
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('连接Socket服务失败:', error);
|
||
airPortStoreVal.setflyLog({
|
||
type: 'error',
|
||
title: 'Socket连接',
|
||
content: `连接Socket服务失败: ${error.message}`
|
||
});
|
||
}
|
||
};
|
||
|
||
// 断开Socket连接
|
||
const disconnectSocket = () => {
|
||
if (socket.value) {
|
||
socket.value.disconnect();
|
||
socket.value = null;
|
||
socketConnected.value = false;
|
||
console.log('Socket连接已断开');
|
||
}
|
||
};
|
||
|
||
// 订阅任务
|
||
const subscribeTask = (taskId: string) => {
|
||
if (socket.value && socketConnected.value) {
|
||
socket.value.emit('subscribe_task', { task_id: taskId }, (response: any) => {
|
||
console.log('订阅任务响应:', response);
|
||
if (response.status === 'subscribed') {
|
||
airPortStoreVal.setflyLog({
|
||
type: 'info',
|
||
title: '任务订阅',
|
||
content: `成功订阅任务 ${taskId}`
|
||
});
|
||
} else {
|
||
airPortStoreVal.setflyLog({
|
||
type: 'error',
|
||
title: '任务订阅',
|
||
content: `订阅任务失败: ${response.message}`
|
||
});
|
||
}
|
||
});
|
||
} else {
|
||
airPortStoreVal.setflyLog({
|
||
type: 'error',
|
||
title: '任务订阅',
|
||
content: 'Socket未连接,无法订阅任务'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 处理任务提交事件
|
||
const handleTaskSubmitted = (data: any) => {
|
||
console.log('接收到任务提交事件:', data);
|
||
if (data.success && data.taskId) {
|
||
// 连接Socket服务
|
||
connectSocket();
|
||
|
||
// 延迟订阅任务,确保Socket连接成功
|
||
setTimeout(() => {
|
||
subscribeTask(data.taskId);
|
||
}, 1000);
|
||
|
||
// 显示飞行日志面板
|
||
logVisible.value = true;
|
||
|
||
// 记录任务提交成功日志
|
||
airPortStoreVal.setflyLog({
|
||
type: 'success',
|
||
title: '任务提交',
|
||
content: `智能巡检任务提交成功,已连接Socket服务监听任务状态`
|
||
});
|
||
}
|
||
};
|
||
// 指点飞行数据
|
||
const flyToData = reactive({
|
||
fly_to_id: buildGUID(),
|
||
max_speed: 10,
|
||
points: [{}],
|
||
});
|
||
// 直播可见性
|
||
const liveVisible = ref(false);
|
||
// 巡检可见性
|
||
const patrolVisible = ref(false);
|
||
|
||
// 飞行日志滚动容器引用
|
||
const myDiv = ref(null);
|
||
|
||
// 监听飞行日志变化,自动滚动到底部
|
||
watch(
|
||
() => flyOptions.value,
|
||
() => {
|
||
nextTick(() => {
|
||
if (myDiv.value) {
|
||
myDiv.value.scrollTop = myDiv.value.scrollHeight;
|
||
}
|
||
});
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
// 视频播放器实例
|
||
let player;
|
||
// 播放器尺寸
|
||
const width = ref(300);
|
||
const height = ref(200);
|
||
|
||
// MQTT连接状态
|
||
const connected = ref(false);
|
||
const connectCallback = () => {
|
||
connected.value = true;
|
||
};
|
||
|
||
// 机场MQTT主题
|
||
const topicUrl = computed(() => {
|
||
return 'thing/product/' + airPortInfo.sn + '/osd';
|
||
});
|
||
// 无人机MQTT主题
|
||
const topicUAVUrl = computed(() => {
|
||
return 'thing/product/' + uavInfo.sn + '/osd';
|
||
});
|
||
// 机场选择变更处理
|
||
const changeSelect = async (value?: any) => {
|
||
if (value) {
|
||
changeSelectValue.value = value;
|
||
airportName.value = value.airportName;
|
||
}
|
||
// 订阅机场消息
|
||
const topicUrl = 'thing/product/' + airPortInfo.sn + '/osd';
|
||
clientSubscribe(topicUrl, { qos: 1 });
|
||
|
||
// 获取机场信息
|
||
getDronePortInfoFromRedis({ key: value.airport }).then((res) => {
|
||
const rs = JSON.parse(res);
|
||
// 设置机场位置信息
|
||
airPort.value.latitude = rs.lat;
|
||
airPort.value.longitude = rs.lng;
|
||
airPort.value.height = rs.height;
|
||
|
||
// 如果设备在线,查询任务信息
|
||
if (rs.device_online_status == 1) {
|
||
getLastHandFlyTask({ sn: airPortInfo.sn }).then((res) => {
|
||
if (res.Status == 1 || res.Status == 6) {
|
||
// 设置任务ID
|
||
airPortStoreVal.setTaskId(res.Id);
|
||
// 获取操作日志
|
||
getOperationLogList({ taskid: res.Id, page: 1, limit: 1000 }).then((res) => {
|
||
const optiosns = [];
|
||
res.items.forEach((element) => {
|
||
optiosns.push(JSON.parse(element.data));
|
||
});
|
||
// 更新飞行日志
|
||
airPortStoreVal.setflyLog(optiosns, 'replace');
|
||
flyOptions.value = optiosns;
|
||
});
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// 订阅飞行器消息
|
||
const topicUAVUrl = 'thing/product/' + uavInfo.sn + '/osd';
|
||
clientSubscribe(topicUAVUrl, { qos: 1 });
|
||
};
|
||
|
||
// 打开飞行器控制盒
|
||
const openUavBox = () => {
|
||
getDronePortInfoFromRedis({ key: airPortInfo.sn }).then((res) => {
|
||
const rs = JSON.parse(res);
|
||
if (rs.device_online_status == 1) {
|
||
// 显示飞行器控制盒
|
||
uavBoxVisible.value = true;
|
||
// 调整地图高度
|
||
nextTick(() => {
|
||
mapHeight.value = uavBox.value.clientHeight;
|
||
console.log(uavBox.value.clientHeight);
|
||
});
|
||
} else {
|
||
// 提示飞行器未开机
|
||
createMessage.warning('飞行器未开机');
|
||
}
|
||
});
|
||
};
|
||
// 地图点击定位
|
||
const flyToThere = (e) => {
|
||
// 存储位置信息
|
||
locationVal.value.lat = e._lat;
|
||
locationVal.value.lng = e._lng;
|
||
locationVal.value.alt = e._alt;
|
||
// 触发位置获取事件
|
||
EventBus.emit('obtainTheLocation', locationVal.value);
|
||
};
|
||
// 获取几何类型
|
||
function getGeomType(area) {
|
||
let geom = area.geom;
|
||
let radiusFlag = area.properties.indexOf('radius') > -1 ? true : false;
|
||
// 点类型
|
||
if (geom.indexOf('POINT') > -1 && !radiusFlag) {
|
||
return 'Point';
|
||
}
|
||
// 线类型
|
||
if (geom.indexOf('LINESTRING') > -1 && !radiusFlag) {
|
||
return 'Polyline';
|
||
}
|
||
// 多边形类型
|
||
if (geom.indexOf('POLYGON') > -1 && !radiusFlag) {
|
||
return 'Polygon';
|
||
}
|
||
// 圆形类型
|
||
if (geom.indexOf('POINT') > -1 && radiusFlag) {
|
||
return 'Circle';
|
||
}
|
||
}
|
||
// 飞行任务ID
|
||
const flight_id = ref(uuid(14, 14));
|
||
// 一键起飞
|
||
const takeOff = () => {
|
||
createConfirm({
|
||
iconType: 'info',
|
||
title: '一键起飞',
|
||
content: '确定要一键起飞吗?',
|
||
onOk: async () => {
|
||
// 显示飞行日志
|
||
logVisible.value = true;
|
||
|
||
// 起飞参数
|
||
const data = {
|
||
flight_id: flight_id.value,
|
||
security_takeoff_height: 100, // 安全起飞高度
|
||
rth_altitude: 115, // 返航高度
|
||
rth_mode: 1, // 返航模式
|
||
max_speed: 10, // 最大速度
|
||
commander_flight_mode: 1, // 指挥官飞行模式
|
||
rc_lost_action: 2, // 遥控器丢失动作
|
||
commander_mode_lost_action: 1, // 指挥官模式丢失动作
|
||
commander_flight_height: 115.0, // 指挥官飞行高度
|
||
flight_safety_advance_check: 1, // 飞行安全预先检查
|
||
target_height: Number(airPort.value.height) + 100, // 目标高度
|
||
target_latitude: Number(airPort.value.latitude), // 目标纬度
|
||
target_longitude: Number(airPort.value.longitude), // 目标经度
|
||
};
|
||
|
||
// 适配不同类型的机场
|
||
if (typeId == 'Dock 1') {
|
||
delete data.rth_mode;
|
||
delete data.flight_safety_advance_check;
|
||
}
|
||
|
||
// 构建请求参数
|
||
const querys = {
|
||
bid: buildGUID(),
|
||
data: data,
|
||
tid: buildGUID(),
|
||
timestamp: new Date().getTime(),
|
||
method: 'takeoff_to_point',
|
||
};
|
||
|
||
console.log(querys);
|
||
|
||
// 检查设备状态
|
||
getDronePortInfoFromRedis({ key: airPortInfo.sn }).then((res) => {
|
||
const rs = JSON.parse(res);
|
||
if (rs.device_online_status == 1) {
|
||
// 设备已开机,不能起飞
|
||
airPortStoreVal.setflyLog({
|
||
type: 'info',
|
||
title: '飞行器已开机',
|
||
content: '不能进行起飞操作',
|
||
});
|
||
} else {
|
||
// 检查是否可以飞行
|
||
isCanFly({
|
||
sn: airPortInfo.sn,
|
||
dock: airPortInfo.getTypeId,
|
||
userid: userInfo.id,
|
||
}).then((res) => {
|
||
console.log(res);
|
||
if (res.status) {
|
||
// 发送起飞指令
|
||
servicesTopic(querys);
|
||
services_replyTopic();
|
||
} else {
|
||
// 起飞被阻止
|
||
airPortStoreVal.setflyLog({
|
||
type: 'error',
|
||
title: '一键起飞阻止',
|
||
content: res.msg,
|
||
});
|
||
}
|
||
});
|
||
}
|
||
});
|
||
},
|
||
});
|
||
};
|
||
// 直播任务ID
|
||
const startTid = buildGUID();
|
||
// 获取并显示机场直播
|
||
const getRtmpList = () => {
|
||
// 显示直播面板
|
||
liveVisible.value = true;
|
||
liveCode.value = airPortInfo.sn;
|
||
|
||
// 查询直播流列表
|
||
axios.get(live_info.getUrl + 'api/v1/streams/').then((res) => {
|
||
console.log(res);
|
||
if (res.data.streams.length > 0) {
|
||
console.log(res.data.streams);
|
||
// 查找当前机场的直播流
|
||
const index = res.data.streams.findIndex((item) => item.url === `/live/` + airPortInfo.sn);
|
||
console.log(index);
|
||
if (index == -1) {
|
||
// 未找到直播流,启动直播
|
||
startLiveFun();
|
||
} else {
|
||
// 检查直播流是否活跃
|
||
if (res.data.streams[index].publish.active) {
|
||
// 播放直播
|
||
player.src(live_info.url + liveCode.value + '.flv');
|
||
player.play();
|
||
// 记录日志
|
||
airPortStoreVal.setflyLog({
|
||
type: 'success',
|
||
title: airPortInfo.name + '直播',
|
||
content: '已开启',
|
||
});
|
||
} else {
|
||
// 直播流未活跃,启动直播
|
||
startLiveFun();
|
||
}
|
||
}
|
||
} else {
|
||
// 无直播流,启动直播
|
||
startLiveFun();
|
||
}
|
||
});
|
||
};
|
||
// 启动直播
|
||
const startLiveFun = () => {
|
||
// 构建直播启动参数
|
||
const querys = {
|
||
bid: buildGUID(),
|
||
method: 'live_start_push',
|
||
tid: startTid,
|
||
timestamp: new Date().getTime(),
|
||
data: {
|
||
url_type: 1, // 0 = 自适应;如需 RTMP 改为 1;GB28181 为 3;WebRTC 为 4
|
||
url: live_info.rtmp + liveCode.value,
|
||
video_id: airPortInfo.video_id,
|
||
video_quality: 3, // 0=自适应,1=流畅,2=标清,3=高清,4=超清
|
||
},
|
||
};
|
||
|
||
console.log(querys);
|
||
// 发送直播启动指令
|
||
servicesTopic(querys);
|
||
services_replyTopic();
|
||
};
|
||
// 重新加载直播
|
||
const reloadLive = () => {
|
||
player.src(live_info.url + liveCode.value + '.flv');
|
||
player.play();
|
||
};
|
||
// 停止直播任务ID
|
||
const stopTid = buildGUID();
|
||
// 关闭直播
|
||
const closeLive = () => {
|
||
// 隐藏直播面板
|
||
liveVisible.value = false;
|
||
};
|
||
// 打开指点飞行面板
|
||
const flyTobtn = () => {
|
||
flyToVisible.value = true;
|
||
EventBus.emit('flyTobtn', '');
|
||
};
|
||
// 执行指点飞行
|
||
const execute = () => {
|
||
// 检查是否选择了目标点
|
||
if (
|
||
flyToData.points[0].latitude == null ||
|
||
flyToData.points[0].longitude == null ||
|
||
flyToData.points[0].height == null
|
||
) {
|
||
createMessage.warning('请先选择目标点');
|
||
return;
|
||
}
|
||
|
||
// 转换坐标类型为数字
|
||
flyToData.points[0].latitude = Number(flyToData.points[0].latitude);
|
||
flyToData.points[0].longitude = Number(flyToData.points[0].longitude);
|
||
flyToData.points[0].height = Number(flyToData.points[0].height);
|
||
|
||
// 构建指点飞行参数
|
||
const querys = {
|
||
bid: buildGUID(),
|
||
data: flyToData,
|
||
tid: buildGUID(),
|
||
timestamp: new Date().getTime(),
|
||
method: 'fly_to_point',
|
||
};
|
||
|
||
console.log(querys);
|
||
// 发送指点飞行指令
|
||
servicesTopic(querys);
|
||
};
|
||
// 停止指点飞行
|
||
const stop = () => {
|
||
// 构建停止指令参数
|
||
const querys = {
|
||
bid: buildGUID(),
|
||
data: {},
|
||
tid: buildGUID(),
|
||
timestamp: new Date().getTime(),
|
||
method: 'fly_to_point_stop',
|
||
};
|
||
|
||
console.log(querys);
|
||
// 发送停止指令
|
||
servicesTopic(querys);
|
||
};
|
||
// 打开智能巡检面板
|
||
const patrolbtn = () => {
|
||
patrolVisible.value = true;
|
||
};
|
||
// 关闭智能巡检面板
|
||
const changePatrol = () => {
|
||
patrolVisible.value = false;
|
||
};
|
||
// 监听机场选择变化,加载工作区域数据
|
||
watch(
|
||
() => changeSelectValue.value,
|
||
async () => {
|
||
// 获取工作区域列表
|
||
allAreaDataList.value = await GetWorkAreaList({
|
||
workspaceid: changeSelectValue.value.project,
|
||
});
|
||
// 处理工作区域数据
|
||
if (allAreaDataList.value.length > 0) {
|
||
allAreaDataList.value.forEach((area, index) => {
|
||
// 转换WKT为GeoJSON
|
||
let geomjson = WktToGeojson(area.geom);
|
||
// 更新区域数据
|
||
area = {
|
||
...area,
|
||
properties: JSON.parse(area.properties),
|
||
geomtype: getGeomType(area),
|
||
coordinates: geomjson.coordinates,
|
||
};
|
||
allAreaDataList.value[index] = area;
|
||
});
|
||
}
|
||
},
|
||
);
|
||
|
||
// 方向控制变化处理
|
||
const changeDRC = (val) => {
|
||
console.log(val);
|
||
};
|
||
// 键盘事件超时器
|
||
const timeout = ref();
|
||
const timeoutUp = ref();
|
||
// 键盘按下事件处理
|
||
const handleKeyDown = (event) => {
|
||
clearTimeout(timeout.value);
|
||
timeout.value = setTimeout(() => {
|
||
console.log('handleKeyDown', event.key);
|
||
changeDRC(event.key);
|
||
}, 500);
|
||
};
|
||
// 键盘释放事件处理
|
||
const handleKeyUp = (event) => {
|
||
clearTimeout(timeoutUp.value);
|
||
timeoutUp.value = setTimeout(() => {
|
||
console.log('handleKeyUp', event.key);
|
||
changeDRC('');
|
||
}, 500);
|
||
};
|
||
// 组件挂载时初始化
|
||
onMounted(() => {
|
||
console.log('index--onMounted');
|
||
console.log(getClient());
|
||
|
||
// 监听窗口大小变化,调整地图高度
|
||
window.addEventListener('resize', function () {
|
||
const pageHeight = document.documentElement.clientHeight;
|
||
console.log(pageHeight);
|
||
mapHeight.value = document.documentElement.clientHeight - 124;
|
||
});
|
||
|
||
// 初始化MQTT连接
|
||
if (!getClient() || !getClient().connected) {
|
||
createConnection(connectCallback);
|
||
}
|
||
|
||
// 监听键盘事件
|
||
document.addEventListener('keydown', handleKeyDown);
|
||
document.addEventListener('keyup', handleKeyUp);
|
||
|
||
// 初始化视频播放器
|
||
player = TCPlayer('player-container-id', {
|
||
sources: [
|
||
{
|
||
src: live_info.url + liveCode.value + '.flv', // 播放地址
|
||
},
|
||
],
|
||
licenseUrl: live_info.url + liveCode.value + '.flv', // license 地址,必传
|
||
autoplay: true, // 是否自动播放
|
||
});
|
||
|
||
// 监听位置获取事件
|
||
EventBus.on('obtainTheLocation', (val: any) => {
|
||
flyToData.points[0].latitude = val.lat.toFixed(6);
|
||
flyToData.points[0].longitude = val.lng.toFixed(6);
|
||
flyToData.points[0].height = val.alt.toFixed(2);
|
||
});
|
||
|
||
// 监听MQTT消息
|
||
setTimeout(() => {
|
||
getClient().on('message', (topic, message) => {
|
||
const rs = JSON.parse(message);
|
||
const rsData = rs.data;
|
||
|
||
// 处理机场消息
|
||
if (topic === topicUrl.value) {
|
||
if (rsData.flighttask_step_code) {
|
||
airporOsdVal.value.flighttask_step_code = rsData.flighttask_step_code;
|
||
}
|
||
if (rsData.temperature) {
|
||
airporOsdVal.value.temperature = rsData.temperature;
|
||
}
|
||
if (rsData.rainfall) {
|
||
airporOsdVal.value.rainfall = rsData.rainfall;
|
||
}
|
||
if (rsData.wind_speed) {
|
||
airporOsdVal.value.wind_speed = rsData.wind_speed;
|
||
}
|
||
if (rsData.network_state && rsData.network_state.rate) {
|
||
airporOsdVal.value.rate = rsData.network_state.rate;
|
||
}
|
||
}
|
||
|
||
// 处理无人机消息
|
||
if (topic == topicUAVUrl.value) {
|
||
if (rsData.latitude && rsData.longitude) {
|
||
uavTrack.value = rs.data;
|
||
}
|
||
}
|
||
|
||
// 处理一键起飞消息
|
||
if (rs.method == 'takeoff_to_point') {
|
||
console.log('一键起飞');
|
||
console.log(rs);
|
||
if (rsData.result == 0) {
|
||
// 保存飞行任务
|
||
saveHandFlyTask({
|
||
flightId: flight_id.value,
|
||
workspaceId: airPortStoreVal.getProject,
|
||
sn: airPortInfo.sn,
|
||
}).then((res) => {
|
||
if (res) {
|
||
airPortStoreVal.setTaskId(res.taskId);
|
||
// 记录起飞成功日志
|
||
airPortStoreVal.setflyLog({
|
||
type: 'success',
|
||
title: '一键起飞',
|
||
content: '成功',
|
||
mark: rs.timestamp
|
||
? rs.method + '-' + rs.timestamp
|
||
: rs.method + '-' + new Date().getTime(),
|
||
});
|
||
// 延迟记录起飞中日志
|
||
setTimeout(() => {
|
||
airPortStoreVal.setflyLog({
|
||
type: 'success',
|
||
title: '一键起飞',
|
||
content: '正在起飞,请稍后',
|
||
});
|
||
}, 500);
|
||
}
|
||
});
|
||
} else {
|
||
// 记录起飞失败日志
|
||
airPortStoreVal.setflyLog({
|
||
type: 'error',
|
||
title: '一键起飞',
|
||
content: '失败,' + errorName(rsData.result),
|
||
mark: rs.timestamp
|
||
? rs.method + '-' + rs.timestamp
|
||
: rs.method + '-' + new Date().getTime(),
|
||
});
|
||
}
|
||
}
|
||
|
||
// 处理机场直播消息
|
||
if (rs.method == 'live_start_push' && rs.tid == startTid) {
|
||
if (rsData.result == 0) {
|
||
// 开始直播
|
||
player.src(live_info.url + liveCode.value + '.flv');
|
||
player.play();
|
||
// 记录直播成功日志
|
||
airPortStoreVal.setflyLog({
|
||
type: 'success',
|
||
title: airPortInfo.name + '开始直播',
|
||
content: '成功',
|
||
mark: rs.timestamp
|
||
? rs.method + '-' + rs.timestamp
|
||
: rs.method + '-' + new Date().getTime(),
|
||
});
|
||
} else if (rsData.result == 513003) {
|
||
// 直播已开启
|
||
player.src(live_info.url + liveCode.value + '.flv');
|
||
player.play();
|
||
airPortStoreVal.setflyLog({
|
||
type: 'success',
|
||
title: airPortInfo.name + '直播',
|
||
content: '已开启',
|
||
mark: rs.timestamp
|
||
? rs.method + '-' + rs.timestamp
|
||
: rs.method + '-' + new Date().getTime(),
|
||
});
|
||
} else {
|
||
// 记录直播失败日志
|
||
airPortStoreVal.setflyLog({
|
||
type: 'error',
|
||
title: airPortInfo.name + '开始直播',
|
||
content: '失败,' + errorName(rsData.result),
|
||
mark: rs.timestamp
|
||
? rs.method + '-' + rs.timestamp
|
||
: rs.method + '-' + new Date().getTime(),
|
||
});
|
||
}
|
||
} else if (rs.method == 'live_stop_push' && rs.tid == stopTid) {
|
||
if (rs.data.result == 0) {
|
||
// 记录停止直播成功日志
|
||
airPortStoreVal.setflyLog({
|
||
type: 'success',
|
||
title: airPortInfo.name + '停止直播',
|
||
content: '成功',
|
||
mark: rs.timestamp
|
||
? rs.method + '-' + rs.timestamp
|
||
: rs.method + '-' + new Date().getTime(),
|
||
});
|
||
} else {
|
||
// 记录停止直播失败日志
|
||
airPortStoreVal.setflyLog({
|
||
type: 'error',
|
||
title: airPortInfo.name + '停止直播',
|
||
content: '失败,' + errorName(rs.data.result),
|
||
mark: rs.timestamp
|
||
? rs.method + '-' + rs.timestamp
|
||
: rs.method + '-' + new Date().getTime(),
|
||
});
|
||
}
|
||
}
|
||
|
||
// 处理指点飞行消息
|
||
if (rs.method == 'fly_to_point') {
|
||
if (rs.data.result == 0) {
|
||
// 记录指点飞行成功日志
|
||
airPortStoreVal.setflyLog({
|
||
type: 'success',
|
||
title: '指点飞行',
|
||
content: '成功',
|
||
mark: rs.timestamp
|
||
? rs.method + '-' + rs.timestamp
|
||
: rs.method + '-' + new Date().getTime(),
|
||
});
|
||
EventBus.emit('closeTranslation', null);
|
||
// 修改云台的角度至最下方
|
||
const querys = {
|
||
bid: buildGUID(),
|
||
method: 'camera_screen_drag',
|
||
tid: buildGUID(),
|
||
timestamp: new Date().getTime(),
|
||
data: {
|
||
payload_index: uavInfo.camera_index,
|
||
locked: true,
|
||
pitch_speed: -180,
|
||
yaw_speed: 0,
|
||
},
|
||
};
|
||
servicesTopicReize(querys);
|
||
} else {
|
||
// 记录指点飞行失败日志
|
||
airPortStoreVal.setflyLog({
|
||
type: 'error',
|
||
title: '指点飞行',
|
||
content: '失败,' + errorName(rs.data.result),
|
||
mark: rs.timestamp
|
||
? rs.method + '-' + rs.timestamp
|
||
: rs.method + '-' + new Date().getTime(),
|
||
});
|
||
}
|
||
}
|
||
|
||
// 处理结束指点飞行消息
|
||
if (rs.method == 'fly_to_point_stop') {
|
||
if (rs.data.result == 0) {
|
||
// 记录结束指点飞行成功日志
|
||
airPortStoreVal.setflyLog({
|
||
type: 'success',
|
||
title: '结束指点飞行',
|
||
content: '成功',
|
||
mark: rs.timestamp
|
||
? rs.method + '-' + rs.timestamp
|
||
: rs.method + '-' + new Date().getTime(),
|
||
});
|
||
} else {
|
||
// 记录结束指点飞行失败日志
|
||
airPortStoreVal.setflyLog({
|
||
type: 'error',
|
||
title: '结束指点飞行',
|
||
content: '失败,' + errorName(rs.data.result),
|
||
mark: rs.timestamp
|
||
? rs.method + '-' + rs.timestamp
|
||
: rs.method + '-' + new Date().getTime(),
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}, 1000);
|
||
});
|
||
// 组件卸载时清理
|
||
onUnmounted(() => {
|
||
// 取消服务订阅
|
||
unServices_replyTopic();
|
||
// 清理相机设置
|
||
airPortStoreVal.setCamera('type', null);
|
||
airPortStoreVal.setCamera('name', null);
|
||
});
|
||
</script>
|
||
<style lang="less" scoped>
|
||
.flight-control {
|
||
background: #00152a;
|
||
|
||
.flight-control-top {
|
||
width: 100%;
|
||
background: #10203a;
|
||
height: 42px;
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
|
||
.airport-name {
|
||
color: #fff;
|
||
margin-left: 10px;
|
||
|
||
img {
|
||
width: 16px;
|
||
}
|
||
}
|
||
|
||
.airport-status {
|
||
margin-left: 10px;
|
||
padding: 4px 6px;
|
||
color: #f2762d;
|
||
border-radius: 20px;
|
||
box-shadow:
|
||
0px 10px 30px 0px rgba(0, 0, 6, 0.15),
|
||
0px 7px 6px 0px rgba(28, 29, 34, 0.06),
|
||
inset 0px 1px 3px 0px rgba(0, 0, 0, 0.5);
|
||
border: 1px solid #f2762d;
|
||
}
|
||
|
||
.airport-live {
|
||
margin-left: 10px;
|
||
padding: 4px 6px;
|
||
color: #fff;
|
||
border-radius: 20px;
|
||
background: #0377f6;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.airport-info {
|
||
margin-left: 10px;
|
||
color: #fff;
|
||
|
||
.airport-info-title {
|
||
display: inline-block;
|
||
margin: 0 6px;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
img {
|
||
width: 12px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.flight-contain {
|
||
position: relative;
|
||
margin-top: 2px;
|
||
display: flex;
|
||
overflow: auto;
|
||
|
||
.uavbox-open {
|
||
position: absolute;
|
||
right: 0;
|
||
top: 0;
|
||
background: #10203a;
|
||
color: #fff;
|
||
z-index: 2;
|
||
padding: 6px;
|
||
cursor: pointer;
|
||
|
||
img {
|
||
width: 20px;
|
||
}
|
||
}
|
||
|
||
.map-container {
|
||
// width: calc(100% - 44px);
|
||
// height: 90vh;
|
||
}
|
||
|
||
.map-container-content {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.uav-box {
|
||
width: 62vw;
|
||
}
|
||
}
|
||
|
||
.fly-log {
|
||
position: absolute;
|
||
top: 20px;
|
||
left: 20px;
|
||
width: 15vw;
|
||
height: 50vh;
|
||
box-shadow: 0px 10px 30px 0px rgba(0, 0, 6, 0.15);
|
||
border-radius: 6px;
|
||
border: 1px solid #3662a5;
|
||
background: #00152a;
|
||
|
||
.fly-log-title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 20px;
|
||
color: #fff;
|
||
border-bottom: 1px solid #4e5778;
|
||
|
||
span:last-child {
|
||
font-size: 20px;
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
|
||
.fly-log-content {
|
||
width: 90%;
|
||
height: 40vh;
|
||
box-shadow:
|
||
0px 10px 30px 0px rgba(0, 0, 6, 0.15),
|
||
0px 10px 30px 0px rgba(0, 0, 6, 0.15);
|
||
border-radius: 6px;
|
||
border: 1px solid #3662a5;
|
||
margin: 20px auto;
|
||
background: #10203a;
|
||
padding: 10px 12px;
|
||
color: #fff;
|
||
|
||
.fly-status {
|
||
color: #868e9b;
|
||
font-size: 12px;
|
||
margin: 10px 0 20px 0;
|
||
}
|
||
|
||
.log-opstions {
|
||
font-size: 14px;
|
||
height: 29vh;
|
||
overflow: scroll;
|
||
}
|
||
|
||
.log-item {
|
||
padding: 5px 0;
|
||
|
||
.item-desc {
|
||
font-size: 14px;
|
||
color: #fff;
|
||
margin-left: 10px;
|
||
}
|
||
}
|
||
|
||
.MinusCircleFilled {
|
||
color: #737d8c;
|
||
font-size: 16px;
|
||
|
||
.item-desc {
|
||
color: #737d8c;
|
||
}
|
||
}
|
||
|
||
.CheckCircleFilled {
|
||
font-size: 16px;
|
||
color: #1aa053;
|
||
}
|
||
|
||
.CloseCircleFilled {
|
||
font-size: 16px;
|
||
color: #0e5528;
|
||
}
|
||
}
|
||
}
|
||
|
||
.fly-to-form {
|
||
position: absolute;
|
||
left: 16.6vw;
|
||
bottom: 90px;
|
||
width: 17vw;
|
||
height: 34vh;
|
||
box-shadow: 0px 10px 30px 0px rgba(0, 0, 6, 0.15);
|
||
border-radius: 6px;
|
||
border: 1px solid #3662a5;
|
||
background: #00152a;
|
||
color: #fff;
|
||
|
||
.fly-to-title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
|
||
.title-name {
|
||
margin: 10px 0 0 10px;
|
||
}
|
||
|
||
.title-icon {
|
||
border: 1px solid #3662a5;
|
||
border-top: none;
|
||
border-right: none;
|
||
border-radius: 6px;
|
||
width: 26px;
|
||
height: 26px;
|
||
text-align: center;
|
||
line-height: 26px;
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
|
||
.form-desc {
|
||
color: #f2762d;
|
||
font-size: 12px;
|
||
padding: 14px;
|
||
display: inline-block;
|
||
}
|
||
|
||
.form-content {
|
||
padding: 10px 16px;
|
||
|
||
.form-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16px;
|
||
|
||
div {
|
||
background: #263f66;
|
||
width: 60%;
|
||
border-radius: 6px;
|
||
border: 1px solid #3662a5;
|
||
height: 34px;
|
||
}
|
||
|
||
input {
|
||
background: #263f66;
|
||
width: 140px;
|
||
text-align: right;
|
||
color: #fff;
|
||
font-size: 12px;
|
||
border: none;
|
||
}
|
||
|
||
::v-deep .ant-input-number,
|
||
.ant-input-number-group-wrapper {
|
||
background: none;
|
||
width: 260px !important;
|
||
text-align: right;
|
||
color: #fff;
|
||
font-size: 12px;
|
||
}
|
||
|
||
::v-deep .ant-input-number-group-wrapper {
|
||
input {
|
||
background: none;
|
||
width: 260px !important;
|
||
text-align: right;
|
||
color: #fff;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.fly-to-button {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-around;
|
||
|
||
span {
|
||
width: 40%;
|
||
color: '#fff';
|
||
height: 34px;
|
||
border-radius: 6px;
|
||
text-align: center;
|
||
line-height: 34px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
span:first-child {
|
||
background: rgba(207, 39, 48, 0.6);
|
||
}
|
||
|
||
span:last-child {
|
||
background: #0377f6;
|
||
}
|
||
}
|
||
}
|
||
|
||
.airport-live-contain {
|
||
position: absolute;
|
||
left: 16.6vw;
|
||
top: 20px;
|
||
width: 17vw;
|
||
height: 26vh;
|
||
box-shadow: 0px 10px 30px 0px rgba(0, 0, 6, 0.15);
|
||
border-radius: 6px;
|
||
border: 1px solid #3662a5;
|
||
background: #00152a;
|
||
color: #fff;
|
||
|
||
.live-title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
|
||
img {
|
||
width: 16px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
span {
|
||
width: 30px;
|
||
height: 30px;
|
||
border: 1px solid #3662a5;
|
||
text-align: center;
|
||
}
|
||
}
|
||
}
|
||
|
||
.player {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
|
||
#player-container-id-live {
|
||
margin: 0 auto;
|
||
}
|
||
}
|
||
|
||
.patrol-btn {
|
||
position: absolute;
|
||
left: 32vw;
|
||
top: 4px;
|
||
width: 16vw;
|
||
height: 59vh;
|
||
box-shadow: 0px 10px 30px 0px rgba(0, 0, 6, 0.15);
|
||
border-radius: 6px;
|
||
border: 1px solid #3662a5;
|
||
background: #00152a;
|
||
color: #fff;
|
||
}
|
||
</style>
|
||
<style scoped>
|
||
body :global(.ant-message-notice) {
|
||
text-align: right;
|
||
}
|
||
</style>
|