Vue3后台管理系统(十九)路由vue-router

  • Post author:
  • Post category:vue




前言:这一章非常重要,首先我们要思考,路由涉及到了哪些东西? ①它要生成URL地址与vue组件的路由关系,②它要根据当前用户的角色与菜单来决定要生成哪些地址路由。③它要把静态路由和后端传来的动态菜单路由结合在一起。④有些路由不需要权限控制(白名单),比如登录页。



1.提供查询角色菜单的api

2.用pinia存储router信息

3.路由守卫中去查询用户角色和权限,动态添加路由。

4.注销退出后删除路由。


目录


一、安装 vue-router


二、创建几个页面


三、菜单API


四、创建路由实例


五、Pinia存储路由


六、全局注册


七、路由守卫


一、

安装 vue-router

npm install vue-router@next

二、创建几个页面

在src下新建layout文件夹,在layout文件夹下新建index.vue。

在src下新建views文件夹,在views文件夹下新建login文件夹,在login文件夹下新建index.vue。

在views文件夹下新建error-page文件夹,然后在error-page文件夹下新建404.vue和401.vue。

在views文件夹下新建redirect文件夹,然后在redirect文件夹下新建index.vue。

三、菜单API

在src/api文件夹下新建menu文件夹,在menu文件夹下新建index.ts和types.ts

// src/api/menu/index.ts
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { MenuQuery, Menu, Resource, MenuForm } from './types';

/**
 * 获取路由列表
 */
export function listRoutes() {
  return request({
    url: '/api/v1/menus/routes',
    method: 'get'
  });
}

/**
 * 获取菜单表格列表
 *
 * @param queryParams
 */
export function listMenus(queryParams: MenuQuery): AxiosPromise<Menu[]> {
  return request({
    url: '/api/v1/menus',
    method: 'get',
    params: queryParams
  });
}

/**
 * 获取菜单下拉树形列表
 */
export function listMenuOptions(): AxiosPromise<OptionType[]> {
  return request({
    url: '/api/v1/menus/options',
    method: 'get'
  });
}

/**
 * 获取资源(菜单+权限)树形列表
 */
export function listResources(): AxiosPromise<Resource[]> {
  return request({
    url: '/api/v1/menus/resources',
    method: 'get'
  });
}

/**
 * 获取菜单详情
 * @param id
 */
export function getMenuDetail(id: string): AxiosPromise<MenuForm> {
  return request({
    url: '/api/v1/menus/' + id,
    method: 'get'
  });
}

/**
 * 添加菜单
 *
 * @param data
 */
export function addMenu(data: MenuForm) {
  return request({
    url: '/api/v1/menus',
    method: 'post',
    data: data
  });
}

/**
 * 修改菜单
 *
 * @param id
 * @param data
 */
export function updateMenu(id: string, data: MenuForm) {
  return request({
    url: '/api/v1/menus/' + id,
    method: 'put',
    data: data
  });
}

/**
 * 批量删除菜单
 *
 * @param ids 菜单ID,多个以英文逗号(,)分割
 */
export function deleteMenus(ids: string) {
  return request({
    url: '/api/v1/menus/' + ids,
    method: 'delete'
  });
}
// src/api/menu/types.ts
/**
 * 菜单查询参数类型声明
 */
export interface MenuQuery {
  keywords?: string;
}

/**
 * 菜单分页列表项声明
 */

export interface Menu {
  id?: number;
  parentId: number;
  type?: string | 'CATEGORY' | 'MENU' | 'EXTLINK';
  createTime: string;
  updateTime: string;
  name: string;
  icon: string;
  component: string;
  sort: number;
  visible: number;
  children: Menu[];
}

/**
 * 菜单表单类型声明
 */
export interface MenuForm {
  /**
   * 菜单ID
   */
  id?: string;
  /**
   * 父菜单ID
   */
  parentId: string;
  /**
   * 菜单名称
   */
  name: string;
  /**
   * 菜单是否可见(1:是;0:否;)
   */
  visible: number;
  icon?: string;
  /**
   * 排序
   */
  sort: number;
  /**
   * 组件路径
   */
  component?: string;
  /**
   * 路由路径
   */
  path: string;
  /**
   * 跳转路由路径
   */
  redirect?: string;

  /**
   * 菜单类型
   */
  type: string;

  /**
   * 权限标识
   */
  perm?: string;
}

/**
 * 资源(菜单+权限)类型
 */
export interface Resource {
  /**
   * 菜单值
   */
  value: string;
  /**
   * 菜单文本
   */
  label: string;
  /**
   * 子菜单
   */
  children: Resource[];
}

/**
 * 权限类型
 */
export interface Permission {
  /**
   * 权限值
   */
  value: string;
  /**
   * 权限文本
   */
  label: string;
}

四、

创建路由实例

在src文件夹下新建router文件夹,在router文件夹下新建index.ts

// src/router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import { usePermissionStoreHook } from '@/store/modules/permission';

export const Layout = () => import('@/layout/index.vue');

// 静态路由
export const constantRoutes: RouteRecordRaw[] = [
  {
    path: '/redirect',
    component: Layout,
    meta: { hidden: true },
    children: [
      {
        path: '/redirect/:path(.*)',
        component: () => import('@/views/redirect/index.vue')
      }
    ]
  },
  {
    path: '/login',
    component: () => import('@/views/login/index.vue'),
    meta: { hidden: true }
  },
  {
    path: '/404',
    component: () => import('@/views/error-page/404.vue'),
    meta: { hidden: true }
  },

  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/dashboard/index.vue'),
        name: 'Dashboard',
        meta: { title: 'dashboard', icon: 'homepage', affix: true }
      },
      {
        path: '401',
        component: () => import('@/views/error-page/401.vue'),
        meta: { hidden: true }
      }
    ]
  }

  // 外部链接
  /*{
        path: '/external-link',
        component: Layout,
        children: [
            {
                path: 'https://www.cnblogs.com/haoxianrui/',
                meta: { title: '外部链接', icon: 'link' }
            }
        ]
    }*/
  // 多级嵌套路由
  /* {
         path: '/nested',
         component: Layout,
         redirect: '/nested/level1/level2',
         name: 'Nested',
         meta: {title: '多级菜单', icon: 'nested'},
         children: [
             {
                 path: 'level1',
                 component: () => import('@/views/nested/level1/index.vue'),
                 name: 'Level1',
                 meta: {title: '菜单一级'},
                 redirect: '/nested/level1/level2',
                 children: [
                     {
                         path: 'level2',
                         component: () => import('@/views/nested/level1/level2/index.vue'),
                         name: 'Level2',
                         meta: {title: '菜单二级'},
                         redirect: '/nested/level1/level2/level3',
                         children: [
                             {
                                 path: 'level3-1',
                                 component: () => import('@/views/nested/level1/level2/level3/index1.vue'),
                                 name: 'Level3-1',
                                 meta: {title: '菜单三级-1'}
                             },
                             {
                                 path: 'level3-2',
                                 component: () => import('@/views/nested/level1/level2/level3/index2.vue'),
                                 name: 'Level3-2',
                                 meta: {title: '菜单三级-2'}
                             }
                         ]
                     }
                 ]
             },
         ]
     }*/
];

// 创建路由
const router = createRouter({
  history: createWebHashHistory(),
  routes: constantRoutes as RouteRecordRaw[],
  // 刷新时,滚动条位置还原
  scrollBehavior: () => ({ left: 0, top: 0 })
});

// 重置路由
export function resetRouter() {
  const permissionStore = usePermissionStoreHook();
  permissionStore.routes.forEach(route => {
    const name = route.name;
    if (name && router.hasRoute(name)) {
      router.removeRoute(name);
    }
  });
}

export default router;

五、Pinia存储路由

在src/store/modules/文件夹下新建permission文件夹,在permission文件夹下新建index.ts

// src/store/modules/permission/index.ts
import { RouteRecordRaw } from 'vue-router';
import { defineStore } from 'pinia';
import { constantRoutes } from '@/router';
import { store } from '@/store';
import { listRoutes } from '@/api/menu';
import { ref } from 'vue';

const modules = import.meta.glob('../../views/**/**.vue');
export const Layout = () => import('@/layout/index.vue');

const hasPermission = (roles: string[], route: RouteRecordRaw) => {
    if (route.meta && route.meta.roles) {
        if (roles.includes('ROOT')) {
            return true;
        }
        return roles.some(role => {
            if (route.meta?.roles !== undefined) {
                return (route.meta.roles as string[]).includes(role);
            }
        });
    }
    return false;
};

const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
    const res: RouteRecordRaw[] = [];
    routes.forEach(route => {
        const tmp = { ...route } as any;
        if (hasPermission(roles, tmp)) {
            if (tmp.component == 'Layout') {
                tmp.component = Layout;
            } else {
                const component = modules[`../../views/${tmp.component}.vue`] as any;
                if (component) {
                    tmp.component = component;
                } else {
                    tmp.component = modules[`../../views/error-page/404.vue`];
                }
            }
            res.push(tmp);

            if (tmp.children) {
                tmp.children = filterAsyncRoutes(tmp.children, roles);
            }
        }
    });
    return res;
};

// setup
export const usePermissionStore = defineStore('permission', () => {
    // state
    const routes = ref<RouteRecordRaw[]>([]);
    const addRoutes = ref<RouteRecordRaw[]>([]);

    // actions
    function setRoutes(newRoutes: RouteRecordRaw[]) {
        addRoutes.value = newRoutes;
        routes.value = constantRoutes.concat(newRoutes);
    }

    function generateRoutes(roles: string[]) {
        return new Promise<RouteRecordRaw[]>((resolve, reject) => {
            listRoutes()
                .then(response => {
                    const asyncRoutes = response.data;
                    const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
                    setRoutes(accessedRoutes);
                    resolve(accessedRoutes);
                })
                .catch(error => {
                    reject(error);
                });
        });
    }
    return { routes, setRoutes, generateRoutes };
});

// 非setup
export function usePermissionStoreHook() {
    return usePermissionStore(store);
}


六、全局注册

// main.ts
import router from "@/router";

app.use(router)
   .mount('#app')


七、路由守卫



通过路由守卫添加动态路由

//src/permission.ts
import router from '@/router';
import { RouteRecordRaw } from 'vue-router';
import { useUserStoreHook } from '@/store/modules/user';
import { usePermissionStoreHook } from '@/store/modules/permission';

import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
NProgress.configure({ showSpinner: false }); // 进度条

const permissionStore = usePermissionStoreHook();

// 白名单路由
const whiteList = ['/login'];

router.beforeEach(async (to, from, next) => {
  NProgress.start();
  const userStore = useUserStoreHook();
  if (userStore.token) {
    // 登录成功,跳转到首页
    if (to.path === '/login') {
      next({ path: '/' });
      NProgress.done();
    } else {
      const hasGetUserInfo = userStore.roles.length > 0;
      if (hasGetUserInfo) {
        if (to.matched.length === 0) {
          from.name ? next({ name: from.name as any }) : next('/401');
        } else {
          next();
        }
      } else {
        try {
          const { roles } = await userStore.getInfo();
          const accessRoutes: RouteRecordRaw[] =
            await permissionStore.generateRoutes(roles);
          accessRoutes.forEach((route: any) => {
            router.addRoute(route);
          });
          next({ ...to, replace: true });
        } catch (error) {
          // 移除 token 并跳转登录页
          await userStore.resetToken();
          next(`/login?redirect=${to.path}`);
          NProgress.done();
        }
      }
    }
  } else {
    // 未登录可以访问白名单页面
    if (whiteList.indexOf(to.path) !== -1) {
      next();
    } else {
      next(`/login?redirect=${to.path}`);
      NProgress.done();
    }
  }
});

router.afterEach(() => {
  NProgress.done();
});



版权声明:本文为wenxingchen原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。