SpringBoot3+Vue3 前后端分离项目实现基于RBAC权限访问控制-(1)权限管理
SpringBoot3+Vue3 前后端分离项目实现基于RBAC权限访问控制-(2)角色管理
1、RBAC-基于角色访问控制
权限授权给角色,角色分配给用户。即,用户拥有了该角色下的权限。
2、数据库设计
-- 用户表
CREATE TABLE `user` (
`id` varchar(40) NOT NULL COMMENT '主键',
`username` varchar(40) DEFAULT NULL COMMENT '用户名',
`password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '密码',
`name` varchar(40) DEFAULT NULL COMMENT '姓名',
`email` varchar(40) DEFAULT NULL COMMENT '邮箱',
`avatar_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '头像地址',
`creator_id` varchar(40) DEFAULT NULL COMMENT '创建人ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`ts` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_flag` int NOT NULL DEFAULT '0' COMMENT '逻辑删除字段 0.正常 1.删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
-- 用户-角色关联表
CREATE TABLE `user_role` (
`id` varchar(40) NOT NULL COMMENT '主键',
`user_id` varchar(40) NOT NULL COMMENT '用户ID',
`role_id` varchar(40) NOT NULL COMMENT '角色ID',
`creator_id` varchar(40) DEFAULT NULL COMMENT '创建人ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`ts` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_flag` int NOT NULL DEFAULT '0' COMMENT '逻辑删除字段 0.正常 1.删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色表';
-- 角色表
CREATE TABLE `role` (
`id` varchar(40) NOT NULL COMMENT '主键',
`role_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '角色名称',
`role` varchar(40) DEFAULT NULL COMMENT '角色',
`creator_id` varchar(40) DEFAULT NULL COMMENT '创建人ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`ts` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_flag` int NOT NULL DEFAULT '0' COMMENT '逻辑删除字段 0.正常 1.删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色表';
-- 角色-权限关联表
CREATE TABLE `role_menu` (
`id` varchar(40) NOT NULL COMMENT '主键',
`role_id` varchar(40) NOT NULL COMMENT '角色ID',
`menu_id` varchar(40) NOT NULL COMMENT '菜单ID',
`creator_id` varchar(40) DEFAULT NULL COMMENT '创建人ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`ts` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_flag` int NOT NULL DEFAULT '0' COMMENT '逻辑删除字段 0.正常 1.删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色菜单表';
-- 权限表
CREATE TABLE `menu` (
`id` varchar(40) NOT NULL COMMENT '主键',
`parent_id` varchar(40) DEFAULT NULL COMMENT '父ID',
`menu_name` varchar(40) DEFAULT NULL COMMENT '菜单名',
`type` int DEFAULT NULL COMMENT '类型(1:目录,2:菜单,3:按钮)',
`icon` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '图标',
`path` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '前端路由',
`auth` varchar(128) DEFAULT NULL COMMENT '权限',
`sort` int DEFAULT NULL COMMENT '排序',
`status` int DEFAULT '1' COMMENT '状态(0:禁用,1:启用)',
`creator_id` varchar(40) DEFAULT NULL COMMENT '创建人ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`ts` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_flag` int NOT NULL DEFAULT '0' COMMENT '逻辑删除字段 0.正常 1.删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='菜单表';
3、SpringBoot3 后端实现菜单管理
3.1 Maven
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.dragon</groupId>
<artifactId>springboot3-vue3</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot3-vue3</name>
<description>springboot3-vue3</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
<!-- mybatis-plus-boot-starter 中 mybatis-spring 版本不够,排除之后引入新版本 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.6</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<!-- mybatis-plus 多表查询 -->
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join-boot-starter</artifactId>
<version>1.4.12</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- jaxb-api 处理 xml 数据 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.4.0-b180830.0359</version>
</dependency>
<!-- fastjson2 处理 json 数据 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.48</version>
</dependency>
<!-- 使用 springdoc 生成 swagger 文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
<!-- 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.6</version>
</dependency>
<!-- 模板引擎 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
<!-- Sa-Token -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.38.0</version>
</dependency>
<!-- Sa-Token 集成 jwt -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>1.38.0</version>
</dependency>
<!-- Sa-Token 集成 redis, 使用 jackson 序列化 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.38.0</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- Java 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.28</version>
</dependency>
<!-- 导入导出 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.2 Entity-实体类
package com.dragon.springboot3vue3.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 菜单表
* </p>
*/
@Data
@Schema(name = "Menu", description = "菜单表")
public class Menu implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键")
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@Schema(description = "父ID")
private String parentId;
@Schema(description = "菜单名")
private String menuName;
@Schema(description = "类型")
private Integer type;
@Schema(description = "图标")
private String icon;
@Schema(description = "路径")
private String path;
@Schema(description = "权限")
private String auth;
@Schema(description = "排序")
private int sort=99;
@Schema(description = "状态(0:禁用,1:启用)")
private Integer status;
@Schema(description = "创建人ID")
@TableField(fill = FieldFill.INSERT)
private String creatorId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime ts;
@Schema(description = "逻辑删除字段(0:正常,1:删除)")
@TableLogic
private Integer deleteFlag;
}
3.3 Dto
3.3.1 MenuDto
package com.dragon.springboot3vue3.controller.dto.entityDto;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Data
public class MenuDto {
@Schema(description = "主键")
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@Schema(description = "父ID")
private String parentId;
@Schema(description = "菜单名")
private String menuName;
@Schema(description = "类型")
private Integer type;
@Schema(description = "图标")
private String icon;
@Schema(description = "路径")
private String path;
@Schema(description = "权限")
private String auth;
@Schema(description = "排序")
private int sort=99;
@Schema(description = "状态(0:禁用,1:启用)")
private Integer status;
@Schema(description = "创建人ID")
@TableField(fill = FieldFill.INSERT)
private String creatorId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime ts;
@Schema(description = "逻辑删除字段 0.正常 1.删除")
private Integer deleteFlag;
@Schema(description = "菜单列表")
private List<MenuDto> children=new ArrayList<>();
}
3.3.2 MenuPageDto
package com.dragon.springboot3vue3.controller.dto.pageDto;
import com.dragon.springboot3vue3.utils.PageDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class MenuPageDto extends PageDTO {
@Schema(description = "菜单名称")
private String menuName;
@Schema(description = "权限名称")
private String auth;
}
3.3.3 PageDTO
package com.dragon.springboot3vue3.utils;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
@Schema(description = "基础分页")
public class PageDTO {
@NotNull
@Schema(description = "当前页码")
public Long currentPage;
@NotNull
@Schema(description = "每页记录数")
public Long pageSize;
}
3.4 MenuController
package com.dragon.springboot3vue3.controller;
import cn.dev33.satoken.util.SaResult;
import com.dragon.springboot3vue3.controller.dto.pageDto.MenuPageDto;
import com.dragon.springboot3vue3.entity.Menu;
import com.dragon.springboot3vue3.service.IMenuService;
import com.dragon.springboot3vue3.utils.StringIdsDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* <p>
* 菜单表 前端控制器
* </p>
*/
@Tag(name = "菜单接口")
@RestController
@RequestMapping("/menu")
public class MenuController {
@Autowired
private IMenuService menuService;
@Operation(summary = "登录用户的菜单列表")
@GetMapping("getMyMenu")
public SaResult getMyMenu(){
return SaResult.ok().setData(menuService.getMyMenu());
}
@Operation(summary = "登录用户的路由列表")
@GetMapping("getMyRoute")
public SaResult getMyRoute(){
return SaResult.ok().setData(menuService.getMyRoute());
}
@Operation(summary = "所有菜单层级列表")
@GetMapping("getAllMenu")
public SaResult getAllMenu(){
return SaResult.ok().setData(menuService.getAllMenu());
}
@Operation(summary = "分页列表")
@PostMapping("/list")
public SaResult list(@RequestBody MenuPageDto pageDto){
return SaResult.ok().setData(menuService.list(pageDto));
}
@Operation(summary = "新增或更新")
@PostMapping("/saveOrUpdate")
public SaResult saveOrUpdate(@RequestBody @Validated Menu menuDto){
Menu menu=new Menu();
BeanUtils.copyProperties(menuDto,menu);
menuService.saveOrUpdate(menu);
return SaResult.ok();
}
@Operation(summary = "删除")
@DeleteMapping("/remove")
public SaResult remove(@RequestBody @Validated StringIdsDTO stringIdsDTO){
menuService.removeByIds(stringIdsDTO.getIds());
return SaResult.ok();
}
}
3.5 IMenuService
package com.dragon.springboot3vue3.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.dragon.springboot3vue3.controller.dto.entityDto.MenuDto;
import com.dragon.springboot3vue3.controller.dto.pageDto.MenuPageDto;
import com.dragon.springboot3vue3.entity.Menu;
import java.util.List;
/**
* <p>
* 菜单表 服务类
* </p>
*/
public interface IMenuService extends IService<Menu> {
List<MenuDto> getMyMenu();
List<Menu> getMyRoute();
List<MenuDto> getAllMenu();
Page<MenuDto> list(MenuPageDto pageDto);
}
3.6 MenuServiceImpl
package com.dragon.springboot3vue3.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dragon.springboot3vue3.controller.dto.entityDto.MenuDto;
import com.dragon.springboot3vue3.controller.dto.pageDto.MenuPageDto;
import com.dragon.springboot3vue3.entity.Menu;
import com.dragon.springboot3vue3.entity.RoleMenu;
import com.dragon.springboot3vue3.entity.UserRole;
import com.dragon.springboot3vue3.mapper.MenuMapper;
import com.dragon.springboot3vue3.service.IMenuService;
import com.dragon.springboot3vue3.service.IRoleMenuService;
import com.dragon.springboot3vue3.service.IUserRoleService;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* <p>
* 菜单表 服务实现类
* </p>
*
* 获取菜单分级逻辑:先获取一级菜单,再往一级菜单存其所有子菜单
*
*/
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IMenuService {
@Autowired
private IUserRoleService userRoleService;
@Autowired
private IRoleMenuService roleMenuService;
/**
* 登录用户的菜单列表
* @return
*/
public List<MenuDto> getMyMenu(){
List<UserRole> list = userRoleService.lambdaQuery().eq(UserRole::getUserId, StpUtil.getLoginIdAsString()).list();
List<String> roleIds=list.stream().map(UserRole::getRoleId).toList();
List<RoleMenu> list1 = roleMenuService.lambdaQuery().in(RoleMenu::getRoleId, roleIds).list();
List<String> menuIds = list1.stream().distinct().map(RoleMenu::getMenuId).toList();
List<Menu> menuList=this.lambdaQuery().in(Menu::getId,menuIds).orderByAsc(Menu::getSort).list();
return getMenuList(menuList);
}
/**
* 登录用户的路由列表
* @return
*/
public List<Menu> getMyRoute(){
List<UserRole> list = userRoleService.lambdaQuery().eq(UserRole::getUserId, StpUtil.getLoginIdAsString()).list();
List<String> roleIds=list.stream().map(UserRole::getRoleId).toList();
List<RoleMenu> list1 = roleMenuService.lambdaQuery().in(RoleMenu::getRoleId, roleIds).list();
List<String> menuIds = list1.stream().distinct().map(RoleMenu::getMenuId).toList();
List<Menu> menuList=this.lambdaQuery().in(Menu::getId,menuIds).list();
return getRouteList(menuList);
}
/**
* 获取所有菜单层级列表
* @return
*/
@Override
public List<MenuDto> getAllMenu() {
// 所有菜单
List<Menu> allList = this.list();
return getMenuList(allList);
}
/**
* 分页列表
*/
@Override
public Page<MenuDto> list(MenuPageDto pageDto) {
// 创建分页对象
Page<MenuDto> page=new Page<>(pageDto.getCurrentPage(),pageDto.getPageSize());
Page<MenuDto> response = baseMapper.selectJoinPage(page,MenuDto.class,new MPJLambdaWrapper<>());
// 重新封装分页数据
List<MenuDto> menuList = getMenuList(this.list());
response.setRecords(menuList);
response.setTotal(menuList.size());
return response;
}
/**
* 获取 menuList 菜单层级列表
*/
private List<MenuDto> getMenuList(List<Menu> menuList){
List<MenuDto> menuDtoList=new ArrayList<>();
// menuList 中一级菜单列表(要在所有一级菜单存其所有子菜单)
List<Menu> list = menuList.stream().sorted((Comparator.comparing(Menu::getSort))).filter(item -> item.getParentId().equals("0")).toList();
// 一级菜单 id 列表
List<String> parentIds = menuList.stream().map(Menu::getId).toList();
// menuList 中所有一级菜单 Menu 转换 MenuDto
list.forEach(item->{
MenuDto menuDto=new MenuDto();
BeanUtils.copyProperties(item,menuDto);
menuDtoList.add(menuDto);
});
// 遍历 menuList 中一级菜单列表,存储父id在 parentIds 的菜单即为其子菜单
menuDtoList.forEach(item->{
List<MenuDto> childrenList = recursive(item.getId(),parentIds);
item.setChildren(childrenList);
});
return menuDtoList;
}
/**
* 根据 parentId 递归,获取 父id 在 parentIds 内的菜单,即为其子菜单
* @return
*/
private List<MenuDto> recursive(String parentId,List<String> parentIds){
List<MenuDto> childrenList=new ArrayList<>();
// 所有菜单
List<Menu> menuList=this.lambdaQuery().orderByAsc(Menu::getSort).list();
menuList.forEach(item->{
// 遍历所有菜单,筛选 parentId 的子菜单,并且子菜单类型不是按钮(item.getType()!=3),并且子菜单id包含在menuIds内
if(item.getParentId().equals(parentId) && item.getType()!=3 && parentIds.contains(item.getId())){
// 将 menu 转为 menuDto
MenuDto menuDto=new MenuDto();
BeanUtils.copyProperties(item,menuDto);
childrenList.add(menuDto);
}
});
// 递归查询 childrenList 子节点(遍历每个子节点,以每个子节点为 parentId 查询其子节点,直到没有子节点)
childrenList.forEach(item->{
item.setChildren(recursive(item.getId(),parentIds));
});
return childrenList;
}
/**
* 获取路由列表
*/
private List<Menu> getRouteList(List<Menu> menuList){
List<Menu> routeList=new ArrayList<>();
// menuList 中所有一级菜单(要在所有一级菜单存其所有子菜单)
List<Menu> list = menuList.stream().filter(item -> item.getParentId().equals("0")).toList();
// 遍历一级菜单
list.forEach(item->{
if(item.getType()==2){
routeList.add(item);
}
});
// 遍历 menuList 中所有一级菜单,获取所有子菜单
list.forEach(item->{
routeList.addAll(routeRecursive(item.getId()));
});
return routeList;
}
/**
* 根据 parentId 递归 获取所有子节点
* @return
*/
private List<Menu> routeRecursive(String parentId){
List<Menu> routeList=new ArrayList<>();
// 所有菜单
List<Menu> menuList=this.list();
// 遍历 item.getParentId().equals(parentId) 的数据
menuList.forEach(item->{
// item.getType()!=3 过滤按钮
if(item.getParentId().equals(parentId) && item.getType()!=3){
routeList.add(item);
}
});
// 递归查询 childrenList 子节点(遍历每个子节点,以每个子节点为 parentId 查询其子节点,直到没有子节点)
routeList.forEach(item->{
routeRecursive(item.getId());
});
return routeList;
}
}
3.7 Swagger 请求后端数据
{
"code": 200,
"msg": "ok",
"data": {
"records": [
{
"id": "1",
"parentId": "0",
"menuName": "首页",
"type": 2,
"icon": "HomeFilled",
"path": "/home/index",
"auth": "",
"sort": 99,
"status": 1,
"creatorId": null,
"createTime": "2024-05-13 09:43:42",
"ts": "2024-06-07 10:15:49",
"deleteFlag": 0,
"children": []
},
{
"id": "3",
"parentId": "0",
"menuName": "文章管理",
"type": 1,
"icon": "Management",
"path": "",
"auth": "",
"sort": 100,
"status": 1,
"creatorId": null,
"createTime": "2024-05-13 09:46:16",
"ts": "2024-06-13 16:02:31",
"deleteFlag": 0,
"children": [
{
"id": "4",
"parentId": "3",
"menuName": "文章",
"type": 2,
"icon": "Notebook",
"path": "/article/index",
"auth": "article.list",
"sort": 99,
"status": 1,
"creatorId": null,
"createTime": "2024-05-14 17:51:30",
"ts": "2024-06-07 10:16:05",
"deleteFlag": 0,
"children": []
},
{
"id": "7",
"parentId": "3",
"menuName": "文章分类",
"type": 2,
"icon": "Collection",
"path": "/article/category/index",
"auth": "article.category.list",
"sort": 99,
"status": 1,
"creatorId": null,
"createTime": "2024-05-14 18:59:39",
"ts": "2024-06-07 10:16:08",
"deleteFlag": 0,
"children": []
}
]
},
{
"id": "2",
"parentId": "0",
"menuName": "系统管理",
"type": 1,
"icon": "Setting",
"path": null,
"auth": "",
"sort": 101,
"status": 1,
"creatorId": null,
"createTime": "2024-05-13 09:44:38",
"ts": "2024-06-07 10:18:24",
"deleteFlag": 0,
"children": [
{
"id": "16",
"parentId": "2",
"menuName": "菜单管理",
"type": 2,
"icon": "Menu",
"path": "/sys/menu/index",
"auth": "sys.menu.list",
"sort": 99,
"status": 1,
"creatorId": null,
"createTime": "2024-05-21 09:22:05",
"ts": "2024-06-07 10:15:59",
"deleteFlag": 0,
"children": []
},
{
"id": "10",
"parentId": "2",
"menuName": "用户管理",
"type": 2,
"icon": "UserFilled",
"path": "/sys/user/index",
"auth": "sys.user.list",
"sort": 100,
"status": 1,
"creatorId": null,
"createTime": "2024-05-21 09:14:19",
"ts": "2024-06-07 10:34:17",
"deleteFlag": 0,
"children": []
},
{
"id": "13",
"parentId": "2",
"menuName": "角色管理",
"type": 2,
"icon": "User",
"path": "/sys/role/index",
"auth": "sys.role.list",
"sort": 101,
"status": 1,
"creatorId": null,
"createTime": "2024-05-21 09:19:01",
"ts": "2024-06-07 11:12:04",
"deleteFlag": 0,
"children": []
},
{
"id": "4d4d9ea9fb38520109b904008d42c158",
"parentId": "2",
"menuName": "文件管理",
"type": 2,
"icon": "Folder",
"path": "/sys/files/index",
"auth": "sys.files.list",
"sort": 102,
"status": 1,
"creatorId": "1",
"createTime": "2024-06-13 16:03:24",
"ts": "2024-06-14 16:07:10",
"deleteFlag": 0,
"children": []
},
{
"id": "125ae0e59d0d00d8ef4fc5f735557e0c",
"parentId": "2",
"menuName": "图标管理",
"type": 2,
"icon": "Files",
"path": "/sys/icon/index",
"auth": "sys.icon.list",
"sort": 103,
"status": 1,
"creatorId": "1",
"createTime": "2024-05-28 17:02:18",
"ts": "2024-06-14 16:07:33",
"deleteFlag": 0,
"children": []
}
]
},
{
"id": "19",
"parentId": "0",
"menuName": "个人中心",
"type": 1,
"icon": "UserFilled",
"path": null,
"auth": null,
"sort": 102,
"status": 1,
"creatorId": null,
"createTime": "2024-05-25 17:00:07",
"ts": "2024-06-07 10:18:27",
"deleteFlag": 0,
"children": [
{
"id": "155aa550a6d10c94932d83f5e1f42d9c",
"parentId": "19",
"menuName": "个人信息",
"type": 2,
"icon": "User",
"path": "/user/Info",
"auth": "user.info",
"sort": 99,
"status": 1,
"creatorId": "1",
"createTime": "2024-05-25 17:02:39",
"ts": "2024-06-07 10:15:58",
"deleteFlag": 0,
"children": []
},
{
"id": "9626a79c1e689e01e100ad5cf6a265c6",
"parentId": "19",
"menuName": "重置密码",
"type": 2,
"icon": "Switch",
"path": "/user/ResetPassword",
"auth": "user.ResetPassword",
"sort": 99,
"status": 1,
"creatorId": "1",
"createTime": "2024-05-25 17:03:34",
"ts": "2024-06-07 10:16:12",
"deleteFlag": 0,
"children": []
}
]
}
],
"total": 4,
"size": 10,
"current": 1,
"pages": 1
}
}
4、Vue3 前端实现菜单管理
4.1 前端效果
4.2 Vue3 代码
4.2.1 api -> menu.ts(相关接口)
import request from "@/utils/request";
// 菜单管理 API
export default{
menuList(value:any){
return request.post('/menu/list',value);
},
saveOrUpdate(value:any){
return request.post('/menu/saveOrUpdate',value);
},
remove(ids:any){
return request.delete('/menu/remove',{ data:{ ids } });
},
getMyMenu(){
return request.get('/menu/getMyMenu');
},
getMyRoute(){
return request.get('/menu/getMyRoute');
}
}
4.2.2 sys -> menu - > index.vue
<template>
<el-card class="container">
<template #header>
<div class="header">
<el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item :to="{ path: '/home/index' }" class="title">首页</el-breadcrumb-item>
<el-breadcrumb-item class="title">菜单管理</el-breadcrumb-item>
</el-breadcrumb>
<div>
<el-button type="primary" @click="addButton">新增菜单</el-button>
<el-button type="danger" @click="batchRemove">批量删除</el-button>
</div>
</div>
</template>
<!-- 搜索表单 -->
<el-form inline>
<el-form-item label="菜单名称">
<el-input v-model="searchModel.menuName" placeholder="请输入菜单名称" style="width: 150px" clearable></el-input>
</el-form-item>
<el-form-item label="权限名称">
<el-input v-model="searchModel.auth" placeholder="请输入权限名称" style="width: 150px" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getMenuList">搜索</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- 列表 -->
<!-- row-key="id" table 显示树形结构数据 -->
<el-table :data="menuList" border stripe style="width: 100%" height="550" row-key="id" default-expand-all @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" />
<el-table-column label="菜单名称" width="230" prop="menuName"></el-table-column>
<el-table-column label="图标" width="60">
<template #default="{ row }">
<component :is="getIconComponent(row.icon)" style="height: 15px;" />
</template>
</el-table-column>
<el-table-column label="类型" prop="type" width="60">
<template #default="{ row }">
<span v-if="row.type==1">目录</span>
<span v-if="row.type==2">菜单</span>
<span v-if="row.type==3">按钮</span>
</template>
</el-table-column>
<el-table-column label="前端路由" prop="path"></el-table-column>
<el-table-column label="权限标识" prop="auth"></el-table-column>
<el-table-column label="排序" prop="sort"></el-table-column>
<el-table-column label="是否启用" prop="status">
<template #default="{ row }">
<el-switch v-model="row.status" :active-value="1" :inactive-value="0" @change="change(row)" />
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime"></el-table-column>
<el-table-column label="更新时间" prop="ts"> </el-table-column>
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button :icon="Edit" circle plain type="primary" @click="edit(row)"></el-button>
<el-button :icon="Delete" circle plain type="danger" @click="remove(row)"></el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="没有数据" />
</template>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="searchModel.currentPage"
v-model:page-size="searchModel.pageSize"
:page-sizes="[10, 30, 50, 100]"
layout="jumper, total, sizes, prev, pager, next"
:total="searchModel.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
style="margin: 10px 0; justify-content: flex-end"
/>
<!-- 新增和修改的弹窗 -->
<el-dialog v-model="dialogVisible" width="50%" center @close="close">
<template #header>
<h1>{{ title }}</h1>
</template>
<el-form :model="menuModel" ref="menuModelRef" label-width="120px" :rules="rules">
<el-row>
<el-col :span="12">
<el-form-item label="上级菜单" prop="parentId">
<el-tree-select v-model="menuModel.parentId" :data="menuSelectData" check-strictly :render-after-expand="false" style="max-height: 200px;" :props="defaultProps" default-expand-all placeholder="请选择上级菜单"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="菜单类型" prop="type">
<el-select v-model="menuModel.type" placeholder="请选择菜单类型">
<el-option v-for="item in menuTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="菜单名称" prop="menuName">
<el-input v-model="menuModel.menuName" placeholder="请输入菜单名称"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="菜单图标" prop="icon">
<el-select v-model="menuModel.icon" clearable placeholder="请选择菜单图标">
<template #prefix>
<component :is="getIconComponent(menuModel.icon)" style="height: 15px;" />
</template>
<el-option class="icon" v-for="item in iconList" :key="item.id" :label="item.icon" :value="item.icon" >
<component :is="getIconComponent(item.icon)" style="height: 15px;" />
<span class="text">{{ item.icon }}</span>
</el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="前端路由" prop="path">
<el-input v-model="menuModel.path" placeholder="请输入前端路由,如:/sys/user/index"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="权限标识" prop="auth">
<el-input v-model="menuModel.auth" placeholder="请输入权限标识,如:sys.user.list"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-switch v-model="menuModel.status" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序" prop="sort">
<el-input v-model="menuModel.sort" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="save">确认</el-button>
</span>
</template>
</el-dialog>
</el-card>
</template>
<script setup lang="ts">
import { ref,reactive,onMounted } from 'vue'
import { Edit,Delete,ArrowRight } from '@element-plus/icons-vue'
import * as Icons from '@element-plus/icons-vue';
import menuApi from '@/api/menu';
import iconApi from '@/api/icon';
import { ElMessage, ElMessageBox } from 'element-plus'
// 菜单分页列表
const menuList=ref()
// 新增、编辑时,选拉框选择的菜单列表
const menuSelectData=ref()
// 新增、编辑数据模型
const menuModel=reactive({
id:'',
parentId:'',
menuName:'',
type:undefined,
path:'',
auth:'',
status:1,
icon:'',
sort:99
})
// 图标列表
const iconList=ref()
const initMenuModel={ ...menuModel }
// 批量删除的 id
const ids=ref<string[]>([])
const dialogVisible = ref(false)
const defaultProps = {
children: 'children',
label: 'label'
}
// 选中的图标
const selectedIcon = ref('');
// 新增或编辑的标题
const title=ref();
const menuModelRef=ref()
// 分页&搜索模型
const searchModel=reactive({
currentPage:1,
pageSize:10,
total:0,
menuName:'',
auth:''
})
const initSearchModel={ ...searchModel }
const rules = reactive({
parentId:[
{ required: true, message:'请选择上级菜单', trigger: 'blur'},
],
type:[
{ required: true, message:'请选择菜单类型', trigger: 'blur'},
],
menuName:[
{ required: true, message:'请输入菜单名称', trigger: 'blur'},
],
// path:[
// { required: true, message:'请输入前端路由', trigger: 'blur'},
// ],
// auth:[
// { required: true, message:'请输入权限标识', trigger: 'blur'},
// ],
icon:[
{ required: true, message:'请选择菜单图标', trigger: 'blur'},
],
sort:[
{ required: true, message:'请输入排序', trigger: 'blur'},
]
})
// 菜单类型选择器
let menuTypeOptions = ref([
{
value: 1,
label: "目录"
},
{
value: 2,
label: "菜单"
},
{
value: 3,
label: "按钮"
}
])
// pageSize 变化时触发
const handleSizeChange = (val: number) => {
searchModel.pageSize=val;
getMenuList();
}
// currentPage 变化时触发
const handleCurrentChange = (val: number) => {
searchModel.currentPage=val;
getMenuList();
}
// 菜单列表
const getMenuList= async()=>{
const response= await menuApi.menuList(searchModel);
menuList.value=response.data.records;
searchModel.currentPage=response.data.current;
searchModel.pageSize=response.data.size;
searchModel.total=response.data.total;
getMenuSelectData(menuList.value);
}
// 新增、编辑时的下拉菜单
const getMenuSelectData = (val: any) => {
// 前端添加一个根目录,将后端返回的菜单都放在根目录下
const menuSelect = [];
menuSelect.push({label: '根目录',value: '0',children: []})
menuSelect[0].children=recursive(val);
menuSelectData.value = menuSelect;
}
// 递归处理子菜单
const recursive = (data: any) => {
return data.map((item: { id: any, menuName: any, children: any }) => ({
value: item.id,
label: item.menuName,
children: Array.isArray(item.children) ? recursive(item.children) : []
}));
};
// 新增或编辑保存
const save= async()=>{
menuModelRef.value.validate(async(valid:any) => {
if (valid) {
const response=await menuApi.saveOrUpdate(menuModel) as any;
ElMessage.success(response.msg);
dialogVisible.value=false;
getMenuList();
} else {
return false;
}
})
}
// 编辑
const edit= (row:any)=>{
dialogVisible.value=true;
title.value="编辑菜单";
selectedIcon.value=row.icon;
// 源对象赋值给目标对象,Object.assign(目标对象, 源对象)
Object.assign(menuModel, row);
}
// 新增菜单按钮
const addButton= ()=>{
dialogVisible.value=true;
title.value="添加菜单";
}
// 关闭新增、编辑对话框时,清空数据和校验信息
const close= ()=>{
// 清空数据
Object.assign(menuModel, initMenuModel);
// 清除校验信息
menuModelRef.value.clearValidate();
}
// 重置搜索表单
const reset= ()=>{
Object.assign(searchModel, initSearchModel);
getMenuList();
}
// 批量删除选择
const handleSelectionChange = (rows: any) => {
ids.value = rows.map((item:any) => item.id);
}
// 批量删除
const batchRemove= ()=>{
if(ids.value.length > 0){
ElMessageBox.confirm(
`是否批量删除?`,
'温馨提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async() => {
await menuApi.remove(ids.value);
ElMessage({ type: 'success',message: '删除成功' });
getMenuList();
})
}else{
ElMessage.warning('请选择批量删除项');
}
}
// 单条删除
const remove= async(row:any)=>{
ElMessageBox.confirm(
`是否删除 [ ${row.menuName} ] 菜单?`,
'温馨提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async() => {
ids.value.push(row.id);
await menuApi.remove(ids.value);
ElMessage({ type: 'success', message: '删除成功' });
getMenuList();
})
}
// 菜单中是否启用按钮
const change= async(row:any)=>{
await menuApi.saveOrUpdate(row);
}
// 获取图标列表
const getIconList= async()=>{
const response=await iconApi.getAll();
iconList.value=response.data;
}
// 获取图标组件
const getIconComponent = (icon: string) => {
return (Icons as any)[icon] || null;
};
onMounted(()=>{
getMenuList();
getIconList();
})
</script>
<style scoped lang="less">
.container{
height: 100%;
box-sizing: border-box;
}
.header{
display: flex;
align-items: center;
justify-content: space-between;
}
.title{
font-size: large;
font-weight: 600;
}
.icon{
display: flex;
align-items: center;
}
.text{
margin-left: 10px;
}
</style>