小记——根据用户动态加载菜单

引用博客:Vue + Spring Boot 项目实战(十五):动态加载后台菜单

不同用户登录后菜单显示不同的实现,需要同时结合前端和后端。
后端主要实现:1. 数据库设计用户可以访问的菜单列表。2. 接收url请求,查询数据库返回允许的菜单列表。
前端主要实现:1. Vuex Store添加菜单数组,保存允许访问的菜单项。2. 配置路由,包括Router入口、利用前置守卫添加菜单项路由。 3. 编写前端界面。

后端:

  1. 数据库设计用户可以访问的菜单列表:
    访问控制采用RBAC,数据库涉及的表包括用户表user、角色表role、菜单表menu、用户-角色映射表user_role、角色-菜单映射表role_menu。
    sql文件链接是:blog.sql
    这5个表的表属性及内容截图:
    user.png

    user_content.png
role.png

role_content.png
menu.png

menu_content.png
user_role.png

user_role_cont.png
role_menu.png

role_menu_cont.png
  1. 后端接收url请求,查询数据库返回允许的菜单列表。
    需要设计menuService以及menuController,当接收"api/menu"请求后,从数据库中查询当前用户可以访问菜单列表,并返回给前端。
    注意:本文使用的ORM框架为Mybatis-Plus,相关CRUD请访问官网:CRUD 接口条件构造器

IMenuService:

public interface IMenuService extends IService<Menu> {
    public List<Menu> getMenuByCurrentUser();
}

menuServiceImpl:
代码逻辑是:先根据当前用户查询对应的角色,再根据角色查询允许的菜单项。

@Transactional
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IMenuService {

    @Autowired
    IUserService userService;
    @Autowired
    IUserRoleService userRoleService;

    @Autowired
    IRoleMenuService roleMenuService;

    @Override
    public List<Menu> getMenuByCurrentUser() {
        // 获取当前用户
        String username = SecurityUtils.getSubject().getPrincipal().toString();
        User user = userService.getUser(username);
        System.out.println("CurrentUser:" + username);
        // 查询UserRole表, 找到用户对应的role id列表
        LambdaQueryWrapper<UserRole> query = Wrappers.<UserRole>lambdaQuery().eq(UserRole::getUid, user.getId());
        List<Integer> rids = userRoleService.list(query).stream().map(UserRole::getRid).collect(Collectors.toList());
        // 找出这些角色对应的菜单项
        LambdaQueryWrapper<RoleMenu> query2 = Wrappers.<RoleMenu>lambdaQuery().in(RoleMenu::getRid, rids);
        List<Integer> menuIds = roleMenuService.list(query2).stream().map(RoleMenu::getMid).collect(Collectors.toList());
        List<Menu> menus = listByIds(menuIds).stream().distinct().collect(Collectors.toList());
        //处理菜单项的结构
        handleMenus(menus);
        return menus;
    }

   
    public void handleMenus(List<Menu> menus) {
        menus.forEach(menu -> {
//            LambdaQueryWrapper<Menu> query = Wrappers.<Menu>lambdaQuery().eq(Menu::getParentId, menu.getId());
//            List<Menu> children = list(query);
            List<Menu> children = menus.stream().filter(m -> m.getParentId() == menu.getId()).collect(Collectors.toList());
            menu.setChildren(children);
        });
      // 只是移除显示上的层次关系,但内部多级层次关系并没有删除
        menus.removeIf(m -> m.getParentId() != 0);
    }
}

menuController:

@RestController
public class MenuController {

    @Autowired
    IMenuService menuService;

    @GetMapping("/api/menu")
    public List<Menu> menu() {
        return menuService.getMenuByCurrentUser();
    }
}

前端:

  1. Vuex Store添加菜单数组,保存允许访问的菜单项。
 export default new Vuex.Store({
  state: {
    user: {
      username: window.localStorage.getItem('user' || '[]') == null ? '' : JSON.parse(window.localStorage.getItem('user' || '[]')).username
    },
  // 新增的用来保存可访问菜单项的数组
    adminMenus: []
  },
  mutations: {
    login (state, user) {
      state.user = {username: user.username}
      window.localStorage.setItem('user', JSON.stringify(user))
    },
    logout (state) {
      state.user = []
      window.localStorage.removeItem('user')
    },
  // 新增的菜单数组驱动
    initMenu (state, menus) {
      state.adminMenus = menus
    }
  }
})
  1. 配置路由,包括Router入口、利用前置守卫添加菜单项路由
    1. 首先配置router下的index.js,新增'/admin'路由,作为展示菜单界面的入口。
      router/index.js:
    {
          path: '/admin',
          name: 'Admin',
          component: AdminIndex,
          meta: {
            requireAuth: true
          }
      }
    
  2. 利用Vue-Router前置守卫,在真正发出url请求之前初始话菜单,包括1. 将后端返回的菜单项path添加到路由,2. 更新store的adminMenus。这部分代码是在main.js中书写。
router.beforeEach((to, from, next) => {
  if (store.state.user.username && to.path.startsWith('/admin')) {
    // console.log('initMenu')
    initMenu(router, store)
  }
  // 已登录状态下访问login直接跳转到后台首页
  if (store.state.user.username && to.path.startsWith('/login')) {
    next({
      path: 'admin/dashboard'
    })
  }
// 登录部分
  if (to.meta.requireAuth) {
    // console.log(store.state.user.username)
    if (store.state.user) {
      axios.get('/authentication')
        .then(resp => {
          if (resp.data) next()
          // resp.data为空代表后端拦截器判断是未认证、未RememberMe,但这时候依然有resp
          else {
            next({
              path: 'login',
              // path后缀, 以path?xxx=yyy附加拼接, redirect代表拼接字符xxx, 可以自定义
              // 该URL=login?redirect=%2Findex
              query: {redirect: to.fullPath}
            })
          }
        })
    } else {
      next({
        path: 'login',
        query: {redirect: to.fullPath}
      })
    }
  } else {
    next()
  }
})
//初始化菜单
const initMenu = (router, store) => {
  if (store.state.adminMenus.length > 0) {
    return
  }
  axios.get('/menu')
    .then(resp => {
      if (resp && resp.status === 200) {
        // 把后端返回的菜单列表进行拼接
        var fmtRoutes = formatRoutes(resp.data)
        // 并添加到Router
        router.addRoutes(fmtRoutes)
        store.commit('initMenu', fmtRoutes)
      }
    })
}
//拼接菜单项路由
const formatRoutes = (routes) => {
  let fmtRoutes = []
  routes.forEach(route => {
    if (route.children) {
      route.children = formatRoutes(route.children)
    }
    let fmtRoute = {
      path: route.path,
      component: resolve => {
        require(['./components/administration/' + route.component + '.vue'], resolve)
      },
      name: route.name,
      nameZh: route.nameZh,
      iconCls: route.iconCls,
      children: route.children
    }
    fmtRoutes.push(fmtRoute)
  })
  return fmtRoutes
}
  1. 编写前端界面。
<template>
    <div>
        <el-menu
                :default-active="currentPath"
                class="el-menu-admin"
                router
                mode="vertical"
                background-color="#545c64"
                text-color="#fff"
                active-text-color="#ffd04b"
                :collapse="isCollapse">
            <div style="height: 80px;"></div>
            <!--index 没有用但是必需字段且为 string -->
            <el-submenu v-for="(item,i) in adminMenus" :key="i" :index="(i).toString()" style="text-align: left">
                    <span slot="title" style="font-size: 17px;">
                        <i :class="item.iconCls"></i>
                        {{ item.nameZh }}
                    </span>
                <el-menu-item v-for="child in item.children" :key="child.path" :index="child.path">
                    <i :class="child.icon"></i>
                    {{ child.nameZh }}
                </el-menu-item>
            </el-submenu>
        </el-menu>
    </div>
</template>

<script>
export default {
  name: 'AdminMenu',
  data () {
    return {
      isCollapse: false
    }
  },
  computed: {
    adminMenus () {
      return this.$store.state.adminMenus
    },
    currentPath () {
      return this.$route.path
    }
  }
}
</script>

<style scoped>
    .el-menu-admin {
        border-radius: 5px;
        height: 100%;
    }
</style>

总结步骤:1. 后端设计数据库。2. 后端设计service和controller,接收请求返回当前用户允许的菜单列表。3. 前端设计Vuex.store,新添菜单数组。4. 前端设计Router,包括router/index.js入口路由、main.js中动态添加菜单项路由。 5. 编写Vue组件AdminMenu.vue

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容