- 在 App.vue 中实现路由组件缓存 - 通过性能监控和资源管理优化 AdminLayout - 为常用组件添加延迟加载和预加载 - 通过 keepAlive 元和块命名改进路由器配置 - 通过更好的加载和错误处理来增强视频组件 - 使用精致的 UI 和数据呈现更新仪表板和微信模块
474 lines
11 KiB
Vue
474 lines
11 KiB
Vue
<script setup>
|
|
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 {
|
|
HomeFilled,
|
|
Monitor,
|
|
Setting,
|
|
User,
|
|
DataAnalysis,
|
|
DataBoard,
|
|
Tools,
|
|
Document,
|
|
DataLine,
|
|
Collection,
|
|
Histogram,
|
|
Location,
|
|
VideoCamera,
|
|
ChatLineRound,
|
|
InfoFilled,
|
|
Grid,
|
|
ChatDotRound
|
|
} from "@element-plus/icons-vue";
|
|
import logo from '../assets/images/logo.png';
|
|
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
const isCollapse = ref(false);
|
|
const activeMenu = ref(route.path);
|
|
const userStore = useUserStore();
|
|
const systemLogStore = useSystemLogStore();
|
|
|
|
// 添加路由切换状态控制
|
|
const isRouting = ref(false);
|
|
|
|
// 使用 shallowRef 包装所有图标组件
|
|
const icons = shallowRef({
|
|
HomeFilled: markRaw(HomeFilled),
|
|
Monitor: markRaw(Monitor),
|
|
Setting: markRaw(Setting),
|
|
User: markRaw(User),
|
|
DataAnalysis: markRaw(DataAnalysis),
|
|
DataBoard: markRaw(DataBoard),
|
|
Tools: markRaw(Tools),
|
|
Document: markRaw(Document),
|
|
DataLine: markRaw(DataLine),
|
|
Collection: markRaw(Collection),
|
|
Histogram: markRaw(Histogram),
|
|
Location: markRaw(Location),
|
|
VideoCamera: markRaw(VideoCamera),
|
|
ChatLineRound: markRaw(ChatLineRound),
|
|
InfoFilled: markRaw(InfoFilled),
|
|
Grid: markRaw(Grid),
|
|
ChatDotRound: markRaw(ChatDotRound)
|
|
});
|
|
|
|
// 图标映射关系
|
|
const iconMapping = {
|
|
'dashboard': 'HomeFilled',
|
|
'screen': 'Grid',
|
|
'system': 'Setting',
|
|
'monitor': 'DataAnalysis',
|
|
'patrol': 'Location',
|
|
'AIPatrol': 'VideoCamera',
|
|
'report': 'Document',
|
|
'activity': 'Collection',
|
|
'course': 'DataLine',
|
|
'feedback': 'ChatLineRound',
|
|
'about': 'InfoFilled',
|
|
'wechat': 'ChatDotRound'
|
|
};
|
|
|
|
// 缓存路由菜单
|
|
const menuRoutes = computed(() => {
|
|
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 !== 'dashboard' && route.meta && !route.meta.hideInMenu;
|
|
}).map(route => ({
|
|
...route,
|
|
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.value[iconMapping[key] || 'Document'];
|
|
};
|
|
|
|
// 节流函数
|
|
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);
|
|
}
|
|
}, 300); // 300ms 的节流时间
|
|
|
|
// 处理退出登录
|
|
const handleLogout = () => {
|
|
ElMessageBox.confirm(
|
|
'确认退出登录?',
|
|
'提示',
|
|
{
|
|
confirmButtonText: '退出',
|
|
cancelButtonText: '取消',
|
|
type: 'warning',
|
|
}
|
|
).then(() => {
|
|
systemLogStore.addLog({
|
|
type: "用户操作",
|
|
user: userStore.username || '',
|
|
action: "退出系统",
|
|
ip: "192.168.1.100",
|
|
status: "成功",
|
|
detail: "用户退出登录"
|
|
});
|
|
userStore.logout();
|
|
router.push('/login');
|
|
}).catch(() => {});
|
|
};
|
|
|
|
// 处理下拉菜单命令
|
|
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>
|
|
<div class="admin-layout">
|
|
<el-container class="layout-container">
|
|
<el-aside :width="isCollapse ? '64px' : '200px'">
|
|
<div class="logo-container">
|
|
<img :src="logo" class="logo-image" alt="logo">
|
|
<h1 v-if="!isCollapse" class="logo-title">
|
|
<span class="ai-text">AI</span>
|
|
<span class="platform-text">智慧平台</span>
|
|
</h1>
|
|
</div>
|
|
<el-menu
|
|
:default-active="activeMenu"
|
|
class="el-menu-vertical"
|
|
:collapse="isCollapse"
|
|
@select="handleSelect"
|
|
router
|
|
unique-opened
|
|
:class="{ 'is-routing': isRouting }"
|
|
>
|
|
<!-- 控制台 -->
|
|
<el-menu-item index="/dashboard">
|
|
<el-icon><component :is="icons.HomeFilled" /></el-icon>
|
|
<template #title>控制台</template>
|
|
</el-menu-item>
|
|
|
|
<!-- 数据大屏 -->
|
|
<el-menu-item index="/screen">
|
|
<el-icon><component :is="icons.Grid" /></el-icon>
|
|
<template #title>数据大屏</template>
|
|
</el-menu-item>
|
|
|
|
<!-- 动态生成的菜单项 -->
|
|
<template v-for="route in menuRoutes" :key="route.path">
|
|
<el-sub-menu v-if="route.children?.length" :index="'/' + route.path">
|
|
<template #title>
|
|
<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="child.fullPath"
|
|
>
|
|
{{ child.meta.title }}
|
|
</el-menu-item>
|
|
</el-sub-menu>
|
|
<el-menu-item v-else :index="'/' + route.path">
|
|
<el-icon><component :is="route.icon" /></el-icon>
|
|
<template #title>{{ route.meta.title }}</template>
|
|
</el-menu-item>
|
|
</template>
|
|
</el-menu>
|
|
</el-aside>
|
|
|
|
<el-container>
|
|
<el-header>
|
|
<div class="header-left">
|
|
<el-button link @click="isCollapse = !isCollapse">
|
|
<el-icon><component :is="icons.HomeFilled" /></el-icon>
|
|
</el-button>
|
|
</div>
|
|
<div class="header-right">
|
|
<el-dropdown @command="handleCommand">
|
|
<span class="user-info">
|
|
{{ userStore.username || '管理员' }}
|
|
<el-icon><component :is="icons.User" /></el-icon>
|
|
</span>
|
|
<template #dropdown>
|
|
<el-dropdown-menu>
|
|
<el-dropdown-item command="profile">个人信息</el-dropdown-item>
|
|
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
</el-dropdown>
|
|
</div>
|
|
</el-header>
|
|
<el-main>
|
|
<router-view></router-view>
|
|
</el-main>
|
|
</el-container>
|
|
</el-container>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
@use "../styles/variables" as v;
|
|
|
|
.admin-layout {
|
|
height: 100vh;
|
|
.layout-container {
|
|
height: 100%;
|
|
}
|
|
}
|
|
|
|
.el-aside {
|
|
background-color: v.$sidebar-bg;
|
|
border-right: 1px solid v.$border-color;
|
|
transition: width 0.3s;
|
|
|
|
.logo-container {
|
|
height: 60px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0 20px;
|
|
border-bottom: 1px solid v.$border-color;
|
|
background-color: v.$sidebar-bg;
|
|
overflow: hidden;
|
|
gap: 10px;
|
|
|
|
.logo-image {
|
|
width: 32px;
|
|
height: 32px;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.logo-title {
|
|
cursor: pointer;
|
|
margin: 0;
|
|
font-size: 18px;
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
|
|
.ai-text {
|
|
color: #409EFF;
|
|
font-weight: 800;
|
|
font-style: italic;
|
|
text-shadow: 2px 2px 4px rgba(64, 158, 255, 0.2);
|
|
background: linear-gradient(120deg, #409EFF, #67C23A);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
padding: 0 2px;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.platform-text {
|
|
color: v.$text-primary;
|
|
font-weight: 500;
|
|
letter-spacing: 2px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.el-menu {
|
|
border-right: none;
|
|
background-color: v.$sidebar-bg;
|
|
|
|
&.el-menu-vertical {
|
|
.el-menu-item,
|
|
.el-sub-menu__title {
|
|
color: v.$text-regular;
|
|
|
|
&:hover {
|
|
color: v.$primary-color;
|
|
background-color: #ecf5ff;
|
|
}
|
|
|
|
&.is-active {
|
|
color: v.$primary-color;
|
|
background-color: #ecf5ff;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.el-header {
|
|
background-color: v.$header-bg;
|
|
box-shadow: v.$box-shadow;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
height: 60px;
|
|
padding: 0 20px;
|
|
border-bottom: 1px solid v.$border-color;
|
|
}
|
|
|
|
.header-left {
|
|
.el-button {
|
|
padding: 8px;
|
|
|
|
.el-icon {
|
|
font-size: 20px;
|
|
color: v.$text-regular;
|
|
}
|
|
}
|
|
}
|
|
|
|
.header-right {
|
|
.user-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
color: v.$text-regular;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
transition: all 0.3s;
|
|
|
|
&:hover {
|
|
background-color: #f5f7fa;
|
|
}
|
|
|
|
.el-icon {
|
|
font-size: 16px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.el-main {
|
|
background-color: v.$bg-color;
|
|
padding: 20px;
|
|
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>
|