Skip to content

🧭 动态路由与权限管理指南

🔑 权限配置流程

1️⃣ 中台权限配置

在业务中台-系统设置-应用管理页面添加应用、菜单、功能按钮:

应用管理

2️⃣ 角色授权配置

在业务中台-用户中心-角色管理页面选择角色并进行权限授权:

角色授权

📁 路由文件配置

⚙️ 主路由文件 (index.ts)

创建 routers/index.ts 文件,配置路由实例和路由守卫:

ts
import { createRouter, createWebHistory } from 'vue-router'

import { AuthStore, initDynamicRouter } from '@pt/common-ui'
import type { Menu } from '@pt/utils/modules/router'
import { storage } from '@pt/utils/modules/storage'

import { getLoginUrl, ROUTER_WHITE_LIST } from '@/config/config'
import ConditionalNProgress from '@/config/nprogress'
import { errorRouter, staticRouter } from '@/router/modules/staticRouter'

// 引入 views 文件夹下所有 vue 文件
// @ts-ignore
const modules = import.meta.glob('@/views/**/*.vue')
/**
 * @description 动态路由参数配置简介
 * @param path ==> 菜单路径
 * @param name ==> 菜单别名
 * @param redirect ==> 重定向地址
 * @param component ==> 视图文件路径
 * @param meta ==> 菜单信息
 * @param meta.icon ==> 菜单图标
 * @param meta.title ==> 菜单标题
 * @param meta.isLink ==> 是否外链
 * @param meta.isOpenInNewTab ==> 外链是否独立tab打开
 * @param meta.isHide ==> 是否隐藏
 * @param meta.isFull ==> 是否全屏(示例:数据大屏页面)
 * @param meta.isAffix ==> 是否固定在 tabs nav
 * @param meta.isKeepAlive ==> 是否缓存
 * */

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [...staticRouter, ...errorRouter],
  strict: false,
  scrollBehavior: () => ({ left: 0, top: 0 }),
})

/**
 * @description 路由拦截 beforeEach
 * */
router.beforeEach(async (to, _from, next) => {
  // 1.NProgress 开始
  ConditionalNProgress.start()

  const cookie_token = storage.get('token')
  const cookie_userInfoStr = storage.get('userInfo')
  const authStore = AuthStore()
  authStore.setRouteName(to.name as string)

  // 2.动态设置标题
  const title = import.meta.env.VITE_GLOB_APP_TITLE
  document.title = to.meta.title ? `${to.meta.title} - ${title}` : title

  // 3.判断是访问登陆页,有 Token 就在当前页面,没有 Token 重置路由并放行到登陆页
  /* if (to.path.toLocaleLowerCase() === LOGIN_URL) {
    if (cookie_token && cookie_userInfoStr) return next(from.fullPath);

    storage.clear();
    resetRouter();
    return next();
  } */

  // 4. 白名单可以直接访问
  if (ROUTER_WHITE_LIST.includes(to.path)) {
    return next()
  }

  // 5.判断是否有 Token,没有重定向到 login
  if (!cookie_token || !cookie_userInfoStr) {
    window.location.href = getLoginUrl()
    return next(false)
  }
  if (!authStore.authMenuListGet.length) {
    await initDynamicRouter(router, {
      appCode: "app_zhishiku",
      parentRouteName: 'layout',
      resolveComponent: (path: string) => {
        const module = modules['/src/views' + path + '.vue']
        if (module) return module
        console.warn(`未找到组件: /src/views${path}.vue`)
        console.warn('可用的组件路径:', Object.keys(modules))
        return () => Promise.reject(new Error(`Component ${path} not found`))
      },
      registerRoute(router, route, parentName) {
        // 注册到主布局
        router.addRoute(parentName, route)
        // 同时注册到 simple-layout (用于 iframe 嵌入)
        router.addRoute('simple-layout', {
          ...route,
          path: route.path.replace(/^\//, ''),
          name: `${String(route.name)}Simple`,
          meta: { ...route.meta, embed: 'iframe' },
        })
      },
      goLogin: () => {
        window.location.href = getLoginUrl()
      },
    })
    return next({ ...to, replace: true })
  }

  // 8. 动态首页跳转:如果是访问根路径,跳转到有权限的第一个菜单
  if (to.path === '/') {
    const firstMenu = authStore.flatMenuListGet.find(
      (item: Menu.MenuOptions) => !item.meta?.isHide && !item.meta?.isLink && item.path !== '/'
    )
    if (firstMenu) {
      return next({ path: firstMenu.path, replace: true })
    }
    // 如果一个菜单都没有,跳转到 404
    return next({ path: '/404', replace: true })
  }

  // 9.正常访问页面
  next()
})

/**
 * @description 路由跳转结束
 * */
router.afterEach(() => {
  ConditionalNProgress.done()
})

/**
 * @description 路由跳转错误
 * */
router.onError((error) => {
  ConditionalNProgress.done()
  console.warn('路由错误', error.message)
})

export default router

📊 静态路由文件 (staticRouter.ts)

创建 src/routers/staticRouter.ts 文件,配置基础路由结构:

ts
import type { RouteRecordRaw } from 'vue-router'

// import { HOME_URL } from '@/config/config'

// ===== 主布局(带 Header / Sider)下的静态路由 =====
const mainLayoutRoutes: RouteRecordRaw[] = []

// ===== SimpleLayout(纯 router-view)下的静态路由 =====
const simpleLayoutRoutes: RouteRecordRaw[] = []

/**
 * staticRouter(静态路由)
 */
export const staticRouter: RouteRecordRaw[] = [
  // 移除根路径重定向,改由路由守卫动态跳转首页
  /* {
    path: '/',
    redirect: HOME_URL,
  }, */
  {
    path: '/',
    name: 'layout',
    component: () => import('@/layouts/Main.vue'),
    meta: { layoutMode: 'main' },
    children: mainLayoutRoutes,
  },
  {
    path: '/simple',
    name: 'simple-layout',
    component: () => import('@/layouts/Simple.vue'),
    meta: { layoutMode: 'simple' },
    children: simpleLayoutRoutes,
  },
  {
    path: '/dataset/create',
    name: 'DatasetCreate',
    component: () => import('@/views/DataSet/IndexComponents/CreateDataset.vue'),
  },
]

/**
 * errorRouter(错误页面路由)
 */
export const errorRouter = [
  {
    path: '/403',
    name: '403',
    component: () => import('@/components/ErrorPage/403.vue'),
    meta: {
      title: '403页面',
    },
  },
  {
    path: '/404',
    name: '404',
    component: () => import('@/components/ErrorPage/404.vue'),
    meta: {
      title: '404页面',
    },
  },
  {
    path: '/500',
    name: '500',
    component: () => import('@/components/ErrorPage/500.vue'),
    meta: {
      title: '500页面',
    },
  },
]

/**
 * notFoundRouter(找不到路由)
 */
export const notFoundRouter = {
  path: '/:pathMatch(.*)*',
  name: 'notFound',
  redirect: { name: '404' },
}

⚓ 基础配置文件 (config/index.ts)

创建 src/config/index.ts 文件,定义基础路由配置:

ts
// * 首页地址(默认)
export const HOME_URL: string = '/components';  // 👉用不到了,自动取 firstRouteName

// * 登录页地址(默认)
export const LOGIN_URL: string = '/login';

// * Tabs(白名单地址,不需要添加到 tabs 的路由地址)
export const TABS_WHITE_LIST: string[] = ['/403', '/404', '/500', LOGIN_URL];

// * 路由白名单地址,不需要权限就可以访问(必须是本地存在的路由 staticRouter.ts)
export const ROUTER_WHITE_LIST: string[] = [
  '/register',
  '/forgotPassword',
  '/login/agreement',
  '/login/privacy',
  '/svg-preview/index',
  '/demo2',
  '/demo3',
];

🌟 主要功能说明

  • 🔐 权限控制:通过业务中台配置菜单和功能权限
  • 🔄 动态路由:使用 initDynamicRouter 方法动态加载路由
  • 🛡️ 路由守卫:自动处理登录状态、权限检查和路由跳转
  • 🗂️ 代码组织:将路由配置拆分为主文件、静态路由和基础配置

📝 最佳实践

  1. 将所有页面组件放在 views 目录下,便于动态路由自动加载
  2. 使用约定式路径命名,保持与后台权限配置的一致性
  3. 合理配置白名单,避免公共页面也需要权限验证
  4. 使用路由懒加载提高性能

Released under the MIT License.