通用权限管理系统PermissionAdmin

通用权限管理系统PermissionAdmin

基于SpringBoot+Vue前后端分离通用权限管理后台系统PermissionAdmin

一、技术架构

总体采用前后端分离架构

后端:

主要开发框架:SpringBoot2x+MyBatisPlus3.4.1

开发语言:Java

开发技术:java技术、SpringBoot技术、MyBatisPlus代码生成器、MyBatisPlus分页技术、MyBatisPlus查询技术、Lombok技术、文件上传技术、screw数据库文档生成技术、kaptcha技术、mail邮箱验证码技术、Sa-token权限技术等

数据库:MySQL5.7

数据库测试工具:Navicat Premium 12

前端:

主要开发框架:Node.js16+Vue2x

开发语言:vue2

开发技术:vue技术、vue-router技术、axios技术、vuex技术、路由守卫技术、element-ui技术、echarts技术、邮箱验证码技术等

二、数据库搭建

1.数据库设计文档

数据库名: permissionadmin

文档版本: 1.0.0

文档描述: 数据库设计文档生成

表名说明
menu菜单表
role角色表
role_menu角色-菜单表
user用户表
user_role用户-角色表

表名: menu

说明:

数据列:

序号名称数据类型长度小数位允许空值主键默认值说明
1idbigint200NY菜单ID
2parent_idbigint200YN父菜单ID,一级菜单为0
3namevarchar640NN菜单名称
4pathvarchar2550YN菜单URL
5permsvarchar2550YN授权(多个用逗号分隔,如:user:list,user:create)
6componentvarchar2550YN菜单组件
7typeint100NN类型0:目录1:菜单2:按钮
8iconvarchar320YN菜单图标
9orderNumint100YN排序
10statusint100NN1菜单状态

表名: role

说明:

数据列:

序号名称数据类型长度小数位允许空值主键默认值说明
1idbigint200NY角色ID
2namevarchar640NN角色名称
3codevarchar640NN角色代码
4remarkvarchar640YN备注
5createddatetime190YNCURRENT_TIMESTAMP角色创建时间
6statusint100NN角色状态

表名: role_menu

说明:

数据列:

序号名称数据类型长度小数位允许空值主键默认值说明
1idbigint200NY角色菜单关联ID
2role_idbigint200NN角色ID
3menu_idbigint200NN菜单ID

表名: user

说明:

数据列:

序号名称数据类型长度小数位允许空值主键默认值说明
1idint100NY用户ID
2usernamevarchar500NN用户名
3passwordvarchar500NN用户密码
4avatarvarchar2550NNavatar3用户头像
5emailvarchar640YN用户邮箱
6createddatetime190NNCURRENT_TIMESTAMP用户注册时间
7statusint100NN1账号状态,注销为0,正常为1

表名: user_role

说明:

数据列:

序号名称数据类型长度小数位允许空值主键默认值说明
1idbigint200NY用户角色关联ID
2user_idbigint200NN用户ID
3role_idbigint200NN角色ID

2.建表sql

CREATE TABLE `user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
  `password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户密码',
  `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'avatar3' COMMENT '用户头像',
  `email` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户邮箱',
  `created` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '用户注册时间',
  `status` int(255) NOT NULL DEFAULT 1 COMMENT '账号状态,注销为0,正常为1',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `UK_USERNAME`(`username`) USING BTREE
);

CREATE TABLE `role`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色名称',
  `code` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色代码',
  `remark` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '备注',
  `created` datetime(0) DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '角色创建时间',
  `status` int(5) NOT NULL COMMENT '角色状态',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `name`(`name`) USING BTREE,
  UNIQUE INDEX `code`(`code`) USING BTREE
);

CREATE TABLE `menu`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `parent_id` bigint(20) DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
  `name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '菜单名称',
  `path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '菜单URL',
  `perms` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',
  `component` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '菜单组件',
  `type` int(5) NOT NULL COMMENT '类型     0:目录   1:菜单   2:按钮',
  `icon` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '菜单图标',
  `orderNum` int(11) DEFAULT NULL COMMENT '排序',
  `status` int(5) NOT NULL DEFAULT 1 COMMENT '菜单状态',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `name`(`name`) USING BTREE
) ;

CREATE TABLE `user_role`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户角色关联ID',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`) USING BTREE
);

CREATE TABLE `role_menu`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色菜单关联ID',
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  `menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
  PRIMARY KEY (`id`) USING BTREE
);

三、后端搭建

1.新建空项目PermissionAdmin

2.新建后端springboot模块springboot-permission-admin

3.添加pom依赖

<!--        Spring Boot的Web启动器,用于创建基于Spring MVC的Web应用程序。-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<!--        MySQL Connector/J驱动,用于连接MySQL数据库。-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.44</version>
            <scope>runtime</scope>
        </dependency>
<!--        MyBatis Plus Generator,用于自动生成MyBatis的Mapper、Service等代码。-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.1</version>
        </dependency>
<!--        Lombok,用于简化Java实体类的编写,自动生成Getters、Setters等方法。-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
<!--        Spring Boot的测试启动器,用于编写单元测试。-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
<!--        MyBatis Plus的Spring Boot集成,简化了MyBatis Plus的配置。-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.1</version>
        </dependency>
<!--        FreeMarker模板引擎,用于生成动态HTML页面。-->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.30</version>
        </dependency>
<!--        Swagger,用于生成API文档。-->
        <dependency>
            <groupId>com.spring4all</groupId>
            <artifactId>spring-boot-starter-swagger</artifactId>
            <version>1.5.1.RELEASE</version>
        </dependency>
<!--        Apache Commons IO,提供了常用的IO操作工具类。-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>
<!--        Apache Commons FileUpload,用于处理文件上传。-->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.4</version>
        </dependency>
<!--        Screw,用于生成数据库文档。-->
        <dependency>
            <groupId>cn.smallbun.screw</groupId>
            <artifactId>screw-core</artifactId>
            <version>1.0.5</version>
        </dependency>
<!--        Kaptcha,用于生成验证码图片。-->
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>
<!--        Spring Boot的邮件发送功能启动器,用于发送电子邮件。-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
           <!-- hutool 工具类 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.8</version>
        </dependency>
           <!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot-starter</artifactId>
            <version>1.30.0</version>
        </dependency>

4.编写配置类

(1)MyBatis Plus的配置类,配置MyBatis Plus的分页插件
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {

    /**
     * 配置MyBatis Plus的分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        
        // 创建PaginationInnerInterceptor对象,并设置数据库类型为MySQL
        PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        
        // 将PaginationInnerInterceptor对象添加为MybatisPlusInterceptor的内部拦截器
        interceptor.addInnerInterceptor(paginationInterceptor);
        
        return interceptor;
    }
}
(2)跨域请求配置类
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    /**
     * 配置跨域请求
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                // 是否发送Cookie
                .allowCredentials(true)
                // 放行哪些原始域
                .allowedOriginPatterns("*")
                // 允许的请求方法
                .allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"})
                // 允许的请求头
                .allowedHeaders("*");
    }
}
(3)验证码生成器配置类
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

@Configuration
public class KaptchaConfig {

    /**
     * 配置验证码生成器
     */
    @Bean
    DefaultKaptcha producer() {
        // 创建属性对象,用于配置验证码生成器
        Properties properties = new Properties();
        properties.put("kaptcha.border", "no");  // 设置边框样式为无边框
        properties.put("kaptcha.textproducer.font.color", "black");  // 设置验证码文字颜色为黑色
        properties.put("kaptcha.textproducer.char.space", "4");  // 设置验证码字符间距为4个像素
        properties.put("kaptcha.image.height", "40");  // 设置验证码图片高度为40像素
        properties.put("kaptcha.image.width", "120");  // 设置验证码图片宽度为120像素
        properties.put("kaptcha.textproducer.font.size", "30");  // 设置验证码文字大小为30像素

        // 创建配置对象,使用上述属性初始化
        Config config = new Config(properties);

        // 创建默认的验证码生成器
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);  // 将配置对象设置到验证码生成器中

        return defaultKaptcha;  // 返回配置好的验证码生成器
    }

}

5.编写工具类

(1)代码生成器
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class CodeGenerator {
    /**
     * 读取控制台内容
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotBlank(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }

    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir")+"/springboot-permission-admin";
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor("longyi");
        gc.setOpen(false);
        gc.setSwagger2(true); // 实体属性 Swagger2 注解
        gc.setBaseResultMap(true);// XML ResultMap
        gc.setBaseColumnList(true);// XML columList
        gc.setServiceName("%sService"); // 去掉service接口首字母的I, 如DO为User则叫UserService
        mpg.setGlobalConfig(gc);

        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        // 修改数据源
        dsc.setUrl("jdbc:mysql://localhost:3306/permissionadmin?useUnicode=true&characterEncoding=UTF8&useSSL=false");
        dsc.setDriverName("com.mysql.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("root");
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
        // 模块配置
        pc.setParent("com.longyi.springbootpermissionadmin")
                .setEntity("entity")
                .setMapper("mapper")
                .setService("service")
                .setServiceImpl("service.impl")
                .setController("controller");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // do nothing
            }
        };
        String templatePath = "templates/mapper.xml.ftl";
        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名,如果你的Entity设置了前后缀,此处注意xml的名称会跟着发生变化!
                return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
                        + "/" + tableInfo.getEntityName() + "Mapper" +
                        StringPool.DOT_XML;
            }
        });
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();
        templateConfig.setXml(null); // 不生成xml文件
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
    System.out.println("user,role,menu,user_role,role_menu");
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }
}
(2)数据库文档生成器
import cn.smallbun.screw.core.Configuration;import cn.smallbun.screw.core.engine.EngineConfig;
import cn.smallbun.screw.core.engine.EngineFileType;
import cn.smallbun.screw.core.engine.EngineTemplateType;
import cn.smallbun.screw.core.execute.DocumentationExecute;import cn.smallbun.screw.core.process.ProcessConfig;import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;import java.util.ArrayList;

public class ScrewGenerator {
    public static void main(String[] args){
        documentGeneration();
    }

    static void documentGeneration() {
        //数据源
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setDriverClassName("com.mysql.jdbc.Driver");
        hikariConfig.setJdbcUrl("jdbc:mysql://localhost:3306/permissionadmin?useUnicode=true&characterEncoding=UTF8&useSSL=false");
        hikariConfig.setUsername("root");
        hikariConfig.setPassword("root");
        String fileOutputDir = System.getProperty("user.dir");
        //设置可以获取tables remarks信息
        hikariConfig.addDataSourceProperty("useInformationSchema", "true");
        hikariConfig.setMinimumIdle(2);
        hikariConfig.setMaximumPoolSize(5);
        DataSource dataSource = new HikariDataSource(hikariConfig);
        // 生成配置
        EngineConfig engineConfig =
                EngineConfig.builder()
                        // 生成文件路径
                        .fileOutputDir(fileOutputDir)
                        // 打开目录
                        .openOutputDir(true)
                        // 文件类型
                        .fileType(EngineFileType.MD)
                        // 生成模板实现
                        .produceType(EngineTemplateType.freemarker)
                        // 自定义文件名称
//                        .fileName("基于springboot+vue的共享单车时空地图综合管理平台系统数据库文档")
                        .fileName("PermissionAdmin通用权限管理系统数据库文档")
                        .build();

        //忽略表
        ArrayList<String> ignoreTableName = new ArrayList<>();
        ignoreTableName.add("test_user");
        ignoreTableName.add("test_group");
        //忽略表前缀
        ArrayList<String> ignorePrefix = new ArrayList<>();
        ignorePrefix.add("test_");
        //忽略表后缀
        ArrayList<String> ignoreSuffix = new ArrayList<>();
        ignoreSuffix.add("_test");
        ProcessConfig processConfig = ProcessConfig.builder()
                //指定生成逻辑、当存在指定表、指定表前缀、指定表后缀时,将生成指定表,其余表不生成、并跳过忽略表配置
                //根据名称指定表生成
                .designatedTableName(new ArrayList<>())
                //根据表前缀生成
                .designatedTablePrefix(new ArrayList<>())
                //根据表后缀生成
                .designatedTableSuffix(new ArrayList<>())
                //忽略表名
                .ignoreTableName(ignoreTableName)
                //忽略表前缀
                .ignoreTablePrefix(ignorePrefix)
                //忽略表后缀
                .ignoreTableSuffix(ignoreSuffix).build();
        //配置
        Configuration config = Configuration.builder()
                //版本
                .version("1.0.0")
                //描述
                .description("数据库设计文档生成")
                //数据源
                .dataSource(dataSource)
                //生成配置
                .engineConfig(engineConfig)
                //生成配置
                .produceConfig(processConfig)
                .build();
        //执行生成
        new DocumentationExecute(config).execute();
    }
}

(3)路径获取
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;

@Component
public class GetPath {

  @Autowired
  private ApplicationContext applicationContext;

  /**
   * 获取项目路径
   *
   * @return 项目路径
   * @throws IOException 当获取路径时发生IO异常
   */
  public String getPath() throws IOException {
    ResourceLoader resourceLoader = applicationContext;
    Resource resource = resourceLoader.getResource("classpath:");
    String path =
        resource.getFile().getParentFile().getParentFile().getParentFile().getAbsolutePath();
    return path;
  }

  /**
   * 获取项目中静态资源的绝对路径
   *
   * @return 静态资源绝对路径
   * @throws IOException 当获取路径时发生IO异常
   */
  public String getProjectAbsolutePath() throws IOException {
    return getPath() + "\\springboot-permission-admin\\src\\main\\resources\\static";
  }
}
(4)分页查询参数的封装
import lombok.Data;
import java.util.HashMap;

@Data
public class QueryPageParam {
    // 默认每页数量
    private static int PAGE_SIZE=20;
    // 默认页码
    private static int PAGE_NUM=1;

    // 每页数量
    private int pageSize=PAGE_SIZE;
    // 页码
    private int pageNum=PAGE_NUM;

    // 查询参数
    private HashMap<String, Object> param = new HashMap<>();
}
(5)返回结果集的封装
import lombok.Data;

@Data
public class Result {

    private int code;       // 编码 200/400
    private String msg;     // 消息 成功/失败
    private Long total;     // 总记录数
    private Object data;    // 数据

    /**
     * 创建一个表示失败的Result对象
     *
     * @return Result对象
     */
    public static Result fail() {
        return result(400, "失败", 0L, null);
    }

    /**
     * 创建一个表示失败的Result对象,并指定消息
     *
     * @param msg 失败消息
     * @return Result对象
     */
    public static Result fail(String msg) {
        return result(400, msg, 0L, null);
    }

    /**
     * 创建一个表示成功的Result对象
     *
     * @return Result对象
     */
    public static Result suc() {
        return result(200, "成功", 0L, null);
    }

    /**
     * 创建一个表示成功的Result对象,并指定数据
     *
     * @param data 成功返回的数据
     * @return Result对象
     */
    public static Result suc(Object data) {
        return result(200, "成功", 0L, data);
    }

    /**
     * 创建一个表示成功的Result对象,并指定数据和总记录数
     *
     * @param data  成功返回的数据
     * @param total 总记录数
     * @return Result对象
     */
    public static Result suc(Object data, Long total) {
        return result(200, "成功", total, data);
    }

    /**
     * 根据指定参数创建Result对象
     *
     * @param code  编码
     * @param msg   消息
     * @param total 总记录数
     * @param data  数据
     * @return Result对象
     */
    private static Result result(int code, String msg, Long total, Object data) {
        Result res = new Result();
        res.setData(data);
        res.setMsg(msg);
        res.setCode(code);
        res.setTotal(total);
        return res;
    }

}
(6)随机验证码的生成
import java.util.Random;

public class CodeCreate {
    /**
     * 生成随机字符验证码
     *
     * @param length 验证码长度
     * @return 随机字符验证码
     */
    public static String RandomStringCode(int length) {
        String characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
        Random random = new Random();
        StringBuilder code = new StringBuilder();

        for (int i = 0; i < length; i++) {
            int index = random.nextInt(characters.length());
            code.append(characters.charAt(index));
        }

        return code.toString();
    }

    /**
     * 生成随机数字验证码
     *
     * @param length 验证码长度
     * @return 随机数字验证码
     */
    public static String RandomNumberCode(int length) {
        String characters = "0123456789";
        Random random = new Random();
        StringBuilder code = new StringBuilder();

        for (int i = 0; i < length; i++) {
            int index = random.nextInt(characters.length());
            code.append(characters.charAt(index));
        }

        return code.toString();
    }
}

6.yml的配置

server:                           # 配置服务器相关信息
  port: 8090                      # 配置端口号

spring:                           # 配置Spring框架相关信息
  datasource:                    # 配置数据源
    url: jdbc:mysql://localhost:3306/permissionadmin?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8   # 数据库连接URL
    driver-class-name: com.mysql.jdbc.Driver     # 数据库驱动
    username: root                # 数据库用户名
    password: root                # 数据库密码
  mail:                           # 配置邮件发送相关信息
    default-encoding: UTF-8      # 邮件编码方式
    host: smtp.qq.com            # 邮箱SMTP服务器地址
    username:   # 发件人邮箱
    password:    # 发件人邮箱授权码
  servlet:                       # 配置Servlet相关信息
    multipart:                   # 配置Spring Boot的文件上传功能
      enabled: true              # 是否启用文件上传功能
      max-file-size: 100MB       # 允许上传单个文件的最大大小
      max-request-size: 100MB    # 允许上传多个文件的总大小限制
    http:                        # 配置HTTP相关信息
      multipart:                 # 启用HTTP的文件上传功能
        enabled=true             # 是否启用文件上传功能
  logging:                       # 配置日志相关信息
    level:                       # 配置包的日志等级
      com.longyi.springboot-permission-admin: debug   # 为com.longyi.springboot-permission-admin包开启debug级别日志输出
  ## Sa-Token配置
  sa-token:
    ## token 名称 (同时也是cookie名称)
    token-name: token
    ## token 有效期,单位s 默认30天, -1代表永不过期
    timeout: 2592000
    ## token 临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
    activity-timeout: -1
    ## 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
    is-concurrent: true
    ## 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
    is-share: false
    ## token风格
    token-style: uuid
    ## 是否输出操作日志
    is-log: true

7.QQ邮箱的授权码获取

(1)登录进自己的qq邮箱

(2)进入设置

(3)在常规或者账号中找到并开启第三方服务IMAP/SMTP服务

(4)完成验证后可以获得邮箱授权码,记得保存下来

四、前端搭建

1.打开命令行,创建vue2项目vue-permission-admin

2.用vscode打开项目,安装相关依赖

安装vue-router
npm i vue-router@3.5.4
安装axios
npm install axios --save
安装vuex
npm i vuex@3.0.0
npm i vuex-persistedstate
安装element-ui
npm i element-ui -S
安装echarts
npm install echarts --save

3.配置main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

import Element from "element-ui" // 导入 element-ui 组件库
import "element-ui/lib/theme-chalk/index.css" // 引入其样式文件

import axios from 'axios' // 导入axios

Vue.prototype.$axios = axios
Vue.prototype.$url='http://localhost:8090'
Vue.config.productionTip = false

Vue.use(Element) // 使用 element-ui 组件库

// 创建根 Vue 实例
new Vue({
  router, // 路由实例
  store, // 状态管理实例
  render: h => h(App) // 渲染根组件 App
}).$mount('#app')

4.配置router.js

创建Index.vue,Home.vue,Login.vue和Register.vue页面
空模板代码
<template>
	<div>
	</div>
</template>

<script>
	export default {
		// eslint-disable-next-line vue/multi-word-component-names
		name: "Index",
	
  }

</script>

<style>
</style>

router.js代码
import Vue from "vue";
import VueRouter from "vue-router";
import Index from "../components/Index/Index.vue";
import Home from "../components/Home/Home.vue";
Vue.use(VueRouter);

// 定义路由配置
const routes = [
  {
    path: "/",
    name: "Index",
    component: Index,
    children: [
      {
        path: "/Home",
        name: "Home",
        meta: { title: "系统首页" },
        component: Home,
      },
      // 可以根据需要添加其他子路由
    ],
  },
  {
    path: "/login",
    name: "Login",
    component: () => import("@/components/Login.vue"),
  },
  {
    path: "/Register",
    name: "Register",
    component: () => import("@/components/Register.vue"),
  },
];

// 创建 Vue Router 实例
const router = new VueRouter({
  mode: "history", // 路由模式为 history 模式,去掉 URL 中的 #
  base: process.env.BASE_URL, // 根据配置设置基本 URL
  routes, // 路由配置
});

export default router;

5.配置store.js

创建模块 menus

配置modules/menus.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default {
	state: {
		menuList: [], // 菜单列表
		permList: [], // 权限列表
		hasRoutes: false, // 是否存在路由

		editableTabsValue: 'Home', // 可编辑标签页的值,默认为系统首页
		editableTabs: [{
			title: '系统首页',
			name: 'Home',
		}] // 可编辑标签页的数组,初始只包含系统首页
	},
	mutations: {
		setMenuList(state, menus) {
			state.menuList = menus // 更新菜单列表
		},
		setPermList(state, perms) {
			state.permList = perms // 更新权限列表
		},
		changeRouteStatus(state, hasRoutes) {
			state.hasRoutes = hasRoutes // 更新是否存在路由的状态
		},

		addTab(state, tab) {
			// 添加标签页
			let index = state.editableTabs.findIndex(e => e.name === tab.name)
			if (index === -1) { // 若该标签页不存在,则添加
				state.editableTabs.push({
					title: tab.title,
					name: tab.name,
				});
			}
			state.editableTabsValue = tab.name; // 将当前选中的标签页值更新为添加的标签页
		},

		resetState: (state) => {
			// 重置状态
			state.menuList = [] // 清空菜单列表
			state.permList = [] // 清空权限列表

			state.hasRoutes = false // 设置是否存在路由的状态为 false
			state.editableTabsValue = 'Home' // 将当前选中的标签页值重置为系统首页
			state.editableTabs = [{
				title: '系统首页',
				name: 'Home',
			}] // 重置可编辑标签页数组,只包含系统首页
			
  // 移除本地存储数据
  localStorage.removeItem("LocalUser")
		}
	},
	actions: {},
}
配置vuex
import Vue from 'vue'
import Vuex from 'vuex'
import menus from "./modules/menus";

Vue.use(Vuex)

export default new Vuex.Store({
	state: {
		LocalUser: '' // 定义状态属性 LocalUser,用于存储本地用户信息,默认为空字符串

	},
	mutations: {
		SETUSER: (state, LocalUser) => {
			state.LocalUser = LocalUser // 更新状态属性 LocalUser 的值为传入的 LocalUser
			localStorage.setItem("LocalUser", LocalUser) // 将 LocalUser 存储到 localStorage 中
		},
	},
	actions: {},
	modules: {
		menus // 注册模块 menus
	}
})

6.修改App.vue

<template>
	<div id="app">
		<router-view/>
	</div>
</template>

<script>
	export default {
		name: "App",
	
  }

</script>

<style>
	html, body, #app {
		font-family: 'Helvetica Neue', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', 'Microsoft Yahei', sans-serif;
		height: 100%;
		padding: 0;
		margin: 0;
		font-size: 15px;
	}
</style>

7.运行

五、后端Menu功能的实现

Controller层

1.注入服务

    @Resource
    private MenuService menuService; // 菜单服务类
    @Resource
    private RoleMenuService roleMenuService; // 角色菜单服务类

2.菜单列表

// 菜单列表
    @GetMapping("/list")
    public Result list() {
        List<Menu> menuList = menuService.list(); // 获取菜单列表
        return Result.suc(menuList); // 返回成功结果和菜单列表
    }

3.新增菜单

 // 新增菜单
    @PostMapping("/save")
    public Result save(@Validated @RequestBody Menu menu) {
        return menuService.save(menu)?Result.suc(menu):Result.fail(); // 返回成功结果和保存的菜单
    }

4.修改菜单

    // 修改菜单
    @PostMapping("/update")
    public Result update(@Validated @RequestBody Menu menu) {
        return menuService.updateById(menu)?Result.suc(menu):Result.fail(); // 返回成功结果和更新的菜单
    }

5.删除菜单

// 删除菜单
@PostMapping("/delete/{id}")
public Result delete(@PathVariable("id") Long id) {
    int count = menuService.count(new QueryWrapper<Menu>().eq("parent_id", id)); // 统计子菜单数量
    if (count > 0) {
        return Result.fail("请先删除子菜单"); // 存在子菜单时返回错误结果
    }
    menuService.removeById(id); // 删除菜单
    // 同步删除中间关联表
    roleMenuService.remove(new QueryWrapper<RoleMenu>().eq("menu_id", id)); // 删除角色菜单关联表中的记录
    return Result.suc("删除菜单成功"); // 返回成功结果
}

6.获取菜单信息

// 获取菜单信息
@GetMapping("/info/{id}")
public Result info(@PathVariable(name = "id") Long id) {
    return Result.suc(menuService.getById(id)); // 返回成功结果和指定id的菜单信息
}

7.获取菜单树形结构列表

// 获取菜单树形结构列表
@GetMapping("/tree")
public Result tree() {
    List<Menu> menuList = menuService.tree(); // 获取菜单树形结构列表
    return Result.suc(menuList); // 返回成功结果和菜单树形结构列表
}
entity实体类
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value="Menu对象", description="")
public class Menu implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @ApiModelProperty(value = "父菜单ID,一级菜单为0")
    private Long parentId;

    private String name;

    @ApiModelProperty(value = "菜单URL")
    private String path;

    @ApiModelProperty(value = "授权(多个用逗号分隔,如:user:list,user:create)")
    private String perms;

    private String component;

    @ApiModelProperty(value = "类型     0:目录   1:菜单   2:按钮")
    private Integer type;

    @ApiModelProperty(value = "菜单图标")
    private String icon;

    @ApiModelProperty(value = "排序")
    @TableField("orderNum")
    private Integer ordernum;

    private Integer status;
    @TableField(exist = false)
    private List<Menu> children = new ArrayList<>();

}

Service层
List<Menu> tree();

MenuServiceImpl
注入mapper
@Resource UserMapper userMapper;
@Resource MenuMapper menuMapper;

    @Override
    public List<Menu> tree() {
        // 获取所有菜单信息
        List<Menu> menuList = this.list(new QueryWrapper<Menu>().orderByAsc("orderNum"));
        // 转成树状结构
        return buildTreeMenu(menuList);
    }
    /**
     * 构建菜单树形结构
     *
     * @param menus 菜单信息列表
     * @return 构建好的菜单信息树形结构
     */
    private List<Menu> buildTreeMenu(List<Menu> menus) {
        // 存放构建好的菜单信息树形结构
        List<Menu> finalMenus = new ArrayList<>();

        // 遍历每个菜单信息
        for (Menu menu : menus) {
            // 将当前菜单的子节点添加到其父节点的children属性中
            for (Menu e : menus) {
                if (menu.getId() == e.getParentId()) {
                    menu.getChildren().add(e);
                }
            }

            // 如果当前菜单没有父菜单,则认为是顶级菜单,将其添加到结果集中
            if (menu.getParentId() == 0L) {
                finalMenus.add(menu);
            }
        }

        // 返回构造完成的菜单信息树形结构
        return finalMenus;
    }
    

8.获取导航栏信息

// 获取导航栏信息
@PostMapping("/nav")
public Result nav(@RequestBody User user) {
    // 获取导航栏信息
    List<MenuDto> navs = menuService.getCurrentUserNav(user); // 根据用户获取当前用户的导航栏信息
    return Result.suc(MapUtil.builder()
            .put("auth", user.getRoles()) // 设置返回结果中的权限信息
            .put("nav", navs) // 设置返回结果中的导航栏信息
            .map()
    );
}

entity实体类;需要需要创建MenuDto实体来存放导航栏信息

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
public class MenuDto implements Serializable {

    private Long id;
    private String name;
    private String title;
    private String icon;
    private String path;
    private String component;
    private Integer type;
    @ApiModelProperty(value = "父菜单ID,一级菜单为0")
    private Long parentId;
    private List<MenuDto> children = new ArrayList<>();

}
User实体类
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value="User对象", description="")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "用户ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @ApiModelProperty(value = "用户名")
    private String username;

    @ApiModelProperty(value = "用户密码")
    private String password;

    @ApiModelProperty(value = "用户头像")
    private String avatar;

    @ApiModelProperty(value = "用户邮箱")
    private String email;

    @ApiModelProperty(value = "用户注册时间")
    private LocalDateTime created;

    @ApiModelProperty(value = "账号状态,注销为0,正常为1")
    private Integer status;
    @TableField(exist = false)
    private  Role roles;
}

MenuService
List<MenuDto> getCurrentUserNav(User user);

MenuServiceImpl
@Override
public List<MenuDto> getCurrentUserNav(User user) {
    // 获取当前用户的导航菜单ID列表
    List<Long> menuIds = userMapper.getNavMenuIds(Long.valueOf(user.getId()));

    // 根据导航菜单ID列表查找对应的菜单信息
    List<Menu> menus = new ArrayList<>();
    for (int i = 0; i < menuIds.size(); i++) {
        menus.add(menuMapper.selectById(menuIds.get(i)));
    }

    // 构建菜单树形结构
    List<Menu> menuTree = buildTreeMenu(menus);

    // 将菜单树形结构转换为DTO对象
    return convert(menuTree);
}
/**
     * 将菜单信息构建成树形结构
     *
     * @param menuTree 菜单信息列表
     * @return 构建好的菜单树形结构
     */
    private List<MenuDto> convert(List<Menu> menuTree) {
        // 存放转换后的MenuDto信息
        List<MenuDto> menuDtos = new ArrayList<>();

        // 遍历每个菜单信息
        menuTree.forEach(
                m -> {
                    // 新建一个MenuDto对象
                    MenuDto dto = new MenuDto();

                    // 将当前菜单信息的属性值赋值给MenuDto对象的对应属性
                    dto.setId(m.getId());
                    dto.setName(m.getPerms());
                    dto.setTitle(m.getName());
                    dto.setComponent(m.getComponent());
                    dto.setPath(m.getPath());
                    dto.setType(m.getType());
                    dto.setIcon(m.getIcon());
                    dto.setParentId(m.getParentId());

                    // 如果当前菜单有子节点,则递归调用当前方法进行再次转换
                    if (m.getChildren().size() > 0) {
                        dto.setChildren(convert(m.getChildren()));
                    }

                    // 将转换后的MenuDto对象添加到结果集中
                    menuDtos.add(dto);
                });

        // 返回构造完成的菜单信息树形结构
        return menuDtos;
    }
    
UserMapper
List<Long> getNavMenuIds(Long valueOf);

UserMapper.xml
<select id="getNavMenuIds" resultType="java.lang.Long">
        SELECT
            DISTINCT rm.menu_id
        FROM
            user_role ur
                LEFT JOIN role_menu rm ON ur.role_id = rm.role_id
        WHERE ur.user_id = #{userId}
    </select>

9.插入数据

INSERT INTO `menu` VALUES (1, 0, '系统首页', '/Home', 'Home', 'Home/Home', 1, 'el-icon-s-home', 1, 1);
INSERT INTO `menu` VALUES (2, 0, '系统管理', '', 'sys:manage', '', 0, 'el-icon-s-operation', 2, 1);
INSERT INTO `menu` VALUES (3, 2, '角色管理', '/System/Role', 'sys:role:list', 'System/Role', 1, 'el-icon-rank', 3, 1);
INSERT INTO `menu` VALUES (4, 2, '菜单管理', '/System/Menu', 'sys:menu:list', 'System/Menu', 1, 'el-icon-menu', 4, 1);
INSERT INTO `menu` VALUES (5, 0, '用户管理', '', 'sys:user', 'User/User', 0, 'el-icon-user-solid', 5, 1);
INSERT INTO `menu` VALUES (6, 5, '用户信息管理', '/user/User', 'sys:user:list', 'User/User', 1, 'el-icon-user', 6, 1);
INSERT INTO `menu` VALUES (7, 5, '用户权限管理', '/user/Role', 'sys:role', 'User/Role', 1, 'el-icon-s-tools', 7, 1);
INSERT INTO `menu` VALUES (8, 0, '个人中心', '/account/Account', 'sys:account', 'Account/Account', 1, 'el-icon-menu', 8, 1);
INSERT INTO `menu` VALUES (9, 3, '添加角色', '', 'sys:role:save', '', 2, '', 1, 1);
INSERT INTO `menu` VALUES (10, 3, '修改角色', NULL, 'sys:role:update', NULL, 2, NULL, 2, 1);
INSERT INTO `menu` VALUES (11, 3, '删除角色', NULL, 'sys:role:delete', NULL, 2, NULL, 3, 1);
INSERT INTO `menu` VALUES (12, 3, '分配权限', NULL, 'sys:role:perm', NULL, 2, NULL, 4, 1);
INSERT INTO `menu` VALUES (13, 4, '添加菜单', NULL, 'sys:menu:save', NULL, 2, NULL, 1, 1);
INSERT INTO `menu` VALUES (14, 4, '修改菜单', NULL, 'sys:menu:update', NULL, 2, NULL, 2, 1);
INSERT INTO `menu` VALUES (15, 4, '删除菜单', NULL, 'sys:menu:delete', NULL, 2, NULL, 3, 1);
INSERT INTO `menu` VALUES (16, 6, '添加用户', NULL, 'sys:user:save', NULL, 2, NULL, 1, 1);
INSERT INTO `menu` VALUES (17, 6, '修改用户', NULL, 'sys:user:update', NULL, 2, NULL, 2, 1);
INSERT INTO `menu` VALUES (18, 6, '删除用户', NULL, 'sys:user:delete', NULL, 2, NULL, 3, 1);
INSERT INTO `menu` VALUES (19, 7, '用户控制', NULL, 'sys:user:on', NULL, 2, NULL, 1, 1);
INSERT INTO `menu` VALUES (20, 7, '重置密码', NULL, 'sys:user:repass', NULL, 2, NULL, 2, 1);
INSERT INTO `menu` VALUES (21, 7, '分配角色', NULL, 'sys:user:role', NULL, 2, NULL, 3, 1);
INSERT INTO `role` VALUES (1, '超级管理员', 'admin', '系统默认最高权限,不可以编辑和任意修改', '2021-01-16 13:29:03', 1);
INSERT INTO `role_menu` VALUES (1, 1, 1);
INSERT INTO `role_menu` VALUES (2, 1, 2);
INSERT INTO `role_menu` VALUES (3, 1, 3);
INSERT INTO `role_menu` VALUES (4, 1, 9);
INSERT INTO `role_menu` VALUES (5, 1, 10);
INSERT INTO `role_menu` VALUES (6, 1, 11);
INSERT INTO `role_menu` VALUES (7, 1, 12);
INSERT INTO `role_menu` VALUES (8, 1, 4);
INSERT INTO `role_menu` VALUES (9, 1, 13);
INSERT INTO `role_menu` VALUES (10, 1, 14);
INSERT INTO `role_menu` VALUES (11, 1, 15);
INSERT INTO `role_menu` VALUES (12, 1, 5);
INSERT INTO `role_menu` VALUES (13, 1, 6);
INSERT INTO `role_menu` VALUES (14, 1, 16);
INSERT INTO `role_menu` VALUES (15, 1, 17);
INSERT INTO `role_menu` VALUES (16, 1, 18);
INSERT INTO `role_menu` VALUES (17, 1, 7);
INSERT INTO `role_menu` VALUES (18, 1, 19);
INSERT INTO `role_menu` VALUES (19, 1, 20);
INSERT INTO `role_menu` VALUES (20, 1, 21);
INSERT INTO `role_menu` VALUES (21, 1, 8);
INSERT INTO `user` VALUES (1, 'admin', '123456', 'avatar1', '1158842161@qq.com', '2023-09-27 23:27:12', 1);
INSERT INTO `user_role` VALUES (1, 1, 1);

10.测试

六、后端Role功能的实现

Controller层

1.注入服务

@Resource private RoleService roleService;
@Resource private RoleMenuService roleMenuService;

2.角色列表

  @GetMapping("/list")
  public List<Role> list() {
    return roleService.list();
  }

3.新增角色

@PostMapping("/save")
public Result save(@RequestBody Role role) {
  return roleService.save(role) ? Result.suc() : Result.fail();
}

4.修改角色

@PostMapping("/update")
public Result update(@RequestBody Role role) {
  return roleService.updateById(role) ? Result.suc() : Result.fail();
}

5.删除角色

@PostMapping("/delete/{id}")
public Result delete(@PathVariable("id") Long id) {
  return roleService.removeById(id) ? Result.suc() : Result.fail();
}

6.逻辑删除角色

@PostMapping("/deleteLogical/{id}")
public Result deleteLogical(@PathVariable("id") Long id) {
  // 获取用户数据
  Role role = roleService.getById(id);
  //    修改用户状态
  role.setStatus(0);
  // 更新数据
  return roleService.updateById(role) ? Result.suc() : Result.fail();
}

7.禁用启用角色

@PostMapping("/control/{id}")
public Result control(@PathVariable("id") Long id) {
  Role role = roleService.getById(id);
  if (role.getStatus() == 1) {
    role.setStatus(0);
  } else {
    role.setStatus(1);
  }
  return roleService.updateById(role) ? Result.suc() : Result.fail();
}

8.批量删除角色

@Transactional
@PostMapping("/deleteBatch")
public Result deleteBatch(@RequestBody Long[] ids) {
  roleService.removeByIds(Arrays.asList(ids));
  return Result.suc("批量删除成功");
}

9.获取角色信息

 @GetMapping("/info/{id}")
    public Result info(@PathVariable("id") Long id) {
        Role role = roleService.getById(id);
        // 获取角色相关联的菜单id
        List<RoleMenu> roleMenus = roleMenuService.list(new QueryWrapper<RoleMenu>().eq("role_id", id));
        List<Long> menuIds = roleMenus.stream().map(p -> p.getMenuId()).collect(Collectors.toList());
        role.setMenuIds(menuIds);
        //把角色的权限信息带过去,好做角色权限分配
        return Result.suc(role);
    }
    
Role实体类
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value="Role对象", description="")
public class Role implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "角色ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @ApiModelProperty(value = "角色名称")
    private String name;

    @ApiModelProperty(value = "角色代码")
    private String code;

    @ApiModelProperty(value = "备注")
    private String remark;

    @ApiModelProperty(value = "角色创建时间")
    private LocalDateTime created;

    @ApiModelProperty(value = "角色状态")
    private Integer status;
    @TableField(exist = false)
    private List<Long> menuIds = new ArrayList<>();

}

10.分配角色权限

@Transactional
    @PostMapping("/perm/{roleId}")
    public Result info(@PathVariable("roleId") Long roleId, @RequestBody Long[] menuIds) {
        List<RoleMenu> roleMenus = new ArrayList<>();
//        查询角色权限
        Arrays.stream(menuIds)
                .forEach(
                        menuId -> {
                            RoleMenu roleMenu = new RoleMenu();
                            roleMenu.setMenuId(menuId);
                            roleMenu.setRoleId(roleId);
                            roleMenus.add(roleMenu);
                        });
        // 先删除原来的记录,再保存新的
        roleMenuService.remove(new QueryWrapper<RoleMenu>().eq("role_id", roleId));
        roleMenuService.saveBatch(roleMenus);
        return Result.suc(menuIds);
    }

11.分页查询角色

@PostMapping("/listPage")
public Result listPage(@RequestBody QueryPageParam query) {
  HashMap param = query.getParam();
  // 查询条件
  String name = (String) param.get("name");
  String status = (String) param.get("status");
  // 引入分页组件
  Page<Role> page = new Page();
  page.setCurrent(query.getPageNum());
  page.setSize(query.getPageSize());
  LambdaQueryWrapper<Role> lambdaQueryWrapper = new LambdaQueryWrapper();
  if (StringUtils.isNotBlank(status) && !"null".equals(status)) {
    lambdaQueryWrapper.eq(Role::getStatus, status);
  }
  if (StringUtils.isNotBlank(name) && !"null".equals(name)) {
    lambdaQueryWrapper.like(Role::getName, name);
  }
  IPage result = roleService.listPage(page, lambdaQueryWrapper);
  return Result.suc(result.getRecords(), result.getTotal());
}

RoleService
IPage listPage(IPage<Role> page, Wrapper wrapper);

RoleServiceImpl
@Resource
    private RoleMapper roleMapper;
    @Override
    public IPage listPage(IPage<Role> page, Wrapper wrapper) {
        return roleMapper.listPage(page,wrapper);
    }

RoleMapper
  IPage listPage(IPage<Role> page, @Param(Constants.WRAPPER) Wrapper wrapper);

RoleMapper.xml
    <select id="listPage" resultType="com.longyi.springbootpermissionadmin.entity.Role">
        select * from role ${ew.customSqlSegment}
    </select>

12.测试

七、后端User基本功能的实现

Controller层

1.注入服务

@Resource private UserService userService;
@Resource private RoleService roleService;
@Resource private UserRoleService userRoleService;

2.用户列表

@GetMapping("/list")
public List<User> list() {
  return userService.list();
}

3.新增用户

  @Transactional
    @PostMapping("/save")
    public Result save(@RequestBody User user) {
        userService.save(user);//保存用户信息
        User user1 = userService.lambdaQuery().eq(User::getUsername, user.getUsername()).list().get(0);//查询用户信息
        UserRole userRole = new UserRole();//创建userole对象
        userRole.setUserId(Long.valueOf(user1.getId()));//设置userole对象
        userRole.setRoleId(2L);//默认新用户都是普通用户,也就是第二个role
        return userRoleService.save(userRole) ? Result.suc(user1) : Result.fail();//保存userrole对象
    }

4.修改用户

@PostMapping("/update")
public Result update(@RequestBody User user) {
  return userService.updateById(user) ? Result.suc() : Result.fail();
}

5.删除用户

@PostMapping("/delete/{id}")
public Result delete(@PathVariable("id") Long id) {
  return userService.removeById(id) ? Result.suc() : Result.fail();
}

6.逻辑删除用户

@PostMapping("/deleteLogical/{id}")
public Result deleteLogical(@PathVariable("id") Long id) {
  // 获取用户数据
  User user = userService.getById(id);
  //    修改用户状态
  user.setStatus(0);
  // 更新数据
  return userService.updateById(user) ? Result.suc() : Result.fail();
}

7.禁用启用用户

@PostMapping("/control/{id}")
public Result control(@PathVariable("id") Long id) {
  User user = userService.getById(id);
  if (user.getStatus() == 1) {
    user.setStatus(0);
  } else {
    user.setStatus(1);
  }
  return userService.updateById(user) ? Result.suc() : Result.fail();
}

8.批量删除用户

@Transactional
@PostMapping("/deleteBatch")
public Result deleteBatch(@RequestBody Long[] ids) {
  userService.removeByIds(Arrays.asList(ids));
  return Result.suc("批量删除成功");
}

9.获取用户信息

@GetMapping("/info/{id}")
public Result info(@PathVariable("id") Long id) {
  User user = userService.getById(id);
  if (user.getStatus() == 0) {
    return Result.fail("该账号已注销");
  } else {
    Role role = roleService.listRolesByUserId(id);
    user.setRoles(role);
    return Result.suc(user);
  }
}

RoleService
    Role listRolesByUserId(Long id);

RoleServiceImpl
    @Override
    public Role listRolesByUserId(Long id) {
        return roleMapper.listRolesByUserId(id);
    }

RoleMapper
  Role listRolesByUserId(Long id);

RoleMapper.xml
  <select id="listRolesByUserId" resultType="com.longyi.springbootpermissionadmin.entity.Role">
        SELECT role.*
        FROM role, user_role
        WHERE user_role.user_id = #{id} AND role.id = user_role.role_id 
    </select>

10.获取用户权限

@GetMapping("/auth/{id}")
public List<String> auth(@PathVariable("id") Long id) {
  return userService.getUserAuthorityInfo(id);
}

UserService
List<String> getUserAuthorityInfo(Long id);

UserServiceImpl
注入
    @Resource private UserMapper userMapper;
    @Resource private RoleService roleService;
    @Resource private MenuService menuService;
    
       @Override
    public List<String> getUserAuthorityInfo(Long id) {
        List<String> authority = new ArrayList<>();
        // 获取角色编码
        Role roles = roleService.listRolesByUserId(id);
        authority.add(roles.getCode());
        // 获取菜单操作编码
        List<Long> menuIds = userMapper.getNavMenuIds(id);
        if (menuIds.size() > 0) {
            List<Menu> menuList= menuService.listByIds(menuIds);
            for (int i=0;i<menuList.size();i++){
                authority.add(menuList.get(i).getPerms());
            }
        }
        return authority;
    }

11.分配用户角色

@Transactional
@PostMapping("/role")
public Result rolePerm(@RequestParam Long userId, Long roleId) {
  List<UserRole> userRoles = userRoleService.lambdaQuery().eq(UserRole::getUserId, userId).list();
  UserRole userRole = userRoles.get(0);
  userRole.setRoleId(roleId);
  return userRoleService.updateById(userRole) ? Result.suc() : Result.fail();
}

12.分页查询用户

@PostMapping("/listPage")
public Result listPage(@RequestBody QueryPageParam query) {
  HashMap param = query.getParam();
  String status = (String) param.get("status");
  String username = (String) param.get("username");
  String email = (String) param.get("email");
  Page<User> page = new Page();
  page.setCurrent(query.getPageNum());
  page.setSize(query.getPageSize());
  LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper();
  if (StringUtils.isNotBlank(status) && !"null".equals(status)) {
    lambdaQueryWrapper.eq(User::getStatus, status);
  }
  if (StringUtils.isNotBlank(username) && !"null".equals(username)) {
    lambdaQueryWrapper.like(User::getUsername, username);
  }
  if (StringUtils.isNotBlank(email) && !"null".equals(email)) {
    lambdaQueryWrapper.like(User::getEmail, email);
  }
  IPage result = userService.listPage(page, lambdaQueryWrapper);
  List<User> userList = new ArrayList<>();
  for (int i = 0; i < result.getRecords().size(); i++) {
    User user = (User) result.getRecords().get(i);
    user.setRoles(roleService.listRolesByUserId(Long.valueOf(user.getId())));
    userList.add(i, user);
  }
  result.setRecords(userList);
  return Result.suc(result.getRecords(), result.getTotal());
}

UserService
    IPage listPage(IPage<User> page, Wrapper wrapper);

UserServiceImpl
@Override
    public IPage listPage(IPage<User> page, Wrapper wrapper) {
        return userMapper.listPage(page, wrapper);
    }
    
UserMapper
 IPage listPage(IPage<User> page, @Param(Constants.WRAPPER) Wrapper wrapper);
 
UserMapper.xml
  <select id="listPage" resultType="com.longyi.springbootpermissionadmin.entity.User">
        select * from user ${ew.customSqlSegment}
    </select>

13.测试

八、头像上传下载功能的实现

1.创建存放目录文件夹

static/avatar

static/file

2.编写代码

获取文件扩展名

头像上传接口

头像下载接口

文件下载接口

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;

import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;

import com.longyi.springbootpermissionadmin.common.GetPath;
import com.longyi.springbootpermissionadmin.common.Result;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/upload")
public class UploadController {

    @Resource
    private GetPath getPath;
    // 注入 GetPath 对象,用于获取项目文件夹路径

    // 获取文件扩展名
    private String getFileExtension(String fileName) {
        int index = fileName.lastIndexOf(".");
        if (index > 0 && index < fileName.length() - 1) {
            return fileName.substring(index + 1);
        }
        return "";
    }

    // 头像上传接口
    @PostMapping("/avatar")
    public Result avatarUpload(String fileName, @RequestParam MultipartFile file) {
        if (file == null) {
            return Result.fail("文件为空,请上传文件");
        }
        synchronized (UploadController.class) {
            // 获取当前时间戳作为文件名前缀
            String flag = System.currentTimeMillis() + "";
            String extension = getFileExtension(fileName);  // 获取文件扩展名
            try {
                // 如果没有 avatar 文件夹,会在项目根目录下创建一个 avatar 文件夹
                if (!FileUtil.isDirectory(getPath.getProjectAbsolutePath())) {
                    FileUtil.mkdir(getPath.getProjectAbsolutePath());
                }
                // 文件存储形式:时间戳-文件名.扩展名
                FileUtil.writeBytes(
                        file.getBytes(),
                        getPath.getProjectAbsolutePath() + "/avatar/" + flag + "." + extension);
                System.out.println(fileName + "--上传成功");
                return Result.suc(flag);  // 返回上传成功信息和文件名前缀
            } catch (Exception e) {
                System.err.println(fileName + "--文件上传失败");
                return Result.fail("文件上传失败");
            }
        }
    }
  // 头像下载接口
    @GetMapping("/avatar/{flag}")
    public void avatarPath(@PathVariable String flag, HttpServletResponse response)
            throws IOException {
        String filePath = getPath.getProjectAbsolutePath() + "/avatar/";
        if (!FileUtil.isDirectory(filePath)) {
            FileUtil.mkdir(filePath);
        }
        OutputStream os;
        List<String> fileNames = FileUtil.listFileNames(filePath);
        // 筛选出符合特定标识符 flag 的文件名
        String targetFileName = fileNames.stream()
                .filter(name -> name.contains(flag))
                .findAny()
                .orElse("");
        try {
            if (StrUtil.isNotEmpty(targetFileName)) {
                response.addHeader("Content-Disposition",
                        "attachment;filename=" + URLEncoder.encode(targetFileName, "UTF-8"));
                response.setContentType("application/octet-stream");
                byte[] bytes = FileUtil.readBytes(filePath + targetFileName);
                os = response.getOutputStream();
                os.write(bytes);
                os.flush();
                os.close();
            }
        } catch (Exception e) {
            System.out.println("文件下载失败");
        }
    }
    // 文件下载接口
    @GetMapping("/file/{flag}")
    public void filePath(@PathVariable String flag, HttpServletResponse response) throws IOException {
        String filePath = getPath.getProjectAbsolutePath() + "/file/";
        if (!FileUtil.isDirectory(filePath)) {
            FileUtil.mkdir(filePath);
        }
        OutputStream os;
        List<String> fileNames = FileUtil.listFileNames(filePath);
        // 筛选出符合特定标识符 flag 的文件名
        String targetFileName = fileNames.stream()
                .filter(name -> name.contains(flag))
                .findAny()
                .orElse("");
        try {
            if (StrUtil.isNotEmpty(targetFileName)) {
                response.addHeader("Content-Disposition",
                        "attachment;filename=" + URLEncoder.encode(targetFileName, "UTF-8"));
                response.setContentType("application/octet-stream");
                byte[] bytes = FileUtil.readBytes(filePath + targetFileName);
                os = response.getOutputStream();
                os.write(bytes);
                os.flush();
                os.close();
            }
        } catch (Exception e) {
            System.out.println("文件下载失败");
        }
    }

  
}

九、前端注册功能的实现

1.搭建注册页面基本架构

 <div class="loginBody">
        <div class="loginDiv">
            <div class="login-content">
                <h1 class="login-title">账号注册</h1>
            </div>
        </div>
  </div>
  
  
.loginBody {
    position: absolute;
    width: 100%;
    height: 100%;
    background-color: darkgrey;
    background-size: 100% 100%;
}

.loginDiv {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 500px;
    height: 500px;
    background: rgba(255, 255, 255, 0.5);
    border-radius: 5%;
    display: flex;
    text-align: center;
    justify-content: center;
    align-items: center;
    backdrop-filter: blur(5px);
    /* 添加毛玻璃效果,模糊值可根据需要进行调整 */

}
.login-title {
  text-align: center;
}
.login-content {
  width: 500px;
  height: 450px;
  position: absolute;
}

2.form表单

  <el-form
          :model="registerForm"
          label-width="100px"
          :rules="rules"
          ref="registerForm"
        >
          <el-row style="margin-top: 20px; height: 40px">
           </el-row>
          <el-row style="margin-top: 20px; height: 40px">
          </el-row>
          <el-row style="margin-top: 20px; height: 40px">
          </el-row>
          <el-row style="margin-top: 20px; height: 40px">
            </el-row>
          <el-row style="margin-top: 20px; height: 40px">
          </el-row>
             <el-row style="margin-top: 20px; height: 40px"> 
            <el-button
              type="primary"
              @click="register"
              :disabled="confirm_disabled"
              style="width: 150px"
              >注册</el-button
            >
            <el-button
              type="success"
              @click="login"
              style="width: 150px;margin-left:100px"
              >登录</el-button
            >
          </el-row>
        </el-form>

3.配置数据和校验规则

 data() {
    var validatePass = (rule, value, callback) => {
      if (value === "") {
        callback(new Error("请再次输入密码"));
      } else if (value !== this.registerForm.password) {
        callback(new Error("两次输入密码不一致!"));
      } else {
        callback();
      }
    };
    var validateCode = (rule, value, callback) => {
      if (value === "") {
        callback(new Error("请再次输入验证码"));
      } else if (value !== this.registerForm.rcode) {
        callback(new Error("验证码不正确!"));
      } else {
        callback();
      }
    };

    return {
      confirm_disabled: false,
      registerForm: {
        username: "",
        password: "",
        rpassword: "",
        email: "",
        code: "",
        rcode: "",
      },
      rules: {
        username: [
          { required: true, message: "请输入姓名", trigger: "blur" },
          { min: 2, max: 10, message: "2-10个字符", trigger: "blur" },
        ],
        email: [
          { required: true, message: "请输入邮箱", trigger: "blur" },
          {
            pattern:
              /^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(.[a-zA-Z0-9_-]+)+$/,
            message: "邮箱格式不正确",
          },
        ],
        code: [
          { required: true, validator: validateCode, trigger: "blur" },
          {
            min: 6,
            max: 6,
            message: "请输入6位数字邮箱验证码",
            trigger: "blur",
          },
        ],
        password: [
          { required: true, message: "请输入密码", trigger: "blur" },
          { min: 6, max: 16, message: "6-16个字符", trigger: "blur" },
        ],
        rpassword: [
          { required: true, validator: validatePass, trigger: "blur" },
          { min: 6, max: 16, message: "6-16个字符", trigger: "blur" },
        ],
      },
    };
  },

4.编写form表单项

 <el-form-item label="用户名" prop="username">
              <el-input
                style="width: 300px"
                type="text"
                v-model="registerForm.username"
                autocomplete="off"
                size="small"
              ></el-input> </el-form-item
          >
  <el-form-item label="邮箱" prop="email">
              <el-input
                v-model="registerForm.email"
                style="width: 300px"
                @change="emailchange"
                placeholder="请输入邮箱"
              ></el-input>
            </el-form-item>
   <el-form-item prop="code" label="验证码">
              <div style="display: flex;margin-left:50px;"> <el-input
                v-model.lazy.trim="registerForm.code"
                placeholder="请输入验证码"
                style="width: 150px"
              ></el-input>
              <el-button
                class="code-btn"
                size="medium"
                :disabled="sendCodeDisabled"
                style="width: 120px; margin-left: 25px; font-size: 8px"
                @click.stop.prevent.native="handleGetCode"
                >{{ sendCodeText }}</el-button
              >
              </div>
            </el-form-item>
 <el-form-item label="密码" prop="password">
              <el-input
                style="width: 300px"
                type="password"
                v-model="registerForm.password"
                show-password
                autocomplete="off"
                size="small"
                @keyup.enter.native="register"
              ></el-input> </el-form-item
          >
  <el-form-item label="重复密码" prop="password">
              <el-input
                style="width: 300px"
                type="password"
                v-model="registerForm.rpassword"
                show-password
                autocomplete="off"
                size="small"
                @keyup.enter.native="register"
              ></el-input>
            </el-form-item>

5.配置邮箱验证码数据

sendCodeText: '获取验证码',
            sendCodeDisabled: false,
            countDown: 60,

6.登录跳转方法

 methods: {
        login() {
            this.$router.replace('/login');
        },
        }

7.获取邮箱验证码

  handleGetCode() {
      // 校验邮箱格式是否正确
      if (!this.validateEmail(this.registerForm.email)) {
        this.$message({
          message: "请输入正确的邮箱地址",
          type: "error",
        });
        return;
      }
      //   设置发送验证码按钮
      this.sendCodeDisabled = true;
      this.sendCodeText = `${this.countDown}s 后重新获取`;
      //   开启定时器
      this.timer = setInterval(() => {
        if (this.countDown === 1) {
          clearInterval(this.timer);
          this.countDown = 60;
          this.sendCodeDisabled = false;
          this.sendCodeText = "获取验证码";
          return;
        }
        this.countDown -= 1;
        this.sendCodeText = `${this.countDown}s 后重新获取`;
      }, 1000);
      //   向后端发送请求,往用户邮箱发送验证码
      this.$axios
        .get(this.$url + "/email/bind?email=" + this.registerForm.email)
        .then((res) => res.data)
        .then((res) => {
          console.log(res);
          if (res.code == 200) {
            this.$message({
              message: "邮箱验证码发送成功!",
              type: "success",
            });
            this.registerForm.rcode = res.data;
          } else {
            this.$message({
              message: res.msg,
              type: "error",
            });
            this.sendCodeDisabled = false;
            this.registerForm.rcode = "";
            this.sendCodeText = "获取验证码";
          }
        });
    },
    validateEmail(email) {
      // 正则表达式匹配邮箱格式
      const emailRegex = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/;
      return emailRegex.test(email);
    },
    // 当用户修改邮箱时,重置发送按钮状态
    emailchange() {
      clearInterval(this.timer);
      this.countDown = 60;
      this.sendCodeDisabled = false;
      this.sendCodeText = "获取验证码";
      this.registerForm.rcode = "";
      this.registerForm.code = "";
    },

8.邮箱验证码后端接口

import com.longyi.springbootpermissionadmin.common.CodeCreate;
import com.longyi.springbootpermissionadmin.common.Result;
import com.longyi.springbootpermissionadmin.entity.User;
import com.longyi.springbootpermissionadmin.service.UserService;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;

@RestController

@RequestMapping("/email")
public class EmailController {

    @Resource
    JavaMailSender javamail;

    /**
     * 普通文本邮件发送
     *
     * @return
     */
    @GetMapping("/bind")
    public Result bind(@RequestParam String email) {
        try {
            SimpleMailMessage message = new SimpleMailMessage();
            String rcode = CodeCreate.RandomNumberCode(6);
            message.setFrom("1158842161@qq.com");//换成自己的邮箱
            message.setTo(email);
            message.setSubject("邮箱验证码");
            message.setText("尊敬的客户,您好!欢迎使用龙毅的通用权限管理平台PermissionAdmin,您此次的邮箱验证码为:" + rcode);
            javamail.send(message); //发送邮件
            System.out.println("邮件已发送。"+rcode);
            return Result.suc(rcode);
        } catch (Exception e) {
            System.out.println("发送邮件时发生异常了!" + e.getMessage());
            return Result.fail("邮件发送失败");
        }

    }

}

9.注册方法

register() {
      this.confirm_disabled = true;
      this.$refs.registerForm.validate((valid) => {
        console.log(valid);
        if (valid) {
          this.$axios
            .post(this.$url + "/user/register", this.registerForm)
            .then((res) => res.data)
            .then((res) => {
              console.log(res);
              if (res.code == 200) {
                this.$router.replace("/login");
              } else {
                this.confirm_disabled = false;
                this.$message({
                  message: res.msg,
                  type: "error",
                });
                return false;
              }
            });
        } else {
          this.confirm_disabled = false;
          this.$message({
            message: "校验失败",
            type: "error",
          });
          return false;
        }
      });
    },

10.后端注册接口

@Transactional
@PostMapping("/register")
public Result register(@RequestBody User user) {
  List list = userService.lambdaQuery().eq(User::getUsername, user.getUsername()).list();
  if (list.size() > 0) {
    System.out.println(list);
    return Result.fail("账号已被注册");
  } else {
    list = userService.lambdaQuery().eq(User::getEmail, user.getEmail()).list();

    if (list.size() > 0) {

      System.out.println(list);
      return Result.fail("邮箱已被注册");
    } else {
      user.setAvatar("avatar3");
      userService.save(user);
      User user1 =
          userService.lambdaQuery().eq(User::getUsername, user.getUsername()).list().get(0);
      UserRole userRole = new UserRole();
      userRole.setUserId(Long.valueOf(user1.getId()));
      userRole.setRoleId(2L);

      return userRoleService.save(userRole) ? Result.suc(user1) : Result.fail();
    }
  }
}

11.测试

十、前端登录功能的实现

1.搭建登录页面基本架构

 <div class="loginBody">
        <div class="loginDiv">
            <div class="login-content">
                <h1 class="login-title">账号登录</h1>
            </div>
        </div>
  </div>
  
  

.loginBody {
  position: absolute;
  width: 100%;
  height: 100%;
  background-color:chocolate;
  background-size: 100% 100%;
}
.loginDiv {
  position: absolute;
  top: 50%;
  left: 50%;
  /* 居中 */
  width: 500px;
  height: 400px;
  /* 注册盒子宽高 */
  transform: translate(-50%, -50%);
  background: rgba(255, 255, 255, 0.5);
  backdrop-filter: blur(5px);
  /* 添加毛玻璃效果,模糊值可根据需要进行调整 */
  border-radius: 5%;
  display: flex;
  text-align: center;
  justify-content: center;
  align-items: center;
}
.login-title {
  text-align: center;
}
.login-content {
  width: 500px;
  height: 400px;
  position: absolute;
}

2.form表单

    <el-form
          :model="loginForm"
          label-width="100px"
          :rules="rules"
          ref="loginForm"
        >
          <el-row style="margin-top: 20px; height: 60px">
            <el-form-item label="账号" prop="username">
              <el-input
                style="width: 300px"
                type="text"
                v-model="loginForm.username"
                autocomplete="off"
                size="small"
              ></el-input>
            </el-form-item>
          </el-row>
          <el-row style="margin-top: 20px; height: 60px">
            <el-form-item label="密码" prop="password">
              <el-input
                style="width: 300px"
                type="password"
                v-model="loginForm.password"
                show-password
                autocomplete="off"
                size="small"
                @keyup.enter.native="confirm"
              ></el-input>
            </el-form-item>
          </el-row>
          <el-row style="margin-top: 20px;height: 60px">
            <el-form-item label="验证码" prop="code" >
              <div style="display: flex;margin-left:50px;">
                <el-input
                  v-model="loginForm.code"
                  style="width: 150px"
                ></el-input>
                <el-image
                  :src="captchaImg"
                  class="captcha-img"
                  @click="getCaptcha"
                  lazy
                  style="width: 120px; margin-left: 25px"
                ></el-image>
              </div>
            </el-form-item>
          </el-row>
          <el-row style="margin-top: 20px; height: 60px">
            <el-button
              type="primary"
              @click="login"
              :disabled="confirm_disabled"
              style="width: 150px"
              >登 录</el-button
            >
            <el-button
              type="success"
              @click="register"
              style="width: 150px; margin-left: 100px"
              >注 册</el-button
            >
          </el-row>
        </el-form>

3.配置数据和校验规则

data() {
        var validateCode = (rule, value, callback) => {
            if (value === '') {
                callback(new Error('请再次输入验证码'))
            } else if (value !== this.loginForm.rcode) {
                callback(new Error('验证码不正确!'))
            } else {
                callback()
            }
        }
        return {
            confirm_disabled: false,
            captchaImg: null,
            loginForm: {
                username: '',
                password: '',
                code: '',
                rcode: ''
            },
            rules: {
                username: [
                    { required: true, message: '请输入账号', trigger: 'blur' }
                ],
                password: [
                    { required: true, message: '请输密码', trigger: 'blur' }
                ],
                code: [
                    { required: true, validator: validateCode, trigger: 'blur' },
                    { min: 4, max: 4, message: '长度为 4 个字符', trigger: 'blur' }
                ],
            }
        }
    },

4.注册跳转方法

 methods: {
        register() {
            this.$router.replace('/Register');

        },
        }

5.获取验证码

 getCaptcha() {
            this.$axios.get(this.$url + '/captcha').then(res => {
                this.captchaImg = res.data.data.captchaImg
                this.loginForm.rcode = res.data.data.rcode
                this.loginForm.code = ''
            })
        },
        
        初始化获取
         mounted() {
        this.getCaptcha()
    }

6.验证码后端接口

import cn.hutool.core.map.MapUtil;
import com.google.code.kaptcha.Producer;
import com.longyi.springbootpermissionadmin.common.CodeCreate;
import com.longyi.springbootpermissionadmin.common.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.misc.BASE64Encoder;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

@RestController
public class KaptchaController {
    @Resource
    Producer producer;

    @GetMapping("/captcha")
    public Result captcha() throws IOException {
        String code = CodeCreate.RandomStringCode(4);
        BufferedImage image = producer.createImage(code);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ImageIO.write(image, "jpg", outputStream);
        BASE64Encoder encoder = new BASE64Encoder();
        String str = "data:image/jpeg;base64,";
        String base64Img = str + encoder.encode(outputStream.toByteArray());
        System.out.println(code);
        return Result.suc(
                MapUtil.builder()
                        .put("captchaImg", base64Img)
                        .put("rcode",code)
                        .build()

        );
    }
}

7.登录方法

         login() {
            this.confirm_disabled = true;
            this.$refs.loginForm.validate((valid) => {
                if (valid) { //valid成功为true,失败为false
                    //去后台验证用户名密码
                    this.$axios.post(this.$url + '/user/login', this.loginForm).then(res => res.data).then(res => {
                        // console.log(res)
                        if (res.code == 200) {
                            //存储
                            this.$store.commit("SETUSER", JSON.stringify(res.data))
                            this.$router.push("/Home")
                        } else {
                            this.confirm_disabled = false;
                            this.$message({
                                message: res.msg,
                                type: 'error'
                            });
                            return false;
                        }
                    });
                } else {
                    this.confirm_disabled = false;
                    this.$message({
                        message: '登录信息不正确!',
                        type: 'error'
                    });
                    return false;
                }
            });
        },

8.后端登录接口

 // 登录
  @PostMapping("/login")
  public Result login(@RequestBody User user) {
    if (StpUtil.isLogin()) {
      User usertail =
          userService
              .lambdaQuery()
              .eq(User::getUsername, user.getUsername())
              .eq(User::getPassword, user.getPassword())
              .list()
              .get(0);
      Role roleList = roleService.listRolesByUserId(Long.valueOf(usertail.getId()));
      usertail.setRoles(roleList);
      StpUtil.login(usertail.getId());
      return Result.suc(usertail);
    } else {
      List list =
          userService
              .lambdaQuery()
              .eq(User::getUsername, user.getUsername())
              .eq(User::getPassword, user.getPassword())
              .list();
      if (list.size() > 0) {
        User usertail = (User) list.get(0);
        if (usertail.getStatus() == 0) {
          return Result.fail("该账号已注销");
        } else {

          Role roleList = roleService.listRolesByUserId(Long.valueOf(usertail.getId()));
          usertail.setRoles(roleList);
          StpUtil.login(usertail.getId());
          return Result.suc(usertail);
        }
      }
      list =
          userService
              .lambdaQuery()
              .eq(User::getEmail, user.getUsername())
              .eq(User::getPassword, user.getPassword())
              .list();
      if (list.size() > 0) {
        User usertail = (User) list.get(0);
        if (usertail.getStatus() == 0) {
          return Result.fail("该账号已注销");
        } else {

          Role roleList = roleService.listRolesByUserId(Long.valueOf(usertail.getId()));
          usertail.setRoles(roleList);
          StpUtil.login(usertail.getId());
          return Result.suc(usertail);
        }
      }
      return Result.fail("账号不存在或密码错误");
    }
  }

11.测试

十一、前端首页布局以及路由守卫功能

1.首页布局

   创建Header.vue  SideMenu.vue TabMenu.vue
   <el-container>
        <el-aside :width=aside_witdh>
            <SideMenu :isCollapse="isCollapse"></SideMenu>
        </el-aside>
        <el-container>
            <el-header
                style="text-align: right; font-size: 12px;height: 8%;border-bottom: rgba(168,168,168,0.3) 1px solid;">
                <Header @doCollapse="doCollapse" :icon="icon">
                </Header>
            </el-header>
            <el-main>
                <TabMenu></TabMenu>
                <div style="margin: 0 15px;">
                    <router-view />
                </div>
            </el-main>
            <el-footer style="text-align: center;height: 8%;">
                @龙毅开发 2023.10.01
            </el-footer>
        </el-container>
    </el-container>
    
    引入组件
import SideMenu from "./SideMenu";
import TabMenu from "./TabMenu";
import Header from "./Header";
export default {
    // eslint-disable-next-line vue/multi-word-component-names
    name: "Home",
    components: {
        SideMenu, TabMenu, Header,
    },
 }

2.首页样式

.el-container {
    padding: 0;
    margin: 0;
    height: 100%;
}

.header-avatar {
    float: right;
    width: 210px;
    display: flex;
    justify-content: space-around;
    align-items: center;
}

.el-dropdown-link {
    cursor: pointer;
}

.el-header {
    background-color: #17B3A3;
    color: #333;
    text-align: center;
    line-height: 60px;
}

.el-aside {
    background-color: #D3DCE6;
    color: #333;
    line-height: 200px;
}

.el-main {
    color: #333;
    padding: 0;
}

a {
    text-decoration: none;
}

.el-footer {

    background-color: #17B3A3;
    color: #333;
    text-align: center;
    line-height: 60px;
}

3.配置数据项

data() {
        return {
            user: {},
            isCollapse: false,
            aside_witdh: '200px',
            icon: 'el-icon-s-fold'
        }
    },

4.侧边栏伸缩方法

methods: {
        doCollapse() {
            this.isCollapse = !this.isCollapse
            if (!this.isCollapse) {// 展开
                this.aside_witdh = '200px'
                this.icon = 'el-icon-s-fold'
            } else {//关起、关闭、收起
                this.aside_witdh = '64px'
                this.icon = 'el-icon-s-unfold'
            }
        },

    
    }

5.初始化获取用户登录信息

 created() {
        this.getUser()
    },
getUser() {
            this.user = JSON.parse(localStorage.getItem("LocalUser"))
        },

6.配置路由守卫

import store from "../store";
// router.beforeEach是Vue.js的路由守卫,在用户访问任何页面之前都会执行这个函数。
router.beforeEach((to, from, next) => {
  // 获取Store中menus里的hasRoutes属性和localStorage中的LocalUser属性
  let hasRoute = store.state.menus.hasRoutes;
  let token = localStorage.getItem("LocalUser");
  
  // 如果访问路径为/login或/Register,则直接跳转路由
  if (to.path == "/login" || to.path == "/Register") {
    next();
    
  // 如果LocalStorage中没有LocalUser,则跳转至登录界面
  } else if (!token) {
    next({ path: "/login" });
  
  // 如果LocalStorage中有LocalUser且Store中menus没有路由信息,则通过Api获取菜单信息并动态加载路由
  } else if (token && !hasRoute) {
    Vue.prototype.$axios
      .post("http://localhost:8090/menu/nav", localStorage.getItem("LocalUser"),{headers: {
          'Content-Type': 'application/json;charset=UTF-8'
        }})
      .then((res) => {
        // 打印获取到的菜单和权限信息
        console.log(res.data.data);
        
        // 将菜单列表和权限列表保存到Store中
        store.commit("setMenuList", res.data.data.nav);
        store.commit("setPermList", res.data.data.auth);
  
        console.log(store.state.menus.permList);
        
        // 将菜单信息转为路由,并动态绑定到router实例的routes属性中
        let newRoutes = router.options.routes;
        
        res.data.data.nav.forEach((menu) => {
          let route = menuToRoute(menu);
          if (route) {
            newRoutes[0].children.push(route);
          }
          if (menu.children) {
            menu.children.forEach((e) => {
              let route = menuToRoute(e);
              if (route) {
                newRoutes[0].children.push(route);
              }
            });
          }
        });
  
        console.log("newRoutes");
        console.log(newRoutes);
        router.addRoutes(newRoutes);
        
        // 修改Store中menus的hasRoutes为true
        hasRoute = true;
        store.commit("changeRouteStatus", hasRoute);
      });
  }
  
  // 继续执行下一个路由
  next();
});

7.菜单转换路由

// 导航转成路由函数,将菜单转换为路由对象
const menuToRoute = (menu) => {
  if (!menu.component) {
    return null;
  }
  
  let route = {
    name: menu.name,
    path: menu.path,
    meta: {
      icon: menu.icon,
      title: menu.title,
    },
  };
  // 路由懒加载,根据菜单信息动态加载组件
  route.component = () => import("@/components/" + menu.component + ".vue");
  console.log(route);
  return route;
};

十二、前端首页头部功能

1.头部布局

  <div style="line-height: 60px;">
        <div style="margin-top: 8px;float: left;">
            <i :class="icon" style="font-size: 20px;cursor: pointer;" @click="collapse"></i>
        </div>
        <div style="text-align: center;">
            <strong>通用权限管理系统</strong>
            <div class="header-avatar">
                <el-avatar size="medium" :src="'http://localhost:8090/upload/avatar/' + user.avatar"></el-avatar>
                <el-dropdown>
                    <span class="el-dropdown-link">
                        {{ user.username }}<i class="el-icon-arrow-down el-icon--right"></i>
                    </span>
                    <el-dropdown-menu slot="dropdown">
                        <el-dropdown-item @click.native="toAccount">个人中心
                        </el-dropdown-item>
                        <el-dropdown-item @click.native="logout">退出登录</el-dropdown-item>
                    </el-dropdown-menu>
                </el-dropdown>

                <el-link href="https://github.com/shengguanglongyi/PermissionAdmin.git" target="_blank">网站</el-link>
                <el-link href="https://www.bilibili.com/video/BV1bH4y1Z7jM/"
                    target="_blank">B站</el-link>
            </div>
        </div>
    </div>
    
    
    
.header-avatar {
    float: right;
    width: 210px;
    display: flex;
    justify-content: space-around;
    align-items: center;
}

.el-dropdown-link {
    cursor: pointer;
}

.el-header {
    background-color: #17B3A3;
    color: #333;
    text-align: center;
    line-height: 60px;
}

a {
    text-decoration: none;
}

2.准备EventBus.js

import Vue from 'vue'
export default new Vue()

3.导入EventBus.js

import EventBus from '../../assets/EventBus.js';

4.配置数据

data() {
        return {
            //获取用户信息
            user: JSON.parse(localStorage.getItem('LocalUser')),
        }
    },
// 接收父组件传递的icon属性
    props: {
        icon: String
    },

5.监听用户更新数据

mounted() {
        // 监听全局事件,用于更新当前用户信息
        EventBus.$on('update-cur-user', this.updateCurUser);
    }, 
    methods: {
        // 更新当前用户信息的方法
        updateCurUser(user) {
            this.user = JSON.parse(user)
        },
        }

6.监听导航标签页

 computed: {
        // 通过computed属性绑定this.$store.state.menus.editableTabs到editableTabs属性上
        editableTabs: {
            get() {
                return this.$store.state.menus.editableTabs
            },
            set(val) {
                this.$store.state.menus.editableTabs = val
            }
        },
        // 通过computed属性绑定this.$store.state.menus.editableTabsValue到editableTabsValue属性上
        editableTabsValue: {
            get() {
                return this.$store.state.menus.editableTabsValue
            },
            set(val) {
                this.$store.state.menus.editableTabsValue = val
            }
        }
    },

7.跳转个人中心

// 跳转至个人中心页面的方法
    toAccount() {
      let activeName = this.editableTabsValue;
      if ("sys:account" != activeName && "sys:account" !== this.$route.name) {
        // 如果当前激活的标签页不是个人中心页,则将激活的标签页设置为个人中心页,并跳转路由至个人中心页
        let tab = this.$store.state.menus.menuList.find((e) => e.name === "sys:account");
        // 先获取对应个人中心的菜单数据
        this.$store.commit("addTab", tab);
        // 然后再添加标签栏
        this.$router.push({ name: "sys:account" });
        // 最后跳转路由
      } else if (
        "sys:account" === activeName &&
        "sys:account" !== this.$route.name
      ) {
        // 如果当前激活的标签页是个人中心页且不是当前路由页,则直接跳转路由至个人中心页
        this.$router.push({ name: "sys:account" });
      }
    },

8.退出登录

// 退出登录的方法
        logout() {
            // 弹出确认框,确认退出登录
            this.$confirm('您确定要退出登录吗?', '提示', {
                confirmButtonText: '确定',  // 确认按钮的文字显示
                type: 'warning',
                center: true, // 文字居中显示
            })
                .then(() => {
                    // 发送退出登录请求
                    this.$axios.get(this.$url + '/user/logout').then(res => res.data).then(res => {
                        // console.log(res)
                        if (res.code == 200) {
                            // 重置全局状态
                            this.$store.commit("resetState")
                            // 跳转至登录页面
                            this.$router.push("/login")
                        } else {
                            // 提示错误消息
                            this.$message({
                                message: res.msg,
                                type: 'error'
                            });
                        }
                    });
                })
                .catch(() => {
                    // 提示取消退出登录信息
                    this.$message({
                        type: 'info',
                        message: '已取消退出登录'
                    })
                })

        },

9.退出登录后端接口

 @GetMapping("/logout")
    public SaResult logout(){
        if(StpUtil.isLogin()){
            StpUtil.logout();
        }
        return SaResult.ok();
    }

10.折叠菜单方法

  // 折叠展开菜单的方法,通过事件向父组件发送折叠命令
        collapse() {
            this.$emit('doCollapse')
        }

十三、前端首页侧边菜单栏功能

1.菜单布局

<el-menu :default-active="this.$store.state.menus.editableTabsValue" class="el-menu-vertical-demo"
		background-color="#545c64" text-color="#fff" :collapse="isCollapse" :collapse-transition="false"
		:unique-opened="true" active-text-color="#ffd04b">
		<el-menu-item>
			<i>
				<el-image style="width: 32px; height: 32px" :src="'http://localhost:8090/upload/file/logo'"></el-image>
			</i>
			<span slot="title">PermissionAdmin</span>
		</el-menu-item>
		<template v-for="menu in menuList">
			<template v-if="menu.type == 1 && menu.parentId == 0">
				<router-link :to="menu.path" :key="menu.id">
					<el-menu-item :to="menu.path" @click="selectMenu(menu)" :index="menu.name">
						<i :class="menu.icon"></i>
						<span slot="title">{{ menu.title }}</span>
					</el-menu-item>
				</router-link>
			</template>
			<template v-if="menu.type == 0 && menu.parentId == 0">
				<el-submenu :index="menu.name" :key="menu.id">
					<template slot="title">
						<i :class="menu.icon"></i>
						<span>{{ menu.title }}</span>
					</template>
					<router-link :to="item.path" v-for="item in menu.children" :key="item.id">
						<el-menu-item :index="item.name" @click="selectMenu(item)">
							<template slot="title">
								<i :class="item.icon"></i>
								<span slot="title">{{ item.title }}</span>
							</template>
						</el-menu-item>
					</router-link>
				</el-submenu>
			</template>
		</template>
	</el-menu>
	
	
.el-menu-vertical-demo {
	height: 100%;
}

2.更新菜单

 // 计算属性
  computed: {
    // menuList绑定到this.$store.state.menus.menuList上,当menuList发生变化时,会自动更新
    menuList: {
      get() {
        console.log(this.$store.state.menus.menuList);
        return this.$store.state.menus.menuList;
      }
    }
  },

3.折叠菜单

// 属性定义,接收是否折叠的属性
  props: {
    isCollapse: Boolean
  }

4.添加标签导航方法

 methods: {
    // 选择菜单项
    selectMenu(item) {
      // 调用$store.commit方法来将item添加到标签页中
      this.$store.commit("addTab", item);
    }
  },

十四、前端首页导航标签栏功能

1.导航标签布局

<el-tabs v-model="editableTabsValue" type="card" closable @tab-remove="removeTab" @tab-click="clickTab">
		<el-tab-pane v-for="(item) in editableTabs" :key="item.name" :label="item.title" :name="item.name">
		</el-tab-pane>
	</el-tabs>

2.更新导航标签

// 计算属性
  computed: {
    // 将this.$store.state.menus.editableTabs绑定到editableTabs上,并在修改时同步更新vuex中的状态
    editableTabs: {
      get() {
        return this.$store.state.menus.editableTabs;
      },
      set(val) {
        this.$store.state.menus.editableTabs = val;
      }
    },
    // 将this.$store.state.menus.editableTabsValue绑定到editableTabsValue上,并在修改时同步更新vuex中的状态
    editableTabsValue: {
      get() {
        return this.$store.state.menus.editableTabsValue;
      },
      set(val) {
        this.$store.state.menus.editableTabsValue = val;
      }
    }
  },

3.删除标签

 // 删除标签页
    removeTab(targetName) {
      // 获取可编辑标签页列表
      let tabs = this.editableTabs;
      // 获取当前激活的标签页名称
      let activeName = this.editableTabsValue;
      console.log(tabs);
      // 如果当前激活的标签页名称为"Home",则不能删除
      if (activeName === "Home") {
        return;
      }
      // 如果要删除的标签页名称为"Home",则跳转至"Home"页
      if (targetName === "Home") {
        this.$router.push({ name: targetName });
        return;
      }
      // 遍历所有可编辑标签页
      if (targetName === activeName) {
        tabs.forEach((tab, index) => {
          // 判断当前tab是否为要删除的tab
          if (tab.name === activeName) {
            console.log(activeName);
            // 如果当前tab为最后一个tab,则激活前一个tab
            if (index === tabs.length - 1) {
              activeName = tabs[index - 1].name;
              console.log(activeName);
              tabs.splice(index, 1); // 删除tab

              this.editableTabsValue = activeName;
              this.editableTabs = tabs;
              this.$router.push({ name: activeName });
            } else { // 否则激活后一个tab
              activeName = tabs[index + 1].name;
              console.log(activeName);
              tabs.splice(index, 1); // 删除tab

              this.editableTabsValue = activeName;
              this.editableTabs = tabs;
              this.$router.push({ name: activeName });
            }
          }
        });
      } else {
        tabs.forEach((tab, index) => {
          if (tab.name === targetName) {
            tabs.splice(index, 1); // 删除tab
            console.log(activeName);
          }
        });
      }
    },

4.点击标签

// 点击标签页
    clickTab(target) {
      console.log(target);
      // 如果点击的标签页名称不是当前路由名称,则跳转至该标签页
      if (target.name !== this.$route.name) {
        console.log(target.name);
        this.$router.push({ name: target.name });
      }
    },

5.默认初始化标签

  // 组件挂载后,调用clickFirstTab方法
  mounted() {
    this.clickFirstTab();
  }
  
  // 点击第一个标签页
    clickFirstTab() {
      let activeName = this.editableTabsValue;
      // 如果当前激活的标签页是"Home",且当前路由不是"Home",则跳转至"Home"页
      if ("Home" === activeName && "Home" !== this.$route.name) {
        this.$router.push({ name: "Home" });
      }
    }

6.查看效果

创建Account/Account.vue System/Menu.vue System/Role.vue User/User.vue User/Role.vue
运行查看效果

十五、前端菜单管理的实现

1.按钮栏

 <div style=" height: 50px;margin: 5px;float: right;">
            <el-button type="success" @click="getMenuTree">
                <i class="el-icon-refresh"></i></el-button>
            <el-button type="primary" style="margin-left: 5px;" @click="dialogVisible = true"
               > <i class="el-icon-plus"></i></el-button>
        </div>

2.表格部分

        <el-table :data="tableData" style="width: 100%;margin-bottom: 20px;" row-key="id" border stripe default-expand-all
            :tree-props="{ children: 'children', hasChildren: 'hasChildren' }">
            <el-table-column prop="name" label="名称" sortable width="180">
            </el-table-column>
            <el-table-column prop="perms" label="权限编码" sortable width="180">
            </el-table-column>
            <el-table-column prop="icon" label="图标">
            </el-table-column>
            <el-table-column prop="type" label="类型">
                <template slot-scope="scope">
                    <el-tag size="small" v-if="scope.row.type === 0" type="primary">目录</el-tag>
                    <el-tag size="small" v-else-if="scope.row.type === 1" type="success">菜单</el-tag>
                    <el-tag size="small" v-else-if="scope.row.type === 2" type="info">按钮</el-tag>
                </template>
            </el-table-column>
            <el-table-column prop="path" label="菜单URL">
            </el-table-column>
            <el-table-column prop="component" label="菜单组件">
            </el-table-column>
            <el-table-column prop="ordernum" label="排序号">
            </el-table-column>
            <el-table-column prop="status" label="状态">
                <template slot-scope="scope">
                    <el-tag size="small" v-if="scope.row.status === 1" type="success">正常</el-tag>
                    <el-tag size="small" v-else-if="scope.row.status === 0" type="danger">禁用</el-tag>
                </template>
            </el-table-column>
            <el-table-column prop="icon" label="操作">
                <template slot-scope="scope">
                    <el-button type="text" @click="editHandle(scope.row.id)"
                        >编辑</el-button>
                    <el-divider direction="vertical"></el-divider>
                    <template>
                        <el-popconfirm title="这是一段内容确定删除吗?" @confirm="delHandle(scope.row.id)"
                            >
                            <el-button type="text" slot="reference">删除</el-button>
                        </el-popconfirm>
                    </template>
                </template>
            </el-table-column>
        </el-table>

3.增改表单

        <el-dialog title="菜单表单" :visible.sync="dialogVisible" width="600px" :before-close="handleClose">
            <el-form :model="editForm" :rules="editFormRules" ref="editForm" label-width="100px" class="demo-editForm">
                <el-form-item label="上级菜单" prop="parentId">
                    <el-select v-model="editForm.parentId" placeholder="请选择上级菜单">
                        <template v-for="item in tableData">
                            <el-option :label="item.name" :value="item.id" :key="item.id"></el-option>
                            <template v-for="child in item.children">
                                <el-option :label="child.name" :value="child.id" :key="child.id">
                                    <span>{{ "- " + child.name }}</span>
                                </el-option>
                            </template>
                        </template>
                    </el-select>
                </el-form-item>
                <el-form-item label="菜单名称" prop="name" label-width="100px">
                    <el-input v-model="editForm.name" autocomplete="off"></el-input>
                </el-form-item>
                <el-form-item label="权限编码" prop="perms" label-width="100px">
                    <el-input v-model="editForm.perms" autocomplete="off"></el-input>
                </el-form-item>
                <el-form-item label="图标" prop="icon" label-width="100px">
                    <el-input v-model="editForm.icon" autocomplete="off"></el-input>
                </el-form-item>
                <el-form-item label="菜单URL" prop="path" label-width="100px">
                    <el-input v-model="editForm.path" autocomplete="off"></el-input>
                </el-form-item>
                <el-form-item label="菜单组件" prop="component" label-width="100px">
                    <el-input v-model="editForm.component" autocomplete="off"></el-input>
                </el-form-item>
                <el-form-item label="类型" prop="type" label-width="100px">
                    <el-radio-group v-model="editForm.type">
                        <el-radio :label=0>目录</el-radio>
                        <el-radio :label=1>菜单</el-radio>
                        <el-radio :label=2>按钮</el-radio>
                    </el-radio-group>
                </el-form-item>
                <el-form-item label="状态" prop="status" label-width="100px">
                    <el-radio-group v-model="editForm.status">
                        <el-radio :label=0>禁用</el-radio>
                        <el-radio :label=1>正常</el-radio>
                    </el-radio-group>
                </el-form-item>
                <el-form-item label="排序号" prop="ordernum" label-width="100px">
                    <el-input-number v-model="editForm.ordernum" :min="1" label="排序号">1</el-input-number>
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" @click="submitForm('editForm')">确定</el-button>
                    <el-button @click="resetForm('editForm')">重置</el-button>
                    <el-button type="danger" @click="dialogVisible=false">取消</el-button>
                </el-form-item>
            </el-form>
        </el-dialog>

4.表单数据配置

data() {
        return {
            dialogVisible: false,
            editForm: {},
            editFormRules: {
                parentId: [
                    { required: true, message: '请选择上级菜单', trigger: 'blur' }
                ],
                name: [
                    { required: true, message: '请输入名称', trigger: 'blur' }
                ],
                perms: [
                    { required: true, message: '请输入权限编码', trigger: 'blur' }
                ],
                type: [
                    { required: true, message: '请选择状态', trigger: 'blur' }
                ],
                ordernum: [
                    { required: true, message: '请填入排序号', trigger: 'blur' }
                ],
                status: [
                    { required: true, message: '请选择状态', trigger: 'blur' }
                ]
            },
        }
    },

5.表格数据配置

tableData: [],
 created() {
        this.getMenuTree()
    },
    methods: {
        getMenuTree() {
            this.$axios.get(this.$url + "/menu/tree").then(res => {
                this.tableData = res.data.data
            })
        },
    }

6.删除菜单

     delHandle(id) {
            this.$axios.post(this.$url + "/menu/delete/" + id).then(res => {
                console.log(res)
                this.$message({
                    showClose: true,
                    message: '恭喜你,操作成功',
                    type: 'success',
                }); 
                this.getMenuTree()
            })
        }

7.增改菜单

submitForm(formName) {
            this.$refs[formName].validate((valid) => {
                if (valid) {
                    this.$axios.post(this.$url + '/menu/' + (this.editForm.id ? 'update' : 'save'), this.editForm)
                        .then(res => {
                            console.log(res)
                            this.$message({
                                showClose: true,
                                message: '恭喜你,操作成功',
                                type: 'success',
                            });
                            this.getMenuTree()
                            this.dialogVisible = false
                        })
                } else {

                    return false;
                }
            });
        },
        editHandle(id) {
            this.$axios.get(this.$url + '/menu/info/' + id).then(res => {
                this.editForm = res.data.data
                this.dialogVisible = true
            })
        },
        resetForm(formName) {
            this.$refs[formName].resetFields();
            this.dialogVisible = false
            this.editForm = {}
        },
        handleClose() {
            this.resetForm('editForm')
        },

十六、前端角色管理的实现

1.搜索栏

<div style=" height: 50px;margin: 5px;">
            <el-input v-model="name" placeholder="名称" suffix-icon="el-icon-search" style="width: 200px;"
                @keyup.enter.native="getRoleList"></el-input>
            <el-select v-model="status" filterable placeholder="请选择权限状态" style="margin-left: 5px;">
                <el-option v-for="item in statusmap" :key="item.value" :label="item.label" :value="item.value">
                </el-option>
            </el-select>
            <el-button type="primary" style="margin-left: 5px;" @click="getRoleList"> <i
                    class="el-icon-search"></i></el-button>
            <el-button type="success" @click="resetParam">
                <i class="el-icon-refresh"></i></el-button>
            <el-button type="primary" style="margin-left: 5px;" @click="dialogVisible = true"
                > <i class="el-icon-plus"></i></el-button>
            <el-popconfirm title="这是确定批量删除吗?" @confirm="delHandle(null)" >
                <el-button type="danger" slot="reference" :disabled="delBtlStatu">批量删除</el-button>
            </el-popconfirm>
        </div>

2.表格部分

        <el-table ref="multipleTable" :data="tableData" tooltip-effect="dark" style="width: 100%" border stripe
            @selection-change="handleSelectionChange">
            <el-table-column type="selection" width="55">
            </el-table-column>
            <el-table-column prop="name" label="名称" width="120">
            </el-table-column>
            <el-table-column prop="code" label="唯一编码" show-overflow-tooltip>
            </el-table-column>
            <el-table-column prop="remark" label="描述" show-overflow-tooltip>
            </el-table-column>
            <el-table-column prop="status" label="状态">
                <template slot-scope="scope">
                    <el-tag size="small" v-if="scope.row.status === 1" type="success">正常</el-tag>
                    <el-tag size="small" v-else-if="scope.row.status === 0" type="danger">禁用</el-tag>
                </template>
            </el-table-column>
            <el-table-column prop="icon" label="操作">
                <template slot-scope="scope">
                    <el-button type="text" @click="permHandle(scope.row.id)">分配权限</el-button>
                    <el-divider direction="vertical"></el-divider>
                    <el-button type="text" @click="editHandle(scope.row.id)">编辑</el-button>
                    <el-divider direction="vertical"></el-divider>
                    <template>
                        <el-popconfirm title="你确定删除吗?" @confirm="delHandle(scope.row.id)">
                            <el-button type="text" slot="reference">删除</el-button>
                        </el-popconfirm>
                    </template>
                </template>
            </el-table-column>
        </el-table>

3.分页部分

<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="pageNum"
            :page-sizes="[5, 10, 20, 30]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper"
            :total="total">
        </el-pagination>
        
        <style scoped>
.el-pagination {
    text-align: center;
    margin-top: 22px;
}
</style>

4.增改表单

        <el-dialog title="角色表单" :visible.sync="dialogVisible" width="600px" :before-close="handleClose">
            <el-form :model="editForm" :rules="editFormRules" ref="editForm" label-width="100px" class="demo-editForm">
                <el-form-item label="角色名称" prop="name" label-width="100px">
                    <el-input v-model="editForm.name" autocomplete="off"></el-input>
                </el-form-item>
                <el-form-item label="唯一编码" prop="code" label-width="100px">
                    <el-input v-model="editForm.code" autocomplete="off"></el-input>
                </el-form-item>
                <el-form-item label="描述" prop="remark" label-width="100px">
                    <el-input v-model="editForm.remark" autocomplete="off"></el-input>
                </el-form-item>
                <el-form-item label="状态" prop="status" label-width="100px">
                    <el-radio-group v-model="editForm.status">
                        <el-radio :label=0>禁用</el-radio>
                        <el-radio :label=1>正常</el-radio>
                    </el-radio-group>
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" @click="submitForm('editForm')">确定</el-button>
                    <el-button @click="resetForm('editForm')">重置</el-button>
                    <el-button type="danger" @click="dialogVisible=false">取消</el-button>
                </el-form-item>
            </el-form>
        </el-dialog>

5.分配权限表单

        <el-dialog title="分配权限" :visible.sync="permDialogVisible" width="600px">
            <el-form :model="permForm">
                <el-tree :data="permTreeData" show-checkbox ref="permTree" :default-expand-all=true node-key="id"
                    :check-strictly=true :props="defaultProps">
                </el-tree>
            </el-form>
            <span slot="footer" class="dialog-footer">
                <el-button @click="permDialogVisible = false">取 消</el-button>
                <el-button type="primary" @click="submitPermFormHandle('permForm')">确 定</el-button>
            </span>
        </el-dialog>

6.数据配置

  data() {
        return {
        //表格数据
            tableData: [],
            //分页配置
            pageSize: 10,
            pageNum: 1,
            total: 0,
            //条件搜索
            name: '',
            status: '',
            statusmap: [
                {
                    value: 1,
                    label: '正常'
                }, {
                    value: 0,
                    label: '禁用'
                },
            ],
            //多选删除
            delBtlStatu: true,
            multipleSelection: [],
            //增改表单
            dialogVisible: false,
            editForm: {},
            editFormRules: {
                name: [
                    { required: true, message: '请输入角色名称', trigger: 'blur' }
                ],
                code: [
                    { required: true, message: '请输入唯一编码', trigger: 'blur' }
                ],
                status: [
                    { required: true, message: '请选择状态', trigger: 'blur' }
                ]
            },
            //权限树数据
            permTreeData: [],
            defaultProps: {
                children: 'children',
                label: 'name'
            },
            权限分配表单
            permDialogVisible: false,
            permForm: {},
        }
    },

7.初始化数据获取

    created() {
        this.getRoleList()
        this.getpermTree()
    },
    methods: {
        getpermTree(){
            this.$axios.get(this.$url + '/menu/tree').then(res => {
            this.permTreeData = res.data.data
        })
        },
        //重置条件搜索框
        resetParam() {
      this.name = "";
      this.status = "";
    },
        getRoleList() {
            this.$axios.post(this.$url + "/role/listPage", {
                pageSize: this.pageSize,
                pageNum: this.pageNum,
                param: {
                    name: this.name,
                    status: this.status + ''
                }
            }).then(res => {
                console.log(res)
                this.tableData = res.data.data
                this.total = res.data.total
            })
        },
    }

8.多选删除

 toggleSelection(rows) {
            if (rows) {
                rows.forEach(row => {
                    this.$refs.multipleTable.toggleRowSelection(row);
                });
            } else {
                this.$refs.multipleTable.clearSelection();
            }
        },
       
       handleSelectionChange(val) {
            this.multipleSelection = val;
            this.delBtlStatu = val.length == 0
        },
       
       delHandle(id) {
      var ids = [];
      if (id) {
        ids.push(id);
      } else {
        this.multipleSelection.forEach((row) => {
          ids.push(row.id);
        });
      }
      this.$axios.post(this.$url + "/role/deleteBatch", ids).then((res) => {
        console.log(res);
        this.$message({
          showClose: true,
          message: "恭喜你,操作成功",
          type: "success",
        });
        this.getRoleList();
      });
    },

8.增改角色

resetForm(formName) {
      this.$refs[formName].resetFields();
      this.dialogVisible = false;
      this.editForm = {};
    },
    handleClose() {
      this.resetForm("editForm");
    },
        submitForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          this.$axios
            .post(
              this.$url + "/role/" + (this.editForm.id ? "update" : "save"),
              this.editForm
            )
            .then((res) => {
              console.log(res);
              this.$message({
                showClose: true,
                message: "恭喜你,操作成功",
                type: "success",
              });
              this.getRoleList();
              this.dialogVisible = false;
              this.resetForm(formName);
            });
        } else {
          return false;
        }
      });
    },
    editHandle(id) {
      this.$axios.get(this.$url + "/role/info/" + id).then((res) => {
        this.editForm = res.data.data;
        this.dialogVisible = true;
      });
    },

9.分页函数

handleSizeChange(val) {
      console.log(`每页 ${val} 条`);
      this.size = val;
      this.getRoleList();
    },
    handleCurrentChange(val) {
      console.log(`当前页: ${val}`);
      this.current = val;
      this.getRoleList();
    },

10.权限分配

permHandle(id) {
      this.permDialogVisible = true;
      this.$axios.get(this.$url + "/role/info/" + id).then((res) => {
        this.$refs.permTree.setCheckedKeys(res.data.data.menuIds);
        this.permForm = res.data.data;
      });
    },
    submitPermFormHandle() {
      var menuIds = this.$refs.permTree.getCheckedKeys();
      this.$axios
        .post(this.$url + "/role/perm/" + this.permForm.id, menuIds)
        .then((res) => {
          console.log(res);
          this.$message({
            showClose: true,
            message: "恭喜你,操作成功",
            type: "success",
          });
          this.getRoleList();
          this.permDialogVisible = false;
        });
    },

11.测试

添加用户角色,并分配权限

十七、前端用户管理的实现(上)

1.搜索栏

<div style=" height: 50px;margin: 5px;">
            <el-input v-model="username" placeholder="请输入用户姓名" suffix-icon="el-icon-search" style="width: 200px;"
                @keyup.enter.native="loadPost"></el-input>
            <el-input v-model="email" placeholder="请输入用户邮箱" suffix-icon="el-icon-search" style="width: 200px;"
                @keyup.enter.native="loadPost"></el-input>
            <el-select v-model="status" filterable placeholder="请选择用户状态" style="margin-left: 5px;">
                <el-option v-for="item in statusmap" :key="item.value" :label="item.label" :value="item.value">
                </el-option>
            </el-select>
            <el-button type="primary" style="margin-left: 5px;" @click="loadPost">
                <i class="el-icon-search"></i></el-button>
            <el-button type="success" @click="resetParam"> <i class="el-icon-refresh"></i></el-button>
            <el-button type="primary" style="margin-left: 5px;" @click="add" >
                <i class="el-icon-plus"></i></el-button>
            <el-popconfirm title="这是确定批量删除吗?" @confirm="delHandle(null)">
                <el-button type="danger" slot="reference" :disabled="delBtlStatu">批量删除</el-button>
            </el-popconfirm>
        </div>

2.表格部分

        <el-table ref="multipleTable" :data="tableData" tooltip-effect="dark" style="width: 100%" border stripe
            @selection-change="handleSelectionChange">
            <el-table-column type="selection" width="55">
            </el-table-column>
            <el-table-column label="头像" width="50">
                <template slot-scope="scope">
                    <el-avatar size="small" :src="'http://localhost:8090/upload/avatar/' + scope.row.avatar"
                        v-if="scope.row.avatar"></el-avatar>
                </template>
            </el-table-column>
            <el-table-column prop="username" label="用户名" width="120">
            </el-table-column>
            <el-table-column prop="Roles" label="角色名称">
                <template slot-scope="scope">
                    <el-tag size="small" type="info">{{ scope.row.roles.name
                    }}</el-tag>
                </template>
            </el-table-column>
            <el-table-column prop="email" label="邮箱" width="200">
            </el-table-column>
            <el-table-column prop="status" label="状态">
                <template slot-scope="scope">
                    <el-tag size="small" v-if="scope.row.status === 1" type="success">正常</el-tag>
                    <el-tag size="small" v-else-if="scope.row.status === 0" type="danger">禁用</el-tag>
                </template>
            </el-table-column>
            <el-table-column prop="created" width="200" label="注册时间" :formatter="formatDate">
            </el-table-column>
            <el-table-column prop="icon" width="260px" label="操作">
                <template slot-scope="scope">
                    <el-button type="text" @click="mod(scope.row.id)">编辑</el-button>
                    <el-divider direction="vertical"></el-divider>
                    <el-popconfirm title="确定删除吗?" @confirm="dele(scope.row.id)" >
                        <el-button type="text" slot="reference">删除</el-button>
                    </el-popconfirm>
                </template>
            </el-table-column>
        </el-table>

3.分页部分

参上

4.增改表单

<el-dialog title="用户表单" :visible.sync="centerDialogVisible" width="50%" center>
            <el-form :model="form" :rules="rules" ref="form" label-width="100px" style="width: 800px; margin: 10px auto;"
                method="post" enctype="multipart/form-data">
                <el-row>
                    <el-col :span="12">
                        <el-form-item label="用户姓名" prop="username">
                            <el-input v-model="form.username"></el-input>
                        </el-form-item>
                        <el-form-item label="用户密码" prop="password">
                            <el-input type="password" v-model="form.password"></el-input>
                        </el-form-item>
                        <el-form-item label="用户邮箱" prop="email">
                            <el-input v-model="form.email" autocomplete="off"></el-input>
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="用户头像" prop="avater">
                            <div class="img-show" v-if="imgUrl">
                                <img :src="imgUrl" class="avatar" v-if="!ismod">
                                <img :src="'http://localhost:8090/upload/avatar/' + form.avatar" class="avatar"
                                    v-if="ismod">
                                <span class="actions">
                                    <!-- 删除 -->
                                    <span class="item">
                                        <i class="el-icon-delete" @click="del()"></i>
                                    </span>
                                </span>
                            </div>
                            <!-- 图片上传 -->
                            <el-upload v-else action="#" class="uploader-avatar" list-type="picture" :auto-upload="false"
                                :show-file-list="false" :on-change="imgPreview">
                                <i class="el-icon-plus avatar-uploader-icon"></i>
                            </el-upload>
                        </el-form-item>
                    </el-col>
                </el-row>
            </el-form>
            <span slot="footer" class="dialog-footer">
                <el-button @click="centerDialogVisible = false">取 消</el-button>
                <el-button type="primary" @click="save">确 定</el-button>
            </span>
        </el-dialog>

5.头像上传部分样式

.uploader-avatar {
    display: flex;
    /* 添加flex属性 */
    justify-content: center;
    /* 水平居中 */
    align-items: center;
    /* 垂直居中 */
    background-color: #fbfdff;
    border: 1px dashed #c0ccda;
    border-radius: 6px;
    box-sizing: border-box;
    width: 200px;
    height: 200px;
    cursor: pointer;
    line-height: 300px;
    vertical-align: top;
    margin-bottom: 10px;
    overflow: hidden;
}

.img-show {
    position: relative;
    border: 1px solid #c0ccda;
    border-radius: 6px;
    box-sizing: border-box;
    width: 200px;
    height: 200px;
    cursor: pointer;
    overflow: hidden;
}

.uploader-avatar:hover {
    border-color: #409EFF;
}

.avatar-uploader-icon {
    font-size: 28px;
    color: #8c939d;
}

.avatar {
    width: 200px;
    height: 200px;
    display: block;
}

.actions {
    position: absolute;
    width: 100%;
    height: 100%;
    line-height: 300px;
    left: 0;
    top: 0;
    cursor: default;
    text-align: center;
    color: #fff;
    opacity: 0;
    font-size: 20px;
    background-color: rgba(0, 0, 0, .5);
    transition: opacity .3s;
}

.actions:hover {
    opacity: 1;
}

.actions:hover span {
    display: inline-block;
}

.actions span {
    display: none;
    margin: 0 16px;
    cursor: pointer;
}

6.数据配置

 data() {
        return {
        //表格数据
            tableData: [],
            //分页配置
            pageSize: 5,
            pageNum: 1,
            total: 0,
            //条件查询
            username: '',
            status: '',
            statusmap: [
                {
                    value: 1,
                    label: '正常'
                }, {
                    value: 0,
                    label: '注销'
                },
            ],
            email: '',
              //多选删除
            delBtlStatu: true,
            multipleSelection: [],
            //增改表单
            centerDialogVisible: false,
            form: {
                id: '',
                username: '',
                password: '',
                email: '',
                avatar: '',
            },
            
            rules: {
                username: [
                    { required: true, message: '请输入用户姓名', trigger: 'blur' }
                ],
                password: [
                    { required: true, message: '请输入用户密码', trigger: 'blur' }
                ],
                roleId: [
                    { required: true, message: '请选择用户角色', trigger: 'blur' }
                ],
                email: [
                    { required: true, message: '请输入邮箱', trigger: 'blur' }
                ],
                status: [
                    { required: true, message: '请选择状态', trigger: 'blur' }
                ]
            },
           // 头像数据
            imgUrl: '',
            dialogUrl: '',
            ismod: false,
            dialogVisible: false,
            file: null,
        }
    },

7.分页条件查询

mounted() {
        this.loadPost();
    },
     methods: {
     //格式化时间显示
       formatDate(row, column) {
      let data = row[column.property];
      if (data == null) {
        return null;
      }
      let dt = new Date(data);
      return (
        dt.getFullYear() +
        "-" +
        (dt.getMonth() + 1) +
        "-" +
        dt.getDate() +
        " " +
        dt.getHours() +
        ":" +
        dt.getMinutes() +
        ":" +
        dt.getSeconds()
      );
    },
   // 重置条件搜索
        resetParam() {
            this.username = ''
            this.status = ''
            this.email=''
        },
        loadPost() {
            this.$axios.post(this.$url + '/user/listPage', {
                pageSize: this.pageSize,
                pageNum: this.pageNum,
                param: {
                    username: this.username,
                    status: this.status + '',
                    email:this.email                }
            }).then(res => res.data).then(res => {
                console.log(res)
                if (res.code == 200) {
                    this.tableData = res.data
                    this.total = res.total
                } else {
                    alert('获取数据失败')
                }

            })
        },
}

8.头像上传部分代码

        imgPreview: function (file) {
            //生成临时缩略图
            this.imgUrl = URL.createObjectURL(file.raw);
            this.file = file
        },
        del: function () {
            this.imgUrl = '';
            this.form.avatar = ''
            this.ismod = false
        },
        submitUpload() {
            if (this.file == null) {
                this.$message({
                    message: '请先选择用户头像',
                    type: 'error'
                });
                return false;
            } else {
                const config = {
                    headers: {
                        'Content-Type': 'multipart/form-data'
                    }
                };
                if (this.beforeUpload(this.file.name)) {
                    let fd = new FormData();
                    fd.append("file", this.file.raw);
                    fd.append("fileName", this.file.name)
                    console.log(this.file.raw)
                    this.$axios.post(this.$url + "/upload/avatar", fd, config)
                        .then(res => {
                            console.log(res);
                            if (res.status === 200) {
                                this.form.avatar = res.data.data
                                this.$message({
                                    message: '上传成功!',
                                    type: 'success'
                                });
                            } else {
                                this.$message({
                                    message: '未上传成功!',
                                    type: 'error'
                                });
                            }
                        })

                } else {
                    return false;
                }

            }
        },
        handleSuccess(response) {
            console.log(response)
            this.$message({
                message: '上传成功',
                type: 'success',
            });
            return true;
        },
        beforeUpload(fileName) {
            const isStl = this.checkImgType(fileName)
            if (!isStl) {
                this.$message.error('只能上传图片文件');
            }
            return isStl;
        },
        checkImgType(fileName) {
            //用文件名name后缀判断文件类型,可用size属性判断文件大小不能超过500k , 前端直接判断的好处,免去服务器的压力。
            console.log(fileName)
            if (!/\.(jpg|jpeg|png)$/.test(fileName)) {
                return false;
            } else {
                return true;
            }
        },

9.多选删除

参上

10.增改用户

 resetForm() {
      this.form = {
        id: "",
        username: "",
        password: "",
        email: "",
        avatar: "",
      };
      (this.imgUrl = ""),
        (this.dialogUrl = ""),
        (this.ismod = false),
        (this.dialogVisible = false),
        (this.file = null),
        (this.dialogVisible = false);
    },
    handleClose() {
      this.resetForm();
    },
    add() {
      this.centerDialogVisible = true;
      this.$nextTick(() => {
        this.resetForm();
      });
    },
    mod(id) {
      this.$axios.get(this.$url + "/user/info/" + id).then((res) => {
        this.form = res.data.data;
        console.log(this.form);
        if (this.form.avatar != undefined) {
          this.imgUrl = true;
          this.ismod = true;
        }
        this.centerDialogVisible = true;
      });
    },
    async save() {
      if (this.form.avatar == "" || this.form.avatar == null) {
        await this.submitUpload();
      }
      setTimeout(() => {
        this.$refs.form.validate((valid) => {
          if (valid) {
            if (this.form.id) {
              this.doMod();
            } else {
              this.doSave();
            }
          } else {
            console.log("error submit!!");
            return false;
          }
        });
      }, 500);
    },
    doSave() {
      this.$axios
        .post(this.$url + "/user/save", this.form)
        .then((res) => res.data)
        .then((res) => {
          console.log(res);
          if (res.code == 200) {
            this.$message({
              message: "添加成功!",
              type: "success",
            });
            this.centerDialogVisible = false;
            //关闭弹窗
            this.loadPost();
            //重新加载数据
            this.resetForm("form");
            //重置表单数据
          } else {
            this.$message({
              message: "添加失败!",
              type: "error",
            });
          }
        });
    },
    doMod() {
      this.$axios
        .post(this.$url + "/user/update", this.form)
        .then((res) => res.data)
        .then((res) => {
          console.log(res);
          if (res.code == 200) {
            this.$message({
              message: "修改成功!",
              type: "success",
            });
            this.centerDialogVisible = false;
            this.loadPost();
            this.resetForm("form");
          } else {
            this.$message({
              message: "修改失败!",
              type: "error",
            });
          }
        });
    },

11.删除用户

dele(id) {
      this.$axios
        .get(this.$url + "/user/delete?id=" + id)
        .then((res) => res.data)
        .then((res) => {
          console.log(res);
          if (res.code == 200) {
            this.$message({
              message: "删除成功!",
              type: "success",
            });
            this.loadPost();
          } else {
            this.$message({
              message: "删除失败!",
              type: "error",
            });
          }
        });
    },

十八、前端用户权限管理的实现(下)

1.搜索栏

  <div style=" height: 50px;margin: 5px;">
      <el-input v-model="username" placeholder="请输入用户姓名" suffix-icon="el-icon-search" style="width: 200px;"
        @keyup.enter.native="loadPost"></el-input>
      <el-select v-model="status" filterable placeholder="请选择用户状态" style="margin-left: 5px;">
        <el-option v-for="item in statusmap" :key="item.value" :label="item.label" :value="item.value">
        </el-option>
      </el-select>
      <el-button type="primary" style="margin-left: 5px;" @click="loadPost"> <i class="el-icon-search"></i></el-button>
      <el-button type="success" @click="resetParam"> <i class="el-icon-refresh"></i></el-button>
    </div>

2.表格部分

    <el-table
      :data="tableData"
      tooltip-effect="dark"
      style="width: 100%;text-align:center;"
      border
    >
      <el-table-column prop="username" label="用户名" width="120">
      </el-table-column>
      <el-table-column label="头像" >
        <template slot-scope="scope">
          <el-avatar
            size="small"
            :src="'http://localhost:8090/upload/avatar/' + scope.row.avatar"
            v-if="scope.row.avatar"
          ></el-avatar>
        </template>
      </el-table-column>
      <el-table-column prop="Roles" label="角色名称">
        <template slot-scope="scope">
          <el-tag size="small" type="info">{{ scope.row.roles.name }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="email" label="邮箱" width="200"> </el-table-column>
      <el-table-column prop="status" label="状态">
        <template slot-scope="scope">
          <el-tag size="small" v-if="scope.row.status === 1" type="success"
            >正常</el-tag
          >
          <el-tag size="small" v-else-if="scope.row.status === 0" type="danger"
            >禁用</el-tag
          >
        </template>
      </el-table-column>
      <el-table-column
        prop="created"
        width="200"
        label="注册时间"
        :formatter="formatDate"
      >
      </el-table-column>
      <el-table-column prop="icon" width="400px" label="操作">
        <template slot-scope="scope">
          <el-button
            type="text"
            @click="roleHandle(scope.row.id)"
           
            >分配角色</el-button
          >
          <el-divider direction="vertical"></el-divider>
          <el-switch
            :value="scope.row.status === 1"
            active-text="启用"
            inactive-text="禁用"
            style="margin-right: 20px"
            @change="modStatus(scope.row.id)"
           
          ></el-switch>
          <el-divider direction="vertical"></el-divider>
          <el-button
            type="text"
            @click="repassHandle(scope.row.id, scope.row.username)"
          
            >重置密码</el-button
          >
          <el-divider direction="vertical"></el-divider>
        </template>
      </el-table-column>
    </el-table>

3.分页部分

参上

4.分配角色表单

 <el-dialog
      title="分配角色"
      :visible.sync="roleDialogFormVisible"
      width="600px"
    >
      <el-radio-group v-model="roleId" size="mini">
        <div
          v-for="role in roleData"
          :key="role.id"
          style="width: 100%; padding: 10px"
        >
          <el-radio :label="role.id" border>{{ role.name }}</el-radio>
        </div>
      </el-radio-group>
      <div slot="footer" class="dialog-footer">
        <el-button @click="roleDialogFormVisible = false">取 消</el-button>
        <el-button type="primary" @click="submitRoleHandle()">确 定</el-button>
      </div>
    </el-dialog>

5.数据配置

  data() {
    return {
    //表格数据
      tableData: [],
      //角色数据
      roleData: [],
      //分页数据
      pageSize: 5,
      pageNum: 1,
      total: 0,
      //条件搜索
      username: "",
      status: "",
      statusmap: [
        {
          value: 1,
          label: "正常",
        },
        {
          value: 0,
          label: "注销",
        },
      ],
      //角色分配表单
      roleId: "",
      roleDialogFormVisible: false,
    };
  },

6.初始化数据

 mounted() {
    this.loadPost();
    this.getRole();
  },
  methods: {
    getRole() {
      this.$axios.get(this.$url + "/role/list").then((res) => {
        this.roleData = res.data;
      });
    },
    formatDate(row, column) {
      let data = row[column.property];
      if (data == null) {
        return null;
      }
      let dt = new Date(data);
      return (
        dt.getFullYear() +
        "-" +
        (dt.getMonth() + 1) +
        "-" +
        dt.getDate() +
        " " +
        dt.getHours() +
        ":" +
        dt.getMinutes() +
        ":" +
        dt.getSeconds()
      );
    },
    resetParam() {
      this.name = "";
      this.status = "";
    },

    loadPost() {
      this.$axios
        .post(this.$url + "/user/listPage", {
          pageSize: this.pageSize,
          pageNum: this.pageNum,
          param: {
            name: this.name,
            status: this.status + "",
          },
        })
        .then((res) => res.data)
        .then((res) => {
          console.log(res);
          if (res.code == 200) {
            this.tableData = res.data;
            this.total = res.total;
          } else {
            alert("获取数据失败");
          }
        });
    },
    
  },

7.禁用/启用用户

modStatus(id) {
      this.$axios
        .post(this.$url + "/user/control/" + id)
        .then((res) => res.data)
        .then((res) => {
          console.log(res);
          if (res.code == 200) {
            this.loadPost();
          } else {
            this.$message({
              message: "操作失败!",
              type: "error",
            });
          }
        });
    },

8.分配角色

  roleHandle(id) {
      this.roleDialogFormVisible = true;
      this.$axios.get(this.$url + "/user/info/" + id).then((res) => {
        this.roleId = res.data.data.roles.id;
      });
    },
    submitRoleHandle() {
      this.$axios
        .post(
          this.$url +
            "/user/role?userId=" +
            this.user.id +
            "&roleId=" +
            this.roleId
        )
        .then((res) => {
          console.log(res);
          this.$message({
            showClose: true,
            message: "恭喜你,操作成功",
            type: "success"
          });
  this.loadPost();
          this.roleDialogFormVisible = false;
        });
    },

9.重置密码

 repassHandle(id, username) {
      this.$confirm("将重置用户【" + username + "】的密码, 是否继续?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          this.$axios
            .post(this.$url + "/user/repass", id, {
              headers: {
                "Content-Type": "application/json;charset=UTF-8",
              },
            })
            .then((res) => {
              console.log(res);
              this.$message({
                showClose: true,
                message: "恭喜你,操作成功",
                type: "success",
              });
            })
            .catch((err) => {
              console.error(err);
              this.$message({
                showClose: true,
                message: "操作失败,请重试",
                type: "error",
              });
            });
        })
        .catch(() => {
          this.$message({
            showClose: true,
            message: "已取消操作",
            type: "info",
          });
        });
    },

十九、按钮级别权限控制

1.配置数据项

//用户信息
user: JSON.parse(localStorage.getItem("LocalUser")),
//用户权限信息
Authority: "",

2.获取权限数据

 mounted() {
    this.getAuthority();
  },
  methods: {
    getAuthority() {
      this.$axios.get(this.$url + "/user/auth/" + this.user.id).then((res) => {
        this.Authority = res.data;
      });
    },
    }

3.权限判断函数

   hasAuth(auth) {
      return this.Authority.includes(auth);
    },

4.权限控制

menu部分
<el-button type="primary" style="margin-left: 5px;" @click="dialogVisible = true"
                v-if="hasAuth('sys:menu:save')"> <i class="el-icon-plus"></i></el-button>
<el-button type="text" @click="editHandle(scope.row.id)"
                        v-if="hasAuth('sys:menu:update')">编辑</el-button>
 <el-popconfirm title="这是一段内容确定删除吗?" @confirm="delHandle(scope.row.id)"
                            v-if="hasAuth('sys:menu:delete')">
                            <el-button type="text" slot="reference">删除</el-button>
                        </el-popconfirm>
role部分
<el-button
        type="primary"
        style="margin-left: 5px"
        @click="dialogVisible = true"
        v-if="hasAuth('sys:role:save')"
      >
        <i class="el-icon-plus"></i
      ></el-button>
      <el-popconfirm
        title="这是确定批量删除吗?"
        @confirm="delHandle(null)"
        v-if="hasAuth('sys:role:delete')"
      >
        <el-button type="danger" slot="reference" :disabled="delBtlStatu"
          >批量删除</el-button
        >
      </el-popconfirm>
<el-button type="text" @click="permHandle(scope.row.id)" v-if="hasAuth('sys:role:perm')"
            >分配权限</el-button
          >
          <el-divider direction="vertical"></el-divider>
          <el-button type="text" @click="editHandle(scope.row.id)" v-if="hasAuth('sys:role:edit')"
            >编辑</el-button
          >
          <el-divider direction="vertical"></el-divider>
          <template>
            <el-popconfirm v-if="hasAuth('sys:role:delete')"
              title="你确定删除吗?"
              @confirm="delHandle(scope.row.id)"
            >
              <el-button type="text" slot="reference">删除</el-button>
            </el-popconfirm>
          </template>

user这一块自行补充

二十、个人中心功能的实现(上)

1.个人中心布局

三行布局

第一行4列个人信息

第二行3列修改个人信息功能

第三行2列系统功能

 <div style="text-align: center;background-color: #f1f1f3;height: 100%;padding: 0px;margin: 0px;">
        <el-row :gutter="12" class="card-row">
            <el-col :span="6">
            </el-col>
            <el-col :span="6">
            </el-col>
            <el-col :span="6">
            </el-col>
            <el-col :span="6">
            </el-col>
        </el-row>
        <el-row :gutter="12" class="card-row">
            <el-col :span="8">
            </el-col>
            <el-col :span="8">
            </el-col>
            <el-col :span="8">
            </el-col>
        </el-row>
        <el-row :gutter="12" class="card-row">
            <el-col :span="12">
            </el-col>
            <el-col :span="12">
            </el-col>
        </el-row>
    </div>

2.Card组件样式

<style scoped>
.box-card {
    height: 150px;
    text-align: center;
    margin: 20px;
}

.card-row {
    display: flex;
    justify-content: center;
}

.centered-card {
    display: flex;
    justify-content: center;
    align-items: center;
}
</style>

3.自定义card组件颜色

.red {
	background-color: rgb(231, 9, 9);
}

.green {
	background-color: rgb(152, 246, 11);
}

.purple {
	background-color: rgb(135, 53, 212);
}

.blue {
	background-color: rgb(10, 138, 207);
}

.orange {
	background-color: chocolate;
}

.grey {
	background-color: grey;
}

.pink {
	background-color: blanchedalmond;
}

.gold {
	background-color: goldenrod;
}

4.card组件的应用

第一行:
<el-card shadow="hover" class="box-card centered-card">
                    <div>
                        <i class="el-icon-s-custom"></i>
                        用户名
                        <el-tag type="success">
                            {{ user.username }}
                        </el-tag>
                    </div>
                </el-card>
                
 <el-card shadow="hover" class="box-card centered-card" style="display: flex; align-items: center;">
                    <div>
                        <el-avatar size="large" :src="'http://localhost:8090/upload/avatar/' + user.avatar" fit="cover"
                            style="width:100px;height:100px"></el-avatar>
                    </div>
                    <el-button type="success" size="small" @click="modAvatar">更换头像</el-button>
                </el-card>
 
<el-card shadow="hover" class="box-card centered-card">
                    <div>
                        <i class="el-icon-s-promotion"></i>
                        角色
                        <el-tag disable-transitions
                            :type="user.roles.id == 1 ? 'success' : user.roles.id == 1 ? 'primary' : 'danger'">
                            {{ user.roles.name
                            }}
                        </el-tag>
                    </div>
                </el-card>
                
 <el-card shadow="hover" class="box-card centered-card">
                    <div>
                        <i class="el-icon-s-promotion"></i>
                        邮箱
                        <el-tag disable-transitions>
                            {{ user.email }}
                        </el-tag>
                    </div>
                </el-card>
                
第二行:
  <el-card shadow="hover" class="box-card centered-card">
                    <div>
                        <el-button class="el-icon-edit-outline" type="success" @click="name()">修改昵称</el-button>
                    </div>
                </el-card>
 
 <el-card shadow="hover" class="box-card centered-card">
                    <div>
                        <el-button class="el-icon-lock" type="success" @click="pwd()"> 修改密码</el-button>
                    </div>
                </el-card>
               
 <el-card shadow="hover" class="box-card centered-card" v-if="user.email">
                    <div>
                        <el-button class="el-icon-message" type="success" @click="email()"> 修改邮箱</el-button>
                    </div>
                </el-card>
                <el-card shadow="hover" class="box-card centered-card" v-if="!user.email">
                    <div>
                        <el-button class="el-icon-message" type="success" @click="email()"> 绑定邮箱</el-button>
                    </div>
                </el-card>

第三行:
  <el-card shadow="hover" class="box-card centered-card">
                    <div>
                        <el-button class="el-icon-switch-button" type="danger" @click="logout">退出登录</el-button>
                    </div>
                </el-card>

  <el-card shadow="hover" class="box-card centered-card">
                    <div>
                        <el-button class="el-icon-delete" type="danger" @click="delUser">注销账号</el-button>
                    </div>
                </el-card>

给card加上自定义颜色

5.数据配置

data() {
        return {
            user: {},
            dialogNameVisible: false,
            dialogPwdVisible: false,
            dialogEmailVisible: false,
            roleData: [],
        }
    },
    methods: {
        init() {
            this.user = JSON.parse(localStorage.getItem('LocalUser'))
        },
        pwd() {
            this.dialogPwdVisible = true
        },
        name() {
            this.dialogNameVisible = true
        },
        email() {
            this.dialogEmailVisible = true
        },
        delUser() {
            this.$confirm('您确定要注销账号吗?', '提示', {
                confirmButtonText: '确定',  //确认按钮的文字显示
                type: 'warning',
                center: true, //文字居中显示
            })
                .then(() => {
                    this.$axios.post(this.$url + '/user/deleteLogical/' + this.user.id).then(res => res.data).then(res => {
                        console.log(res)
                        if (res) {
                            this.$message({
                                message: '注销账号成功!',
                                type: 'success'
                            });
                            this.$axios.get(this.$url + '/user/logout').then(res => res.data).then(res => {
                                if (res.code == 200) {
                                    this.$store.commit("resetState")
                                    this.$message({
                                        type: 'success',
                                        message: '退出登录成功'
                                    })
                                    this.$router.push("/login")
                                } else {
                                    this.$message({
                                        message: res.msg,
                                        type: 'error'
                                    });
                                }
                            })
                        } else {
                            this.$message({
                                message: '注销失败!',
                                type: 'error'
                            });
                        }
                    })
                })
                .catch(() => {
                    this.$message({
                        type: 'info',
                        message: '已取消注销账号'
                    })
                })
        },
        logout() {
            //退出登录
            this.$confirm('您确定要退出登录吗?', '提示', {
                confirmButtonText: '确定',  //确认按钮的文字显示
                type: 'warning',
                center: true, //文字居中显示

            })
                .then(() => {
                    this.$axios.get(this.$url + '/user/logout').then(res => res.data).then(res => {
                        // console.log(res)
                        if (res.code == 200) {
                            this.$store.commit("resetState")
                            this.$message({
                                type: 'success',
                                message: '退出登录成功'
                            })
                            this.$router.push("/login")
                        } else {
                            this.$message({
                                message: res.msg,
                                type: 'error'
                            });
                        }
                    });
                })
                .catch(() => {
                    this.$message({
                        type: 'info',
                        message: '已取消退出登录'
                    })
                })
        },
        loadRole() {
            this.$axios.get(this.$url + '/role/list').then(res => res.data).then(res => {
                console.log(res)
                this.roleData = res
            })
        },
        getUserById() {
            this.$axios.get(this.$url + '/user/info/' + this.user.id).then(res => res.data).then(res => {
                console.log(res)
                if (res) {
                    sessionStorage.setItem("LocalUser", JSON.stringify(res.data))
                    this.$store.commit("SETUSER", JSON.stringify(res.data))
                    this.user = res.data
                } else {
                    this.$message({
                        message: res.msg,
                        type: 'error'
                    });
                }
            })
        }
    },
    created() {
        this.init()
        this.getUserById()
        this.loadRole()
    }

6.退出登录后端接口

 @GetMapping("/logout")
    public SaResult logout(){
        if(StpUtil.isLogin()){
            StpUtil.logout();
        }
        return SaResult.ok();
    }

7.修改头像方法

       //注入EventBus.js
      import EventBus from '../../assets/EventBus.js';
      
 modAvatar() {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = 'image/*';
            input.onchange = async () => {
                const file = input.files[0];
                if (file) {
                    // 使用 FormData 构造表单数据
                    let fd = new FormData();
                    fd.append("file", file);  // 修改这行代码
                    fd.append("fileName", file.name);
                    // 发送 POST 请求将表单数据提交到服务器
                    try {
                        const config = {
                            headers: {
                                'Content-Type': 'multipart/form-data'
                            }
                        };
                        await this.$axios.post(this.$url + "/upload/avatar", fd, config)
                            .then(res => {
                                console.log(res);
                                if (res.status === 200) {
                                    this.modUserAvatar(res.data.data);
                                    this.$message({
                                        message: '修改成功!',
                                        type: 'success'
                                    });
                                } else {
                                    this.$message({
                                        message: '未上传成功!',
                                        type: 'error'
                                    });
                                }
                            });
                    } catch (err) {
                        console.error(err);
                        // 提示用户上传失败
                        this.$message.error('修改失败,请重试');
                    }
                } else {
                    // 用户没有选择文件
                    this.$message.error('未选择文件');
                }
            };
            input.click();
        },
        modUserAvatar(avatar) {
            this.$axios.get(this.$url + '/user/modAvatar?id=' + this.user.id + "&avatar=" + avatar).then(res => res.data).then(res => {
                console.log(res)
                if (res.code == 200) {
                    this.getUserById(this.user.id)
                    EventBus.$emit('update-cur-user', JSON.stringify(res.data))
                } else {
                    this.$message({
                        message: res.msg,
                        type: 'error'
                    });
                }
            })
        },

8.修改头像后端接口

@Transactional
  @GetMapping("/modAvatar")
  public Result modAvatar(@RequestParam Integer id, String avatar) {
    User user = userService.getById(id);
    user.setAvatar(avatar);
    userService.updateById(user);
    User usertail = userService.lambdaQuery().eq(User::getId, id).list().get(0);
    Role roleList = roleService.listRolesByUserId(Long.valueOf(usertail.getId()));
    usertail.setRoles(roleList);
    System.out.println(usertail);
    return Result.suc(usertail);
  }

二十一、个人中心功能的实现(下)

1.修改昵称表单

   <el-dialog title="修改昵称" :visible.sync="dialogNameVisible" width="30%" center>
            <el-form :model="modNameForm" label-width="100px" :rules="namerules" ref="modNameForm">
                <el-form-item label="账号昵称">
                    <el-input style="width: 200px" type="text" :placeholder="user.username" size="small"
                        disabled></el-input>
                </el-form-item>
                <el-form-item label="新昵称" prop="username">
                    <el-input style="width: 200px" type="text" v-model="modNameForm.username" autocomplete="off"
                        size="small" @keyup.enter.native="modName"></el-input>
                </el-form-item>
            </el-form>
            <span slot="footer" class="dialog-footer">
                <el-button type="danger" @click="dialogNameVisible = false">取消修改</el-button>
                <el-button type="success" @click="modName">开始修改</el-button>
            </span>
        </el-dialog>

2.昵称表单数据项配置

 modNameForm: {
                username: '',
            },
  namerules: {
                username: [
                    { required: true, message: '请输入账号', trigger: 'blur' },
                    { min: 2, max: 10, message: '2-10个字符', trigger: 'blur' },
                ],
            },

3.修改昵称方法

      modName() {
            this.$refs.modNameForm.validate((valid) => {
                if (valid) {
                    this.$axios.get(this.$url + '/user/modName?id=' + this.user.id + "&username=" + this.modNameForm.username).then(res => res.data).then(res => {
                        console.log(res)
                        if (res.code == 200) {
                            this.$message({
                                message: '修改成功',
                                type: 'success'
                            });
                            this.dialogNameVisible = false
                            this.getUserById(this.user.id)
                            EventBus.$emit('update-cur-user', JSON.stringify(res.data))
                        } else {
                            this.$message({
                                message: res.msg,
                                type: 'error'
                            });
                        }
                    })
                }
                else {
                    this.$message({
                        message: "新账号昵称不符合规范",
                        type: 'warning'
                    });
                    return false;
                }
            })
        },

4.修改昵称后端接口

 @Transactional
  @GetMapping("/modName")
  public Result modName(@RequestParam Integer id, String username) {
    User user = userService.getById(id);
    user.setUsername(username);
    List list =
        userService.lambdaQuery().eq(User::getUsername, username).ne(User::getId, id).list();
    if (list.size() > 0) return Result.fail("账号已被注册,请换一个名字");
    else {
      userService.updateById(user);
      User usertail = userService.lambdaQuery().eq(User::getUsername, username).list().get(0);
      Role roleList = roleService.listRolesByUserId(Long.valueOf(usertail.getId()));
      usertail.setRoles(roleList);
      return Result.suc(usertail);
    }
  }

5.修改密码表单

 <el-dialog title="修改密码" :visible.sync="dialogPwdVisible" width="30%" center>
            <el-form :model="modPwdForm" label-width="100px" :rules="rules" ref="modPwdForm">
                <el-form-item label="原密码" prop="password">
                    <el-input style="width: 200px" type="text" v-model="modPwdForm.password" show-password
                        autocomplete="off" size="small"></el-input>
                </el-form-item>
                <el-form-item label="新密码" prop="newpassword">
                    <el-input style="width: 200px" type="password" v-model="modPwdForm.newpassword" show-password
                        autocomplete="off" size="small"></el-input>
                </el-form-item>
                <el-form-item label="重复密码" prop="rpassword">
                    <el-input style="width: 200px" type="password" v-model="modPwdForm.rpassword" show-password
                        autocomplete="off" size="small" @keyup.enter.native="modPwd"></el-input>
                </el-form-item>
            </el-form>
            <span slot="footer" class="dialog-footer">
                <el-button type="danger" @click="dialogPwdVisible = false">取消修改</el-button>
                <el-button type="success" @click="modPwd">开始修改</el-button>
            </span>
        </el-dialog>

6.密码表单数据项配置

var validatePass = (rule, value, callback) => {
            if (value === '') {
                callback(new Error('请再次输入密码'))
            } else if (value !== this.modPwdForm.newpassword) {
                callback(new Error('两次输入密码不一致!'))
            } else {
                callback()
            }
        }
        
   modPwdForm: {
                password: '',
                newpassword: '',
                rpassword: '',
            },
            rules: {
                password: [
                    { required: true, message: '请输入原密码', trigger: 'blur' },
                    { min: 6, max: 16, message: '6-16个字符', trigger: 'blur' },
                ],
                newpassword: [
                    { required: true, message: '请输入新密码', trigger: 'blur' },
                    { min: 6, max: 16, message: '6-16个字符', trigger: 'blur' },
                ],
                rpassword: [
                    { required: true, validator: validatePass, trigger: 'blur' },
                    { min: 6, max: 16, message: '6-16个字符', trigger: 'blur' },
                ],
            },

7.修改密码方法

   modPwd() {
            this.$refs.modPwdForm.validate((valid) => {
                if (valid) {
                    this.$axios.get(this.$url + '/user/modPwd?id=' + this.user.id + "&password=" + this.modNameForm.newpassword).then(res => res.data).then(res => {
                        console.log(res)
                        if (res.code == 200) {
                            this.$axios.get(this.$url + '/user/logout').then(res => res.data).then(res => {
                                if (res.code == 200) {
                                    this.$store.commit("resetState")
                                    this.$message({
                                        message: '修改成功',
                                        type: 'success'
                                    });
                                    this.dialogPwdVisible = false,
                                        this.$message({
                                            type: 'success',
                                            message: '退出登录成功'
                                        })
                                    this.$router.push("/login")
                                } else {
                                    this.$message({
                                        message: res.msg,
                                        type: 'error'
                                    });
                                }
                            });
                        } else {
                            this.$message({
                                message: "修改失败",
                                type: 'error'
                            });
                        }
                    })
                } else {
                    this.$message({
                        message: "请检查密码",
                        type: 'warning'
                    });
                    return false;
                }
            })
        },

后端接口就自己思考一下喽

8.修改邮箱表单

       <el-dialog title="修改邮箱" :visible.sync="dialogEmailVisible" width="30%" center>
            <el-form :model="emailForm" :rules="emailRules" ref="emailForm" :validate-on-rule-change="false">
                <el-row style="margin-top:20px;height:40px;">
                    <el-col :span="18" :offset="3">
                        <el-form-item>
                            <el-input v-model="emailForm.email" placeholder="请输入邮箱" @change="emailchange"></el-input>
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row style="margin-top:30px;height:45px;">
                    <el-col :span="18" :offset="3" style="height:40px;">
                        <el-form-item prop="code">
                            <el-row>
                                <el-col :span="16">
                                    <el-input v-model.lazy.trim="emailForm.code" placeholder="请输入验证码"
                                        @keyup.enter.native="modEmail"></el-input>
                                </el-col>
                                <el-col :span="4">
                                    <el-button class="code-btn" size="medium" :disabled="sendCodeDisabled"
                                        @click.stop.prevent.native="handleGetCode">{{ sendCodeText
                                        }}</el-button>
                                </el-col>
                            </el-row>
                        </el-form-item>
                    </el-col>
                </el-row>
            </el-form>
            <span slot="footer" class="dialog-footer">
                <el-button type="danger" @click="dialogEmailVisible = false">取消修改</el-button>
                <el-button type="success" @click="modEmail">修改邮箱</el-button>
            </span>
        </el-dialog>

9.邮箱表单数据项配置

    var validateCode = (rule, value, callback) => {
            if (value === '') {
                callback(new Error('请再次输入验证码'))
            } else if (value !== this.loginForm.rcode) {
                callback(new Error('验证码不正确!'))
            } else {
                callback()
            }
        }
        
         sendCodeText: '获取验证码',
            sendCodeDisabled: false,
            countDown: 60,
             emailForm: {
                email: "",
                code: "",
                rcode: ''
            },
            emailRules: {
                email: [{ required: true, message: '请输入邮箱', trigger: 'blur' },
                { pattern: /^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(.[a-zA-Z0-9_-]+)+$/, message: '邮箱格式不正确' }
                ],
                code: [{ required: true, validator: validateCode, trigger: 'blur' },
                { min: 6, max: 6, message: '请输入6位数字邮箱验证码', trigger: 'blur' },
                ],
            },

10.修改邮箱方法

      emailchange() {
            clearInterval(this.timer);
            this.countDown = 60
            this.sendCodeDisabled = false
            this.sendCodeText = '获取验证码'
            this.emailForm.rcode = ''
            this.emailForm.code = ''
        },
        handleGetCode() {
            if (!this.validateEmail(this.emailForm.email)) {
                this.$message({
                    message: '请输入正确的邮箱地址',
                    type: 'error'
                })
                return
            }
            this.sendCodeDisabled = true
            this.sendCodeText = `${this.countDown}s 后重新获取`
            this.timer = setInterval(() => {
                if (this.countDown === 1) {
                    clearInterval(this.timer)
                    this.countDown = 60
                    this.sendCodeDisabled = false
                    this.sendCodeText = '获取验证码'
                    return
                }
                this.countDown -= 1
                this.sendCodeText = `${this.countDown}s 后重新获取`
            }, 1000)
            this.$axios.get(this.$url + '/email/bind?email=' + this.emailForm.email).then(res => res.data).then(res => {
                console.log(res)
                if (res.code == 200) {

                    this.$message({
                        message: '邮箱验证码发送成功!',
                        type: 'success'
                    });
                    this.emailForm.rcode = res.data
                } else {
                    this.$message({
                        message: res.msg,
                        type: 'error'
                    });
                    this.sendCodeDisabled = false
                    this.emailForm.rcode = ''
                    this.sendCodeText = '获取验证码'

                }
            })
        },
        validateEmail(email) {
            // 正则表达式匹配邮箱格式
            const emailRegex = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/
            return emailRegex.test(email)
        },

后端接口自行思考

二十二、系统首页的设计与基本实现(上)

1.系统首页布局

三行布局

第一行4列,为首页仪表盘

第二行2列,为系统介绍内容

第三行2列,为两张可视化图表

	<div style="text-align: center;background-color: #f1f1f3;height: 100%;padding: 0px;margin: 0px;">
		<el-row class="card-row">
			<el-col :span="6">
			</el-col>
			<el-col :span="6">
			</el-col>
			<el-col :span="6">
			</el-col>
			<el-col :span="6">
			</el-col>
		</el-row>
		<el-row class="card-row">
			<el-col :span="16">
			</el-col>
			<el-col :span="8">
			</el-col>
		</el-row>
		<el-row class="card-row">
			<el-col :span="12">
			</el-col>
			<el-col :span="12">
			</el-col>
		</el-row>
	</div>

2.Card组件样式

参上;自定义组件样式同参上

3.自定义仪表盘代码

用户数量:
<el-card shadow="hover" class="box-card centered-card red">
					<div>
						<i class="el-icon-s-custom"></i>
						用户数量:
						<el-tag type="success">
							{{ userCount }}
						</el-tag>
					</div>
				</el-card>
				
角色数量:
<el-card shadow="hover" class="box-card centered-card green">
					<div>
						<i class="el-icon-s-promotion"></i>
						角色数量:
						<el-tag disable-transitions>{{ roleCount }}</el-tag>
					</div>
				</el-card>

菜单数量:
<el-card shadow="hover" class="box-card centered-card  purple">
					<div>
						<i class="el-icon-s-promotion"></i>
						菜单数量
						<el-tag disable-transitions>
							{{ MenuCount }}
						</el-tag>
					</div>
				</el-card>

联系开发者:
<el-card shadow="hover" class="box-card centered-card grey">
					<el-button @click="showDialog = true">点击联系开发者</el-button>
				</el-card>

4.仪表盘数据项配置

data() {
		return {
			userCount: 0,
			roleCount: 0,
			MenuCount: 0,
			group1show: false,
			group2show: false,
			showDialog: false,
		}
	},

5.仪表盘数据获取函数

methods: {
		getUserRole() {
			this.$axios.get(this.$url + '/role/pie1').then(res => res.data).then(res => {
				this.drawChartPie1(res)
			})
		},
		getRoleMenu() {
			this.$axios.get(this.$url + '/role/pie2').then(res => res.data).then(res => {
				console.log(res)
				this.drawChartPie2(res)
			})
		},
		getUserCount() {
			this.$axios.get(this.$url + '/user/count').then(res => res.data).then(res => {
				this.userCount = res
			})
		},
	},
	created() {
		this.getMenuCount()
		this.getRoleCount()
		this.getRoleMenu()
	}

6.仪表盘后端接口

用户:
@GetMapping("/count")
  public Integer count() {
    return userService.list().size();
  }
角色:
  @GetMapping("/count")
  public Integer count() {
    return roleService.list().size();
  }
菜单:
   @GetMapping("/count")
    public Integer count() {
        System.out.println(menuService.list().size()); // 打印菜单列表的大小
        return menuService.list().size(); // 返回菜单列表的大小
    }

7.联系开发者对话框

	<el-dialog :visible.sync="showDialog" width="30%" center>
			<el-image :src="'http://localhost:8090/upload/file/qq'"></el-image>
			<div slot="footer"></div>
		</el-dialog>
		<el-dialog :visible.sync="group1show" width="30%" center>
			<el-image :src="'http://localhost:8090/upload/file/group1'"></el-image>
			<div slot="footer"></div>
		</el-dialog>
		<el-dialog :visible.sync="group2show" width="30%" center>
			<el-image :src="'http://localhost:8090/upload/file/group2'"></el-image>
			<div slot="footer"></div>
		</el-dialog>

二十三、系统首页的设计与基本实现(下)

1.系统技术架构介绍

	<el-card shadow="hover" class="box-card centered-card gold">
					<h2>基于SpringBoot+Vue前后端分离通用权限管理后台系统PermissionAdmin</h2>
					<h3>技术架构:总体采用前后端分离架构</h3>
					<h4>后端:</h4>
					<el-row style="text-align:left">
						<el-col :span="24">主要开发框架:
							<el-tag type="primary" style="margin:2px">SpringBoot2x</el-tag>
							<el-tag type="success" style="margin:2px">MyBatisPlus3.4.1</el-tag>
						</el-col>
					</el-row>
					<el-row style="text-align:left">
						<el-col :span="24">开发语言:
							<el-tag type="primary">Java</el-tag>
						</el-col>
					</el-row>
					<el-row style="text-align:left">
						开发技术:
						<el-col :span="24">
							<el-tag type="primary" style="margin:2px">java技术</el-tag>
							<el-tag type="success" style="margin:2px">SpringBoot技术</el-tag>
							<el-tag type="warning" style="margin:2px">MyBatisPlus代码生成器</el-tag>
							<el-tag type="danger" style="margin:2px">MyBatisPlus分页技术</el-tag>
							<el-tag type="success" style="margin:2px">MyBatisPlus查询技术</el-tag>
							<el-tag type="primary" style="margin:2px">Lombok技术</el-tag>
							<el-tag type="success" style="margin:2px">文件上传技术</el-tag>
							<el-tag type="warning" style="margin:2px">screw数据库文档生成技术</el-tag>
							<el-tag type="danger" style="margin:2px">kaptcha技术</el-tag>
							<el-tag type="success" style="margin:2px">mail邮箱验证码技术</el-tag>
							<el-tag type="primary" style="margin:2px">Sa-token权限技术</el-tag>
						</el-col>
					</el-row>
					<h4>数据库:MySQL5.7</h4>
					<h4>前端:</h4>
					<el-row style="text-align:left">
						<el-col :span="24">主要开发框架:
							<el-tag type="primary" style="margin:2px">Node.js16</el-tag>
							<el-tag type="success" style="margin:2px">Vue2x</el-tag>
						</el-col>
					</el-row>
					<el-row style="text-align:left">
						<el-col :span="24">开发语言:
							<el-tag type="primary">vue2</el-tag>
						</el-col>
					</el-row>
					<el-row style="text-align:left"> 开发技术:
						<el-col :span="24">
							<el-tag type="primary" style="margin:2px">vue技术</el-tag>
							<el-tag type="success" style="margin:2px">vue-router技术</el-tag>
							<el-tag type="warning" style="margin:2px">axios技术</el-tag>
							<el-tag type="danger" style="margin:2px">vuex技术</el-tag>
							<el-tag type="success" style="margin:2px">路由守卫技术</el-tag>
							<el-tag type="primary" style="margin:2px">element-ui技术</el-tag>
							<el-tag type="success" style="margin:2px">echarts技术</el-tag>
							<el-tag type="warning" style="margin:2px">邮箱验证码技术</el-tag>
						</el-col>
					</el-row>
				</el-card>

2.系统作者介绍

				<el-card shadow="hover" class="box-card centered-card orange">
					<div>
						<p>作者:龙毅</p>
						<p>QQ群</p>
						<el-row>
							<el-col :span="24" >
								龙毅前后端技术交流1群 <el-link type="primary" @click.native="group1show = true">398414828</el-link>
								<el-tag type="success" @click.native="group1show = true">前后端分离系统技术交流群兼哔站项目资料群</el-tag>
							</el-col>
							<el-col :span="24" @click.native="group2show = true">
								龙毅前后端技术交流2群<el-link type="primary"  @click.native="group2show = true"> 930942770</el-link>
								<el-tag type="success"  @click.native="group2show = true">通用权限管理系统PermissionAdmin群</el-tag>
							</el-col>
							<el-col :span="24" @click.native="group2show = true">
								龙毅前后端技术交流3群<el-link type="primary"> 477183730</el-link>
								<el-tag type="success" >前后端分离系统技术交流群兼资料分享群</el-tag>
							</el-col>
						</el-row>
						<p>QQ号:群主</p>
						<p>加群时请备注【已三连关注 + 自己B站名字】</p>
						<p>“未三连 + 关注的”【不通过】</p>
						<p>“未三连 + 关注的”【不通过】</p>
						<p>“未三连 + 关注的”【不通过】</p>
						<p>哔站:<el-link type="primary" href="https://b23.tv/F7yFuQK" target="_blank">龙毅代码的个人空间</el-link></p>
						<p>GitHub:<el-link type="primary" href="https://github.com/shengguanglongyi/PermissionAdmin.git"
								target="_blank">
								聖光龙毅的GitHub
							</el-link></p>
					</div>
				</el-card>

二十四、系统首页可视化图表的实现

1.准备可视化容器

<el-card shadow="hover" class="box-card centered-card blue">
					<div id="container1" style="height:320px;width: 500px;"></div>
					<div class="text"> 用户角色比例</div>
				</el-card>

<el-card shadow="hover" class="box-card centered-card pink">
					<div id="container2" style="height:320px;width: 500px;"></div>
					<div class="text"> 角色权限比例</div>
				</el-card>

2.引入Echarts

import * as echarts from 'echarts'

3.编写初始化函数

getUserRole() {
			this.$axios.get(this.$url + '/role/pie1').then(res => res.data).then(res => {
				this.drawChartPie1(res)
			})
		},
		getRoleMenu() {
			this.$axios.get(this.$url + '/role/pie2').then(res => res.data).then(res => {
				console.log(res)
				this.drawChartPie2(res)
			})
		},
		

		this.getRoleMenu()
		this.getUserRole()

4.编写后端接口

1.测试sql
 SELECT r.name , COUNT(ur.user_id) AS value
        FROM role r
            LEFT JOIN user_role ur ON r.id = ur.role_id
        GROUP BY r.id
  SELECT r.name , COUNT(ur.menu_id) AS value
        FROM role r
            LEFT JOIN role_menu ur ON r.id = ur.role_id
        GROUP BY r.id
2.编写实体类
import lombok.Data;

@Data
public class RoleDto {
    private  String name;
    private  Integer value;
}
3.编写Controller层
 @GetMapping("/pie1")
  public List<RoleDto> pie1() {
    List<RoleDto> roleDtoList = roleService.pie1();
    return roleDtoList;
  }

  @GetMapping("/pie2")
  public List<RoleDto> pie2() {
    List<RoleDto> roleDtoList = roleService.pie2();
    return roleDtoList;
  }
4.编写service层,serverimpl层,mapper层
5.编写xml代码
    <select id="pie1" resultType="com.longyi.springbootpermissionadmin.entity.Dto.RoleDto">
        SELECT r.name , COUNT(ur.user_id) AS value
        FROM role r
            LEFT JOIN user_role ur ON r.id = ur.role_id
        GROUP BY r.id
    </select>
    <select id="pie2" resultType="com.longyi.springbootpermissionadmin.entity.Dto.RoleDto">
        SELECT r.name , COUNT(ur.menu_id) AS value
        FROM role r
            LEFT JOIN role_menu ur ON r.id = ur.role_id
        GROUP BY r.id
    </select>

5.编写前端实例化代码

drawChartPie1(data) {
            var chartDom = document.getElementById('container1');
            // 判断是否已经初始化
            if (echarts.getInstanceByDom(chartDom)) {
                // 已初始化,销毁实例
                echarts.getInstanceByDom(chartDom).dispose();
            }
            var myChart = echarts.init(chartDom);
            var option = {
                title: {
                    text: '角色用户数量',
                    subtext: '角色用户比例',
                    left: 'center'
                },
                tooltip: {
                    trigger: 'item'
                },
                legend: {
                    orient: 'vertical',
                    left: 'left'
                },
                series: [
                    {
                        name: '用户数',
                        type: 'pie',
                        radius: '50%',
                        data:data,
                        emphasis: {
                            itemStyle: {
                                shadowBlur: 10,
                                shadowOffsetX: 0,
                                shadowColor: 'rgba(0, 0, 0, 0.5)'
                            }
                        }
                    }
                ]
            };
            myChart.setOption(option);
        },
		drawChartPie2(data) {
            var chartDom = document.getElementById('container2');
            // 判断是否已经初始化
            if (echarts.getInstanceByDom(chartDom)) {
                // 已初始化,销毁实例
                echarts.getInstanceByDom(chartDom).dispose();
            }
            var myChart = echarts.init(chartDom);
            var option = {
                title: {
                    text: '角色权限数量',
                    subtext: '角色权限比例',
                    left: 'center'
                },
                tooltip: {
                    trigger: 'item'
                },
                legend: {
                    orient: 'vertical',
                    left: 'left'
                },
                series: [
                    {
                        name: '权限数',
                        type: 'pie',
                        radius: '50%',
                        data:data,
                        emphasis: {
                            itemStyle: {
                                shadowBlur: 10,
                                shadowOffsetX: 0,
                                shadowColor: 'rgba(0, 0, 0, 0.5)'
                            }
                        }
                    }
                ]
            };
            myChart.setOption(option);
        },
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值