实现页面级权限控制

方案:后端返回完整路由结构,前端动态组件映射

1. 设计思路
  1. 初始状态: 路由表中只有“登录页”、“404”等基础静态路由页面。
  2. 登录: 用户登录,获取 Token。
  3. 获取权限: 前端拿着 Token 请求后端接口,后端返回该用户的菜单树(路由表)
  4. 动态挂载: 前端将后端返回的 JSON 树转换为 Vue Router 路由对象,使用 router.addRoute() 挂载。
  5. 渲染菜单: 侧边栏菜单基于“静态路由 + 动态路由”的合并结果进行渲染。
2. 后端数据结构约定

后端返回的数据结构通常如下。前端组件通常是以字符串形式存储在数据库中的(如 "views/user/index"),前端需要负责将其转换为真正的组件引用。

{
  "code": 200,
  "data": [
    {
      "name": "System",
      "path": "/system",
      "component": "Layout",
      "meta": { "title": "系统管理" },
      "children": [
        {
          "name": "UserManage",
          "path": "user", 
          "component": "views/system/user/index",
          "meta": { "title": "用户管理", "roles": ["admin"] }
        }
      ]
    }
  ]
}
3. 前端实现步骤
第一步:Vite 组件自动导入

在 Vite 中,使用import.meta.glob 是实现动态路由,在 src/store/permission.js 中预先读取所有 Views 文件:

// 匹配 views 下所有的 .vue 文件
const modules = import.meta.glob('../views/**/*.vue')
第二步:Pinia 状态管理

我们需要一个 Store 来处理路由数据的获取和格式化。

关键逻辑: 将后端返回的字符串 component 映射为前端的组件对象。

// src/store/permission.js
import { defineStore } from 'pinia'
import { constantRoutes } from '@/router' // 引入静态路由
import { getUserNav } from '@/api/user'
import Layout from '@/layout/index.vue'

const modules = import.meta.glob('../views/**/*.vue')

export const usePermissionStore = defineStore('permission', {
  state: () => ({
    routes: [] // 最终用于渲染侧边栏的完整菜单
  }),
  actions: {
    generateRoutes() {
      return new Promise(async (resolve) => {
        const res = await getUserNav()
        const remoteRoutes = res.data
        
        // 1. 递归转换后端路由
        const accessedRoutes = this.filterAsyncRoutes(remoteRoutes)
        // 3. 合并菜单:静态 + 动态
        this.routes = constantRoutes.concat(accessedRoutes)
        
        resolve(accessedRoutes)
      })
    },

    filterAsyncRoutes(routes) {
      const res = []
      routes.forEach(route => {
        const tmp = { ...route }
        // 处理 Layout 组件
        if (tmp.component === 'Layout') {
          tmp.component = Layout
        } else {
          // 处理业务组件
          const componentPath = `../${tmp.component}.vue`
          if (modules[componentPath]) {
            tmp.component = modules[componentPath]
          } else {
            console.error('组件路径错误:', componentPath)
          }
        }
        if (tmp.children) {
          tmp.children = this.filterAsyncRoutes(tmp.children)
        }
        res.push(tmp)
      })
      return res
    }
  }
})
第三步:路由守卫 (Gatekeeper)

src/permission.js 中,我们利用 router.beforeEach 来拦截请求。

import router from './router'
import { usePermissionStore } from '@/store/permission'

let hasRoutes = false // 标记是否已加载

router.beforeEach(async (to, from, next) => {
  const permissionStore = usePermissionStore()
  const hasToken = localStorage.getItem('token') //  Token 检查

  if (hasToken) {
    if (to.path === '/login') {
      next({ path: '/' })
    } else {
      if (hasRoutes) {
        // 如果路由已加载,直接放行
        next()
      } else {
        try {
          // 1. 获取动态路由
          const accessRoutes = await permissionStore.generateRoutes()
          // 2. 动态添加到 Router
          accessRoutes.forEach(route => {
            router.addRoute(route)
          })
          hasRoutes = true
          // 3.关键:确保路由添加完成后再跳转
          next({ ...to, replace: true })
        } catch (error) {
          next('/login')
        }
      }
    }
  } else {
    next('/login')
  }
})

记得在 src/main.jsimport './permission' 激活路由守卫。