DiKongGanZhiPingTai/src/views/demo/workmanagement/uavmonitoring/index.vue

942 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="video-contain">
<div class="video-left">
<div class="left-title">无人机实时画面 <ReloadOutlined title="刷新" @click="getList" /></div>
<div class="monitor-status">
<div class="on-line">
<i></i>
<span>在线{{ onlineCount }}</span>
</div>
<span class="line"></span>
<div class="under-line">
<i></i>
<span>离线{{ underlineCount }}</span>
</div>
</div>
<div class="monitor-list">
<div class="monitor-item" v-for="(item, index) in monitoringList" :key="index">
<div
:class="isSn(item.uavSn) ? 'item-parent active' : 'item-parent'"
@click="pushStreaming(item)"
:title="item.uavName"
>
<div class="item-img online" v-if="item.uavStatus == 1">
<img src="@/assets/images/monitoring/monitor-icon.png" alt="" />
</div>
<div class="item-img underline" v-else>
<img src="@/assets/images/monitoring/monitor-no-icon.png" alt="" />
</div>
<div class="item-content">
<span>{{ item.uavName }}</span>
</div>
<i :class="item.uavStatus == 1 ? 'online-i' : 'underline-i'"></i>
</div>
</div>
</div>
</div>
<div class="video-right">
<div class="split-screen">
<div
:class="selectVal == 1 ? 'split-item active' : 'split-item'"
@click="selecttype('classtype', 1, 24)"
>
<img src="@/assets/images/monitoring/one.png" alt="" />
<span>单屏</span>
</div>
<div
:class="selectVal == 4 ? 'split-item active' : 'split-item'"
@click="selecttype('classtype1', 4, 12)"
>
<img src="@/assets/images/monitoring/four.png" alt="" />
<span>四分屏</span>
</div>
<div
:class="selectVal == 9 ? 'split-item active' : 'split-item'"
@click="selecttype('classtype3', 9, 8)"
>
<img src="@/assets/images/monitoring/nine.png" alt="" />
<span>九分屏</span>
</div>
</div>
<div class="main">
<div class="conter" ref="box">
<el-row :gutter="16">
<el-col
v-for="(n, index) in state.fornum"
:key="index"
:xs="24"
:sm="24"
:md="state.clonum"
:lg="state.clonum"
:xl="state.clonum"
style="margin-bottom: 10px"
>
<div
class="player-wrapper"
element-loading-text="加载中..."
element-loading-background="#000"
v-if="onlineList[index]"
>
<div class="video-container">
<div class="video-header">
<div class="video-title">
<div class="video-icon">
<img src="@/assets/images/monitoring/monitor-icon.png" alt="" />
</div>
<span>{{ onlineList[index].uavName }} </span>
</div>
<div class="video-controls" v-if="onlineList[index].uavType == 'GDY'">
<button
class="control-btn"
title="停止"
@click="stopGDY(index, onlineList[index])"
>
<StopOutlined />
</button>
<button
class="control-btn"
title="位置"
@click="showPosition(onlineList[index])"
>
<InfoCircleOutlined />
</button>
<button class="control-btn" title="设置">
<i class="el-icon-setting"></i>
</button>
</div>
</div>
<div class="video-content" :style="{ height: videoHeight }">
<flv-player
style="width: 100%; height: 100%"
:src="live_info.url + onlineList[index].uavSn + '.flv'"
/>
</div>
</div>
</div>
<div v-else>
<div class="video-container">
<div class="video-header">
<div class="video-title">
<div class="video-icon no-icon">
<img src="@/assets/images/monitoring/monitor-no-icon.png" alt="" />
</div>
<span>视频</span>
</div>
<div class="video-controls">
<button class="control-btn" title="全屏">
<i class="el-icon-full-screen"></i>
</button>
<button class="control-btn" title="音量">
<i class="el-icon-video-camera"></i>
</button>
<button class="control-btn" title="设置">
<i class="el-icon-setting"></i>
</button>
</div>
</div>
<div class="video-content" :style="{ height: videoHeight }">
<flv-player style="width: 100%; height: 100%" src="" />
</div>
</div>
</div>
</el-col>
</el-row>
</div>
<div class="status-bar">
<span>最后更新:{{ getCurrentDate('time') }} 系统运行正常</span>
</div>
</div>
</div>
<a-modal
width="100%"
wrap-class-name="full-modal"
v-model:open="gpsVisible"
:destroyOnClose="true"
:footer="null"
title="视频信息弹窗"
>
<GPS :uavSn="showModelSn" />
</a-modal>
</div>
</template>
<script setup>
import { onMounted, reactive, ref, onUnmounted } from 'vue';
import FlvPlayer from '@/views/demo/workmanagement/monitoring/FlvPlayer.vue'; // 确保路径正确
import { getUavStateFromRedis } from '@/api/workmanagement/droneDock';
import { airPortStore } from '@/store/modules/airport';
import { buildGUID } from '@/utils/uuid';
import { getCurrentDate } from '@/utils/index';
import { getClient, createConnection, clientPublish, clientSubscribe } from '@/utils/mqtt';
import { cameraCode } from '@/utils/debugging/remote';
import { useMessage } from '@/hooks/web/useMessage';
import { ReloadOutlined, StopOutlined, InfoCircleOutlined } from '@ant-design/icons-vue';
import GPS from './gps.vue';
const { createMessage } = useMessage();
const airPortStoreVal = airPortStore();
const live_info = airPortStoreVal.getLiveInfo;
const selectVal = ref(1);
const monitoringList = ref([]);
const onlineCount = ref(0);
const underlineCount = ref(0);
const gpsVisible = ref(false);
const showModelSn = ref('');
const state = reactive({
fullscreen: false,
fornum: 1,
clonum: 24,
classtype1: '',
classtype2: 'primary',
classtype3: '',
classtype4: '',
classtype5: '',
items: [false, false, false, false],
});
const videoHeight = ref('73vh');
function selecttype(item, fnum, clo) {
selectVal.value = fnum;
state.items = [];
for (let i = 0; i < fnum; i++) {
state.items[i] = false;
}
state.fornum = fnum;
state.clonum = clo;
switch (fnum) {
case 1:
videoHeight.value = '73vh';
break;
case 4:
videoHeight.value = '38vh';
break;
case 9:
videoHeight.value = '25vh';
break;
}
if (item === 'classtype1') {
state.classtype1 = 'primary';
state.classtype2 = '';
state.classtype3 = '';
state.classtype4 = '';
state.classtype5 = '';
} else if (item === 'classtype2') {
state.classtype1 = '';
state.classtype2 = 'primary';
state.classtype3 = '';
state.classtype4 = '';
state.classtype5 = '';
} else if (item === 'classtype3') {
state.classtype1 = '';
state.classtype2 = '';
state.classtype3 = 'primary';
state.classtype4 = '';
state.classtype5 = '';
} else if (item === 'classtype4') {
state.classtype1 = '';
state.classtype2 = '';
state.classtype3 = '';
state.classtype4 = 'primary';
state.classtype5 = '';
} else if (item === 'classtype5') {
state.classtype1 = '';
state.classtype2 = '';
state.classtype3 = '';
state.classtype4 = '';
state.classtype5 = 'primary';
}
}
const onlineList = ref([]);
const underlineList = ref([]);
// 启动直播
const startLiveFun = (item) => {
let video_id;
if (item.node == 'airport') {
video_id = item.sn + '/165-0-7/normal-0';
} else {
video_id = item.sn + '/' + cameraCode(item.type) + '/normal-0';
}
// 构建直播启动参数
const querys = {
bid: buildGUID(),
method: 'live_start_push',
tid: buildGUID(),
timestamp: new Date().getTime(),
data: {
url_type: 1, // 0 = 自适应;如需 RTMP 改为 1GB28181 为 3WebRTC 为 4
url: live_info.rtmp + item.sn,
video_id: video_id,
video_quality: 3, // 0=自适应1=流畅2=标清3=高清4=超清
},
};
// 发送直播启动指令
clientPublish('thing/product/' + item.parentSn + '/services', querys);
};
const startLive = async (element) => {
console.log(element);
const index = liveList.value.findIndex((item) => item.url === `/live/` + element.sn);
if (index == -1) {
startLiveFun(element);
} else {
// 检查直播流是否活跃
if (!liveList.value[index].publish.active) {
startLiveFun(element);
}
}
};
// 当先点击固定翼sn
const currentlySn = ref({});
// 判断是否正在推流中
const streamingVisible = ref(false);
// 固定翼视频
const fixedWing = (item) => {
console.log(streamingVisible.value);
console.log(item);
console.log(item);
// if (streamingVisible.value && currentlySn.value.uavSn == item.uavSn) {
// return createMessage.warning('已请求,请稍后');
// } else {
streamingVisible.value = true;
currentlySn.value = item;
// }
// 1、检测状态
// 2、状态不在线时进行推流
// 3、推流成功后进行播放
// 4、推流不成功
// 检测状态
const querys = {
type: 'check_status',
// type: 'start_forward',
device_id: currentlySn.value.uavSn,
};
console.log(querys);
// 发送直播启动指令
clientPublish('thing/product/' + currentlySn.value.uavSn + '/onboardcase', querys);
clientSubscribe('thing/product/' + currentlySn.value.uavSn + '/onboardcase');
};
const getList = async () => {
onlineCount.value = 0;
underlineCount.value = 0;
await getUavStateFromRedis({}).then((res) => {
monitoringList.value = res;
res.forEach((element) => {
if (element.uavStatus == 1) {
onlineCount.value++;
} else {
underlineCount.value++;
}
});
});
};
const liveList = ref([]);
const getOpenLiveList = async () => {
// 查询直播流列表
axios.get(live_info.getUrl + 'api/v1/streams/').then((res) => {
if (res.data.streams.length > 0) {
liveList.value = res.data.streams;
}
});
};
// MQTT连接状态
const connected = ref(false);
const connectCallback = () => {
connected.value = true;
};
// 选中推流
const pushStreaming = async (element) => {
let item;
const index = onlineList.value.findIndex((item) => item.name == element.droneName);
if (index != -1) {
return;
}
if (element.uavType == 'GDY') {
console.log('固定翼');
console.log(streamingVisible.value);
console.log(currentlySn.value.uavSn);
if (streamingVisible.value && currentlySn.value.uavSn == element.uavSn) {
return createMessage.warning('已请求,请稍后');
} else {
// if (element.uavSn == '123HSDJASLDJKEJSKSUWJS') {
fixedWing(element);
createMessage.warning('正在开启' + element.uavName + ',请稍后');
// } else {
// createMessage.error('当前设备不在线');
// }
}
} else {
if (element.uavStatus == 1) {
item = {
name: element.uavName,
sn: element.uavSn,
parentSn: element.droneSn,
type: element.uavType,
node: 'uav',
};
startLive(item);
switch (selectVal.value) {
case '1':
onlineList.value = [item];
break;
case '4':
if (onlineList.value.length == 4) {
onlineList.value[3] = item;
} else {
onlineList.value.push(item);
}
break;
case '9':
if (onlineList.value.length == 9) {
onlineList.value[8] = item;
} else {
onlineList.value.push(item);
}
break;
}
} else {
return createMessage.error('当前设备不在线');
}
}
};
const isSn = (sn) => {
return onlineList.value.some((item) => item.uavSn == sn);
};
onMounted(() => {
// 初始化MQTT连接
if (!getClient() || !getClient().connected) {
createConnection(connectCallback);
}
getList();
getOpenLiveList();
setTimeout(() => {
getClient().on('message', (topic, message) => {
const rs = JSON.parse(message);
// console.log(rs);
if ('thing/product/' + currentlySn.value.uavSn + '/onboardcase' == topic) {
if (rs.type == 'command_response') {
const topicType = rs.command.type;
if (topicType == 'check_status') {
// 检测状态
if (rs.data.is_input_available && rs.data.is_forwarding) {
streamingVisible.value = false;
fixedWingPush(currentlySn.value);
} else {
if (rs.data.is_input_available) {
// 推流
const querys = {
type: 'start_forward',
device_id: currentlySn.value.uavSn,
};
clientPublish(
'thing/product/' + currentlySn.value.uavSn + '/onboardcase',
querys,
);
}
}
} else if (topicType == 'start_forward') {
console.log(rs.data);
if (rs.status == 'success') {
console.log('推流成功');
streamingVisible.value = false;
fixedWingPush(currentlySn.value);
} else {
console.log('推流失败');
return createMessage.error(rs.message);
}
}
}
// if (rs.type == 'status_update') {
// // 检测状态
// if (rs.is_input_available && rs.is_forwarding) {
// streamingVisible.value = false;
// fixedWingPush(currentlySn.value);
// } else {
// if (rs.is_input_available) {
// // 推流
// const querys = {
// type: 'start_forward',
// device_id: currentlySn.value.uavSn,
// };
// clientPublish('thing/product/' + currentlySn.value.uavSn + '/onboardcase', querys);
// }
// }
// }
}
// setTimeout(() => {
// console.log(streamingVisible.value);
// if (streamingVisible.value) {
// console.log('推流超时');
// if (!rs) {
// return createMessage.error('当前设备不在线');
// } else {
// if (!rs.command) {
// return createMessage.error('当前设备不在线');
// }
// }
// }
// }, 20000);
});
}, 1000);
});
const fixedWingPush = async (item) => {
console.log('成功后加入到播放列表里');
console.log(item);
console.log(selectVal.value);
switch (selectVal.value) {
case 1:
onlineList.value = [item];
break;
case 4:
if (onlineList.value.length == 4) {
onlineList.value[3] = item;
} else {
onlineList.value.push(item);
}
break;
case 9:
if (onlineList.value.length == 9) {
onlineList.value[8] = item;
} else {
onlineList.value.push(item);
}
break;
}
console.log(onlineList.value);
};
const stopGDY = async (index, item) => {
onlineList.value.splice(index, 1);
const querys = {
type: 'stop_forward',
device_id: item.uavSn,
};
clientPublish('thing/product/' + item.uavSn + '/onboardcase', querys);
};
const showPosition = (item) => {
showModelSn.value = item.uavSn;
gpsVisible.value = true;
};
onUnmounted(() => {
// 停止推流
onlineList.value.forEach((item) => {
if (item.uavType == 'GDY') {
const querys = {
type: 'stop_forward',
device_id: currentlySn.value.uavSn,
};
clientPublish('thing/product/' + item.uavSn + '/onboardcase', querys);
}
});
});
</script>
<style lang="less">
.full-modal {
background-color: #00152a;
.ant-modal {
max-width: 100%;
top: 0;
padding-bottom: 0;
margin: 0;
}
.ant-modal-header {
background-color: #10203a;
border: none;
.ant-modal-title {
color: #fff;
}
}
.ant-modal-content {
display: flex;
flex-direction: column;
background-color: #00152a;
height: calc(100vh);
button {
color: #fff;
}
}
.ant-modal-body {
flex: 1;
}
}
</style>
<style scoped lang="less">
.video-contain {
display: flex;
background: #0f0f13;
height: calc(100vh - 80px);
width: 100%;
}
.video-left {
width: 16%;
background: #16161a;
display: flex;
flex-direction: column;
align-items: center;
border-right: 1px solid #232328;
.left-title {
font-weight: 500;
font-size: 18px;
color: #ffffff;
padding: 20px 15px 15px 0;
width: 90%;
text-align: left;
margin-left: 20px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.monitor-status {
width: 90%;
height: 50px;
background: #1a1a20;
color: #fff;
display: flex;
align-items: center;
border-radius: 6px;
margin-bottom: 20px;
.on-line {
width: 49%;
text-align: center;
i {
width: 8px;
height: 8px;
border-radius: 8px;
background: #4bd884;
display: inline-block;
margin-right: 6px;
}
span {
font-size: 14px;
}
}
.line {
width: 1px;
height: 30px;
background: #2a2a33;
opacity: 0.8;
}
.under-line {
width: 49%;
text-align: center;
i {
width: 8px;
height: 8px;
border-radius: 8px;
background: #fe4848;
margin-right: 6px;
display: inline-block;
}
span {
font-size: 14px;
}
}
}
}
.video-right {
width: 84%;
background: #0f0f13;
display: flex;
flex-direction: column;
.split-screen {
display: flex;
align-items: center;
padding: 20px;
background: #16161a;
border-bottom: 1px solid #232328;
.split-item {
display: flex;
align-items: center;
justify-content: center;
width: 100px;
height: 36px;
color: #fff;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
img {
width: 16px;
margin-right: 8px;
}
span {
font-size: 14px;
}
}
.active {
background: #1e75ff;
color: #ffffff;
}
.split-item:first-child {
border: 1px solid #2a2a33;
border-radius: 6px 0 0 6px;
}
.split-item:nth-child(2) {
border-top: 1px solid #2a2a33;
border-bottom: 1px solid #2a2a33;
}
.split-item:last-child {
border: 1px solid #2a2a33;
border-radius: 0 6px 6px 0;
}
}
}
.main {
flex: 1;
padding: 10px 20px;
overflow: auto;
}
.player-wrapper {
height: 100%;
min-height: 200px;
}
.video-container {
background: #16161a;
border-radius: 6px;
overflow: hidden;
height: 100%;
display: flex;
flex-direction: column;
}
.video-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #1a1a20;
border-bottom: 1px solid #232328;
}
.video-title {
display: flex;
align-items: center;
.video-icon {
width: 32px;
height: 32px;
border-radius: 4px;
background: #1e75ff;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
img {
width: 18px;
height: 18px;
}
}
.no-icon {
background: #25252f;
}
span {
font-size: 14px;
color: #ffffff;
font-weight: 400;
}
}
.video-controls {
display: flex;
align-items: center;
}
.control-btn {
width: 28px;
height: 28px;
border: none;
background: transparent;
color: #92929d;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
margin-left: 8px;
transition: all 0.3s ease;
&:hover {
background: #25252f;
color: #ffffff;
}
i {
font-size: 16px;
}
}
.video-content {
background: #0f0f13;
position: relative;
min-height: 150px;
video {
height: 100%;
}
}
.status-bar {
margin-top: 20px;
padding: 12px 20px;
background: #16161a;
border-radius: 6px;
text-align: right;
span {
font-size: 14px;
color: #92929d;
}
}
.monitor-list {
width: 90%;
margin-top: 10px;
.monitor-item {
.active {
border-left: 4px solid #1e75ff;
}
&:hover {
background: #1e1e26;
}
.item-parent {
display: flex;
align-items: center;
color: #fff;
font-size: 14px;
padding: 12px 16px;
margin-bottom: 8px;
background: #1a1a20;
border-radius: 6px;
transition: all 0.3s ease;
cursor: pointer;
}
.item-child {
display: flex;
align-items: center;
color: #fff;
font-size: 14px;
padding: 12px 16px;
margin-bottom: 8px;
background: #1a1a20;
border-radius: 6px;
transition: all 0.3s ease;
cursor: pointer;
margin-left: 10px;
}
.item-img {
width: 36px;
height: 36px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
img {
width: 20px;
}
}
.online {
background: #1e75ff;
}
.underline {
background: #25252f;
}
.item-content {
flex: 1;
min-width: 0;
span {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
color: #ffffff;
}
}
.online-i {
width: 8px;
height: 8px;
border-radius: 50%;
background: #4bd884;
display: inline-block;
margin-left: 12px;
position: relative;
&:after {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
border-radius: 50%;
background: #4bd884;
opacity: 0.5;
animation: pulse 2s infinite;
}
}
.underline-i {
width: 8px;
height: 8px;
border-radius: 50%;
background: #fe4848;
display: inline-block;
margin-left: 12px;
}
}
}
@keyframes pulse {
0% {
transform: scale(0.8);
opacity: 0.5;
}
70% {
transform: scale(1.2);
opacity: 0;
}
100% {
transform: scale(0.8);
opacity: 0;
}
}
/* */
@media screen and (max-width: 1200px) {
.video-left {
width: 20%;
}
.video-right {
width: 80%;
}
.main {
padding: 15px;
}
}
@media screen and (max-width: 992px) {
.video-left {
width: 25%;
}
.video-right {
width: 75%;
}
.left-title {
font-size: 16px !important;
}
.split-screen {
padding: 15px;
}
.split-item {
width: 90px !important;
height: 32px !important;
}
}
@media screen and (max-width: 768px) {
.video-contain {
flex-direction: column;
}
.video-left {
width: 100%;
border-right: none;
border-bottom: 1px solid #232328;
max-height: 300px;
}
.video-right {
width: 100%;
}
.monitor-list {
max-height: 200px;
overflow-y: auto;
}
.main {
padding: 10px;
}
}
</style>