新手做毕设-后台管理系统
任务十四 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方法。主要修改的思路:用户登录后,根据用户获取到角色标识。然后根据角色标识获取到角色id,由角色id获取菜单列表。获取菜单列表的时候注意一级菜单和二级菜单。
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)
}
}
}
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页面。