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>