核心思想就是通过登录请求此用户对应的权限菜单,然后跳转首页,触发全局前置导航守卫,在全局导航守卫中通过 addRoute 添加动态路由进去。addRoute有一个需要注意的地方,就是我们添加完动态路由后,地址栏上立即访问添加的动态路由,它不会跳转,需要我们手动触发下,push或者replace都可以进行触发。但是用在全局前置导航守卫中,写法又不太一样,可以参照官网说明
这是我自己练习的项目文件分布

我这里把主要文件的代码都贴出来
首先是 pinia 文件,这里面主要存储了 token 与 动态路由 数据。我做了一个持久化存储
stores 里面的 counter.ts
- import { ref } from 'vue'
- import { defineStore } from 'pinia'
-
- export const useUserStore = defineStore('user', () => {
- // token
- const token = ref('')
- // 动态路由
- const dynamicRoutes = ref([])
-
- // 设置token
- const setToken = (t: string) => { token.value = t }
- // 设置动态路由
- const setDynamicRoutes = (r: any) => { dynamicRoutes.value = r }
-
- // 清空token
- const clearToken = () => { token.value = '' }
- // 清空动态路由
- const clearDynamicRoutes = () => { dynamicRoutes.value = [] }
-
- return {
- token,
- dynamicRoutes,
- setToken,
- setDynamicRoutes,
- clearToken,
- clearDynamicRoutes
- }
- }, {
- persist: {
- enabled: true // true 表示开启持久化保存
- }
- })
路由信息 router 文件夹里面的 index.ts
- import { createRouter, createWebHistory } from 'vue-router'
- import HomeView from '../views/HomeView.vue'
-
- const router = createRouter({
- history: createWebHistory(import.meta.env.BASE_URL),
- routes: [
- {
- path: '/',
- name: 'home',
- component: HomeView,
- // 重定向的首页
- redirect: '/test'
- },
- {
- path: '/login',
- name: 'login',
- component: () => import('../views/Login.vue')
- }
- ]
- })
-
- export default router
登录页面:views 文件夹里面的 Login.vue
- <div>
- <el-button @click="submit">登录el-button>
- div>
-
- <script setup lang="ts">
- import { useUserStore } from '@/stores/counter'
- import { useRouter } from 'vue-router'
-
- const userStore = useUserStore()
- const router = useRouter()
-
- const submit = () => {
- // 模拟登录
- setTimeout(() => {
- // 存储token和动态路由
- userStore.setToken('Bearen Xxx')
- userStore.setDynamicRoutes([
- {
- path: '/about',
- name: 'about',
- children: [
- { path: '/about/music', name: 'music', component: 'Music' },
- { path: '/about/movie', name: 'movie', component: 'Movie' },
- {
- path: '/about/parent',
- name: 'parent',
- children: [
- { path: '/about/parent/child', name: 'parent', component: 'Parents' }
- ]
- },
- ]
- },
- { path: '/test', name: 'test', component: 'Test' },
- ])
- // 跳转路由。触发全局前置导航守卫
- router.replace('/')
- })
- }
- script>
点击登录页面的登录按钮后,会跳转到 '/' ,路由变化了,就会触发全局前置导航守卫,全局前置导航守卫我写在了 mian.ts 文件中
自定义指令可以忽略,那是我做按钮级权限用的(可以看我上一篇文章)。
需要注意的地方就是 hasAddAliveRoutes 这个变量,记录是否已经添加过动态路由了,如果添加过了,就赋值为true。在去其他路由的时候,就不会重新添加动态路由了。还有一个作用就是,刷新的时候,hasAddAliveRoutes会重新变为false,会重新添加一下动态路由,防止刷新路由丢失
- import { createApp } from 'vue'
- import { createPinia } from 'pinia'
-
- import App from './App.vue'
- import router from './router'
- import ElementPlus from 'element-plus'
- import 'element-plus/dist/index.css'
-
- import piniaPersist from 'pinia-plugin-persist'
- import { useUserStore } from './stores/counter'
-
- const app = createApp(App)
-
- const pinia = createPinia()
- app.use(pinia)
- app.use(router)
- app.use(ElementPlus)
-
- pinia.use(piniaPersist)
-
- // 处理动态路由,下面的全局前置导航守卫会用到
- const getNewRoutes = (routes: any) => {
- let res = routes.map((i: any) => {
- return {
- ...i,
- // 动态添加component,有就添加,没有就不添加 (带有子级的路由是没有component的)
- ...(i.component && { component: () => import(`./views/${i.component}.vue`) }),
- // 再把内层的处理一下
- ...(i.children && { children: getNewRoutes(i.children) }),
- }
- })
- return res
- }
-
- // 是否添加过动态路由 这里的标识作用:刷新的时候,会变为false,然后就会重新添加动态路由,防止路由丢市的
- let hasAddAliveRoutes = false
- router.beforeEach((to, from, next) => {
- if (to.path === '/login') {
- next()
- } else {
- let token = useUserStore().token
- if (token) { // 存在token
- // 判断是否有动态路由添加了
- if (!hasAddAliveRoutes) { // 没有添加,则添加动态路由并进行触发
- // 对pinia中的动态路由进行处理,component字段只是一个文件名称,不是我们想要的动态引入,所以需要修改
- let arr = getNewRoutes(useUserStore().dynamicRoutes)
- console.log('处理之后的路由', arr)
- // 开始添加动态路由
- for (let i = 0; i < arr.length; i++) {
- router.addRoute('home', arr[i])
- }
- // 修改添加动态路由的状态
- hasAddAliveRoutes = true
- // 触发添加的动态路由
- next(to.fullPath)
- } else { // 已经添加了,则直接通过
- next()
- }
- } else { // 不存在token
- next('/login')
- }
- }
- })
-
-
- // 假装此用户在tset页面只有 改 和 查 的按钮权限
- let buttonAuth = [
- { path: '/test', btn: ['check', 'change'] }
- ]
- // 自定义指令: 控制按钮级权限
- app.directive('permission', {
- mounted(el, binding) {
- // console.log(el) // 元素
- // console.log(binding.value) // 值
- // console.log(binding.arg) // 路由
-
- // 遍历按钮数组,根绝当前的路由找到这一项的按钮权限
- let btnAuth = buttonAuth.find(item => item.path === binding.arg)
- if (btnAuth) { // 找到了
- // 不包含此按钮权限就移除按钮
- !btnAuth.btn.includes(binding.value) && el.parentNode.removeChild(el)
- }
- }
- })
-
- app.mount('#app')
然后就会跳往首页了,也就是 HomeView.vue 页面,这个文件里面用到了递归组件MenuTree.vue
这个递归组件就是用来递归菜单的,多少级菜单都能进行展示
- <el-container class="layout-container-demo" style="height: 100%">
- <el-aside :width="aside">
- <el-scrollbar>
- <el-menu
- :router="true"
- :collapse="isCollapse"
- :mode="mode"
- :collapse-transition="false"
- :default-active="$router.currentRoute.value.path"
- >
- <MenuTree :routes="routes" :isCollapse="isCollapse">MenuTree>
- el-menu>
- el-scrollbar>
- el-aside>
-
- <el-container>
- <el-header style="text-align: right; font-size: 12px">
- <div class="toolbar">
- <el-button @click="collapse">折叠菜单el-button>
- <el-button @click="changeMode">改变布局el-button>
- <el-dropdown>
- <el-icon style="margin-right: 8px; margin-top: 1px">
- <setting />
- el-icon>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item>Viewel-dropdown-item>
- <el-dropdown-item>Addel-dropdown-item>
- <el-dropdown-item>Deleteel-dropdown-item>
- el-dropdown-menu>
- template>
- el-dropdown>
- <span @click="reback">退出span>
- div>
- el-header>
-
- <el-main>
- <el-scrollbar>
- <router-view>router-view>
- el-scrollbar>
- el-main>
- el-container>
- el-container>
-
- <script lang="ts" setup>
- import { Setting } from '@element-plus/icons-vue'
- import MenuTree from './MenuTree.vue'
- import { useUserStore } from '@/stores/counter'
- import { ref } from 'vue'
- import { useRouter } from 'vue-router'
-
- const userStore = useUserStore()
- const router = useRouter()
-
- // 路由
- const routes = ref(userStore.dynamicRoutes)
- // 是否折叠
- const isCollapse = ref(false)
- // 宽度
- const aside = ref('200px')
- // 菜单展示方式
- const mode = ref('vertical')
-
- // 点击折叠菜单
- const collapse = () => {
- isCollapse.value = !isCollapse.value
- aside.value = isCollapse.value ? '60px' : '200px'
- }
- // 点击改变布局
- const changeMode = () => {
- mode.value = mode.value === 'vertical' ? 'horizontal' : 'vertical'
- }
- // 点击退出
- const reback = () => {
- userStore.clearToken()
- userStore.clearDynamicRoutes()
- router.replace('/login')
- }
- script>
-
- <style scoped>
- .layout-container-demo .el-header {
- position: relative;
- background-color: var(--el-color-primary-light-7);
- color: var(--el-text-color-primary);
- }
-
- .layout-container-demo .el-aside {
- color: var(--el-text-color-primary);
- background: var(--el-color-primary-light-8);
- /* 新加的过度效果 */
- transition: width 0.15s;
- -webkit-transition: width 0.15s;
- -moz-transition: width 0.15s;
- -webkit-transition: width 0.15s;
- -o-transition: width 0.15s;
- }
-
- .layout-container-demo .el-menu {
- border-right: none;
- }
-
- .layout-container-demo .el-main {
- padding: 0;
- }
-
- .layout-container-demo .toolbar {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- height: 100%;
- right: 20px;
- }
- style>
递归组件MenuTree.vue
- <div v-for="item in routes" :key="item.path">
-
- <el-menu-item :index="item.path" v-if="!item.children">
- <el-icon>
- <message />
- el-icon>
- <span v-show="!isCollapse">
- {{ item.path }}
- span>
- el-menu-item>
-
-
- <el-sub-menu :index="item.path" v-else>
- <template #title>
-
- <el-icon class="more-menu-icon-hover">
- <message />
- el-icon>
- <span v-show="!isCollapse">
- {{ item.path }}
- span>
- template>
- <MenuTree :routes="item.children">MenuTree>
- el-sub-menu>
- div>
- template>
-
- <script setup lang="ts">
- import { Message } from '@element-plus/icons-vue'
-
- defineProps({
- routes: {
- type: Array,
- default: () => [],
- },
- isCollapse: Boolean,
- })
- script>
-
- <style scoped>
- /* 新增的样式,解决箭头问题 */
- .more-menu-icon-hover {
- z-index: 99;
- background-color: #fff;
- transition: all 0.3s;
- }
-
- :deep(.el-sub-menu__title:hover) {
- .more-menu-icon-hover {
- background-color: #ecf5ff;
- }
- }
- style>
但是路由规则数组中,我其实做了重定向。也就是路由匹配到 '/' 的时候,会重定向到 /test ,也就是Test.vue页面。首页HomeView.vue文件中我指定的有二级路由出口,所以Test.vue页面的内容会展示在HomeView.vue页面的路由出口处
- <div>
- <el-button v-permission:[currentRoute]="'add'">增加el-button>
- <el-button v-permission:[currentRoute]="'delete'">删除el-button>
- <el-button v-permission:[currentRoute]="'change'">修改el-button>
- <el-button v-permission:[currentRoute]="'check'">查看el-button>
- div>
-
- <script setup lang="ts">
- import { ref } from 'vue'
- import { useRouter } from 'vue-router'
-
- const router = useRouter()
- // 获取当前的路由
- const currentRoute = ref(router.currentRoute.value.path)
- script>
好了,到这里,前端路由其实就已经做好了。实现的是菜单级别和按钮级别的权限
下面的百度网盘地址,有需要的可以自提:安装依赖后,直接 npm run dev 即可启动
链接:https://pan.baidu.com/s/1qYq8TzsroanggPfhVQEPoQ?pwd=x89u
提取码:x89u