springboot+sa-token+vue-admin 动态菜单的实现

使用技术

后端 springboot + mybatisplus+ sa-token

前端 vue-element-admin 模板 + vuex

背景

最近在做一个前后分离的后台管理,其中系统内有不同的角色用户。我想实现根据不同类型的用户登录,分别显示用户各自所拥有的菜单。话不多说上图:

  1. 管理员登录

管理员拥有所有菜单,所以登录有显示所有菜单

  1. 学生用户登录

实现原理

后端

  1. 使用sa-token 对当前用户进行判断,查看该用户拥有哪些角色

  1. 根据该用户拥有的角色去数据库里查询菜单数据

  1. 将菜单数据转为树形结构,提供接口,以便将菜单数据 以JSON字符串的形式传给前端

前端

  1. 通过vuex 获取菜单数据,然后将其转换为路由的对象数组,并添加至路由

  1. 在 sidebar-item 组件里遍历该菜单数据。

实现步骤

数据库设计

  1. 用户表

-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user`  (
  `id` bigint NOT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '账户',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
  `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像',
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '描述',
  `state` int NOT NULL COMMENT '状态(0启用,1停用)',
  `is_deleted` int NOT NULL COMMENT '逻辑删除(0存在,1删除)',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime NOT NULL COMMENT '修改时间',
  `operator_id` bigint NOT NULL COMMENT '操作员编号(企业id、学生id、教师id,可通过此id查询对应信息)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_user
-- ----------------------------
INSERT INTO `t_user` VALUES (9527, 'tonkey', 'admin', '111111', NULL, '管理员', 0, 0, '2023-02-21 21:45:57', '2023-02-21 21:45:59', 9527);
INSERT INTO `t_user` VALUES (10086, 'feifei', 'student01', '111111', NULL, '学生用户', 0, 0, '2023-02-22 17:28:54', '2023-02-22 17:28:56', 9527);
INSERT INTO `t_user` VALUES (230001, '蓝月科技', 'lanyue', '111111', NULL, '企业用户', 0, 0, '2023-02-23 16:08:39', '2023-02-23 16:08:41', 9527);
  1. 角色表

-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名',
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色描述',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime NOT NULL COMMENT '修改时间',
  `operator_id` bigint NOT NULL COMMENT '操作员编号',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `name`(`name` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_role
-- ----------------------------
INSERT INTO `t_role` VALUES (1, 'admin', '管理员', '2023-02-22 16:12:10', '2023-02-22 16:12:10', 1620600815042273282);
INSERT INTO `t_role` VALUES (2, 'student', '学生用户', '2023-02-22 16:12:10', '2023-02-22 16:12:10', 1620600815042273282);
INSERT INTO `t_role` VALUES (3, 'company', '企业用户', '2023-02-22 16:13:29', '2023-02-22 16:13:29', 1620600815042273282);
INSERT INTO `t_role` VALUES (4, 'teacher', '教师用户', '2023-02-22 16:13:29', '2023-02-22 16:13:29', 1620600815042273282);
  1. 用户-角色 关联表

-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `role_id` int NOT NULL,
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime NOT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_user_role
-- ----------------------------
INSERT INTO `t_user_role` VALUES (1, 10086, 2, '2023-02-22 17:29:22', '2023-02-22 17:29:25');
INSERT INTO `t_user_role` VALUES (2, 230001, 3, '2023-02-23 16:09:23', '2023-02-23 16:09:25');
INSERT INTO `t_user_role` VALUES (3, 9527, 1, '2023-02-27 14:25:08', '2023-02-27 14:25:10');
INSERT INTO `t_user_role` VALUES (4, 9527, 2, '2023-02-27 14:42:45', '2023-02-27 14:42:47');
  1. 菜单表

DROP TABLE IF EXISTS `t_menu`;
CREATE TABLE `t_menu`  (
  `id` int NOT NULL COMMENT '菜单id',
  `parent_id` int NULL DEFAULT NULL COMMENT '父级菜单',
  `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '标题',
  `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '路径',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称',
  `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '组件',
  `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '图标',
  `always_show` tinyint NULL DEFAULT NULL COMMENT '始终显示',
  `has_hidden` tinyint NOT NULL COMMENT '是否隐藏(0否,1是)',
  `has_props` tinyint NOT NULL COMMENT '是否有props参数',
  `has_beforeEnter` tinyint NOT NULL COMMENT '独享路由',
  `has_children_node` tinyint NOT NULL COMMENT '是否有子路由',
  `redirect` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '重定向',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
  `operator_id` bigint NULL DEFAULT NULL COMMENT '操作员编号',
  `operator` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作员姓名',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_menu
-- ----------------------------
INSERT INTO `t_menu` VALUES (1000, NULL, '学生管理', '/studentMng', 'studentMng', 'Layout', 'studentMng', 1, 0, 0, 0, 1, '/studentMng/studentList', '2023-02-27 13:34:12', '2023-02-27 20:46:58', 9527, 'tonkey');
INSERT INTO `t_menu` VALUES (1001, 1000, '学生列表', 'studentList', 'studentList', 'Custom/StudentMng/StudentMng', 'studentList', NULL, 0, 0, 0, 0, NULL, '2023-02-27 13:37:24', '2023-02-27 13:37:25', 9527, 'tonkey');
INSERT INTO `t_menu` VALUES (1002, 1000, '新增学生', 'addStudent', 'addStudent', 'Custom/StudentMng/OperateForm', 'el-icon-s-operation', NULL, 1, 1, 1, 0, NULL, '2023-02-27 13:40:08', '2023-02-27 20:09:59', 9527, 'tonkey');
INSERT INTO `t_menu` VALUES (1003, 1000, '编辑学员', 'editStudent', 'editStudent', 'Custom/StudentMng/OperateForm', 'el-icon-s-operation', NULL, 1, 1, 1, 0, NULL, '2023-02-27 20:10:36', '2023-02-27 20:10:36', 9527, 'admin');
INSERT INTO `t_menu` VALUES (2000, NULL, '院系管理', '/academyMng', 'academyMng', 'Layout', 'el-icon-school', 1, 0, 0, 0, 1, '/academyMng/departmentList', '2023-02-27 19:08:22', '2023-02-27 19:54:51', 9527, 'tonkey');
INSERT INTO `t_menu` VALUES (2001, 2000, '院系列表', 'departmentList', 'departmentList', 'Custom/AcademyMng/DepartmentMng', 'el-icon-tickets', NULL, 0, 0, 0, 0, NULL, '2023-02-27 19:43:57', '2023-02-27 19:43:57', 9527, 'admin');
INSERT INTO `t_menu` VALUES (2003, 2000, '专业列表', 'majorList', 'majorList', 'Custom/AcademyMng/MajorMng', 'el-icon-collection', NULL, 0, 0, 0, 0, NULL, '2023-02-27 19:49:24', '2023-02-27 19:49:24', 9527, 'admin');
INSERT INTO `t_menu` VALUES (3000, NULL, '企业管理', '/companyMng', 'companyMng', 'Layout', 'el-icon-office-building', 1, 0, 0, 0, 1, '/companyMng/companyList', '2023-02-27 19:56:05', '2023-02-27 19:56:05', 9527, 'admin');
INSERT INTO `t_menu` VALUES (3001, 3000, '企业列表', 'companyList', 'companyList', 'Custom/CompanyMng/CompanyMng', 'el-icon-tickets', NULL, 0, 0, 0, 0, NULL, '2023-02-27 19:56:28', '2023-02-27 19:56:28', 9527, 'admin');
INSERT INTO `t_menu` VALUES (3002, 3000, '岗位发布管理', 'postposting', 'postposting', 'Custom/CompanyMng/PostPosting', 'el-icon-s-management', NULL, 0, 0, 0, 0, NULL, '2023-02-27 19:56:48', '2023-02-27 19:56:48', 9527, 'admin');
INSERT INTO `t_menu` VALUES (4000, NULL, '工作信息管理', '/jobCategoryMng', 'jobCategoryMng', 'Layout', 'el-icon-user-solid', 1, 0, 0, 0, 1, '/jobCategoryMng/jobCategoryList', '2023-02-27 19:57:33', '2023-02-27 19:59:36', 9527, 'tonkey');
INSERT INTO `t_menu` VALUES (4001, 4000, '职位类别列表', 'jobCategoryList', 'jobCategoryList', 'Custom/JobCategoryMng/JobCategoryMng', 'el-icon-user', NULL, 0, 0, 0, 0, NULL, '2023-02-27 19:58:11', '2023-02-27 19:58:11', 9527, 'admin');
INSERT INTO `t_menu` VALUES (5000, NULL, '菜单管理', '/menuMng', 'menuMng', 'Layout', 'el-icon-user-solid', 1, 0, 0, 0, 1, '/menuMng/menuList', '2023-02-27 19:59:29', '2023-02-27 19:59:29', 9527, 'admin');
INSERT INTO `t_menu` VALUES (5001, 5000, '菜单列表', 'menuList', 'menuList', 'Custom/SystemSettingsMng/MenuMng/MenuMng', 'el-icon-user', NULL, 0, 0, 0, 0, NULL, '2023-02-27 20:00:12', '2023-02-27 20:01:55', 9527, 'tonkey');
INSERT INTO `t_menu` VALUES (6000, NULL, '简历管理', '/resumenMng', 'resumenMng', 'Layout', 'el-icon-tickets', 1, 0, 0, 0, 1, '/resumenMng/resumeList', '2023-02-27 20:02:53', '2023-02-27 20:02:53', 9527, 'admin');
INSERT INTO `t_menu` VALUES (6001, 6000, '简历列表', 'resumeList', 'resumeList', 'Custom/ResumeMng/ResumeMng', 'el-icon-s-operation', NULL, 0, 0, 0, 0, NULL, '2023-02-27 20:03:27', '2023-02-27 20:03:27', 9527, 'admin');
INSERT INTO `t_menu` VALUES (6002, 6000, '简历编辑页面', 'resumeEdit', 'resumeEdit', 'Custom/ResumeMng/ResumeEdit', 'el-icon-edit', NULL, 0, 1, 0, 0, NULL, '2023-02-27 20:04:05', '2023-02-27 20:04:05', 9527, 'admin');
INSERT INTO `t_menu` VALUES (6003, 6000, '简历模板', 'resumetemplate', 'resumetemplate', 'Custom/ResumeMng/ResumeTemplate', 'el-icon-printer', NULL, 0, 1, 0, 0, NULL, '2023-02-27 20:04:25', '2023-02-27 20:04:25', 9527, 'admin');

后端实现步骤

  1. 逆向工程生成代码

  1. controller层代码

@WebLog(description = "获取菜单树结构")
@ApiOperation("获取菜单树结构")
@GetMapping("/queryMenuTree")
public R queryMenuTree() {
    // 系统使用sa-token 框架实现登录,鉴权功能 
    // 所以这里用 StpUtil.getRoleList() 获取登录用户拥有的角色
    List<String> roleList = StpUtil.getRoleList();
    // 如果是admin 直接查询全部菜单
    boolean admin = roleList.stream().anyMatch(s -> s.equals("admin"));
    return admin?  menuService.getMenuTree(): menuService.queryMenuListByRole();
}
  1. service层代码

// 该方法查询全部菜单
@Override
public R getMenuTree() {
    List<Menu> menuList1 = baseMapper.selectList(null);
    // 实体类转VO
    List<MenuVO> menuList = objectMapper.convertValue(menuList1, new TypeReference<List<MenuVO>>() {
    });
    // 构建菜单树
    List<MenuVO> menuTree = new ArrayList<>();
    for (MenuVO menu : menuList) {
        if (menu.getParentId() == null) {
            // 该菜单为根节点
            menu.setChildren(getChildren(menu.getId(), menuList));
            menuTree.add(menu);
        }
    }
    return R.ok().data("menuTree",menuTree);
}

private List<MenuVO> getChildren(Integer parentId, List<MenuVO> menuList) {
    // 获取子菜单
    List<MenuVO> children = new ArrayList<>();
    for (MenuVO menu : menuList) {
        if (parentId.equals(menu.getParentId())) {
            // 递归获取子菜单
            menu.setChildren(getChildren(menu.getId(), menuList));
            children.add(menu);
        }
    }
    return children;
}

@Override
public R queryMenuListByRole() {
    // 获取登录用户拥有的角色
    List<String> roleList = StpUtil.getRoleList();
    if(CollectionUtil.isEmpty(roleList)){
        return R.ok().data("displayMenus",new ArrayList<Menu>(0));
    }
    // 根据角色名称查询角色表,获取角色编号
    LambdaQueryWrapper<Role> roleLambdaQueryWrapper = new LambdaQueryWrapper<>();
    roleLambdaQueryWrapper.in(Role::getName,roleList);
    List<Role> roles = roleMapper.selectList(roleLambdaQueryWrapper);
    List<Integer> roleIds = roles.stream().map(Role::getId).collect(Collectors.toList());
    // 根据角色编号查询 角色菜单关联表 获取菜单编号
    LambdaQueryWrapper<RoleMenu> roleMenuLambdaQueryWrapper = new LambdaQueryWrapper<>();
    roleMenuLambdaQueryWrapper.in(RoleMenu::getRoleId,roleIds);
    List<RoleMenu> roleMenus = roleMenuMapper.selectList(roleMenuLambdaQueryWrapper);
    if(CollectionUtil.isEmpty(roleMenus)){
        return R.ok().data("displayMenus",new ArrayList<Menu>(0));
    }
    // 拿到菜单编号
    List<Integer> menuIds = roleMenus.stream().map(RoleMenu::getMenuId).collect(Collectors.toList());
    // 查询菜单表,获取对应菜单信息
    List<Menu> menus = baseMapper.selectBatchIds(menuIds);
    // 根据菜单编号去重
    List<Menu> menuList = menus.stream().distinct().collect(Collectors.toList());

    // 实体类转VO
    List<MenuVO> menuVOS = objectMapper.convertValue(menuList, new TypeReference<List<MenuVO>>() {
    });

    // 构建菜单树
    List<MenuVO> menuTree = new ArrayList<>();
    for (MenuVO menu : menuVOS) {
        if (menu.getParentId() == null) {
            // 该菜单为根节点
            menu.setChildren(getChildren(menu.getId(), menuVOS));
            menuTree.add(menu);
        }
    }
    return R.ok().data("menuTree",menuTree);
}

前端实现步骤

系统用的是 vue-element-admin 基础模板

  1. 通过封装的axios 发送请求获取菜单数据

// 获取菜单树结构
export const reqQueryMenuTree = () => requests.get('/admin/menu/queryMenuTree');
  1. 在 store文件夹下新建文件 permission.js (名称可以随意来),编写三连环(这里参考的是若依(ruoyi)的代码)

import router, { constantRoutes, dynamicRoutes } from '@/router';
import { reqQueryMenuTree } from '@/views/Custom/SystemSettingsMng/MenuMng/api/MenuMngApi';
import Layout from '@/layout/index';

const permission = {
  // namespaced: true,
  state: {
    routes: [],
    addRoutes: [],
    defaultRoutes: [],
    topbarRouters: [],
    sidebarRouters: [],
  },
  mutations: {
    SET_ROUTES: (state, routes) => {  
      state.addRoutes = routes;
      state.routes = constantRoutes.concat(routes);
    },
    SET_DEFAULT_ROUTES: (state, routes) => {
      state.defaultRoutes = constantRoutes.concat(routes);
    },
    SET_TOPBAR_ROUTES: (state, routes) => {
      state.topbarRouters = routes;
    },
    SET_SIDEBAR_ROUTERS: (state, routes) => {
      // 这里是路由子组件遍历的数据
      state.sidebarRouters = routes;
    },
  },
  actions: {
    // 生成路由
    GenerateRoutes({ commit }) {
      return new Promise((resolve) => {
        // 向后端请求路由数据
        reqQueryMenuTree().then((res) => {
          console.log();
          const sdata = res.data.menuTree;
          const rdata = res.data.menuTree;
          const sidebarRoutes = convertMenuToRoutes(sdata);
          const rewriteRoutes = convertMenuToRoutes(rdata);
          // const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
          // 404 页面
          rewriteRoutes.push(
            {
              path: '/404',
              component: () => import('@/views/404'),
            },
            { path: '*', redirect: '/404', hidden: true }
          );
          // 404 页面
          sidebarRoutes.push(
            {
              path: '/404',
              component: () => import('@/views/404'),
            },
            { path: '*', redirect: '/404', hidden: true }
          );
          // router.addRoutes(asyncRoutes);
          commit('SET_ROUTES', rewriteRoutes);
          commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes));
          commit('SET_DEFAULT_ROUTES', sidebarRoutes);
          commit('SET_TOPBAR_ROUTES', sidebarRoutes);
          resolve(rewriteRoutes);
        });
      });
    },
  },
};

// 菜单转路由
function convertMenuToRoutes(menus) {
  const routes = [];  // 存放路由对象的数组
  menus.forEach((menu) => {
    const route = {  // 路由对象
      path: menu.path,  // path
      name: menu.name,  // name
      redirect: menu.redirect,  // 重定向
      // 是否总是显示,即当该菜单只有一个子项时,也显示该菜单加子菜单。
      // 如果为false,则只显示子菜单
      alwaysShow: menu.alwaysShow == 1 ? true : false,  
      hidden: menu.hasHidden == 1 ? true : false,  // 是否隐藏
      meta: { title: menu.title, icon: menu.icon },  // 菜单title与图标icon
    };
    // 父菜单的component 都是 上面导入的 Layout
    if (menu['component'] === 'Layout') route.component = Layout;
    else {
      // 子菜单进入 loadView 方法,拼接后返回
      route.component = loadView(menu.component);
    }
    // props属性,路由传参时使用的到。
    // 是否有 props 属性
    if (menu.hasProps) {
      switch (menu.title) {
        case '专业列表':
          route.props = ({ params: { departmentId } }) => ({ departmentId });
          break;
        case '编辑学员':
          route.props = ({ params: { editStudentInfo, depAndMajorMenu } }) => ({
            editStudentInfo,
            depAndMajorMenu,
          });
          break;
        case '新增学员':
          route.props = ({ params: { depAndMajorMenu } }) => ({
            depAndMajorMenu,
          });
          break;
        case '简历编辑页面':
        case '简历模板':
          route.props = ({ params: { isEdit, resumeRowInfo } }) => ({
            isEdit,
            resumeRowInfo,
          });
          break;
      }
    }
    // 是否有 独享守卫,
    if (menu.hasBeforeenter) {
      switch (menu.title) {
        case '编辑学员':
        case '新增学员':
          route.beforeEnter = (to, from, next) => {
            //第一个参数to,包含的内容是切换后的路由对象,也就是跳转后的路由对象
            //第二个参数from,包含的内容的是切换前的路由对象,也就是跳转前的路由对象
            //第三个参数next(),是否往下执行,执行的话,如果不写的话路由就不会跳转,操作将会终止
            if (from.path.indexOf(['studentList']) != -1) {
              next();
            } else {
              next({
                name: 'studentList',
              });
            }
          };
          break;
      }
    }
    // 子路由
    if (menu.children && menu.children.length > 0) {
      route.children = convertMenuToRoutes(menu.children);
    }
    routes.push(route);
  });
  return routes;
}

export const loadView = (view) => {
  if (process.env.NODE_ENV === 'development') {
    // 数据库里存放的组件路径不要写成 @/views/xxxxx,
    // 最好只写 /xxxx 然后,在用 `@/views/${view}` 拼接
    // 这里需要注意:不要写 ([`${view}`)
    return (resolve) => require([`@/views/${view}`], resolve);
  } else {
    // 使用 import 实现生产环境的路由懒加载
    return () => import(`@/views/${view}`);
  }
};

export default permission;
  1. 将 permission.js 注册到 store 的index.js 文件里

import permission from './modules/permission';
import Vue from 'vue';
import Vuex from 'vuex';
import getters from './getters';
import app from './modules/app';
import settings from './modules/settings';
import user from './modules/user';
Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    app,
    settings,
    user,
    permission,
  },
  getters,
});

export default store;
  1. 将getters里将 导出 sidebarRouters,routes

const getters = {
  sidebarRouters: (state) => state.permission.sidebarRouters,
  routes: (state) => state.permission.routes,
};
export default getters;
  1. 发送请求获取菜单列表

注意:发送请求的时机 -- 这里参考了尚硅谷的尚品汇视频内容

import router, { constantRoutes } from './router';
import store from './store';
import { Message } from 'element-ui';
import NProgress from 'nprogress'; // progress bar
import 'nprogress/nprogress.css'; // progress bar style
import { getToken } from '@/utils/auth'; // get token from cookie
import getPageTitle from '@/utils/get-page-title';


NProgress.configure({ showSpinner: false }); // NProgress Configuration


const whiteList = ['/login', '/register']; // no redirect whitelist


// import Layout from '@/layout';


// 菜单数据
// let menuTree = JSON.parse(sessionStorage.getItem('menuTree'));
// let routes = convertMenuToRoutes(menuTree);
// console.dir(routes);
router.beforeEach(async (to, from, next) => {
  // start progress bar
  NProgress.start();


  // set page title
  document.title = getPageTitle(to.meta.title);


  // determine whether the user has logged in
  const hasToken = getToken();


  if (hasToken) {  // 判断有无token
    if (to.path === '/login') {
      // if is logged in, redirect to the home page
      next({ path: '/' });
      NProgress.done();
    } else {
      const hasGetUserInfo = store.getters.name;
      if (hasGetUserInfo && hasGetUserInfo != '') {
        next();
      } else {
        try {
          // get user info
          await store.dispatch('user/getInfo').then(() => {
            // 在获取用户信息完成后,拉取菜单列表
            	store.dispatch('GenerateRoutes').then((accessRoutes) => {
              // 根据roles权限生成可访问的路由表
              router.addRoutes(accessRoutes); // 动态添加可访问路由表
              next({ ...to, replace: true }); // hack方法 确保addRoutes已完成
            });
          });
        } catch (error) {
          // remove token and go to login page to re-login
          await store.dispatch('user/resetToken');
          Message.error(error || 'Has Error');
          next(`/login?redirect=${to.path}`);
          NProgress.done();
        }
      }
    }
  } else {
    /* has no token*/
    if (whiteList.indexOf(to.path) !== -1) {
      // in the free login whitelist, go directly
      next();
    } else {
      // other pages that do not have permission to access are redirected to the login page.
      next(`/login?redirect=${to.path}`);
      NProgress.done();
    }
  }
});


router.afterEach(() => {
  // finish progress bar
  NProgress.done();
});
  1. 找layout 下 components 下的Sidebar 下的 index.vue,

<template>
  <div :class="{ 'has-logo': showLogo }">
    <logo v-if="showLogo" :collapse="isCollapse" />
    <el-scrollbar wrap-class="scrollbar-wrapper">
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :background-color="variables.menuBg"
        :text-color="variables.menuText"
        :unique-opened="false"
        :active-text-color="variables.menuActiveText"
        :collapse-transition="false"
        mode="vertical"
      >
        <!-- 这里遍历菜单-->
        <sidebar-item
          v-for="route in routes"
          :key="route.path"
          :item="route"
          :base-path="route.path"
        />
      </el-menu>
    </el-scrollbar>
  </div>
</template>


<script>
import { mapGetters } from 'vuex';
import Logo from './Logo';
import SidebarItem from './SidebarItem';
import variables from '@/styles/variables.scss';


export default {
  components: { SidebarItem, Logo },
  computed: {
    ...mapGetters(['sidebar']),
    // 获取getter里的routes
    ...mapGetters(['sidebarRouters', 'sidebar', 'routes']),
    // routes() {
    //   return this.$router.options.routes;
    // },
    activeMenu() {
      const route = this.$route;
      const { meta, path } = route;
      // if set path, the sidebar will highlight the path you set
      if (meta.activeMenu) {
        return meta.activeMenu;
      }
      return path;
    },
    showLogo() {
      return this.$store.state.settings.sidebarLogo;
    },
    variables() {
      return variables;
    },
    isCollapse() {
      return !this.sidebar.opened;
    },
  },
};
</script>

到此就完成了

补充

mybatis-plus代码生成器

简单模板

package ${package.Mapper};

import ${package.Entity}.${entity};
import ${superMapperClassPackage};
import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* ${table.comment!} Mapper 接口
* </p>
*
* @author ${author}
* @since ${date}
*/
<#if kotlin>
    interface ${table.mapperName} : ${superMapperClass}<${entity}>
    <#else>
    @Mapper
    public interface ${table.mapperName} extends ${superMapperClass}<${entity}> {

}
</#if>

核心代码 --生成的位置可以改动一下

package cn.qiyu5522.tk_ems.utils;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {
    /**
* <p>
* 读取控制台内容
* </p>
*/
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotEmpty(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }

    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/service-core/src/main/java");  // 生成的位置
        gc.setAuthor("Tonkey");  // 设置作者名
        gc.setOpen(false);
        gc.setSwagger2(true);  // 开启 swagger 注解
        gc.setFileOverride(false);  // 每次生成是否覆盖已生成的同名文件
        gc.setBaseResultMap(true);
        gc.setServiceName("%sService");    //去掉Service接口的首字母I
        mpg.setGlobalConfig(gc);

        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/tk_ems_db?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("root");
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
        //        pc.setModuleName("template");
        pc.setParent("cn.qiyu5522.tk_ems");
        pc.setMapper("mapper");
        pc.setEntity("entity");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
    String templatePath = "/templates/mapper.xml.ftl";
    // 如果模板引擎是 velocity
    //         String templatePath = "/templates/mapper.xml.vm";

    // 自定义输出配置
    List<FileOutConfig> focList = new ArrayList<>();
    // 自定义配置会被优先输出
    focList.add(new FileOutConfig(templatePath) {
    @Override
    public String outputFile(TableInfo tableInfo) {
    // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
    return projectPath + "/service-core/src/main/java/cn/qiyu5522/tk_ems/mapper/xml/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
    }
    });
    /*
    cfg.setFileCreate(new IFileCreate() {
    @Override
    public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
    // 判断自定义文件夹是否需要创建
    checkDir("调用默认方法创建的目录");
    return false;
    }
    });
    */
    cfg.setFileOutConfigList(focList);
    mpg.setCfg(cfg);

    // 配置模板
    TemplateConfig templateConfig = new TemplateConfig();

    // 配置自定义输出模板
    //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
    // templateConfig.setEntity("templates/entity2.java");
    // templateConfig.setService();
    // templateConfig.setController();

    templateConfig.setXml(null);
    mpg.setTemplate(templateConfig);

    // 策略配置
    StrategyConfig strategy = new StrategyConfig();
    strategy.setNaming(NamingStrategy.underline_to_camel);
    strategy.setColumnNaming(NamingStrategy.underline_to_camel);
    strategy.setEntityLombokModel(true);


    strategy.setEntityTableFieldAnnotationEnable(true);
    strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
    strategy.setControllerMappingHyphenStyle(true);
    strategy.setTablePrefix("t_");
    strategy.setChainModel(true);
    //设置乐观锁字段名
    strategy.setVersionFieldName("version");
    //设置逻辑删除字段名
    strategy.setLogicDeleteFieldName("is_deleted");
    strategy.setEntityLombokModel(true);
    strategy.setEntityBooleanColumnRemoveIsPrefix(true);//去掉布尔值的is_前缀(确保tinyint(1))
    strategy.setRestControllerStyle(true); //restful api风格控制器 返回json
    strategy.setChainModel(true);

    // 自动填充策略--可以看mybatis官网  -- 不需要可以注册
    // 自定义需要填充的字段
    // List<TableFill> tableFillList = new ArrayList<>();
    // tableFillList.add(new TableFill("create_time", FieldFill.INSERT));
    // tableFillList.add(new TableFill("operator_id", FieldFill.INSERT_UPDATE));
    // tableFillList.add(new TableFill("update_time", FieldFill.INSERT_UPDATE));
    // tableFillList.add(new TableFill("operator", FieldFill.INSERT_UPDATE));
    // strategy.setTableFillList(tableFillList);


    mpg.setStrategy(strategy);
    mpg.setTemplateEngine(new FreemarkerTemplateEngine());
    mpg.execute();
    }
}

链接地址

若依地址[http://www.ruoyi.vip/]

sa-token地址https://sa-token.cc/

vue-element-admin[https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/]

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
SpringBoot中整合sa-token,可以按照以下步骤进行操作。 1. 添加依赖:在`pom.xml`文件中添加sa-token的Redis集成包依赖。可以使用官方提供的Redis集成包`sa-token-dao-redis-jackson`,具体依赖如下: ``` <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-dao-redis-jackson</artifactId> <version>1.34.0</version> </dependency> ``` 2. 配置sa-token:在SpringBoot的配置文件中,配置sa-token的相关属性,包括Redis连接信息、token有效期等。可以参考sa-token的官方文档进行配置。 3. 注解鉴权:在需要进行鉴权的方法上添加相应的注解。例如,使用`@SaCheckLogin`注解表示该方法需要登录认证,使用`@SaCheckRole`注解表示该方法需要具有指定角色才能访问。可以根据具体需求选择合适的注解进行鉴权。 4. 注册拦截器:在高版本的SpringBoot中(≥2.6.x),需要额外添加`@EnableWebMvc`注解才能使注册拦截器生效。可以在配置类上添加该注解。 通过以上步骤,就可以在SpringBoot中成功整合sa-token,并实现基于注解的鉴权功能。请根据具体需求进行配置和使用。 #### 引用[.reference_title] - *1* [【SaToken使用】SpringBoot整合SaToken(一)token自动续期+token定期刷新+注解鉴权](https://blog.csdn.net/weixin_43165220/article/details/126889045)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [springboot:整合sa-token](https://blog.csdn.net/weixin_43296313/article/details/124274443)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Spring Boot中使用Sa-Token实现轻量级登录与鉴权](https://blog.csdn.net/m0_71777195/article/details/129175616)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值