3.左侧菜单动态路由的实现 一、在此之前我先把具体的路由写好1.为了后续分开两个不同账号的路由权限所以两个账号的路由我是分开的用于登陆接口返回新增路由文件resourceList.ts区分admin和user账号// \mock\modules\resourceList.ts const adminRouterLists [ { id: 1, name: 系统管理, route: /sysManage, resourceType: folder, icon: manage, children: [ { id: 11, name: 用户管理, route: /sysManage/userManage, resourceType: menu }, { id: 11, name: 菜单管理, resourceType: menu, route: /sysManage/menuManage } ] } ] const userRouterLists [ { id: 1, name: 系统管理, route: /sysManage, resourceType: folder, icon: manage, children: [ { id: 11, name: 用户管理, route: /sysManage/userManage, resourceType: menu } ] } ] export { adminRouterLists, userRouterLists }登录接口中根据不同账号resourceList返回不同路由// mock/index.ts import Mock from mockjs import { adminRouterLists, userRouterLists } from ./modules/resourceList // 设置延迟 Mock.setup({ timeout: 200-600 }) // 登录接口 Mock.mock(/api/auth/login, post, (options: any) { const body JSON.parse(options.body) const { username, password } body // 模拟验证逻辑 if (username admin password admin) { return { code: 200, data: { // guid 是 Mock.js 内置的占位符用于生成一个 全局唯一标识符GUID。 token: Mock.mock(guid), // id 是 Mock.js 内置的占位符用于生成一个随机且合法的身份证号码。 userId: Mock.mock(id), username: admin, nickname: 超级管理员, // 使用 DiceBear Avatar API 生成一个随机头像 SVG 图片 avatar: https://api.dicebear.com/7.x/avataaars/svg?seed${Mock.mock(name)}, roles: [admin], resourceList: adminRouterLists }, msg: 登陆成功, success: true } } else if (username user password user) { return { code: 200, data: { token: Mock.mock(guid), userId: Mock.mock(id), username: user, nickname: 普通用户, avatar: https://api.dicebear.com/7.x/avataaars/svg?seed${Mock.mock(name)}, roles: [user], resourceList: userRouterLists }, msg: 登陆成功, success: true } } else { return { code: 401, data: null, msg: 用户名或密码错误, success: false } } }) // 获取用户信息 Mock.mock(/api/auth/userInfo, get, () { const token localStorage.getItem(token) if (!token) { return { code: 401, data: null, message: 未登录, success: false } } // 模拟不同用户的权限 const isAdmin token.includes(admin) || Math.random() 0.5 return { code: 200, data: { id: Mock.mock(id), username: isAdmin ? admin : user, nickname: isAdmin ? 管理员 : 普通用户, avatar: https://api.dicebear.com/7.x/avataaars/svg?seed${Mock.mock(name)}, email: Mock.mock(email), phone: Mock.mock(phone), roles: isAdmin ? [admin] : [user], permissions: isAdmin ? [*] : [read, write] }, msg: 获取成功, success: true } }) Mock.mock(/api/auth/logout, post, () { return { code: 200, data: null, msg: 退出成功, success: true } }) export default Mock2.将其他涉及路由的地方都要新增相关信息1登录api中新增Route路由接口定义LoginResult接口新增返回resourceList// api/auth.ts import request, { request as Request} from /utils/request; // 路由接口定义 interface Route { id: number, name: string, route: string, icon: string, resourceType: string, children: Array{ id: number, name: string, icon: string, route: string, resourceType: string} } // 登录请求参数 export interface LoginParams { username: string, password: string } // 登陆响应数据 export interface LoginResult { token: string, userId: string, username: string, nickname: string, avatar?: string, roles: string[], resourceType: string, resourceList: Route[] } // 用户信息 export interface UserInfo { id: string, username: string, nickname: string, avatar: string, email: string, phone: string, roles: string[], permissions: string[] } // 登录快捷写法指定接口为post // export const login ( data: LoginParams ) { // return request.postLoginResult(/auth/login, data) // } // 通用写法动态method export const login ( data: LoginParams ) { return RequestLoginResult({ url: /auth/login, method: POST, data }) } // 获取用户信息 export const getUserInfo () { return request.getUserInfo(/auth/userInfo) } // 退出登录 export const logout () { return request.post(/auth/logout) } // 刷新token export const refreshToken () { return request.post{ token: string }(/auth/refresh) }2新增路由的store用于放置路由信息和相关操作// \src\store\modules\routerList.ts import { defineStore } from pinia; import { ref } from vue export const useRouterStore defineStore(router,() { interface Route { id: number, name: string, route: string, icon: string, resourceType: string, children: Array{ id: number, name: string, icon: string, route: string, resourceType: string} } // state // 路由列表 const routerList refRoute[]([]) const restoreRouter () { const stored localStorage.getItem(resource) || sessionStorage.getItem(resource) if (!stored) return try { const parsed JSON.parse(stored) if (Array.isArray(parsed)) routerList.value parsed } catch { routerList.value [] } } // actions // 从登录保存路由 function saveRouter(r: Route[]) { localStorage.setItem(resource, JSON.stringify(r)) sessionStorage.setItem(resource, JSON.stringify(r)) routerList.value r } function getRouter() { restoreRouter() } restoreRouter() return { routerList, saveRouter, getRouter, restoreRouter } })index.js中要引入这个模块// src/store/index.ts import { createPinia } from pinia const pinia createPinia() export default pinia // 导出所有模块 export * from ./modules/user export * from ./modules/setting export * from ./modules/routerList在这里我又新增了一个setting的store也要在index中引入用于控制左边菜单栏的折叠功能import { defineStore } from pinia import { ref } from vue export const useSettingStore defineStore(settingStore, () { // state const menuCollapse refboolean(false) // 控制左边菜单是否折叠 // actions function changeMenuCollapse() { menuCollapse.value !menuCollapse.value console.log(menuCollapse, menuCollapse.value) } return { menuCollapse, changeMenuCollapse } })3最重要的还有一个路由文件,里面包含了所有路由信息也要同步更新同时我还对里面的路由守卫做了一些优化针对刷新以后路由和token丢失每次刷新后就需要重新登陆和左边才菜单消失的问题做的优化当然路由守卫的优化这是我后面在初步实现路由功能后才出现的问题但我把他先放到前面来和其他的ts文件一起src\router\index.ts// src/router/index.ts // createRouter创建路由实例的函数createWebHistory使用HTML5 History模式URL不带 # import { createRouter, createWebHistory } from vue-router // type { RouteRecordRaw }TypeScript类型用于类型检查type关键字表示这是纯类型打包时会被移除 // RouteRecordRaw是Vue Router源码中定义的一个TypeScript类型/接口规定了每个路由配置必须包含哪些字段、可选哪些字段、每个字段是什么类型 import type { RouteRecordRaw } from vue-router // 加上 type 关键字 // 登陆时的vue组件 import LoginView from ../views/login/index.vue import { useUserStore } from /store/modules/user import { useRouterStore } from /store/modules/routerList.ts import Layout from ../views/layout/index.vue const routes: RouteRecordRaw[] [ { // 登录页用同步加载因为是必须的 path: /login, name: Login, component: LoginView, meta: { requiresAuth: false } }, { // 其他页面懒加载 path: /, name: Home, redirect: /sysManage, meta: { requiresAuth: true } }, { path: /sysManage, name: sysManage, component: Layout, meta: { title: 系统管理 }, redirect: /sysManage/userManage, children: [ { path: /sysManage/userManage, name: userManage, component: () import(/views/sys/UserManage/index.vue), meta: { title: 用户管理 } }, { path: /sysManage/menuManage, name: menuManage, component: () import(/views/sys/menuManage/index.vue), meta: { title: 菜单管理 } } ] } ] // 创建路由实例 const router createRouter({ history: createWebHistory(), // 使用history模式 routes // 传入路由表 }) // 路由守卫(to:目标路由对象,from: 来源路由对象,next()控制跳转) router.beforeEach(async (to, _from, next) { const token localStorage.getItem(token) const userStore useUserStore() const routerStore useRouterStore() console.log(token,token) // 如果页面需要认证 if (to.meta.requiresAuth ! false) { if (!token) { // 未登录跳到登录页 next({ path: /login, query: { redirect: to.fullPath } // 记录原本想访问的地址 }) return } // 如果已登录但没有用户信息尝试获取 if (!userStore.userInfo) { userStore.restoreUserInfo() // 从localStorage中恢复 // 如果回复后还为空则重新获取用户信息 if (!userStore.userInfo) { try { await userStore.fetchUserInfo() next() } catch { // 获取信息失败清除token跳转登录 userStore.clearUserInfo() next({ path: /login, query: { redirect: to.fullPath } }) } return } } if (!routerStore.routerList.length) { routerStore.getRouter() } // 已经登录并且有用户信息 next() } else { // 已经登陆的情况下访问登录页跳转首页 if (token to.path /login) { next(/) return } // 页面不需要认证 next() } }) //导出路由 export default router二、开始写组件1从layout.vue主页面写起ant-design-vue中有基本的框架可以直接用最终实际实现效果图为我的设计为左侧路由菜单——SiderBar组件右边顶部为Header组件Header组件中又分为了headerLeft—— 折叠按钮headerRight——暂时还未开发打算后续先写个人信息和退出登录的功能然后是下面的路由展示区域src\views\layout\index.vuetemplate a-layout a-layout-sider classsiderBar v-model:collapsedcollapsed :triggernull collapsible div logo /div SiderBar/SiderBar /a-layout-sider a-layout Header/Header a-layout-content classmain-content router-view v-slot{ Component, route } transition namefade-transform modeout-in keep-alive component v-ifComponent :isComponent :keyroute.path / /keep-alive /transition /router-view /a-layout-content /a-layout /a-layout /template script setup langts import { defineComponent, computed } from vue; import SiderBar from ./components/siderBar/index.vue import Header from ./components/header/index.vue import { useSettingStore } from /store/modules/setting.ts defineComponent({ name: siderBar }) const settingStore useSettingStore() const collapsed computed(() { return settingStore.menuCollapse }) /script style scoped langscss .siderBar { background-color: #ffffff; color: #000000; font-size: 26px; } /stylesrc\views\layout\components\header\index.vuetemplate div classcontainer HeaderLeft/HeaderLeft HeaderRight/HeaderRight /div /template script setup langts import HeaderLeft from ./components/headerLeft.vue import HeaderRight from ./components/headerRight.vue /script style scoped .container { display: flex; } /stylesrc\views\layout\components\header\components\headerLeft.vue要注意这里控制折叠的按钮状态是在store里的setting.ts中控制的哦所以这里是还要引入useSettingStoretemplate div classcontainer menu-unfold-outlined v-ifcollapsed classtrigger clickchangeCollapsed / menu-fold-outlined v-else classtrigger clickchangeCollapsed / /div /template script setup langts import { computed } from vue import { useSettingStore } from /store/modules/setting.ts const settingStore useSettingStore() const collapsed computed(() { return settingStore.menuCollapse }) function changeCollapsed() { settingStore.changeMenuCollapse() } /script style langscss scoped .container { width: 50%; padding: 15px 20px; } .trigger { font-size: 18px; } // #components-layout-demo-custom-trigger .trigger { // font-size: 18px; // line-height: 64px; // padding: 0 24px; // cursor: pointer; // transition: color 0.3s; // } // #components-layout-demo-custom-trigger .trigger:hover { // color: #1890ff; // } // #components-layout-demo-custom-trigger .logo { // height: 32px; // background: rgba(255, 255, 255, 0.3); // margin: 16px; // } // .site-layout .site-layout-background { // background: #fff; // } /stylesrc\views\layout\components\header\components\headerRight.vue还没开始开发留空template div classcontainer headerRight /div /template script setup langts /script style scoped .container { flex: 1; } /style关键的来了sideBar的开发这里我使用了一个组件的递归调用便于之后有多层嵌套路由虽然现在只有两层\src\views\layout\components\siderBar\index.vuetemplate div classmenu-scrollbar a-menu modeinline v-model:selectedKeysselectedKeys v-model:openKeysopenKeys MenuItem v-foritem in menuList :keyitem.route || item.id :menu-itemitem/MenuItem /a-menu /div /template script setup langts import { ref, defineComponent, computed, watch} from vue import { useRoute } from vue-router import { useRouterStore } from /store/modules/routerList import MenuItem from ./menuItem.vue defineComponent({ name: menu }) const route useRoute() const routerStore useRouterStore() const selectedKeys refstring[]([route.path]) const openKeys refstring[]([]) const menuList computed(() routerStore.routerList) interface MenuRoute { route: string children?: MenuRoute[] } // 找出当前路由所属的所有父级菜单用于初始化展开菜单 function findParentKeys(list: MenuRoute[], currentPath: string, parents: string[] []): string[] { for (const item of list) { const nextParents item.route ? [...parents, item.route] : parents if (item.route currentPath) return parents if (item.children?.length) { const result findParentKeys(item.children, currentPath, nextParents) if (result.length || item.children.some(child child.route currentPath)) return result } } return [] } // 监听路由变化自动高亮 watch( [() route.path, menuList], ([newPath, list]) { selectedKeys.value [newPath] openKeys.value findParentKeys(list as MenuRoute[], newPath) }, { immediate: true } ) // function extractAllRoutes(routes: Route[]){ // const result: string[] [] // routes.forEach(r { // // 添加当前route // result.push(r.route) // // 如果有子路由递归提取 // if (r.children r.children.length) { // result.push(...extractAllRoutes(r.children)) // } // }) // return result // } /script style langscss scoped .menu-scrollbar { height: 100%; overflow-y: auto; overflow-x: hidden; } /* 自定义滚动条样式 */ .menu-scrollbar::-webkit-scrollbar { width: 6px; height: 6px; } .menu-scrollbar::-webkit-scrollbar-track { background: transparent; border-radius: 3px; } .menu-scrollbar::-webkit-scrollbar-thumb { background: #d9d9d9; border-radius: 3px; transition: all 0.3s; } .menu-scrollbar::-webkit-scrollbar-thumb:hover { background: #bfbfbf; } /* Firefox 兼容 */ .menu-scrollbar { scrollbar-width: thin; scrollbar-color: #d9d9d9 transparent; } /style\src\views\layout\components\siderBar\menuItem.vueMenuItem组件内部又使用了MenuItem组件自身template template v-ifmenuItem.children menuItem.children.length a-sub-menu :keymenuItem.route || String(menuItem.id) template #title span{{ menuItem.name }}/span /template MenuItem v-forchild in menuItem.children :keychild.route || child.id :menu-itemchild/MenuItem /a-sub-menu /template a-menu-item v-else :keymenuItem.route clickhandleMenuClick(menuItem.route) {{ menuItem.name }} /a-menu-item /template script setup langts import { defineComponent, } from vue import { useRouter } from vue-router defineComponent({ name: menuItem }) interface MenuItem { id: number name: string route: string resourceType: string children?: MenuItem[] } defineProps{ menuItem: MenuItem }() const router useRouter() function handleMenuClick(route: string) { router.push(route) } /script style scoped /style到这里功能就已经基本实现了还有一些小细节的优化放在下方别忘了要添加相关路由的组件哦比如我新增的菜单管理和用户管理就要新增对应的组件才行哦菜单管理\src\views\sys\menuManage.vuetemplate div classcontainer 菜单管理 /div /template script setup langts /script style scoped .container { } /style用户管理\src\views\sys\UserManag.vuetemplate div classcontainer 用户管理 /div /template script setup langts /script style scoped .container { } /style小优化来啦1上面提到的路由守卫因为我发现每次刷新后token就会丢失需要重新登陆且路由也会丢失所以我在路由守卫中新增了routerStore.getRouter()和userStore.restoreUserInfo()的相关逻辑意思是在发起请求前都会检测一遍有没有路由信息和用户信息如果没有则重新获取。具体文件改动上方有提。2将‘\home’改为了‘\’在自动重定向去系统管理修改前{ // 其他页面懒加载 path: /home, name: Home, component: () import(../views/layout/index.vue), meta: { requiresAuth: true } }修改后{ // 其他页面懒加载 path: /, name: Home, redirect: /sysManage, meta: { requiresAuth: true } }3每次登陆后虽然自动选中了我重定向后的路由但是左侧的路由菜单仍然是收起状态未展开使用感和观感都不好所以我用了ant-design-vue中的openKeys控制菜单展开主要是findParentKeys这个方法找出需要展开的父级菜单节点\src\views\layout\components\siderBar\index.vuetemplate div classmenu-scrollbar a-menu modeinline v-model:selectedKeysselectedKeys v-model:openKeysopenKeys MenuItem v-foritem in menuList :keyitem.route || item.id :menu-itemitem/MenuItem /a-menu /div /template script setup langts import { ref, defineComponent, computed, watch} from vue import { useRoute } from vue-router import { useRouterStore } from /store/modules/routerList import MenuItem from ./menuItem.vue defineComponent({ name: menu }) const route useRoute() const routerStore useRouterStore() const selectedKeys refstring[]([route.path]) const openKeys refstring[]([]) const menuList computed(() routerStore.routerList) interface MenuRoute { route: string children?: MenuRoute[] } // 找出当前路由所属的所有父级菜单用于初始化展开菜单 function findParentKeys(list: MenuRoute[], currentPath: string, parents: string[] []): string[] { for (const item of list) { const nextParents item.route ? [...parents, item.route] : parents if (item.route currentPath) return parents if (item.children?.length) { const result findParentKeys(item.children, currentPath, nextParents) if (result.length || item.children.some(child child.route currentPath)) return result } } return [] } // 监听路由变化自动高亮 watch( [() route.path, menuList], ([newPath, list]) { selectedKeys.value [newPath] openKeys.value findParentKeys(list as MenuRoute[], newPath) }, { immediate: true } ) // function extractAllRoutes(routes: Route[]){ // const result: string[] [] // routes.forEach(r { // // 添加当前route // result.push(r.route) // // 如果有子路由递归提取 // if (r.children r.children.length) { // result.push(...extractAllRoutes(r.children)) // } // }) // return result // } /script style langscss scoped .menu-scrollbar { height: 100%; overflow-y: auto; overflow-x: hidden; } /* 自定义滚动条样式 */ .menu-scrollbar::-webkit-scrollbar { width: 6px; height: 6px; } .menu-scrollbar::-webkit-scrollbar-track { background: transparent; border-radius: 3px; } .menu-scrollbar::-webkit-scrollbar-thumb { background: #d9d9d9; border-radius: 3px; transition: all 0.3s; } .menu-scrollbar::-webkit-scrollbar-thumb:hover { background: #bfbfbf; } /* Firefox 兼容 */ .menu-scrollbar { scrollbar-width: thin; scrollbar-color: #d9d9d9 transparent; } /style最后实现效果为刚登陆时切换菜单菜单收缩有点小瑕疵之后再优化

相关新闻

最新新闻

代码知识图谱:从抽象语法树到交互式可视化,快速理解复杂项目架构

代码知识图谱:从抽象语法树到交互式可视化,快速理解复杂项目架构

1. 项目概述:当代码库变成一张“活地图”最近在GitHub上闲逛,又被一个项目给“震”到了,短短时间就冲上了17.8K Star。它的名字和功能一样直白:Understand Anything。说白了,它能把任何一个你扔给它的代码仓库&#xf…

2026/8/25 5:54:09
网络安全学习路线+自学笔记(超详细)自学网络安全看这一篇就够了_网络安全自学路线_网络安全专业知识点大全

网络安全学习路线+自学笔记(超详细)自学网络安全看这一篇就够了_网络安全自学路线_网络安全专业知识点大全

2026最新版 | 从零基础到入门实战 | 建议收藏反复看 📌 写在前面 经常有人在微信里问我:“我刚入门网络安全,该怎么学?要学哪些东西?有哪些方向?” 不同于Java、C/C等后端开发岗位有非常明晰的学习路线&a…

2026/8/25 5:54:09
基于QClaw框架的AI Agent趣味应用:程序员版MBTI测试H5游戏开发实战

基于QClaw框架的AI Agent趣味应用:程序员版MBTI测试H5游戏开发实战

1. 项目概述:一个程序员视角的MBTI趣味实验最近在捣鼓AI Agent开发,正好看到QClaw这个框架挺火,就想着用它来整点活儿。作为一个对MBTI(迈尔斯-布里格斯类型指标)有点兴趣,又天天和代码打交道的程序员&…

2026/8/25 5:54:09
UE Niagara Ribbon渲染器实战:打造动态刀锋特效与动画交互全流程

UE Niagara Ribbon渲染器实战:打造动态刀锋特效与动画交互全流程

大家好,我是专注于UE引擎技术分享的博主。在游戏或影视特效制作中,刀光剑影、能量斩击这类“刀锋特效”是提升视觉冲击力的关键。很多朋友在尝试用UE的Niagara粒子系统制作时,常常卡在如何让粒子形成锐利的线条、如何控制其动态消散&#xff…

2026/8/25 5:54:09
2026年软件测试面试题解析与备考指南

2026年软件测试面试题解析与备考指南

1. 项目概述2026年软件测试面试题整理汇总是一个面向软件测试工程师的面试准备资源库。随着数字化转型加速和软件质量要求提升,软件测试岗位的需求持续增长。这份资料整理了未来三年内可能流行的测试技术面试题,覆盖从基础理论到前沿技术的完整知识体系。…

2026/8/25 5:54:09
自研轻量级甘特图:像编辑文档一样协作,解决项目管理断层

自研轻量级甘特图:像编辑文档一样协作,解决项目管理断层

你有没有过这样的经历:项目排期会上,大家对着一个密密麻麻的表格争论不休,谁的任务该什么时候开始、什么时候结束,依赖关系怎么理都理不顺。你打开一个在线文档,试图用表格和颜色块手动画出一个甘特图,结果…

2026/8/25 5:49:09