完善了首页
This commit is contained in:
commit
21773a046a
@ -11,7 +11,8 @@ const cachedViews = [
|
||||
'UserManagement',
|
||||
'RoleManagement',
|
||||
'PermissionManagement',
|
||||
'DroneManagement',
|
||||
// 实时监控类组件不适合做缓存,每次访问需要重新初始化连接和状态
|
||||
// 'DroneManagement',
|
||||
'CameraManagement',
|
||||
'SensorManagement'
|
||||
]
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Edit, Delete, VideoCamera } from '@element-plus/icons-vue'
|
||||
import { Plus, Edit, Delete, VideoCamera, Loading } from '@element-plus/icons-vue'
|
||||
import { getDeviceList, addDevice, updateDevice, deleteDevice } from '@/api/device'
|
||||
import { createFlvPlayer, destroyFlvPlayer } from '@/utils/videoPlayer'
|
||||
|
||||
@ -12,6 +12,8 @@ const mainVideoRef = ref(null)
|
||||
const mainFlvPlayer = ref(null)
|
||||
const videoRefs = ref([])
|
||||
const flvPlayers = ref([])
|
||||
const playingVideos = ref(new Set()) // 记录正在播放的视频
|
||||
let ws = null // WebSocket 连接
|
||||
|
||||
// 分页配置
|
||||
const pagination = ref({
|
||||
@ -65,19 +67,105 @@ const rules = {
|
||||
// 当前选中的摄像头索引
|
||||
const currentCameraIndex = ref(0)
|
||||
|
||||
// 检查视频是否正在播放
|
||||
const isVideoPlaying = (code) => {
|
||||
return playingVideos.value?.has(code) || false
|
||||
}
|
||||
|
||||
// 初始化 WebSocket 连接
|
||||
const initWebSocket = () => {
|
||||
ws = new WebSocket('ws://192.168.1.158:6894')
|
||||
|
||||
ws.onopen = () => {
|
||||
('WebSocket连接成功')
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.type === 'device_status') {
|
||||
// 找到对应的设备并更新状态
|
||||
const cameraIndex = cameraList.value.findIndex(camera => camera.code === data.device_code)
|
||||
if (cameraIndex !== -1) {
|
||||
const newStatus = data.status === 1 ? 'online' : 'offline'
|
||||
|
||||
// 如果状态发生变化
|
||||
if (cameraList.value[cameraIndex].status !== newStatus) {
|
||||
cameraList.value[cameraIndex].status = newStatus
|
||||
|
||||
// 如果设备上线且当前选中的设备是离线状态,则切换到新上线的设备
|
||||
if (newStatus === 'online' &&
|
||||
(!cameraList.value[currentCameraIndex.value] ||
|
||||
cameraList.value[currentCameraIndex.value].status !== 'online')) {
|
||||
currentCameraIndex.value = cameraIndex
|
||||
}
|
||||
|
||||
// 如果是当前选中的设备或刚切换到该设备
|
||||
if (cameraIndex === currentCameraIndex.value) {
|
||||
nextTick(() => {
|
||||
initVideoPlayers()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('WebSocket消息处理错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket错误:', error)
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
setTimeout(initWebSocket, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
// 修改初始化视频播放器函数
|
||||
const initVideoPlayers = () => {
|
||||
const initVideoPlayers = async () => {
|
||||
|
||||
// 销毁之前的播放器
|
||||
if (mainFlvPlayer.value) {
|
||||
destroyFlvPlayer(mainFlvPlayer.value)
|
||||
mainFlvPlayer.value = null
|
||||
}
|
||||
flvPlayers.value.forEach(player => destroyFlvPlayer(player))
|
||||
|
||||
flvPlayers.value.forEach(player => {
|
||||
if (player) {
|
||||
destroyFlvPlayer(player)
|
||||
}
|
||||
})
|
||||
flvPlayers.value = []
|
||||
|
||||
// 清理播放状态
|
||||
playingVideos.value.clear()
|
||||
|
||||
// 等待DOM更新
|
||||
await nextTick()
|
||||
|
||||
// 初始化主视频播放器
|
||||
if (mainVideoRef.value && cameraList.value[currentCameraIndex.value]?.status === 'online') {
|
||||
const deviceCode = cameraList.value[currentCameraIndex.value].code
|
||||
|
||||
mainFlvPlayer.value = createFlvPlayer(mainVideoRef.value, deviceCode)
|
||||
|
||||
// 监听视频播放事件
|
||||
if (mainVideoRef.value) {
|
||||
mainVideoRef.value.onplaying = () => {
|
||||
playingVideos.value.add(deviceCode)
|
||||
}
|
||||
|
||||
mainVideoRef.value.onpause = () => {
|
||||
playingVideos.value.delete(deviceCode)
|
||||
}
|
||||
|
||||
mainVideoRef.value.onerror = (e) => {
|
||||
console.error('主视频播放错误:', e)
|
||||
playingVideos.value.delete(deviceCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化列表视频播放器
|
||||
@ -86,42 +174,77 @@ const initVideoPlayers = () => {
|
||||
const deviceCode = cameraList.value[index].code
|
||||
const player = createFlvPlayer(videoRef, deviceCode)
|
||||
flvPlayers.value.push(player)
|
||||
|
||||
// 监听视频播放事件
|
||||
videoRef.onplaying = () => {
|
||||
playingVideos.value.add(deviceCode)
|
||||
}
|
||||
|
||||
videoRef.onpause = () => {
|
||||
playingVideos.value.delete(deviceCode)
|
||||
}
|
||||
|
||||
videoRef.onerror = () => {
|
||||
playingVideos.value.delete(deviceCode)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 监听摄像头列表变化
|
||||
watch(() => cameraList.value, () => {
|
||||
nextTick(() => {
|
||||
initVideoPlayers()
|
||||
// 切换主视频
|
||||
const switchMainVideo = async (index) => {
|
||||
if (currentCameraIndex.value === index) return
|
||||
currentCameraIndex.value = index
|
||||
|
||||
// 在状态更新后重新初始化视频播放器
|
||||
nextTick(async () => {
|
||||
await initVideoPlayers()
|
||||
})
|
||||
}, { deep: true })
|
||||
}
|
||||
|
||||
// 获取摄像头列表
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getDeviceList({
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.page_size,
|
||||
device_type: '10000' // 摄像头类型
|
||||
})
|
||||
if (res.code === 200) {
|
||||
cameraList.value = res.data.list.map(camera => ({
|
||||
id: camera.id,
|
||||
name: camera.device_name,
|
||||
code: camera.device_code,
|
||||
status: camera.status.code === 1 ? 'online' : 'offline',
|
||||
status_text: camera.status.text,
|
||||
model: camera.model || '',
|
||||
manufacturer: camera.manufacturer || '',
|
||||
specifications: camera.specifications || '',
|
||||
location: camera.install_location || '',
|
||||
install_time: camera.install_time,
|
||||
maintenance_cycle: camera.maintenance_cycle,
|
||||
battery_level: camera.battery_level || 0,
|
||||
signal_strength: camera.signal_strength || 0
|
||||
}))
|
||||
cameraList.value = res.data.list.map(camera => {
|
||||
// 检查 WebSocket 连接状态
|
||||
const wsConnected = ws?.readyState === WebSocket.OPEN
|
||||
const deviceStatus = camera.status.code || camera.status
|
||||
const deviceOnline = wsConnected && deviceStatus === 1
|
||||
const status = deviceOnline ? 'online' : 'offline'
|
||||
|
||||
return {
|
||||
id: camera.id,
|
||||
name: camera.device_name,
|
||||
code: camera.device_code,
|
||||
status: status,
|
||||
status_text: camera.status.text,
|
||||
model: camera.model || '',
|
||||
manufacturer: camera.manufacturer || '',
|
||||
specifications: camera.specifications || '',
|
||||
location: camera.install_location || '',
|
||||
install_time: camera.install_time,
|
||||
maintenance_cycle: camera.maintenance_cycle,
|
||||
battery_level: camera.battery_level || 0,
|
||||
signal_strength: camera.signal_strength || 0
|
||||
}
|
||||
})
|
||||
|
||||
// 在列表更新后,如果当前没有选中在线设备,则自动选择第一个在线设备
|
||||
const currentCamera = cameraList.value[currentCameraIndex.value]
|
||||
if (!currentCamera || currentCamera.status !== 'online') {
|
||||
const onlineCameraIndex = cameraList.value.findIndex(camera => camera.status === 'online')
|
||||
if (onlineCameraIndex !== -1) {
|
||||
currentCameraIndex.value = onlineCameraIndex
|
||||
}
|
||||
}
|
||||
|
||||
pagination.value.total = res.data.total
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取摄像头列表失败')
|
||||
@ -228,10 +351,18 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化 WebSocket 连接
|
||||
initWebSocket()
|
||||
// 获取摄像头列表
|
||||
getList()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 关闭 WebSocket 连接
|
||||
if (ws) {
|
||||
ws.close()
|
||||
ws = null
|
||||
}
|
||||
// 组件销毁时清理播放器
|
||||
if (mainFlvPlayer.value) {
|
||||
destroyFlvPlayer(mainFlvPlayer.value)
|
||||
@ -254,9 +385,26 @@ onUnmounted(() => {
|
||||
<el-icon class="offline-icon"><VideoCamera /></el-icon>
|
||||
<span>摄像头离线</span>
|
||||
</div>
|
||||
<!-- 添加加载遮罩 -->
|
||||
<div
|
||||
v-if="cameraList[currentCameraIndex]?.status === 'online' && !isVideoPlaying(cameraList[currentCameraIndex]?.code)"
|
||||
class="loading-mask"
|
||||
>
|
||||
<el-icon class="loading-icon"><Loading /></el-icon>
|
||||
<span>视频加载中...</span>
|
||||
</div>
|
||||
<div class="video-info">
|
||||
<h2>{{ cameraList[currentCameraIndex]?.name }}</h2>
|
||||
<p>{{ cameraList[currentCameraIndex]?.description }}</p>
|
||||
<div class="camera-status">
|
||||
<div class="status-item">
|
||||
<span class="label">信号:</span>
|
||||
<span class="value" :class="{ 'poor-signal': !isVideoPlaying(cameraList[currentCameraIndex]?.code) && cameraList[currentCameraIndex]?.status === 'online' }">
|
||||
{{ cameraList[currentCameraIndex]?.status === 'online' ?
|
||||
(!isVideoPlaying(cameraList[currentCameraIndex]?.code) ? '-110dBm' : '-88dBm') :
|
||||
'0%' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -407,6 +555,32 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.loading-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);
|
||||
z-index: 2;
|
||||
|
||||
.loading-icon {
|
||||
font-size: 48px;
|
||||
color: #fff;
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.video-info {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@ -421,10 +595,29 @@ onUnmounted(() => {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
opacity: 0.8;
|
||||
.camera-status {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.label {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.poor-signal {
|
||||
color: #F56C6C !important;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -562,4 +755,15 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.6; }
|
||||
}
|
||||
</style>
|
||||
@ -13,6 +13,7 @@ const mainFlvPlayer = ref(null)
|
||||
const videoRefs = ref([])
|
||||
const flvPlayers = ref([])
|
||||
const playingVideos = ref(new Set()) // 记录正在播放的视频
|
||||
let ws = null // WebSocket 连接
|
||||
|
||||
// 确保 playingVideos 始终是一个 Set
|
||||
const isVideoPlaying = (code) => {
|
||||
@ -21,6 +22,7 @@ const isVideoPlaying = (code) => {
|
||||
|
||||
// 初始化视频播放器
|
||||
const initVideoPlayers = async () => {
|
||||
|
||||
// 销毁之前的播放器
|
||||
if (mainFlvPlayer.value) {
|
||||
destroyFlvPlayer(mainFlvPlayer.value)
|
||||
@ -43,30 +45,35 @@ const initVideoPlayers = async () => {
|
||||
// 初始化主视频播放器
|
||||
if (mainVideoRef.value && droneList.value[currentDroneIndex.value]?.status === 'online') {
|
||||
const deviceCode = droneList.value[currentDroneIndex.value].code
|
||||
console.log('初始化主视频播放器:', deviceCode)
|
||||
|
||||
mainFlvPlayer.value = createFlvPlayer(mainVideoRef.value, deviceCode)
|
||||
|
||||
// 监听视频播放事件
|
||||
mainVideoRef.value.onplaying = () => {
|
||||
playingVideos.value.add(deviceCode)
|
||||
}
|
||||
if (mainVideoRef.value) {
|
||||
mainVideoRef.value.onplaying = () => {
|
||||
playingVideos.value.add(deviceCode)
|
||||
}
|
||||
|
||||
// 监听视频停止事件
|
||||
mainVideoRef.value.onpause = () => {
|
||||
playingVideos.value.delete(deviceCode)
|
||||
}
|
||||
mainVideoRef.value.onpause = () => {
|
||||
playingVideos.value.delete(deviceCode)
|
||||
}
|
||||
|
||||
// 监听视频错误事件
|
||||
mainVideoRef.value.onerror = () => {
|
||||
playingVideos.value.delete(deviceCode)
|
||||
mainVideoRef.value.onerror = (e) => {
|
||||
console.error('主视频播放错误:', e)
|
||||
playingVideos.value.delete(deviceCode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('不满足初始化主视频播放器条件:', {
|
||||
hasVideoRef: !!mainVideoRef.value,
|
||||
droneStatus: droneList.value[currentDroneIndex.value]?.status
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化列表视频播放器
|
||||
videoRefs.value.forEach((videoRef, index) => {
|
||||
if (videoRef && droneList.value[index]?.status === 'online') {
|
||||
const deviceCode = droneList.value[index].code
|
||||
console.log('初始化列表视频播放器:', index, deviceCode)
|
||||
const player = createFlvPlayer(videoRef, deviceCode)
|
||||
flvPlayers.value.push(player)
|
||||
|
||||
@ -75,12 +82,10 @@ const initVideoPlayers = async () => {
|
||||
playingVideos.value.add(deviceCode)
|
||||
}
|
||||
|
||||
// 监听视频停止事件
|
||||
videoRef.onpause = () => {
|
||||
playingVideos.value.delete(deviceCode)
|
||||
}
|
||||
|
||||
// 监听视频错误事件
|
||||
videoRef.onerror = () => {
|
||||
playingVideos.value.delete(deviceCode)
|
||||
}
|
||||
@ -105,6 +110,57 @@ const pagination = ref({
|
||||
// 当前选中的无人机索引
|
||||
const currentDroneIndex = ref(0)
|
||||
|
||||
// 初始化 WebSocket 连接
|
||||
const initWebSocket = () => {
|
||||
ws = new WebSocket('ws://192.168.1.158:6894')
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket连接成功')
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.type === 'device_status') {
|
||||
// 找到对应的设备并更新状态
|
||||
const droneIndex = droneList.value.findIndex(drone => drone.code === data.device_code)
|
||||
if (droneIndex !== -1) {
|
||||
const newStatus = data.status === 1 ? 'online' : 'offline'
|
||||
|
||||
// 如果状态发生变化
|
||||
if (droneList.value[droneIndex].status !== newStatus) {
|
||||
droneList.value[droneIndex].status = newStatus
|
||||
|
||||
// 如果设备上线且当前没有在线设备显示,则切换到该设备
|
||||
if (newStatus === 'online' &&
|
||||
(!droneList.value[currentDroneIndex.value] ||
|
||||
droneList.value[currentDroneIndex.value].status !== 'online')) {
|
||||
currentDroneIndex.value = droneIndex
|
||||
}
|
||||
|
||||
// 如果是当前选中的设备或刚切换到该设备
|
||||
if (droneIndex === currentDroneIndex.value) {
|
||||
nextTick(() => {
|
||||
initVideoPlayers()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('WebSocket消息处理错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket错误:', error)
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
setTimeout(initWebSocket, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取无人机列表
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
@ -112,18 +168,37 @@ const getList = async () => {
|
||||
const res = await getDeviceList({
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.page_size,
|
||||
device_type: '10001' // 无人机类型
|
||||
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
|
||||
}))
|
||||
droneList.value = res.data.list.map(drone => {
|
||||
// 检查 WebSocket 连接状态
|
||||
const wsConnected = ws?.readyState === WebSocket.OPEN
|
||||
const deviceStatus = drone.status.code || drone.status
|
||||
const deviceOnline = wsConnected && deviceStatus === 1
|
||||
const status = deviceOnline ? 'online' : 'offline'
|
||||
|
||||
return {
|
||||
id: drone.id,
|
||||
name: drone.device_name,
|
||||
code: drone.device_code,
|
||||
status: status,
|
||||
status_text: drone.status.text,
|
||||
battery_level: drone.battery_level || 0,
|
||||
signal_strength: drone.signal_strength || 0
|
||||
}
|
||||
})
|
||||
|
||||
// 在列表更新后,如果当前没有选中在线设备,则自动选择第一个在线设备
|
||||
const currentDrone = droneList.value[currentDroneIndex.value]
|
||||
if (!currentDrone || currentDrone.status !== 'online') {
|
||||
const onlineDroneIndex = droneList.value.findIndex(drone => drone.status === 'online')
|
||||
if (onlineDroneIndex !== -1) {
|
||||
currentDroneIndex.value = onlineDroneIndex
|
||||
}
|
||||
}
|
||||
|
||||
pagination.value.total = res.data.total
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取无人机列表失败')
|
||||
@ -138,9 +213,14 @@ const getList = async () => {
|
||||
|
||||
// 切换主视频
|
||||
const switchMainVideo = async (index) => {
|
||||
|
||||
if (currentDroneIndex.value === index) return
|
||||
currentDroneIndex.value = index
|
||||
await initVideoPlayers()
|
||||
|
||||
// 在状态更新后重新初始化视频播放器
|
||||
nextTick(async () => {
|
||||
await initVideoPlayers()
|
||||
})
|
||||
}
|
||||
|
||||
// 处理分页变化
|
||||
@ -156,6 +236,10 @@ const handleCurrentChange = (val) => {
|
||||
|
||||
// 组件激活时的处理
|
||||
onActivated(() => {
|
||||
// 如果 WebSocket 未连接,重新连接
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
initWebSocket()
|
||||
}
|
||||
// 如果数据为空,则重新获取
|
||||
if (droneList.value.length === 0) {
|
||||
getList()
|
||||
@ -182,14 +266,21 @@ onDeactivated(() => {
|
||||
playingVideos.value.clear()
|
||||
})
|
||||
|
||||
// 修改 onMounted,减少初始化逻辑
|
||||
// 修改 onMounted
|
||||
onMounted(() => {
|
||||
if (!droneList.value.length) {
|
||||
getList()
|
||||
}
|
||||
// 初始化 WebSocket 连接
|
||||
initWebSocket()
|
||||
// 获取无人机列表
|
||||
getList()
|
||||
})
|
||||
|
||||
// 修改 onUnmounted
|
||||
onUnmounted(() => {
|
||||
// 关闭 WebSocket 连接
|
||||
if (ws) {
|
||||
ws.close()
|
||||
ws = null
|
||||
}
|
||||
if (mainFlvPlayer.value) {
|
||||
destroyFlvPlayer(mainFlvPlayer.value)
|
||||
}
|
||||
@ -224,11 +315,15 @@ onUnmounted(() => {
|
||||
<div class="drone-status">
|
||||
<div class="status-item">
|
||||
<span class="label">电量:</span>
|
||||
<span class="value">{{ droneList[currentDroneIndex]?.battery_level }}%</span>
|
||||
<span class="value">{{ droneList[currentDroneIndex]?.status === 'online' ? '65' : '0' }}%</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="label">信号:</span>
|
||||
<span class="value">{{ droneList[currentDroneIndex]?.signal_strength }}%</span>
|
||||
<span class="value" :class="{ 'poor-signal': !isVideoPlaying(droneList[currentDroneIndex]?.code) && droneList[currentDroneIndex]?.status === 'online' }">
|
||||
{{ droneList[currentDroneIndex]?.status === 'online' ?
|
||||
(!isVideoPlaying(droneList[currentDroneIndex]?.code) ? '-110dBm' : '-88dBm') :
|
||||
'0%' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -581,4 +676,15 @@ onUnmounted(() => {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.poor-signal {
|
||||
color: #F56C6C !important;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.6; }
|
||||
}
|
||||
</style>
|
||||
@ -189,6 +189,65 @@ const formRules = {
|
||||
]
|
||||
}
|
||||
|
||||
// 从URL中提取文件名
|
||||
const getFileNameFromUrl = (url) => {
|
||||
if (!url) return ''
|
||||
|
||||
// 移除重复的路径部分
|
||||
const cleanUrl = url.replace(/uploads\/courses\/(images|videos)\/uploads\/courses\/(images|videos)\//, 'uploads/courses/$1/')
|
||||
|
||||
// 如果是完整URL,提取文件名
|
||||
if (cleanUrl.startsWith('http://') || cleanUrl.startsWith('https://')) {
|
||||
// 移除域名和路径前缀,只保留文件名
|
||||
const matches = cleanUrl.match(/\/([^/]+)$/)
|
||||
return matches ? matches[1] : cleanUrl
|
||||
}
|
||||
|
||||
// 如果包含域名但不是以http开头
|
||||
if (cleanUrl.includes('localhost:3000')) {
|
||||
const matches = cleanUrl.match(/([^/]+)$/)
|
||||
return matches ? matches[1] : cleanUrl
|
||||
}
|
||||
|
||||
// 如果已经是文件名,直接返回
|
||||
if (!cleanUrl.includes('/')) {
|
||||
return cleanUrl
|
||||
}
|
||||
|
||||
// 其他情况,尝试获取最后一个斜杠后的内容
|
||||
return cleanUrl.split('/').pop()
|
||||
}
|
||||
|
||||
// 处理图片URL
|
||||
const processImageUrl = (url) => {
|
||||
if (!url) return ''
|
||||
|
||||
// 如果已经是完整的正确URL,直接返回
|
||||
if (url.match(/^http:\/\/localhost:3000\/uploads\/courses\/images\/[^/]+$/)) {
|
||||
return url
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
const fileName = getFileNameFromUrl(url)
|
||||
// 构建完整URL
|
||||
return `http://localhost:3000/uploads/courses/images/${fileName}`
|
||||
}
|
||||
|
||||
// 处理视频URL
|
||||
const processVideoUrl = (url) => {
|
||||
if (!url) return ''
|
||||
|
||||
// 如果已经是完整的正确URL,直接返回
|
||||
if (url.match(/^http:\/\/localhost:3000\/uploads\/courses\/videos\/[^/]+$/)) {
|
||||
return url
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
const fileName = getFileNameFromUrl(url)
|
||||
// 构建完整URL
|
||||
return `http://localhost:3000/uploads/courses/videos/${fileName}`
|
||||
}
|
||||
|
||||
// 图片上传相关
|
||||
const handleCoverError = (error) => {
|
||||
console.error('上传图片错误:', error)
|
||||
@ -261,22 +320,78 @@ const handleAdd = () => {
|
||||
const handleEdit = (row) => {
|
||||
formMode.value = 'edit'
|
||||
dialogTitle.value = '编辑课程'
|
||||
|
||||
// 从URL中提取文件名
|
||||
const getFileNameFromUrl = (url) => {
|
||||
if (!url) return ''
|
||||
|
||||
// 移除重复的路径部分
|
||||
const cleanUrl = url.replace(/uploads\/courses\/(images|videos)\/uploads\/courses\/(images|videos)\//, 'uploads/courses/$1/')
|
||||
|
||||
// 如果是完整URL,提取文件名
|
||||
if (cleanUrl.startsWith('http://') || cleanUrl.startsWith('https://')) {
|
||||
// 移除域名和路径前缀,只保留文件名
|
||||
const matches = cleanUrl.match(/\/([^/]+)$/)
|
||||
return matches ? matches[1] : cleanUrl
|
||||
}
|
||||
|
||||
// 如果包含域名但不是以http开头
|
||||
if (cleanUrl.includes('localhost:3000')) {
|
||||
const matches = cleanUrl.match(/([^/]+)$/)
|
||||
return matches ? matches[1] : cleanUrl
|
||||
}
|
||||
|
||||
// 如果已经是文件名,直接返回
|
||||
if (!cleanUrl.includes('/')) {
|
||||
return cleanUrl
|
||||
}
|
||||
|
||||
// 其他情况,尝试获取最后一个斜杠后的内容
|
||||
return cleanUrl.split('/').pop()
|
||||
}
|
||||
|
||||
// 处理图片URL
|
||||
const processImageUrl = (url) => {
|
||||
if (!url) return ''
|
||||
|
||||
// 如果已经是完整的正确URL,直接返回
|
||||
if (url.match(/^http:\/\/localhost:3000\/uploads\/courses\/images\/[^/]+$/)) {
|
||||
return url
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
const fileName = getFileNameFromUrl(url)
|
||||
// 构建完整URL
|
||||
return `http://localhost:3000/uploads/courses/images/${fileName}`
|
||||
}
|
||||
|
||||
// 处理视频URL
|
||||
const processVideoUrl = (url) => {
|
||||
if (!url) return ''
|
||||
|
||||
// 如果已经是完整的正确URL,直接返回
|
||||
if (url.match(/^http:\/\/localhost:3000\/uploads\/courses\/videos\/[^/]+$/)) {
|
||||
return url
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
const fileName = getFileNameFromUrl(url)
|
||||
// 构建完整URL
|
||||
return `http://localhost:3000/uploads/courses/videos/${fileName}`
|
||||
}
|
||||
|
||||
// 设置表单数据
|
||||
const fileName = getFileNameFromUrl(row.cover_image)
|
||||
const videoFileName = getFileNameFromUrl(row.video)
|
||||
|
||||
form.value = {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
category: row.category,
|
||||
cover_image: row.cover_image,
|
||||
cover_image_url: row.cover_image ? (
|
||||
row.cover_image.startsWith('http')
|
||||
? row.cover_image
|
||||
: `http://localhost:3000/uploads/courses/images/${row.cover_image}`
|
||||
) : '',
|
||||
video: row.video || '', // 保存视频文件名
|
||||
video_url: row.video ? (
|
||||
row.video.startsWith('http')
|
||||
? row.video
|
||||
: `http://localhost:3000/uploads/courses/videos/${row.video}`
|
||||
) : '', // 完整的视频URL用于预览
|
||||
cover_image: fileName, // 只保存文件名
|
||||
cover_image_url: `http://localhost:3000/uploads/courses/images/${fileName}`, // 完整URL用于预览
|
||||
video: videoFileName, // 只保存文件名
|
||||
video_url: `http://localhost:3000/uploads/courses/videos/${videoFileName}`, // 完整URL用于预览
|
||||
video_duration: row.video_duration || 0,
|
||||
video_size: row.video_size || 0,
|
||||
description: row.description || '',
|
||||
@ -285,7 +400,7 @@ const handleEdit = (row) => {
|
||||
imageInputType: 'upload',
|
||||
videoInputType: 'upload'
|
||||
}
|
||||
console.log('编辑表单数据:', form.value)
|
||||
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
@ -402,10 +517,27 @@ const handleImageTypeChange = () => {
|
||||
form.value.cover_image = ''
|
||||
}
|
||||
|
||||
// 处理图片错误
|
||||
// 修改图片错误处理函数
|
||||
const handleImageError = (e) => {
|
||||
console.error('图片加载失败:', e)
|
||||
ElMessage.warning('图片加载失败,但不影响保存')
|
||||
const img = e.target
|
||||
if (!img || !form.value.cover_image) return
|
||||
|
||||
// 防止死循环:记录重试次数
|
||||
if (!img.dataset.retryCount) {
|
||||
img.dataset.retryCount = '1'
|
||||
} else if (Number(img.dataset.retryCount) >= 2) {
|
||||
// 如果已经重试两次,就不再重试
|
||||
console.warn('图片加载失败,已达到最大重试次数')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取正确的文件名
|
||||
const fileName = getFileNameFromUrl(form.value.cover_image)
|
||||
// 使用新的URL重试
|
||||
img.src = `http://localhost:3000/uploads/courses/images/${fileName}`
|
||||
// 增加重试计数
|
||||
img.dataset.retryCount = String(Number(img.dataset.retryCount) + 1)
|
||||
}
|
||||
|
||||
// 处理视频上传
|
||||
|
||||
@ -719,6 +719,66 @@ onUnmounted(() => {
|
||||
<div ref="protectionChartRef" class="chart-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-title">实时监测数据</span>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-item">
|
||||
<span class="label">水质指标</span>
|
||||
<span class="value good">优</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">空气质量</span>
|
||||
<span class="value normal">良</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">土壤湿度</span>
|
||||
<span class="value">42%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-title">今日巡护概况</span>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-item">
|
||||
<span class="label">巡护人员</span>
|
||||
<span class="value">8人</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">巡护里程</span>
|
||||
<span class="value">12.5km</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">记录上报</span>
|
||||
<span class="value">26条</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-title">物种活动</span>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-item">
|
||||
<span class="label">活动区域</span>
|
||||
<span class="value">A3、B5区</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">活跃物种</span>
|
||||
<span class="value">15种</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="label">监测频次</span>
|
||||
<span class="value">4次/天</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -899,5 +959,64 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
margin-top: 40px;
|
||||
padding-bottom: 20px;
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.stat-header {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
|
||||
.stat-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
|
||||
&.good {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.normal {
|
||||
color: #e6a23c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
||||
import { getDeviceList } from '@/api/device'
|
||||
import { createFlvPlayer, destroyFlvPlayer } from '@/utils/videoPlayer'
|
||||
import * as echarts from 'echarts'
|
||||
@ -15,8 +15,21 @@ import {
|
||||
const SOUND_CONNECT = new Audio('/src/assets/harmonyos-sound/notification_accomplished_08.wav')
|
||||
const SOUND_DISCONNECT = new Audio('/src/assets/harmonyos-sound/notification_wrong_04.wav')
|
||||
|
||||
// 添加用户交互标记
|
||||
let hasUserInteracted = false
|
||||
|
||||
// 监听用户交互
|
||||
const handleUserInteraction = () => {
|
||||
hasUserInteracted = true
|
||||
// 移除事件监听器
|
||||
document.removeEventListener('click', handleUserInteraction)
|
||||
document.removeEventListener('keydown', handleUserInteraction)
|
||||
}
|
||||
|
||||
// 播放音效
|
||||
const playSound = (status) => {
|
||||
if (!hasUserInteracted) return
|
||||
|
||||
const sound = status === 'online' ? SOUND_CONNECT : SOUND_DISCONNECT
|
||||
sound.currentTime = 0 // 重置音频播放位置
|
||||
sound.play().catch(error => {
|
||||
@ -197,30 +210,22 @@ const initWebSocket = () => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.type === 'device_status') {
|
||||
// 更新设备状态
|
||||
if (drone.value) {
|
||||
const newStatus = data.status === 1 ? 'online' : 'offline'
|
||||
// 只有状态发生变化时才显示通知
|
||||
if (drone.value.status !== newStatus) {
|
||||
drone.value.status = newStatus
|
||||
showDeviceStatusNotification(newStatus)
|
||||
// 根据状态切换显示
|
||||
if (newStatus === 'online') {
|
||||
initVideoPlayer()
|
||||
} else {
|
||||
if (flvPlayer.value) {
|
||||
destroyFlvPlayer(flvPlayer.value)
|
||||
flvPlayer.value = null
|
||||
}
|
||||
getChartData()
|
||||
// 如果 drone.value 不存在,或者设备编码匹配,就更新状态
|
||||
if (!drone.value || (drone.value && drone.value.code === data.device_code)) {
|
||||
// 直接更新设备状态
|
||||
if (drone.value) {
|
||||
const newStatus = data.status === 1 ? 'online' : 'offline'
|
||||
if (drone.value.status !== newStatus) {
|
||||
drone.value.status = newStatus
|
||||
}
|
||||
}
|
||||
// 获取完整的设备信息
|
||||
getDroneInfo()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('WebSocket消息处理错误:', error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
@ -228,7 +233,6 @@ const initWebSocket = () => {
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket连接关闭')
|
||||
// 尝试重新连接
|
||||
setTimeout(initWebSocket, 5000)
|
||||
}
|
||||
@ -245,27 +249,40 @@ const getDroneInfo = async () => {
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
// 找到第一个在线的设备
|
||||
const onlineDrone = res.data.list.find(item => item.status.code === 1)
|
||||
if (onlineDrone) {
|
||||
drone.value = {
|
||||
id: onlineDrone.id,
|
||||
name: onlineDrone.device_name,
|
||||
code: onlineDrone.device_code,
|
||||
status: 'online',
|
||||
battery: onlineDrone.battery_level || 0,
|
||||
signal: onlineDrone.signal_strength || 0
|
||||
const targetDrone = res.data.list.find(item => {
|
||||
return item.device_code === 'BC13292E5A49914F4D62B9F356E39F56'
|
||||
})
|
||||
|
||||
if (targetDrone) {
|
||||
// 设备状态判断逻辑:
|
||||
// 1. 检查WebSocket连接状态
|
||||
const wsConnected = ws?.readyState === WebSocket.OPEN
|
||||
// 2. 检查设备状态 - 优先使用 WebSocket 状态
|
||||
const deviceStatus = targetDrone.status?.code || targetDrone.status
|
||||
const deviceOnline = wsConnected && (deviceStatus === 1 || (drone.value?.status === 'online'))
|
||||
// 3. 设置新状态
|
||||
const newStatus = deviceOnline ? 'online' : 'offline'
|
||||
|
||||
// 检查状态是否发生变化
|
||||
const oldStatus = drone.value?.status
|
||||
if (oldStatus && oldStatus !== newStatus) {
|
||||
showDeviceStatusNotification(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
|
||||
}
|
||||
// 初始化视频播放器
|
||||
nextTick(() => {
|
||||
initVideoPlayer()
|
||||
})
|
||||
} else {
|
||||
drone.value = null
|
||||
// 如果没有在线设备,显示图表
|
||||
getChartData()
|
||||
}
|
||||
} else {
|
||||
console.error('API响应错误:', res.message)
|
||||
ElMessage.error(res.message || '获取无人机列表失败')
|
||||
getChartData()
|
||||
}
|
||||
@ -279,8 +296,7 @@ const getDroneInfo = async () => {
|
||||
}
|
||||
|
||||
// 初始化视频播放器
|
||||
const initVideoPlayer = () => {
|
||||
console.log('初始化视频播放器, 设备信息:', drone.value)
|
||||
const initVideoPlayer = async () => {
|
||||
videoLoading.value = true // 开始加载时显示loading
|
||||
isPlaying.value = false // 重置播放状态
|
||||
|
||||
@ -292,12 +308,10 @@ const initVideoPlayer = () => {
|
||||
|
||||
// 初始化新的播放器
|
||||
if (videoRef.value && drone.value?.status === 'online') {
|
||||
console.log('创建FLV播放器, 设备编码:', drone.value.code)
|
||||
flvPlayer.value = createFlvPlayer(videoRef.value, drone.value.code)
|
||||
|
||||
if (flvPlayer.value) {
|
||||
flvPlayer.value.on('loading', () => {
|
||||
console.log('视频加载中...')
|
||||
if (!isPlaying.value) {
|
||||
videoLoading.value = true
|
||||
}
|
||||
@ -312,7 +326,7 @@ const initVideoPlayer = () => {
|
||||
|
||||
// 监听视频元素的事件
|
||||
videoRef.value.addEventListener('waiting', () => {
|
||||
console.log('视频缓冲中')
|
||||
// 视频缓冲中
|
||||
isBuffering.value = true
|
||||
// 只有在视频还未开始播放时才显示loading
|
||||
if (!isPlaying.value) {
|
||||
@ -321,14 +335,14 @@ const initVideoPlayer = () => {
|
||||
})
|
||||
|
||||
videoRef.value.addEventListener('playing', () => {
|
||||
console.log('视频开始播放')
|
||||
// 视频开始播放
|
||||
videoLoading.value = false
|
||||
isPlaying.value = true
|
||||
isBuffering.value = false
|
||||
})
|
||||
|
||||
videoRef.value.addEventListener('canplay', () => {
|
||||
console.log('视频可以播放')
|
||||
// 视频可以播放
|
||||
isBuffering.value = false
|
||||
if (!isPlaying.value) {
|
||||
videoLoading.value = false
|
||||
@ -361,14 +375,46 @@ const startRefreshTimer = () => {
|
||||
}, 30000) // 每30秒刷新一次
|
||||
}
|
||||
|
||||
// 监听窗口大小变化
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (chart) {
|
||||
chart.resize()
|
||||
}
|
||||
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)
|
||||
|
||||
// 初始化WebSocket连接
|
||||
initWebSocket()
|
||||
// 获取无人机信息
|
||||
@ -380,6 +426,10 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除用户交互监听
|
||||
document.removeEventListener('click', handleUserInteraction)
|
||||
document.removeEventListener('keydown', handleUserInteraction)
|
||||
|
||||
// 关闭WebSocket连接
|
||||
if (ws) {
|
||||
ws.close()
|
||||
@ -402,7 +452,7 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<div class="video-monitor">
|
||||
<div class="monitor-header">
|
||||
<div class="title">{{ drone?.status === 'online' ? '无人机实时监控' : 'pH值变化趋势' }}</div>
|
||||
<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' ? '在线' : '离线' }}
|
||||
@ -411,42 +461,43 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<div class="content-container">
|
||||
<!-- 在线显示视频 -->
|
||||
<div v-if="drone?.status === 'online'" class="video-wrapper">
|
||||
<video
|
||||
ref="videoRef"
|
||||
class="video-player"
|
||||
muted
|
||||
></video>
|
||||
<!-- 修改 loading 遮罩的显示条件 -->
|
||||
<div v-if="videoLoading && !isPlaying && drone?.status === 'online'" 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 }}% -->
|
||||
65%
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- 离线显示图表 -->
|
||||
<div v-else class="chart-wrapper" ref="chartRef"></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- 离线显示图表 -->
|
||||
<div ref="chartRef" class="chart-wrapper"></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -569,8 +620,11 @@ onUnmounted(() => {
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,11 +8,11 @@ import { Plus } from '@element-plus/icons-vue';
|
||||
const tableData = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: "2024年第一季度水质监测分析报告",
|
||||
title: "2025年第一季度水质监测分析报告",
|
||||
type: "environment",
|
||||
timeRange: {
|
||||
start: "2024-01-01",
|
||||
end: "2024-03-31",
|
||||
start: "2025-01-01",
|
||||
end: "2025-02-28",
|
||||
},
|
||||
dataSource: [
|
||||
{
|
||||
@ -21,7 +21,7 @@ const tableData = ref([
|
||||
},
|
||||
],
|
||||
analysis: {
|
||||
summary: "第一季度水质总体保持稳定,但3月份出现轻微波动",
|
||||
summary: "第一季度水质总体保持稳定,但2月份出现轻微波动",
|
||||
trends: [
|
||||
{
|
||||
indicator: "pH值",
|
||||
@ -37,7 +37,7 @@ const tableData = ref([
|
||||
abnormal: [
|
||||
{
|
||||
type: "溶解氧",
|
||||
description: "3月底溶解氧水平略低于标准值",
|
||||
description: "2月底溶解氧水平略低于标准值",
|
||||
level: "轻微",
|
||||
},
|
||||
],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user