470 lines
11 KiB
Vue
470 lines
11 KiB
Vue
<script setup>
|
|
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
|
import { Plus, VideoCamera } from '@element-plus/icons-vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { getDeviceList } from '@/api/device'
|
|
import { createFlvPlayer, destroyFlvPlayer } from '@/utils/videoPlayer'
|
|
|
|
// 无人机数据
|
|
const droneList = ref([])
|
|
const loading = ref(false)
|
|
const mainVideoRef = ref(null)
|
|
const mainFlvPlayer = ref(null)
|
|
const videoRefs = ref([])
|
|
const flvPlayers = ref([])
|
|
|
|
// 初始化视频播放器
|
|
const initVideoPlayers = () => {
|
|
// 销毁之前的播放器
|
|
if (mainFlvPlayer.value) {
|
|
destroyFlvPlayer(mainFlvPlayer.value)
|
|
}
|
|
flvPlayers.value.forEach(player => destroyFlvPlayer(player))
|
|
flvPlayers.value = []
|
|
|
|
// 初始化主视频播放器
|
|
if (mainVideoRef.value && droneList.value[currentDroneIndex.value]?.status === 'online') {
|
|
const deviceCode = droneList.value[currentDroneIndex.value].code
|
|
mainFlvPlayer.value = createFlvPlayer(mainVideoRef.value, deviceCode)
|
|
}
|
|
|
|
// 初始化列表视频播放器
|
|
videoRefs.value.forEach((videoRef, index) => {
|
|
if (videoRef && droneList.value[index]?.status === 'online') {
|
|
const deviceCode = droneList.value[index].code
|
|
const player = createFlvPlayer(videoRef, deviceCode)
|
|
flvPlayers.value.push(player)
|
|
}
|
|
})
|
|
}
|
|
|
|
// 监听无人机列表变化
|
|
watch(() => droneList.value, () => {
|
|
nextTick(() => {
|
|
initVideoPlayers()
|
|
})
|
|
}, { deep: true })
|
|
|
|
// 分页配置
|
|
const pagination = ref({
|
|
page: 1,
|
|
page_size: 10,
|
|
total: 0
|
|
})
|
|
|
|
// 当前选中的无人机索引
|
|
const currentDroneIndex = ref(0)
|
|
|
|
// 获取无人机列表
|
|
const getList = async () => {
|
|
loading.value = true
|
|
try {
|
|
const res = await getDeviceList({
|
|
page: pagination.value.page,
|
|
page_size: pagination.value.page_size,
|
|
device_type: '10001' // 无人机类型
|
|
})
|
|
if (res.code === 200) {
|
|
droneList.value = res.data.list.map(drone => ({
|
|
id: drone.id,
|
|
name: drone.device_name,
|
|
code: drone.device_code,
|
|
status: drone.status.code === 1 ? 'online' : 'offline',
|
|
status_text: drone.status.text,
|
|
battery_level: drone.battery_level || 0,
|
|
signal_strength: drone.signal_strength || 0
|
|
}))
|
|
pagination.value.total = res.data.total
|
|
} else {
|
|
ElMessage.error(res.message || '获取无人机列表失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('获取无人机列表错误:', error)
|
|
ElMessage.error('获取无人机列表失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// 切换主视频
|
|
const switchMainVideo = (index) => {
|
|
currentDroneIndex.value = index
|
|
nextTick(() => {
|
|
initVideoPlayers()
|
|
})
|
|
}
|
|
|
|
// 处理分页变化
|
|
const handleSizeChange = (val) => {
|
|
pagination.value.page_size = val
|
|
getList()
|
|
}
|
|
|
|
const handleCurrentChange = (val) => {
|
|
pagination.value.page = val
|
|
getList()
|
|
}
|
|
|
|
onMounted(() => {
|
|
getList()
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (mainFlvPlayer.value) {
|
|
destroyFlvPlayer(mainFlvPlayer.value)
|
|
}
|
|
flvPlayers.value.forEach(player => destroyFlvPlayer(player))
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="drone-management">
|
|
<!-- 左侧主视频区域 -->
|
|
<div class="main-video-container">
|
|
<video
|
|
v-if="droneList[currentDroneIndex]?.status === 'online'"
|
|
ref="mainVideoRef"
|
|
class="main-video"
|
|
muted
|
|
></video>
|
|
<div v-else class="offline-mask">
|
|
<el-icon class="offline-icon"><VideoCamera /></el-icon>
|
|
<span>无人机离线</span>
|
|
</div>
|
|
<div class="video-info">
|
|
<h2>{{ droneList[currentDroneIndex]?.name }}</h2>
|
|
<div class="drone-status">
|
|
<div class="status-item">
|
|
<span class="label">电量:</span>
|
|
<span class="value">{{ droneList[currentDroneIndex]?.battery_level }}%</span>
|
|
</div>
|
|
<div class="status-item">
|
|
<span class="label">信号:</span>
|
|
<span class="value">{{ droneList[currentDroneIndex]?.signal_strength }}%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 右侧视频列表 -->
|
|
<div class="video-list-container">
|
|
<div class="list-header">
|
|
<h3>无人机列表</h3>
|
|
<el-button type="primary" :icon="Plus">添加无人机</el-button>
|
|
</div>
|
|
<div class="video-list">
|
|
<div
|
|
v-for="(drone, index) in droneList"
|
|
:key="drone.id"
|
|
class="video-item"
|
|
:class="{
|
|
'offline': drone.status !== 'online',
|
|
'active': currentDroneIndex === index
|
|
}"
|
|
@click="switchMainVideo(index)"
|
|
>
|
|
<div class="video-container">
|
|
<video
|
|
v-if="drone.status === 'online'"
|
|
:ref="el => { if (el) videoRefs[index] = el }"
|
|
class="video-player"
|
|
muted
|
|
></video>
|
|
<div v-else class="offline-mask">
|
|
<el-icon class="offline-icon"><VideoCamera /></el-icon>
|
|
<span>无人机离线</span>
|
|
</div>
|
|
</div>
|
|
<div class="drone-info">
|
|
<div class="info-header">
|
|
<h4>{{ drone.name }}</h4>
|
|
<el-tag
|
|
size="small"
|
|
:type="drone.status === 'online' ? 'success' : 'danger'"
|
|
>
|
|
{{ drone.status === 'online' ? '在线' : '离线' }}
|
|
</el-tag>
|
|
</div>
|
|
<div class="status-info">
|
|
<span class="battery">电量: {{ drone.battery_level }}%</span>
|
|
<span class="signal">信号: {{ drone.signal_strength }}%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- 分页器 -->
|
|
<div class="pagination-container">
|
|
<el-pagination
|
|
v-model:current-page="pagination.page"
|
|
v-model:page-size="pagination.page_size"
|
|
:total="pagination.total"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
background
|
|
layout="total, sizes, prev, pager, next"
|
|
@size-change="handleSizeChange"
|
|
@current-change="handleCurrentChange"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.drone-management {
|
|
height: 100%;
|
|
display: flex;
|
|
gap: 16px;
|
|
padding: 16px;
|
|
background: #f5f7fa;
|
|
overflow: hidden;
|
|
box-sizing: border-box;
|
|
|
|
.main-video-container {
|
|
flex: 1;
|
|
height: calc(100% - 32px);
|
|
background: #000;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
|
|
.main-video {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.offline-mask {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
|
|
.offline-icon {
|
|
font-size: 48px;
|
|
color: #909399;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
span {
|
|
font-size: 16px;
|
|
color: #909399;
|
|
}
|
|
}
|
|
|
|
.video-info {
|
|
position: absolute;
|
|
left: 0;
|
|
bottom: 0;
|
|
width: 100%;
|
|
padding: 16px;
|
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
|
|
color: #fff;
|
|
|
|
h2 {
|
|
font-size: 20px;
|
|
margin: 0 0 12px;
|
|
}
|
|
|
|
.drone-status {
|
|
display: flex;
|
|
gap: 24px;
|
|
|
|
.status-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
|
|
.label {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.value {
|
|
font-weight: 500;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.video-list-container {
|
|
width: 360px;
|
|
height: calc(100% - 32px);
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
|
|
.list-header {
|
|
padding: 16px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
border-bottom: 1px solid #eee;
|
|
flex-shrink: 0;
|
|
|
|
h3 {
|
|
margin: 0;
|
|
font-size: 16px;
|
|
color: #333;
|
|
}
|
|
}
|
|
|
|
.video-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 16px;
|
|
margin-right: 0;
|
|
|
|
&::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
&::-webkit-scrollbar-thumb {
|
|
background: #dcdfe6;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
&::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.video-item {
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
transition: all 0.3s;
|
|
cursor: pointer;
|
|
border: 1px solid #eee;
|
|
margin-bottom: 16px;
|
|
|
|
&:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
&.active {
|
|
border: 2px solid #409EFF;
|
|
transform: translateX(-2px);
|
|
}
|
|
|
|
&:hover {
|
|
transform: translateX(-2px);
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
&.offline {
|
|
.video-container {
|
|
background: #f5f5f5;
|
|
}
|
|
}
|
|
|
|
.video-container {
|
|
height: 200px;
|
|
background: #000;
|
|
position: relative;
|
|
|
|
video {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.offline-mask {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #909399;
|
|
|
|
.offline-icon {
|
|
font-size: 32px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
span {
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.drone-info {
|
|
padding: 12px;
|
|
|
|
.info-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
|
|
h4 {
|
|
color: #333;
|
|
margin: 0;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}
|
|
}
|
|
|
|
.status-info {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 12px;
|
|
color: #666;
|
|
|
|
.battery, .signal {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.pagination-container {
|
|
padding: 16px;
|
|
border-top: 1px solid #eee;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
flex-shrink: 0;
|
|
background: #fff;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 隐藏组件内所有滚动条
|
|
:deep(.drone-management *:not(.video-list)) {
|
|
&::-webkit-scrollbar {
|
|
width: 0 !important;
|
|
display: none !important;
|
|
}
|
|
scrollbar-width: none;
|
|
-ms-overflow-style: none;
|
|
}
|
|
|
|
// 只允许列表区域显示滚动条
|
|
.video-list {
|
|
&::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
&::-webkit-scrollbar-thumb {
|
|
background: #dcdfe6;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
&::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
}
|
|
</style> |