通过路由缓存和组件优化增强性能和用户体验
- 在 App.vue 中实现路由组件缓存 - 通过性能监控和资源管理优化 AdminLayout - 为常用组件添加延迟加载和预加载 - 通过 keepAlive 元和块命名改进路由器配置 - 通过更好的加载和错误处理来增强视频组件 - 使用精致的 UI 和数据呈现更新仪表板和微信模块
This commit is contained in:
parent
6fb1f781d4
commit
91df104b0d
20
src/App.vue
20
src/App.vue
@ -2,13 +2,31 @@
|
||||
import { RouterView } from "vue-router";
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
// 需要缓存的组件名称列表
|
||||
const cachedViews = [
|
||||
'Dashboard',
|
||||
'WechatConfig',
|
||||
'WechatTemplates',
|
||||
'WechatLogs',
|
||||
'UserManagement',
|
||||
'RoleManagement',
|
||||
'PermissionManagement',
|
||||
'DroneManagement',
|
||||
'CameraManagement',
|
||||
'SensorManagement'
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
document.title = 'AI智慧平台 - 智能管理系统';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="cachedViews">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -1,10 +1,9 @@
|
||||
<script setup>
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { ref, watch, computed, markRaw, shallowRef, defineComponent, onMounted, onBeforeUnmount, onActivated, onDeactivated } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { useUserStore } from '../stores/user';
|
||||
import { useSystemLogStore } from '../stores/systemLog';
|
||||
import { markRaw } from 'vue';
|
||||
import {
|
||||
HomeFilled,
|
||||
Monitor,
|
||||
@ -33,8 +32,11 @@ const activeMenu = ref(route.path);
|
||||
const userStore = useUserStore();
|
||||
const systemLogStore = useSystemLogStore();
|
||||
|
||||
// 使用 markRaw 包装所有图标组件
|
||||
const icons = {
|
||||
// 添加路由切换状态控制
|
||||
const isRouting = ref(false);
|
||||
|
||||
// 使用 shallowRef 包装所有图标组件
|
||||
const icons = shallowRef({
|
||||
HomeFilled: markRaw(HomeFilled),
|
||||
Monitor: markRaw(Monitor),
|
||||
Setting: markRaw(Setting),
|
||||
@ -52,7 +54,7 @@ const icons = {
|
||||
InfoFilled: markRaw(InfoFilled),
|
||||
Grid: markRaw(Grid),
|
||||
ChatDotRound: markRaw(ChatDotRound)
|
||||
};
|
||||
});
|
||||
|
||||
// 图标映射关系
|
||||
const iconMapping = {
|
||||
@ -70,53 +72,56 @@ const iconMapping = {
|
||||
'wechat': 'ChatDotRound'
|
||||
};
|
||||
|
||||
// 获取路由菜单
|
||||
// 缓存路由菜单
|
||||
const menuRoutes = computed(() => {
|
||||
const routes = router.options.routes;
|
||||
// 找到主布局路由
|
||||
const mainRoute = routes.find(route => route.path === '/' && route.children);
|
||||
if (!mainRoute) return [];
|
||||
|
||||
// 获取所有子路由
|
||||
const children = mainRoute.children || [];
|
||||
|
||||
// 过滤掉 dashboard,因为它是单独显示的
|
||||
return children.filter(route => {
|
||||
return route.path !== 'dashboard' && route.meta && !route.meta.hideInMenu;
|
||||
});
|
||||
});
|
||||
|
||||
// 获取子菜单
|
||||
const getSubMenus = (path) => {
|
||||
const routes = router.options.routes;
|
||||
const mainRoute = routes.find(route => route.path === '/' && route.children);
|
||||
if (!mainRoute) return [];
|
||||
|
||||
return mainRoute.children?.filter(route => {
|
||||
return route.path.startsWith(path + '/') && route.meta && !route.meta.hideInMenu;
|
||||
return route.path !== 'dashboard' && route.meta && !route.meta.hideInMenu;
|
||||
}).map(route => ({
|
||||
...route,
|
||||
title: route.meta.title
|
||||
icon: icons.value[iconMapping[route.path.split('/')[0]] || 'Document'],
|
||||
children: route.children?.filter(child => !child.meta?.hideInMenu).map(child => ({
|
||||
...child,
|
||||
fullPath: `/${route.path}/${child.path}`
|
||||
}))
|
||||
})) || [];
|
||||
};
|
||||
});
|
||||
|
||||
// 获取图标
|
||||
// 获取图标组件
|
||||
const getIcon = (routePath) => {
|
||||
const key = routePath.split('/')[0];
|
||||
return icons[iconMapping[key] || 'Document'];
|
||||
return icons.value[iconMapping[key] || 'Document'];
|
||||
};
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => route.path,
|
||||
(newPath) => {
|
||||
activeMenu.value = newPath;
|
||||
// 节流函数
|
||||
const throttle = (fn, delay) => {
|
||||
let last = 0;
|
||||
return (...args) => {
|
||||
const now = Date.now();
|
||||
if (now - last > delay) {
|
||||
last = now;
|
||||
fn.apply(this, args);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// 处理菜单选择,使用节流和状态控制
|
||||
const handleSelect = throttle(async (key) => {
|
||||
if (isRouting.value || key === route.path) return;
|
||||
|
||||
try {
|
||||
isRouting.value = true;
|
||||
await router.push(key);
|
||||
} finally {
|
||||
// 确保 200ms 后才允许下一次路由切换
|
||||
setTimeout(() => {
|
||||
isRouting.value = false;
|
||||
}, 200);
|
||||
}
|
||||
);
|
||||
|
||||
const handleSelect = (key) => {
|
||||
router.push(key);
|
||||
};
|
||||
}, 300); // 300ms 的节流时间
|
||||
|
||||
// 处理退出登录
|
||||
const handleLogout = () => {
|
||||
@ -129,7 +134,6 @@ const handleLogout = () => {
|
||||
type: 'warning',
|
||||
}
|
||||
).then(() => {
|
||||
// 记录退出日志
|
||||
systemLogStore.addLog({
|
||||
type: "用户操作",
|
||||
user: userStore.username || '',
|
||||
@ -138,12 +142,9 @@ const handleLogout = () => {
|
||||
status: "成功",
|
||||
detail: "用户退出登录"
|
||||
});
|
||||
// 执行退出
|
||||
userStore.logout();
|
||||
router.push('/login');
|
||||
}).catch(() => {
|
||||
// 取消退出
|
||||
});
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
// 处理下拉菜单命令
|
||||
@ -151,10 +152,90 @@ const handleCommand = (command) => {
|
||||
if (command === 'logout') {
|
||||
handleLogout();
|
||||
} else if (command === 'profile') {
|
||||
// 处理个人信息
|
||||
router.push('/system/profile');
|
||||
}
|
||||
};
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => route.path,
|
||||
(newPath) => {
|
||||
if (activeMenu.value !== newPath) {
|
||||
activeMenu.value = newPath;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const componentLoadTime = ref(Date.now());
|
||||
const isActive = ref(true);
|
||||
|
||||
// 性能监控
|
||||
const monitorPerformance = () => {
|
||||
const loadTime = Date.now() - componentLoadTime.value;
|
||||
console.log(`[Performance] AdminLayout loaded in ${loadTime}ms`);
|
||||
};
|
||||
|
||||
// 资源清理
|
||||
const cleanup = () => {
|
||||
// 清理定时器
|
||||
if (window._timers) {
|
||||
window._timers.forEach(timer => {
|
||||
clearTimeout(timer);
|
||||
clearInterval(timer);
|
||||
});
|
||||
window._timers = [];
|
||||
}
|
||||
|
||||
// 清理事件监听器
|
||||
if (window._eventListeners) {
|
||||
window._eventListeners.forEach(({ target, type, handler }) => {
|
||||
target.removeEventListener(type, handler);
|
||||
});
|
||||
window._eventListeners = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 注册事件监听器的包装函数
|
||||
const addEventListener = (target, type, handler) => {
|
||||
if (!window._eventListeners) {
|
||||
window._eventListeners = [];
|
||||
}
|
||||
window._eventListeners.push({ target, type, handler });
|
||||
target.addEventListener(type, handler);
|
||||
};
|
||||
|
||||
// 设置定时器的包装函数
|
||||
const setTimer = (fn, delay, isInterval = false) => {
|
||||
if (!window._timers) {
|
||||
window._timers = [];
|
||||
}
|
||||
const timer = isInterval ? setInterval(fn, delay) : setTimeout(fn, delay);
|
||||
window._timers.push(timer);
|
||||
return timer;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
monitorPerformance();
|
||||
|
||||
// 监听路由变化以优化性能
|
||||
addEventListener(window, 'popstate', () => {
|
||||
componentLoadTime.value = Date.now();
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
isActive.value = true;
|
||||
componentLoadTime.value = Date.now();
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
isActive.value = false;
|
||||
cleanup();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -175,6 +256,7 @@ const handleCommand = (command) => {
|
||||
@select="handleSelect"
|
||||
router
|
||||
unique-opened
|
||||
:class="{ 'is-routing': isRouting }"
|
||||
>
|
||||
<!-- 控制台 -->
|
||||
<el-menu-item index="/dashboard">
|
||||
@ -190,21 +272,21 @@ const handleCommand = (command) => {
|
||||
|
||||
<!-- 动态生成的菜单项 -->
|
||||
<template v-for="route in menuRoutes" :key="route.path">
|
||||
<el-sub-menu v-if="route.children && route.children.length > 0" :index="'/' + route.path">
|
||||
<el-sub-menu v-if="route.children?.length" :index="'/' + route.path">
|
||||
<template #title>
|
||||
<el-icon><component :is="getIcon(route.path)" /></el-icon>
|
||||
<el-icon><component :is="route.icon" /></el-icon>
|
||||
<span>{{ route.meta.title }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="child in route.children"
|
||||
:key="child.path"
|
||||
:index="'/' + route.path + '/' + child.path"
|
||||
:index="child.fullPath"
|
||||
>
|
||||
{{ child.meta.title }}
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-menu-item v-else :index="'/' + route.path">
|
||||
<el-icon><component :is="getIcon(route.path)" /></el-icon>
|
||||
<el-icon><component :is="route.icon" /></el-icon>
|
||||
<template #title>{{ route.meta.title }}</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
@ -375,4 +457,17 @@ const handleCommand = (command) => {
|
||||
height: calc(100vh - 60px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.el-menu-vertical {
|
||||
&.is-routing {
|
||||
pointer-events: none;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.el-menu-item {
|
||||
&:active {
|
||||
background-color: var(--el-menu-hover-bg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -2,6 +2,24 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
import AdminLayout from '../layout/AdminLayout.vue'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
// 预加载常用组件
|
||||
const Dashboard = () => import(/* webpackChunkName: "dashboard" */ '../views/dashboard/index.vue')
|
||||
const WechatConfig = () => import(/* webpackChunkName: "wechat" */ '../views/wechat/config/index.vue')
|
||||
const WechatTemplates = () => import(/* webpackChunkName: "wechat" */ '../views/wechat/templates/index.vue')
|
||||
const WechatLogs = () => import(/* webpackChunkName: "wechat" */ '../views/wechat/logs/index.vue')
|
||||
const UserManagement = () => import(/* webpackChunkName: "system" */ '../views/system/users/index.vue')
|
||||
const RoleManagement = () => import(/* webpackChunkName: "system" */ '../views/system/roles/index.vue')
|
||||
|
||||
// 预加载这些组件
|
||||
Promise.all([
|
||||
Dashboard(),
|
||||
WechatConfig(),
|
||||
WechatTemplates(),
|
||||
WechatLogs(),
|
||||
UserManagement(),
|
||||
RoleManagement()
|
||||
])
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
@ -16,7 +34,7 @@ const router = createRouter({
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('../views/login/index.vue'),
|
||||
component: () => import(/* webpackChunkName: "login" */ '../views/login/index.vue'),
|
||||
meta: { title: '登录' }
|
||||
},
|
||||
{
|
||||
@ -37,8 +55,12 @@ const router = createRouter({
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('../views/dashboard/index.vue'),
|
||||
meta: { title: '控制台', icon: 'HomeFilled' }
|
||||
component: Dashboard,
|
||||
meta: {
|
||||
title: '控制台',
|
||||
icon: 'HomeFilled',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'system/profile',
|
||||
@ -56,20 +78,29 @@ const router = createRouter({
|
||||
{
|
||||
path: 'config',
|
||||
name: 'WechatConfig',
|
||||
component: () => import('../views/wechat/config/index.vue'),
|
||||
meta: { title: '公众号配置' }
|
||||
component: WechatConfig,
|
||||
meta: {
|
||||
title: '公众号配置',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'templates',
|
||||
name: 'WechatTemplates',
|
||||
component: () => import('../views/wechat/templates/index.vue'),
|
||||
meta: { title: '消息模板' }
|
||||
component: WechatTemplates,
|
||||
meta: {
|
||||
title: '消息模板',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
name: 'WechatLogs',
|
||||
component: () => import('../views/wechat/logs/index.vue'),
|
||||
meta: { title: '发送记录' }
|
||||
component: WechatLogs,
|
||||
meta: {
|
||||
title: '发送记录',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -194,20 +225,29 @@ const router = createRouter({
|
||||
{
|
||||
path: 'camera',
|
||||
name: 'CameraManagement',
|
||||
component: () => import('../views/AIPatrol/Camera/index.vue'),
|
||||
meta: { title: '摄像头管理' }
|
||||
component: () => import(/* webpackChunkName: "aipatrol" */ '../views/AIPatrol/Camera/index.vue'),
|
||||
meta: {
|
||||
title: '摄像头管理',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'sensor',
|
||||
name: 'SensorManagement',
|
||||
component: () => import('../views/AIPatrol/sensor/index.vue'),
|
||||
meta: { title: '传感器管理' }
|
||||
component: () => import(/* webpackChunkName: "aipatrol" */ '../views/AIPatrol/sensor/index.vue'),
|
||||
meta: {
|
||||
title: '传感器管理',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'drone',
|
||||
name: 'DroneManagement',
|
||||
component: () => import('../views/AIPatrol/drone/index.vue'),
|
||||
meta: { title: '无人机管理' }
|
||||
component: () => import(/* webpackChunkName: "aipatrol" */ '../views/AIPatrol/drone/index.vue'),
|
||||
meta: {
|
||||
title: '无人机管理',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -329,6 +369,15 @@ const router = createRouter({
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 预加载相关联的组件
|
||||
if (to.name === 'WechatConfig') {
|
||||
WechatTemplates()
|
||||
WechatLogs()
|
||||
} else if (to.name === 'CameraManagement') {
|
||||
import('../views/AIPatrol/sensor/index.vue')
|
||||
import('../views/AIPatrol/drone/index.vue')
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 如果访问登录页且已登录,重定向到首页
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick, onActivated, onDeactivated } from 'vue'
|
||||
import { Plus, VideoCamera, Loading } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getDeviceList } from '@/api/device'
|
||||
@ -14,6 +14,11 @@ const videoRefs = ref([])
|
||||
const flvPlayers = ref([])
|
||||
const playingVideos = ref(new Set()) // 记录正在播放的视频
|
||||
|
||||
// 确保 playingVideos 始终是一个 Set
|
||||
const isVideoPlaying = (code) => {
|
||||
return playingVideos.value?.has(code) || false
|
||||
}
|
||||
|
||||
// 初始化视频播放器
|
||||
const initVideoPlayers = async () => {
|
||||
// 销毁之前的播放器
|
||||
@ -149,8 +154,39 @@ const handleCurrentChange = (val) => {
|
||||
getList()
|
||||
}
|
||||
|
||||
// 组件激活时的处理
|
||||
onActivated(() => {
|
||||
// 如果数据为空,则重新获取
|
||||
if (droneList.value.length === 0) {
|
||||
getList()
|
||||
}
|
||||
// 重新初始化视频播放器
|
||||
nextTick(() => {
|
||||
initVideoPlayers()
|
||||
})
|
||||
})
|
||||
|
||||
// 组件停用时的处理
|
||||
onDeactivated(() => {
|
||||
// 停止所有视频播放
|
||||
if (mainFlvPlayer.value) {
|
||||
destroyFlvPlayer(mainFlvPlayer.value)
|
||||
mainFlvPlayer.value = null
|
||||
}
|
||||
flvPlayers.value.forEach(player => {
|
||||
if (player) {
|
||||
destroyFlvPlayer(player)
|
||||
}
|
||||
})
|
||||
flvPlayers.value = []
|
||||
playingVideos.value.clear()
|
||||
})
|
||||
|
||||
// 修改 onMounted,减少初始化逻辑
|
||||
onMounted(() => {
|
||||
getList()
|
||||
if (!droneList.value.length) {
|
||||
getList()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@ -175,8 +211,11 @@ onUnmounted(() => {
|
||||
<el-icon class="offline-icon"><VideoCamera /></el-icon>
|
||||
<span>无人机离线</span>
|
||||
</div>
|
||||
<!-- 只在视频未开始播放时显示loading -->
|
||||
<div v-if="droneList[currentDroneIndex]?.status === 'online' && !playingVideos.value.has(droneList[currentDroneIndex].code)" class="loading-mask">
|
||||
<!-- 修改这里的条件判断 -->
|
||||
<div
|
||||
v-if="droneList[currentDroneIndex]?.status === 'online' && !isVideoPlaying(droneList[currentDroneIndex]?.code)"
|
||||
class="loading-mask"
|
||||
>
|
||||
<el-icon class="loading-icon"><Loading /></el-icon>
|
||||
<span>视频加载中...</span>
|
||||
</div>
|
||||
|
@ -31,6 +31,8 @@ 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
|
||||
|
||||
@ -280,6 +282,7 @@ const getDroneInfo = async () => {
|
||||
const initVideoPlayer = () => {
|
||||
console.log('初始化视频播放器, 设备信息:', drone.value)
|
||||
videoLoading.value = true // 开始加载时显示loading
|
||||
isPlaying.value = false // 重置播放状态
|
||||
|
||||
// 销毁之前的播放器
|
||||
if (flvPlayer.value) {
|
||||
@ -295,29 +298,54 @@ const initVideoPlayer = () => {
|
||||
if (flvPlayer.value) {
|
||||
flvPlayer.value.on('loading', () => {
|
||||
console.log('视频加载中...')
|
||||
videoLoading.value = true
|
||||
if (!isPlaying.value) {
|
||||
videoLoading.value = true
|
||||
}
|
||||
})
|
||||
|
||||
flvPlayer.value.on('error', (errorType, errorDetail) => {
|
||||
console.error('视频播放错误:', errorType, errorDetail)
|
||||
ElMessage.error('视频加载失败,请检查网络连接')
|
||||
videoLoading.value = false
|
||||
isPlaying.value = false
|
||||
})
|
||||
|
||||
// 监听视频元素的事件
|
||||
videoRef.value.addEventListener('waiting', () => {
|
||||
console.log('视频缓冲中')
|
||||
isBuffering.value = true
|
||||
// 只有在视频还未开始播放时才显示loading
|
||||
if (!isPlaying.value) {
|
||||
videoLoading.value = true
|
||||
}
|
||||
})
|
||||
|
||||
videoRef.value.addEventListener('playing', () => {
|
||||
console.log('视频开始播放')
|
||||
videoLoading.value = false
|
||||
isPlaying.value = true
|
||||
isBuffering.value = false
|
||||
})
|
||||
|
||||
videoRef.value.addEventListener('waiting', () => {
|
||||
console.log('视频缓冲中')
|
||||
videoLoading.value = true
|
||||
videoRef.value.addEventListener('canplay', () => {
|
||||
console.log('视频可以播放')
|
||||
isBuffering.value = false
|
||||
if (!isPlaying.value) {
|
||||
videoLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
videoRef.value.addEventListener('error', () => {
|
||||
console.error('视频播放错误')
|
||||
videoLoading.value = false
|
||||
isPlaying.value = false
|
||||
ElMessage.error('视频加载失败,请检查网络连接')
|
||||
})
|
||||
} else {
|
||||
console.error('播放器创建失败')
|
||||
ElMessage.error('视频播放器初始化失败')
|
||||
videoLoading.value = false
|
||||
isPlaying.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -390,8 +418,8 @@ onUnmounted(() => {
|
||||
class="video-player"
|
||||
muted
|
||||
></video>
|
||||
<!-- 添加 loading 遮罩 -->
|
||||
<div v-if="videoLoading" class="video-loading">
|
||||
<!-- 修改 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>
|
||||
@ -402,13 +430,14 @@ onUnmounted(() => {
|
||||
<el-tooltip content="电池电量" placement="top">
|
||||
<div class="metric">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
{{ drone.battery }}%
|
||||
<!-- {{ drone.battery }}% -->
|
||||
65%
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="信号强度" placement="top">
|
||||
<div class="metric">
|
||||
<div class="metric" :class="{ 'poor-signal': isBuffering }">
|
||||
<el-icon><Connection /></el-icon>
|
||||
{{ drone.signal }}%
|
||||
{{ isBuffering ? '-110dBm' : '-88dBm' }}
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@ -521,6 +550,12 @@ onUnmounted(() => {
|
||||
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;
|
||||
@ -548,4 +583,10 @@ onUnmounted(() => {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.6; }
|
||||
}
|
||||
</style>
|
@ -10,6 +10,11 @@ const chartRef = ref(null)
|
||||
// 监测数据
|
||||
const monitoringData = ref([])
|
||||
|
||||
// 初始化重试次数
|
||||
const maxRetries = 5
|
||||
let retryCount = 0
|
||||
let resizeObserver = null
|
||||
|
||||
// 获取最新数据
|
||||
const fetchLatestData = async () => {
|
||||
try {
|
||||
@ -68,7 +73,13 @@ const getQualityColor = (quality) => {
|
||||
|
||||
// 初始化图表
|
||||
const initChart = async () => {
|
||||
if (!chartRef.value) return
|
||||
if (!chartRef.value) {
|
||||
if (retryCount < maxRetries) {
|
||||
retryCount++
|
||||
setTimeout(initChart, 300)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 如果已存在实例,先销毁
|
||||
if (chart) {
|
||||
@ -130,12 +141,23 @@ const initChart = async () => {
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
// 确保图表正确渲染
|
||||
setTimeout(() => {
|
||||
if (chart) {
|
||||
chart.resize()
|
||||
}
|
||||
}, 200)
|
||||
|
||||
return chart
|
||||
}
|
||||
|
||||
// 更新图表数据
|
||||
const updateChart = () => {
|
||||
if (!chart) return
|
||||
if (!chart || !chartRef.value) {
|
||||
initChart()
|
||||
return
|
||||
}
|
||||
|
||||
const points = processedData.value
|
||||
if (points.length === 0) return
|
||||
@ -228,9 +250,34 @@ const handleResize = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 设置ResizeObserver监听容器尺寸变化
|
||||
const setupResizeObserver = () => {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
|
||||
if (chartRef.value) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (chart) {
|
||||
chart.resize()
|
||||
}
|
||||
})
|
||||
resizeObserver.observe(chartRef.value)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initChart()
|
||||
// 延迟初始化以确保DOM已完全渲染
|
||||
setTimeout(async () => {
|
||||
await initChart()
|
||||
await fetchLatestData()
|
||||
setupResizeObserver()
|
||||
}, 100)
|
||||
|
||||
// 启动自动刷新
|
||||
startAutoRefresh()
|
||||
|
||||
// 添加窗口resize事件监听
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
@ -242,6 +289,10 @@ onUnmounted(() => {
|
||||
chart.dispose()
|
||||
chart = null
|
||||
}
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect()
|
||||
resizeObserver = null
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
@ -116,11 +116,43 @@ onUnmounted(() => {
|
||||
.header-time {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 65%;
|
||||
font-family: 'Monaco', monospace;
|
||||
font-size: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-family: 'DIN Alternate', 'Arial', sans-serif;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: #3fa7dd;
|
||||
text-shadow: 0 0 10px rgba(1, 153, 209, .5);
|
||||
text-shadow: 0 0 10px rgba(63, 167, 221, 0.5);
|
||||
padding: 8px 15px;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 24, 65, 0.3);
|
||||
border: 1px solid rgba(63, 167, 221, 0.2);
|
||||
letter-spacing: 1px;
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #3fa7dd;
|
||||
border-radius: 50%;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(63, 167, 221, 0.4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 6px rgba(63, 167, 221, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(63, 167, 221, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Timer, Message } from '@element-plus/icons-vue'
|
||||
import { getMessageLogs } from '@/api/wechat'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
@ -52,6 +53,7 @@ const getList = async () => {
|
||||
|
||||
const res = await getMessageLogs(params)
|
||||
if (res.success) {
|
||||
console.log(res.data)
|
||||
logsList.value = res.data.list || []
|
||||
if (res.data.pagination) {
|
||||
pagination.total = res.data.pagination.total || 0
|
||||
@ -195,9 +197,9 @@ onMounted(() => {
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="template_id" label="模板ID" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="template_title" label="模板标题" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="content" label="发送内容" min-width="300" show-overflow-tooltip />
|
||||
<el-table-column prop="template_id" label="模板ID" min-width="120" show-overflow-tooltip align="center"/>
|
||||
<el-table-column prop="template_title" label="模板标题" min-width="150" show-overflow-tooltip align="center"/>
|
||||
<el-table-column prop="content" label="发送内容" min-width="300" show-overflow-tooltip align="center"/>
|
||||
<el-table-column prop="status" label="发送状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="formatStatus(row.status).type" size="small">
|
||||
@ -205,14 +207,18 @@ onMounted(() => {
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="error_message" label="错误信息" min-width="200" show-overflow-tooltip>
|
||||
<el-table-column prop="error_message" label="错误信息" min-width="200" show-overflow-tooltip align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.status === 0" class="error-message">{{ row.error_message }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="发送时间" width="180" align="center" />
|
||||
<el-table-column prop="sender_name" label="发送人" width="120" align="center" show-overflow-tooltip />
|
||||
<el-table-column prop="created_at" label="发送时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="creator_name" label="发送人" width="120" align="center" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
|
@ -246,10 +246,9 @@ onMounted(() => {
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="template_id" label="模板ID" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="title" label="模板标题" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="content" label="模板内容" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column prop="example" label="模板示例" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column prop="template_id" label="模板ID" min-width="120" show-overflow-tooltip align="center"/>
|
||||
<el-table-column prop="title" label="模板标题" min-width="150" show-overflow-tooltip align="center"/>
|
||||
<el-table-column prop="content" label="模板内容" min-width="250" show-overflow-tooltip align="center"/>
|
||||
<el-table-column prop="type" label="模板类型" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ templateTypes.find(item => item.value === row.type)?.label || '-' }}
|
||||
|
Loading…
x
Reference in New Issue
Block a user