目录
一、前言
此文章在上次调整的基础上开发后端管理系统必备的权限管理功能,具体功能包含用户管理、角色管理、菜单管理,并对页面整体布局进行了调整。
此项目是在我上一个文章的后续开发, 需要的同学可以关注一下,文章链接如下:SpringBoot+Mybatis+MySQL+Vue+ElementUI前后端分离版:整体布局、架构调整(二)
(注:源码我会在文章结尾提供gitee连接,需要的同学可以去自行下载)
二、后端开发及调整
1.新增实体(表)
1.角色表(role >> RoleEntity.java)
java
-- role
CREATE TABLE `role` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_name` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名称',
`role_key` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '角色标识',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`create_by` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`update_by` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
`del_flag` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '删除标识0未删除,1已删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='角色表'
-- RoleEntity.java
/**
* 角色表
* @TableName role
*/
@Data
public class RoleEntity extends BaseEntity {
/**
* 主键
*/
private Integer id;
/**
* 角色名称
*/
private String roleName;
/**
* 角色标识
*/
private String roleKey;
/**
* 备注
*/
private String remark;
}
2.用户-角色表(user_role >> UserRoleEntity.java)
java
-- user_role
CREATE TABLE `user_role` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_id` int NOT NULL COMMENT '用户ID',
`role_id` int NOT NULL COMMENT '角色ID',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`create_by` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`update_by` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
`del_flag` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '删除标识0未删除,1已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_user_id` (`user_id`) USING BTREE,
KEY `idx_role_id` (`role_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户角色关联表'
-- UserRoleEntity.java
/**
* 角色菜单关联表
* @TableName role_menu
*/
@Data
public class RoleMenuEntity extends BaseEntity {
/**
* 主键
*/
private Integer id;
/**
* 角色ID
*/
private Integer roleId;
/**
* 菜单ID
*/
private Integer menuId;
}
3.菜单表(menu >> MenuEntity.java)
java
-- menu
CREATE TABLE `menu` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`menu_name` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '菜单名称',
`parent_id` int DEFAULT '0' COMMENT '父菜单ID',
`path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '路由路径',
`component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '组件路径',
`perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '图标',
`sort` int DEFAULT '0' COMMENT '排序',
`visible` tinyint(1) DEFAULT '1' COMMENT '是否显示(0隐藏,1显示)',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`create_by` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`update_by` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
`del_flag` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '删除标识0未删除,1已删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='菜单表'
-- MenuEntity.java
/**
* 菜单表
* @TableName menu
*/
@Data
public class MenuEntity extends BaseEntity {
/**
* 主键
*/
private Integer id;
/**
* 菜单名称
*/
private String menuName;
/**
* 父菜单ID
*/
private Integer parentId;
/**
* 路由路径
*/
private String path;
/**
* 组件路径
*/
private String component;
/**
* 权限标识
*/
private String perms;
/**
* 图标
*/
private String icon;
/**
* 排序
*/
private Integer sort;
/**
* 是否显示(0隐藏,1显示)
*/
private Integer visible;
}
- 角色-菜单表(role_menu >> RoleMenuEntity.java)
java
-- role_menu
CREATE TABLE `role_menu` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_id` int NOT NULL COMMENT '角色ID',
`menu_id` int NOT NULL COMMENT '菜单ID',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`create_by` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`update_by` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
`del_flag` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '删除标识0未删除,1已删除',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_role_id` (`role_id`) USING BTREE,
KEY `idx_menu_id` (`menu_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=36 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='角色菜单关联表'
-- RoleMenuEntity.java
/**
* 角色菜单关联表
* @TableName role_menu
*/
@Data
public class RoleMenuEntity extends BaseEntity {
/**
* 主键
*/
private Integer id;
/**
* 角色ID
*/
private Integer roleId;
/**
* 菜单ID
*/
private Integer menuId;
}
- 权限管理表关联

2.DTO类调整
1.Resp类
1.调整菜单返回树结构类TreeDataResp .java
java
package org.wal.userdemo.DTO.resp;
import lombok.Data;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Data
public class TreeDataResp {
/**
* 主键
*/
private Integer id;
/**
* 菜单名称
*/
private String menuName;
/**
* 父菜单ID
*/
private Integer parentId;
/**
* 路由路径
*/
private String path;
/**
* 组件路径
*/
private String component;
/**
* 权限标识
*/
private String perms;
/**
* 图标
*/
private String icon;
/**
* 排序
*/
private Integer sort;
/**
* 是否显示(0隐藏,1显示)
*/
private Integer visible;
/**
* 子菜单
*/
private List<TreeDataResp> children;
public static List<TreeDataResp> buildTree(List<TreeDataResp> menus) {
Map<Integer, TreeDataResp> menuMap = new HashMap<>();
menus.forEach(menu -> menuMap.put(menu.getId(), menu));
List<TreeDataResp> rootMenus = new ArrayList<>();
menus.forEach(menu -> {
Integer parentId = menu.getParentId();
if (parentId == null || parentId == 0) {
rootMenus.add(menu);
} else {
TreeDataResp parent = menuMap.get(parentId);
if (parent != null) {
if (parent.getChildren() == null) {
parent.setChildren(new ArrayList<>());
}
parent.getChildren().add(menu);
}
}
});
return rootMenus;
}
}
2.新增用户信息返回类UserDataResp .java
java
package org.wal.userdemo.DTO.resp;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import org.wal.userdemo.entity.MenuEntity;
import org.wal.userdemo.entity.RoleEntity;
import java.util.Date;
import java.util.List;
@Data
public class UserDataResp {
/**
* id 主键
*/
private Integer id;
/**
* name 姓名
*/
private String name;
/**
* age 年龄
*/
private Integer age;
/**
* birthday 生日
*/
@JsonFormat(pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 创建人
*/
private String createBy;
/**
* 修改时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/**
* 修改人
*/
private String updateBy;
/**
* 删除标记0未删除1已删除(逻辑删除)
*/
private Integer delFlag;
/**
* 角色列表
*/
private List<RoleEntity> roles;
/**
* 菜单列表
*/
private List<TreeDataResp> menus;
}
2.Req类
1.新增查询用户请求类QueryUserReq .java
java
package org.wal.userdemo.DTO.req;
import lombok.Data;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* 查询用户参数
*/
@ToString(callSuper = true)
@Data
public class QueryUserReq extends PageReq{
/**
* 姓名
*/
private String name;
/**
* 生日
*/
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;
}
2.新增查询角色请求类QueryRoleReq.java
java
package org.wal.userdemo.DTO.req;
import lombok.Data;
@Data
public class QueryRoleReq extends PageReq{
private String roleName;
private String roleKey;
}
3.新增查询菜单请求类QueryMenuReq.java
java
package org.wal.userdemo.DTO.req;
public class QueryMenuReq{
private String menuName;
}
3.用户管理
1.新增分配角色接口、重置密码接口,部分接口添加用户校验、微调。
1.1 UserController.java
java
/**
* 给用户分配角色
* @param userId
* @param roleIds
* @return Integer
*/
@PostMapping("/assignRole")
public Result<Integer> assignRole(@RequestParam Integer userId, @RequestBody List<Integer> roleIds) {
return userService.assignRole(userId, roleIds) > 0 ? Result.success() : Result.error("分配角色失败");
}
/**
* 重置密码
* @param id
*/
@PostMapping("/resetPassword")
public Result<Integer> resetPassword(@RequestParam Integer id) {
return userService.resetPassword(id) > 0 ? Result.success() : Result.error("重置密码失败");
}
1.2 UserServiceImpl.java
java
@Override
public Integer resetPassword(Integer id) {
return userMapper.resetPassword( id);
}
/**
* 检查用户是否存在
*
* @param username
* @return Integer
*/
@Override
public Integer checkUserExist(String username) {
return userMapper.checkUserExist(username);
}
/**
* 分配角色
*
* @param userId
* @param roleIds
* @return Integer
*/
@Override
@Transactional
public Integer assignRole(Integer userId, List<Integer> roleIds) {
//删除用户-角色中间表数据
Integer result = userRoleMapper.deleteByUserId(userId);
if (roleIds != null && !roleIds.isEmpty()) {
//分配角色
return userRoleMapper.assignRole(userId, roleIds);
}
return result;
}
1.3UserMapper.xml
XML
<update id="resetPassword" parameterType="Integer">
update user set password = '123456' where id = #{id};
</update>
<select id="checkUserExist" resultType="java.lang.Integer" parameterType="String">
select count(*)
from user
where name = #{username}
and del_flag = 0;
</select>
1.4UserRoleMapper.xml
XML
<insert id="assignRole">
insert into user_role(user_id,role_id,create_time,create_by,del_flag) values
<foreach collection="roleIds" item="roleId" separator=",">
(#{userId},#{roleId},NOW(),'王',0)
</foreach>
;
</insert>
1.5优化登录角色查询结果UserServiceImpl.java
java
/**
* 获取用户信息
*
* @return UserEntity
*/
@Override
public UserDataResp getUserById(Integer id) {
UserEntity user = userMapper.getUserById(id);
UserDataResp resp = BeanUtils.copyAs(user, UserDataResp.class);
//查询用户角色
List<RoleEntity> roles = roleMapper.getRoleListByUserId(id);
resp.setRoles(roles);
//查询用户角色的菜单
List<MenuEntity> menus = menuMapper.getMenuListByUserId(id);
List<TreeDataResp> treeDataResp = BeanUtils.copyAsList(menus, TreeDataResp.class);
List<TreeDataResp> menuTree =TreeDataResp.buildTree(treeDataResp);
resp.setMenus(menuTree);
return resp;
}
4.角色管理
1.新增用户管理相关接口
1.1RoleController.java
java
@Autowired
private RoleService roleService;
/**
* 查询角色列表
*
* @param queryRoleReq
* @return List<RoleEntity>
*/
@PostMapping("/getRoleList")
public Result<RoleEntity> getRoleList(@RequestBody QueryRoleReq queryRoleReq) {
List<RoleEntity> roleList = roleService.getRoleList(queryRoleReq);
int total = roleService.getRoleCount(queryRoleReq);
return Result.page(roleList, total);
}
@GetMapping("/getAllsRoleList")
public Result<?> getAllsRoleList() {
List<RoleEntity> roleList = roleService.getAllsRoleList();
return Result.success(roleList);
}
/**
* 根据id查询角色
*
* @param id
* @return
*/
@GetMapping("/getRoleById")
public Result<?> getRoleById(@RequestParam Integer id) {
return Result.success(roleService.getRoleById(id));
}
/**
* 添加角色
*
* @param roleEntity
* @return Integer
*/
@PostMapping("/addRole")
public Result<?> addRole(@RequestBody RoleEntity roleEntity) {
Integer result = roleService.checkRoleExist(roleEntity.getRoleName());
if (result > 0) return Result.error("角色已存在");
return roleService.addRole(roleEntity) > 0 ? Result.success() : Result.error("新增角色失败");
}
/**
* 修改角色
*
* @param roleEntity
* @return Integer
*/
@PostMapping("/updateRole")
public Result<?> updateRole(@RequestBody RoleEntity roleEntity) {
Integer result = roleService.checkRoleExist(roleEntity.getRoleName());
if (result > 0) return Result.error("角色已存在");
return roleService.updateRole(roleEntity) > 0 ? Result.success() : Result.error("修改角色失败");
}
/**
* 删除角色
*
* @param id
* @return Integer
*/
@PostMapping("/deleteRole")
public Result<?> deleteRole(@RequestParam Integer id) {
return roleService.deleteRole(id) > 0 ? Result.success() : Result.error("删除角色失败");
}
/**
* 给角色分配菜单
*
* @param roleId
* @param menuIds
* @return Integer
*/
@PostMapping("/assignMenu")
public Result<?> assignMenu(@RequestParam Integer roleId, @RequestBody List<Integer> menuIds) {
return roleService.assignMenu(roleId, menuIds) > 0 ? Result.success() : Result.error("分配菜单失败");
}
1.2RoleServiceImpl.java
java
@Autowired
private RoleMapper roleMapper;
@Autowired
private MenuMapper menuMapper;
@Autowired
private RoleMenuMapper roleMenuMapper;
/**
* 获取角色列表
*
* @param queryRoleReq
* @return
*/
@Override
public List<RoleEntity> getRoleList(QueryRoleReq queryRoleReq) {
return roleMapper.getRoleList(queryRoleReq);
}
/**
* 获取角色列表数量
*
* @param queryRoleReq
* @return
*/
@Override
public Integer getRoleCount(QueryRoleReq queryRoleReq) {
return roleMapper.getRoleCount(queryRoleReq);
}
/**
* 获取角色详情
*
* @param id
* @return
*/
@Override
public RoleDataResp getRoleById(Integer id) {
RoleEntity roleEntity = roleMapper.getRoleById(id);
//查询角色对应的菜单列表
RoleDataResp resp = BeanUtils.copyAs(roleEntity, RoleDataResp.class);
resp.setMenus(menuMapper.getMenuListByRoleId(id));
return resp;
}
/**
* 检查角色是否存在
*
* @param roleName
* @return
*/
@Override
public Integer checkRoleExist(String roleName) {
return roleMapper.checkRoleExist(roleName);
}
/**
* 添加角色
*
* @param roleEntity
* @return
*/
@Override
public Integer addRole(RoleEntity roleEntity) {
return roleMapper.addRole(roleEntity);
}
/**
* 修改角色
*
* @param roleEntity
* @return
*/
@Override
public Integer updateRole(RoleEntity roleEntity) {
return roleMapper.updateRole(roleEntity);
}
/**
* 删除角色
*
* @param id
* @return
*/
@Override
public Integer deleteRole(Integer id) {
return roleMapper.deleteRole(id);
}
/**
* 给角色分配菜单
* @param roleId
* @param menuIds
* @return Integer
*/
@Override
@Transactional
public Integer assignMenu(Integer roleId, List<Integer> menuIds) {
//删除角色-菜单中间表数据
Integer result =roleMenuMapper.deleteByRoleId(roleId);
//分配菜单
if(menuIds != null && !menuIds.isEmpty()){
return roleMenuMapper.assignMenu(roleId, menuIds);
}
return result;
}
/**
* 获取所有角色列表
* @return
*/
@Override
public List<RoleEntity> getAllsRoleList() {
return roleMapper.getAllsRoleList();
}
1.3RoleMapper.xml
XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.wal.userdemo.mapper.RoleMapper">
<resultMap id="BaseResultMap" type="org.wal.userdemo.entity.RoleEntity">
<id property="id" column="id" />
<result property="roleName" column="role_name" />
<result property="roleKey" column="role_key" />
<result property="remark" column="remark" />
<result property="createTime" column="create_time" />
<result property="createBy" column="create_by" />
<result property="updateTime" column="update_time" />
<result property="updateBy" column="update_by" />
<result property="delFlag" column="del_flag" />
</resultMap>
<select id="getRoleList" resultMap="BaseResultMap" parameterType="org.wal.userdemo.DTO.req.QueryRoleReq">
select * from role
<where>
<if test="roleName != null and roleName != ''">
and role_name like concat('%',#{roleName},'%')
</if>
<if test="roleKey != null and roleKey != ''">
and role_key like concat('%',#{roleKey},'%')
</if>
and del_flag = 0
</where>
order by id
limit #{page},#{limit};
</select>
<select id="getRoleCount" resultType="java.lang.Integer" parameterType="org.wal.userdemo.DTO.req.QueryRoleReq">
select count(*) from role
<where>
<if test="roleName != null and roleName != ''">
and role_name like concat('%',#{roleName},'%')
</if>
<if test="roleKey != null and roleKey != ''">
and role_key like concat('%',#{roleKey},'%')
</if>
and del_flag = 0
</where>
;
</select>
<select id="getRoleById" resultMap="BaseResultMap">
select * from role where id = #{id} and del_flag = 0;
</select>
<select id="checkRoleExist" resultType="java.lang.Integer" parameterType="String">
select count(*) from role where role_name = #{roleName} and del_flag = 0;
</select>
<select id="getRoleListByUserId" resultMap="BaseResultMap" parameterType="Integer">
select r.* from role r
left join user_role ur on r.id = ur.role_id
where ur.user_id = #{userId} and r.del_flag = 0;
</select>
<select id="getAllsRoleList" resultMap="BaseResultMap">
select * from role where del_flag = 0 ;
</select>
<insert id="addRole" parameterType="org.wal.userdemo.entity.RoleEntity">
insert into role
<trim prefix="(" suffixOverrides=", " suffix=")">
<if test="roleName != null">role_name,</if>
<if test="roleKey != null">role_key,</if>
<if test="remark != null">remark,</if>
<if test="createTime != null">create_time,</if>
<if test="createBy != null">create_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="updateBy != null">update_by,</if>
<if test="delFlag != null">del_flag,</if>
</trim>
values
<trim prefix="(" suffixOverrides=", " suffix=")">
<if test="roleName != null">#{roleName},</if>
<if test="roleKey != null">#{roleKey},</if>
<if test="remark != null">#{remark},</if>
<if test="createTime != null">#{createTime},</if>
<if test="createBy != null">#{createBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="updateBy != null">#{updateBy},</if>
</trim>;
</insert>
<update id="updateRole" parameterType="org.wal.userdemo.entity.RoleEntity">
update role
<set>
<if test="roleName != null">role_name = #{roleName},</if>
<if test="roleKey != null">role_key = #{roleKey},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="delFlag != null">del_flag = #{delFlag},</if>
</set>
;
</update>
<delete id="deleteRole">
update role set del_flag = 1 where id = #{id};
</delete>
</mapper>
1.4RoleMenuMapper.xml
XML
<delete id="deleteByRoleId" parameterType="Integer">
delete from role_menu where role_id = #{roleId};
</delete>
<update id="assignMenu">
insert into role_menu(role_id, menu_id, create_time, create_by, del_flag) values
<foreach item="item" index="index" collection="menuIds" separator=",">
(#{roleId}, #{item}, now(), '王',0)
</foreach>;
</update>
5.菜单管理
1.新增菜单管理相关接口、动态化显示前端菜单
1.1MenuController.java
java
package org.wal.userdemo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.wal.userdemo.DTO.req.QueryMenuReq;
import org.wal.userdemo.DTO.resp.TreeDataResp;
import org.wal.userdemo.entity.MenuEntity;
import org.wal.userdemo.mapper.MenuMapper;
import org.wal.userdemo.service.MenuService;
import org.wal.userdemo.utils.Result;
import java.util.List;
@RestController
@RequestMapping("/api/menu")
public class MenuController {
@Autowired
private MenuService menuService;
@Autowired
private MenuMapper menuMapper;
@GetMapping("/getAllMenuList")
public Result<List<TreeDataResp>> getAllMenuList() {
return Result.success(menuService.getAllMenuList());
}
@PostMapping("/getMenuList")
public Result<?> getMenuList(@RequestBody QueryMenuReq queryMenuReq) {
List<TreeDataResp> menuList = menuService.getMenuList(queryMenuReq);
return Result.success(menuList);
}
@GetMapping("/getMenuListByUserId")
public Result<?> getMenuListByUserId(@RequestParam Integer userId) {
return Result.success(menuService.getMenuListByUserId(userId));
}
@GetMapping("getMenuById")
public Result<?> getMenuById(@RequestParam Integer id) {
return Result.success(menuService.getMenuById(id));
}
@PostMapping("addMenu")
public Result<?> addMenu(@RequestBody MenuEntity menu) {
Integer num = menuService.addMenu(menu);
return num > 0 ? Result.success() : num == -1 ? Result.error("菜单名称/路由/标识有重复项") : Result.error("添加失败");
}
@PostMapping("updateMenu")
public Result<?> updateMenu(@RequestBody MenuEntity menu) {
Integer num = menuService.updateMenu(menu);
return num > 0 ? Result.success() : num == -1 ? Result.error("菜单名称/路由/标识有重复项") : Result.error("修改失败");
}
@PostMapping("deleteMenu")
public Result<?> deleteMenu(@RequestParam Integer id) {
//校验菜单是否有用户在用
Integer count = menuMapper.checkMenuForUser(id);
if(count > 0){
return Result.error("菜单有 "+count+" 个用户在使用,不能删除");
}
return menuService.deleteMenu(id) > 0 ? Result.success() : Result.error("删除失败");
}
}
1.2MenuServiceImpl.java
java
package org.wal.userdemo.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.wal.userdemo.DTO.req.QueryMenuReq;
import org.wal.userdemo.DTO.resp.TreeDataResp;
import org.wal.userdemo.entity.MenuEntity;
import org.wal.userdemo.entity.RoleMenuEntity;
import org.wal.userdemo.mapper.MenuMapper;
import org.wal.userdemo.mapper.RoleMenuMapper;
import org.wal.userdemo.service.MenuService;
import org.wal.userdemo.service.RoleService;
import org.wal.userdemo.utils.BeanUtils;
import java.util.*;
@Service
public class MenuServiceImpl implements MenuService {
@Autowired
private MenuMapper menuMapper;
@Autowired
private RoleService roleService;
@Autowired
private RoleMenuMapper roleMenuMapper;
/**
* 获取菜单列表
*
* @return
*/
@Override
public List<TreeDataResp> getAllMenuList() {
List<MenuEntity> menuList = menuMapper.getAllMenuList();
List<TreeDataResp> treeDataRespList = BeanUtils.copyAsList(menuList, TreeDataResp.class);
return buildMenuTree(treeDataRespList);
}
@Override
public List<TreeDataResp> getMenuList(QueryMenuReq queryMenuReq) {
List<MenuEntity> menuList = menuMapper.getMenuList(queryMenuReq);
List<TreeDataResp> treeDataRespList = BeanUtils.copyAsList(menuList, TreeDataResp.class);
return buildMenuTree(treeDataRespList);
}
@Override
public List<TreeDataResp> getMenuListByUserId(Integer userId) {
List<MenuEntity> menuList = menuMapper.getMenuListByUserId(userId);
List<TreeDataResp> treeDataRespList = BeanUtils.copyAsList(menuList, TreeDataResp.class);
return buildMenuTree(treeDataRespList);
}
@Override
public MenuEntity getMenuById(Integer id) {
return menuMapper.getMenuById(id);
}
@Override
@Transactional
public Integer addMenu(MenuEntity menu) {
List<MenuEntity> repeatList = menuMapper.checkMenu(menu);
if (!repeatList.isEmpty()) {
return -1;
}
return menuMapper.addMenu(menu);
}
@Override
@Transactional
public Integer updateMenu(MenuEntity menu) {
List<MenuEntity> repeatList = menuMapper.checkMenu(menu);
if (!repeatList.isEmpty()) {
return -1;
}
MenuEntity source = menuMapper.getMenuById(menu.getId());
//只修改了菜单部分属性,未修改父级菜单
if (source.getParentId() == menu.getParentId()) {
//不做处理
} else {
//修改了父级菜单
//批量新增role-menu表(新增挪到新父菜单的数据,不管挪到那个父菜单下,角色都有该子菜单的权限)
List<RoleMenuEntity> insertRoleMenuList = new ArrayList<>();
List<RoleMenuEntity> roleMenuList = roleMenuMapper.getRoleMenuListByMenuId(menu.getId());
for (RoleMenuEntity item : roleMenuList) {
RoleMenuEntity roleMenuEntity = new RoleMenuEntity();
roleMenuEntity.setRoleId(item.getRoleId());
roleMenuEntity.setMenuId(menu.getParentId());
insertRoleMenuList.add(roleMenuEntity);
}
roleMenuMapper.insertRoleMenuBatch(insertRoleMenuList);
}
return menuMapper.updateMenu(menu);
}
@Override
public Integer deleteMenu(Integer id) {
return menuMapper.deleteMenu(id);
}
/**
* 构建菜单树
*
* @param menus
* @return
*/
public List<TreeDataResp> buildMenuTree(List<TreeDataResp> menus) {
Map<Integer, TreeDataResp> menuMap = new HashMap<>();
menus.forEach(menu -> menuMap.put(menu.getId(), menu));
List<TreeDataResp> rootMenus = new ArrayList<>();
menus.forEach(menu -> {
Integer parentId = menu.getParentId();
if (parentId == null || parentId == 0) {
rootMenus.add(menu);
} else {
TreeDataResp parent = menuMap.get(parentId);
if (parent != null) {
if (parent.getChildren() == null) {
parent.setChildren(new ArrayList<>());
}
parent.getChildren().add(menu);
}
}
});
return rootMenus;
}
}
1.3MenuMapper.xml
XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.wal.userdemo.mapper.MenuMapper">
<resultMap id="BaseResultMap" type="org.wal.userdemo.entity.MenuEntity">
<id property="id" column="id" />
<result property="menuName" column="menu_name" />
<result property="parentId" column="parent_id" />
<result property="path" column="path" />
<result property="component" column="component" />
<result property="perms" column="perms" />
<result property="icon" column="icon" />
<result property="sort" column="sort" />
<result property="visible" column="visible" />
<result property="createTime" column="create_time" />
<result property="createBy" column="create_by" />
<result property="updateTime" column="update_time" />
<result property="updateBy" column="update_by" />
<result property="delFlag" column="del_flag" />
</resultMap>
<select id="getAllMenuList" resultMap="BaseResultMap">
SELECT * FROM menu WHERE del_flag = 0 ORDER BY parent_id, sort;
</select>
<select id="getMenuListByRoleId" resultMap="BaseResultMap" parameterType="Integer">
select m.* from menu m
left join role_menu rm on m.id = rm.menu_id
where rm.role_id = #{roleId} and m.del_flag = 0;
</select>
<select id="getMenuListByUserId" resultMap="BaseResultMap" parameterType="Integer">
select m.* from menu m
left join role_menu rm on m.id = rm.menu_id
left join user_role ur on rm.role_id = ur.role_id
where ur.user_id = #{userId} and m.del_flag = 0 group by m.id;
</select>
<select id="getMenuList" resultMap="BaseResultMap" parameterType="org.wal.userdemo.DTO.req.QueryMenuReq">
SELECT * FROM menu
<where>
<if test="menuName != null">
AND menu_name LIKE CONCAT('%',#{menuName},'%')
</if>
and del_flag = 0
</where>
ORDER BY parent_id, sort;
</select>
<insert id="addMenu" parameterType="org.wal.userdemo.entity.MenuEntity">
INSERT INTO menu
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="menuName != null">menu_name,</if>
<if test="parentId != null">parent_id,</if>
<if test="path != null">path,</if>
<if test="component != null">component,</if>
<if test="perms != null">perms,</if>
<if test="icon != null">icon,</if>
<if test="sort != null">sort,</if>
<if test="visible != null">visible,</if>
<if test="createTime != null">create_time,</if>
<if test="createBy != null">create_by,</if>
<if test="delFlag != null">del_flag,</if>
</trim>
VALUES
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="menuName != null">#{menuName},</if>
<if test="parentId != null">#{parentId},</if>
<if test="path != null">#{path},</if>
<if test="component != null">#{component},</if>
<if test="perms != null">#{perms},</if>
<if test="icon != null">#{icon},</if>
<if test="sort != null">#{sort},</if>
<if test="visible != null">#{visible},</if>
<if test="createTime != null">#{createTime},</if>
<if test="createBy != null">#{createBy},</if>
<if test="delFlag != null">#{delFlag},</if>
</trim>;
</insert>
<update id="updateMenu" parameterType="org.wal.userdemo.entity.MenuEntity">
UPDATE menu
<set>
<if test="menuName != null">menu_name = #{menuName},</if>
<if test="parentId != null">parent_id = #{parentId},</if>
<if test="path != null">path = #{path},</if>
<if test="component != null">component = #{component},</if>
<if test="perms != null">perms = #{perms},</if>
<if test="icon != null">icon = #{icon},</if>
<if test="sort != null">sort = #{sort},</if>
<if test="visible != null">visible = #{visible},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="delFlag != null">del_flag = #{delFlag}</if>
</set>
WHERE id = #{id};
</update>
<delete id="deleteMenu" parameterType="Integer">
DELETE FROM menu WHERE id = #{id};
</delete>
<select id="checkMenu" parameterType="org.wal.userdemo.entity.MenuEntity" resultMap="BaseResultMap">
SELECT *
FROM menu
WHERE del_flag = 0
and (menu_name = #{menuName}
or path = #{path}
or component = #{component}
or perms = #{perms})
<if test="id != null">
and id != #{id}
</if>
</select>
<select id="checkMenuForUser" resultType="java.lang.Integer">
SELECT count(ur.user_id)
FROM menu m
LEFT JOIN role_menu rm ON m.id = rm.menu_id
LEFT JOIN role r ON rm.role_id = r.id
LEFT JOIN user_role ur ON r.id = ur.role_id
where m.id = #{id}
</select>
<select id="getMenuById" resultMap="BaseResultMap">
select * from menu where id = #{id} and del_flag = 0;
</select>
</mapper>
1.4RoleMenuMapper.xml
XML
<insert id="insertRoleMenuBatch" parameterType="list">
insert into role_menu(role_id, menu_id, create_time, create_by, del_flag) values
<foreach item="item" index="index" collection="list" separator=",">
(#{item.roleId}, #{item.menuId}, now(), '王',0)
</foreach>;
</insert>
<select id="getRoleMenuListByMenuId" resultMap="BaseResultMap" parameterType="Integer">
select id,role_id,menu_id from role_menu where menu_id = #{menuId} ORDER BY role_id;
</select>
6.配置调整
1.优化JwtUtils工具类
把 用户ID 作为JWT的主题比 用户名称 更合适、更合理,上下文调用也方便。
java
/**
* 生成 JWT 令牌。
*
* @param userId 用户id,作为 JWT 的 subject 字段
* @return 返回生成的 JWT 字符串
*/
public static String generateToken(Integer userId) {
return Jwts.builder()
// 设置 JWT 的主题(通常为用户标识)
.setSubject(userId.toString())
// 设置 JWT 的过期时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
// 使用 HS512 算法签名,并指定密钥
.signWith(SIGNING_KEY)
// 构建并返回紧凑格式的 JWT 字符串
.compact();
}
/**
* 从 JWT 令牌中解析出用户ID。
*
* @param token 需要解析的 JWT 字符串
* @return 解析出的用户ID(subject)
* @throws JwtException 如果 token 无效或签名不匹配会抛出异常
*/
public static String parseUserId(String token) {
return Jwts.parser()
// 设置签名验证所使用的密钥
.setSigningKey(SIGNING_KEY)
// 解析并验证 JWT 令牌
.parseClaimsJws(token)
// 获取 JWT 中的负载(claims),并提取 subject(用户名)
.getBody()
.getSubject();
}
2.优化JwtInterceptor拦截器
2.1JwtInterceptor.java(解析token中userId用户上下文调用)
java
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
try {
String userId = JwtUtil.parseUserId(token);
// 可以将 username 存入 request 或 SecurityContext
log.info("用户 {} 使用正确的token访问了后端接口", userId);
return true;
} catch (JwtException e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "无效 Token");
return false;
}
} else {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "缺少 Token");
return false;
}
}
(注:避免篇幅过长,省略了service类和mapper类的接口,需要可直接下载源代码)
三、前端开发及调整
(权限管理相关页面统一挪到view/permission/下,请求js统一挪到api/permission/下)
1.用户管理
1.1.页面功能(user.vue)
html
<template>
<div>
<!-- 查询条件 -->
<el-form :inline="true" label-position="right" label-width="80px" :model="queryForm" class="query-border-container">
<el-row :gutter="20" justify="center">
<!-- 姓名 -->
<el-col :span="7">
<el-form-item label="姓名">
<el-input v-model="queryForm.name" placeholder="请输入姓名"></el-input>
</el-form-item>
</el-col>
<!-- 出生日期 -->
<el-col :span="7">
<el-form-item label="出生日期">
<el-date-picker v-model="queryForm.birthday" type="date" placeholder="选择日期"
style="width: 100%;"></el-date-picker>
</el-form-item>
</el-col>
<!-- 按钮组 -->
<el-col :span="7">
<el-form-item>
<div style="display: flex; gap: 10px;">
<el-button type="primary" @click="onQuery" size="small">查询</el-button>
<el-button @click="onReset" size="small">重置</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" justify="center">
<el-col :span="7">
<div style="display: flex; gap: 10px;">
<el-button type="primary" size="small" @click="handleAdd()">新增</el-button>
</div>
</el-col>
</el-row>
</el-form>
<!-- 用户列表 -->
<el-table :data="tableData" style="width: 100%;" class="table-border-container" max-height="480"
v-loading="loading">
<el-table-column type="index" label="序号" width="100" align="center">
<template #default="scope">
{{ (queryForm.page - 1) * queryForm.limit + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" width="180" align="center">
</el-table-column>
<el-table-column prop="age" label="年龄" width="180" align="center">
</el-table-column>
<el-table-column prop="birthday" label="出生日期" width="180" align="center">
</el-table-column>
<el-table-column label="操作" width="350" align="center">
<template #default="scope">
<el-button type="primary" size="small" v-if="scope.row.name != 'admin'"
@click="handleEdit(scope.$index, scope.row)">修改</el-button>
<el-button type="danger" size="small" v-if="scope.row.name != 'admin'"
@click="handleDelete(scope.$index, scope.row)">删除</el-button>
<el-button type="primary" size="small" @click="setRoles(scope.row)">分配角色</el-button>
<el-button type="primary" size="small" @click="resetPassword(scope.row)">重置密码</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination background layout="total,sizes,prev, pager, next" :total="total" @size-change="handleSizeChange"
@current-change="handleCurrentChange" :page-size.sync="queryForm.limit" :page-sizes="[10, 20, 50, 100]"
class="page-border-container">
</el-pagination>
<!-- 添加用户 and 修改用户 -->
<el-dialog title="用户信息" :visible.sync="showUser" width="600px" append-to-body>
<el-form ref="form" :model="form" label-width="80px" center="false" :rules="rules">
<el-row>
<el-col :span="12">
<el-form-item label="用户名" prop="name">
<el-input v-model="form.name"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出生日期" prop="birthday">
<el-date-picker v-model="form.birthday" type="date" placeholder="选择日期"
style="width: 100%;"></el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="用户年龄" prop="age">
<el-input v-model="form.age" disabled></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="密码" prop="password">
<el-input v-model="form.password"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="确认密码" prop="password2">
<el-input v-model="form.password2"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer" style="text-align: center;">
<el-button type="primary" size="small" @click="submit()">保存</el-button>
<el-button @click="close()" size="small">取消</el-button>
</div>
</el-dialog>
<!-- 给用户分配角色穿梭框 -->
<el-dialog title="分配角色" :visible.sync="showRoleDialog" width="800px" append-to-body>
<div style="text-align: left">
<el-transfer v-model="selectedRoles" filterable :data="roleList" :render-content="renderRole"
:titles="['未拥有角色', '已有角色']" :button-texts="['移除', '添加']"
:format="{ noChecked: '${total}', hasChecked: '${checked}/${total}' }">
</el-transfer>
</div>
<!-- 按钮区域 -->
<div slot="footer" class="dialog-footer" style="text-align: center;">
<el-button size="small" @click="resetRoles">重置</el-button>
<el-button type="primary" size="small" @click="saveRoles">保存</el-button>
<el-button size="small" @click="closeRoleDialog">取消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getUserList, updateUser, getUserById, addUser, assignRole, deleteUser, resetPassword } from '@/api/permission/user';
import { getAllsRoleList } from '@/api/permission/role'
export default {
name: 'userView',
data() {
return {
tableData: [],
queryForm: {
page: 1,
limit: 10,
username: '',
birthday: '',
},
total: 0,
loading: false,
showUser: false,
form: {},
//是否禁用 【暂时不需要】
isDisabled: true,
rules: {
name: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 12, message: '长度在 3 到 12 个字符', trigger: 'blur' },
{
pattern: /^[^\s]+$/,
message: '用户名不能包含空格',
trigger: 'blur'
}, {
validator: (rule, value, callback) => {
const forbiddenNames = ['null', 'undefined', 'root', 'system', 'admin'];
if (forbiddenNames.includes(value.toLowerCase())) {
callback(new Error(`用户名不能是 ${value}`));
} else if (value && value.toLowerCase().includes('admin')) {
callback(new Error('用户名不能包含 "admin"'));
} else {
callback();
}
},
trigger: 'blur'
}
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 10, message: '长度在 6 到 10 个字符', trigger: 'blur' },
{
pattern: /^(?=.*[A-Za-z])(?=.*\d)|^[A-Za-z]+$|^\d+$/,
message: '只能包含大小写字母和数字,或纯字母、纯数字',
trigger: 'blur'
}
],
password2: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value !== this.form.password) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
},
trigger: 'blur'
}]
},
//分配角色区
showRoleDialog: false, // 控制角色弹窗显示
selectedRoles: [], // 当前选中的角色 ID 数组
roleList: [], //所有角色列表
userId: '', //当前分配角色的用户ID(暂定这样)
//原始角色副本列表
roleListCopy: [],
};
},
watch: {
'form.birthday': function (newVal) {
if (newVal) {
const age = this.calculateAge(newVal);
this.$set(this.form, 'age', age); // 使用 Vue.set 确保响应式更新
}
}
},
created() {
this.getUserList();
},
methods: {
// 获取用户列表
getUserList() {
this.loading = true;
getUserList(this.queryForm).then(res => {
if (res.data.code == 200) {
this.tableData = res.data.data;
this.total = res.data.total;
this.$message.success("获取用户列表成功!");
} else {
this.$message.error("获取用户列表失败!");
}
}).finally(() => {
this.loading = false;
});
},
resetPassword(row) {
resetPassword(row.id).then(res => {
if (res.data.code == 200) {
this.$message.success("重置密码成功,默认密码是:123456");
localStorage.removeItem("token");
this.$router.push("/login");
} else {
this.$message.error("重置密码失败!");
}
});
},
// 查询
onQuery() {
this.getUserList();
},
// 重置表单并查询
onReset() {
this.queryForm = {
page: 1,
limit: 10,
name: '',
birthday: '',
};
this.getUserList();
},
//分页器改变
handleSizeChange(val) {
this.queryForm.limit = val;
this.getUserList();
},
//改变页码
handleCurrentChange(val) {
this.queryForm.page = val;
this.getUserList();
},
//修改按钮 - 打开修改模态框
handleEdit(index, row) {
getUserById(row.id).then(res => {
if (res.data.code == 200) {
this.form = res.data.data;
this.showUser = true;
} else {
this.$message.error("获取用户信息失败!");
}
});
// console.log(index, row);
// this.$message.success('编辑成功');
},
//新增按钮 -打开新增框
handleAdd() {
this.form = {};
this.showUser = true;
},
//给用户分配角色
setRoles(row) {
getUserById(row.id).then(res => {
this.userId = row.id;
if (res.data.code == 200) {
this.selectedRoles = res.data.data.roles.map(item => item.id) || [];
//保存一份角色列表,用于重置操作
this.roleListCopy = [...this.selectedRoles];
} else {
this.$message.error("获取用户角色信息失败!");
}
});
getAllsRoleList().then(res => {
if (res.data.code == 200) {
this.roleList = res.data.data.map(item => ({
key: item.id,
label: item.roleName
}));
} else {
this.$message.error("获取所有角色信息失败!");
}
});
this.showRoleDialog = true;
},
// 渲染穿梭框内容(可自定义格式)
renderRole(h, option) {
return h('span', {}, `${option.label}`);
},
// 重置按钮:清空所有选择
resetRoles() {
this.selectedRoles = [...this.roleListCopy];
},
// 保存按钮-【分配角色】
saveRoles() {
console.log('this.selectedRoles', this.selectedRoles);
assignRole({ userId: this.userId }, this.selectedRoles).then(res => {
if (res.data.code === 200) {
this.$message.success("分配角色成功!");
} else {
this.$message.error("分配角色失败!");
}
});
this.userId = null;
this.showRoleDialog = false; // 关闭弹窗
},
// 取消按钮:关闭弹窗
closeRoleDialog() {
this.userId = null;
this.selectedRoles = [];
this.showRoleDialog = false;
},
//保存按钮-【新增、修改】
submit() {
this.$refs.form.validate(valid => {
if (valid) {
this.loading = true;
if (this.form.id) {
//修改
updateUser(this.form).then(res => {
if (res.data.code == 200) {
this.$message.success("更新用户信息成功!");
this.showUser = false;
this.getUserList();
this.form = {};
} else {
this.$message.error(res.data.message);
}
});
} else {
//新增
addUser(this.form).then(res => {
if (res.data.code == 200) {
this.$message.success("添加用户成功!");
this.showUser = false;
this.getUserList();
} else {
this.$message.error(res.data.message);
}
});
}
}
this.loading = false;
});
},
//删除按钮
handleDelete(index, row) {
this.$confirm('确认删除用户:' + row.name + '吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteUser(row.id).then(res => {
if (res.data.code == 200) {
this.$message.success("删除用户成功!");
this.getUserList();
} else {
this.$message.error(res.data.message);
}
});
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
close() {
this.showUser = false;
this.form = {};
},
calculateAge(birthday) {
const today = new Date();
const birthDate = new Date(birthday);
let age = today.getFullYear() - birthDate.getFullYear();
const m = today.getMonth() - birthDate.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
},
},
};
</script>
<style scoped></style>
1.2.请求js调整(user.js)
javascript
import request from '@/utils/request';
/**
* 查询用户列表(分页)
* @param {Object} params - 请求参数,如 page, limit 等
*/
export function getUserList(params) {
return request({
url: '/user/getUserList',
method: 'post',
data : params,
});
}
/**
* 修改用户信息
* @param {Object} data - 请求参数
*/
export function updateUser(data) {
return request({
url: '/user/updateUser',
method: 'post',
data : data,
});
}
/**
* 获取用户信息
* @param {Object} id - 用户ID
*/
export function getUserById(id) {
return request({
url: '/user/getUserById',
method: 'get',
params : {'id': id},
});
}
/**
* 新增用户
* @param {Object} data - 添加用户信息
*/
export function addUser(data) {
return request({
url: '/user/addUser',
method: 'post',
data : data,
});
}
/**
* 给用户分配角色
*
*/
export function assignRole(params,data) {
return request({
url: '/user/assignRole',
method: 'post',
params : params,
data : data,
});
}
/**
* 删除用户
* @param {Object} id - 用户ID
*/
export function deleteUser(id) {
return request({
url: '/user/deleteUser',
method: 'post',
params : {'id':id},
});
}
/**
* 重置密码
* @param {Object} id - 用户ID
*/
export function resetPassword(id) {
return request({
url: '/user/resetPassword',
method: 'post',
params : {'id':id},
});
}
2.角色管理
1.1.页面功能(role.vue)
html
<template>
<div>
<!-- 查询条件 -->
<el-form :inline="true" label-position="right" label-width="80px" :model="queryForm"
class="query-border-container">
<el-row :gutter="20" justify="center">
<!-- 角色名称 -->
<el-col :span="7">
<el-form-item label="角色名称">
<el-input v-model="queryForm.roleName" placeholder="请输入角色名称"></el-input>
</el-form-item>
</el-col>
<!-- 角色标识 -->
<el-col :span="7">
<el-form-item label="角色标识">
<el-input v-model="queryForm.roleKey" placeholder="请输入角色标识"></el-input>
</el-form-item>
</el-col>
<!-- 按钮组 -->
<el-col :span="7">
<el-form-item>
<div style="display: flex; gap: 10px;">
<el-button type="primary" @click="onQuery" size="small">查询</el-button>
<el-button @click="onReset" size="small">重置</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" justify="center">
<el-col :span="7">
<div style="display: flex; gap: 10px;">
<el-button type="primary" size="small" @click="handleAdd()">新增</el-button>
</div>
</el-col>
</el-row>
</el-form>
<!-- 角色列表 -->
<el-table :data="tableData" style="width: 100%;" class="table-border-container" max-height="480"
v-loading="loading">
<el-table-column type="index" label="序号" width="100" align="center">
<template #default="scope">
{{ (queryForm.page - 1) * queryForm.limit + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="roleName" label="角色名称" width="180" align="center">
</el-table-column>
<el-table-column prop="roleKey" label="角色标识" width="180" align="center">
</el-table-column>
<el-table-column prop="remark" label="备注" width="180" align="center">
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" align="center">
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" align="center">
</el-table-column>
<el-table-column label="操作" width="270" align="center">
<template #default="scope">
<el-button type="primary" size="small" @click="handleEdit(scope.$index, scope.row)">修改</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
<el-button type="primary" size="small" @click="setMenus(scope.row)">分配菜单</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination background layout="total,sizes,prev, pager, next" :total="total" @size-change="handleSizeChange"
@current-change="handleCurrentChange" :page-size.sync="queryForm.limit" :page-sizes="[10, 20, 50, 100]"
class="page-border-container">
</el-pagination>
<!-- 添加角色 and 修改角色 -->
<el-dialog title="角色信息" :visible.sync="showRole" width="600px" append-to-body>
<el-form ref="form" :model="form" label-width="80px" center="false" :rules="rules">
<el-row>
<el-col :span="12">
<el-form-item label="角色名称">
<el-input v-model="form.roleName"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="角色标识">
<el-input v-model="form.roleKey"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer" style="text-align: center;">
<el-button type="primary" size="small" @click="submit()">保存</el-button>
<el-button @click="close()" size="small">取消</el-button>
</div>
</el-dialog>
<!-- 给角色分配菜单权限 -->
<el-dialog title="分配菜单" :visible.sync="showMenuDialog" width="400px" append-to-body>
<el-input placeholder="输入关键字进行过滤" v-model="filterText">
</el-input>
<el-tree ref="menuTree" :data="menuList" node-key="key" show-checkbox default-expand-all
:default-checked-keys="selectedMenus" style="text-align: center; height: 300px; max-height: 350px;"
width="100%" :filter-node-method="filterNode">
<template #default="{ node }">
<span>{{ node.label }}</span>
</template>
</el-tree>
<!-- 按钮区域 -->
<div slot="footer" class="dialog-footer" style="text-align: center;">
<el-button size="small" @click="resetMenus">重置</el-button>
<el-button type="primary" size="small" @click="saveMenus">保存</el-button>
<el-button size="small" @click="closeMenuDialog">取消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getRoleList, addRole, updateRole, getRoleById, deleteRole, assignMenu } from '@/api/permission/role'
import { getAllMenuList } from '@/api/permission/menu'
export default {
name: 'roleView',
data() {
return {
queryForm: {
page: 1,
limit: 10,
roleName: '',
roleKey: ''
},
tableData: [],
total: 0,
showRole: false,
loading: false,
form: {},
rules: {
roleName: [
{ required: true, message: '请输入角色名称', trigger: 'blur' }
],
roleKey: [
{ required: true, message: '请输入角色标识', trigger: 'blur' }
]
},
//tree组件
showMenuDialog: false,
menuList: [],
selectedMenus: [],
//操作的role
roleId: '',
//用于重置数据
menuListCopy: [],
filterText: '',
};
},
watch: {
filterText(val) {
this.$refs.menuTree.filter(val);
}
},
created() {
this.getRoleList();
this.getAllMenuList();
},
methods: {
getRoleList() {
this.loading = true;
getRoleList(this.queryForm).then(res => {
if (res.data.code === 200) {
this.tableData = res.data.data;
this.total = res.data.total;
this.$message.success("获取角色列表成功!");
} else {
this.$message.error("获取角色列表失败!");
}
})
this.loading = false;
},
//保存按钮-【新增、修改】
submit() {
this.$refs.form.validate(valid => {
if (valid) {
this.loading = true;
if (this.form.id) {
//修改
updateRole(this.form).then(res => {
if (res.data.code == 200) {
this.$message.success("更新角色信息成功!");
this.showRole = false;
this.getRoleList();
this.form = {};
} else {
this.$message.error(res.data.message);
}
});
} else {
//新增
addRole(this.form).then(res => {
if (res.data.code == 200) {
this.$message.success("添加角色成功!");
this.showRole = false;
this.getRoleList();
} else {
this.$message.error(res.data.message);
}
});
}
}
this.loading = false;
})
},
handleAdd() {
this.form = {};
this.showRole = true;
},
handleEdit(index, row) {
getRoleById(row.id).then(res => {
if (res.data.code == 200) {
this.form = res.data.data;
this.showRole = true;
} else {
this.$message.error("获取角色信息失败!");
}
});
},
handleDelete(index, row) {
this.$confirm('确认删除角色:' + row.roleName + '吗?', '是否删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteRole(row.id).then(res => {
if (res.data.code == 200) {
this.$message.success("删除角色成功!");
this.getRoleList();
} else {
this.$message.error("删除角色失败!");
}
});
}).catch(() => {
});
},
close() {
this.showRole = false;
this.form = {};
},
closeMenuDialog() {
this.roleId = '';
this.showMenuDialog = false;
this.menuListCopy = [];
},
setMenus(row) {
this.roleId = row.id;
this.showMenuDialog = true;
getRoleById(row.id).then(res => {
if (res.data.code == 200) {
this.selectedMenus = res.data.data.menus.map(item => item.id);
//保存一份菜单列表,用于重置操作
this.menuListCopy = [...this.selectedMenus];
} else {
this.$message.error("获取角色拥有菜单失败!");
}
});
},
getAllMenuList() {
getAllMenuList().then(res => {
if (res.data.code === 200) {
this.menuList = this.mapMenuTree(res.data.data);
} else {
this.$message.error("获取菜单列表失败!");
}
});
},
saveMenus() {
// 获取所有被选中的节点(包括父节点和子节点)
const checkedNodes = this.$refs.menuTree.getCheckedNodes(false, true);
// 提取 key(即菜单 id)
const menuIds = checkedNodes.map(node => node.key);
console.log("menuIds:" + menuIds);
// const menuIds = this.$refs.menuTree.getCheckedKeys();
assignMenu({ roleId: this.roleId }, menuIds).then(res => {
if (res.data.code === 200) {
this.$message.success("保存成功!");
} else {
this.$message.error("保存失败!");
}
});
this.showMenuDialog = false;
this.selectedMenus = [];
this.menuListCopy = [];
this.roleId = null;
},
// 递归映射菜单树结构
mapMenuTree(data) {
return data.map(item => ({
key: item.id,
label: item.menuName,
children: Array.isArray(item.children) && item.children.length > 0
? this.mapMenuTree(item.children)
: undefined
}));
},
// 渲染穿梭框内容(可自定义格式)
renderMenu(h, option) {
return h('span', {}, `${option.label}`);
},
resetMenus() {
this.selectedMenus = [...this.menuListCopy];
},
// 查询
onQuery() {
this.getRoleList();
},
//分页器改变
handleSizeChange(val) {
this.queryForm.limit = val;
this.getRoleList();
},
//改变页码
handleCurrentChange(val) {
this.queryForm.page = val;
this.getRoleList();
},
// 重置表单并查询
onReset() {
this.queryForm = {
page: 1,
limit: 10,
roleName: ''
};
this.getRoleList();
},
//筛选菜单树节点
filterNode(value, data) {
if (!value) return true;
return data.label.indexOf(value) !== -1;
}
}
}
</script>
1.2.请求js调整(role.js)
javascript
import request from '@/utils/request';
/**
* 查询所有角色列表
*/
export function getAllsRoleList() {
return request({
url: '/role/getAllsRoleList',
method: 'get',
});
}
/**
* 获取角色列表
* @param {Object} params - 查询参数
*/
export function getRoleList(params) {
return request({
url: '/role/getRoleList',
method: 'post',
data: params,
});
}
/**
* 新增角色
* @param {Object} params - 新增角色参数
*/
export function addRole(params) {
return request({
url: '/role/addRole',
method: 'post',
data: params,
});
}
/**
* 修改角色
* @param {Object} params - 修改角色参数
*/
export function updateRole(params) {
return request({
url: '/role/updateRole',
method: 'post',
data: params,
});
}
/**
* 删除角色
* @param {Number|String} id - 角色 ID
*/
export function deleteRole(params) {
return request({
url: `/role/delete`,
method: 'post',
params: params,
});
}
/**
* 获取角色信息
* @param {Number|String} id - 角色 ID
*/
export function getRoleById(id) {
return request({
url: `/role/getRoleById`,
method: 'get',
params : {'id': id},
});
}
/**
* 给角色分配菜单权限
* @param {Object} params - 参数对象
* @param {Number|String} params.roleId - 角色 ID
*/
export function assignMenu(params,data) {
return request({
url: `/role/assignMenu`,
method: 'post',
params: params,
data: data,
});
}
3.菜单管理
1.1.页面功能(menu.vue)
html
<template>
<div>
<!-- 查询条件 -->
<el-form :inline="true" label-position="right" label-width="80px" :model="queryForm"
class="query-border-container">
<el-row :gutter="20" justify="center">
<!-- 菜单名称 -->
<el-col :span="7">
<el-form-item label="菜单名称">
<el-input v-model="queryForm.menuName" placeholder="请输入菜单名称"></el-input>
</el-form-item>
</el-col>
<!-- 按钮组 -->
<el-col :span="7">
<el-form-item>
<div style="display: flex; gap: 10px;">
<el-button type="primary" @click="onQuery" size="small">查询</el-button>
<el-button @click="onReset" size="small">重置</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" justify="center">
<el-col :span="7">
<div style="display: flex; gap: 10px;">
<el-button type="primary" size="small" @click="toggleAll()">展开/折叠</el-button>
</div>
</el-col>
</el-row>
</el-form>
<!-- 菜单树形列表 -->
<el-table :data="tableData" style="width: 100%;" class="table-border-container" max-height="480"
v-loading="loading" row-key="id" :tree-props="{ children: 'children' }" :default-expand-all="isExpandAll"
ref="menuTableRef">
<el-table-column type="index" label="序号" width="100" align="center">
</el-table-column>
<el-table-column prop="menuName" label="菜单名称" width="180" align="center">
</el-table-column>
<el-table-column prop="path" label="路由路径" width="180" align="center">
</el-table-column>
<el-table-column prop="component" label="组件路径" width="180" align="center">
</el-table-column>
<el-table-column prop="perms" label="权限标识" width="80" align="center">
</el-table-column>
<el-table-column prop="icon" label="图标" width="180" align="center">
</el-table-column>
<el-table-column prop="visible" label="显示/隐藏" width="80" align="center">
<template #default="scope">
<el-switch v-model="scope.row.visible" :active-value="1" :inactive-value="0" :disabled="true">
</el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="270" align="center">
<template #default="scope">
<el-button type="primary" size="small" @click="handleEdit(scope.$index, scope.row)">修改</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.$index, scope.row)"
v-if="scope.row.parentId !== 0">删除</el-button>
<el-button type="primary" size="small" @click="handleAdd(scope.$index, scope.row)">添加</el-button>
</template>
</el-table-column>
</el-table>
<!-- 添加菜单 and 修改菜单 -->
<el-dialog title="菜单信息" :visible.sync="showMenu" width="600px" append-to-body>
<el-form ref="form" :model="form" label-width="80px" center="false" :rules="rules">
<el-row>
<el-col :span="24">
<el-form-item label="上级菜单">
<!-- <el-input v-model="form.parentId" ></el-input> -->
<el-cascader v-model="parentIdCascader" :disabled="menuDisabled" :options="options"
:props="{ checkStrictly: true }" clearable></el-cascader>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="菜单名称">
<el-input v-model="form.menuName"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="路由路径">
<el-input v-model="form.path"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="组件路径">
<el-input v-model="form.component"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="权限标识">
<el-input v-model="form.perms"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="图标">
<el-input v-model="form.icon"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序">
<el-input v-model="form.sort"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="显示/隐藏">
<!-- <el-input v-model="form.visible"></el-input> -->
<el-switch v-model="form.visible" :active-value="1" :inactive-value="0">
</el-switch>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer" style="text-align: center;">
<el-button type="primary" size="small" @click="submit()">保存</el-button>
<el-button @click="close()" size="small">取消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getMenuList, addMenu, updateMenu, deleteMenu, getMenuById } from '@/api/permission/menu'
export default {
name: 'menuView',
data() {
return {
queryForm: {
menuName: '',
},
tableData: [],
loading: false,
menuDisabled: false,
showMenu: false,
form: {},
menuOptions: [],
parentIdCascader: [],
rules: {
menuName: [
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
],
path: [
{ required: true, message: '请输入菜单地址', trigger: 'blur' },
],
component: [
{ required: true, message: '请输入菜单图标', trigger: 'blur' },
],
icon: [
{ required: true, message: '请输入菜单图标', trigger: 'blur' },
],
perms: [
{ required: true, message: '请输入权限标识', trigger: 'blur' },
],
},
isExpandAll: true,
}
},
created() {
this.getMenuList();
},
methods: {
// 查询
onQuery() {
this.getMenuList()
},
// 重置
onReset() {
this.queryForm = {
menuName: '',
};
this.getMenuList();
},
getMenuList() {
getMenuList(this.queryForm).then(res => {
if (res.data.code === 200) {
this.tableData = res.data.data || [];
// this.total = res.data.total;
this.$message.success("获取菜单列表成功!");
this.options = this.buildCascaderOptions(this.tableData);
} else {
this.$message.error("获菜单列表失败!");
}
});
},
//菜单列表转换成级联选择器数据
buildCascaderOptions(data) {
return data.map(item => ({
value: item.id,
label: item.menuName,
children: item.children && item.children.length > 0
? this.buildCascaderOptions(item.children)
: undefined
}));
},
// 递归查找父级路径
findParentPath(data, targetId, path = []) {
for (const item of data) {
const currentPath = [...path, item.value];
if (item.value === targetId) {
return currentPath;
}
if (item.children && item.children.length > 0) {
const result = this.findParentPath(item.children, targetId, currentPath);
if (result) return result;
}
}
return null;
},
toggleAll() {
this.isExpandAll = !this.isExpandAll;
this.tableData.forEach(row => {
this.$refs.menuTableRef.toggleRowExpansion(row, this.isExpandAll);
});
},
handleAdd(index, row) {
// this.form = {};
this.form.parentId = row.id;
console.table("父菜单id" + row);
this.showMenu = true;
this.menuDisabled = true;
},
handleEdit(index, row) {
getMenuById(row.id).then(res => {
if (res.data.code == 200) {
this.form = res.data.data;
this.parentIdCascader = this.findParentPath(this.options, this.form.parentId) || [];
} else {
this.$message.error("获取菜单信息失败!");
}
});
this.showMenu = true;
this.menuDisabled = false;
},
handleDelete(index, row) {
this.$confirm('确认删除菜单:' + row.menuName + '吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteMenu(row.id).then(res => {
if (res.data.code == 200) {
this.getMenuList();
this.$message.success("删除成功!");
} else {
this.$message.error(res.data.message);
}
});
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
// 保存菜单
submit() {
this.$refs.form.validate(valid => {
if (valid) {
this.loading = true;
this.form.parentId = this.parentIdCascader[this.parentIdCascader.length - 1];
if (this.form.id) {
updateMenu(this.form).then(res => {
if (res.data.code === 200) {
this.$message.success("修改成功!");
this.showMenu = false;
this.getMenuList();
} else {
this.$message.error(res.data.message);
}
})
} else {
addMenu(this.form).then(res => {
if (res.data.code === 200) {
this.$message.success("添加成功!");
this.showMenu = false;
this.getMenuList();
} else {
this.$message.error(res.data.message);
}
})
}
}
this.loading = false;
})
},
close() {
this.form = {};
this.menuDisabled = false;
this.showMenu = false;
},
}
}
</script>
1.2.请求js调整(menu.js)
javascript
import request from '@/utils/request';
/**
* 查询所有菜单列表-【树形结构】
*
*/
export function getAllMenuList() {
return request({
url: '/menu/getAllMenuList',
method: 'get',
});
}
/**
* 查询菜单列表 - 【树形结构】
* @param {Object} params - 菜单参数
*/
export function getMenuList(data) {
return request({
url: '/menu/getMenuList',
method: 'post',
data: data,
});
}
/**
* 查询登录角色菜单列表
* @param {Object} params - 菜单参数
*/
export function getMenuListByUserId(userId) {
return request({
url: '/menu/getMenuListByUserId',
method: 'get',
params: {'userId': userId},
});
}
/**
* 新增菜单
* @param {Object} data - 菜单信息
*/
export function addMenu(data) {
return request({
url: '/menu/addMenu',
method: 'post',
data: data,
});
}
/**
* 修改菜单
* @param {Object} data - 菜单信息
*/
export function updateMenu(data) {
return request({
url: '/menu/updateMenu',
method: 'post',
data: data,
});
}
/**
* 删除菜单
* @param {Number|String} id - 菜单 ID
*/
export function deleteMenu(id) {
return request({
url: `/menu/delete`,
method: 'post',
params: {'id': id},
});
}
/**
* 获取菜单
* @param {Number|String} id - 菜单 ID
*/
export function getMenuById(id) {
return request({
url: `/menu/getMenuById`,
method: 'get',
params: {'id': id},
});
}
4.配置调整
1.1配置权限管理路由
在router/下新建permission.js如下:
javascript
export default [
{
path: 'user',
name: 'user',
component: () => import('@/view/permission/user.vue'),
meta: { title: '用户管理', requiresAuth: true }
}
,
{
path: 'role',
name: 'role',
component: () => import('@/view/permission/role.vue'),
meta: { title: '角色管理', requiresAuth: true }
}
,
{
path: 'menu',
name: 'menu',
component: () => import('@/view/permission/menu.vue'),
meta: { title: '菜单管理', requiresAuth: true }
}
]
在router/index.js中引入权限管理路由
javascript
import permission from './permission.js'
......
{
path: '/',
name: 'Index',
component: Index,
redirect: '/login', // 默认重定向到 /home
children: [
{
path: '/home',
name: 'home',
component: () => import('@/view/home.vue'),
meta: { title: '首页', requiresAuth: true }
},
// 其他子路由也可以放在这里
...permission,
]
},
5.首页布局
1.1.菜单动态化显示
html
<template>
<el-container style="height: 100vh;">
<el-main class="login-main">
<el-row type="flex" justify="center" align="middle" style="height: 100%;">
<el-col :xs="20" :sm="12" :md="8" :lg="6" :xl="4">
<el-card class="login-card">
<div slot="header" class="login-header">
<h2>用户管理平台</h2>
</div>
<el-form ref="form" :model="formData" label-width="80px" :rules="rules">
<el-form-item label="用户名" prop="username">
<el-input v-model="formData.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" show-password placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="login" style="width: 100%;">登录</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
</el-main>
</el-container>
</template>
<script>
import { login } from '@/api/login';
const jwtDecode = require('jwt-decode').default;
export default {
name: 'UserLogin',
data() {
return {
formData: {
username: '',
password: ''
},
rules: {
username: [
{ required: true, message: '用户名不能为空', trigger: 'blur' }
],
password: [
{ required: true, message: '密码不能为空', trigger: 'blur' }
],
}
};
},
methods: {
async login() {
try {
const res = await login(this.formData);
console.log('jwtDecode:', jwtDecode);
console.log('res.data.data', res.data.data)
if (res.data.code === 200) {
const token = res.data.data;
const decoded = this.parseJwt(token.trim());
console.log('decoded', decoded);
localStorage.setItem('token', token);
// 跳转到首页 传递用户id
this.$router.push({
path: '/home',
query: { userId: decoded.sub }
});
// this.$router.push('/');
this.$message.success('登录成功');
} else {
this.$message.error(res.data.message || '登录失败');
}
} catch (error) {
this.$message.error('请求异常,请检查网络或服务端状态');
}
},
parseJwt(token) {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
return JSON.parse(jsonPayload);
} catch (e) {
console.error('JWT 解析失败:', e);
return null;
}
}
}
};
</script>
<style scoped>
.login-main {
background: linear-gradient(to right, #e0f7fa, #fffde7);
/* 柔和渐变背景 */
}
.login-card {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-radius: 10px;
}
.login-header {
text-align: center;
margin-bottom: 20px;
}
</style>
解析token获取用户Id,查询用户详细信息(基本信息、角色、菜单等)
javascript
//解析前要安装jwt依赖
npm install jwt-decode --save
有时新版 jwt-decode@4.0.0
在 Vue 2 中有问题,你可以尝试降级:
javascript
npm install jwt-decode@3.1.2 --save
1.2.首页统计信息展示
1.在view/新建home.vue
html
<template>
<el-container class="home-page">
<!-- 上部:统计数字 -->
<el-header>
<el-row :gutter="20" class="stats-header">
<el-col :span="6" v-for="(stat, index) in stats" :key="index">
<el-card class="chart-container stat-card">
<el-statistic :title="stat.title" :value="stat.value"
:value-style="{ fontSize: '20px', color: '#333' }">
<template #suffix v-if="stat.suffix">
{{ stat.suffix }}
</template>
</el-statistic>
</el-card>
</el-col>
</el-row>
</el-header>
<!-- 中部:图表 -->
<el-main>
<el-row :gutter="20" class="chart-section">
<el-col :span="12">
<el-card class="chart-container">
<div slot="header" class="card-header">月度趋势分析</div>
<div id="bar-chart"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card class="chart-container">
<div slot="header" class="card-header">用户角色占比</div>
<div id="pie-chart"></div>
</el-card>
</el-col>
</el-row>
</el-main>
<!-- 下部:项目信息 -->
<el-footer>
<el-row :gutter="20" class="info-section">
<el-col :span="8">
<el-card class="section-card small-card">
<div slot="header" class="card-header">项目介绍</div>
<p>这是一个基于 Vue.js 和 Element Plus 的后台管理系统...</p>
</el-card>
</el-col>
<el-col :span="8">
<el-card class="section-card small-card">
<div slot="header" class="card-header">所用技术</div>
<ul>
<li>Vue.js, Vue Router, Vuex</li>
<li>Element Plus</li>
</ul>
</el-card>
</el-col>
<el-col :span="8">
<el-card class="section-card small-card">
<div slot="header" class="card-header">功能模块</div>
<ul>
<li>用户管理</li>
<li>权限管理</li>
</ul>
</el-card>
</el-col>
</el-row>
</el-footer>
</el-container>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'homeView',
data() {
return {
stats: [
{ title: '用户总数', value: 12345 },
{ title: '本月活跃人数', value: 4567 },
{ title: '转化率', value: 68.4, suffix: '%' },
{ title: '本月增长人数', value: 987 }
]
}
},
mounted() {
this.$nextTick(() => {
this.initBarChart();
this.initPieChart();
});
},
methods: {
initBarChart() {
const chart = echarts.init(document.getElementById('bar-chart'));
const option = {
color: ['#a3c4dc', '#b6d7a8', '#f9cb9c', '#ead1dc'], // 浅色系配色
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
legend: {
data: ['用户总数', '活跃人数', '增长人数', '转化率']
},
xAxis: {
type: 'category',
data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
},
yAxis: [
{ name: '数量', type: 'value' },
{ name: '转化率', type: 'value' }
],
series: [
{ name: '用户总数', type: 'bar', data: [12000, 12500, 13000, 13500, 14000, 14500] },
{ name: '活跃人数', type: 'bar', data: [4000, 4200, 4500, 4600, 4800, 5000] },
{ name: '增长人数', type: 'bar', data: [800, 900, 950, 1000, 1100, 1200] },
{
name: '转化率', type: 'line', yAxisIndex: 1, data: [65, 66, 67, 68, 69, 70],
itemStyle: {
borderRadius: 2,
borderColor: '#d9d9d9',
borderWidth: 1
}
}
]
};
chart.setOption(option);
},
initPieChart() {
const chart = echarts.init(document.getElementById('pie-chart'));
const option = {
tooltip: { trigger: 'item' },
legend: { show: false },
color: ['#a3c4dc', '#b6d7a8', '#f9cb9c', '#ead1dc'],
series: [{
type: 'pie',
radius: '70%',
data: [
{ value: 120, name: '管理员' },
{ value: 80, name: '编辑' },
{ value: 150, name: '访客' },
{ value: 200, name: '普通用户' }
],
insideLabel: {
show: true
},
label: {
show: true,
position: 'outside',
fontSize: 16,
formatter: '{b}:({d}%)', // 显示名称、数值、百分比
rich: {
b: (params) => ({ color: params.color }) // 名称颜色跟随扇区颜色
},
color: 'inherit' // 关键:继承数据项颜色
},
labelLine: {
show: true,
length: 15,
length2: 25,
lineStyle: {
width: 1.5,
type: 'solid',
color: null // 关键:线条颜色继承扇区颜色(inherit在这里不生效所以设置为null,会自动继承)
}
},
roseType: true,
itemStyle: {
borderRadius: 5,
borderColor: '#fff', // 白色边框(可根据背景调整)
borderWidth: 2, // 边框宽度
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.2)'
}
}]
};
chart.setOption(option);
}
}
};
</script>
<style scoped>
.home-page {
padding: 10px;
background-color: #f5f7fa;
font-family: 'Segoe UI', sans-serif;
}
/* 新增:为 header 添加底部边距 */
.el-header {
margin-bottom: 10px !important;
}
.stats-header .el-row :deep(.el-col) {
padding: 10px;
}
.stat-card {
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 20px;
text-align: center;
font-size: 18px;
height: 80px !important;
}
.stat-card strong {
font-size: 20px;
color: #333;
}
.chart-section {
margin-top: 20px;
}
.info-section {
margin-top: 0px;
}
.chart-container,
.card-container {
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 10px;
height: 400px;
position: relative;
}
#bar-chart,
#pie-chart {
width: 100%;
height: 100%;
min-height: 300px;
}
.section-card {
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 15px;
font-size: 13px;
}
.small-card {
font-size: 12px;
}
.card-header {
font-size: 16px;
font-weight: bold;
color: #333;
}
</style>
使用echart图表需要安装以下依赖:
javascript
npm install echarts --save
2.在router/index.js中,添加home.vue路由到index下,如下:
javascript
{
path: '/',
name: 'Index',
component: Index,
redirect: '/login', // 默认重定向到 /home
children: [
{
path: '/home',
name: 'home',
component: () => import('@/view/home.vue'),
meta: { title: '首页', requiresAuth: true }
},
// 其他子路由也可以放在这里
...permission,
]
},
(注:首页数据由于后端整体功能还需扩展,例如转化率、图表、活跃度等需要日志相关信息,暂定使用假数据,后续会继续开发维护。)
6.其他调整
1.标签调整
修改项目启动后浏览器显示的标签页名称(修改public/index.html的<title></title>)如下:
html
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<!-- <title><%= htmlWebpackPlugin.options.title %></title> -->
<title>我的管理系统</title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
四、权限管理逻辑
1.用户-角色-菜单

2.权限管理功能

3.权限管理功能规则
1.用户-角色是一对多关系,即一个用户可以有多个角色,且不同用户可以有部分相同角色。
2.角色-菜单是一对多关系,即一个角色可以有多个菜单权限,且不同角色可以有部分相同菜单。
3.角色-菜单权限,拥有一个菜单权限,则必然有其父菜单(如果有父菜单)权限,父子联动。
五、附:源码
1.源码下载地址
https://gitee.com/wangaolin/user-demo.git
同学们有需要可以自行下载查看,此文章是dev-vue分支
六、结语
此次开发总结:
- 整体流程我做了简单测试(功能测试),未压测、未安测,可能也不需要。
- 整个项目相对来说还可以,虽然不少地方有些粗糙(还能用),奈何博主能力有限
- 后续我会继续优化,也欢迎各位同学指出问题,加以改进。
部分调整可能未全发布,多多少少会有漏的,有需要全代码的同学自行下载。
(注:接定制化开发前后端分离项目,私我)