SpringBoot+Mybatis+MySQL+Vue+ElementUI前后端分离版:权限管理(三)

目录

一、前言

二、后端开发及调整

1.新增实体(表)

2.DTO类调整

1.Resp类

2.Req类

3.用户管理

4.角色管理

5.菜单管理

6.配置调整

1.优化JwtUtils工具类

2.优化JwtInterceptor拦截器

三、前端开发及调整

1.用户管理

1.1.页面功能(user.vue)

1.2.请求js调整(user.js)

2.角色管理

1.1.页面功能(role.vue)

1.2.请求js调整(role.js)

3.菜单管理

1.1.页面功能(menu.vue)

1.2.请求js调整(menu.js)

4.配置调整

5.首页布局

1.1.菜单动态化显示

1.2.首页统计信息展示

6.其他调整

1.标签调整

四、权限管理逻辑

1.用户-角色-菜单

2.权限管理功能规则

五、附:源码

1.源码下载地址

六、结语

一、前言

此文章在上次调整的基础上开发后端管理系统必备的权限管理功能,具体功能包含用户管理、角色管理、菜单管理,并对页面整体布局进行了调整。

此项目是在我上一个文章的后续开发, 需要的同学可以关注一下,文章链接如下: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;

}
  1. 角色-菜单表(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;

}
  1. 权限管理表关联

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分支

六、结语

此次开发总结:

  • 整体流程我做了简单测试(功能测试),未压测、未安测,可能也不需要。
  • 整个项目相对来说还可以,虽然不少地方有些粗糙(还能用),奈何博主能力有限
  • 后续我会继续优化,也欢迎各位同学指出问题,加以改进。

部分调整可能未全发布,多多少少会有漏的,有需要全代码的同学自行下载。

(注:接定制化开发前后端分离项目,私我)

相关推荐
神仙别闹43 分钟前
基于Java+MySQL实现(Web)文件共享管理系统(仿照百度文库)
java·前端·mysql
wsj__WSJ1 小时前
IDEA(2024.3.1) 配置 Spring Boot 热部署
java·spring boot·intellij-idea
_代号0071 小时前
MySQL梳理二:索引
后端·mysql
匚WYHaovous2 小时前
SQL执行计划分析-分页查询
mysql
匚WYHaovous2 小时前
mysql执行计划分析-分页查询
mysql
懂得节能嘛.3 小时前
【SpringAI实战】实现仿DeepSeek页面对话机器人
java·开发语言·spring boot
LUCIAZZZ3 小时前
TTL+日志的MDC实现简易链路追踪
java·大数据·spring boot·spring·操作系统·计算机系统
paopaokaka_luck4 小时前
基于SpringBoot+Uniapp的非遗文化宣传小程序(AI问答、协同过滤算法、Echarts图形化分析)
java·vue.js·spring boot·后端·学习·小程序·uni-app
NineData4 小时前
NineData新增SQL Server到MySQL复制链路,高效助力异构数据库迁移
数据库·人工智能·mysql
程序员阿明4 小时前
netty的编解码器,以及内置的编解码器
java·spring boot