(十六)VUE权限菜单之动态路由--基于SpringBoot+MySQL+Vue+ElementUI+Mybatis前后端分离面向小白管理系统搭建

任务十四 VUE权限菜单之菜单管理

任务十五 VUE权限菜单之角色管理

任务十六 VUE权限菜单之动态路由

在任务十四中完成了权限菜单的菜单管理,主要实现了一级、二级等菜单的添加。任务十五中完成了权限菜单的角色管理,主要实现了角色增删改查已经为角色分配权限菜单。这些基本内容完成之后,就需要做本次任务的内容,即根据登录用户的角色进行动态路由分配,通过本次任务,大家能够:
(1)理解用户认证、授权的概念;
(2)学会登录验证以及基于网页的HTML5浏览器存储localStorage,存储用户信息和用户授权菜单;
(3)掌握动态路由设计;
(4)学会登录login基本方法。
首先梳理一下整个逻辑;
(1)用户登录需要做两件事,一件事是验证用户的合法性,设置TOKEN,然后将信息进行浏览器存储;另一件事是得到这个合法用户的role(也就是角色标识,如ROLE_ADMIN),根据这个角色标识获取到角色ID,根据角色ID获取权限菜单。
(2)获取到权限菜单后,只是一个列表,在确保确实是menu表中的数据之外,还需要与menu表中数据进行匹配,对每一个menuid判断出它是一级菜单还是二级菜单;
(3)将权限菜单数据同样进行浏览器存储;
(4)用户验证合法、获取授权菜单后登录,进入主页面,原先固定的Aside组件需要根据动态存储的menus值重新设置。

涉及到的实体类

表结构为:
在这里插入图片描述
具体数据表的讲解请参考任务十五。
为了方便期间,如果只做这一个任务实现登录并能动态路由分配,把涉及到的实体类再罗列出,如果是跟着前面的任务在做,就不需要重复做了。

1. 用户实体类User

package com.example.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.sun.javafx.beans.IDProperty;
import lombok.Data;

import java.sql.Date;


@Data
//可以使用 @TableName 表名注解指定当前实体类对应的表名,比如下面 User 实体类对应表名为 sys_user
@TableName(value="sys_user")
public class User {
    //可以使用 @TableId 注解(标注在主键上)和 @TableField 注解(标注在其他成员属性上)来指定对应的字段名
    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    private String username;
    private String password;
    private String email;
    private String phone;
    private String nickname;
    private String address;
    @TableField(value="created_time")//这样处理的主要目的是java对带有下划线的字段不识别,所以改为驼峰形式
    private Date createdTime;//如果需要年月日格式的可以使用Date类型,如果需要具体到时分秒就使用String类型
    private String avatar;
    private String role;
}

2. 菜单实体类Menu

package com.example.demo.entity;


import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.util.List;

@Data
//可以使用 @TableName 表名注解指定当前实体类对应的表名,比如下面 Menu 实体类对应表名为 sys_menu
@TableName(value="sys_menu")
public class Menu {

    //可以使用 @TableId 注解(标注在主键上)和 @TableField 注解(标注在其他成员属性上)来指定对应的字段名
    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    private String name;
    private String path;
    private String icon;
    private String description;

    //在数据表中没有children这个字段,这个在做菜单的时候会用到,所以使用exist=false忽略
    @TableField(exist = false)
    private List<Menu> children;

    private Integer pid;
    @TableField(value="page_path")//这样处理的主要目的是java对带有下划线的字段不识别,所以改为驼峰形式
    private String pagePath;
}

3.角色实体类 Role

package com.example.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.sql.Date;

@Data
//可以使用 @TableName 表名注解指定当前实体类对应的表名,比如下面 Role 实体类对应表名为 sys_role
@TableName(value="sys_role")
public class Role {
    //可以使用 @TableId 注解(标注在主键上)和 @TableField 注解(标注在其他成员属性上)来指定对应的字段名
    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    private String name;
    private String description;
    private String flag;
}

4. 角色菜单实体类RoleMenu

package com.example.demo.entity;


import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
//可以使用 @TableName 表名注解指定当前实体类对应的表名,比如下面 RoleMenu 实体类对应表名为 sys_role_menu
@TableName(value="sys_role_menu")
public class RoleMenu {
    private Integer roleId;
    private Integer menuId;
}

5. 登录用户类UserDTO

package com.example.demo.controller.dto;

import com.example.demo.entity.Menu;
import lombok.Data;

import java.util.List;


//UserDTO用来接受前端登录时传递的用户名和密码
@Data
public class UserDTO {
    private String username;
    private String password;
    private String nickname;
    private String avatar;
    private String token;
    //把当前登录用户的角色以及他的菜单项带出来
    private String role;
    private List<Menu> menus;
}

一、后端登录接口

1. 修改userService类中的login方法

基于任务十三 JWT+SpringSecurity实现基于Token的登录修改userService类中添加login方法。

package com.example.demo.service;

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.common.Constants;
import com.example.demo.controller.dto.UserDTO;
import com.example.demo.entity.Menu;
import com.example.demo.entity.User;
import com.example.demo.exception.ServiceException;
import com.example.demo.mapper.RoleMapper;
import com.example.demo.mapper.RoleMenuMapper;
import com.example.demo.mapper.UserMapper;
import com.example.demo.utils.TokenUtils;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@Service
public class UserService extends ServiceImpl<UserMapper,User>{
    private UserMapper userMapper;
    @Resource
    private RoleMenuMapper roleMenuMapper;
    @Resource
    private RoleMapper roleMapper;
    @Resource
    private MenuService menuService;
   
    public Boolean saveUser(User user) {       
       return  saveOrUpdate(user);
    }

    public UserDTO login(UserDTO userDTO) {
        QueryWrapper<User> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("username",userDTO.getUsername());
        queryWrapper.eq("password",userDTO.getPassword());
        User one;
        try{
            one=getOne(queryWrapper);
        }catch (Exception e){
            throw new ServiceException(Constants.CODE_500,"系统错误");//这里假设查询了多于1条记录,就让他报系统错误
        }
        if(one!=null){  //以下是登录判断业务
            BeanUtil.copyProperties(one,userDTO,true);
            //设置token
            String token=TokenUtils.genToken(one.getId().toString(),one.getPassword().toString());
            userDTO.setToken(token);
            String role=one.getRole();//查询出用户的角色标识,比如ROLE_ADMIN
            //设置用户的菜单列表
            List<Menu> roleMenus=getRoleMenus(role);
            userDTO.setMenus(roleMenus);
            return userDTO;
        }else {
            throw new ServiceException(Constants.CODE_600,"用户名或密码错误");
        }
    }

    /**
     * 获取当前用户的菜单列表

     */
    private List<Menu> getRoleMenus(String roleFlag){
        //根据角色标识获取角色Id
        Integer roleId=roleMapper.selectByflag(roleFlag);
        //当前角色Id的所有菜单id集合
        List<Integer> menuIds=roleMenuMapper.selectByRoleId(roleId);
        //查出系统所有菜单
        List<Menu> menus=menuService.findMenus("");
        //筛选当前用户菜单
        List<Menu> roleMenus=new ArrayList<>();
        for(Menu menu:menus){
            if(menuIds.contains(menu.getId())){
                roleMenus.add(menu);
            }
            List<Menu> children=menu.getChildren();
            //removeIf移除children里面不在menuIds集合中的元素
            children.removeIf(child->!menuIds.contains(child.getId()));
        }
        return roleMenus;
    }
}

其中涉及到的方法在任务十四和任务十五中均有,如果前面没有做的童鞋,可以在前面的任务中找到。
roleMapper.selectByflag(roleFlag);在任务十五有详细讲解,完整代码为:

package com.example.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.Role;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

public interface RoleMapper extends BaseMapper<Role> {
    //根据角色唯一标识flag查找角色id
    @Select("select id from sys_role where flag=#{flag}")
    Integer selectByflag(@Param("flag") String role);
}

roleMenuMapper.selectByRoleId(roleId);在任务十五有详细讲解,完整代码为:

package com.example.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.RoleMenu;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface RoleMenuMapper  extends BaseMapper<RoleMenu> {
    //根据角色id删除角色菜单数据
    @Delete("delete from sys_role_menu where role_id=#{roleId}")
    int deleteByRoleId(@Param("roleId") Integer roleId);

    //根据角色id查找菜单id
    @Select("select menu_Id from sys_role_menu where role_id=#{roleId}")
    List<Integer> selectByRoleId(@Param("roleId") Integer roleId);
}

menuService.findMenus(“”);在任务十四有详细讲解,完整代码为:

package com.example.demo.service;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
importcom.example.demo.entity.Menu;;
import com.example.demo.mapper.MenuMapper;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class MenuService extends ServiceImpl<MenuMapper, Menu> {
    public List<Menu> findMenus(String name) {
        QueryWrapper<Menu> queryWrapper=new QueryWrapper<>();
        if(StrUtil.isNotBlank(name)){
            queryWrapper.like("name",name);
        }
        List<Menu> list = list(queryWrapper);
        // 找出pid为null的一级菜单
        List<Menu> parentNodes=list.stream().filter(menu -> menu.getPid()==null).collect(Collectors.toList());
        //找出一级菜单为null的二级菜单放到Children中
        for(Menu menu:parentNodes){
            menu.setChildren(list.stream().filter(m->menu.getId().equals(m.getPid())).collect(Collectors.toList()));
        }
        System.out.println(parentNodes);
        return parentNodes;
    }
}

二、前端登录页面

login.vue完整代码

这个页面与任务十三登录页面没有太多大的改动,因为那里已经实现了基于Token的认证,并且保存了用户信息。
继续添加一个获取菜单的接口即可。
完整代码如下:

<template>
 <div class="login_container">
    <div class="login_box">
      <div style="margin:20px 0; text-align:center; font-size:24px"><b>登录</b></div>
      <!-- 用户名-->
      <el-form ref="LoginFormRef" :model="loginForm" :rules="LoginFormRules" >
        <el-form-item prop="username">
          <el-input size="medium" style="margin:10px 0px;width: 300px;margin-left:25px" v-model="loginForm.username" prefix-icon="el-icon-user"></el-input>
        </el-form-item>
      <!-- 密码-->
         <el-form-item prop="password">
           <el-input size="medium" style="margin:10px 0px;width: 300px;margin-left:25px" show-password v-model="loginForm.password" prefix-icon="el-icon-lock" type="password"></el-input>
        </el-form-item>
        <div style="margin:10px 0; text-align:center">
          <el-button type="primary" size="small" @click="login" >登录</el-button>
          <el-button type="warning" size="small" @click="resetLoginForm">重置</el-button>
        </div> 
      </el-form> 
    </div>
  </div>
</template>

<script>
import {setRoutes} from "@/router";
import {resetRouter} from "@/router";
  export default {
    name: "Login",
    data() {
      return {
        loginForm: {
          username:'',
          password:''
        },
        LoginFormRules:{
          username:[
            { required: true, message: '请输入用户名', trigger: 'blur' },
          ],
          password:[
            { required: true, message: '请输入密码', trigger: 'blur' },
          ]
        }
      }
    },
    methods:{
      login(){
        this.$refs['LoginFormRef'].validate(async (valid) => {
          if (valid) {
            this.request.post("http://localhost:8084/user/login",this.loginForm).then(res=>{
              if(res.code=='200'){
                localStorage.setItem("user",JSON.stringify(res.data));//存储用户信息到浏览器
                localStorage.setItem("menus",JSON.stringify(res.data.menus));//存储用户权限菜单信息到浏览器
                //动态设置当前用户的路由
                setRoutes()
                this.$router.push("/home");
                this.$message.success("登录成功");
              }else{
                this.$message.error(res.msg);
              }
            })
          }
          
        })
      },
      resetLoginForm(){
        this.$refs.LoginFormRef.resetFields()
      }
    }    
  }
</script>

<style scoped>
  .login_container{
    background-color: #2b4b6b;
    height: 100%;
  }

  .login_box{
    width: 350px;
    height: 300px;
    background-color: #fff;
    border-radius: 3px;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%)
  }
</style>>

在这里插入图片描述
特别注意:这里调用了 setRoutes(),进行动态设置当前用户的路由

三、动态路由index.js

1. 修改router里的index.js

在这里插入图片描述
完整代码如下:

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/Login.vue'

Vue.use(VueRouter)

const routes = [ 
  {
    path: '/login',
    name: 'Login',
    component: Login
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

// 提供一个重置路由的方法
export const resetRouter=()=>{
  router.matcher=new VueRouter({
    mode: 'history',
    base: process.env.BASE_URL,
    routes
  })
}
//刷新页面会重置路由
export const setRoutes=()=>{
  const storeMenus=localStorage.getItem("menus");
  if(storeMenus){
    //拼装动态路由
    const manageRoute={ path: '/', name:'Manage', component: () => import('../views/Manage.vue'),redirect: '/login', children: []}
    const menus=JSON.parse(storeMenus)
    menus.forEach(item=>{
      if(item.path){//当且仅当path不为空的时候才去设置路由
        let itemMenu={ path: item.path.replace("/",""),name: item.name,component: () => import('../views/'+item.pagePath+'.vue')}
        manageRoute.children.push(itemMenu)
      }else if(item.children.length){
        item.children.forEach(item=>{
          if(item.path){
            let itemMenu={ path: item.path.replace("/",""),name: item.name,component: () => import('../views/'+item.pagePath+'.vue')}
            manageRoute.children.push(itemMenu)
          }
        })
      }
    })  
    //获取当前的路由对象名称数组
    const currentRouteNames=router.getRoutes().map(v=>v.name)
    if(!currentRouteNames.includes('Manage')){
      //动态加载到想在的路由对象
    router.addRoute(manageRoute)
    }    
  }
}
setRoutes()
export default router

2. 修改Aside组件

任务十VUE侧边菜单栏导航 中我们修改过VUE侧边菜单栏导航,但那时候还是固定的。现在根据动态路由生成侧边菜单栏。
完整代码如下:

<template>
  <el-menu :default-openeds="opens" style="min-height:100%; overflow-x:hidden"
    background-color=rgb(48,65,86)
    text-color=#ccc
    active-text-color=red
    router=""
    >
    <div style="height:60px; line-height:60px; text-align:center">
    <img src="../assets/logo.png" style="width:20px;position:relative;top:5px;margin-right:5px"/>
    <b style="color:white">后台管理系统</b>
    </div>
    <div v-for="item in menus" :key="item.id">
      <div v-if="item.path">
          <el-menu-item :index="item.path">               
              <i :class="item.icon"></i>
              <span slot="title">{
   
   {item.name}}</span>                 
          </el-menu-item>
      </div>
      <div v-else>
         <el-submenu :index="item.id+'' ">
          <template slot="title">
            <i :class="item.icon"></i>
            <span slot="title">{
   
   {item.name}}</span>
          </template>
          <div v-for="subItem in item.children" :key="subItem.id">
             <el-menu-item :index="subItem.path">
              <i :class="subItem.icon"></i>
              <span slot="title">{
   
   {subItem.name}}</span>
            </el-menu-item>
          </div>
        </el-submenu>
      </div>
    </div>    
  </el-menu>
</template>

<script>
export default {
  name: "Aside",
  props:{

  },
  data(){
    return{
      menus:localStorage.getItem("menus")?JSON.parse(localStorage.getItem("menus")):[],  
      opens:localStorage.getItem("menus")?JSON.parse(localStorage.getItem("menus")).map(v=>v.id+''):[],
    }
  },
  methods:{
    showmst(){
      console(this.opens)
    }
  }
}
</script>

<style scoped>
</style>

理解 的重点就是根据已存储的menus动态部署侧栏。

四、vue全局状态管理

对于一些全局变量或者方法,可以进行全局设置,然后在任何时候都可以直接使用。比如常用的登出logout方法等。
具体方法为:

1.安装vuex

npm install vuex --save

安装完成后注意观察一个vuex的版本。
vuex分3.x版本和4.x版本,分别对应vue2.0与3.0。
这里用的是VUE2.0,所以需要vuex是3.x版本。具体安装方法也可以到网上查找。
在这里插入图片描述

2. 在src下新建文件夹store,并在文件夹中新建文件index.js

在这里插入图片描述
index.js完整代码如下:

import Vue from 'vue'
import Vuex from 'vuex'
import router from '@/router'

Vue.use(Vuex)

const store=new Vuex.Store({
    state:{
        currntPathName:''
    },
    mutations:{
        logout(){
            localStorage.removeItem("user")
            localStorage.removeItem("menus")
            router.push("/login")
        }
    }
})
export default store

3. 在main.js中引用,并添加到组件中

新增代码段如下:

import store from './store/index'
 
new Vue({
    router,
    store,//定义成全局变量
    render: h => h(App)
}).$mount('#app')

在这里插入图片描述

4.引用

比如在任务十五中当判断给管理员重新分配了权限,就需要重新登录,这时候先登出,就调用store中的logout方法。
在这里插入图片描述

五、运行项目

1. 登录

在这里插入图片描述

2.菜单管理-新增一级菜单

在这里插入图片描述

3.菜单管理-新增两个子菜单

在这里插入图片描述

4.角色管理

为管理员角色分配菜单
在这里插入图片描述
因为是给管理员重新分配权限菜单,所以点击“确定”后需要重新登录。

5.新增一个Test1.vue组件

在这里插入图片描述
代码为:

<template>
    <div>
        <h1>测试一个新路由</h1>
    </div>
</template>

6.保存重新运行登录

在这里插入图片描述

任务总结

本次任务完成后,整个项目将根据不同用户部署不同菜单,实现动态路由,而且,只要有新的页面生成,只要添加到菜单中即可,就可以实现动态访问。
通过本次任务,大家能够:
(1)理解动态路由的概念;
(2)VUE对整个框架的管理机制有所了解;
(3)学会VUE 状态管理定义与使用。
下一个任务将使用Echart做一个简单Home页面。

猜你喜欢

转载自blog.csdn.net/wdyan297/article/details/128759654