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

六、结语

此次开发总结:

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

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

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

相关推荐
JH30739 小时前
SpringBoot 优雅处理金额格式化:拦截器+自定义注解方案
java·spring boot·spring
qq_124987075312 小时前
基于SSM的动物保护系统的设计与实现(源码+论文+部署+安装)
java·数据库·spring boot·毕业设计·ssm·计算机毕业设计
Coder_Boy_12 小时前
基于SpringAI的在线考试系统-考试系统开发流程案例
java·数据库·人工智能·spring boot·后端
2301_8187320612 小时前
前端调用控制层接口,进不去,报错415,类型不匹配
java·spring boot·spring·tomcat·intellij-idea
汤姆yu16 小时前
基于springboot的尿毒症健康管理系统
java·spring boot·后端
暮色妖娆丶16 小时前
Spring 源码分析 单例 Bean 的创建过程
spring boot·后端·spring
·云扬·17 小时前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
biyezuopinvip17 小时前
基于Spring Boot的企业网盘的设计与实现(任务书)
java·spring boot·后端·vue·ssm·任务书·企业网盘的设计与实现
JavaGuide17 小时前
一款悄然崛起的国产规则引擎,让业务编排效率提升 10 倍!
java·spring boot
figo10tf18 小时前
Spring Boot项目集成Redisson 原始依赖与 Spring Boot Starter 的流程
java·spring boot·后端