Skip to content

侧边栏&路由

路由和侧边栏是组织起一个后台应用的关键骨架。

项目侧边栏和路由是绑定在一起的,resource/admin/router/index.ts 是整个路由的入口文件,如果是按正常顺序文档看下来,应该知道本项目的路由分为两种情况

  • 动态生成的路由,也就是权限管理打开后,菜单管理的数据
  • 每个模块的views目录下的 router.js 配置静态路由

所以一般情况下不用管resource/admin/router/index.ts路由这个入口文件。

类型

对于权限和菜单,系统定义了两种类型,查看 resource/admin/types

权限类型

js
export interface Permission {
  id: number // id
  parent_id: number // 父级 ID
  permission_name: string // 权限名称
  type: number // 类型
  icon: string // icon 图标
  component: string // 组件
  module: string // 模块
  permission_mark: string // 权限标识
  route: string // 路由,对应的是 vue route 的 path
  redirect: string
  keepAlive: boolean
  hidden: boolean // 是否隐藏
  is_inner: boolean // 是否是内页
}

菜单类型

菜单类型,最终都是由权限类型转换而来,所以一旦是动态生成的路由,那么元数据都是由菜单数据提供

js
import { Component } from 'vue'
import { RouteRecordRaw } from 'vue-router'

// meta 元数据
// 在记录上附加自定义数据。
// 这个数据将会附着在 vue route 上
export interface Meta {
  title: string
  icon: string // icon
  roles?: string[] // 哪些角色可以访问页面,未实现,保留
  cache?: boolean // 页面缓存,未实现,保留
  hidden: boolean // 是否隐藏,当设置成 true 时,菜单则不会在侧边栏显示。例如内页编辑页面啊,Login,页面 404 页面啊
  keepalive?: boolean // 是否 keepalive 目前未实现,保留数据结构
  is_inner?: boolean // 是否是内页
}

// @ts-ignore
// Menu 类型和 Vue Route 类型一样了
export interface Menu extends Omit<RouteRecordRaw, 'meta'> {
  path: string // path 访问路径
  name: string // name 菜单名称
  meta?: Meta // meta,路由附着的额外数据
  redirect?: string
  component?: Component // 页面组件
  children?: Menu[] // 子菜单
}

在了解完这两个相关类型之后,再来看动态菜单和权限如何实现的,静态菜单就不做介绍了。首先找到resource/admin/route/guard/index.ts 文件,从这里开始,这里是路由导航守卫。下面直接通过代码来注解如何实现

js
const guard = (router: Router) => {
  // white list
  const whiteList: string[] = [WhiteListPage.LOGIN_PATH, WhiteListPage.NOT_FOUND_PATH]

  router.beforeEach(async (to, from, next) => {
    // set page title
    setPageTitle(to.meta.title as unknown as string)
    // page start
    progress.start()
    // 获取用户的 token
    const authToken = getAuthToken()
    // 如果 token 存在
    if (authToken) {
      // 如果进入 /login 页面,重定向到首页
      if (to.path === WhiteListPage.LOGIN_PATH) {
        next({ path: '/' })
      } else {
        const userStore = useUserStore()
        // 获取用户ID
        if (userStore.getId) {
          next()
        } else {
          try {
            // 阻塞获取用户信息
            // ⚠️ 用户信息已经包含了该用户所有可用权限,在 `permissions` 里
            await userStore.getUserInfo()
            // 如果后端没有返回 permissions,前台则只使用静态路由
            if (userStore.getPermissions !== undefined) {
              // 挂载路由(实际是从后端获取用户的权限)
              const permissionStore = usePermissionsStore()
              // 动态路由挂载,这里是主要实现动态路由菜单的地方
              const asyncRoutes = permissionStore.getAsyncMenusFrom(toRaw(userStore.getPermissions))
              // 在这里使用 addRoute 动态挂在路由
              asyncRoutes.forEach((route: Menu) => {
                router.addRoute(route as unknown as RouteRecordRaw)
              })
            }
            next({ ...to, replace: true })
          } catch (e) {
            removeAuthToken()
            next({ path: `${WhiteListPage.LOGIN_PATH}?redirect=/${to.path}` })
          }
        }
      }
      progress.done()
    } else {
      // 如果不在白名单
      if (whiteList.indexOf(to.path) !== -1) {
        next()
      } else {
        next({ path: WhiteListPage.LOGIN_PATH })
      }
      progress.done()
    }
  })

  router.afterEach(() => {
    progress.done()
  })
}

export default guard

侧边栏

上面经过路由导航守卫之后,动态权限就已经转化为动态菜单了。主要通过这个方法来实现转换

js
const asyncRoutes = permissionStore.getAsyncMenusFrom(toRaw(userStore.getPermissions))

这里就不细说里面的实现了,是通过递归实现无限极菜单。但是这里一个非常重要的点,就是权限是通过pinia 进行保存的,因为 pinia 是响应式的。找到 resource/admin/store/user/permissions.ts,看下 permissionStore 的定义

js
interface Permissions {
  menus: Menu[] // 菜单

  asyncMenus: Menu[] // 动态菜单

  permissions: Permission[] // 权限

  menuPathMap: Map<string, string> // menu 和 path 的 MAP 数据
}

export const usePermissionsStore = defineStore('PermissionsStore', {
  // state 里面定义的几个数据都是响应式的
  state: (): Permissions => {
    return {
      menus: [],

      asyncMenus: [],

      permissions: [],

      menuPathMap: new Map(),
    }
  },
}

既然菜单都是响应式的,那就好办了呀!菜单的数据就直接从 store 获取就可以了。 侧边栏的实现是在 layout/components/Menu,侧边栏的菜单也是基于ElementPlusel-menu 实现的。因为是动态菜单,所以这里的用到了vue渲染函数。源码在 layout/components/Menu/index.vue