SpringBoot+VUE前后端分离项目学习笔记 - 【22 权限菜单 下】
本节主要实现用户角色菜单分配功能,并且实现前端的动态渲染以及动态路由
数据库: sys_user表增加role字段; sys_role增加flag字段;sys_menu表增加page_path字段。前两者表示用户角色标识,后者是前端页面vue路径,用以实现动态路由
后端: 改写相应实体类,并且在UserDTO添加role和menus,以便于前端获取登录用户的角色以及菜单信息;改写User的login方法:根据用户role信息查询对应Menu菜单【具体流程:User-role > Role-flag > Role-roleid > RoleMenu-menuIds > Menu-ListMenu信息】
前端: 【在登录时在把菜单项变成实际的路由】 User页面增加role选项;Role页面增加flag项,同时设置Admin用户修改菜单后需重新登录的功能;Menu增加PagePath项;Login实现将Menus储存在本地;修改Aside组件实现左侧菜单栏动态渲染;修改router.js实现动态路由;增加404页面
数据库
sys_user表
增加role字段,表示用户角色
CREATE TABLE `sys_user` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
`username` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户名\r\n',
`password` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码',
`nickname` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '昵称',
`email` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '邮编',
`phone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '电话',
`address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地址',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间\r\n',
`avatar_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '头像',
`role` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '角色',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
sys_role表
之前role的唯一标识id是数字类型,无法直观地来辨别角色含义,现在添加flag字段作为角色的唯一标识
CREATE TABLE `sys_role` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '名称',
`description` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '描述',
`flag` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '唯一标识',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
sys_user表的role字段与 sys_role表的flag字段是对应的
sys_menu表
增加page_path字段用以保存菜单对应的页面路径【就是view里对应vue名称】实现前端动态路由
CREATE TABLE `sys_menu` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '名称',
`path` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '路径',
`icon` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '图标',
`description` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '描述',
`pid` int DEFAULT NULL COMMENT '父级id',
`page_path` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '页面路径',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
后台代码
整体结构
Role.java
Role、User、UserDTO实体类加入相应字段
package com.zj.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
/**
* <p>
*
* </p>
*
* @author Joyce
* @since 2023-01-10
*/
@Getter
@Setter
@TableName("sys_role")
@ApiModel(value = "Role对象", description = "")
public class Role implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty("名称")
private String name;
@ApiModelProperty("描述")
private String description;
@ApiModelProperty("唯一标识")
private String flag;
}
User.java
package com.zj.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
*
* </p>
*
* @author Joyce
* @since 2023-01-04
*/
@Getter
@Setter
@TableName("sys_user")
@ApiModel(value = "User对象", description = "")
@ToString
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("密码")
private String password;
@ApiModelProperty("昵称")
private String nickname;
@ApiModelProperty("邮编")
private String email;
@ApiModelProperty("电话")
private String phone;
@ApiModelProperty("地址")
private String address;
@ApiModelProperty("创建时间 ")
private Date createTime;
@ApiModelProperty("头像")
private String avatarUrl;
@ApiModelProperty("角色")
private String role;
}
UserDTO.java
UserDTO加入菜单信息menus和用户角色信息role【这个类是前端注册传回的用户信息】
package com.zj.demo.controller.dao;
import com.zj.demo.entity.Menu;
import lombok.Data;
import java.util.List;
/**
* 接受前端登录请求的参数
*/
@Data
public class UserDTO {
private String username;
private String password;
private String nickname;
private String avatarUrl;
private String token;
private String role;
private List<Menu> menus;
}
Menu.java
加入Page_path页面路径字段
package com.zj.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 io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.List;
/**
* <p>
*
* </p>
*
* @author Joyce
* @since 2023-01-10
*/
@Getter
@Setter
@TableName("sys_menu")
@ApiModel(value = "Menu对象", description = "")
public class Menu implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty("名称")
private String name;
@ApiModelProperty("路径")
private String path;
@ApiModelProperty("图标")
private String icon;
@ApiModelProperty("描述")
private String description;
@TableField(exist = false)
private List<Menu> children;
@ApiModelProperty("父级id")
private Integer pid;
@ApiModelProperty("页面路径")
private String pagePath;
}
Common/RoleEnum.java
新建枚举类型,将角色标识加入系统常量成为规范【也可以直接在Constants里声明】
package com.zj.demo.common;
public enum RoleEnum {
ROLE_ADMIN, ROLE_USER;
}
UserController.java
在登录接口login(),调用userService.login(userDTO)方法,把用户的角色以及菜单信息返回, 在用户管理的地方加入角色
package com.zj.demo.controller;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.poi.excel.ExcelReader;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.zj.demo.common.Constants;
import com.zj.demo.common.Result;
import com.zj.demo.controller.dao.UserDTO;
import com.zj.demo.entity.User;
import com.zj.demo.service.IUserService;
import com.zj.demo.utils.TokenUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author Joyce
* @since 2023-01-04
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
/**
* 登录界面
*/
@PostMapping("/login")
public Result login(@RequestBody UserDTO userDTO)
{
String username = userDTO.getUsername();
String password = userDTO.getPassword();
//判断用户名和密码是否为空[HUTOOL工具包]
if(StrUtil.isBlank(username) || StrUtil.isBlank(password))
{
return Result.error(Constants.CODE_400,"参数错误");
}
UserDTO dto = userService.login(userDTO);
return Result.success(dto);
}
@PostMapping("/register")
public Result register(@RequestBody UserDTO userDTO)
{
String username = userDTO.getUsername();
String password = userDTO.getPassword();
//判断用户名和密码是否为空[HUTOOL工具包]
if(StrUtil.isBlank(username) || StrUtil.isBlank(password))
{
return Result.error(Constants.CODE_400,"参数错误");
}
User dto = userService.register(userDTO);
return Result.success(dto);
}
// 新增或者更新
@PostMapping
public Result save(@RequestBody User user) {
return Result.success(userService.saveOrUpdate(user));
}
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) {
return Result.success(userService.removeById(id));
}
@PostMapping("/del/batch")
public Result deleteBatch(@RequestBody List<Integer> ids) {
return Result.success(userService.removeByIds(ids));
}
@GetMapping
public Result findAll() {
return Result.success(userService.list());
}
@GetMapping("/{id}")
public Result findOne(@PathVariable Integer id) {
return Result.success(userService.getById(id));
}
@GetMapping("/page")
public Result findPage(@RequestParam Integer pageNum,
@RequestParam Integer pageSize,
@RequestParam(defaultValue ="") String username,
@RequestParam(defaultValue ="") String email,
@RequestParam(defaultValue ="") String address
) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (!"".equals(username)) {
queryWrapper.like("username", username);
}
if (!"".equals(email)) {
queryWrapper.like("nickname", email);
}
//和上面的if判断效果一样
queryWrapper.like(!address.equals(""),"address", address);
queryWrapper.orderByDesc("id");
// 获取当前用户信息
User currentUser = TokenUtils.getCurrentUser();
System.out.println("获取当前用户信息=========================" + currentUser.getNickname());
return Result.success(userService.page(new Page<>(pageNum, pageSize), queryWrapper));
}
/**
* 导出接口
*/
@GetMapping("/export")
public void export(HttpServletResponse response) throws Exception {
// 从数据库查询出所有的数据
List<User> list = userService.list();
// 通过工具类创建writer 写出到磁盘路径
// ExcelWriter writer = ExcelUtil.getWriter(filesUploadPath + "/用户信息.xlsx");
// 在内存操作,写出到浏览器
ExcelWriter writer = ExcelUtil.getWriter(true);
//自定义标题别名,不设置则使用实体类字段名
writer.addHeaderAlias("username", "用户名");
writer.addHeaderAlias("password", "密码");
writer.addHeaderAlias("nickname", "昵称");
writer.addHeaderAlias("email", "邮箱");
writer.addHeaderAlias("phone", "电话");
writer.addHeaderAlias("address", "地址");
writer.addHeaderAlias("createTime", "创建时间");
writer.addHeaderAlias("avatarUrl", "头像");
// 一次性写出list内的对象到excel,使用默认样式,强制输出标题
writer.write(list, true);
// 设置浏览器响应的格式
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
String fileName = URLEncoder.encode("用户信息", "UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
ServletOutputStream out = response.getOutputStream();
writer.flush(out, true);
out.close();
writer.close();
}
/**
* excel 导入
* @param file
* @throws Exception
*/
@PostMapping("/import")
public Result imp(MultipartFile file) throws Exception {
InputStream inputStream = file.getInputStream();
ExcelReader reader = ExcelUtil.getReader(inputStream);
// 方式1:(推荐) 通过 javabean的方式读取Excel内的对象,但是要求表头必须是英文,跟javabean的属性要对应起来
// List<User> list = reader.readAll(User.class);
// 方式2:忽略表头的中文,直接读取表的内容
List<List<Object>> list = reader.read(1);
List<User> users = CollUtil.newArrayList();
for (List<Object> row : list) {
User user = new User();
user.setUsername(row.get(0).toString());
user.setPassword(row.get(1).toString());
user.setNickname(row.get(2).toString());
user.setEmail(row.get(3).toString());
user.setPhone(row.get(4).toString());
user.setAddress(row.get(5).toString());
user.setAvatarUrl(row.get(6).toString());
users.add(user);
}
userService.saveBatch(users);
return Result.success(true);
}
@GetMapping("/username/{username}")
public Result findOne(@PathVariable String username) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
return Result.success(userService.getOne(queryWrapper));
}
}
UserServiceImpl.java
login的具体方法在这里实现, 之前设置了用户的token,这次需要根据用户的role信息查询对应Menu菜单
User的role信息 对应Role的flag标识
RoleMenu储存的是roleId与menuID的绑定关系
所以应按照以下流程进行
1、根据User-role==Role-flag获取Role的id【在RoleMapper编写selectByflag方法】
2、再根据Role-id在RoleMenu表获取menuIds【调用RoleMenuMapper里的selectByRoleId】
3、根据menuIds获取Menu信息【在】
package com.zj.demo.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.log.Log;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zj.demo.common.Constants;
import com.zj.demo.controller.dao.UserDTO;
import com.zj.demo.entity.Menu;
import com.zj.demo.entity.User;
import com.zj.demo.exception.ServiceException;
import com.zj.demo.mapper.RoleMapper;
import com.zj.demo.mapper.RoleMenuMapper;
import com.zj.demo.mapper.UserMapper;
import com.zj.demo.service.IMenuService;
import com.zj.demo.service.IUserService;
import com.zj.demo.utils.TokenUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* 服务实现类
* </p>
*
* @author Joyce
* @since 2023-01-04
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
private static final Log LOG = Log.get();
@Resource
private RoleMapper roleMapper;
@Resource
private RoleMenuMapper roleMenuMapper;
@Resource
private IMenuService menuService;
@Override
public UserDTO login(UserDTO userDTO) {
User one = getUserInfo(userDTO);
if (one != null) {
BeanUtils.copyProperties(one, userDTO);
//设置Token
String token = TokenUtils.genToken(one.getId().toString(),one.getPassword());
userDTO.setToken(token);
//获取角色的唯一标识ROLE_ADMIN、对应的roleId,对应的菜单menuIds
String roleFlag = one.getRole();
//获取menuIds相应的树级菜单信息
List<Menu> roleMenus = getRoleMenus(roleFlag);
//设置用户菜单列表
userDTO.setMenus(roleMenus);
return userDTO;
} else {
throw new ServiceException(Constants.CODE_600, "用户名或密码错误");
}
}
@Override
public User register(UserDTO userDTO) {
User one = getUserInfo(userDTO);
if (one == null) {
one = new User();
BeanUtil.copyProperties(userDTO, one, true);
save(one); // 把 copy完之后的用户对象存储到数据库
} else {
throw new ServiceException(Constants.CODE_600, "用户已存在");
}
return one;
}
private User getUserInfo(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) {
LOG.error(e);
throw new ServiceException(Constants.CODE_500, "系统错误");
}
return one;
}
/**
* 根据roleFlag标识获取当前角色的菜单列表
* @param roleFlag
* @return
*/
private List<Menu> getRoleMenus(String roleFlag){
//获取角色的唯一标识ROLE_ADMIN对应的roleId,对应的菜单menuIds
Integer roleId = roleMapper.selectByflag(roleFlag);
List<Integer> menuIds = roleMenuMapper.selectByRoleId(roleId);
//查出系统所有的菜单【返回的是树形结构,menus是父级菜单的集合】
List<Menu> menus = menuService.findMenus("");
//筛选当前用户角色拥有的菜单
List<Menu> roleMenus = new ArrayList<>();
//从父级菜单开始筛选,如果父级菜单的id在menuIds中,那么就把父级菜单添加到roleMenus中
for(Menu menu:menus){
if(menuIds.contains(menu.getId())){
roleMenus.add(menu);
}
//获取父级菜单的子菜单,筛选在menuIds中子菜单的id
List<Menu> children = menu.getChildren();
//removeIf()方法是java8的新特性,用于删除集合中满足条件的元素
//移除children里面不在menuIds集合中的元素
children.removeIf(child -> !menuIds.contains(child.getId()));
}
return roleMenus;
}
}
RoleMapper.java
package com.zj.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zj.demo.entity.Role;
import org.apache.ibatis.annotations.Select;
/**
* <p>
* Mapper 接口
* </p>
*
* @author Joyce
* @since 2023-01-10
*/
public interface RoleMapper extends BaseMapper<Role> {
@Select("select * from sys_role where flag = #{flag}")
Integer selectByflag(String role);
}
MenuController.java
改写原有的findAll,将其封装到MenuService里,以提供给其他Service使用
package com.zj.demo.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.zj.demo.common.Constants;
import com.zj.demo.common.Result;
import com.zj.demo.entity.Dict;
import com.zj.demo.entity.Menu;
import com.zj.demo.mapper.DictMapper;
import com.zj.demo.service.IMenuService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author Joyce
* @since 2023-01-10
*/
@RestController
@RequestMapping("/menu")
public class MenuController {
@Resource
private IMenuService menuService;
@Resource
private DictMapper dictMapper;
// 新增或者更新
@PostMapping
public Result save(@RequestBody Menu menu) {
return Result.success(menuService.saveOrUpdate(menu));
}
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) {
return Result.success(menuService.removeById(id));
}
@PostMapping("/del/batch")
public Result deleteBatch(@RequestBody List<Integer> ids) {
return Result.success(menuService.removeByIds(ids));
}
@GetMapping
public Result findAll(@RequestParam(defaultValue ="") String name) {
return Result.success(menuService.findMenus(name));
}
@GetMapping("/{id}")
public Result findOne(@PathVariable Integer id) {
return Result.success(menuService.getById(id));
}
@GetMapping("/page")
public Result findPage(@RequestParam(defaultValue ="") String name,
@RequestParam Integer pageNum,
@RequestParam Integer pageSize) {
QueryWrapper<Menu> queryWrapper = new QueryWrapper<>();
if (!"".equals(name)) {
queryWrapper.like("name", name);
}
queryWrapper.orderByDesc("id");
return Result.success(menuService.page(new Page<>(pageNum, pageSize), queryWrapper));
}
@GetMapping("/icons")
public Result getIcons() {
QueryWrapper<Dict> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("type", Constants.DICT_TYPE_ICON);
return Result.success(dictMapper.selectList(queryWrapper));
}
}
IMenuService.java
package com.zj.demo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.zj.demo.entity.Menu;
import java.util.List;
/**
* <p>
* 服务类
* </p>
*
* @author Joyce
* @since 2023-01-10
*/
public interface IMenuService extends IService<Menu> {
List<Menu> findMenus(String name);
}
MenuServiceImpl.java
package com.zj.demo.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zj.demo.entity.Menu;
import com.zj.demo.mapper.MenuMapper;
import com.zj.demo.service.IMenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* <p>
* 服务实现类
* </p>
*
* @author Joyce
* @since 2023-01-10
*/
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IMenuService {
@Override
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());
//找出一级菜单的子菜单
for(Menu menu:parentNodes){
//筛选所有数据中pid父级id的数据 作为二级菜单
menu.setChildren(list.stream().filter(m -> menu.getId().equals(m.getPid())).collect(Collectors.toList()));
}
return parentNodes;
}
}
前端代码
把菜单项变成实际的路由,左侧的菜单项动态渲染,router里实现动态路由
整体结构
整体结构
## User.vue 用户页面 向后台请求角色信息,加入角色选择的选项 ```js## Role.vue
角色页面 加入唯一标识flag,在saveRoleMenu方法里加入 操作管理员角色后需要重新登录
```js
<template>
<div>
<div style="margin: 10px 0">
<el-input style="width: 200px" placeholder="请输入名称" suffix-icon="el-icon-search" v-model="name"></el-input>
<el-button class="ml-5" type="primary" @click="load">搜索</el-button>
<el-button type="warning" @click="reset">重置</el-button>
</div>
<div style="margin: 10px 0">
<el-button type="primary" @click="handleAdd">新增 <i class="el-icon-circle-plus-outline"></i></el-button>
<el-popconfirm
class="ml-5"
confirm-button-text='确定'
cancel-button-text='我再想想'
icon="el-icon-info"
icon-color="red"
title="您确定批量删除这些数据吗?"
@confirm="delBatch"
>
<el-button type="danger" slot="reference">批量删除 <i class="el-icon-remove-outline"></i></el-button>
</el-popconfirm>
<!-- <el-upload action="http://localhost:9090/role/import" :show-file-list="false" accept="xlsx" :on-success="handleExcelImportSuccess" style="display: inline-block">-->
<!-- <el-button type="primary" class="ml-5">导入 <i class="el-icon-bottom"></i></el-button>-->
<!-- </el-upload>-->
<!-- <el-button type="primary" @click="exp" class="ml-5">导出 <i class="el-icon-top"></i></el-button>-->
</div>
<el-table :data="tableData" border stripe :header-cell-class-name="'headerBg'" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="ID"></el-table-column>
<el-table-column prop="name" label="名称"></el-table-column>
<el-table-column prop="description" label="描述"></el-table-column>
<el-table-column label="操作" width="280" align="center">
<template slot-scope="scope">
<el-button type="info" @click="selectMenu(scope.row)">分配菜单<i class="el-icon-menu"></i></el-button>
<el-button type="success" @click="handleEdit(scope.row)">编辑 <i class="el-icon-edit"></i></el-button>
<el-popconfirm
class="ml-5"
confirm-button-text='确定'
cancel-button-text='我再想想'
icon="el-icon-info"
icon-color="red"
title="您确定删除吗?"
@confirm="del(scope.row.id)"
>
<el-button type="danger" slot="reference">删除 <i class="el-icon-remove-outline"></i></el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div style="padding: 10px 0">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-sizes="[2, 5, 10, 20]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
<el-dialog title="角色信息" :visible.sync="dialogFormVisible" width="30%" >
<el-form label-width="80px" size="small">
<el-form-item label="名称">
<el-input v-model="form.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="唯一标识">
<el-input v-model="form.flag" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="form.description" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="save">确 定</el-button>
</div>
</el-dialog>
<el-dialog title="菜单分配" :visible.sync="menuDialogVis" width="30%">
<el-tree
:props="props"
:data="menuData"
show-checkbox
node-key="id"
ref="tree"
:check-strictly="true"
:default-expanded-keys="expends"
:default-checked-keys="checks">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span><i :class="data.icon"></i> {{ data.name }}</span>
</span>
</el-tree>
<div slot="footer" class="dialog-footer">
<el-button @click="menuDialogVis = false">取 消</el-button>
<el-button type="primary" @click="saveRoleMenu">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: "Role",
data() {
return {
tableData: [],
total: 0,
pageNum: 1,
pageSize: 10,
name: "",
description: "",
form: {},
dialogFormVisible: false,
menuDialogVis: false,
multipleSelection: [],
menuData: [],
props:{
label:'name',
},
expends: [],
checks: [],
roleId: 0,
roleFlag: '',
ids: []
}
},
created() {
this.load()
},
methods: {
load() {
this.request.get("/role/page", {
params: {
pageNum: this.pageNum,
pageSize: this.pageSize,
name: this.name,
}
}).then(res => {
console.log(res)
this.tableData = res.data.records
this.total = res.data.total
})
},
save() {
this.request.post("/role", this.form).then(res => {
if (res.code === '200') {
this.$message.success("保存成功")
this.dialogFormVisible = false
this.load()
} else {
this.$message.error("保存失败")
}
})
},
del(id) {
this.request.delete("/role/" + id).then(res => {
if (res.code === '200') {
this.$message.success("删除成功")
this.load()
} else {
this.$message.error("删除失败")
}
})
},
delBatch() {
let ids = this.multipleSelection.map(v => v.id) // [{}, {}, {}] => [1,2,3]
this.request.post("/role/del/batch", ids).then(res => {
if (res.code === '200') {
this.$message.success("批量删除成功")
this.load()
} else {
this.$message.error("批量删除失败")
}
})
},
handleAdd() {
this.dialogFormVisible = true
this.form = {}
},
handleEdit(row) {
this.form = row
this.dialogFormVisible = true
},
handleSelectionChange(val) {
console.log(val)
this.multipleSelection = val
},
reset() {
this.name = ""
this.load()
},
handleSizeChange(pageSize) {
console.log(pageSize)
this.pageSize = pageSize
this.load()
},
handleCurrentChange(pageNum) {
console.log(pageNum)
this.pageNum = pageNum
this.load()
},
saveRoleMenu() {
this.request.post("/role/roleMenu/" + this.roleId, this.$refs.tree.getCheckedKeys()).then(res => {
if (res.code === '200') {
this.$message.success("绑定成功")
this.menuDialogVis = false
// 操作管理员角色后需要重新登录
if (this.roleFlag === 'ROLE_ADMIN') {
this.$store.commit("logout")
}
} else {
this.$message.error(res.msg)
}
})
},
async selectMenu(role) {
this.roleId = role.id
this.roleFlag = role.flag
// 请求菜单数据
this.request.get("/menu").then(res => {
this.menuData = res.data
// 把后台返回的菜单数据处理成 id数组
this.expends = this.menuData.map(v => v.id)
})
this.request.get("/role/roleMenu/" + this.roleId).then(res => {
this.checks = res.data
this.ids.forEach(id => {
if (!this.checks.includes(id)) {
// 可能会报错:Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'setChecked')
this.$nextTick(() => {
this.$refs.tree.setChecked(id, false)
})
}
})
this.menuDialogVis = true
})
},
}
}
</script>
<style>
.headerBg {
background: #eee!important;
}
</style>
Login.vue
防止只有登录之后才能获得menus,在login页面单独把menus信息储存在localStorage,然后调用router里的setRoutes方法实现动态路由
<template>
<div class="wrapper">
<div style="margin: 200px auto; background-color: #fff; width: 350px; height: 300px; padding: 20px; border-radius: 10px">
<div style="margin: 20px 0; text-align: center; font-size: 24px"><b>登 录</b></div>
<el-form :model="user" :rules="rules" ref="userForm">
<el-form-item prop="username">
<el-input size="medium" style="margin: 10px 0" prefix-icon="el-icon-user" v-model="user.username"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input size="medium" style="margin: 10px 0" prefix-icon="el-icon-lock" show-password v-model="user.password"></el-input>
</el-form-item>
<el-form-item style="margin: 10px 0; text-align: right">
<el-button type="warning" size="small" autocomplete="off" @click="$router.push('/register')">注册</el-button>
<el-button type="primary" size="small" autocomplete="off" @click="login">登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
import {setRoutes} from "@/router";
export default {
name: "Login",
data() {
return {
user: {},
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 10, message: '长度在 3 到 5 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' }
],
}
}
},
methods: {
login() {
this.$refs['userForm'].validate((valid) => {
if (valid) { // 表单校验合法
this.request.post("/user/login", this.user).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("/")
this.$message.success("登录成功")
} else {
this.$message.error(res.msg)
}
})
}
});
}
}
}
</script>
<style>
.wrapper {
height: 100vh;
background-image: linear-gradient(to bottom right, #FC466B , #3F5EFB);
overflow: hidden;
}
</style>
Aside.vue
获取localStorage里的用户菜单信息
<template>
<el-menu :default-openeds="opens" style="min-height: 100%; overflow-x: hidden"
background-color="rgb(48, 65, 86)"
text-color="#fff"
active-text-color="#ffd04b"
:collapse-transition="false"
:collapse="isCollapse"
router
>
<div style="height: 60px; line-height: 60px; text-align: center">
<img src="../assets/logo.png" alt="" style="width: 20px; position: relative; top: 5px; right: 5px">
<b style="color: white" v-show="logoTextShow">后台管理系统</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">
<div v-if="subItem.path">
<el-menu-item :index="subItem.path">
<i class="subItem.icon"></i>
<span slot="title">{{ subItem.name }}</span>
</el-menu-item>
</div>
</div>
</el-submenu>
</div>
</div>
</el-menu>
</template>
<script>
export default {
name: "Aside",
props: {
isCollapse: Boolean,
logoTextShow: Boolean
},
data() {
return {
menus: localStorage.getItem("menus") ? JSON.parse(localStorage.getItem("menus")) : [],
opens: localStorage.getItem("menus") ? JSON.parse(localStorage.getItem("menus")).map(v => v.id + '') : []
}
},
}
</script>
<style scoped>
</style>
Menu.vue
增加页面路径pagePath选项
<template>
<div>
<div style="margin: 10px 0">
<el-input style="width: 200px" placeholder="请输入名称" suffix-icon="el-icon-search" v-model="name"></el-input>
<el-button class="ml-5" type="primary" @click="load">搜索</el-button>
<el-button type="warning" @click="reset">重置</el-button>
</div>
<div style="margin: 10px 0">
<el-button type="primary" @click="handleAdd1">新增 <i class="el-icon-circle-plus-outline"></i></el-button>
<el-popconfirm
class="ml-5"
confirm-button-text='确定'
cancel-button-text='我再想想'
icon="el-icon-info"
icon-color="red"
title="您确定批量删除这些数据吗?"
@confirm="delBatch"
>
<el-button type="danger" slot="reference">批量删除 <i class="el-icon-remove-outline"></i></el-button>
</el-popconfirm>
</div>
<el-table :data="tableData" border stripe :header-cell-class-name="'headerBg'"
row-key="id" default-expand-all @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="ID"></el-table-column>
<el-table-column prop="name" label="名称"></el-table-column>
<el-table-column prop="path" label="路径"></el-table-column>
<el-table-column prop="pagePath" label="页面路径"></el-table-column>
<el-table-column label="图标" class-name="fontSize16" align="center" label-class-name="fontSize12">
<template slot-scope="scope">
<span :class="scope.row.icon" />
</template>
</el-table-column>
<el-table-column prop="description" label="描述"></el-table-column>
<el-table-column label="操作" width="300" align="center">
<template slot-scope="scope">
<el-button type="primary" @click="handleAdd(scope.row.id)" v-if="!scope.row.pid && !scope.row.path">新增子菜单 <i class="el-icon-plus"></i></el-button>
<el-button type="success" @click="handleEdit(scope.row)">编辑 <i class="el-icon-edit"></i></el-button>
<el-popconfirm
class="ml-5"
confirm-button-text='确定'
cancel-button-text='我再想想'
icon="el-icon-info"
icon-color="red"
title="您确定删除吗?"
@confirm="del(scope.row.id)"
>
<el-button type="danger" slot="reference">删除 <i class="el-icon-remove-outline"></i></el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-dialog title="菜单信息" :visible.sync="dialogFormVisible" width="30%" >
<el-form label-width="80px" size="small">
<el-form-item label="名称">
<el-input v-model="form.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="路径">
<el-input v-model="form.path" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="页面路径">
<el-input v-model="form.pagePath" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-select clearable v-model="form.icon" placeholder="请选择" style="width: 100%">
<el-option v-for="item in options" :key="item.name" :label="item.name" :value="item.value">
<i :class="item.value"/> {{ item.name }}
</el-option>
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="form.description" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="save">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: "Menu",
data() {
return {
tableData: [],
total: 0,
pageNum: 1,
pageSize: 10,
name: "",
path: "",
icon: "",
description: "",
form: {},
dialogFormVisible: false,
multipleSelection: [],
options:[]
}
},
created() {
this.load()
},
methods: {
load() {
this.request.get("/menu", {
params: {
name: this.name,
}
}).then(res => {
this.tableData = res.data
})
},
save() {
this.request.post("/menu", this.form).then(res => {
if (res.code === '200') {
this.$message.success("保存成功")
this.dialogFormVisible = false
this.load()
} else {
this.$message.error("保存失败")
}
})
},
del(id) {
this.request.delete("/menu/" + id).then(res => {
if (res.code === '200') {
this.$message.success("删除成功")
this.load()
} else {
this.$message.error("删除失败")
}
})
},
delBatch() {
let ids = this.multipleSelection.map(v => v.id) // [{}, {}, {}] => [1,2,3]
this.request.post("/menu/del/batch", ids).then(res => {
if (res.code === '200') {
this.$message.success("批量删除成功")
this.load()
} else {
this.$message.error("批量删除失败")
}
})
},
handleAdd1() {
this.dialogFormVisible = true
this.form = {}
},
handleAdd(pid) {
this.dialogFormVisible = true
this.form = {}
if(pid)
{
this.form.pid = pid
}
},
handleEdit(row) {
this.form = row
this.dialogFormVisible = true
// 请求图标的数据
this.request.get("/menu/icons").then(res => {
this.options = res.data
})
},
handleSelectionChange(val) {
console.log(val)
this.multipleSelection = val
},
reset() {
this.name = ""
this.load()
},
handleSizeChange(pageSize) {
console.log(pageSize)
this.pageSize = pageSize
this.load()
},
handleCurrentChange(pageNum) {
console.log(pageNum)
this.pageNum = pageNum
this.load()
},
}
}
</script>
<style>
.headerBg {
background: #eee!important;
}
.fontSize16{
font-size: 16px;
}
.fontSize12{
font-size: 12px;
}
</style>
router/index.js
动态渲染完了,需要实现动态路由
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from "@/store";
//import store from "../store"
Vue.use(VueRouter)
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue')
},
{
path: '/register',
name: 'Register',
component: () => import('../views/Register.vue')
},
{
path: '/404',
name: '404',
component: () => import('../views/404.vue')
},
]
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 currentRouteNames = router.getRoutes().map(v => v.name)
if (!currentRouteNames.includes('Manage')) {
// 拼装动态路由
const manageRoute = { path: '/', name: 'Manage', component: () => import('../views/Manage.vue'), redirect: "/home", 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)
}
}
}
}
// 重置我就再set一次路由
setRoutes()
// 路由守卫
router.beforeEach((to, from, next) => {
localStorage.setItem("currentPathName", to.name) // 设置当前的路由名称,为了在Header组件中去使用
store.commit("setPath") // 触发store的数据更新
next()
})
export default router
404.vue
跳转到错误路由,返回404页面
<template>
<div style="overflow: hidden; height: 100vh">
<img src="../assets/404.png" alt="" style="width: 100%; height: 100%">
</div>
</template>
<script>
export default {
name: "NotFound"
}
</script>
<style>
.bgImg {
background: url("../assets/404.png") no-repeat;
background-size: 100% 100vh;
}
</style>
store/index.js
写一个重置方法
import Vue from 'vue'
import Vuex from 'vuex'
import router, {resetRouter} from "@/router";
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
currentPathName: ''
},
mutations: {
setPath (state) {
state.currentPathName = localStorage.getItem("currentPathName")
},
logout() {
// 清空缓存
localStorage.removeItem("user")
localStorage.removeItem("menus")
router.push("/login")
// 重置路由
resetRouter()
}
}
})
export default store
header.vue
相应退出方法重写
<template>
<div style="line-height: 60px; display: flex">
<div style="flex: 1;">
<span :class="collapseBtnClass" style="cursor: pointer; font-size: 18px" @click="collapse"></span>
<el-breadcrumb separator="/" style="display: inline-block; margin-left: 10px">
<el-breadcrumb-item :to="'/'">
首页</el-breadcrumb-item>
<el-breadcrumb-item>{{ currentPathName }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<el-dropdown style="width: 150px; cursor: pointer;text-align: right">
<div style="display: inline-block">
<img :src="user.avatarUrl" alt=""
style="width: 30px; height: 30px; border-radius: 50%; position: relative; top:8px; right: 8px">
<span>{{user.nickname}}</span><i class="el-icon-arrow-down" style="margin-left: 5px"></i>
</div>
<el-dropdown-menu slot="dropdown" style="width: 100px; text-align: center">
<el-dropdown-item style="font-size: 14px; padding: 5px 0">
<router-link to="/person">个人信息</router-link>
</el-dropdown-item>
<el-dropdown-item style="font-size: 14px; padding: 5px 0">
<span style="text-decoration: none" @click="logout">退出</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script>
export default {
name: "Header",
props: {
collapseBtnClass: String,
user: Object
},
computed: {
currentPathName () {
return this.$store.state.currentPathName; //需要监听的数据
}
},
data() {
return {
}
},
methods: {
collapse() {
// this.$parent.$parent.$parent.$parent.collapse() // 通过4个 $parent 找到父组件,从而调用其折叠方法
this.$emit("asideCollapse")
},
logout() {
this.$store.commit("logout")
this.$message.success("退出成功")
}
}
}
</script>
<style scoped>
</style>
测试页面
User页面显示Role信息
登录后获取用户菜单列表信息
以普通用户user1为例【即前面在sys_user里设置了role为ROLE_USER的数据】,进行界面测试
获取到了menu数据
Aside依据LocalStorage里的Menus信息动态渲染
在菜单管理里设置页面路径
动态路由设置好后,使用普通用户user1登录,访问role页面,测试结果为空白页【成功】
访问错误路由时返回404页面
重新分配菜单后,再登陆一次可以刷新菜单页面
对管理员类型用户进行菜单分配后,需要重新登陆设置才生效【普通用户不用重新登陆】
其他补充
MySQL数据库删除数据后自增ID不连续问题的解决方法
情况一、还没有出现不连贯的数据ID
删除完还没有新增数据,即还没有出现不连贯的数据ID时,执行以下语句:
ALTERTABLE 表名AUTO_INCREMENT=1;
情况二、已知下一条数据自增的ID
如果已知下一条数据自增的ID(假设是10),可以直接写成
ALTER TABLE 表名 AUTO_INCREMENT = 10;
这样再插入数据时,自增ID会从10开始,也可以用这个语法来跳过一些编号。
此外,如果AUTO_INCREMENT 的值小于ID的最大值,那么ID是从MAX(ID)+1开始自增,所以当AUTO_INCREMENT = 1时,一般默认ID是从最大值加一开始自增的。
情况三、已经出现不连贯的数据ID
表中已经出现不连贯的数据ID时,执行以下语句进行修改
SET@auto_id=0;UPDATE 表名SET 自增字段名=(@auto_id :=@auto_id+1); ALTERTABLE
表名AUTO_INCREMENT=1;
情况四、需要清空表的数据
如果需要清空表的数据的话,最好使用TRUNCATE 来删除
TRUNCATE TABLE 表名
这样新增的数据自增ID会从1开始,如果使用DELETE来删除,新增的数据会沿着之前的ID进行自增。如果使用的数据库管理软件是Navicat,那可以选中表右键选择截断表,其效果和TRUNCATE的效果是一样的。