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