以下是具体实现步骤:
1. 创建路由工具文件
首先,需要创建一个路由工具文件 src/renderer/src/router/utils.ts
,用于将菜单列表转换为路由配置。请复制以下代码并保存到该文件中:
import { RouteRecordRaw } from 'vue-router'
/**
* 递归将菜单列表转换为路由配置
* @param menuList 菜单列表
* @returns 路由配置数组
*/
export function generateRoutes(menuList: any[]): RouteRecordRaw[] {
const routes: RouteRecordRaw[] = []
menuList.forEach(menu => {
const route: RouteRecordRaw = {
path: menu.path || '',
name: menu.name || '',
meta: {
title: menu.title || '',
icon: menu.icon || '',
requiresAuth: menu.requiresAuth !== false
}
}
// 处理组件路径
if (menu.component) {
// 假设组件路径格式为 '@renderer/view/xxx/index.vue'
route.component = () => import(`@renderer/view/${menu.component}/index.vue`)
}
// 处理子菜单
if (menu.children && menu.children.length > 0) {
route.children = generateRoutes(menu.children)
}
routes.push(route)
})
return routes
}
/**
* 将生成的路由添加到路由实例
* @param router 路由实例
* @param routes 生成的路由配置
*/
export function addRoutesToRouter(router: any, routes: RouteRecordRaw[]) {
routes.forEach(route => {
router.addRoute(route)
})
}
2. 修改 home/index.vue 文件
接下来,需要修改 src/renderer/src/view/home/index.vue
文件,在获取菜单列表后动态生成路由:
// ... existing code ...
import { useRouter } from 'vue-router'
import { generateRoutes, addRoutesToRouter } from '@renderer/router/utils'
const router = useRouter()
const userStore = useUserStore()
const getRouters = async () => {
const res = await getRoutersApi(userStore.roles[0].rolePerm)
console.log('菜单列表:', res)
// 假设res.data包含菜单列表
if (res.data && res.data.length > 0) {
// 生成路由
const dynamicRoutes = generateRoutes(res.data)
// 添加到路由实例
addRoutesToRouter(router, dynamicRoutes)
console.log('动态路由生成成功:', dynamicRoutes)
}
}
// ... existing code ...
3. 优化路由守卫
为了确保动态路由加载完成后再进行路由跳转,可以修改 src/renderer/src/router/index.ts
中的路由守卫:
// ... existing code ...
import { ref } from 'vue'
// 标记动态路由是否已加载
const isDynamicRoutesLoaded = ref(false)
// 路由守卫
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
// 检查是否需要登录
if (to.meta.requiresAuth) {
// 检查登录状态
const isAuthenticated = await userStore.checkAuth()
if (isAuthenticated) {
// 如果动态路由未加载,则加载动态路由
if (!isDynamicRoutesLoaded.value) {
// 这里需要调用获取菜单列表的接口
const res = await getRoutersApi(userStore.roles[0].rolePerm)
if (res.data && res.data.length > 0) {
const dynamicRoutes = generateRoutes(res.data)
addRoutesToRouter(router, dynamicRoutes)
isDynamicRoutesLoaded.value = true
// 重新导航到目标路由
next({ ...to, replace: true })
return
}
}
next()
} else {
next('/login')
}
} else {
// 不需要登录的页面
if (to.path === '/login' && await userStore.checkAuth()) {
// 如果已登录,重定向到首页
next('/')
} else {
next()
}
}
})
// ... existing code ...
注意事项
-
上述代码假设您的菜单列表结构包含
path
,name
,title
,component
和children
等字段。您可能需要根据实际的接口返回结构进行调整。 -
需要在
router/index.ts
中导入generateRoutes
和addRoutesToRouter
函数,以及getRoutersApi
接口。 -
动态路由加载逻辑可能需要根据您的项目实际情况进行调整,例如添加错误处理、加载状态等。
-
确保在使用路由守卫时正确处理异步操作,避免出现路由跳转异常。
侧边栏菜单渲染实现
为 menus.vue
文件添加模板部分,实现侧边栏菜单的渲染。以下是完整的实现代码:
<template>
<el-menu
:default-active="activeMenu"
class="el-menu-vertical-demo"
@select="handleMenuSelect"
background-color="#f5f5f5"
text-color="#333"
active-text-color="#409EFF"
:collapse="isCollapse"
>
<!-- 递归渲染菜单 -->
<template v-for="menu in menuList" :key="menu.path">
<!-- 有子菜单的项 -->
<el-sub-menu v-if="menu.children && menu.children.length > 0" :index="menu.path">
<template #title>
<el-icon :size="18"><component :is="menu.meta.icon || 'Menu'"></component></el-icon>
<span>{{ menu.meta.title }}</span>
</template>
<!-- 递归渲染子菜单 -->
<template v-for="subMenu in menu.children" :key="subMenu.path">
<el-sub-menu v-if="subMenu.children && subMenu.children.length > 0" :index="subMenu.path">
<template #title>
<el-icon :size="18"><component :is="subMenu.meta.icon || 'Menu'"></component></el-icon>
<span>{{ subMenu.meta.title }}</span>
</template>
<!-- 第三级菜单 -->
<el-menu-item
v-for="thirdMenu in subMenu.children"
:key="thirdMenu.path"
:index="thirdMenu.path"
>
<el-icon :size="18"><component :is="thirdMenu.meta.icon || 'Menu'"></component></el-icon>
<span>{{ thirdMenu.meta.title }}</span>
</el-menu-item>
</el-sub-menu>
<!-- 无子菜单的项 -->
<el-menu-item v-else :index="subMenu.path">
<el-icon :size="18"><component :is="subMenu.meta.icon || 'Menu'"></component></el-icon>
<span>{{ subMenu.meta.title }}</span>
</el-menu-item>
</template>
</el-sub-menu>
<!-- 无子菜单的项 -->
<el-menu-item v-else :index="menu.path">
<el-icon :size="18"><component :is="menu.meta.icon || 'Menu'"></component></el-icon>
<span>{{ menu.meta.title }}</span>
</el-menu-item>
</template>
</el-menu>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { getRoutersApi } from '@renderer/api/user'
import { useUserStore } from '@renderer/store/user'
import { useRouter, useRoute } from 'vue-router'
import { generateRoutes, addRoutesToRouter } from '@renderer/router/routerChange'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const userStore = useUserStore()
const router = useRouter()
const route = useRoute()
// 菜单列表
const menuList = ref<any[]>([])
// 菜单折叠状态
const isCollapse = ref(false)
// 当前激活的菜单
const activeMenu = computed(() => {
return route.path
})
// 获取路由
const getRouters = async () => {
const data = await getRoutersApi(userStore.roles[0].rolePerm)
// 生成路由
const dynamicRoutes = generateRoutes(data)
// 添加到路由实例
addRoutesToRouter(router, dynamicRoutes)
menuList.value = dynamicRoutes
console.log('动态路由生成成功:', dynamicRoutes)
}
// 菜单选择事件处理
const handleMenuSelect = (index: string) => {
router.push(index)
}
onMounted(() => {
getRouters()
})
</script>
<style lang="scss" scoped>
.el-menu-vertical-demo {
height: 100vh;
width: 200px;
position: fixed;
left: 0;
top: 30px; // 与顶部窗口控制栏高度对齐
overflow-y: auto;
}
// 菜单折叠时的样式
.el-menu--collapse {
width: 64px;
}
// 自定义滚动条样式
.el-menu-vertical-demo::-webkit-scrollbar {
width: 6px;
}
.el-menu-vertical-demo::-webkit-scrollbar-thumb {
background-color: #ddd;
border-radius: 3px;
}
.el-menu-vertical-demo::-webkit-scrollbar-track {
background-color: #f5f5f5;
}
</style>
实现说明
-
模板部分:
-
使用 Element Plus 的
<el-menu>
组件创建垂直菜单 -
通过
v-for
遍历menuList
数据渲染菜单项 -
对有子菜单的项使用
<el-sub-menu>
,无子菜单的项使用<el-menu-item>
-
递归渲染子菜单,支持多级菜单结构
-
使用动态组件
<component :is="menu.meta.icon">
渲染菜单图标
-
-
脚本部分:
-
导入并注册所有 Element Plus 图标
-
添加
activeMenu
计算属性,根据当前路由高亮对应的菜单项 -
实现
handleMenuSelect
方法,处理菜单点击事件并跳转路由 -
保留原有的
getRouters
方法获取和处理菜单数据
-
-
样式部分:
-
设置菜单高度为全屏,宽度为 200px
-
固定菜单在左侧,顶部留出 30px 给窗口控制栏
-
添加滚动条样式美化
-
配置菜单折叠时的宽度为 64px
-
使用说明
-
确保已在项目中正确引入 Element Plus 和图标库
-
将上述代码替换到
menus.vue
文件中 -
菜单数据会从后端 API 获取并动态渲染
-
点击菜单项会自动跳转到对应的路由
-
菜单会根据当前路由自动高亮