942 lines
25 KiB
Vue
942 lines
25 KiB
Vue
<script setup>
|
||
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
||
import { getDeviceList } from '@/api/device'
|
||
import { createFlvPlayer, destroyFlvPlayer } from '@/utils/videoPlayer'
|
||
import * as echarts from 'echarts'
|
||
import { generateChartData } from '@/api/chart'
|
||
import { ElNotification, ElMessage } from 'element-plus'
|
||
import {
|
||
Monitor,
|
||
Connection,
|
||
Loading
|
||
} from '@element-plus/icons-vue'
|
||
import soundEffect from '@/utils/SoundEffect'
|
||
|
||
// 添加用户交互标记
|
||
let hasUserInteracted = false
|
||
|
||
// 监听用户交互
|
||
const handleUserInteraction = () => {
|
||
hasUserInteracted = true
|
||
// 移除事件监听器
|
||
document.removeEventListener('click', handleUserInteraction)
|
||
document.removeEventListener('keydown', handleUserInteraction)
|
||
}
|
||
|
||
// 无人机数据
|
||
const drone = ref(null)
|
||
const loading = ref(false)
|
||
const videoRef = ref(null)
|
||
const flvPlayer = ref(null)
|
||
const chartRef = ref(null)
|
||
const videoLoading = ref(true) // 添加视频加载状态
|
||
const isPlaying = ref(false) // 添加视频播放状态标志
|
||
const isBuffering = ref(false) // 添加缓冲状态标志
|
||
let chart = null
|
||
let ws = null
|
||
|
||
// 添加 WebSocket 状态管理
|
||
const wsConnected = ref(false)
|
||
let wsRetryCount = 0
|
||
const MAX_RETRY_COUNT = 3
|
||
let wsRetryTimer = null
|
||
|
||
// 存储键名常量
|
||
const CHART_DATA_STORAGE_KEY = 'wetland_ph_chart_data'
|
||
const CHART_DATA_TIMESTAMP_KEY = 'wetland_ph_chart_timestamp'
|
||
const CHART_DATA_EXPIRE_TIME = 24 * 60 * 60 * 1000 // 24小时过期
|
||
|
||
// 保存图表数据到本地存储
|
||
const saveChartDataToStorage = (data) => {
|
||
try {
|
||
localStorage.setItem(CHART_DATA_STORAGE_KEY, JSON.stringify(data))
|
||
localStorage.setItem(CHART_DATA_TIMESTAMP_KEY, Date.now().toString())
|
||
} catch (error) {
|
||
console.error('保存图表数据到本地存储失败:', error)
|
||
}
|
||
}
|
||
|
||
// 从本地存储获取图表数据
|
||
const getChartDataFromStorage = () => {
|
||
try {
|
||
const timestamp = localStorage.getItem(CHART_DATA_TIMESTAMP_KEY)
|
||
if (!timestamp) return null
|
||
|
||
// 检查数据是否过期
|
||
if (Date.now() - parseInt(timestamp) > CHART_DATA_EXPIRE_TIME) {
|
||
localStorage.removeItem(CHART_DATA_STORAGE_KEY)
|
||
localStorage.removeItem(CHART_DATA_TIMESTAMP_KEY)
|
||
return null
|
||
}
|
||
|
||
const data = localStorage.getItem(CHART_DATA_STORAGE_KEY)
|
||
return data ? JSON.parse(data) : null
|
||
} catch (error) {
|
||
console.error('从本地存储获取图表数据失败:', error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
// 获取图表数据
|
||
const getChartData = async () => {
|
||
try {
|
||
const res = await generateChartData({
|
||
message: "ph柱状图"
|
||
})
|
||
|
||
if (res.success) {
|
||
console.log('完整的响应数据:', res);
|
||
console.log('图表配置数据:', res.data?.echart_options);
|
||
|
||
if (!res.data?.echart_options) {
|
||
console.error('图表配置数据结构不完整');
|
||
// 尝试使用本地存储的数据
|
||
const storageData = getChartDataFromStorage()
|
||
if (storageData) {
|
||
console.log('使用本地存储的图表数据');
|
||
initChart(storageData)
|
||
} else {
|
||
initChart(getDefaultChartData().echart_options)
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 保存新数据到本地存储
|
||
saveChartDataToStorage(res.data.echart_options)
|
||
initChart(res.data.echart_options)
|
||
} else {
|
||
console.error('获取图表数据失败:', res.message)
|
||
// 尝试使用本地存储的数据
|
||
const storageData = getChartDataFromStorage()
|
||
if (storageData) {
|
||
console.log('使用本地存储的图表数据');
|
||
initChart(storageData)
|
||
} else {
|
||
initChart(getDefaultChartData().echart_options)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('获取图表数据失败:', error)
|
||
// 尝试使用本地存储的数据
|
||
const storageData = getChartDataFromStorage()
|
||
if (storageData) {
|
||
console.log('使用本地存储的图表数据');
|
||
initChart(storageData)
|
||
} else {
|
||
initChart(getDefaultChartData().echart_options)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 初始化图表
|
||
const initChart = (options) => {
|
||
|
||
if (!chartRef.value) {
|
||
console.error('图表DOM引用未找到');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (chart) {
|
||
chart.dispose()
|
||
}
|
||
|
||
chart = echarts.init(chartRef.value)
|
||
|
||
// 检查必要的数据结构
|
||
if (!options.xAxis?.data && !options.series?.[0]?.data) {
|
||
console.error('数据结构不正确,使用默认配置');
|
||
options = getDefaultChartData().echart_options;
|
||
}
|
||
|
||
// 确保 yAxis 配置正确
|
||
const yAxisConfig = Array.isArray(options.yAxis) ? options.yAxis[0] : options.yAxis;
|
||
|
||
const option = {
|
||
backgroundColor: 'transparent',
|
||
title: {
|
||
...(options.title || { text: 'pH值变化趋势' }),
|
||
textStyle: {
|
||
color: '#fff'
|
||
}
|
||
},
|
||
tooltip: options.tooltip || {
|
||
trigger: 'axis',
|
||
axisPointer: {
|
||
type: 'shadow'
|
||
}
|
||
},
|
||
legend: {
|
||
...(options.legend || {}),
|
||
textStyle: {
|
||
color: '#fff'
|
||
}
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '3%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: options.xAxis?.data || [],
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: 'rgba(255, 255, 255, 0.3)'
|
||
}
|
||
},
|
||
axisLabel: {
|
||
color: 'rgba(255, 255, 255, 0.7)'
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
name: 'pH值',
|
||
max: 8,
|
||
...(yAxisConfig || {}),
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: 'rgba(255, 255, 255, 0.3)'
|
||
}
|
||
},
|
||
axisLabel: {
|
||
color: 'rgba(255, 255, 255, 0.7)'
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(255, 255, 255, 0.1)'
|
||
}
|
||
}
|
||
},
|
||
series: Array.isArray(options.series) ? options.series.map(series => ({
|
||
...series,
|
||
yAxisIndex: 0, // 确保所有系列都使用同一个 yAxis
|
||
areaStyle: series.areaStyle || {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: 'rgba(64, 158, 255, 0.7)' },
|
||
{ offset: 1, color: 'rgba(64, 158, 255, 0.1)' }
|
||
])
|
||
}
|
||
})) : [
|
||
{
|
||
name: 'pH值',
|
||
type: 'line',
|
||
smooth: true,
|
||
yAxisIndex: 0,
|
||
data: [],
|
||
areaStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: 'rgba(64, 158, 255, 0.7)' },
|
||
{ offset: 1, color: 'rgba(64, 158, 255, 0.1)' }
|
||
])
|
||
}
|
||
}
|
||
]
|
||
}
|
||
|
||
// 设置配置前确保数据结构完整
|
||
if (!option.yAxis || !option.xAxis || !option.series) {
|
||
throw new Error('图表配置数据结构不完整');
|
||
}
|
||
|
||
chart.setOption(option)
|
||
|
||
// 确保图表正确渲染
|
||
nextTick(() => {
|
||
if (chart) {
|
||
chart.resize()
|
||
}
|
||
})
|
||
} catch (error) {
|
||
console.error('图表初始化失败:', error);
|
||
// 尝试使用完全默认的配置重新初始化
|
||
try {
|
||
if (chart) {
|
||
const defaultOption = getDefaultChartData().echart_options;
|
||
chart.setOption(defaultOption);
|
||
}
|
||
} catch (retryError) {
|
||
console.error('使用默认配置重试失败:', retryError);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 修改默认图表数据的结构
|
||
const getDefaultChartData = () => {
|
||
return {
|
||
echart_options: {
|
||
title: {
|
||
text: "pH值变化趋势"
|
||
},
|
||
tooltip: {
|
||
trigger: "axis",
|
||
axisPointer: {
|
||
type: "shadow"
|
||
}
|
||
},
|
||
legend: {
|
||
data: ["pH值"]
|
||
},
|
||
xAxis: {
|
||
type: "category",
|
||
data: []
|
||
},
|
||
yAxis: {
|
||
type: "value",
|
||
name: "pH值",
|
||
max: 8
|
||
},
|
||
series: [
|
||
{
|
||
name: "pH值",
|
||
type: "line",
|
||
smooth: true,
|
||
yAxisIndex: 0,
|
||
areaStyle: {},
|
||
data: []
|
||
}
|
||
]
|
||
}
|
||
}
|
||
}
|
||
|
||
// 显示设备状态通知
|
||
const showDeviceStatusNotification = (status) => {
|
||
// 播放对应音效
|
||
soundEffect.playStatusSound(status === 'online')
|
||
|
||
ElNotification({
|
||
title: status === 'online' ? '设备已接入' : '设备已接出',
|
||
message: status === 'online' ?
|
||
`设备 ${drone.value?.name || '无人机'} 已接入,正在获取视频流...` :
|
||
`设备 ${drone.value?.name || '无人机'} 已接出,切换为数据图表显示`,
|
||
type: status === 'online' ? 'success' : 'warning',
|
||
duration: 3000
|
||
})
|
||
}
|
||
|
||
// 修改 WebSocket 初始化和重连逻辑
|
||
const initWebSocket = () => {
|
||
if (wsRetryCount >= MAX_RETRY_COUNT) {
|
||
console.warn('WebSocket重试次数超过限制,切换到HTTP轮询模式')
|
||
startPolling()
|
||
return
|
||
}
|
||
|
||
try {
|
||
if (ws) {
|
||
ws.close()
|
||
ws = null
|
||
}
|
||
|
||
// 修改为本地地址
|
||
ws = new WebSocket('ws://127.0.0.1:6894')
|
||
|
||
ws.onopen = () => {
|
||
wsConnected.value = true
|
||
wsRetryCount = 0 // 重置重试次数
|
||
if (wsRetryTimer) {
|
||
clearTimeout(wsRetryTimer)
|
||
wsRetryTimer = null
|
||
}
|
||
|
||
// WebSocket连接成功后立即查询设备状态
|
||
if (ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify({
|
||
type: 'query_status',
|
||
device_code: 'BC13292E5A49914F4D62B9F356E39F56'
|
||
}))
|
||
}
|
||
}
|
||
|
||
ws.onmessage = (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data)
|
||
if (data.type === 'device_status') {
|
||
const newStatus = data.status === 1 ? 'online' : 'offline'
|
||
const oldStatus = drone.value?.status
|
||
|
||
// 只在状态真实发生变化时更新
|
||
if (!drone.value || oldStatus !== newStatus) {
|
||
// 立即更新设备状态
|
||
if (!drone.value) {
|
||
drone.value = {
|
||
id: data.device_id,
|
||
name: data.device_name || '无人机',
|
||
code: data.device_code,
|
||
status: newStatus,
|
||
battery: data.battery_level || 0,
|
||
signal: data.signal_strength || 0
|
||
}
|
||
} else {
|
||
drone.value.status = newStatus
|
||
}
|
||
|
||
// 显示状态变化通知
|
||
showDeviceStatusNotification(newStatus)
|
||
|
||
// 根据新状态初始化视频或图表
|
||
nextTick(async () => {
|
||
if (newStatus === 'online') {
|
||
if (chart) {
|
||
chart.dispose()
|
||
chart = null
|
||
}
|
||
await initVideoPlayer()
|
||
} else {
|
||
if (flvPlayer.value) {
|
||
destroyFlvPlayer(flvPlayer.value)
|
||
flvPlayer.value = null
|
||
}
|
||
getChartData()
|
||
}
|
||
})
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('WebSocket消息处理错误:', error)
|
||
}
|
||
}
|
||
|
||
ws.onerror = (error) => {
|
||
console.error('WebSocket错误:', error)
|
||
wsConnected.value = false
|
||
wsRetryCount++
|
||
|
||
// 如果重试次数超过限制,切换到HTTP轮询
|
||
if (wsRetryCount >= MAX_RETRY_COUNT) {
|
||
console.warn('WebSocket连接失败,切换到HTTP轮询模式')
|
||
startPolling()
|
||
} else {
|
||
// 否则尝试重连
|
||
wsRetryTimer = setTimeout(() => {
|
||
|
||
console.log(`尝试WebSocket重连 (${wsRetryCount}/${MAX_RETRY_COUNT})...`)
|
||
initWebSocket()
|
||
}, 5000 * Math.min(wsRetryCount, 3)) // 重试间隔随次数增加但最多15秒
|
||
}
|
||
}
|
||
|
||
ws.onclose = () => {
|
||
console.log('WebSocket连接关闭')
|
||
wsConnected.value = false
|
||
|
||
// 只有在非主动关闭的情况下才重连
|
||
if (!ws._closeInitiated) {
|
||
wsRetryTimer = setTimeout(() => {
|
||
initWebSocket()
|
||
}, 5000)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('WebSocket初始化失败:', error)
|
||
startPolling()
|
||
}
|
||
}
|
||
|
||
// 添加HTTP轮询模式
|
||
let pollingTimer = null
|
||
const startPolling = () => {
|
||
// 清理已有的定时器
|
||
if (pollingTimer) {
|
||
clearInterval(pollingTimer)
|
||
}
|
||
|
||
// 立即执行一次
|
||
getDroneInfo()
|
||
|
||
// 设置轮询间隔
|
||
pollingTimer = setInterval(() => {
|
||
getDroneInfo()
|
||
}, 10000) // 10秒轮询一次
|
||
}
|
||
|
||
// 修改获取设备信息的逻辑
|
||
const getDroneInfo = async () => {
|
||
if (loading.value) return
|
||
loading.value = true
|
||
|
||
try {
|
||
const res = await getDeviceList({
|
||
page: 1,
|
||
page_size: 10,
|
||
device_type: '10001'
|
||
})
|
||
|
||
if (res.code === 200) {
|
||
const targetDrone = res.data.list.find(item =>
|
||
item.device_code === 'BC13292E5A49914F4D62B9F356E39F56'
|
||
)
|
||
|
||
if (targetDrone) {
|
||
// 设备状态判断逻辑
|
||
const deviceStatus = targetDrone.status?.code || targetDrone.status
|
||
const deviceOnline = deviceStatus === 1
|
||
const newStatus = deviceOnline ? 'online' : 'offline'
|
||
|
||
// 检查状态是否发生变化
|
||
const oldStatus = drone.value?.status
|
||
if (oldStatus && oldStatus !== newStatus) {
|
||
showDeviceStatusNotification(newStatus)
|
||
}
|
||
|
||
// 只有在状态发生变化或首次加载时才更新数据
|
||
if (!drone.value || oldStatus !== newStatus) {
|
||
drone.value = {
|
||
id: targetDrone.id,
|
||
name: targetDrone.device_name,
|
||
code: targetDrone.device_code,
|
||
status: newStatus,
|
||
battery: targetDrone.battery_level || 0,
|
||
signal: targetDrone.signal_strength || 0
|
||
}
|
||
}
|
||
} else {
|
||
if (drone.value !== null) {
|
||
drone.value = null
|
||
getChartData()
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('获取设备信息失败:', error)
|
||
if (!drone.value) {
|
||
getChartData()
|
||
}
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 初始化视频播放器
|
||
const initVideoPlayer = async () => {
|
||
videoLoading.value = true // 开始加载时显示loading
|
||
isPlaying.value = false // 重置播放状态
|
||
isBuffering.value = false // 重置缓冲状态
|
||
|
||
try {
|
||
// 销毁之前的播放器
|
||
if (flvPlayer.value) {
|
||
destroyFlvPlayer(flvPlayer.value)
|
||
flvPlayer.value = null
|
||
}
|
||
|
||
// 等待 DOM 更新
|
||
await nextTick()
|
||
|
||
// 初始化新的播放器
|
||
if (videoRef.value && drone.value?.status === 'online') {
|
||
// 重置视频元素
|
||
const videoElement = videoRef.value
|
||
videoElement.pause()
|
||
videoElement.currentTime = 0
|
||
videoElement.muted = true
|
||
|
||
// 移除旧的事件监听器
|
||
const oldListeners = videoElement._eventListeners || []
|
||
oldListeners.forEach(({ event, handler }) => {
|
||
videoElement.removeEventListener(event, handler)
|
||
})
|
||
videoElement._eventListeners = []
|
||
|
||
// 创建新的播放器
|
||
flvPlayer.value = createFlvPlayer(videoElement, drone.value.code)
|
||
|
||
if (flvPlayer.value) {
|
||
// 添加 flv.js 事件监听
|
||
flvPlayer.value.on('loading', () => {
|
||
if (!isPlaying.value) {
|
||
videoLoading.value = true
|
||
isBuffering.value = true
|
||
}
|
||
})
|
||
|
||
flvPlayer.value.on('error', (errorType, errorDetail) => {
|
||
console.error('视频播放错误:', errorType, errorDetail)
|
||
ElMessage.error('视频加载失败,请检查网络连接')
|
||
videoLoading.value = false
|
||
isPlaying.value = false
|
||
isBuffering.value = false
|
||
})
|
||
|
||
// 添加视频元素事件监听
|
||
const addVideoListener = (event, handler) => {
|
||
videoElement.addEventListener(event, handler)
|
||
videoElement._eventListeners.push({ event, handler })
|
||
}
|
||
|
||
addVideoListener('loadstart', () => {
|
||
videoLoading.value = true
|
||
isBuffering.value = true
|
||
})
|
||
|
||
addVideoListener('waiting', () => {
|
||
isBuffering.value = true
|
||
if (!isPlaying.value) {
|
||
videoLoading.value = true
|
||
}
|
||
})
|
||
|
||
addVideoListener('playing', () => {
|
||
videoLoading.value = false
|
||
isPlaying.value = true
|
||
isBuffering.value = false
|
||
})
|
||
|
||
addVideoListener('canplay', () => {
|
||
isBuffering.value = false
|
||
if (!isPlaying.value) {
|
||
videoLoading.value = false
|
||
}
|
||
})
|
||
|
||
addVideoListener('error', (e) => {
|
||
console.error('视频元素错误:', e)
|
||
videoLoading.value = false
|
||
isPlaying.value = false
|
||
isBuffering.value = false
|
||
ElMessage.error('视频加载失败,请检查网络连接')
|
||
})
|
||
|
||
// 尝试播放视频
|
||
try {
|
||
await videoElement.play()
|
||
} catch (error) {
|
||
if (error.name === 'AbortError') {
|
||
console.log('视频播放被中断,重试中...')
|
||
// 短暂延迟后重试播放
|
||
setTimeout(async () => {
|
||
try {
|
||
await videoElement.play()
|
||
} catch (retryError) {
|
||
console.error('视频重试播放失败:', retryError)
|
||
ElMessage.error('视频播放失败,请刷新页面重试')
|
||
}
|
||
}, 100)
|
||
} else {
|
||
console.error('视频播放失败:', error)
|
||
ElMessage.error('视频播放失败,请检查网络连接')
|
||
}
|
||
}
|
||
} else {
|
||
console.error('播放器创建失败')
|
||
ElMessage.error('视频播放器初始化失败')
|
||
videoLoading.value = false
|
||
isPlaying.value = false
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('视频播放器初始化失败:', error)
|
||
ElMessage.error('视频播放器初始化失败')
|
||
videoLoading.value = false
|
||
isPlaying.value = false
|
||
isBuffering.value = false
|
||
}
|
||
}
|
||
|
||
// 定时刷新设备状态和图表数据
|
||
let refreshTimer = null
|
||
const startRefreshTimer = () => {
|
||
refreshTimer = setInterval(() => {
|
||
// 只在WebSocket连接断开时才通过HTTP轮询获取状态
|
||
if (ws?.readyState !== WebSocket.OPEN) {
|
||
getDroneInfo()
|
||
}
|
||
|
||
// 只在设备离线时才刷新图表数据
|
||
if (drone.value?.status !== 'online') {
|
||
getChartData()
|
||
}
|
||
}, 60000) // 改为每60秒刷新一次
|
||
}
|
||
|
||
// 处理窗口大小变化
|
||
const handleResize = () => {
|
||
chart?.resize()
|
||
}
|
||
|
||
// 监听设备状态变化
|
||
watch(() => drone.value?.status, (newStatus, oldStatus) => {
|
||
// 设备状态变化
|
||
if (newStatus !== oldStatus) {
|
||
// 显示状态变化通知
|
||
if (oldStatus) { // 只在状态真实变化时显示通知
|
||
showDeviceStatusNotification(newStatus)
|
||
}
|
||
|
||
// 根据新状态处理视频或图表
|
||
nextTick(async () => {
|
||
if (newStatus === 'online') {
|
||
// 设备上线,销毁图表并初始化视频播放器
|
||
if (chart) {
|
||
chart.dispose()
|
||
chart = null
|
||
}
|
||
await initVideoPlayer()
|
||
} else {
|
||
// 设备离线,销毁视频播放器并初始化图表
|
||
if (flvPlayer.value) {
|
||
destroyFlvPlayer(flvPlayer.value)
|
||
flvPlayer.value = null
|
||
}
|
||
getChartData()
|
||
}
|
||
})
|
||
}
|
||
}, { immediate: true })
|
||
|
||
onMounted(() => {
|
||
// 添加用户交互监听
|
||
document.addEventListener('click', handleUserInteraction)
|
||
document.addEventListener('keydown', handleUserInteraction)
|
||
|
||
// 初始化时先尝试使用本地存储的数据
|
||
const storageData = getChartDataFromStorage()
|
||
if (storageData) {
|
||
console.log('使用本地存储的图表数据初始化');
|
||
initChart(storageData)
|
||
}
|
||
|
||
// 初始化WebSocket连接
|
||
initWebSocket()
|
||
// 获取无人机信息
|
||
getDroneInfo()
|
||
// 启动定时刷新
|
||
startRefreshTimer()
|
||
// 监听窗口大小变化
|
||
window.addEventListener('resize', handleResize)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
// 移除用户交互监听
|
||
document.removeEventListener('click', handleUserInteraction)
|
||
document.removeEventListener('keydown', handleUserInteraction)
|
||
|
||
// 标记WebSocket为主动关闭
|
||
if (ws) {
|
||
ws._closeInitiated = true
|
||
ws.close()
|
||
ws = null
|
||
}
|
||
|
||
// 清理所有定时器
|
||
if (wsRetryTimer) {
|
||
clearTimeout(wsRetryTimer)
|
||
}
|
||
if (pollingTimer) {
|
||
clearInterval(pollingTimer)
|
||
}
|
||
if (refreshTimer) {
|
||
clearInterval(refreshTimer)
|
||
}
|
||
|
||
// 清理播放器和图表
|
||
if (flvPlayer.value) {
|
||
destroyFlvPlayer(flvPlayer.value)
|
||
}
|
||
if (chart) {
|
||
chart.dispose()
|
||
chart = null
|
||
}
|
||
|
||
wsConnected.value = false
|
||
window.removeEventListener('resize', handleResize)
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="video-monitor">
|
||
<div class="monitor-header">
|
||
<div class="title">{{ drone?.status === 'online' ? '无人机实时监控' : '历史数据监测' }}</div>
|
||
<div class="status" v-if="drone">
|
||
<el-tag :type="drone?.status === 'online' ? 'success' : 'danger'" size="small">
|
||
{{ drone?.status === 'online' ? '在线' : '离线' }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content-container">
|
||
<template v-if="drone?.status === 'online'">
|
||
<!-- 在线显示视频 -->
|
||
<div class="video-wrapper">
|
||
<video
|
||
ref="videoRef"
|
||
class="video-player"
|
||
muted
|
||
></video>
|
||
<div v-if="videoLoading && !isPlaying" class="video-loading">
|
||
<el-icon class="loading-icon"><Loading /></el-icon>
|
||
<span class="loading-text">视频加载中...</span>
|
||
</div>
|
||
<div class="video-info">
|
||
<div class="info-row">
|
||
<span class="device-name">{{ drone.name }}</span>
|
||
<div class="device-metrics">
|
||
<el-tooltip content="电池电量" placement="top">
|
||
<div class="metric">
|
||
<el-icon><Monitor /></el-icon>
|
||
{{ drone.battery }}%
|
||
</div>
|
||
</el-tooltip>
|
||
<el-tooltip content="信号强度" placement="top">
|
||
<div class="metric" :class="{ 'poor-signal': isBuffering }">
|
||
<el-icon><Connection /></el-icon>
|
||
{{ isBuffering ? '-110dBm' : '-88dBm' }}
|
||
</div>
|
||
</el-tooltip>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<!-- 离线显示图表 -->
|
||
<div ref="chartRef" class="chart-wrapper"></div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.video-monitor {
|
||
height: 100%;
|
||
padding: 20px;
|
||
background: rgba(0, 0, 0, 0.2);
|
||
border-radius: 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.monitor-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
|
||
.title {
|
||
font-size: 20px;
|
||
font-weight: 500;
|
||
color: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
}
|
||
|
||
.content-container {
|
||
flex: 1;
|
||
position: relative;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
background: rgba(0, 0, 0, 0.4);
|
||
|
||
.video-wrapper {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
|
||
.video-player {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
// 添加 loading 样式
|
||
.video-loading {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 16px;
|
||
z-index: 2;
|
||
|
||
.loading-icon {
|
||
font-size: 32px;
|
||
color: #fff;
|
||
animation: rotate 1s linear infinite;
|
||
}
|
||
|
||
.loading-text {
|
||
color: #fff;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
.video-info {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
padding: 20px;
|
||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||
|
||
.info-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
|
||
.device-name {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
color: #fff;
|
||
}
|
||
|
||
.device-metrics {
|
||
display: flex;
|
||
gap: 20px;
|
||
|
||
.metric {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
color: #fff;
|
||
font-size: 14px;
|
||
transition: all 0.3s ease;
|
||
|
||
&.poor-signal {
|
||
color: #F56C6C;
|
||
animation: blink 1s infinite;
|
||
}
|
||
|
||
.el-icon {
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.chart-wrapper {
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 24, 65, 0.3);
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
box-sizing: border-box;
|
||
border: 1px solid rgba(63, 167, 221, 0.2);
|
||
}
|
||
}
|
||
}
|
||
|
||
@keyframes rotate {
|
||
from {
|
||
transform: rotate(0deg);
|
||
}
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
@keyframes blink {
|
||
0% { opacity: 0.6; }
|
||
50% { opacity: 1; }
|
||
100% { opacity: 0.6; }
|
||
}
|
||
</style> |