springboot-vue-2022笔记

springboot-vue-2022

魔改内容

  • 修改部分字段
  • 添加创建时间更新时间
  • 使用乐观锁和逻辑删除
  • 用vue3+elementplus重写
  • 将用户与角色的关系抽离为表结构
  • 引入图标库
  • 引入中国地址,并拆分地区和地址
  • 修改返回接口封装
  • 注册添加邮箱验证码
  • 登录添加人机验证码

源码地址

前端环境

尝试Vite2+Vue3+Router+Pinia+Vue-tsc+Elementplus+Volar的新体系

vite创建项目并运行

npm init vite@latest
√ Project name: ... vue
√ Select a framework: » vue
√ Select a variant: » vue-ts

Scaffolding project in D:\Learning_materials\study self\Product\springboot-vue-pro\doc\vue...

Done. Now run:

cd vue
npm install
npm run dev

引入element plus

element plus官网的按需引入可能会有问题,切换版本解决会有组件错误,建议直接完整引入

  • 安装elementplus

    npm install element-plus --save
    
  • 修改main.ts

    import { createApp } from "vue";
    import App from "@/App.vue";
    import ElementPlus from 'element-plus'
    import 'element-plus/dist/index.css'
    import zhCn from "element-plus/es/locale/lang/zh-cn";
    
    createApp(App)
      .use(router)
      .use(ElementPlus, { size: "mini", locale: zhCn })
      .mount("#app");
    

引入element plus icon

  • 安装@element-plus/icons-vue

    npm install @element-plus/icons-vue
    
  • 按需导入

     import { Edit } from '@element-plus/icons-vue';
    

引入vue-router

src的@别名被移除,配置别名

  • 安装vue-router

    npm install vue-router@next --save	
    
  • 修改main.ts

    import { createApp } from "vue";
    import App from "@/App.vue";
    import router from "@/router/index";
    import ElementPlus from "element-plus";
    import "element-plus/dist/index.css";
    import zhCn from "element-plus/es/locale/lang/zh-cn";
    import "@/assets/global.css";
    
    createApp(App)
      .use(router)
      .use(ElementPlus, { size: "mini", locale: zhCn })
      .mount("#app");
    
  • src创建router文件夹,新建index.ts

    import {createRouter, RouteRecordRaw, Router, createWebHistory} from 'vue-router'
    
    const routes: Array<RouteRecordRaw> = [
        {
            path: '/',
            name: 'Home',
            component: () => import('@/views/Home.vue'),
            meta: {
                title: '首页'
            }
        }
    ]
    
    const router: Router = createRouter({
        history: createWebHistory(),
        routes
    })
    
    export default router
    
  • 配置别名

    • 修改vite.config.ts

      import { defineConfig } from "vite";
      import vue from "@vitejs/plugin-vue";
      
      export default defineConfig({
        plugins: [
          vue(),
          
        ],
        resolve: {
          alias: {
            "@": "/src/",
          },
        },
      });
      
    • 修改tsconfig.json

      {
        "compilerOptions": {
          "target": "esnext",
          "useDefineForClassFields": true,
          "module": "esnext",
          "moduleResolution": "node",
          "strict": true,
          "jsx": "preserve",
          "sourceMap": true,
          "resolveJsonModule": true,
          "isolatedModules": true,
          "esModuleInterop": true,
          "lib": [
            "esnext",
            "dom"
          ],
          "baseUrl": "./",
          "paths": {
            "@/*": ["./src/*"]
          }
        },
        "include": [
          "src/**/*.ts",
          "src/**/*.d.ts",
          "src/**/*.tsx",
          "src/**/*.vue"
        ],
        "references": [
          {
            "path": "./tsconfig.node.json"
          }
        ]
      }
      

引入pinia

  • 安装pinia

    npm install pinia@next
    
  • 修改main.ts

    import { createApp } from "vue";
    import App from "@/App.vue";
    import router from "@/router/index";
    import ElementPlus from "element-plus";
    import "element-plus/dist/index.css";
    import zhCn from "element-plus/es/locale/lang/zh-cn";
    import { createPinia } from "pinia";
    import "@/assets/global.css";
    
    createApp(App)
      .use(router)
      .use(ElementPlus, { size: "mini", locale: zhCn })
      .use(createPinia())
      .mount("#app");
    

引入axios

  • 安装axios

    npm i axios -S
    
  • 封装axios

    import axios from 'axios'
    
    const request = axios.create({
        baseURL: '/api',
        timeout: 5000,
        headers: {
            'Content-Type': 'application/json;charset=UTF-8;',
        }
    })
    
    // request 拦截器
    // 可以自请求发送前对请求做一些处理
    // 比如统一加token,对请求参数统一加密
    request.interceptors.request. use(config => {
        // config.headers['token'] = user.token;  // 设置请求头
        return config
    }, error => {
        return Promise.reject(error)
    });
    
    // response 拦截器
    // 可以在接口响应后统一处理结果
    request.interceptors.response.use(
        response => {
            let res = response.data;
            // 如果是返回的文件
            if (response.config.responseType === 'blob') {
                return res
            }
            // 兼容服务端返回的字符串数据
            if (typeof res === 'string') {
                res = res ? JSON.parse(res) : res
            }
            return res;
        },
        error => {
            console.log('err' + error) // for debug
            return Promise.reject(error)
        }
    )
    
    
    export default request
    

引入Echarts

npm install echarts --save

引入e-icon-picker

替换后期自建的图标库

  • 安装e-icon-picker-beta.8

    npm install e-icon-picker@next -S
    
  • 添加声明文件

    src下新建type文件夹,创建index.d.ts文件

    declare module 'e-icon-picker';
    

引入element-china-area-data

引入中国地区数据,替换地址

  • 安装element-china-area-data

    npm install element-china-area-data -S
    
  • 添加声明文件

    declare module 'element-china-area-data';
    

引入wangEditor

  • 安装wangEditor

    npm i wangeditor --save
    
  • scss

    npm install node-sass --save-dev
    

引入vue3-puzzle-vcode

  • 安装

    npm install vue3-puzzle-vcode --save
    

前端跨域

前后端都可以配置跨域

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      "@": "/src/", //别名
    },
  },
  server: {
    open: true, //启动项目自动弹出浏览器
    port: 9090, //启动端口
    proxy: {
      "/api": {
        target: "http://localhost:8080", //服务端地址
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ""),
      },
    },
  },
});

目录解析

vue
├── .vscode
│   └── extensions.json
├── public
│   └── favicon.ico
├── src
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   ├── App.vue
│   ├── env.d.ts //环境变量
│   └── main.ts //项目入口文件
├── README.md
├── index.html //首页
├── package-lock.json //所有模块的具体来源和版本号以及其他的信息
├── package.json //依赖模块的版本信息
├── tsconfig.json //表明该目录是 TypeScript 项目的根目录
├── tsconfig.node.json //指定了编译项目所需的根文件和编译器选项
└── vite.config.ts //vite配置

前端主体代码

注意v-slot和submenu的改变和setup语法

Home.vue

<script setup lang="ts">

import { ref, reactive } from 'vue'
import { Edit, Remove, Download, Upload, CirclePlus, ArrowDownBold, Message, Menu, Setting, Fold, Expand, Search, Position } from '@element-plus/icons-vue';
const item = {
    date: '2016-05-02',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
}
let tableData = ref(Array.from({ length: 7 }).fill(item))
let isCollapse = ref<boolean>(false)
let sideWidth = ref<number>(200)
const headerBg = ref('headerBg')
const collapse = () => {
    if (!isCollapse.value) {
        isCollapse.value = true
        sideWidth.value = 64
    } else {
        isCollapse.value = false
        sideWidth.value = 200
    }
}
</script>

<template>
    <el-container style="min-height: 100vh">
        <el-aside :width="sideWidth + 'px'" style="box-shadow: 2px 0 6px rgb(0 21 41 / 35%);  ">
            <el-menu
                :default-openeds="['1', '3']"
                style="min-height: 100%; overflow-x: hidden"
                background-color="rgb(48, 65, 86)"
                text-color="#fff"
                active-text-color="#ffd04b"
                :collapse-transition="false"
                :collapse="isCollapse"
            >
                <div style="height: 60px; line-height: 60px; text-align: center">
                    <img
                        src="../assets/logo.png"
                        style="width: 20px; position: relative; top: 5px; right: 5px"
                    />
                    <b style="color: white" v-show="!isCollapse">后台管理系统</b>
                </div>
                <el-sub-menu index="1">
                    <template #title>
                        <message style="width: 1em; height: 1em; margin-right: 8px" />
                        <span>消息</span>
                    </template>
                </el-sub-menu>
                <el-sub-menu index="2">
                    <template #title>
                        <Menu style="width: 1em; height: 1em; margin-right: 8px" />
                        <span>菜单</span>
                    </template>
                </el-sub-menu>
                <el-sub-menu index="3">
                    <template #title>
                        <setting style="width: 1em; height: 1em; margin-right: 8px" />
                        <span>设置</span>
                    </template>
                </el-sub-menu>
            </el-menu>
        </el-aside>

        <el-container style="postion: relative">
            <el-header
                style="font-size: 12px; border-bottom: 1px solid #ccc; line-height: 60px; display: flex"
            >
                <div style="flex: 1; font-size: 20px; backgroud-color: rgb(48, 65, 86)">
                    <el-icon @click="collapse()">
                        <fold v-show="!isCollapse" />
                        <expand v-show="isCollapse" />
                    </el-icon>
                </div>
                <div style="width: 100px">
                    <el-dropdown>
                        <span class="el-dropdown-link">
                            <el-avatar :size="30" style="position: relative; top: 15px"></el-avatar>
                        </span>
                        <template #dropdown>
                            <el-dropdown-menu>
                                <el-dropdown-item>个人信息</el-dropdown-item>
                                <el-dropdown-item>修改密码</el-dropdown-item>
                                <el-dropdown-item>退出系统</el-dropdown-item>
                            </el-dropdown-menu>
                        </template>
                    </el-dropdown>
                </div>
            </el-header>

            <el-main>
                <div style="margin-bottom: 30px">
                    <el-breadcrumb separator="/">
                        <el-breadcrumb-item :to="'/home'">首页</el-breadcrumb-item>
                        <el-breadcrumb-item>用户管理</el-breadcrumb-item>
                    </el-breadcrumb>
                </div>
                <div style="margin: 10px 0">
                    <el-input style="width: 200px" placeholder="请输入名称" :suffix-icon="Search"></el-input>
                    <el-input
                        style="width: 200px"
                        placeholder="请输入邮箱"
                        :suffix-icon="Message"
                        class="ml-5"
                    ></el-input>
                    <el-input
                        style="width: 200px"
                        placeholder="请输入地址"
                        :suffix-icon="Position"
                        class="ml-5"
                    ></el-input>
                    <el-button class="ml-5" type="primary">搜索</el-button>
                </div>

                <div style="margin: 10px 0">
                    <el-button type="primary">
                        新增
                        <circle-plus style="width: 1em; height: 1em; margin-right: 8px" />
                    </el-button>
                    <el-button type="danger">
                        批量删除
                        <remove style="width: 1em; height: 1em; margin-right: 8px" />
                    </el-button>
                    <el-button type="primary">
                        导入
                        <upload style="width: 1em; height: 1em; margin-right: 8px" />
                    </el-button>
                    <el-button type="primary">
                        导出
                        <download style="width: 1em; height: 1em; margin-right: 8px" />
                    </el-button>
                </div>

                <el-table :data="tableData" border stripe :header-cell-class-name="headerBg">
                    <el-table-column prop="date" label="日期" width="140"></el-table-column>
                    <el-table-column prop="name" label="姓名" width="120"></el-table-column>
                    <el-table-column prop="address" label="地址"></el-table-column>
                    <el-table-column label="操作" width="300" align="center">
                        <template v-slot="scope">
                            <el-button type="success">
                                编辑
                                <edit style="width: 1em; height: 1em; margin-right: 8px" />
                            </el-button>
                            <el-button type="danger">
                                删除
                                <remove style="width: 1em; height: 1em; margin-right: 8px" />
                            </el-button>
                        </template>
                    </el-table-column>
                </el-table>
                <div style="padding: 10px 0">
                    <el-pagination
                        :page-sizes="[5, 10, 15, 20]"
                        :page-size="10"
                        layout="total, sizes, prev, pager, next, jumper"
                        :total="400"
                    ></el-pagination>
                </div>
            </el-main>
            <el-footer height="5px">
                <div class="footer">&copy; 献给最爱的青哥哥</div>
            </el-footer>
        </el-container>
    </el-container>
</template>



<style scoped>
.headerBg {
    background: #eee !important;
}
.footer {
    /* display: flex; */
    /* justify-content: center; */
    /* align-items: center; */
    margin: 0px auto;
    /* right: 0px; */
    left: 50%;
    position: absolute;
    bottom: 0;
}
</style>

后端环境

Maven依赖

 		<!--poi-->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>5.2.2</version>
        </dependency>
        <!--velocity-->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.3</version>
        </dependency>
        <!--mybatisPlusGenerator-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!--knife4j-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.0.M2</version>
        </dependency>
        <!--mybatisPlus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!--springWeb-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

基础配置

spring:
  profiles:
    active: dev
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot_vite?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2b8
    username: root
    password: root
    type: com.zaxxer.hikari.HikariDataSource #Spring Boot 2.0 默认,无需配置
    hikari:
      maximum-pool-size: 15 #最大连接数
      minimum-idle: 5 #最小空闲连接
      connection-timeout: 60000 #连接超时时间(毫秒)
      idle-timeout: 600000 #连接在连接池中空闲的最长时间
      max-lifetime: 3000000 #连接最大存活时间
      connection-test-query: select 1 #连接测试查询
      auto-commit: true #自动提交行为
      pool-name: HikariCPDataSource
server:
  port: 8080

后端跨域

@Configuration
public class CorsConfig {
    /**
     * 当前跨域请求最大有效时长。这里默认1天
     */
    private static final long MAX_AGE = 24 * 60 * 60;

    /**
     * 后端跨域
     *
     * @return CorsFilter
     * @author 石一歌
     * @date 2022/4/10 15:04
     */
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 1 设置访问源地址
        corsConfiguration.addAllowedOrigin("http://localhost:8080");
        // 2 设置访问源请求头
        corsConfiguration.addAllowedHeader("*");
        // 3 设置访问源请求方法
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setMaxAge(MAX_AGE);
        // 4 对接口配置跨域设置
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(source);
    }
}

结果集包装

@Getter
@AllArgsConstructor
public enum ConstantEnum {
    /**
     * 200
     *
     * @date 2022/4/10 23:16
     */
    CODE_200(1, "请求成功", 200),
    /**
     * 401
     *
     * @date 2022/4/10 23:16
     */
    CODE_401(2, "权限不足", 401),
    /**
     * 400
     *
     * @date 2022/4/10 23:16
     */
    CODE_400(3, "参数错误", 400),
    /**
     * 500
     *
     * @date 2022/4/10 23:16
     */
    CODE_500(4, "系统错误", 500),
    /**
     * 600
     *
     * @date 2022/4/10 23:16
     */
    CODE_600(5, "其他业务异常", 600);
    private final Integer id;
    private final String name;
    private final Integer status;
}
@Builder
@Data
public class Result<T> {
    /**
     * 状态码
     */
    private final int code;
    /**
     * 消息
     */
    private final String message;
    /**
     * 响应时间
     */
    private final String responseTime = DateUtil.now();
    /**
     * 结果
     */
    private T result;

    /**
     * 无返回数据请求成功
     *
     * @return Result
     * @author 石一歌
     * @date 2022/4/10 17:26
     */
    public static Result<Object> success() {
        return Result.builder().code(ConstantEnum.CODE_200.getStatus()).message(ConstantEnum.CODE_200.getName()).build();
    }

    /**
     * 带返回数据请求成功
     *
     * @param data 数据
     * @return Result<Object>
     * @author 石一歌
     * @date 2022/4/10 17:34
     */
    public static <T> Result<Object> success(T data) {
        return Result.builder().code(ConstantEnum.CODE_200.getStatus()).message(ConstantEnum.CODE_200.getName()).result(data).build();
    }

    /**
     * 自定义请求失败
     *
     * @param code    状态码
     * @param message 信息
     * @return Result<Object>
     * @author 石一歌
     * @date 2022/4/10 17:34
     */
    public static Result<Object> error(Integer code, String message) {
        return Result.builder().code(code).message(message).build();
    }

    /**
     * 请求失败
     *
     * @return Result<Object>
     * @author 石一歌
     * @date 2022/4/10 23:20
     */
    public static Result<Object> error() {
        return Result.builder().code(ConstantEnum.CODE_500.getStatus()).message(ConstantEnum.CODE_500.getName()).build();
    }
}

引入knife4j

引入knife4j来代替swagger,实体类和控制层类的代码注解后期完成

http://localhost:8080/doc.html

新版需要配置 mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER

spring:
  profiles:
    active: dev
  mvc:
    pathmatch:
      matching-strategy: ANT_PATH_MATCHER
@Configuration
@EnableOpenApi
public class Knife4jConfig {
    /**
     * Knife4j配置
     *
     * @param environment 环境参数
     * @return Docket
     * @author 石一歌
     * @date 2022/4/11 0:39
     */
    @Bean(value = "SpringBoot-Vue-2022-Api")
    public Docket docket(Environment environment) {
        return new Docket(DocumentationType.OAS_30)
                .apiInfo(new ApiInfoBuilder()
                        .title("接口文档列表")
                        .description("接口文档")
                        .termsOfServiceUrl("http://www.springboot.vue.com")
                        .contact(new Contact("石一歌", "https://www.cnblogs.com/faetbwac/", "1456923076@qq.com"))
                        .version("1.0")
                        .license("Apache 2.0 许可")
                        .licenseUrl("许可链接").build())
                // 根据配置文件选择是否开启
                .enable(environment.acceptsProfiles(Profiles.of("dev", "test")))
                .groupName("1.0版本")
                .select()
                // 这里指定Controller扫描包路径
                .apis(RequestHandlerSelectors.basePackage("com.nuc.controller"))
                .paths(PathSelectors.any())
                .build();
    }
}

引入mybatis plus

分别配置分页、乐观锁、逻辑删除、自动填充创建时间和更新时间

注意:

  • 自动填充字段名和字段类型一定要与实体类一致,否则失效,官网默认为LocalDateTime
    • 乐观锁需要提供version值,否则失效
mybatis-plus:
  configuration:
    # 日志水平
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      # 逻辑删除
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0
	/**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime gmtCreate;
    /**
     * 更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime gmtModified;
    /**
     * 乐观锁
     */
    @Version
    private Integer version;
    /**
     * 逻辑删除
     */
    @TableLogic
    private Integer deleted;
@Configuration
@MapperScan("com.nuc.mapper")
public class MybatisPlusConfig {
    /**
     * 分页插件和乐观锁插件
     *
     * @return MybatisPlusInterceptor
     * @author 石一歌
     * @date 2022/4/10 21:12
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    /**
     * 插入时自动填充
     *
     * @param metaObject 元对象
     * @author 石一歌
     * @date 2022/4/10 21:10
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "gmtCreate", LocalDateTime::now, LocalDateTime.class);
        this.strictInsertFill(metaObject, "gmtModified", LocalDateTime::now, LocalDateTime.class);
    }

    /**
     * 更新时自动填充
     *
     * @param metaObject 元对象
     * @author 石一歌
     * @date 2022/4/10 21:10
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "gmtModified", LocalDateTime::now, LocalDateTime.class);

    }
}

后端主体代码

pojo

@TableName(value = "sys_user", autoResultMap = true)
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    /**
     * 主键
     */
    @TableId(type = IdType.AUTO)
    private Integer id;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 昵称
     */
    private String nickname;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 性别
     */
    private String sex;
    /**
     * 地址
     */
    private String address;
    /**
     * 头像
     */
    private String avatar;
    /**
     * 邮箱
     */
    private String email;
    /**
     * 手机号
     */
    private String phone;
    /**
     * 角色列表
     */
    private String role;

    /**
     * 账户金额
     */
    private BigDecimal account;
    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime gmtCreate;
    /**
     * 更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime gmtModified;
    /**
     * 乐观锁
     */
    @Version
    private Integer version;
    /**
     * 逻辑删除
     */
    @TableLogic
    private Integer deleted;
}

mapper

public interface UserMapper extends BaseMapper<User> {
}

service

@Service
public class UserService extends ServiceImpl<UserMapper, User> {
}

controller

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private UserService userService;


    /**
     * 增加或修改用户
     *
     * @param user 用户
     * @return Result<Object>
     * @author 石一歌
     * @date 2022/4/10 22:39
     */
    @PostMapping
    public Result<Object> save(@RequestBody User user) {
        return userService.saveOrUpdate(user) ? Result.success() : Result.error();
    }


    /**
     * 查询所有用户
     *
     * @return Result<Object>
     * @author 石一歌
     * @date 2022/4/10 22:40
     */
    @GetMapping
    public Result<Object> findAll() {
        return CollUtil.isNotEmpty(userService.list()) ? Result.success(userService.list()) : Result.error();
    }


    /**
     * 删除用户
     *
     * @param id 用户id
     * @return Result<Object>
     * @author 石一歌
     * @date 2022/4/10 22:40
     */
    @DeleteMapping("/{id}")
    public Result<Object> delete(@PathVariable Integer id) {
        return userService.removeById(id) ? Result.success() : Result.error();
    }

    /**
     * 批量删除用户
     *
     * @param ids id列表
     * @return Result<Object>
     * @author 石一歌
     * @date 2022/4/11 15:46
     */
    @PostMapping("/del/batch")
    public Result<Object> deleteBatch(@RequestBody List<Integer> ids) {
        return userService.removeByIds(ids) ? Result.success() : Result.error();
    }

    /**
     * 分页查询 - mybatis-plus的方式
     *
     * @param pageNum  分页页码
     * @param pageSize 分页大小
     * @param username 用户名
     * @param nickname 用户昵称
     * @param address  用户住址
     * @return Result<Object>
     * @author 石一歌
     * @date 2022/4/10 16:24
     */
    @GetMapping("/page")
    public Result<Object> findPage(@RequestParam Integer pageNum,
                                   @RequestParam Integer pageSize,
                                   @RequestParam(defaultValue = "") String username,
                                   @RequestParam(defaultValue = "") String nickname,
                                   @RequestParam(defaultValue = "") String address) {
        IPage<User> page = new Page<>(pageNum, pageSize);
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        if (StrUtil.isNotEmpty(username)) {
            queryWrapper.like("username", username);
        }
        if (StrUtil.isNotEmpty(nickname)) {
            queryWrapper.like("nickname", nickname);
        }
        if (StrUtil.isNotEmpty(address)) {
            queryWrapper.like("address", address);
        }
        return ObjectUtil.isNotEmpty(userService.page(page, queryWrapper)) ? Result.success(userService.page(page, queryWrapper)) : Result.error();
    }
}

前后端联调

Home.vue

<script setup lang="ts">

import { ref, reactive, toRefs } from 'vue'
import { Edit, Remove, Download, Upload, CirclePlus, Message, Menu, Setting, Fold, Expand, Search, Position } from '@element-plus/icons-vue';
import request from '@/utils/request';
import { ElMessage } from 'element-plus';

const tableData = ref([])
const isCollapse = ref<boolean>(false)
const sideWidth = ref<number>(200)
const total = ref<number>(0)
const dialogFormVisible = ref<boolean>(false)
const pageNum = ref<number>(1)
const pageSize = ref<number>(5)
const username = ref<string>('')
const email = ref<string>('')
const address = ref<string>('')
const multipleSelection = ref<any>([])
const form = reactive({
    username: '',
    nickname: '',
    password: '',
    age: null,
    sex: '未知',
    address: '',
    avatar: '',
    email: '',
    phone: '',
    account: '',
    role: '',
    id: null,
    version: null
})
const formAsRefs = toRefs(form)
const headerBg = ref('headerBg')
const collapse = () => {
    if (!isCollapse.value) {
        isCollapse.value = true
        sideWidth.value = 64
    } else {
        isCollapse.value = false
        sideWidth.value = 200
    }
}
const load = () => {
    request.get("/user/page", {
        params: {
            pageNum: pageNum.value,
            pageSize: pageSize.value,
            username: username.value,
            email: email.value,
            address: address.value,
        }
    }).then(res => {
        tableData.value = res.data.records
        total.value = res.data.total
    })
}
load()
const save = () => {
    request.post("/user", form).then(res => {
        if (res) {
            ElMessage.success('保存成功')
            dialogFormVisible.value = false
            load()
        } else {
            ElMessage.error("保存失败")
        }
    })
}
const handleAdd = () => {
    dialogFormVisible.value = true
    formAsRefs.username.value = ''
    formAsRefs.nickname.value = ''
    formAsRefs.age.value = null
    formAsRefs.sex.value = ''
    formAsRefs.address.value = ''
    formAsRefs.email.value = ''
    formAsRefs.phone.value = ''
    formAsRefs.account.value = ''
    formAsRefs.avatar.value = ''
    formAsRefs.role.value = ''
    formAsRefs.password.value = '123456'
}
const handleEdit = (row: { username: string; nickname: string; age: null; sex: string; avatar: string; address: string; email: string; phone: string; account: string; role: string; id: null; version: null; }) => {
    formAsRefs.username.value = row.username
    formAsRefs.nickname.value = row.nickname
    formAsRefs.age.value = row.age
    formAsRefs.sex.value = row.sex
    formAsRefs.avatar.value = row.avatar
    formAsRefs.address.value = row.address
    formAsRefs.email.value = row.email
    formAsRefs.phone.value = row.phone
    formAsRefs.account.value = row.account
    formAsRefs.role.value = row.role
    formAsRefs.id.value = row.id
    formAsRefs.version.value = row.version
    dialogFormVisible.value = true
}
const handleSelectionChange = (val: never[]) => {
    multipleSelection.value = val
}
const del = (id: number) => {
    request.delete("/user/" + id).then(res => {
        if (res) {
            ElMessage.success("删除成功")
            load()
        } else {
            ElMessage.error("删除失败")
        }
    })
}
const delBatch = () => {
    let ids: number[] = []
    multipleSelection.value.forEach((element: any) => {
        ids.push(element.id)
    });
    request.post("/user/del/batch", ids).then(res => {
        if (res) {
            ElMessage.success("批量删除成功")
            load()
        } else {
            ElMessage.error("批量删除失败")
        }
    })
}
const reset = () => {
    username.value = ""
    email.value = ""
    address.value = ""
    load()
}
const handleSizeChange = (size: number) => {
    pageSize.value = size
    load()
}
const handleCurrentChange = (num: number) => {
    pageNum.value = num
    load()
}
const dataFormat = (data: string) => {
    return data.split('T').join(' ')
}
</script>

<template>
    <el-container style="min-height: 100vh">
        <el-aside :width="sideWidth + 'px'" style="box-shadow: 2px 0 6px rgb(0 21 41 / 35%);  ">
            <el-menu :default-openeds="['1', '3']" style="min-height: 100%; overflow-x: hidden"
                background-color="rgb(48, 65, 86)" text-color="#fff" active-text-color="#ffd04b"
                :collapse-transition="false" :collapse="isCollapse">
                <div style="height: 60px; line-height: 60px; text-align: center">
                    <img src="../assets/logo.png" style="width: 20px; position: relative; top: 5px; right: 5px" />
                    <b style="color: white" v-show="!isCollapse">后台管理系统</b>
                </div>
                <el-sub-menu index="1">
                    <template #title>
                        <message style="width: 1em; height: 1em; margin-right: 8px" />
                        <span>消息</span>
                    </template>
                </el-sub-menu>
                <el-sub-menu index="2">
                    <template #title>
                        <Menu style="width: 1em; height: 1em; margin-right: 8px" />
                        <span>菜单</span>
                    </template>
                </el-sub-menu>
                <el-sub-menu index="3">
                    <template #title>
                        <setting style="width: 1em; height: 1em; margin-right: 8px" />
                        <span>设置</span>
                    </template>
                </el-sub-menu>
            </el-menu>
        </el-aside>

        <el-container style="postion: relative">
            <el-header style="font-size: 12px; border-bottom: 1px solid #ccc; line-height: 60px; display: flex">
                <div style="flex: 1; font-size: 20px; backgroud-color: rgb(48, 65, 86)">
                    <el-icon @click="collapse()">
                        <fold v-show="!isCollapse" />
                        <expand v-show="isCollapse" />
                    </el-icon>
                </div>
                <div style="width: 100px">
                    <el-dropdown>
                        <span class="el-dropdown-link">
                            <el-avatar :size="30" style="position: relative; top: 15px"></el-avatar>
                        </span>
                        <template #dropdown>
                            <el-dropdown-menu>
                                <el-dropdown-item>个人信息</el-dropdown-item>
                                <el-dropdown-item>修改密码</el-dropdown-item>
                                <el-dropdown-item>退出系统</el-dropdown-item>
                            </el-dropdown-menu>
                        </template>
                    </el-dropdown>
                </div>
            </el-header>

            <el-main>
                <div style="margin-bottom: 30px">
                    <el-breadcrumb separator="/">
                        <el-breadcrumb-item :to="'/home'">首页</el-breadcrumb-item>
                        <el-breadcrumb-item>用户管理</el-breadcrumb-item>
                    </el-breadcrumb>
                </div>
                <div style="margin: 10px 0">
                    <el-input style="width: 200px" placeholder="请输入名称" :suffix-icon="Search" class="ml-5"
                        v-model="username"></el-input>
                    <el-input style="width: 200px" placeholder="请输入邮箱" :suffix-icon="Message" class="ml-5"
                        v-model="email"></el-input>
                    <el-input style="width: 200px" placeholder="请输入地址" :suffix-icon="Position" class="ml-5"
                        v-model="address"></el-input>
                    <el-button class="ml-5" type="primary" @click="load">搜索</el-button>
                    <el-button class="ml-5" type="warning" @click="reset">清空</el-button>
                </div>

                <div style="margin: 10px 0">
                    <el-button type="primary" @click="handleAdd">
                        新增
                        <circle-plus style="width: 1em; height: 1em; margin-right: 8px" />
                    </el-button>
                    <el-popconfirm title="确定删除吗?" class="ml-5" confirm-button-text='确定' cancel-button-text='我再想想'
                        icon-color="red" @confirm="delBatch">
                        <template #reference>
                            <el-button type="danger">
                                批量删除
                                <remove style="width: 1em; height: 1em; margin-right: 8px" />
                            </el-button>
                        </template>
                    </el-popconfirm>

                    <el-button type="primary">
                        导入
                        <upload style="width: 1em; height: 1em; margin-right: 8px" />
                    </el-button>
                    <el-button type="primary">
                        导出
                        <download style="width: 1em; height: 1em; margin-right: 8px" />
                    </el-button>
                </div>

                <el-table :data="tableData" border stripe :header-cell-class-name="headerBg"
                    @selection-change="handleSelectionChange">
                    <el-table-column type="selection" width="55" fixed></el-table-column>
                    <el-table-column prop="id" label="ID" sortable width="55" fixed>
                    </el-table-column>
                    <el-table-column prop="username" label="用户名">
                    </el-table-column>
                    <el-table-column prop="nickname" label="昵称">
                    </el-table-column>
                    <el-table-column prop="age" label="年龄">
                    </el-table-column>
                    <el-table-column prop="sex" label="性别">
                    </el-table-column>
                    <el-table-column prop="address" label="地址" width="200px">
                    </el-table-column>
                    <el-table-column prop="email" label="邮箱" width="200px">
                    </el-table-column>
                    <el-table-column prop="phone" label="手机号" width="200px">
                    </el-table-column>
                    <el-table-column prop="avatar" label="头像">
                    </el-table-column>
                    <el-table-column prop="role" label="角色">
                    </el-table-column>
                    <el-table-column prop="gmtCreate" label="创建时间" width="200px">
                        <template v-slot="scope">
                            {{ dataFormat(scope.row.gmtCreate) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="gmtModified" label="修改时间" width="200px">
                        <template v-slot="scope">
                            {{ dataFormat(scope.row.gmtModified) }}
                        </template>
                    </el-table-column>
                    <el-table-column prop="version" label="修改次数">
                    </el-table-column>
                    <el-table-column label="操作" width="300" align="center" fixed="right">
                        <template v-slot="scope">
                            <el-button type="success" @click="handleEdit(scope.row)">
                                编辑
                                <edit style="width: 1em; height: 1em; margin-right: 8px" />
                            </el-button>
                            <el-popconfirm title="确定删除吗?" class="ml-5" confirm-button-text='确定'
                                cancel-button-text='我再想想' icon-color="red" @confirm="del(scope.row.id)">
                                <template #reference>
                                    <el-button type="danger">
                                        删除
                                        <remove style="width: 1em; height: 1em; margin-right: 8px" />
                                    </el-button>
                                </template>
                            </el-popconfirm>
                        </template>
                    </el-table-column>
                </el-table>
                <div style="padding: 10px 0">
                    <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
                        :current-page="pageNum" :page-sizes="[5, 10, 15, 20]" :page-size="pageSize"
                        layout="total, sizes, prev, pager, next, jumper" :total="total"></el-pagination>
                </div>
                <el-dialog title="提示" v-model="dialogFormVisible" width="30%">
                    <el-form :model="form" label-width="120px">
                        <el-form-item label="用户名">
                            <el-input v-model="form.username" style="width: 80%"></el-input>
                        </el-form-item>
                        <el-form-item label="昵称">
                            <el-input v-model="form.nickname" style="width: 80%"></el-input>
                        </el-form-item>
                        <el-form-item label="年龄">
                            <el-input v-model="form.age" style="width: 80%"></el-input>
                        </el-form-item>
                        <el-form-item label="性别">
                            <el-radio v-model="form.sex" label="男">男</el-radio>
                            <el-radio v-model="form.sex" label="女">女</el-radio>
                            <el-radio v-model="form.sex" label="未知">未知</el-radio>
                        </el-form-item>
                        <el-form-item label="地址">
                            <el-input type="textarea" v-model="form.address" style="width: 80%"></el-input>
                        </el-form-item>
                        <el-form-item label="邮箱">
                            <el-input v-model="form.email" style="width: 80%"></el-input>
                        </el-form-item>
                        <el-form-item label="手机号">
                            <el-input v-model="form.phone" style="width: 80%"></el-input>
                        </el-form-item>
                        <el-form-item label="账户余额">
                            <el-input v-model="form.account" style="width: 80%"></el-input>
                        </el-form-item>
                    </el-form>
                    <template #footer>
                        <span class="dialog-footer">
                            <el-button @click="dialogFormVisible = false">取 消</el-button>
                            <el-button type="primary" @click="save">确 定</el-button>
                        </span>
                    </template>
                </el-dialog>

            </el-main>
            <el-footer height="5px">
                <div class="footer">&copy; 献给最爱的青哥哥</div>
            </el-footer>
        </el-container>
    </el-container>
</template>



<style scoped>
.headerBg {
    background: #eee !important;
}

.footer {
    /* display: flex; */
    /* justify-content: center; */
    /* align-items: center; */
    margin: 0px auto;
    /* right: 0px; */
    left: 50%;
    position: absolute;
    bottom: 0;
}
</style>

代码生成器

前端组件分离

目录

D:.
│  App.vue
│  env.d.ts
│  main.ts
├─assets
│      global.css
│      logo.png
├─components
│      Aside.vue
│      Footer.vue
│      Header.vue
├─layout
│      Layout.vue
├─router
│      index.ts
├─stores
│      store.ts
├─utils
│      request.ts
└─views
       About.vue
       Home.vue
       Login.vue
       User.vue

Aside.vue

<script setup lang="ts">
import { Message, Menu, Setting } from '@element-plus/icons-vue';
import { defineProps } from 'vue'

const props = defineProps({
    isCollapse: {
        type: Boolean
    }
})
</script>
<template>
    <el-menu :default-openeds="['1', '3']" style="min-height: 100%; overflow-x: hidden"
        background-color="rgb(48, 65, 86)" text-color="#fff" active-text-color="#ffd04b" :collapse-transition="false"
        :collapse="props.isCollapse"
        :router="true">
        <div style="height: 60px; line-height: 60px; text-align: center">
            <img src="../assets/logo.png" style="width: 20px; position: relative; top: 5px; right: 5px" />
            <b style="color: white" v-show="!props.isCollapse">后台管理系统</b>
        </div>

        <el-sub-menu index="/">
            <template #title>
                <message style="width: 1em; height: 1em; margin-right: 8px" />
                <span>主页</span>
            </template>
        </el-sub-menu>
        <el-sub-menu index="2">
            <template #title>
                <Menu style="width: 1em; height: 1em; margin-right: 8px" />
                <span>系统管理</span>
            </template>
            <el-menu-item index="/user">
                <message style="width: 1em; height: 1em; margin-right: 8px" />
                <span>用户管理</span>
            </el-menu-item>
        </el-sub-menu>
        <el-sub-menu index="3">
            <template #title>
                <setting style="width: 1em; height: 1em; margin-right: 8px" />
                <span>设置</span>
            </template>
        </el-sub-menu>
    </el-menu>
</template>
<style scoped>
</style>

Footer.vue

<script setup lang="ts">
</script>

<template>
    <div>&copy; 献给最爱的青哥哥</div>
</template>

<style scoped>
</style>

Header.vue

<script setup lang="ts">
import { Fold, Expand } from '@element-plus/icons-vue';
import { defineProps, onMounted } from 'vue'
import { useStore } from '@/stores/store'
const props = defineProps({
    isCollapse: {
        type: Boolean
    }
})
interface EmitType {
    (e: "asideCollapse"): void
}
const emit = defineEmits<EmitType>();
const collapse = () => {
    emit('asideCollapse')
}
const currentPathName = useStore().path
</script>
<template>
    <div style="line-height: 60px; display: flex">
        <div style="flex: 1;">
            <el-icon @click="collapse()" style="cursor: pointer; font-size: 18px">
                <fold v-show="!props.isCollapse" />
                <expand v-show="props.isCollapse" />
            </el-icon>
            <el-breadcrumb separator="/" style="display: inline-block; margin-left: 10px">
                <el-breadcrumb-item :to="'/'">首页</el-breadcrumb-item>
                <el-breadcrumb-item>{{ currentPathName }}</el-breadcrumb-item>
            </el-breadcrumb>
        </div>
        <div style="width: 100px">
            <el-dropdown>
                <span class="el-dropdown-link">
                    <el-avatar :size="30" style="position: relative; top: 15px"></el-avatar>
                </span>
                <template #dropdown>
                    <el-dropdown-menu>
                        <el-dropdown-item>个人信息</el-dropdown-item>
                        <el-dropdown-item>修改密码</el-dropdown-item>
                        <el-dropdown-item>退出系统</el-dropdown-item>
                    </el-dropdown-menu>
                </template>
            </el-dropdown>
        </div>
    </div>
</template>
<style scoped>
</style>

Layout.vue

<script setup lang="ts">
import { ref } from 'vue'
import Aside from '@/components/Aside.vue';
import Header from '@/components/Header.vue';
import Footer from '@/components/Footer.vue';
const isCollapse = ref<boolean>(false)
const sideWidth = ref<number>(200)
const collapse = () => {
  if (!isCollapse.value) {
    isCollapse.value = true
    sideWidth.value = 64
  } else {
    isCollapse.value = false
    sideWidth.value = 200
  }
}
</script>
<template>
  <el-container style="min-height: 100vh">
    <el-aside :width="sideWidth + 'px'" style="box-shadow: 2px 0 6px rgb(0 21 41 / 35%);  ">
      <Aside :isCollapse="isCollapse" />
    </el-aside>
    <el-container style="		position: relative;padding-bottom: 50px;min-height: 100%;	">
      <el-header style="border-bottom: 1px solid #ccc;">
        <Header :isCollapse="isCollapse" @asideCollapse="collapse" />
      </el-header>
      <el-main>
        <router-view />
      </el-main>
      <el-footer height="5px">
        <Footer class="footer" />
      </el-footer>
    </el-container>
  </el-container>
</template>
<style scoped>
.footer {
  /* display: flex; */
  /* justify-content: center; */
  /* align-items: center; */
  margin: 0px auto;
  left: 50%;
  position: absolute;
  bottom: 0;
}
</style>

index.ts

import { useStore } from "@/stores/store";
import {
  createRouter,
  RouteRecordRaw,
  Router,
  createWebHistory,
} from "vue-router";

const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "layout",
    component: () => import("@/layout/Layout.vue"),
    redirect: "/user",
    children: [
      {
        path: "home",
        name: "首页",
        component: () => import("@/views/Home.vue"),
      },
      {
        path: "user",
        name: "用户管理",
        component: () => import("@/views/User.vue"),
      },
    ],
  },
  {
    path: "/login",
    name: "Login",
    component: () => import("@/views/Login.vue"),
  },
  {
    path: "/about",
    name: "About",
    component: () => import("@/views/About.vue"),
  },
];

const router: Router = createRouter({
  history: createWebHistory(),
  routes,
});

router.beforeEach((to, from, next) => {
  localStorage.setItem("currentPathName", to.name as string)  // 设置当前的路由名称
  const Store = useStore()
  Store.setPath()

  // // 未找到路由的情况
  // if (!to.matched.length) {
  //   const storeMenus = localStorage.getItem("menus")
  //   if (storeMenus) {
  //     next("/404")
  //   } else {
  //     // 跳回登录页面
  //     next("/login")
  //   }
  // }
  // 其他的情况都放行
  next()

})
export default router;

store.ts

import { defineStore } from "pinia";

// defineStore 调用后返回一个函数,调用该函数获得 Store 实体
export const useStore = defineStore({
  // id: 必须的,在所有 Store 中唯一
  id: "currentPathState",
  // state: 返回对象的函数
  state: ()=> ({
    path: ''
  }),
  actions:{
    setPath () {
        this.path = localStorage.getItem("currentPathName") as string
    }
  }
});

User.vue

<script setup lang="ts">

import { ref, reactive, toRefs } from 'vue'
import { Edit, Remove, Download, Upload, CirclePlus, Message, Search, Position } from '@element-plus/icons-vue';
import request from '@/utils/request';
import { ElMessage } from 'element-plus';
import { regionData, CodeToText } from 'element-china-area-data'

const tableData = ref([])
const total = ref<number>(0)
const dialogFormVisible = ref<boolean>(false)
const dialogRoleVisible = ref<boolean>(false)
const pageNum = ref<number>(1)
const pageSize = ref<number>(5)
const username = ref<string>('')
const email = ref<string>('')
const address = ref<string>('')
const multipleSelection = ref<any>([])
const form = reactive({
  username: '',
  nickname: '',
  password: '',
  age: null,
  sex: '未知',
  address: '',
  avatar: '',
  email: '',
  phone: '',
  account: '',
  id: null,
  version: null
})
const formAsRefs = toRefs(form)
const headerBg = ref('headerBg')
const load = () => {
  request.get("/user/page", {
    params: {
      pageNum: pageNum.value,
      pageSize: pageSize.value,
      username: username.value,
      email: email.value,
      address: address.value,
    }
  }).then(res => {
    if (res.status === 200) {
      tableData.value = res.data.records
      total.value = res.data.total
    } else {
      ElMessage.error("加载失败")
    }
  })
}
load()
const save = () => {
  request.post("/user", form).then(res => {
    if (res.status === 200) {
      ElMessage.success('保存成功')
      dialogFormVisible.value = false
      load()
    } else {
      ElMessage.error("保存失败")
    }
  })
}
const handleAdd = () => {
  dialogFormVisible.value = true
  formAsRefs.username.value = ''
  formAsRefs.nickname.value = ''
  formAsRefs.age.value = null
  formAsRefs.sex.value = ''
  formAsRefs.address.value = ''
  formAsRefs.email.value = ''
  formAsRefs.phone.value = ''
  formAsRefs.account.value = ''
  formAsRefs.avatar.value = ''
  formAsRefs.id.value = null
  formAsRefs.password.value = '123456'
}
const handleEdit = (row: { username: string; nickname: string; age: null; sex: string; avatar: string; email: string; phone: string; account: string; id: null; version: null; password: string; address: (string | number)[]; }) => {
  formAsRefs.username.value = row.username
  formAsRefs.nickname.value = row.nickname
  formAsRefs.age.value = row.age
  formAsRefs.sex.value = row.sex
  formAsRefs.avatar.value = row.avatar
  // formAsRefs.address.value = row.address
  formAsRefs.email.value = row.email
  formAsRefs.phone.value = row.phone
  formAsRefs.account.value = row.account
  formAsRefs.id.value = row.id
  formAsRefs.version.value = row.version
  formAsRefs.password.value = row.password
  dialogFormVisible.value = true
  addressChange(row.address)
}
const handleSelectionChange = (val: never[]) => {
  multipleSelection.value = val
}
const del = (id: number) => {
  request.delete("/user/" + id).then(res => {
    if (res.status === 200) {
      ElMessage.success("删除成功")
      load()
    } else {
      ElMessage.error("删除失败")
    }
  })
}
const delBatch = () => {
  let ids: number[] = []
  multipleSelection.value.forEach((element: any) => {
    ids.push(element.id)
  });
  request.post("/user/del/batch", ids).then(res => {
    if (res.status === 200) {
      ElMessage.success("批量删除成功")
      load()
    } else {
      ElMessage.error("批量删除失败")
    }
  })
}
const reset = () => {
  username.value = ""
  email.value = ""
  address.value = ""
  load()
}
const handleSizeChange = (size: number) => {
  pageSize.value = size
  load()
}
const handleCurrentChange = (num: number) => {
  pageNum.value = num
  load()
}
const dataFormat = (data: string) => {
  if (data === null) return null
  else
    return data.split('T').join(' ')
}
const exp = () => {
  window.open("http://localhost:8080/user/export")
}
const handleExcelImportSuccess = () => {
  ElMessage.success("导入成功")
}
const roleData = ref()
const roleArray = ref([])


const userId = ref<number>()
const selectRole = (id: any) => {
  dialogRoleVisible.value = true
  userId.value = id
  request.get("/role").then(res => {
    if (res.status === 200) {
      roleData.value = res.data
    } else {
      ElMessage.error("加载失败")
    }
  })
  request.get("/userRole/" + userId.value).then(res => {
    if (res.status === 200) {
      roleArray.value = res.data
    }
  })
}

const saveUserRole = () => {
  request.post("/userRole/" + userId.value, roleArray.value).then(res => {
    if (res.status === 200) {
      ElMessage.success("绑定成功")
    } else {
      ElMessage.error(res.statusText)
    }
    dialogRoleVisible.value = false
  })
  roleArray.value = []
}
let addressText = ref()
const addressChange = (arr: (string | number)[]) => {
    if (arr[1] != null && arr[2] != null) {
        addressText.value = CodeToText[arr[0]] + CodeToText[arr[1]] + CodeToText[arr[2]]
    } else {
        if (arr[1] != null) {
            addressText.value = CodeToText[arr[0]] + CodeToText[arr[1]]
        } else {
            addressText.value = CodeToText[arr[0]]
        }
    }
}
const addressFormat = (address: (string | number)[]) => {
  if (address === null) return null
  else {
    if (address[1] != null && address[2] != null) {
      return CodeToText[address[0]] + CodeToText[address[1]] + CodeToText[address[2]]
    } else {
      if (address[1] != null) {
        return CodeToText[address[0]] + CodeToText[address[1]]
      } else {
        return CodeToText[address[0]]
      }
    }
  }

}
</script>

<template>
  <el-main>
    <div style="margin: 10px 0">
      <el-input style="width: 200px" placeholder="请输入名称" :suffix-icon="Search" class="ml-5" v-model="username">
      </el-input>
      <el-input style="width: 200px" placeholder="请输入邮箱" :suffix-icon="Message" class="ml-5" v-model="email">
      </el-input>
      <el-input style="width: 200px" placeholder="请输入地址" :suffix-icon="Position" class="ml-5" v-model="address">
      </el-input>
      <el-button class="ml-5" type="primary" @click="load">搜索</el-button>
      <el-button class="ml-5" type="warning" @click="reset">清空</el-button>
    </div>

    <div style="margin: 10px 0; position: absolut;">
      <el-button type="primary" @click="handleAdd">
        新增
        <circle-plus style="width: 1em; height: 1em; margin-right: 8px" />
      </el-button>
      <el-popconfirm title="确定删除吗?" class="ml-5" confirm-button-text='确定' cancel-button-text='我再想想' icon-color="red"
        @confirm="delBatch">
        <template #reference>
          <el-button type="danger">
            批量删除
            <remove style="width: 1em; height: 1em; margin-right: 8px" />
          </el-button>
        </template>
      </el-popconfirm>
      <el-upload action="http://localhost:8080/user/import" :show-file-list="false" accept="xlsx"
        :on-success="handleExcelImportSuccess"
        style="display: inline-block; margin: 0 10px; position: relative;top:2px">
        <el-button type="primary">
          导入
          <upload style="width: 1em; height: 1em; margin-right: 8px" />
        </el-button>
      </el-upload>

      <el-button type="primary" @click="exp">
        导出
        <download style="width: 1em; height: 1em; margin-right: 8px" />
      </el-button>
    </div>

    <el-table :data="tableData" border stripe :header-cell-class-name="headerBg"
      @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" fixed></el-table-column>
      <el-table-column prop="id" label="ID" sortable width="70" fixed>
      </el-table-column>
      <el-table-column prop="username" label="用户名">
      </el-table-column>
      <el-table-column prop="nickname" label="昵称">
      </el-table-column>
      <el-table-column prop="age" label="年龄">
      </el-table-column>
      <el-table-column prop="sex" label="性别">
      </el-table-column>
      <el-table-column prop="address" label="地址" width="200px">
        <template v-slot="scope">
          {{ addressFormat(scope.row.address) }}
        </template>
      </el-table-column>
      <el-table-column prop="email" label="邮箱" width="200px">
      </el-table-column>
      <el-table-column prop="phone" label="手机号" width="200px">
      </el-table-column>
      <el-table-column prop="avatar" label="头像" width="130px">
        <template v-slot="scope">
          <el-image style="width: 100px; height: 100px" :src="scope.row.avatar" :fit="'cover'"
            :preview-src-list="[scope.row.avatar]">
          </el-image>
        </template>
      </el-table-column>
      <el-table-column prop="gmtCreate" label="创建时间" width="200px">
        <template v-slot="scope">
          {{ dataFormat(scope.row.gmtCreate) }}
        </template>
      </el-table-column>
      <el-table-column prop="gmtModified" label="修改时间" width="200px">
        <template v-slot="scope">
          {{ dataFormat(scope.row.gmtModified) }}
        </template>
      </el-table-column>
      <el-table-column prop="version" label="修改次数">
      </el-table-column>
      <el-table-column label="操作" width="400" align="center" fixed="right">
        <template v-slot="scope">
          <el-button type="info" @click="selectRole(scope.row.id)">
            分配角色
          </el-button>
          <el-button type="success" @click="handleEdit(scope.row)">
            编辑
            <edit style="width: 1em; height: 1em; margin-right: 8px" />
          </el-button>
          <el-popconfirm title="确定删除吗?" class="ml-5" confirm-button-text='确定' cancel-button-text='我再想想' icon-color="red"
            @confirm="del(scope.row.id)">
            <template #reference>
              <el-button type="danger">
                删除
                <remove style="width: 1em; height: 1em; margin-right: 8px" />
              </el-button>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    <div style="padding: 10px 0">
      <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="pageNum"
        :page-sizes="[5, 10, 15, 20]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper"
        :total="total"></el-pagination>
    </div>
    <el-dialog title="用户信息" v-model="dialogFormVisible" width="30%">
      <el-form :model="form" label-width="120px">
        <el-form-item label="用户名">
          <el-input v-model="form.username" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="昵称">
          <el-input v-model="form.nickname" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="年龄">
          <el-input v-model="form.age" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="性别">
          <el-radio v-model="form.sex" label="男">男</el-radio>
          <el-radio v-model="form.sex" label="女">女</el-radio>
          <el-radio v-model="form.sex" label="未知">未知</el-radio>
        </el-form-item>
        <el-form-item label="地址">
          {{ addressText }}
          <el-cascader placeholder="请选择地区" size="small" :options="regionData" v-model="form.address"
            @change='addressChange' style="width: 80%">
          </el-cascader>
        </el-form-item>
        <el-form-item label="邮箱">
          <el-input v-model="form.email" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="手机号">
          <el-input v-model="form.phone" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="账户余额">
          <el-input v-model="form.account" style="width: 80%" disabled></el-input>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogFormVisible = false">取 消</el-button>
          <el-button type="primary" @click="save">确 定</el-button>
        </span>
      </template>
    </el-dialog>
    <el-dialog title="角色分配" v-model="dialogRoleVisible" width="20%">
      <el-select v-model="roleArray" multiple collapse-tags collapse-tags-tooltip placeholder="分配角色"
        style="width: 200px" size="large">
        <el-option v-for="item in roleData" :key="item.id" :label="item.name" :value="item.id" />
      </el-select>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogRoleVisible = false">取 消</el-button>
          <el-button type="primary" @click="saveUserRole()">确 定</el-button>
        </span>
      </template>
    </el-dialog>
  </el-main>
</template>



<style scoped>
</style>

导入导出

UserController

	/**
     * 导出
     *
     * @param response 返回值
     * @author 石一歌
     * @date 2022/4/15 0:12
     */
    @GetMapping("/export")
    public void export(HttpServletResponse response) throws IOException {
        // 查询所有数据
        List<User> list = userService.list();
        // 在内存操作,写出到浏览器
        ExcelWriter writer = ExcelUtil.getWriter(true);
        // 一次性写出list内的对象到excel
        writer.write(list, true);
        // 设置浏览器响应格式
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        String fileName = URLEncoder.encode("用户信息", "UTF-8");
        response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
        ServletOutputStream out = response.getOutputStream();
        writer.flush(out, true);
        writer.close();
        IoUtil.close(System.out);
    }

    /**
     * 导入
     *
     * @param file 文件
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/15 0:12
     */
    @PostMapping("/import")
    public Result<?> upload(MultipartFile file) throws IOException {
        InputStream inputStream = file.getInputStream();
        ExcelReader reader = ExcelUtil.getReader(inputStream);
        List<User> list = reader.readAll(User.class);
        return userService.saveBatch(list) ? Result.success() : Result.error();
    }

User

添加别名,导出文件表头自动切换为中文

@EqualsAndHashCode(callSuper = true)
@TableName(value = "sys_user", autoResultMap = true)
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "User对象", description = "用户实体")
public class User extends Model<User> implements Serializable {

    private static final long serialVersionUID = 1L;
    /**
     * id
     */
    @Alias("id")
    @TableId(type = IdType.AUTO)
    @ApiModelProperty("id")
    private Integer id;
    /**
     * 用户名
     */
    @Alias("用户名")
    @ApiModelProperty("用户名")
    private String username;
    /**
     * 密码
     */
    @Alias("密码")
    @ApiModelProperty("密码")
    private String password;
    /**
     * 昵称
     */
    @Alias("昵称")
    @ApiModelProperty("昵称")
    private String nickname;
    /**
     * 年龄
     */
    @Alias("年龄")
    @ApiModelProperty("年龄")
    private Integer age;
    /**
     * 性别
     */
    @Alias("性别")
    @ApiModelProperty("性别")
    private String sex;
    /**
     * 详细地址
     */
    @Alias("详细地址")
    @ApiModelProperty("详细地址")
    private String address;
    /**
     * 地区
     */
    @Alias("地区")
    @ApiModelProperty("地区")
    @TableField(typeHandler = ListHandler.class)
    private List<Integer> region;
    /**
     * 头像
     */
    @Alias("头像")
    @ApiModelProperty("头像")
    private String avatar;
    /**
     * 邮箱
     */
    @Alias("邮箱")
    @ApiModelProperty("邮箱")
    private String email;
    /**
     * 手机号
     */
    @Alias("手机号")
    @ApiModelProperty("手机号")
    private String phone;
    /**
     * 账户金额
     */
    @Alias("账户金额")
    @ApiModelProperty("账户金额")
    private BigDecimal account;
    /**
     * 创建时间
     */
    @Alias("创建时间")
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime gmtCreate;
    /**
     * 更新时间
     */
    @Alias("更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @ApiModelProperty("更新时间")
    private LocalDateTime gmtModified;
    /**
     * 乐观锁
     */
    @Alias("乐观锁")
    @Version
    @ApiModelProperty("乐观锁")
    private Integer version;
    /**
     * 逻辑删除
     */
    @Alias("逻辑删除")
    @TableLogic
    @ApiModelProperty("逻辑删除")
    private Integer deleted;
}

ListHandler

将java的List转换为数据库的Varcher

public class ListHandler extends BaseTypeHandler<List<?>> {


    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, List list, JdbcType jdbcType) throws SQLException {
        preparedStatement.setObject(i, JSONUtil.toJsonStr(list));
    }

    @Override
    public List<?> getNullableResult(ResultSet resultSet, String s) throws SQLException {
        return JSONUtil.parseArray(resultSet.getString(s));
    }

    @Override
    public List<?> getNullableResult(ResultSet resultSet, int i) throws SQLException {
        return JSONUtil.parseArray(resultSet.getString(i));
    }

    @Override
    public List<?> getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        return JSONUtil.parseArray(callableStatement.getString(i));
    }
}

页面

	<div style="margin: 10px 0; position: absolut;">
      <el-upload action="http://localhost:8080/user/import" :show-file-list="false" accept="xlsx"
        :on-success="handleExcelImportSuccess" style="display: inline-block; margin: 0 10px; position: relative;top:2px">
        <el-button type="primary">
          导入
          <upload style="width: 1em; height: 1em; margin-right: 8px" />
        </el-button>
      </el-upload>

      <el-button type="primary" @click="exp">
        导出
        <download style="width: 1em; height: 1em; margin-right: 8px" />
      </el-button>
    </div>
const exp = () => {
  window.open("http://localhost:8080/user/export")
}
const handleExcelImportSuccess = () => {
  ElMessage.success("导入成功")
}

异常处理

ServiceException

@Getter
public class ServiceException extends RuntimeException {
    private final Integer code;

    /**
     * 自定义业务异常
     *
     * @param code 状态码
     * @param msg  状态信息
     * @return null
     * @author 石一歌
     * @date 2022/4/17 16:26
     */
    public ServiceException(Integer code, String msg) {
        super(msg);
        this.code = code;
    }
}

GlobalExceptionHandler

@ControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 如果抛出的的是ServiceException,则调用该方法
     *
     * @param se 业务异常
     * @return Result
     */
    @ExceptionHandler(ServiceException.class)
    @ResponseBody
    public Result<?> handle(ServiceException se) {
        return Result.error(se.getCode(), se.getMessage());
    }

    /**
     * 通用异常处理
     *
     * @param e 通用异常
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/16 8:57
     */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result<?> error(Exception e) {
        return Result.error(ConstantEnum.CODE_500.getStatus(), e.getMessage());
    }
}

登录注册

UserDTO

@Data
@ToString
public class UserDTO {
    @Alias("用户名")
    private String username;
    /**
     * 密码
     */
    @Alias("密码")
    private String password;
    /**
     * 昵称
     */
    @Alias("昵称")
    private String nickname;
    /**
     * 头像
     */
    @Alias("头像")
    private String avatar;
}

UserController

  	/**
     * 登录
     *
     * @param userDTO 用户数据
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/15 22:51
     */
    @PostMapping("/login")
    public Result<?> login(@RequestBody UserDTO userDTO) {
        String username = userDTO.getUsername();
        String password = userDTO.getPassword();
        if (StrUtil.isBlank(username) || StrUtil.isBlank(password)) {
            return Result.error(ConstantEnum.CODE_400.getStatus(), ConstantEnum.CODE_400.getName());
        }
        return ObjectUtil.isNotEmpty(userServiceImpl.login(userDTO)) ? Result.success(userServiceImpl.login(userDTO)) : Result.error();
    }

    /**
     * 注册
     *
     * @param userDTO 用户数据
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/16 15:30
     */
    @PostMapping("/register")
    public Result<?> register(@RequestBody UserDTO userDTO) {
        String username = userDTO.getUsername();
        String password = userDTO.getPassword();
        if (StrUtil.isBlank(username) || StrUtil.isBlank(password)) {
            return Result.error(ConstantEnum.CODE_400.getStatus(), ConstantEnum.CODE_400.getName());
        }
        return userServiceImpl.register(userDTO) ? Result.success() : Result.error();
    }

IUserService

public interface IUserService extends IService<User> {
    /**
     * 登录
     *
     * @param userDTO
     * @return UserDTO
     * @author 石一歌
     * @date 2022/4/15 17:18
     */
    UserDTO login(UserDTO userDTO);

    /**
     * 注册
     *
     * @param userDTO
     * @return User
     * @author 石一歌
     * @date 2022/4/15 17:19
     */
    Boolean register(UserDTO userDTO);
}

UserServiceImpl

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    /**
     * 登录
     *
     * @param userDTO 用户数据
     * @return UserDTO
     * @author 石一歌
     * @date 2022/4/16 15:29
     */
    @Override
    public UserDTO login(UserDTO userDTO) {
        User one = getUserInfo(userDTO);
        if (one != null) {
            System.out.println(one.toString());
            BeanUtil.copyProperties(one, userDTO, true);
            System.out.println(userDTO.toString());
            return userDTO;
        } else {
            throw new ServiceException(ConstantEnum.CODE_600.getStatus(), "用户名或密码错误");
        }
    }

    /**
     * 注册
     *
     * @param userDTO 用户数据
     * @return User
     * @author 石一歌
     * @date 2022/4/16 15:29
     */
    @Override
    public Boolean register(UserDTO userDTO) {
        User one = getUserInfo(userDTO);
        if (one == null) {
            one = new User();
            BeanUtil.copyProperties(userDTO, one, true);
            save(one);
            return true;
        } else {
            throw new ServiceException(ConstantEnum.CODE_600.getStatus(), "用户已存在");
        }
    }

    /**
     * 用户查找
     *
     * @param userDTO 用户数据
     * @return User
     * @author 石一歌
     * @date 2022/4/16 15:28
     */
    private User getUserInfo(UserDTO userDTO) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", userDTO.getUsername());
        queryWrapper.eq("password", userDTO.getPassword());
        User one;
        try {
            one = getOne(queryWrapper);
        } catch (Exception e) {
            throw new ServiceException(ConstantEnum.CODE_500.getStatus(), ConstantEnum.CODE_500.getName());
        }
        return one;
    }
}

Login.vue

<script setup lang="ts">
import { reactive, ref } from 'vue';
import { Lock, User } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import request from '@/utils/request';
import router from '@/router';
const user = reactive({
    username: '',
    password: '',
})
const ruleFormRef = ref('')
const rules = reactive({
    username: [
        { required: true, message: '请输入用户名', trigger: 'blur' },
        { min: 3, max: 10, message: '长度在 3 到 5 个字符', trigger: 'blur' }
    ],
    password: [
        { required: true, message: '请输入密码', trigger: 'blur' },
        { min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' }
    ],
})
const login = async (formEl: { validate: (arg0: (valid: any, fields: any) => void) => any; }) => {
    if (!formEl) return
    await formEl.validate((valid) => {
        if (valid) {
         request.post("/user/login", user).then(res => {
            if(res.status === 200) {
              localStorage.setItem("user", JSON.stringify(res.data))  
              router.push("/")
              ElMessage.success("登录成功")
            } else {
              ElMessage.error(res.statusText)
            }
          })
        }
    })
}
</script>

<template>
    <div class="wrapper">
        <div
            style="margin: 200px auto; background-color: #fff; width: 350px; height: 320px; padding: 20px; border-radius: 10px">
            <div style="margin: 20px 0; text-align: center; font-size: 24px"><b>登 录</b></div>

            <el-form :model="user" :rules="rules" ref="ruleFormRef">
                <el-form-item prop="username">
                    <el-input placeholder="输入账号" :prefix-icon="User" style="margin: 10px 0" v-model="user.username">
                    </el-input>
                </el-form-item>
                <el-form-item prop="password">
                    <el-input placeholder="输入密码" :prefix-icon="Lock" style="margin: 10px 0" show-password v-model="user.password">
                    </el-input>
                </el-form-item>
                <el-form-item style="margin: 10px 0; text-align: center;">
                    <el-button style="width: 100%" type="primary" autocomplete="off" @click="login(ruleFormRef)">登 录
                    </el-button>
                </el-form-item>
                <el-form-item>
                    <el-button type="text" autocomplete="off" @click="router.push('/register')">前往注册 >></el-button>
                </el-form-item>
            </el-form>
        </div>
    </div>
</template>



<style scoped>
.wrapper {
    height: 100vh;
    background-image: linear-gradient(to bottom right, #FC466B, #3F5EFB);
    overflow: hidden;
}
</style>

Register.vue

<script setup lang="ts">
import { reactive, ref } from 'vue';
import { Lock, User } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import request from '@/utils/request';
import router from '@/router';
const user = reactive({
    username: '',
    password: '',
    confirmPassword: ''
})
const ruleFormRef = ref('')
const rules = reactive({
    username: [
        { required: true, message: '请输入用户名', trigger: 'blur' },
        { min: 3, max: 10, message: '长度在 3 到 5 个字符', trigger: 'blur' }
    ],
    password: [
        { required: true, message: '请输入密码', trigger: 'blur' },
        { min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' }
    ],
    confirmPassword: [
        { required: true, message: '请再次输入密码', trigger: 'blur' },
        { min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' }
    ],
})
const register = async (formEl: { validate: (arg0: (valid: any, fields: any) => void) => any; }) => {
    if (!formEl) return
    await formEl.validate((valid) => {
        if (valid) {
            if (user.password !== user.confirmPassword) {
                ElMessage.error("两次输入的密码不一致")
                return false
            }
            request.post("/user/register", user).then(res => {
                if (res.status=== 200) {
                    ElMessage.success("注册成功")
                } else {
                    ElMessage.error(res.statusText)
                }
            })
        }
    })
}
</script>

<template>
    <div class="wrapper">
       <div
            style="margin: 150px auto; background-color: #fff; width: 350px; height: 400px; padding: 20px; border-radius: 10px">
            <div style="margin: 20px 0; text-align: center; font-size: 24px"><b>注 册</b></div>

            <el-form :model="user" :rules="rules" ref="ruleFormRef">
                <el-form-item prop="username">
                    <el-input placeholder="输入账号" :prefix-icon="User" style="margin: 10px 0" v-model="user.username">
                    </el-input>
                </el-form-item>
                <el-form-item prop="password">
                    <el-input placeholder="输入密码" :prefix-icon="Lock" style="margin: 10px 0" show-password v-model="user.password">
                    </el-input>
                </el-form-item>
                  <el-form-item prop="confirmPassword">
                    <el-input placeholder="确认密码" :prefix-icon="Lock" style="margin: 10px 0" show-password v-model="user.confirmPassword">
                    </el-input>
                </el-form-item>
                <el-form-item style="margin: 10px 0; text-align: center;">
                    <el-button style="width: 100%" type="primary" autocomplete="off" @click="register(ruleFormRef)">注 册
                    </el-button>
                </el-form-item>
                 <el-form-item style="margin: 20px 0; text-align: center;">
                    <el-button style="width: 100%" type="warning" autocomplete="off"  @click="router.push('/login')">登 录
                    </el-button>
                </el-form-item>
            </el-form>
        </div>
    </div>
</template>



<style scoped>
.wrapper {
    height: 100vh;
    background-image: linear-gradient(to bottom right, #FC466B, #3F5EFB);
    overflow: hidden;
}
</style>

index.ts

import { useStore } from "@/stores/store";
import {
  createRouter,
  RouteRecordRaw,
  Router,
  createWebHistory,
} from "vue-router";

const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "layout",
    component: () => import("@/layout/Layout.vue"),
    redirect: "/home",
    children: [
      {
        path: "home",
        name: "Home",
        component: () => import("@/views/Home.vue"),
      },
      {
        path: "user",
        name: "User",
        component: () => import("@/views/User.vue"),
      },{
        path: "person",
        name: "Person",
        component: () => import("@/views/Person.vue"),
      },
    ],
  },
  {
    path: "/login",
    name: "Login",
    component: () => import("@/views/Login.vue"),
  },{
    path: "/register",
    name: "Register",
    component: () => import("@/views/Register.vue"),
  },
  {
    path: "/about",
    name: "About",
    component: () => import("@/views/About.vue"),
  },
];

const router: Router = createRouter({
  history: createWebHistory(),
  routes,
});

router.beforeEach((to, from, next) => {
  localStorage.setItem("currentPathName", to.name as string)  // 设置当前的路由名称
  const Store = useStore()
  Store.setPath()
  next()

})
export default router;

个人信息修改

UserController

 	/**
     * 根据用户名查找用户
     *
     * @param username 用户名
     * @return Result
     * @author 石一歌
     * @date 2022/4/16 15:35
     */
    @GetMapping("/username/{username}")
    public Result<?> findByUsername(@PathVariable String username) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        if (StrUtil.isBlank(username)) {
            return Result.error(StatusEnum.CODE_400.getStatus(), StatusEnum.CODE_400.getName());
        }
        queryWrapper.eq("username", username);
        User one = userServiceImpl.getOne(queryWrapper);
        return ObjectUtil.isNotEmpty(one) ? Result.success(one) : Result.error();
    }

    /**
     * 获取用户信息
     *
     * @param userDTO 用户信息
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/22 14:42
     */
    @PostMapping("/userInfo")
    public Result<?> userInfo(@RequestBody UserDTO userDTO) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", userDTO.getUsername());
        queryWrapper.eq("password", userDTO.getPassword());
        User one;
        try {
            one = userServiceImpl.getOne(queryWrapper);
        } catch (Exception e) {
            throw new ServiceException(StatusEnum.CODE_500.getStatus(), StatusEnum.CODE_500.getName());
        }
        if (one != null) {
            BeanUtil.copyProperties(one, userDTO, true);
            return Result.success(userDTO);
        } else {
            throw new ServiceException(StatusEnum.CODE_600.getStatus(), "用户名或密码错误");
        }
    }

Person.vue

<script setup lang="ts">

import { reactive, ref, toRefs } from 'vue'
import request from '@/utils/request';
import { ElMessage, UploadProps } from 'element-plus';
import { Plus } from '@element-plus/icons-vue'
import { regionData, CodeToText } from 'element-china-area-data'

const form = reactive({
    username: '',
    nickname: '',
    password: '',
    age: null,
    sex: '未知',
    address: '',
    region: '',
    avatar: '',
    email: '',
    phone: '',
    account: '',
    role: '',
    id: null,
    version: null
})
const user = localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user") as string) : {}
const getUser = () => {
    request.get("/user/username/" + user.username).then(res => {
        if (res.status === 200) {
            form.id = res.data.id
            form.username = res.data.username
            form.nickname = res.data.nickname
            form.age = res.data.age
            form.sex = res.data.sex
            form.address = res.data.address
            form.region = res.data.region
            form.avatar = res.data.avatar
            form.email = res.data.email
            form.phone = res.data.phone
            form.account = res.data.account
            form.version = res.data.version
            form.password = res.data.password
            form.role = res.data.role
            regionChange(res.data.region)
        }
    })
}
getUser()
const save = () => {
    request.post("/user", form).then(res => {
        if (res.status === 200) {
            ElMessage.success('保存成功')
            getUser()
            refresh()
        } else {
            ElMessage.error("保存失败")
        }
    })
}
const handleAvatarSuccess: UploadProps['onSuccess'] = (res) => {
    form.avatar = res
}
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
    if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png' && rawFile.type !== 'image/webp' && rawFile.type !== 'image/gif') {
        ElMessage.error('头像只能是图片格式!')
        return false
    } else if (rawFile.size / 1024 / 1024 > 2) {
        ElMessage.error('头像必须小于2MB!')
        return false
    }
    return true
}
interface EmitType {
    (e: "refresh"): void
}
const emit = defineEmits<EmitType>();
const refresh = () => {
    const user = localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user") as string) : {}
    request.post("/user/userInfo", user).then(res => {
        if (res.status === 200) {
            localStorage.setItem("user", JSON.stringify(res.data))
            emit('refresh')
        } else {
            ElMessage.error(res.statusText)
        }
    })
}
let regionText = ref()
const regionChange = (arr: (string | number)[]) => {
    if (arr[1] != null && arr[2] != null) {
        regionText.value = CodeToText[arr[0]] + CodeToText[arr[1]] + CodeToText[arr[2]]
    } else {
        if (arr[1] != null) {
            regionText.value = CodeToText[arr[0]] + CodeToText[arr[1]]
        } else {
            regionText.value = CodeToText[arr[0]]
        }
    }
}
</script>

<template>
    <el-card style="width: 500px;">
        <el-form :model="form" label-width="120px" style="text-align: center">
            <el-form-item style="display: inline-block" label-width="0">
                <el-upload class="avatar-uploader" action="http://localhost:8080/files/upload" :show-file-list="false"
                    :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload"> <img v-if="form.avatar"
                        :src="form.avatar" class="avatar">
                    <el-icon v-else class="avatar-uploader-icon">
                        <Plus />
                    </el-icon>
                </el-upload>
            </el-form-item>
            <el-form-item label="用户名">
                <el-input v-model="form.username" style="width: 80%" disabled></el-input>
            </el-form-item>
            <el-form-item label="昵称">
                <el-input v-model="form.nickname" style="width: 80%"></el-input>
            </el-form-item>
            <el-form-item label="年龄">
                <el-input v-model="form.age" style="width: 80%"></el-input>
            </el-form-item>
            <el-form-item label="性别">
                <el-radio v-model="form.sex" label="男">男</el-radio>
                <el-radio v-model="form.sex" label="女">女</el-radio>
                <el-radio v-model="form.sex" label="未知">未知</el-radio>
            </el-form-item>
            <el-form-item label="地区">
                {{ regionText }}
                <el-cascader placeholder="请选择地区" size="small" :options="regionData" v-model="form.region"
                    @change='regionChange' style="width: 80%">
                </el-cascader>
            </el-form-item>
             <el-form-item label="详细地址">
                <el-input type="textarea" v-model="form.address" style="width: 80%"></el-input>
            </el-form-item>
            <el-form-item label="邮箱">
                <el-input v-model="form.email" style="width: 80%"></el-input>
            </el-form-item>
            <el-form-item label="手机号">
                <el-input v-model="form.phone" style="width: 80%"></el-input>
            </el-form-item>
            <el-form-item label="账户余额">
                <el-input v-model="form.account" style="width: 80%" disabled></el-input>
            </el-form-item>
        </el-form>
        <div style="text-align: center">
            <el-button type="primary" @click="save">确 定</el-button>
        </div>

    </el-card>

</template>



<style scoped>
.avatar-uploader .avatar {
    width: 178px;
    height: 178px;
    display: block;
}
</style>
<style>
.avatar-uploader .el-upload {
    border: 1px dashed var(--el-border-color);
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
    transition: var(--el-transition-duration-fast);
}

.avatar-uploader .el-upload:hover {
    border-color: #409EFF var(--el-color-primary);
}

.el-icon.avatar-uploader-icon {
    font-size: 28px;
    color: #8c939d;
    width: 178px;
    height: 178px;
    text-align: center;
}
</style>

修改密码

UserController

	 /**
     * 更新密码
     *
     * @param userPasswordDTO 密码dto
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 15:31
     */
    @PostMapping("/password")
    public Result<?> password(@RequestBody UserPasswordDTO userPasswordDTO) {
        return userServiceImpl.updatePassword(userPasswordDTO) ? Result.success() : Result.error();
    }

UserServiceImpl

  /**
     * 更新密码
     *
     * @param userPasswordDTO 密码dto
     * @return Boolean
     * @author 石一歌
     * @date 2022/5/7 15:30
     */
    @Override
    public Boolean updatePassword(UserPasswordDTO userPasswordDTO) {
        if (userMapper.updatePassword(userPasswordDTO) < 1) {
            throw new ServiceException(StatusEnum.CODE_600.getStatus(), "密码错误");
        } else {
            return true;
        }
    }

UserMapper

public interface UserMapper extends BaseMapper<User> {
    /**
     * 统计用户地址
     *
     * @return List<AddressDTO>
     * @author 石一歌
     * @date 2022/4/22 16:49
     */
    @Select("select count(id) count, address from sys_user GROUP BY address")
    List<AddressDTO> countAddress();

    /**
     * 更新密码
     *
     * @param userPasswordDTO 密码dto
     * @return int
     * @author 石一歌
     * @date 2022/5/7 14:54
     */
    @Update("update sys_user set password = #{newPassword} where username = #{username} and password = #{password}")
    int updatePassword(UserPasswordDTO userPasswordDTO);
}

Password.vue

<script setup lang="ts">
import { useStore } from '@/stores/store';
import request from '@/utils/request';
import { ElMessage } from 'element-plus';
import { reactive, ref } from 'vue'


const form = reactive({
    username: '',
    password: '',
    newPassword: '',
    confirmPassword: '',
})
const user = ref<any>(localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user") as string) : [])
const rules = reactive({
    password: [
        { required: true, message: '请输入原密码', trigger: 'blur' },
        { min: 3, message: '长度不少于3位', trigger: 'blur' }
    ],
    newPassword: [
        { required: true, message: '请输入新密码', trigger: 'blur' },
        { min: 3, message: '长度不少于3位', trigger: 'blur' }
    ],
    confirmPassword: [
        { required: true, message: '请输入密码', trigger: 'blur' },
        { min: 3, message: '长度不少于3位', trigger: 'blur' }
    ],
})

const update = async (formEl: { validate: (arg0: (valid: any) => false | undefined) => any; }) => {
    if (!formEl) return
    await formEl.validate((valid) => {
        if (valid) {
            if (form.newPassword !== form.confirmPassword) {
                ElMessage.error("新密码不相同")
                return false
            }
            form.username=user.value.username
            request.post("/user/password", form).then(res => {
                if (res.status === 200) {
                    ElMessage.success("修改成功")
                    useStore().logout();
                } else {
                    ElMessage.error(res.statusText)
                }
            })
        }
    })

}
const ruleFormRef = ref()
</script>
<template>
    <el-card style="width: 500px;">
        <el-form label-width="120px" size="small" :model="form" :rules="rules" ref="ruleFormRef">
            <el-form-item label="原密码" prop="password">
                <el-input v-model="form.password" autocomplete="off" show-password></el-input>
            </el-form-item>
            <el-form-item label="新密码" prop="newPassword">
                <el-input v-model="form.newPassword" autocomplete="off" show-password></el-input>
            </el-form-item>
            <el-form-item label="确认新密码" prop="confirmPassword">
                <el-input v-model="form.confirmPassword" autocomplete="off" show-password></el-input>
            </el-form-item>
            <el-form-item>
                <el-button type="primary" @click="update(ruleFormRef)">确 定</el-button>
            </el-form-item>
        </el-form>
    </el-card>
</template>



<style scoped>
.avatar-uploader {
    text-align: center;
    padding-bottom: 10px;
}

.avatar-uploader .el-upload {
    border: 1px dashed #d9d9d9;
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
}

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

.avatar-uploader-icon {
    font-size: 28px;
    color: #8c939d;
    width: 138px;
    height: 138px;
    line-height: 138px;
    text-align: center;
}

.avatar {
    width: 138px;
    height: 138px;
    display: block;
}
</style>

集成JWT

TokenUtils

@Component
public class TokenUtils {
    private static IUserService staticUserService;

    @Resource
    private IUserService userServiceImpl;

    /**
     * 生成令牌
     *
     * @param id       id
     * @param password 密码
     * @return String
     * @author 石一歌
     * @date 2022/4/17 21:49
     */
    public static String getToken(String id, String password) {
        Map<String, Object> payload = new HashMap<>(16);
        payload.put(JWTPayload.ISSUED_AT, DateUtil.now());
        payload.put(JWTPayload.EXPIRES_AT, DateUtil.offsetHour(DateUtil.date(), 2));
        payload.put(JWTPayload.NOT_BEFORE, DateUtil.now());
        payload.put("userId", id);
        return JWTUtil.createToken(payload, JWTSignerUtil.hs256(password.getBytes()));
    }

    /**
     * 获取用户
     *
     * @return User
     * @author 石一歌
     * @date 2022/4/17 22:58
     */
    public static User getCurrentUser() {
        try {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String token = request.getHeader("token");
            if (StrUtil.isNotBlank(token)) {
                Integer userId = Convert.toInt(JWTUtil.parseToken(token).getPayload("userId"));
                return staticUserService.getById(userId);
            }
        } catch (Exception e) {
            return null;
        }
        return null;
    }

    @PostConstruct
    public void setUserService() {
        staticUserService = userServiceImpl;
    }
}

UserServiceImpl

 	@Override
    public UserDTO login(UserDTO userDTO) {
        User one = getUserInfo(userDTO);
        if (one != null) {
            BeanUtil.copyProperties(one, userDTO, true);
            userDTO.setToken(TokenUtils.getToken(Convert.toStr(one.getId()), one.getUsername()));
            return userDTO;
        } else {
            throw new ServiceException(ConstantEnum.CODE_600.getStatus(), "用户名或密码错误");
        }
    }

JwtInterceptor

public class JwtInterceptor implements HandlerInterceptor {
    @Resource
    private IUserService userServiceImpl;

    /**
     * 配置规则
     *
     * @param request  请求
     * @param response 响应
     * @param handler  控制器
     * @return boolean
     * @author 石一歌
     * @date 2022/4/17 22:26
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("token");
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        if (StrUtil.isBlank(token)) {
            throw new ServiceException(ConstantEnum.CODE_401.getStatus(), "无token,请重新登录");
        }
        Integer userId;
        try {
            final JWT jwt = JWTUtil.parseToken(token);
            userId = Convert.toInt(jwt.getPayload("userId"));
        } catch (Exception j) {
            throw new ServiceException(ConstantEnum.CODE_401.getStatus(), "token验证失败,请重新登录");
        }
        // 根据token中的userid查询数据库
        User user = userServiceImpl.getById(userId);
        if (ObjectUtil.isNull(user)) {
            throw new ServiceException(ConstantEnum.CODE_401.getStatus(), "用户不存在,请重新登录");
        }
        // 用户密码加签验证 token
        try {
            JWTUtil.verify(token, JWTSignerUtil.hs256(user.getPassword().getBytes()));
        } catch (Exception e) {
            throw new ServiceException(ConstantEnum.CODE_401.getStatus(), "token验证失败,请重新登录");
        }
        return true;
    }

}

InterceptorConfig

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    /**
     * 配置拦截路径
     *
     * @param registry 登记处
     * @author 石一歌
     * @date 2022/4/17 22:25
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/user/login", "/user/register", "/**/export", "/**/import");
    }

    /**
     * 引入JwtInterceptor
     *
     * @return JwtInterceptor
     * @author 石一歌
     * @date 2022/4/17 22:24
     */
    @Bean
    public JwtInterceptor jwtInterceptor() {
        return new JwtInterceptor();
    }
}

request.ts

import router from '@/router';
import axios from "axios";
import { ElMessage } from "element-plus";
const request = axios.create({
  baseURL: "/api",
  timeout: 5000,
  headers: {
    "Content-Type": "application/json;charset=UTF-8;",
  },
});

// request 拦截器
request.interceptors.request.use(
  (config) => {
    const user = localStorage.getItem("user")
      ? JSON.parse(localStorage.getItem("user") as string)
      : {};
    if (user) {
      config!.headers!["token"] = user.token;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// response 拦截器
request.interceptors.response.use(
  (response) => {
    let res = response.data;
    // 如果是返回的文件
    if (response.request.responseType === "blob") {
      return res;
    }
    // 兼容服务端返回的字符串数据
    if (typeof res === "string") {
      res = res ? JSON.parse(res) : res;
    }
    if (res.status === 401) {
      ElMessage.error(res.statusText);
      router.push('/login')
    }
    return res;
  },
  (error) => {
    console.log("err" + error); // for debug
    return Promise.reject(error);
  }
);

export default request;

文件上传

Files

@TableName("sys_files")
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "Files对象", description = "文件实体")
public class Files implements Serializable {

    private static final long serialVersionUID = 1L;
    /**
     * id
     */
    @ApiModelProperty("id")
    @Alias("id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * 文件名称
     */
    @Alias("文件名称")
    @ApiModelProperty("文件名称")
    private String name;
    /**
     * 文件类型
     */
    @Alias("文件类型")
    @ApiModelProperty("文件类型")
    private String type;
    /**
     * 文件大小
     */
    @Alias("文件大小")
    @ApiModelProperty("文件大小(kb)")
    private Long size;
    /**
     * 下载链接
     */
    @Alias("下载链接")
    @ApiModelProperty("下载链接")
    private String url;
    /**
     * 文件MD5
     */
    @Alias("文件md5")
    @ApiModelProperty("文件md5")
    private String md5;
    /**
     * 是否禁用
     */
    @Alias("是否禁用")
    @ApiModelProperty("是否禁用")
    private Boolean enable;
    /**
     * 创建时间
     */
    @Alias("创建时间")
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime gmtCreate;
    /**
     * 更新时间
     */
    @Alias("更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @ApiModelProperty("更新时间")
    private LocalDateTime gmtModified;
    /**
     * 乐观锁
     */
    @Alias("乐观锁")
    @Version
    @ApiModelProperty("乐观锁")
    private Integer version;
    /**
     * 逻辑删除
     */
    @Alias("逻辑删除")
    @TableLogic
    @ApiModelProperty("逻辑删除")
    private Integer deleted;
}

FilesMapper

public interface FilesMapper extends BaseMapper<Files> {
}

IFilesService

public interface IFilesService extends IService<Files> {

}

FilesServiceImpl

@Service
public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements IFilesService {
}

FilesController

@RestController
@RequestMapping("/files")
public class FilesController {
    private static final String BASE_PATH = System.getProperty("user.dir") + "/files/";
    @Resource
    private IFilesService filesServiceImpl;

    /**
     * 文件上传
     *
     * @param file 文件
     * @return String
     * @author 石一歌
     * @date 2022/4/18 21:28
     */
    @PostMapping("/upload")
    public String upload(@RequestParam MultipartFile file) throws IOException {

        //获取基本信息
        String filename = file.getOriginalFilename();
        String type = FileUtil.getSuffix(filename);
        long size = file.getSize() / 1024;
        String uuid = IdUtil.fastSimpleUUID();
        String fileUUID = uuid + StrUtil.DOT + type;

        //判断文件是否存在
        File uploadFile = new File(BASE_PATH + fileUUID);
        File parentFile = uploadFile.getParentFile();
        if (!parentFile.exists()) {
            parentFile.mkdirs();
        }

        String url;
        // 对照文件的md5,文件重复则复用文件地址
        String md5 = SecureUtil.md5(file.getInputStream());
        Files dbFiles = getFileByMd5(md5);
        if (ObjectUtil.isNotNull(dbFiles)) {
            url = dbFiles.getUrl();
        } else {
            FileUtil.writeBytes(file.getBytes(), uploadFile);
            url = "http://localhost:8080/files/" + fileUUID;
        }

        // 存储数据库
        Files files = Files.builder().name(filename).type(type).size(size).url(url).md5(md5).build();
        filesServiceImpl.save(files);
        return url;
    }

    /**
     * 查询数据库MD5是否存在
     *
     * @param md5 文件MD5
     * @return Files
     * @author 石一歌
     * @date 2022/4/18 21:11
     */
    private Files getFileByMd5(String md5) {
        QueryWrapper<Files> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("md5", md5);
        List<Files> filesList = filesServiceImpl.list(queryWrapper);
        return CollUtil.isEmpty(filesList) ? null : filesList.get(0);
    }

    /**
     * 文件下载
     *
     * @param fileUUID 文件id
     * @param response 请求响应
     * @author 石一歌
     * @date 2022/4/18 22:05
     */
    @GetMapping("/{fileUUID}")
    public void download(@PathVariable String fileUUID, HttpServletResponse response) {
        OutputStream os;
        List<String> fileNames = FileUtil.listFileNames(BASE_PATH);
        String fileName = fileNames.stream().filter(name -> name.contains(fileUUID)).findAny().orElse("");

        List<Files> filesList = filesServiceImpl.list(Wrappers.<Files>lambdaQuery().like(Files::getUrl, fileUUID));
        for (Files files : filesList) {
            if (!files.getEnable()) {
                return;
            }
        }
        //设置禁用
        try {
            if (StrUtil.isNotEmpty(fileName)) {
                response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
                response.setContentType("application/octet-stream");
                byte[] bytes = FileUtil.readBytes(BASE_PATH + fileName);
                os = response.getOutputStream();
                os.write(bytes);
                os.flush();
                os.close();
            }
        } catch (Exception e) {
            System.out.println("文件下载失败");
        }
    }

    /**
     * 导出
     *
     * @param response 返回值
     * @author 石一歌
     * @date 2022/4/15 0:12
     */
    @GetMapping("/export")
    public void exportExcel(HttpServletResponse response) throws IOException {
        // 查询所有数据
        List<Files> list = filesServiceImpl.list();
        // 在内存操作,写出到浏览器
        ExcelWriter writer = ExcelUtil.getWriter(true);
        // 一次性写出list内的对象到excel
        writer.write(list, true);
        // 设置浏览器响应格式
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        String fileName = URLEncoder.encode("文件信息", "UTF-8");
        response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
        ServletOutputStream out = response.getOutputStream();
        writer.flush(out, true);
        writer.close();
        IoUtil.close(System.out);
    }

    /**
     * 导入
     *
     * @param file 文件
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/15 0:12
     */
    @PostMapping("/import")
    public Result<?> importExcel(MultipartFile file) throws IOException {
        InputStream inputStream = file.getInputStream();
        ExcelReader reader = ExcelUtil.getReader(inputStream);
        List<Files> list = reader.readAll(Files.class);
        return filesServiceImpl.saveBatch(list) ? Result.success() : Result.error();
    }

    /**
     * 更新文件
     *
     * @param files 文件
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/18 22:04
     */
    @PostMapping("/update")
    public Result<?> update(@RequestBody Files files) {
        return Result.success(filesServiceImpl.updateById(files));
    }

    /**
     * 删除文件
     *
     * @param id id
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/18 22:04
     */
    @DeleteMapping("/{id}")
    public Result<?> delete(@PathVariable Integer id) {
        return filesServiceImpl.removeById(id) ? Result.success() : Result.error();
    }

    /**
     * 批量删除文件
     *
     * @param ids id列表
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/18 22:03
     */
    @PostMapping("/del/batch")
    public Result<?> deleteBatch(@RequestBody List<Integer> ids) {
        return filesServiceImpl.removeByIds(ids) ? Result.success() : Result.error();
    }

    /**
     * 分页查询接口
     *
     * @param pageNum  分页页码
     * @param pageSize 分页大小
     * @param name     名称
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/18 22:03
     */
    @GetMapping("/page")
    public Result<?> findPage(@RequestParam Integer pageNum,
                              @RequestParam Integer pageSize,
                              @RequestParam(defaultValue = "") String name) {
        IPage<Files> page = new Page<>(pageNum, pageSize);
        QueryWrapper<Files> queryWrapper = new QueryWrapper<>();
        if (StrUtil.isNotEmpty(name)) {
            queryWrapper.like("name", name);
        }
        return ObjectUtil.isNotEmpty(filesServiceImpl.page(page, queryWrapper)) ? Result.success(filesServiceImpl.page(page, queryWrapper)) : Result.error();
    }
}

Files.vue

<script setup lang="ts">

import { ref } from 'vue'
import { Remove,Bottom,Top, CirclePlus, Search } from '@element-plus/icons-vue';
import request from '@/utils/request';
import { ElMessage } from 'element-plus';

const tableData = ref([])
const total = ref<number>(0)
const pageNum = ref<number>(1)
const pageSize = ref<number>(5)
const name = ref<string>('')
const multipleSelection = ref<any>([])

const headerBg = ref('headerBg')
const load = () => {
    request.get("/files/page", {
        params: {
            pageNum: pageNum.value,
            pageSize: pageSize.value,
            name: name.value,
        }
    }).then(res => {
        tableData.value = res.data.records
        total.value = res.data.total
    })
}
load()
const handleSelectionChange = (val: never[]) => {
    multipleSelection.value = val
}
const del = (id: number) => {
    request.delete("/files/" + id).then(res => {
        if (res.status === 200) {
            ElMessage.success("删除成功")
            load()
        } else {
            ElMessage.error("删除失败")
        }
    })
}
const delBatch = () => {
    let ids: number[] = []
    multipleSelection.value.forEach((element: any) => {
        ids.push(element.id)
    });
    request.post("/files/del/batch", ids).then(res => {
        if (res.status === 200) {
            ElMessage.success("批量删除成功")
            load()
        } else {
            ElMessage.error("批量删除失败")
        }
    })
}
const reset = () => {
    name.value = ""
    load()
}
const handleSizeChange = (size: number) => {
    pageSize.value = size
    load()
}
const handleCurrentChange = (num: number) => {
    pageNum.value = num
    load()
}
const dataFormat = (data: string) => {
    if (data === null) return null
    else
        return data.split('T').join(' ')
}
const exp = () => {
    window.open("http://localhost:8080/files/export")
}
const handleExcelImportSuccess = () => {
    ElMessage.success("导入成功")
}
const changeEnable = (row: any) => {
    row.gmtModified = null
    row.gmtCreate = null
    request.post("/files/update", row).then(res => {
        if (res.status === 200) {
            ElMessage.success("操作成功")
            load()
        }
    })
}
const handleFileUploadSuccess = () => {
    ElMessage.success("上传成功")
    load()
}
const download = (url: string | URL | undefined) => {
    open(url)
}
</script>

<template>
    <el-main>
        <div style="margin: 10px 0">
            <el-input style="width: 200px" placeholder="请输入名称" :suffix-icon="Search" class="ml-5" v-model="name">
            </el-input>
            <el-button class="ml-5" type="primary" @click="load">搜索</el-button>
            <el-button class="ml-5" type="warning" @click="reset">清空</el-button>
        </div>

        <div style="margin: 10px 0; position: absolut;">
            <el-upload action="http://localhost:8080/files/upload" :show-file-list="false"
                :on-success="handleFileUploadSuccess" style="display: inline-block; margin: 0 10px; position: relative;top:2px">
                <el-button type="primary" class="ml-5">上传文件
                    <circle-plus style="width: 1em; height: 1em; margin-right: 8px" />
                </el-button>
            </el-upload>
            <el-popconfirm title="确定删除吗?" class="ml-5" confirm-button-text='确定' cancel-button-text='我再想想'
                icon-color="red" @confirm="delBatch">
                <template #reference>
                    <el-button type="danger">
                        批量删除
                        <remove style="width: 1em; height: 1em; margin-right: 8px" />
                    </el-button>
                </template>
            </el-popconfirm>
            <el-upload action="http://localhost:8080/files/import" :show-file-list="false" accept="xlsx"
                :on-success="handleExcelImportSuccess"
                style="display: inline-block; margin: 0 10px; position: relative;top:2px">
                <el-button type="primary">
                    导入
                    <top style="width: 1em; height: 1em; margin-right: 8px" />
                </el-button>
            </el-upload>

            <el-button type="primary" @click="exp">
                导出
                <bottom style="width: 1em; height: 1em; margin-right: 8px" />
            </el-button>
        </div>

        <el-table :data="tableData" border stripe :header-cell-class-name="headerBg"
            @selection-change="handleSelectionChange">
            <el-table-column type="selection" width="55" fixed></el-table-column>
            <el-table-column prop="id" label="ID" width="55"></el-table-column>
            <el-table-column prop="name" label="文件名称"></el-table-column>
            <el-table-column prop="type" label="文件类型"></el-table-column>
            <el-table-column prop="size" label="文件大小(kb)"></el-table-column>
            <el-table-column prop="gmtCreate" label="创建时间" width="200px">
                <template v-slot="scope">
                    {{ dataFormat(scope.row.gmtCreate) }}
                </template>
            </el-table-column>
            <el-table-column prop="gmtModified" label="修改时间" width="200px">
                <template v-slot="scope">
                    {{ dataFormat(scope.row.gmtModified) }}
                </template>
            </el-table-column>
            <el-table-column prop="version" label="修改次数">
            </el-table-column>
            <el-table-column label="下载">
                <template v-slot="scope">
                    <el-button type="primary" @click="download(scope.row.url)">下载</el-button>
                </template>
            </el-table-column>
            <el-table-column label="启用">
                <template v-slot="scope">
                    <el-switch v-model="scope.row.enable" active-color="#13ce66" inactive-color="#ccc"
                        @change="changeEnable(scope.row)"></el-switch>
                </template>
            </el-table-column>
            <el-table-column label="操作" width="100" align="center" fixed="right">
                <template v-slot="scope">
                    <el-popconfirm title="确定删除吗?" class="ml-5" confirm-button-text='确定' cancel-button-text='我再想想'
                        icon-color="red" @confirm="del(scope.row.id)">
                        <template #reference>
                            <el-button type="danger">
                                删除
                                <remove style="width: 1em; height: 1em; margin-right: 8px" />
                            </el-button>
                        </template>
                    </el-popconfirm>
                </template>
            </el-table-column>
        </el-table>
        <div style="padding: 10px 0">
            <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="pageNum"
                :page-sizes="[5, 10, 15, 20]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper"
                :total="total"></el-pagination>
        </div>

    </el-main>
</template>



<style scoped>
</style>

Person.vue

<script setup lang="ts">

import { reactive, ref, toRefs } from 'vue'
import request from '@/utils/request';
import { ElMessage, UploadProps } from 'element-plus';
import { Plus } from '@element-plus/icons-vue'
import { regionData, CodeToText } from 'element-china-area-data'

const form = reactive({
    username: '',
    nickname: '',
    password: '',
    age: null,
    sex: '未知',
    address: '',
    avatar: '',
    email: '',
    phone: '',
    account: '',
    role: '',
    id: null,
    version: null
})
const user = localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user") as string) : {}
const getUser = () => {
    request.get("/user/username/" + user.username).then(res => {
        if (res.status === 200) {
            form.id = res.data.id
            form.username = res.data.username
            form.nickname = res.data.nickname
            form.age = res.data.age
            form.sex = res.data.sex
            form.address = res.data.address
            form.avatar = res.data.avatar
            form.email = res.data.email
            form.phone = res.data.phone
            form.account = res.data.account
            form.version = res.data.version
            form.password = res.data.password
            form.role = res.data.role
            addressChange(res.data.address)
        }
    })
}
getUser()
const save = () => {
    request.post("/user", form).then(res => {
        if (res.status === 200) {
            ElMessage.success('保存成功')
            getUser()
            refresh()
        } else {
            ElMessage.error("保存失败")
        }
    })
}
const handleAvatarSuccess: UploadProps['onSuccess'] = (res) => {
    form.avatar = res
}
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
    if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png' && rawFile.type !== 'image/webp' && rawFile.type !== 'image/gif') {
        ElMessage.error('头像只能是图片格式!')
        return false
    } else if (rawFile.size / 1024 / 1024 > 2) {
        ElMessage.error('头像必须小于2MB!')
        return false
    }
    return true
}
interface EmitType {
    (e: "refresh"): void
}
const emit = defineEmits<EmitType>();
const refresh = () => {
    const user = localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user") as string) : {}
    request.post("/user/userInfo", user).then(res => {
        if (res.status === 200) {
            localStorage.setItem("user", JSON.stringify(res.data))
            emit('refresh')
        } else {
            ElMessage.error(res.statusText)
        }
    })
}
let addressText = ref()
const addressChange = (arr: (string | number)[]) => {
    if (arr[1] != null && arr[2] != null) {
        addressText.value = CodeToText[arr[0]] + CodeToText[arr[1]] + CodeToText[arr[2]]
    } else {
        if (arr[1] != null) {
            addressText.value = CodeToText[arr[0]] + CodeToText[arr[1]]
        } else {
            addressText.value = CodeToText[arr[0]]
        }
    }
}
</script>

<template>
    <el-card style="width: 500px;">
        <el-form :model="form" label-width="120px" style="text-align: center">
            <el-form-item style="display: inline-block" label-width="0">
                <el-upload class="avatar-uploader" action="http://localhost:8080/files/upload" :show-file-list="false"
                    :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload"> <img v-if="form.avatar"
                        :src="form.avatar" class="avatar">
                    <el-icon v-else class="avatar-uploader-icon">
                        <Plus />
                    </el-icon>
                </el-upload>
            </el-form-item>
            <el-form-item label="用户名">
                <el-input v-model="form.username" style="width: 80%" disabled></el-input>
            </el-form-item>
            <el-form-item label="昵称">
                <el-input v-model="form.nickname" style="width: 80%"></el-input>
            </el-form-item>
            <el-form-item label="年龄">
                <el-input v-model="form.age" style="width: 80%"></el-input>
            </el-form-item>
            <el-form-item label="性别">
                <el-radio v-model="form.sex" label="男">男</el-radio>
                <el-radio v-model="form.sex" label="女">女</el-radio>
                <el-radio v-model="form.sex" label="未知">未知</el-radio>
            </el-form-item>
            <el-form-item label="地址">
                <!-- <el-input type="textarea" v-model="form.address" style="width: 80%"></el-input> -->
                {{ addressText }}
                <el-cascader placeholder="请选择地区" size="small" :options="regionData" v-model="form.address"
                    @change='addressChange' style="width: 80%">
                </el-cascader>
            </el-form-item>
            <el-form-item label="邮箱">
                <el-input v-model="form.email" style="width: 80%"></el-input>
            </el-form-item>
            <el-form-item label="手机号">
                <el-input v-model="form.phone" style="width: 80%"></el-input>
            </el-form-item>
            <el-form-item label="账户余额">
                <el-input v-model="form.account" style="width: 80%" disabled></el-input>
            </el-form-item>
        </el-form>
        <div style="text-align: center">
            <el-button type="primary" @click="save">确 定</el-button>
        </div>

    </el-card>

</template>



<style scoped>
.avatar-uploader .avatar {
    width: 178px;
    height: 178px;
    display: block;
}
</style>
<style>
.avatar-uploader .el-upload {
    border: 1px dashed var(--el-border-color);
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
    transition: var(--el-transition-duration-fast);
}

.avatar-uploader .el-upload:hover {
    border-color: #409EFF var(--el-color-primary);
}

.el-icon.avatar-uploader-icon {
    font-size: 28px;
    color: #8c939d;
    width: 178px;
    height: 178px;
    text-align: center;
}
</style>

集成Echarts

AddressDTO

@Data
@ToString
public class AddressDTO {
    @Alias("地址")
    private String address;
    @Alias("人数")
    private Integer count;
}

EchartsController

@RestController
@RequestMapping("/echarts")
public class EchartsController {
    @Resource
    private IUserService userServiceImpl;
    @Resource
    private IFilesService filesServiceImpl;
    @Resource
    private ILogService logServiceImpl;

    /**
     * 统计用户地址
     *
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/22 16:53
     */
    @GetMapping("/detailAddress")
    public Result<?> detailAddressCount() {
        return userServiceImpl.countAddress().size() != 0 ? Result.success(userServiceImpl.countAddress()) : Result.error();
    }

    @GetMapping("/IPAddress")
    public Result<?> ipAddressCount() {
        List<IpAddressDTO> ipAddress = logServiceImpl.countIpAddress();
        List<AddressDTO> address = new ArrayList<>();
        if (ipAddress.size() != 0) {
            ipAddress.forEach(v -> address.add(AddressDTO.builder().address(IpUtils.getAddressByDatabase(v.getIp())).count(v.getCount()).build()));
            return Result.success(address);
        } else {
            return Result.error();
        }
    }


    /**
     * 用户数量统计
     *
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/22 17:19
     */
    @GetMapping("/user")
    public Result<?> userCount() {
        return Result.success(userServiceImpl.count());
    }

    /**
     * 文件数量统计
     *
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/22 17:19
     */
    @GetMapping("/files")
    public Result<?> filesCount() {
        return Result.success(filesServiceImpl.count());
    }

    /**
     * 日志数量统计
     *
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/12 14:32
     */
    @GetMapping("/log")
    public Result<?> logCount() {
        return Result.success(logServiceImpl.count());
    }


}

Dashbord.vue

<script setup lang="ts">
import { onMounted, ref } from "vue";
import * as echarts from "echarts/core";
import {
  TitleComponent,
  ToolboxComponent,
  TitleComponentOption,
  TooltipComponent,
  TooltipComponentOption,
  LegendComponent,
  LegendComponentOption,
  ToolboxComponentOption,
} from "echarts/components";
import { PieChart, PieSeriesOption } from "echarts/charts";
import { LabelLayout } from "echarts/features";
import { CanvasRenderer } from "echarts/renderers";
import request from "@/utils/request";
import { Files, User } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";

const userCount = ref<number>();
const filesCount = ref<number>();
const userOnlineCount = ref<number>();
const logCount = ref<number>();
const getInfo = () => {
  request.get("/echarts/user").then((res) => {
    if (res.status === 200) {
      userCount.value = res.data;
    } else {
      ElMessage.error("加载失败");
    }
  });
  request.get("/echarts/files").then((res) => {
    if (res.status === 200) {
      filesCount.value = res.data;
    } else {
      ElMessage.error("加载失败");
    }
  });
  request.get("/user/online").then((res) => {
    if (res.status === 200) {
      userOnlineCount.value = res.data;
    } else {
      ElMessage.error("加载失败");
    }
  });
  request.get("/echarts/log").then((res) => {
    if (res.status === 200) {
      logCount.value = res.data;
    } else {
      ElMessage.error("加载失败");
    }
  });
};
getInfo();

onMounted(() => {
  echarts.use([
    TitleComponent,
    TooltipComponent,
    LegendComponent,
    PieChart,
    CanvasRenderer,
    LabelLayout,
  ]);

  type EChartsOption = echarts.ComposeOption<
    | TitleComponentOption
    | TooltipComponentOption
    | LegendComponentOption
    | PieSeriesOption
  >;

  var chartDom = document.getElementById("pie")!;
  var myChart = echarts.init(chartDom);
  var option: EChartsOption;

  option = {
    title: {
      text: "用户来源分析",
      subtext: "仅供参考",
      left: "center",
    },
    tooltip: {
      trigger: "item",
    },
    legend: {
      orient: "horizontal",
      bottom: "bottom",
    },
    series: [
      {
        name: "用户比例",
        type: "pie",
        radius: "70%",
        center: ["55%", "50%"],
        data: [],
        emphasis: {
          itemStyle: {
            shadowBlur: 10,
            shadowOffsetX: 0,
            shadowColor: "rgba(0, 0, 0, 0.5)",
          },
        },
      },
    ],
  };

  request.get("/echarts/detailAddress").then((res) => {
    if (res.status === 200) {
      res.data.forEach((item: { address: any; count: any }) => {
        option!.series![0].data.push({ name: item.address, value: item.count });
      });
      // 绘制图表
      option && myChart.setOption(option);
    } else {
      ElMessage.error(res.statusText);
    }
  });

  echarts.use([
    ToolboxComponent,
    LegendComponent,
    PieChart,
    CanvasRenderer,
    LabelLayout,
  ]);

  type EChartsOptionTwo = echarts.ComposeOption<
    ToolboxComponentOption | LegendComponentOption | PieSeriesOption
  >;

  var chartDomTwo = document.getElementById("pieTwo")!;
  var myChartTwo = echarts.init(chartDomTwo);
  var optionTwo: EChartsOptionTwo;

  optionTwo = {
    title: { text: "用户登录ip分析", subtext: "仅供参考", left: "center" },
    legend: {
      top: "bottom",
    },
    toolbox: {
      show: true,
      feature: {
        mark: { show: true },
        dataView: { show: true, readOnly: false },
        restore: { show: true },
        saveAsImage: { show: true },
      },
    },
    series: [
      {
        name: "ceshi",
        type: "pie",
        radius: [50, 150],
        center: ["65%", "50%"],
        roseType: "area",
        itemStyle: {
          borderRadius: 8,
        },
        data: [],
      },
    ],
  };

  request.get("/echarts/IPAddress").then((res) => {
    if (res.status === 200) {
      res.data.forEach((item: { address: any; count: any }) => {
        optionTwo!.series![0].data.push({
          name: item.address,
          value: item.count,
        });
      });
      // 绘制图表
      myChartTwo.setOption(optionTwo);
    } else {
      ElMessage.error(res.statusText);
    }
  });
});
</script>

<template>
  <div>
    <el-row :gutter="10" style="margin-bottom: 60px">
      <el-col :span="6">
        <el-card style="color: #409eff">
          <div>
            <user style="width: 1em; height: 1em; margin-right: 8px" /> 用户总数
          </div>
          <div style="padding: 10px 0; text-align: center; font-weight: bold">
            {{ userCount }}
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card style="color: #67c23a">
          <div>
            <user style="width: 1em; height: 1em; margin-right: 8px" /> 在线人数
          </div>
          <div style="padding: 10px 0; text-align: center; font-weight: bold">
            {{ userOnlineCount }}
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card style="color: #f56c6c">
          <div>
            <files style="width: 1em; height: 1em; margin-right: 8px" />
            文件总数
          </div>
          <div style="padding: 10px 0; text-align: center; font-weight: bold">
            {{ filesCount }}
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card style="color: #e6a23c">
          <div>
            <files style="width: 1em; height: 1em; margin-right: 8px" />
            日志总数
          </div>
          <div style="padding: 10px 0; text-align: center; font-weight: bold">
            {{ logCount }}
          </div>
        </el-card>
      </el-col>
    </el-row>
    <el-row>
      <el-col :span="12">
        <div id="pie" style="width: 500px; height: 400px"></div>
      </el-col>

      <el-col :span="12" >
        <div id="pieTwo" style="width: 500px; height: 400px"></div
      ></el-col>
    </el-row>
  </div>
</template>

<style scoped></style>

代码生成器

public class CodeGenerator {

    private static final String BASE_FILE_PATH = System.getProperty("user.dir");
    private static final String URL = "jdbc:mysql://localhost:3306/springboot_vite?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2b8";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "root";

    public static void main(String[] args) {
        generate();
    }

    private static void generate() {
        FastAutoGenerator.create(URL, USERNAME, PASSWORD)
                .globalConfig(builder -> {
                    builder.author("石一歌") // 设置作者
                            .enableSwagger() // 开启 swagger 模式
                            .disableOpenDir()
                            .outputDir(BASE_FILE_PATH + "\\src\\main\\java\\"); // 指定输出目录
                })
                .packageConfig(builder -> {
                    builder.parent("com.nuc") // 设置父包名
                            .moduleName(null) // 设置父包模块名
                            .pathInfo(Collections.singletonMap(OutputFile.xml, BASE_FILE_PATH + "\\src\\main\\resources\\mapper\\"));
                })
                .strategyConfig(builder -> {
                    builder.entityBuilder()
                            .enableLombok()
                            .versionColumnName("version")
                            .versionPropertyName("version")
                            .logicDeleteColumnName("deleted")
                            .logicDeletePropertyName("deleted")
                            .addTableFills(new Column("gmt_create", FieldFill.INSERT))
                            .addTableFills(new Property("gmtModified", FieldFill.INSERT_UPDATE));
                    builder.controllerBuilder().enableHyphenStyle()  // 开启驼峰转连字符
                            .enableRestStyle();  // 开启生成@RestController 控制器
                    builder.addInclude("sys_user_role") // 设置需要生成的表名
                            .addTablePrefix("t_", "sys_"); // 设置过滤表前缀
                })
                .templateEngine(new VelocityTemplateEngine())
                .execute();
    }
}

权限系统

UserRoleController

@RestController
@RequestMapping("/userRole")
public class UserRoleController {

    @Resource
    private IUserRoleService userRoleServiceImpl;

    /**
     * 设置用户角色
     *
     * @param userId  用户id
     * @param roleIds 角色数组
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 14:41
     */
    @PostMapping("/{userId}")
    public Result<?> setUserRole(@PathVariable Integer userId, @RequestBody List<Integer> roleIds) {
        return userRoleServiceImpl.setUserRole(userId, roleIds) ? Result.success() : Result.error();
    }

    /**
     * 获取用户角色
     *
     * @param userId 用户id
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 14:42
     */
    @GetMapping("/{userId}")
    public Result<?> getUserRole(@PathVariable Integer userId) {
        List<UserRole> userRole = userRoleServiceImpl.getUserRole(userId);
        ArrayList<Object> list = new ArrayList<>();
        userRole.forEach(ur -> list.add(ur.getRoleId()));
        return CollUtil.isNotEmpty(list) ? Result.success(list) : Result.error();
    }
}

UserRoleServiceImpl

@Service
public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper, UserRole> implements IUserRoleService {
    @Resource
    private UserRoleMapper userRoleMapper;

    /**
     * 设置用户角色
     *
     * @param userId  用户id
     * @param roleIds 角色数组
     * @return boolean
     * @author 石一歌
     * @date 2022/5/7 14:43
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean setUserRole(Integer userId, List<Integer> roleIds) {
        QueryWrapper<UserRole> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_id", userId);
        userRoleMapper.delete(queryWrapper);
        for (Integer roleId : roleIds) {
            UserRole userRole = UserRole.builder().userId(userId).roleId(roleId).build();
            userRoleMapper.insert(userRole);
        }
        return true;
    }

    /**
     * 获取用户角色
     *
     * @param userId 用户id
     * @return List<UserRole>
     * @author 石一歌
     * @date 2022/5/7 14:44
     */
    @Override
    public List<UserRole> getUserRole(Integer userId) {
        QueryWrapper<UserRole> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_id", userId);
        return userRoleMapper.selectList(queryWrapper);
    }
}

RoleController

@RestController
@RequestMapping("/role")
public class RoleController {

    @Resource
    private IRoleService roleServiceImpl;

    /**
     * 新增或修改
     *
     * @param role 角色
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 14:45
     */
    @PostMapping
    public Result<?> save(@RequestBody Role role) {
        return roleServiceImpl.saveOrUpdate(role) ? Result.success() : Result.error();
    }

    /**
     * 删除角色
     *
     * @param id 角色id
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 14:45
     */
    @DeleteMapping("/{id}")
    public Result<?> delete(@PathVariable Integer id) {
        return roleServiceImpl.removeById(id) ? Result.success() : Result.error();
    }

    /**
     * 批量删除角色
     *
     * @param ids id数组
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 14:45
     */
    @PostMapping("/del/batch")
    public Result<?> deleteBatch(@RequestBody List<Integer> ids) {
        return roleServiceImpl.removeByIds(ids) ? Result.success() : Result.error();
    }

    /**
     * 查找全部角色
     *
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 14:45
     */
    @GetMapping
    public Result<?> findAll() {
        return CollUtil.isNotEmpty(roleServiceImpl.list()) ? Result.success(roleServiceImpl.list()) : Result.error();
    }

    /**
     * 根据id查找
     *
     * @param id 角色id
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 14:46
     */
    @GetMapping("/{id}")
    public Result<?> findOne(@PathVariable Integer id) {
        return ObjectUtil.isNotEmpty(roleServiceImpl.getById(id)) ? Result.success(roleServiceImpl.getById(id)) : Result.error();
    }

    /**
     * 分页查询
     *
     * @param pageNum  分页页码
     * @param pageSize 分页大小
     * @param name     名字
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 14:46
     */
    @GetMapping("/page")
    public Result<?> findPage(@RequestParam Integer pageNum,
                              @RequestParam Integer pageSize,
                              @RequestParam String name) {
        IPage<Role> page = new Page<>(pageNum, pageSize);
        QueryWrapper<Role> queryWrapper = new QueryWrapper<>();
        if (StrUtil.isNotBlank(name)) {
            queryWrapper.like("name", name);
        }
        queryWrapper.orderByDesc("id");
        return ObjectUtil.isNotEmpty(roleServiceImpl.page(page, queryWrapper)) ? Result.success(roleServiceImpl.page(page, queryWrapper)) : Result.error();
    }

    /**
     * 导入
     *
     * @param response 响应
     * @author 石一歌
     * @date 2022/5/7 14:46
     */
    @GetMapping("/export")
    public void exportExcel(HttpServletResponse response) throws IOException {
        List<Role> list = roleServiceImpl.list();
        ExcelWriter writer = ExcelUtil.getWriter(true);
        writer.write(list, true);
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        String fileName = URLEncoder.encode("xx信息", "UTF-8");
        response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
        ServletOutputStream out = response.getOutputStream();
        writer.flush(out, true);
        writer.close();
        IoUtil.close(System.out);
    }

    /**
     * 导入
     *
     * @param file 文件
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 14:46
     */
    @PostMapping("/import")
    public Result<?> importExcel(MultipartFile file) throws IOException {
        InputStream inputStream = file.getInputStream();
        ExcelReader reader = ExcelUtil.getReader(inputStream);
        List<Role> list = reader.readAll(Role.class);
        return roleServiceImpl.saveBatch(list) ? Result.success() : Result.error();
    }

}

RoleMenuController

@RestController
@RequestMapping("/roleMenu")
public class RoleMenuController {

    @Resource
    private IRoleMenuService roleMenuServiceImpl;

    /**
     * 设置角色菜单
     *
     * @param roleId  角色id
     * @param menuIds 菜单数组
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 14:51
     */
    @PostMapping("/{roleId}")
    public Result<?> setRoleMenu(@PathVariable Integer roleId, @RequestBody List<Integer> menuIds) {
        return roleMenuServiceImpl.setRoleMenu(roleId, menuIds) ? Result.success() : Result.error();
    }

    /**
     * 获取角色菜单
     *
     * @param roleId 角色id
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 14:51
     */
    @GetMapping("/{roleId}")
    public Result<?> getRoleMenu(@PathVariable Integer roleId) {
        List<RoleMenu> roleMenu = roleMenuServiceImpl.getRoleMenu(roleId);
        ArrayList<Object> list = new ArrayList<>();
        roleMenu.forEach(rm -> list.add(rm.getMenuId()));
        return CollUtil.isNotEmpty(list) ? Result.success(list) : Result.error();
    }
}

RoleMenuServiceImpl

@Service
public class RoleMenuServiceImpl extends ServiceImpl<RoleMenuMapper, RoleMenu> implements IRoleMenuService {

    @Resource
    private RoleMenuMapper roleMenuMapper;

    @Resource
    private MenuMapper menuMapper;

    /**
     * 设置角色菜单
     *
     * @param roleId  角色id
     * @param menuIds 菜单数组
     * @return boolean
     * @author 石一歌
     * @date 2022/5/7 14:55
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean setRoleMenu(Integer roleId, List<Integer> menuIds) {
        QueryWrapper<RoleMenu> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("role_id", roleId);
        roleMenuMapper.delete(queryWrapper);
        // 再把前端传过来的菜单id数组绑定到当前的这个角色id上去
        List<Integer> menuIdsCopy = CollUtil.newArrayList(menuIds);
        for (Integer menuId : menuIds) {
            Menu menu = menuMapper.selectById(menuId);
            //二级菜单 并且传过来的menuId数组里面没有它的父级id
            if (menu.getPid() != null && !menuIdsCopy.contains(menu.getPid())) {
                // 补全父级id
                RoleMenu roleMenu = RoleMenu.builder().roleId(roleId).menuId(menu.getPid()).build();
                roleMenuMapper.insert(roleMenu);
                menuIdsCopy.add(menu.getPid());
            }
            RoleMenu roleMenu = RoleMenu.builder().roleId(roleId).menuId(menuId).build();
            roleMenuMapper.insert(roleMenu);
        }
        return true;
    }

    /**
     * 获取角色菜单
     *
     * @param roleId 角色id
     * @return List<RoleMenu>
     * @author 石一歌
     * @date 2022/5/7 14:55
     */
    @Override
    public List<RoleMenu> getRoleMenu(Integer roleId) {
        QueryWrapper<RoleMenu> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("role_id", roleId);
        return roleMenuMapper.selectList(queryWrapper);
    }
}
@RestController
@RequestMapping("/menu")
public class MenuController {

    @Resource
    private IMenuService menuServiceImpl;

    /**
     * 新增或修改
     *
     * @param menu 菜单
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 15:01
     */
    @PostMapping
    public Result<?> save(@RequestBody Menu menu) {
        return menuServiceImpl.saveOrUpdate(menu) ? Result.success() : Result.error();
    }

    /**
     * 删除菜单
     *
     * @param id 菜单id
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 15:01
     */
    @DeleteMapping("/{id}")
    public Result<?> delete(@PathVariable Integer id) {
        return menuServiceImpl.removeById(id) ? Result.success() : Result.error();
    }

    /**
     * 批量删除菜单
     *
     * @param ids 菜单数组
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 15:01
     */
    @PostMapping("/del/batch")
    public Result<?> deleteBatch(@RequestBody List<Integer> ids) {
        return menuServiceImpl.removeByIds(ids) ? Result.success() : Result.error();
    }

    /**
     * 查找所有菜单id
     *
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 15:02
     */
    @GetMapping("/ids")
    public Result<?> findAllIds() {
        List<Integer> menuIds = menuServiceImpl.list().stream().map(Menu::getId).collect(Collectors.toList());
        return CollUtil.isNotEmpty(menuIds) ? Result.success(menuIds) : Result.error();
    }

    /**
     * 查询所有菜单
     *
     * @param name 名称
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 15:02
     */
    @GetMapping
    public Result<?> findAll(@RequestParam(defaultValue = "") String name) {
        List<Menu> menus = menuServiceImpl.findMenus(name);
        return CollUtil.isNotEmpty(menus) ? Result.success(menus) : Result.error();
    }

    /**
     * 根据id查询
     *
     * @param id id
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 15:02
     */
    @GetMapping("/{id}")
    public Result<?> findOne(@PathVariable Integer id) {
        return ObjectUtil.isNotEmpty(menuServiceImpl.getById(id)) ? Result.success(menuServiceImpl.getById(id)) : Result.error();
    }

    /**
     * 分页查询
     *
     * @param pageNum  分页页码
     * @param pageSize 分页大小
     * @param name     名称
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 15:02
     */
    @GetMapping("/page")
    public Result<?> findPage(@RequestParam Integer pageNum,
                              @RequestParam Integer pageSize,
                              @RequestParam String name) {
        IPage<Menu> page = new Page<>(pageNum, pageSize);
        QueryWrapper<Menu> queryWrapper = new QueryWrapper<>();
        if (StrUtil.isNotBlank(name)) {
            queryWrapper.like("name", name);
        }
        queryWrapper.orderByDesc("id");
        return ObjectUtil.isNotEmpty(menuServiceImpl.page(page, queryWrapper)) ? Result.success(menuServiceImpl.page(page, queryWrapper)) : Result.error();
    }

    /**
     * 导出
     *
     * @param response 响应
     * @author 石一歌
     * @date 2022/5/7 15:03
     */
    @GetMapping("/export")
    public void exportExcel(HttpServletResponse response) throws IOException {
        List<Menu> list = menuServiceImpl.list();
        ExcelWriter writer = ExcelUtil.getWriter(true);
        writer.write(list, true);
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        String fileName = URLEncoder.encode("xx信息", "UTF-8");
        response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
        ServletOutputStream out = response.getOutputStream();
        writer.flush(out, true);
        writer.close();
        IoUtil.close(System.out);
    }

    /**
     * 导入
     *
     * @param file 文件
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 15:03
     */
    @PostMapping("/import")
    public Result<?> importExcel(MultipartFile file) throws IOException {
        InputStream inputStream = file.getInputStream();
        ExcelReader reader = ExcelUtil.getReader(inputStream);
        List<Menu> list = reader.readAll(Menu.class);
        return menuServiceImpl.saveBatch(list) ? Result.success() : Result.error();
    }

}
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IMenuService {

    @Resource
    private MenuMapper menuMapper;

    /**
     * 通过名称查找菜单
     *
     * @param name 名称
     * @return List<Menu>
     * @author 石一歌
     * @date 2022/5/7 14:59
     */
    @Override
    public List<Menu> findMenus(String name) {
        QueryWrapper<Menu> queryWrapper = new QueryWrapper<>();
        queryWrapper.orderByAsc("sort_num");
        if (StrUtil.isNotBlank(name)) {
            queryWrapper.like("name", name);
        }
        List<Menu> list = menuMapper.selectList(queryWrapper);
        // 找出pid为null的一级菜单
        List<Menu> parentNode = list.stream().filter(menu -> menu.getPid() == null).collect(Collectors.toList());
        // 找出一级菜单的子菜单
        for (Menu parentMenu : parentNode) {
            parentMenu.setChildren(list.stream().filter(childMenu -> parentMenu.getId().equals(childMenu.getPid())).collect(Collectors.toList()));
        }
        return parentNode;
    }
}

UserRole

@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user_role")
@ApiModel(value = "UserRole对象", description = "")
public class UserRole implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 用户id
     */
    @Alias("用户id")
    @ApiModelProperty("用户id")
    private Integer userId;

    /**
     * 角色id
     */
    @Alias("角色id")
    @ApiModelProperty("角色id")
    private Integer roleId;


}

Role

@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_role")
@ApiModel(value = "Role对象", description = "")
public class Role implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * id
     */
    @Alias("id")
    @ApiModelProperty("id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 名称
     */
    @Alias("名称")
    @ApiModelProperty("名称")
    private String name;

    /**
     * 描述
     */
    @Alias("描述")
    @ApiModelProperty("描述")
    private String description;


    /**
     * 创建时间
     */
    @Alias("创建时间")
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime gmtCreate;

    /**
     * 更新时间
     */
    @Alias("更新时间")
    @ApiModelProperty("更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime gmtModified;

    /**
     * 乐观锁
     */
    @Alias("乐观锁")
    @ApiModelProperty("乐观锁")
    @Version
    private Integer version;

    /**
     * 逻辑删除
     */
    @Alias("逻辑删除")
    @ApiModelProperty("逻辑删除")
    @TableLogic
    private Integer deleted;


}

RoleMenu

@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_role_menu")
@ApiModel(value = "RoleMenu对象", description = "")
public class RoleMenu implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 角色id
     */

    @Alias("角色id")
    @ApiModelProperty("角色id")
    private Integer roleId;

    /**
     * 菜单id
     */
    @Alias("菜单id")
    @ApiModelProperty("菜单id")
    private Integer menuId;
}
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_menu")
@ApiModel(value = "Menu对象", description = "")
public class Menu implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * id
     */
    @Alias("id")
    @ApiModelProperty("id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 名称
     */
    @Alias("名称")
    @ApiModelProperty("名称")
    private String name;

    /**
     * 路径
     */
    @Alias("路径")
    @ApiModelProperty("路径")
    private String path;

    /**
     * 图标
     */
    @Alias("图标")
    @ApiModelProperty("图标")
    private String icon;

    /**
     * 描述
     */
    @Alias("描述")
    @ApiModelProperty("描述")
    private String description;

    /**
     * 父级id
     */
    @Alias("父级id")
    @ApiModelProperty("父级id")
    private Integer pid;

    /**
     * 页面路径
     */
    @Alias("页面路径")
    @ApiModelProperty("页面路径")
    private String pagePath;

    /**
     * 排序
     */
    @Alias("排序")
    @ApiModelProperty("排序")
    private Integer sortNum;

    /**
     * 逻辑删除
     */
    @Alias("逻辑删除")
    @ApiModelProperty("逻辑删除")
    @TableLogic
    private Boolean deleted;

    /**
     * 创建时间
     */
    @Alias("创建时间")
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime gmtCreate;

    /**
     * 更新时间
     */
    @Alias("更新时间")
    @ApiModelProperty("更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime gmtModified;

    /**
     * 乐观锁
     */
    @Alias("乐观锁")
    @ApiModelProperty("乐观锁")
    @Version
    private Integer version;

    /**
     * 子菜单
     */
    @Alias("子菜单")
    @ApiModelProperty("子菜单")
    @TableField(exist = false)
    private List<Menu> children;
}

User.vue

<script setup lang="ts">

import { ref, reactive, toRefs } from 'vue'
import { Edit, Remove, Download, Upload, CirclePlus, Message, Search, Position } from '@element-plus/icons-vue';
import request from '@/utils/request';
import { ElMessage } from 'element-plus';
import { regionData, CodeToText } from 'element-china-area-data'

const tableData = ref([])
const total = ref<number>(0)
const dialogFormVisible = ref<boolean>(false)
const dialogRoleVisible = ref<boolean>(false)
const pageNum = ref<number>(1)
const pageSize = ref<number>(5)
const username = ref<string>('')
const email = ref<string>('')
const address = ref<string>('')
const multipleSelection = ref<any>([])
const form = reactive({
  username: '',
  nickname: '',
  password: '',
  age: null,
  sex: '未知',
  address: '',
  region: '',
  avatar: '',
  email: '',
  phone: '',
  account: '',
  id: null,
  version: null
})
const formAsRefs = toRefs(form)
const headerBg = ref('headerBg')
const load = () => {
  request.get("/user/page", {
    params: {
      pageNum: pageNum.value,
      pageSize: pageSize.value,
      username: username.value,
      email: email.value,
      address: address.value,
    }
  }).then(res => {
    if (res.status === 200) {
      tableData.value = res.data.records
      total.value = res.data.total
    } else {
      ElMessage.error("加载失败")
    }
  })
}
load()
const save = () => {
  request.post("/user", form).then(res => {
    if (res.status === 200) {
      ElMessage.success('保存成功')
      dialogFormVisible.value = false
      load()
    } else {
      ElMessage.error("保存失败")
    }
  })
}
const handleAdd = () => {
  dialogFormVisible.value = true
  formAsRefs.username.value = ''
  formAsRefs.nickname.value = ''
  formAsRefs.age.value = null
  formAsRefs.sex.value = ''
  formAsRefs.address.value = ''
  formAsRefs.email.value = ''
  formAsRefs.phone.value = ''
  formAsRefs.account.value = ''
  formAsRefs.avatar.value = ''
  formAsRefs.id.value = null
  formAsRefs.password.value = '123456'
}
const handleEdit = (row: { username: string; nickname: string; age: null; sex: string; avatar: string; address: string; email: string; phone: string; account: string; id: null; version: null; password: string; region: (string | number)[]; }) => {
  formAsRefs.username.value = row.username
  formAsRefs.nickname.value = row.nickname
  formAsRefs.age.value = row.age
  formAsRefs.sex.value = row.sex
  formAsRefs.avatar.value = row.avatar
  formAsRefs.address.value = row.address
  formAsRefs.email.value = row.email
  formAsRefs.phone.value = row.phone
  formAsRefs.account.value = row.account
  formAsRefs.id.value = row.id
  formAsRefs.version.value = row.version
  formAsRefs.password.value = row.password
  dialogFormVisible.value = true
  regionChange(row.region)
}
const handleSelectionChange = (val: never[]) => {
  multipleSelection.value = val
}
const del = (id: number) => {
  request.delete("/user/" + id).then(res => {
    if (res.status === 200) {
      ElMessage.success("删除成功")
      load()
    } else {
      ElMessage.error("删除失败")
    }
  })
}
const delBatch = () => {
  let ids: number[] = []
  multipleSelection.value.forEach((element: any) => {
    ids.push(element.id)
  });
  request.post("/user/del/batch", ids).then(res => {
    if (res.status === 200) {
      ElMessage.success("批量删除成功")
      load()
    } else {
      ElMessage.error("批量删除失败")
    }
  })
}
const reset = () => {
  username.value = ""
  email.value = ""
  address.value = ""
  load()
}
const handleSizeChange = (size: number) => {
  pageSize.value = size
  load()
}
const handleCurrentChange = (num: number) => {
  pageNum.value = num
  load()
}
const dataFormat = (data: string) => {
  if (data === null) return null
  else
    return data.split('T').join(' ')
}
const exp = () => {
  window.open("http://localhost:8080/user/export")
}
const handleExcelImportSuccess = () => {
  ElMessage.success("导入成功")
}
const roleData = ref()
const roleArray = ref([])


const userId = ref<number>()
const selectRole = (id: any) => {
  dialogRoleVisible.value = true
  userId.value = id
  request.get("/role").then(res => {
    if (res.status === 200) {
      roleData.value = res.data
    } else {
      ElMessage.error("加载失败")
    }
  })
  request.get("/userRole/" + userId.value).then(res => {
    if (res.status === 200) {
      roleArray.value = res.data
    }
  })
}

const saveUserRole = () => {
  request.post("/userRole/" + userId.value, roleArray.value).then(res => {
    if (res.status === 200) {
      ElMessage.success("绑定成功")
    } else {
      ElMessage.error(res.statusText)
    }
    dialogRoleVisible.value = false
  })
  roleArray.value = []
}
let regionText = ref()
const regionChange = (arr: (string | number)[]) => {
  if (arr[1] != null && arr[2] != null) {
    regionText.value = CodeToText[arr[0]] + CodeToText[arr[1]] + CodeToText[arr[2]]
  } else {
    if (arr[1] != null) {
      regionText.value = CodeToText[arr[0]] + CodeToText[arr[1]]
    } else {
      regionText.value = CodeToText[arr[0]]
    }
  }
  if (arr[0] != null) {
    regionText.value = ''
  }
}
const regionFormat = (arr: (string | number)[], address: any) => {

  if (arr === null && address === '') return 1
  else if (address === '') {
    if (arr[1] != null && arr[2] != null) {
      return CodeToText[arr[0]] + CodeToText[arr[1]] + CodeToText[arr[2]]
    } else {
      if (arr[1] != null) {
        return CodeToText[arr[0]] + CodeToText[arr[1]]
      } else {
        return CodeToText[arr[0]]
      }
    }
  } else if (arr === null) {
    return address
  }
  else {
    if (arr[1] != null && arr[2] != null) {
      return CodeToText[arr[0]] + CodeToText[arr[1]] + CodeToText[arr[2]] + address
    } else {
      if (arr[1] != null) {
        return CodeToText[arr[0]] + CodeToText[arr[1]] + address
      } else {
        return CodeToText[arr[0]] + address
      }
    }
  }

}
</script>

<template>
  <el-main>
    <div style="margin: 10px 0">
      <el-input style="width: 200px" placeholder="请输入名称" :suffix-icon="Search" class="ml-5" v-model="username">
      </el-input>
      <el-input style="width: 200px" placeholder="请输入邮箱" :suffix-icon="Message" class="ml-5" v-model="email">
      </el-input>
      <el-input style="width: 200px" placeholder="请输入地址" :suffix-icon="Position" class="ml-5" v-model="address">
      </el-input>
      <el-button class="ml-5" type="primary" @click="load">搜索</el-button>
      <el-button class="ml-5" type="warning" @click="reset">清空</el-button>
    </div>

    <div style="margin: 10px 0; position: absolut;">
      <el-button type="primary" @click="handleAdd">
        新增
        <circle-plus style="width: 1em; height: 1em; margin-right: 8px" />
      </el-button>
      <el-popconfirm title="确定删除吗?" class="ml-5" confirm-button-text='确定' cancel-button-text='我再想想' icon-color="red"
        @confirm="delBatch">
        <template #reference>
          <el-button type="danger">
            批量删除
            <remove style="width: 1em; height: 1em; margin-right: 8px" />
          </el-button>
        </template>
      </el-popconfirm>
      <el-upload action="http://localhost:8080/user/import" :show-file-list="false" accept="xlsx"
        :on-success="handleExcelImportSuccess"
        style="display: inline-block; margin: 0 10px; position: relative;top:2px">
        <el-button type="primary">
          导入
          <upload style="width: 1em; height: 1em; margin-right: 8px" />
        </el-button>
      </el-upload>

      <el-button type="primary" @click="exp">
        导出
        <download style="width: 1em; height: 1em; margin-right: 8px" />
      </el-button>
    </div>

    <el-table :data="tableData" border stripe :header-cell-class-name="headerBg"
      @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" fixed></el-table-column>
      <el-table-column prop="id" label="ID" sortable width="70" fixed>
      </el-table-column>
      <el-table-column prop="username" label="用户名">
      </el-table-column>
      <el-table-column prop="nickname" label="昵称">
      </el-table-column>
      <el-table-column prop="age" label="年龄">
      </el-table-column>
      <el-table-column prop="sex" label="性别">
      </el-table-column>
      <el-table-column prop="address" label="地址" width="200px">
        <template v-slot="scope">
          {{ regionFormat(scope.row.region, scope.row.address) }}
        </template>
      </el-table-column>
      <el-table-column prop="email" label="邮箱" width="200px">
      </el-table-column>
      <el-table-column prop="phone" label="手机号" width="200px">
      </el-table-column>
      <el-table-column prop="avatar" label="头像" width="130px">
        <template v-slot="scope">
          <el-image style="width: 100px; height: 100px" :src="scope.row.avatar" :fit="'cover'"
            :preview-src-list="[scope.row.avatar]">
          </el-image>
        </template>
      </el-table-column>
      <el-table-column prop="gmtCreate" label="创建时间" width="200px">
        <template v-slot="scope">
          {{ dataFormat(scope.row.gmtCreate) }}
        </template>
      </el-table-column>
      <el-table-column prop="gmtModified" label="修改时间" width="200px">
        <template v-slot="scope">
          {{ dataFormat(scope.row.gmtModified) }}
        </template>
      </el-table-column>
      <el-table-column prop="version" label="修改次数">
      </el-table-column>
      <el-table-column label="操作" width="400" align="center" fixed="right">
        <template v-slot="scope">
          <el-button type="info" @click="selectRole(scope.row.id)">
            分配角色
          </el-button>
          <el-button type="success" @click="handleEdit(scope.row)">
            编辑
            <edit style="width: 1em; height: 1em; margin-right: 8px" />
          </el-button>
          <el-popconfirm title="确定删除吗?" class="ml-5" confirm-button-text='确定' cancel-button-text='我再想想' icon-color="red"
            @confirm="del(scope.row.id)">
            <template #reference>
              <el-button type="danger">
                删除
                <remove style="width: 1em; height: 1em; margin-right: 8px" />
              </el-button>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    <div style="padding: 10px 0">
      <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="pageNum"
        :page-sizes="[5, 10, 15, 20]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper"
        :total="total"></el-pagination>
    </div>
    <el-dialog title="用户信息" v-model="dialogFormVisible" width="30%">
      <el-form :model="form" label-width="120px">
        <el-form-item label="用户名">
          <el-input v-model="form.username" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="昵称">
          <el-input v-model="form.nickname" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="年龄">
          <el-input v-model="form.age" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="性别">
          <el-radio v-model="form.sex" label="男">男</el-radio>
          <el-radio v-model="form.sex" label="女">女</el-radio>
          <el-radio v-model="form.sex" label="未知">未知</el-radio>
        </el-form-item>
        <el-form-item label="地区">
          {{ regionText }}
          <el-cascader placeholder="请选择地区" size="small" :options="regionData" v-model="form.region"
            @change='regionChange' style="width: 80%">
          </el-cascader>
        </el-form-item>
        <el-form-item label="详细地址">
          <el-input type="textarea" v-model="form.address" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="邮箱">
          <el-input v-model="form.email" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="手机号">
          <el-input v-model="form.phone" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="账户余额">
          <el-input v-model="form.account" style="width: 80%" disabled></el-input>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogFormVisible = false">取 消</el-button>
          <el-button type="primary" @click="save">确 定</el-button>
        </span>
      </template>
    </el-dialog>
    <el-dialog title="角色分配" v-model="dialogRoleVisible" width="20%">
      <el-select v-model="roleArray" multiple collapse-tags collapse-tags-tooltip placeholder="分配角色"
        style="width: 200px" size="large">
        <el-option v-for="item in roleData" :key="item.id" :label="item.name" :value="item.id" />
      </el-select>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogRoleVisible = false">取 消</el-button>
          <el-button type="primary" @click="saveUserRole()">确 定</el-button>
        </span>
      </template>
    </el-dialog>
  </el-main>
</template>



<style scoped>
</style>

Role.vue

<script setup lang="ts">

import { ref, reactive, toRefs } from 'vue'
import request from '@/utils/request';
import { ElMessage, ElTree } from 'element-plus';

import { Edit, Remove, Download, CirclePlus, Search, Menu } from '@element-plus/icons-vue';
import { useStore } from '@/stores/store';

const tableData = ref([])
const total = ref<number>(0)
const dialogFormVisible = ref<boolean>(false)
const dialogMenuVisible = ref<boolean>(false)
const pageNum = ref<number>(1)
const pageSize = ref<number>(5)
const name = ref<string>('')
const multipleSelection = ref<any>([])
const form = reactive({
  name: '',
  id: null,
  description: '',
  version: null
})
const formAsRefs = toRefs(form)
const headerBg = ref('headerBg')
const load = () => {
  request.get("/role/page", {
    params: {
      pageNum: pageNum.value,
      pageSize: pageSize.value,
      name: name.value,
    }
  }).then(res => {
    if (res.status === 200) {
      tableData.value = res.data.records
      total.value = res.data.total
    } else {
      ElMessage.error("加载失败")
    }
  })
}
load()
const save = () => {
  request.post("/role", form).then(res => {
    if (res.status === 200) {
      ElMessage.success('保存成功')
      dialogFormVisible.value = false
      load()
    } else {
      ElMessage.error("保存失败")
    }
  })
}
const handleAdd = () => {
  dialogFormVisible.value = true
  formAsRefs.name.value = ''
  formAsRefs.description.value = ''
  formAsRefs.id.value = null
}
const handleEdit = (row: { name: string; description: string; id: null; version: null; }) => {
  formAsRefs.name.value = row.name
  formAsRefs.description.value = row.description
  formAsRefs.id.value = row.id
  formAsRefs.version.value = row.version
  dialogFormVisible.value = true
}
const handleSelectionChange = (val: never[]) => {
  multipleSelection.value = val
}
const del = (id: number) => {
  request.delete("/role/" + id).then(res => {
    if (res.status === 200) {
      ElMessage.success("删除成功")
      load()
    } else {
      ElMessage.error("删除失败")
    }
  })
}
const delBatch = () => {
  let ids: number[] = []
  multipleSelection.value.forEach((element: any) => {
    ids.push(element.id)
  });
  request.post("/role/del/batch", ids).then(res => {
    if (res.status === 200) {
      ElMessage.success("批量删除成功")
      load()
    } else {
      ElMessage.error("批量删除失败")
    }
  })
}
const reset = () => {
  name.value = ""
  load()
}
const handleSizeChange = (size: number) => {
  pageSize.value = size
  load()
}
const handleCurrentChange = (num: number) => {
  pageNum.value = num
  load()
}
const dataFormat = (data: string) => {
  if (data === null) return null
  else
    return data.split('T').join(' ')
}
const exp = () => {
  window.open("http://localhost:8080/role/export")
}
const handleExcelImportSuccess = () => {
  ElMessage.success("导入成功")
}
const selectMenu = (id: any) => {
  dialogMenuVisible.value = true
  roleId.value = id
  request.get("/menu").then(res => {
    // 获取所有菜单并展开
    if (res.status === 200) {
      menuData.value = res.data
      let ids: number[] = []
      menuData.value.forEach((element: any) => {
        ids.push(element.id)
      });
      expends.value = ids
    }
  })
  //获取菜单关系并选择
  request.get("/roleMenu/" + roleId.value).then(res => {
    if (res.status === 200) {
      checks.value = res.data
      request.get("/menu/ids").then(r => {
        if (res.status === 200) {
          const ids = r.data
          ids.forEach((id :any) => {
            if (!checks.value.includes(id)) {
              treeRef.value!.setChecked(id, false,false)
            }
          })
        }

      })
    }
  })
}
const defaultProps = {
  label: 'name',
}
const menuData = ref([])
let expends = ref<number[]>()
let checks = ref<any>()
const roleId = ref<number>()
const treeRef = ref<InstanceType<typeof ElTree>>()
const saveRoleMenu = () => {
  // 传入角色id和菜单数组,重新绑定关系
  request.post("/roleMenu/" + roleId.value, treeRef.value!.getCheckedKeys(false)).then(res => {
    if (res.status === 200) {
      ElMessage.success("绑定成功")
      useStore().logout();
    } else {
      ElMessage.error(res.statusText)
    }
    dialogMenuVisible.value = false
  })
  checks.value = []
}
</script>

<template>
  <el-main>
    <div style="margin: 10px 0">
      <el-input style="width: 200px" placeholder="请输入名称" :suffix-icon="Search" class="ml-5" v-model="name">
      </el-input>
      <el-button class="ml-5" type="primary" @click="load">搜索</el-button>
      <el-button class="ml-5" type="warning" @click="reset">清空</el-button>
    </div>

    <div style="margin: 10px 0; position: absolut;">
      <el-button type="primary" @click="handleAdd">
        新增
        <circle-plus style="width: 1em; height: 1em; margin-right: 8px" />
      </el-button>
      <el-popconfirm title="确定删除吗?" class="ml-5" confirm-button-text='确定' cancel-button-text='我再想想' icon-color="red"
        @confirm="delBatch">
        <template #reference>
          <el-button type="danger">
            批量删除
            <remove style="width: 1em; height: 1em; margin-right: 8px" />
          </el-button>
        </template>
      </el-popconfirm>
      <!-- <el-upload action="http://localhost:8080/role/import" :show-file-list="false" accept="xlsx"
        :on-success="handleExcelImportSuccess"
        style="display: inline-block; margin: 0 10px; position: relative;top:2px">
        <el-button type="primary">
          导入
          <upload style="width: 1em; height: 1em; margin-right: 8px" />
        </el-button>
      </el-upload> -->

      <el-button type="primary" @click="exp">
        导出
        <download style="width: 1em; height: 1em; margin-right: 8px" />
      </el-button>
    </div>

    <el-table :data="tableData" border stripe :header-cell-class-name="headerBg"
      @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" fixed></el-table-column>
      <el-table-column prop="id" label="ID" sortable width="70" fixed>
      </el-table-column>
      <el-table-column prop="name" label="名称">
      </el-table-column>
      <el-table-column prop="description" label="描述">
      </el-table-column>
      <el-table-column prop="gmtCreate" label="创建时间" width="200px">
        <template v-slot="scope">
          {{ dataFormat(scope.row.gmtCreate) }}
        </template>
      </el-table-column>
      <el-table-column prop="gmtModified" label="修改时间" width="200px">
        <template v-slot="scope">
          {{ dataFormat(scope.row.gmtModified) }}
        </template>
      </el-table-column>
      <el-table-column prop="version" label="修改次数">
      </el-table-column>
      <el-table-column label="操作" width="400px" align="center" fixed="right">
        <template v-slot="scope">
          <el-button type="info" @click="selectMenu(scope.row.id)">
            分配菜单
            <Menu style="width: 1em; height: 1em; margin-right: 8px" />
          </el-button>
          <el-button type="success" @click="handleEdit(scope.row)">
            编辑
            <edit style="width: 1em; height: 1em; margin-right: 8px" />
          </el-button>
          <el-popconfirm title="确定删除吗?" class="ml-5" confirm-button-text='确定' cancel-button-text='我再想想' icon-color="red"
            @confirm="del(scope.row.id)">
            <template #reference>
              <el-button type="danger">
                删除
                <remove style="width: 1em; height: 1em; margin-right: 8px" />
              </el-button>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    <div style="padding: 10px 0">
      <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="pageNum"
        :page-sizes="[5, 10, 15, 20]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper"
        :total="total"></el-pagination>
    </div>
    <el-dialog title="角色信息" v-model="dialogFormVisible" width="30%">
      <el-form :model="form" label-width="120px">
        <el-form-item label="名称">
          <el-input v-model="form.name" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="描述">
          <el-input type="textarea" v-model="form.description" style="width: 80%"></el-input>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogFormVisible = false">取 消</el-button>
          <el-button type="primary" @click="save">确 定</el-button>
        </span>
      </template>
    </el-dialog>

    <el-dialog title="菜单分配" v-model="dialogMenuVisible" width="20%">
      <el-tree :props="defaultProps" :data="menuData" show-checkbox node-key="id" :default-expanded-keys="expends"
        :default-checked-keys="checks" ref="treeRef">
        <template #default="{ node, data }">
          <span class="custom-tree-node">
            <span>
              <e-icon :icon-name="data.icon" />
            </span>
            <span>{{ node.label }}</span>
          </span>
        </template>
      </el-tree>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogMenuVisible = false">取 消</el-button>
          <el-button type="primary" @click="saveRoleMenu()">确 定</el-button>
        </span>
      </template>
    </el-dialog>

  </el-main>
</template>



<style scoped>
</style>
<script setup lang="ts">

import { ref, reactive, toRefs } from 'vue'
import { Edit, Remove, Download, Upload, CirclePlus, Message, Search, Position } from '@element-plus/icons-vue';
import request from '@/utils/request';
import { ElMessage } from 'element-plus';

const tableData = ref([])
const total = ref<number>(0)
const dialogFormVisible = ref<boolean>(false)
const pageNum = ref<number>(1)
const pageSize = ref<number>(5)
const name = ref<string>('')
const multipleSelection = ref<any>([])
const form = reactive({
  name: '',
  path: '',
  pagePath: '',
  icon: '',
  sortNum: null,
  description: '',
  id: null,
  pid: null,
  version: null
})
const formAsRefs = toRefs(form)
const headerBg = ref('headerBg')
const load = () => {
  request.get("/menu", {
    params: {
      name: name.value
    }
  }).then(res => {
    if (res.status === 200) {
      tableData.value = res.data
    } else {
      ElMessage.error("加载失败")
    }

  })
}
const selectLoad = () => {
  request.get("/menu/page", {
    params: {
      pageNum: pageNum.value,
      pageSize: pageSize.value,
      name: name.value
    }
  }).then(res => {
    if (res.status === 200) {
      tableData.value = res.data.records
      total.value = res.data.total
    } else {
      ElMessage.error("加载失败")
    }

  })
}
load()
const save = () => {
  request.post("/menu", form).then(res => {
    if (res.status === 200) {
      ElMessage.success('保存成功')
      dialogFormVisible.value = false
      load()
    } else {
      ElMessage.error("保存失败")
    }
  })
}
const handleAdd = (pid: null) => {
  dialogFormVisible.value = true
  formAsRefs.name.value = ''
  formAsRefs.path.value = ''
  formAsRefs.pagePath.value = ''
  formAsRefs.icon.value = ''
  formAsRefs.description.value = ''
  formAsRefs.id.value = null
  formAsRefs.sortNum.value = null
  if (pid)
    formAsRefs.pid.value = pid
}
const handleEdit = (row: { name: string; path: string; pagePath: string; icon: string; description: string; id: null; sortNum: null; version: null; }) => {
  formAsRefs.name.value = row.name
  formAsRefs.path.value = row.path
  formAsRefs.pagePath.value = row.pagePath
  formAsRefs.icon.value = row.icon
  formAsRefs.description.value = row.description
  formAsRefs.id.value = row.id
  formAsRefs.sortNum.value = row.sortNum
  formAsRefs.version.value = row.version
  dialogFormVisible.value = true
}
const handleSelectionChange = (val: never[]) => {
  multipleSelection.value = val
}
const del = (id: number) => {
  request.delete("/menu/" + id).then(res => {
    if (res.status === 200) {
      ElMessage.success("删除成功")
      load()
    } else {
      ElMessage.error("删除失败")
    }
  })
}
const delBatch = () => {
  let ids: number[] = []
  multipleSelection.value.forEach((element: any) => {
    ids.push(element.id)
  });
  request.post("/menu/del/batch", ids).then(res => {
    if (res.status === 200) {
      ElMessage.success("批量删除成功")
      load()
    } else {
      ElMessage.error("批量删除失败")
    }
  })
}
const reset = () => {
  name.value = ""
  load()
}
const handleSizeChange = (size: number) => {
  pageSize.value = size
  load()
}
const handleCurrentChange = (num: number) => {
  pageNum.value = num
  load()
}
const dataFormat = (data: string) => {
  if (data === null) return null
  else
    return data.split('T').join(' ')
}
const exp = () => {
  window.open("http://localhost:8080/menu/export")
}



</script>

<template>
  <el-main>
    <div style="margin: 10px 0">
      <el-input style="width: 200px" placeholder="请输入名称" :suffix-icon="Search" class="ml-5" v-model="name">
      </el-input>
      <el-button class="ml-5" type="primary" @click="selectLoad">搜索</el-button>
      <el-button class="ml-5" type="warning" @click="reset">清空</el-button>
    </div>

    <div style="margin: 10px 0; position: absolut;">
      <el-button type="primary" @click="handleAdd(null)">
        新增
        <circle-plus style="width: 1em; height: 1em; margin-right: 8px" />
      </el-button>
      <el-popconfirm title="确定删除吗?" class="ml-5" confirm-button-text='确定' cancel-button-text='我再想想' icon-color="red"
        @confirm="delBatch">
        <template #reference>
          <el-button type="danger">
            批量删除
            <remove style="width: 1em; height: 1em; margin-right: 8px" />
          </el-button>
        </template>
      </el-popconfirm>
      <!-- <el-upload action="http://localhost:8080/menu/import" :show-file-list="false" accept="xlsx"
        :on-success="handleExcelImportSuccess"
        style="display: inline-block; margin: 0 10px; position: relative;top:2px">
        <el-button type="primary">
          导入
          <upload style="width: 1em; height: 1em; margin-right: 8px" />
        </el-button>
      </el-upload> -->

      <el-button type="primary" @click="exp">
        导出
        <download style="width: 1em; height: 1em; margin-right: 8px" />
      </el-button>
    </div>

    <el-table :data="tableData" row-key="id" border default-expand-all stripe :header-cell-class-name="headerBg"
      @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" fixed></el-table-column>
      <el-table-column prop="id" label="ID" sortable width="80" fixed>
      </el-table-column>
      <el-table-column prop="name" label="名称">
      </el-table-column>
      <el-table-column prop="path" label="路径">
      </el-table-column>
      <el-table-column prop="pagePath" label="页面路径">
      </el-table-column>
      <el-table-column prop="sortNum" label="顺序">
      </el-table-column>
      <el-table-column prop="icon" label="图标" class-name="">
        <template v-slot="scope">
          <e-icon :icon-name="scope.row.icon" style="font-size: 28px; text-align: center;vertical-align: bottom;  width: 30px;height: 30px;display:inline-block; line-height: 30px; margin:0px" />
        </template>
      </el-table-column>
      <el-table-column prop="description" label="描述">
      </el-table-column>
      <el-table-column prop="gmtCreate" label="创建时间" width="200px">
        <template v-slot="scope">
          {{ dataFormat(scope.row.gmtCreate) }}
        </template>
      </el-table-column>
      <el-table-column prop="gmtModified" label="修改时间" width="200px">
        <template v-slot="scope">
          {{ dataFormat(scope.row.gmtModified) }}
        </template>
      </el-table-column>
      <el-table-column prop="version" label="修改次数">
      </el-table-column>
      <el-table-column label="操作" width="400" align="center" fixed="right">
        <template v-slot="scope">
          <el-button type="primary" @click="handleAdd(scope.row.id)" v-show="!scope.row.pid && !scope.row.path">
            新增子菜单
            <circle-plus style="width: 1em; height: 1em; margin-right: 8px" />
          </el-button>
          <el-button type="success" @click="handleEdit(scope.row)">
            编辑
            <edit style="width: 1em; height: 1em; margin-right: 8px" />
          </el-button>
          <el-popconfirm title="确定删除吗?" class="ml-5" confirm-button-text='确定' cancel-button-text='我再想想' icon-color="red"
            @confirm="del(scope.row.id)">
            <template #reference>
              <el-button type="danger">
                删除
                <remove style="width: 1em; height: 1em; margin-right: 8px" />
              </el-button>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    <div style="padding: 10px 0">
      <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="pageNum"
        :page-sizes="[5, 10, 15, 20]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper"
        :total="total"></el-pagination>
    </div>
    <el-dialog title="菜单信息" v-model="dialogFormVisible" width="30%">
      <el-form :model="form" label-width="120px">
        <el-form-item label="名称">
          <el-input v-model="form.name" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="路径">
          <el-input v-model="form.path" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="页面路径">
          <el-input v-model="form.pagePath" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="图标">
          <!-- <template> -->
          <!-- <el-select v-model="form.icon" class="m-2" placeholder="Select" size="large">
              <el-option v-for="item in options" :key="item.name" :label="item.name" :value="item.value">
                <component class="xxx" :is="item.value"></component>
              </el-option>
            </el-select> -->
          <e-icon-picker v-model="form.icon" clearable :width="800" style="width: 80%" />
          <!-- </template> -->
        </el-form-item>
         <el-form-item label="顺序">
          <el-input v-model="form.sortNum" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="描述">
          <el-input type="textarea" v-model="form.description" style="width: 80%"></el-input>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogFormVisible = false">取 消</el-button>
          <el-button type="primary" @click="save">确 定</el-button>
        </span>
      </template>
    </el-dialog>

  </el-main>
</template>

日志模块

在权限系统更新,以及用户登录,注册、退出处调用log方法添加日志

LogController

@RestController
@RequestMapping("/log")
public class LogController {

    @Resource
    private ILogService logServiceImpl;

    /**
     * 新增和修改日志
     *
     * @param log 日志
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:30
     */
    @PostMapping
    public Result<?> save(@RequestBody Log log) {
        return logServiceImpl.saveOrUpdate(log) ? Result.success() : Result.error();
    }

    /**
     * 删除日志
     *
     * @param id 日志id
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:30
     */
    @DeleteMapping("/{id}")
    public Result<?> delete(@PathVariable Integer id) {
        return logServiceImpl.removeById(id) ? Result.success() : Result.error();
    }

    /**
     * 批量删除日志
     *
     * @param ids id数组
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:31
     */
    @PostMapping("/del/batch")
    public Result<?> deleteBatch(@RequestBody List<Integer> ids) {
        return logServiceImpl.removeByIds(ids) ? Result.success() : Result.error();
    }

    /**
     * 查找所有日志
     *
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:31
     */
    @GetMapping
    public Result<?> findAll() {
        return CollUtil.isNotEmpty(logServiceImpl.list()) ? Result.success(logServiceImpl.list()) : Result.error();
    }

    /**
     * 根据id查找日志
     *
     * @param id 日志id
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:32
     */
    @GetMapping("/{id}")
    public Result<?> findOne(@PathVariable Integer id) {
        return BeanUtil.isNotEmpty(logServiceImpl.getById(id)) ? Result.success(logServiceImpl.getById(id)) : Result.error();
    }

    /**
     * 分页查询
     *
     * @param pageNum  分页页码
     * @param pageSize 分页大小
     * @param username 用户名
     * @param content  内容
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:32
     */
    @GetMapping("/page")
    public Result<?> findPage(@RequestParam Integer pageNum,
                              @RequestParam Integer pageSize,
                              @RequestParam(defaultValue = "") String username,
                              @RequestParam(defaultValue = "") String content) {
        IPage<Log> page = new Page<>(pageNum, pageSize);
        QueryWrapper<Log> queryWrapper = new QueryWrapper<>();
        if (StrUtil.isNotBlank(username)) {
            queryWrapper.like("username", username);
        }
        if (StrUtil.isNotBlank(content)) {
            queryWrapper.like("content", content);
        }
        queryWrapper.orderByDesc("id");
        return ObjectUtil.isNotEmpty(logServiceImpl.page(page, queryWrapper)) ? Result.success(logServiceImpl.page(page, queryWrapper)) : Result.error();
    }

    /**
     * excel导出
     *
     * @param response 响应
     * @author 石一歌
     * @date 2022/5/20 14:33
     */
    @GetMapping("/export")
    public void exportExcel(HttpServletResponse response) throws IOException {
        List<Log> list = logServiceImpl.list();
        ExcelWriter writer = ExcelUtil.getWriter(true);
        writer.write(list, true);
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        String fileName = URLEncoder.encode("日志信息", "UTF-8");
        response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
        ServletOutputStream out = response.getOutputStream();
        writer.flush(out, true);
        writer.close();
        IoUtil.close(System.out);
    }

    /**
     * excel导入
     *
     * @param file 文件
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:34
     */

    @PostMapping("/import")
    public Result<?> importExcel(MultipartFile file) throws IOException {
        InputStream inputStream = file.getInputStream();
        ExcelReader reader = ExcelUtil.getReader(inputStream);
        List<Log> list = reader.readAll(Log.class);
        return logServiceImpl.saveBatch(list) ? Result.success() : Result.error();
    }

}

LogServiceImpl

@Service
public class LogServiceImpl extends ServiceImpl<LogMapper, Log> implements ILogService {
    @Resource
    private LogMapper logMapper;
    @Resource
    private HttpServletRequest request;

    /**
     * 记录日志
     *
     * @param content 内容
     * @author 石一歌
     * @date 2022/5/20 14:57
     */
    @Override
    public void log(String content) {
        Log log = Log.builder().username(getUser().getUsername()).ip(getIpAddress()).content(content).build();
        logMapper.insert(log);
    }

    /**
     * 记录日志
     *
     * @param content  内容
     * @param username 用户名
     * @author 石一歌
     * @date 2022/5/20 14:57
     */
    @Override
    public void log(String content, String username) {
        Log log = Log.builder().username(username).ip(getIpAddress()).content(content).build();
        logMapper.insert(log);
    }

    @Override
    public List<IpAddressDTO> countIpAddress() {
        return logMapper.countIpAddress();
    }

    /**
     * 获取ip地址
     *
     * @return String
     * @author 石一歌
     * @date 2022/5/20 14:56
     */
    public String getIpAddress() {
        String ip = request.getHeader("x-forwarded-for");
        String unknown = "unknown";
        if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        // 如果是多级代理,那么取第一个IP为客户端IP
        if (ip != null && ip.contains(StrUtil.COMMA)) {
            ip = ip.substring(0, ip.indexOf(StrUtil.COMMA)).trim();
        }
        return ip;
    }

    /**
     * 获取用户信息
     *
     * @return User
     * @author 石一歌
     * @date 2022/5/20 14:56
     */
    private User getUser() {
        return TokenUtils.getCurrentUser();
    }
}

LogMapper

public interface LogMapper extends BaseMapper<Log> {
    /**
     * 登录ip统计
     *
     * @return List<IPAddressDTO>
     * @author 石一歌
     * @date 2022/5/20 22:14
     */
    @Select("select count(id) count, ip from sys_log where content like '%登录%' GROUP BY ip ")
    List<IpAddressDTO> countIpAddress();
}

Log.vue

<script setup lang="ts">
import { ref } from "vue";
import {
  Remove,
  Download,
  Upload,
  Search,
} from "@element-plus/icons-vue";
import request from "@/utils/request";
import { ElMessage } from "element-plus";
import { serverIp } from "../../public/config";
const tableData = ref([]);
const total = ref<number>(0);
const pageNum = ref<number>(1);
const pageSize = ref<number>(5);
const username = ref<string>("");
const content = ref<string>("");
const multipleSelection = ref<any>([]);

const headerBg = ref("headerBg");
const load = () => {
  request
    .get("/log/page", {
      params: {
        pageNum: pageNum.value,
        pageSize: pageSize.value,
        username: username.value,
        content: content.value,
      },
    })
    .then((res) => {
      if (res.status === 200) {
        tableData.value = res.data.records;
        total.value = res.data.total;
      } else {
        ElMessage.error("加载失败");
      }
    });
};
load();
const handleSelectionChange = (val: never[]) => {
  multipleSelection.value = val;
};
const del = (id: number) => {
  request.delete("/log/" + id).then((res) => {
    if (res.status === 200) {
      ElMessage.success("删除成功");
      load();
    } else {
      ElMessage.error("删除失败");
    }
  });
};
const delBatch = () => {
  let ids: number[] = [];
  multipleSelection.value.forEach((element: any) => {
    ids.push(element.id);
  });
  request.post("/log/del/batch", ids).then((res) => {
    if (res.status === 200) {
      ElMessage.success("批量删除成功");
      load();
    } else {
      ElMessage.error("批量删除失败");
    }
  });
};
const reset = () => {
  username.value = "";
  content.value = "";
  load();
};
const handleSizeChange = (size: number) => {
  pageSize.value = size;
  load();
};
const handleCurrentChange = (num: number) => {
  pageNum.value = num;
  load();
};
const dataFormat = (data: string) => {
  if (data === null) return null;
  else return data.split("T").join(" ");
};
const exp = () => {
  window.open(`http://${serverIp}:8080/log/export`);
};
const handleExcelImportSuccess = () => {
  ElMessage.success("导入成功");
};
</script>

<template>
  <el-main>
    <div style="margin: 10px 0">
      <el-input
        style="width: 200px"
        placeholder="请输入操作人"
        :suffix-icon="Search"
        class="ml-5"
        v-model="username"
      >
      </el-input>
      <el-input
        style="width: 200px"
        placeholder="请输入操作内容"
        :suffix-icon="Search"
        class="ml-5"
        v-model="content"
      >
      </el-input>
      <el-button class="ml-5" type="primary" @click="load"
        >搜索 <search style="width: 1em; height: 1em; margin-left: 8px"
      /></el-button>
      <el-button class="ml-5" type="warning" @click="reset"
        >清空
        <e-icon
          icon-name="fa fa-trash-o"
          style="width: 1em; height: 1em; margin-left: 8px"
        />
      </el-button>
    </div>

    <div style="margin: 10px 0; position: absolut">
      <el-popconfirm
        title="确定删除吗?"
        class="ml-5"
        confirm-button-text="确定"
        cancel-button-text="我再想想"
        icon-color="red"
        @confirm="delBatch"
      >
        <template #reference>
          <el-button type="danger">
            批量删除
            <remove style="width: 1em; height: 1em; margin-left: 8px" />
          </el-button>
        </template>
      </el-popconfirm>
      <el-upload
        :action="'http://' + serverIp + ':8080/log/import'"
        :show-file-list="false"
        accept="xlsx"
        :on-success="handleExcelImportSuccess"
        style="
          display: inline-block;
          margin: 0 10px;
          position: relative;
          top: 2px;
        "
      >
        <el-button type="primary">
          导入
          <upload style="width: 1em; height: 1em; margin-left: 8px" />
        </el-button>
      </el-upload>

      <el-button type="primary" @click="exp">
        导出
        <download style="width: 1em; height: 1em; margin-left: 8px" />
      </el-button>
    </div>

    <el-table
      :data="tableData"
      border
      stripe
      :header-cell-class-name="headerBg"
      @selection-change="handleSelectionChange"
    >
      <el-table-column type="selection" width="55" fixed></el-table-column>
      <el-table-column prop="id" label="ID" width="70"></el-table-column>
      <el-table-column prop="username" label="操作人"></el-table-column>
      <el-table-column
        prop="content"
        label="操作内容"
        width="470"
      ></el-table-column>
      <el-table-column prop="ip" label="ip地址"></el-table-column>
      <el-table-column prop="gmtCreate" label="操作时间">
        <template v-slot="scope">
          {{ dataFormat(scope.row.gmtCreate) }}
        </template>
      </el-table-column>
      <el-table-column label="操作" width="100" align="center" fixed="right">
        <template v-slot="scope">
          <el-popconfirm
            title="确定删除吗?"
            class="ml-5"
            confirm-button-text="确定"
            cancel-button-text="我再想想"
            icon-color="red"
            @confirm="del(scope.row.id)"
          >
            <template #reference>
              <el-button type="danger">
                删除
                <remove style="width: 1em; height: 1em; margin-left: 8px" />
              </el-button>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    <div style="padding: 10px 0">
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="pageNum"
        :page-sizes="[5, 10, 15, 20]"
        :page-size="pageSize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
      ></el-pagination>
    </div>
  </el-main>
</template>

<style scoped></style>

公告模块

整合普通公告和富文本公告

NoticeController

@RestController
@RequestMapping("/notice")
public class NoticeController {

    @Resource
    private INoticeService noticeServiceImpl;

    /**
     * 保存公告
     *
     * @param notice 公告
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:34
     */
    @PostMapping
    public Result<?> save(@RequestBody Notice notice) {
        if (ObjectUtil.isNull(notice.getId())) {
            notice.setAuthor(getUser().getNickname());
        }
        return noticeServiceImpl.saveOrUpdate(notice) ? Result.success() : Result.error();
    }

    /**
     * 删除公告
     *
     * @param id 公告id
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:35
     */
    @DeleteMapping("/{id}")
    public Result<?> delete(@PathVariable Integer id) {
        return noticeServiceImpl.removeById(id) ? Result.success() : Result.error();
    }

    /**
     * 批量删除公告
     *
     * @param ids id数组
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:35
     */
    @PostMapping("/del/batch")
    public Result<?> deleteBatch(@RequestBody List<Integer> ids) {
        return noticeServiceImpl.removeByIds(ids) ? Result.success() : Result.error();
    }

    /**
     * 根据类型查找公告
     *
     * @param type 类型
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:36
     */
    @GetMapping
    public Result<?> findAll(@RequestParam(required = false) Integer type) {
        QueryWrapper<Notice> queryWrapper = new QueryWrapper<>();
        if (ObjectUtil.isNotNull(type)) {
            queryWrapper.eq("type", type);
        }
        queryWrapper.orderByDesc("id");
        List<Notice> noticeList = noticeServiceImpl.list(queryWrapper);
        return CollUtil.isNotEmpty(noticeList) ? Result.success(noticeList) : Result.error();
    }

    /**
     * 根据id查找公告
     *
     * @param id 公告id
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:37
     */
    @GetMapping("/{id}")
    public Result<?> findOne(@PathVariable Integer id) {
        return BeanUtil.isNotEmpty(noticeServiceImpl.getById(id)) ? Result.success(noticeServiceImpl.getById(id)) : Result.error();
    }

    /**
     * 分页查询
     *
     * @param pageNum  分页页码
     * @param pageSize 分页大小
     * @param title    标题
     * @param type     类型
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:37
     */
    @GetMapping("/page")
    public Result<?> findPage(@RequestParam Integer pageNum,
                              @RequestParam Integer pageSize,
                              @RequestParam String title,
                              @RequestParam(required = false) Integer type) {
        IPage<Notice> page = new Page<>(pageNum, pageSize);
        QueryWrapper<Notice> queryWrapper = new QueryWrapper<>();
        if (StrUtil.isNotBlank(title)) {
            queryWrapper.like("title", title);
        }
        if (ObjectUtil.isNotNull(type)) {
            queryWrapper.eq("type", type);
        }
        queryWrapper.orderByDesc("id");
        return ObjectUtil.isNotEmpty(noticeServiceImpl.page(page, queryWrapper)) ? Result.success(noticeServiceImpl.page(page, queryWrapper)) : Result.error();
    }

    /**
     * excel导出
     *
     * @param response 响应
     * @author 石一歌
     * @date 2022/5/20 14:38
     */
    @GetMapping("/export")
    public void exportExcel(HttpServletResponse response) throws IOException {
        List<Notice> list = noticeServiceImpl.list();
        ExcelWriter writer = ExcelUtil.getWriter(true);
        writer.write(list, true);
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        String fileName = URLEncoder.encode("公告信息", "UTF-8");
        response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
        ServletOutputStream out = response.getOutputStream();
        writer.flush(out, true);
        writer.close();
        IoUtil.close(System.out);
    }

    /**
     * excel导入
     *
     * @param file 文件
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:39
     */
    @PostMapping("/import")
    public Result<?> importExcel(MultipartFile file) throws IOException {
        InputStream inputStream = file.getInputStream();
        ExcelReader reader = ExcelUtil.getReader(inputStream);
        List<Notice> list = reader.readAll(Notice.class);
        return noticeServiceImpl.saveBatch(list) ? Result.success() : Result.error();
    }
/**
 * 获取用户
 *
 * @return User
 * @author 石一歌
 * @date 2022/5/20 14:40
 */
    private User getUser() {
        return TokenUtils.getCurrentUser();
    }
}

Notice.vue

we-editor
<script setup lang="ts">
import { ref, reactive, toRefs } from "vue";
import {
  Edit,
  Remove,
  Download,
  Upload,
  CirclePlus,
  Search,
} from "@element-plus/icons-vue";
import request from "@/utils/request";
import { ElMessage, UploadProps } from "element-plus";
import { serverIp } from "../../public/config";
import { Plus } from "@element-plus/icons-vue";
import EditorBar from "@/components/Editor.vue";

const tableData = ref([]);
const total = ref<number>(0);
const dialogFormVisible = ref<boolean>(false);
const dialogFormTwoVisible = ref<boolean>(false);
const dialogpreviewVisible = ref<boolean>(false);
const content = ref("");
const pageNum = ref<number>(1);
const pageSize = ref<number>(5);
const title = ref<string>("");
const type = ref<number>(1);
const multipleSelection = ref<any>([]);
const form = reactive({
  id: null,
  title: "",
  content: "",
  author: "",
  img: "",
  type: 0,
  version: null,
});
const formAsRefs = toRefs(form);
const headerBg = ref("headerBg");
const load = () => {
  request
    .get("/notice/page", {
      params: {
        pageNum: pageNum.value,
        pageSize: pageSize.value,
        title: title.value,
        type: type.value,
      },
    })
    .then((res) => {
      if (res.status === 200) {
        tableData.value = res.data.records;
        total.value = res.data.total;
      } else {
        ElMessage.error("加载失败");
      }
    });
};
load();
const save = () => {
  request.post("/notice", form).then((res) => {
    if (res.status === 200) {
      ElMessage.success("保存成功");
      dialogFormVisible.value = false;
      dialogFormTwoVisible.value = false;
      load();
    } else {
      ElMessage.error("保存失败");
    }
  });
};
const handleAdd = () => {
  if (!isRichText.value) {
    formAsRefs.type.value = 1;
    dialogFormVisible.value = true;
  } else {
    formAsRefs.type.value = 2;
    dialogFormTwoVisible.value = true;
  }
  formAsRefs.content.value = "";
  formAsRefs.title.value = "";
  formAsRefs.img.value = "";
  formAsRefs.id.value = null;
};
const handleEdit = (row: {
  title: string;
  content: string;
  img: string;
  author: string;
  id: null;
  version: null;
}) => {
  if (!isRichText.value) {
    formAsRefs.type.value = 1;
    dialogFormVisible.value = true;
  } else {
    formAsRefs.type.value = 2;
    dialogFormTwoVisible.value = true;
  }
  formAsRefs.title.value = row.title;
  formAsRefs.content.value = row.content;
  formAsRefs.img.value = row.img;
  formAsRefs.author.value = row.author;
  formAsRefs.id.value = row.id;
  formAsRefs.version.value = row.version;
};
const switchType = () => {
  if (dialogFormVisible.value) {
    dialogFormVisible.value = false;
    dialogFormTwoVisible.value = true;
    formAsRefs.type.value = 2;
  } else {
    dialogFormVisible.value = true;
    dialogFormTwoVisible.value = false;
    formAsRefs.type.value = 1;
  }
};
const handleSelectionChange = (val: never[]) => {
  multipleSelection.value = val;
};
const del = (id: number) => {
  request.delete("/notice/" + id).then((res) => {
    if (res.status === 200) {
      ElMessage.success("删除成功");
      load();
    } else {
      ElMessage.error("删除失败");
    }
  });
};
const delBatch = () => {
  let ids: number[] = [];
  multipleSelection.value.forEach((element: any) => {
    ids.push(element.id);
  });
  request.post("/notice/del/batch", ids).then((res) => {
    if (res.status === 200) {
      ElMessage.success("批量删除成功");
      load();
    } else {
      ElMessage.error("批量删除失败");
    }
  });
};
const reset = () => {
  title.value = "";
  load();
};
const handleSizeChange = (size: number) => {
  pageSize.value = size;
  load();
};
const handleCurrentChange = (num: number) => {
  pageNum.value = num;
  load();
};
const dataFormat = (data: string) => {
  if (data === null) return null;
  else return data.split("T").join(" ");
};
const typeFormat = (type: number | null) => {
  if (type === null) return null;
  else if (type === 1) return "普通文本";
  else return "富文本";
};
const exp = () => {
  window.open(`http://${serverIp}:8080/notice/export`);
};
const handleExcelImportSuccess = () => {
  ElMessage.success("导入成功");
};

const isRichText = ref<boolean>(false);
const typeChange = () => {
  if (isRichText.value) type.value = 2;
  else type.value = 1;
  load();
};

const handleAvatarSuccess: UploadProps["onSuccess"] = (res) => {
  form.img = res;
};
const beforeAvatarUpload: UploadProps["beforeUpload"] = (rawFile) => {
  if (
    rawFile.type !== "image/jpeg" &&
    rawFile.type !== "image/png" &&
    rawFile.type !== "image/webp" &&
    rawFile.type !== "image/gif"
  ) {
    ElMessage.error("头像只能是图片格式!");
    return false;
  } else if (rawFile.size / 1024 / 1024 > 2) {
    ElMessage.error("头像必须小于2MB!");
    return false;
  }
  return true;
};

const preview = (value: string) => {
  content.value = value;
  dialogpreviewVisible.value = true;
};
</script>

<template>
  <el-main>
    <div style="margin: 10px 0">
      <el-input
        style="width: 200px"
        placeholder="请输入名称"
        :suffix-icon="Search"
        class="ml-5"
        v-model="title"
      >
      </el-input>
      <el-button class="ml-5" type="primary" @click="load"
        >搜索 <search style="width: 1em; height: 1em; margin-left: 8px"
      /></el-button>
      <el-button class="ml-5" type="warning" @click="reset"
        >清空
        <e-icon
          icon-name="fa fa-trash-o"
          style="width: 1em; height: 1em; margin-left: 8px"
        />
      </el-button>
      <el-switch
        class="ml-5"
        v-model="isRichText"
        active-color="#409EFF"
        inactive-color="#409EFF"
        active-text="富文本"
        inactive-text="普通文本"
        size="large"
        :width="50"
        @change="typeChange"
      />
    </div>

    <div style="margin: 10px 0; position: absolut">
      <el-button type="primary" @click="handleAdd">
        新增
        <circle-plus style="width: 1em; height: 1em; margin-left: 8px" />
      </el-button>
      <el-popconfirm
        title="确定删除吗?"
        class="ml-5"
        confirm-button-text="确定"
        cancel-button-text="我再想想"
        icon-color="red"
        @confirm="delBatch"
      >
        <template #reference>
          <el-button type="danger">
            批量删除
            <remove style="width: 1em; height: 1em; margin-left: 8px" />
          </el-button>
        </template>
      </el-popconfirm>
      <el-upload
        :action="'http://' + serverIp + ':8080/notice/import'"
        :show-file-list="false"
        accept="xlsx"
        :on-success="handleExcelImportSuccess"
        style="
          display: inline-block;
          margin: 0 10px;
          position: relative;
          top: 2px;
        "
      >
        <el-button type="primary">
          导入
          <upload style="width: 1em; height: 1em; margin-left: 8px" />
        </el-button>
      </el-upload>

      <el-button type="primary" @click="exp">
        导出
        <download style="width: 1em; height: 1em; margin-left: 8px" />
      </el-button>
    </div>

    <el-table
      :data="tableData"
      border
      stripe
      :header-cell-class-name="headerBg"
      @selection-change="handleSelectionChange"
    >
      <el-table-column type="selection" width="55" fixed></el-table-column>
      <el-table-column prop="id" label="ID" sortable width="70" fixed>
      </el-table-column>
      <el-table-column prop="title" label="题目"> </el-table-column>
      <el-table-column prop="content" label="内容">
        <template v-slot="scope">
          <el-button type="primary" @click="preview(scope.row.content)">
            预览
          </el-button></template
        >
      </el-table-column>
      <el-table-column prop="author" label="作者"> </el-table-column>
      <el-table-column prop="type" label="类型">
        <template v-slot="scope">
          {{ typeFormat(scope.row.type) }}
        </template>
      </el-table-column>
      <el-table-column prop="img" label="插图" width="130px">
        <template v-slot="scope">
          <el-image
            style="width: 100px; height: 100px"
            :src="scope.row.img"
            :fit="'cover'"
            :preview-src-list="[scope.row.img]"
          >
          </el-image>
        </template>
      </el-table-column>
      <el-table-column prop="gmtCreate" label="创建时间" width="200px">
        <template v-slot="scope">
          {{ dataFormat(scope.row.gmtCreate) }}
        </template>
      </el-table-column>
      <el-table-column prop="gmtModified" label="修改时间" width="200px">
        <template v-slot="scope">
          {{ dataFormat(scope.row.gmtModified) }}
        </template>
      </el-table-column>
      <el-table-column prop="version" label="修改次数"> </el-table-column>
      <el-table-column label="操作" width="300" align="center" fixed="right">
        <template v-slot="scope">
          <el-button type="success" @click="handleEdit(scope.row)">
            编辑
            <edit style="width: 1em; height: 1em; margin-left: 8px" />
          </el-button>
          <el-popconfirm
            title="确定删除吗?"
            class="ml-5"
            confirm-button-text="确定"
            cancel-button-text="我再想想"
            icon-color="red"
            @confirm="del(scope.row.id)"
          >
            <template #reference>
              <el-button type="danger">
                删除
                <remove style="width: 1em; height: 1em; margin-left: 8px" />
              </el-button>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    <div style="padding: 10px 0">
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="pageNum"
        :page-sizes="[5, 10, 15, 20]"
        :page-size="pageSize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
      ></el-pagination>
    </div>
    <el-dialog title="公告信息" v-model="dialogFormVisible" width="30%">
      <el-form :model="form" label-width="120px">
        <el-form-item label="题目">
          <el-input v-model="form.title" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="内容">
          <el-input
            type="textarea"
            v-model="form.content"
            style="width: 80%"
          ></el-input>
        </el-form-item>
        <el-form-item label="配图">
          <el-upload
            class="avatar-uploader"
            :action="'http://' + serverIp + ':8080/files/upload'"
            :show-file-list="false"
            :on-success="handleAvatarSuccess"
            :before-upload="beforeAvatarUpload"
          >
            <img v-if="form.img" :src="form.img" class="avatar" />
            <el-icon v-else class="avatar-uploader-icon">
              <Plus style="margin: 75px" />
            </el-icon>
          </el-upload>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="switchType()" v-if="!form.title"
            >切换类型</el-button
          >
          <el-button @click="dialogFormVisible = false">取 消</el-button>
          <el-button type="primary" @click="save">确 定</el-button>
        </span>
      </template>
    </el-dialog>
    <el-dialog title="公告信息" v-model="dialogFormTwoVisible" width="60%">
      <el-form :model="form" label-width="120px">
        <el-form-item label="题目">
          <el-input v-model="form.title" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="配图">
          <el-upload
            class="avatar-uploader"
            :action="'http://' + serverIp + ':8080/files/upload'"
            :show-file-list="false"
            :on-success="handleAvatarSuccess"
            :before-upload="beforeAvatarUpload"
          >
            <img v-if="form.img" :src="form.img" class="avatar" />
            <el-icon v-else class="avatar-uploader-icon">
              <Plus style="margin:75px"/>
            </el-icon>
          </el-upload>
        </el-form-item>
        <el-form-item label="内容">
          <EditorBar
            v-model:value="form.content"
            style="width: 80%"
          ></EditorBar>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="switchType()" v-if="!form.title"
            >切换类型</el-button
          >
          <el-button @click="dialogFormTwoVisible = false">取 消</el-button>
          <el-button type="primary" @click="save">确 定</el-button>
        </span>
      </template>
    </el-dialog>
    <el-dialog title="预览" v-model="dialogpreviewVisible" width="60%">
      <el-card> <div v-html="content"></div></el-card>
    </el-dialog>
  </el-main>
</template>

<style scoped>
.avatar-uploader .avatar {
  width: 178px;
  height: 178px;
  display: block;
}
</style>
<style>
.avatar-uploader .el-upload {
  border: 1px dashed var(--el-border-color);
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: var(--el-transition-duration-fast);
}

.avatar-uploader .el-upload:hover {
  border-color: #409eff var(--el-color-primary);
}

.el-icon.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 178px;
  height: 178px;
  text-align: center;
}

#richText {
  width: 100%;
  height: 400px;
  background-color: #fff;
}
</style>

邮箱验证

包括登录验证、注册验证、忘记密码,以及验证码管理

UserController

@RestController
@RequestMapping("/user")
public class UserController {

    public static final ConcurrentHashMap<String, UserDTO> MAP = new ConcurrentHashMap<>();
    @Resource
    private IUserService userServiceImpl;
    @Resource
    private ILogService logServiceImpl;

    /**
     * 增加或修改用户
     *
     * @param user 用户
     * @return Result<Object>
     * @author 石一歌
     * @date 2022/4/10 22:39
     */
    @PostMapping
    public Result<?> save(@RequestBody User user) {
        if (ObjectUtil.isNull(user.getId()) && StrUtil.isBlank(user.getPassword())) {
            user.setPassword("123456");
        }
        if (ObjectUtil.isNull(user.getId())) {
            logServiceImpl.log(StrUtil.format("新增用户: {} ", user.getUsername()));
        } else {
            logServiceImpl.log(StrUtil.format("更新用户: {} ", user.getUsername()));
        }
        return userServiceImpl.saveOrUpdate(user) ? Result.success() : Result.error();
    }


    /**
     * 查询所有用户
     *
     * @return Result<Object>
     * @author 石一歌
     * @date 2022/4/10 22:40
     */
    @GetMapping
    public Result<?> findAll() {
        return CollUtil.isNotEmpty(userServiceImpl.list()) ? Result.success(userServiceImpl.list()) : Result.error();
    }


    /**
     * 删除用户
     *
     * @param id 用户id
     * @return Result<Object>
     * @author 石一歌
     * @date 2022/4/10 22:40
     */
    @DeleteMapping("/{id}")
    public Result<?> delete(@PathVariable Integer id) {
        logServiceImpl.log(StrUtil.format("删除用户: {} ", userServiceImpl.getById(id).getUsername()));
        return userServiceImpl.removeById(id) ? Result.success() : Result.error();
    }

    /**
     * 批量删除用户
     *
     * @param ids id列表
     * @return Result<Object>
     * @author 石一歌
     * @date 2022/4/11 15:46
     */
    @PostMapping("/del/batch")
    public Result<?> deleteBatch(@RequestBody List<Integer> ids) {
        ids.forEach(id -> logServiceImpl.log(StrUtil.format("删除用户: {} ", userServiceImpl.getById(id).getUsername())));
        return userServiceImpl.removeByIds(ids) ? Result.success() : Result.error();
    }

    /**
     * 分页查询 - mybatis-plus的方式
     *
     * @param pageNum  分页页码
     * @param pageSize 分页大小
     * @param username 用户名
     * @param nickname 用户昵称
     * @param address  用户住址
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/10 16:24
     */
    @GetMapping("/page")
    public Result<?> findPage(@RequestParam Integer pageNum,
                              @RequestParam Integer pageSize,
                              @RequestParam(defaultValue = "") String username,
                              @RequestParam(defaultValue = "") String nickname,
                              @RequestParam(defaultValue = "") String address) {
        IPage<User> page = new Page<>(pageNum, pageSize);
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        if (StrUtil.isNotBlank(username)) {
            queryWrapper.like("username", username);
        }
        if (StrUtil.isNotBlank(nickname)) {
            queryWrapper.like("nickname", nickname);
        }
        if (StrUtil.isNotBlank(address)) {
            queryWrapper.like("IPAddress", address);
        }
        queryWrapper.orderByDesc("id");
        return ObjectUtil.isNotEmpty(userServiceImpl.page(page, queryWrapper)) ? Result.success(userServiceImpl.page(page, queryWrapper)) : Result.error();
    }

    /**
     * 导出
     *
     * @param response 返回值
     * @author 石一歌
     * @date 2022/4/15 0:12
     */
    @GetMapping("/export")
    public void exportExcel(HttpServletResponse response) throws IOException {
        // 查询所有数据
        List<User> list = userServiceImpl.list();
        // 在内存操作,写出到浏览器
        ExcelWriter writer = ExcelUtil.getWriter(true);
        // 一次性写出list内的对象到excel
        writer.write(list, true);
        // 设置浏览器响应格式
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        String fileName = URLEncoder.encode("用户信息", "UTF-8");
        response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
        ServletOutputStream out = response.getOutputStream();
        writer.flush(out, true);
        writer.close();
        IoUtil.close(System.out);
    }

    /**
     * 导入
     *
     * @param file 文件
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/15 0:12
     */
    @PostMapping("/import")
    public Result<?> importExcel(MultipartFile file) throws IOException {
        InputStream inputStream = file.getInputStream();
        ExcelReader reader = ExcelUtil.getReader(inputStream);
        List<User> list = reader.readAll(User.class);
        return userServiceImpl.saveBatch(list) ? Result.success() : Result.error();
    }

    /**
     * 账号登录
     *
     * @param userDTO 用户数据
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/15 22:51
     */
    @PostMapping("/loginAccount")
    public Result<?> loginAccount(@RequestBody UserDTO userDTO) {
        String username = userDTO.getUsername();
        String password = userDTO.getPassword();
        if (StrUtil.isBlank(username) || StrUtil.isBlank(password)) {
            return Result.error(StatusEnum.CODE_400.getStatus(), StatusEnum.CODE_400.getName());
        }
        UserDTO resDTO = userServiceImpl.loginAccount(userDTO);
        MAP.put(resDTO.getUsername(), resDTO);
        logServiceImpl.log(StrUtil.format("用户 {} 登录系统", resDTO.getUsername()), resDTO.getUsername());
        return BeanUtil.isNotEmpty(resDTO) ? Result.success(resDTO) : Result.error();
    }

    /**
     * 邮箱登录
     *
     * @param userDTO 用户资料
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:41
     */
    @PostMapping("/loginEmail")
    public Result<?> loginEmail(@RequestBody UserDTO userDTO) {
        String email = userDTO.getEmail();
        String code = userDTO.getCode();
        if (StrUtil.isBlank(email) || StrUtil.isBlank(code)) {
            return Result.error(StatusEnum.CODE_400.getStatus(), StatusEnum.CODE_400.getName());
        }
        UserDTO resDTO = userServiceImpl.loginEmail(userDTO);
        MAP.put(resDTO.getUsername(), resDTO);
        logServiceImpl.log(StrUtil.format("用户 {} 登录系统", resDTO.getUsername()), resDTO.getUsername());
        return BeanUtil.isNotEmpty(resDTO) ? Result.success(resDTO) : Result.error();
    }

    /**
     * 注册
     *
     * @param userDTO 用户数据
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/16 15:30
     */
    @PostMapping("/register")
    public Result<?> register(@RequestBody UserDTO userDTO) {
        String username = userDTO.getUsername();
        String password = userDTO.getPassword();
        String email = userDTO.getEmail();
        String code = userDTO.getCode();
        if (StrUtil.isBlank(username) || StrUtil.isBlank(password) || StrUtil.isBlank(email) || StrUtil.isBlank(code)) {
            return Result.error(StatusEnum.CODE_400.getStatus(), StatusEnum.CODE_400.getName());
        }
        logServiceImpl.log(StrUtil.format("用户 {} 注册账号成功", userDTO.getUsername()), userDTO.getUsername());
        return userServiceImpl.register(userDTO) ? Result.success() : Result.error();
    }

    /**
     * 退出
     *
     * @param username 用户名
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/12 14:08
     */
    @GetMapping("/logout/{username}")
    public Result<?> logout(@PathVariable String username) {
        if (StrUtil.isNotBlank(username)) {
            logServiceImpl.log(StrUtil.format("用户 {} 退出系统", username));
            MAP.remove(username);
        }
        return Result.success();
    }

    /**
     * 在线人数
     *
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/12 14:08
     */
    @GetMapping("/online")
    public Result<?> online() {
        return Result.success(MAP.size());
    }

    /**
     * 根据用户名查找用户
     *
     * @param username 用户名
     * @return Result
     * @author 石一歌
     * @date 2022/4/16 15:35
     */
    @GetMapping("/username/{username}")
    public Result<?> findByUsername(@PathVariable String username) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        if (StrUtil.isBlank(username)) {
            return Result.error(StatusEnum.CODE_400.getStatus(), StatusEnum.CODE_400.getName());
        }
        queryWrapper.eq("username", username);
        User one = userServiceImpl.getOne(queryWrapper);
        return BeanUtil.isNotEmpty(one) ? Result.success(one) : Result.error();
    }

    /**
     * 获取用户信息
     *
     * @param userDTO 用户信息
     * @return Result<?>
     * @author 石一歌
     * @date 2022/4/22 14:42
     */
    @PostMapping("/userInfo")
    public Result<?> userInfo(@RequestBody UserDTO userDTO) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", userDTO.getUsername());
        queryWrapper.eq("password", userDTO.getPassword());
        User one;
        try {
            one = userServiceImpl.getOne(queryWrapper);
        } catch (Exception e) {
            throw new ServiceException(StatusEnum.CODE_500.getStatus(), StatusEnum.CODE_500.getName());
        }
        if (BeanUtil.isNotEmpty(one)) {
            BeanUtil.copyProperties(one, userDTO, true);
            return Result.success(userDTO);
        } else {
            throw new ServiceException(StatusEnum.CODE_600.getStatus(), "用户名或密码错误");
        }
    }

    /**
     * 更新密码
     *
     * @param userPasswordDTO 密码dto
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/7 15:31
     */
    @PostMapping("/updatePassword")
    public Result<?> updatePassword(@RequestBody UserPasswordDTO userPasswordDTO) {
        return userServiceImpl.updatePassword(userPasswordDTO) ? Result.success() : Result.error();
    }

    /**
     * 重置密码
     *
     * @param userDTO 用户资料
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:41
     */
    @PostMapping("/resetPassword")
    public Result<?> resetPassword(@RequestBody UserDTO userDTO) {
        String email = userDTO.getEmail();
        String code = userDTO.getCode();
        if (StrUtil.isBlank(email) || StrUtil.isBlank(code)) {
            return Result.error(StatusEnum.CODE_400.getStatus(), StatusEnum.CODE_400.getName());
        }
        userServiceImpl.resetPassword(userDTO);
        return Result.success();
    }

    /**
     * 发送邮箱验证码
     *
     * @param email 邮箱
     * @param type  验证码类型
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:42
     */
    @GetMapping("/sendEmailCode/{email}/{username}/{type}")
    public Result<?> sendEmailCode(@PathVariable String email, @PathVariable Integer type, @PathVariable String username) {
        if (StrUtil.isBlank(email) || ObjectUtil.isNull(type) || StrUtil.isBlank(username)) {
            throw new ServiceException(StatusEnum.CODE_400.getStatus(), StatusEnum.CODE_400.getName());
        }
        userServiceImpl.sendEmailCode(email, type, username);
        return Result.success();
    }

}

UserService

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Resource
    private UserMapper userMapper;
    @Resource
    private IUserRoleService userRoleServiceImpl;
    @Resource
    private IRoleService roleServiceImpl;
    @Resource
    private IRoleMenuService roleMenuServiceImpl;
    @Resource
    private IMenuService menuServiceImpl;
    @Resource
    private JavaMailSender javaMailSender;
    @Value("${spring.mail.username}")
    private String from;
    @Resource
    private IValidationService validationServiceImpl;

    /**
     * 邮箱登录
     *
     * @param userDTO 用户信息
     * @return UserDTO
     * @author 石一歌
     * @date 2022/5/20 14:58
     */
    @Override
    public UserDTO loginEmail(UserDTO userDTO) {
        QueryWrapper<Validation> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("email", userDTO.getEmail());
        queryWrapper.eq("type", ValidationEnum.Login.getType());
        queryWrapper.eq("code", userDTO.getCode());
        queryWrapper.ge("expiration_time", DateUtil.date());
        Validation validation = validationServiceImpl.getOne(queryWrapper);
        if (ObjectUtil.isNull(validation.getId())) {
            throw new ServiceException(-1, "验证码已过期,请重新发送。");
        }

        QueryWrapper<User> queryWrapperTwo = new QueryWrapper<>();
        queryWrapperTwo.eq("email", userDTO.getEmail());
        User one = getOne(queryWrapperTwo);

        if (BeanUtil.isNotEmpty(one)) {
            BeanUtil.copyProperties(one, userDTO, true);
            userDTO.setToken(TokenUtils.getToken(one.getId(), one.getUsername()));
            setRolesAndMenus(userDTO);
            return userDTO;
        } else {
            throw new ServiceException(-1, "该邮箱未被使用!");
        }
    }

    /**
     * 账号登录
     *
     * @param userDTO 用户数据
     * @return UserDTO
     * @author 石一歌
     * @date 2022/4/16 15:29
     */
    @Override
    public UserDTO loginAccount(UserDTO userDTO) {
        User one = getUserInfo(userDTO);
        if (BeanUtil.isNotEmpty(one)) {
            BeanUtil.copyProperties(one, userDTO, true);
            userDTO.setToken(TokenUtils.getToken(one.getId(), one.getUsername()));
            setRolesAndMenus(userDTO);
            return userDTO;
        } else {
            throw new ServiceException(StatusEnum.CODE_600.getStatus(), "用户名或密码错误");
        }
    }

    /**
     * 注册
     *
     * @param userDTO 用户数据
     * @return User
     * @author 石一歌
     * @date 2022/4/16 15:29
     */
    @Override
    public Boolean register(UserDTO userDTO) {
        QueryWrapper<Validation> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("email", userDTO.getEmail());
        queryWrapper.eq("type", ValidationEnum.Register.getType());
        queryWrapper.eq("code", userDTO.getCode());
        queryWrapper.ge("expiration_time", DateUtil.date());
        Validation validation = validationServiceImpl.getOne(queryWrapper);
        if (ObjectUtil.isNull(validation.getId())) {
            throw new ServiceException(-1, "验证码已过期,请重新发送。");
        }
        User one = getUserInfo(userDTO);
        if (BeanUtil.isEmpty(one)) {
            one = new User();
            BeanUtil.copyProperties(userDTO, one, true);
            save(one);
            List<Integer> list = new ArrayList<>();
            list.add(6);
            userRoleServiceImpl.setUserRole(one.getId(), list);
            return true;
        } else {
            throw new ServiceException(StatusEnum.CODE_600.getStatus(), "用户已存在");
        }
    }

    /**
     * 用户查找
     *
     * @param userDTO 用户数据
     * @return User
     * @author 石一歌
     * @date 2022/4/16 15:28
     */
    private User getUserInfo(UserDTO userDTO) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", userDTO.getUsername());
        queryWrapper.eq("password", userDTO.getPassword());
        User one;
        try {
            one = getOne(queryWrapper);
        } catch (Exception e) {
            throw new ServiceException(StatusEnum.CODE_500.getStatus(), StatusEnum.CODE_500.getName());
        }
        return one;
    }

    /**
     * 统计用户地址
     *
     * @return List<AddressDTO>
     * @author 石一歌
     * @date 2022/4/22 16:52
     */
    @Override
    public List<AddressDTO> countAddress() {
        return userMapper.countAddress();
    }

    /**
     * 更新密码
     *
     * @param userPasswordDTO 密码dto
     * @return Boolean
     * @author 石一歌
     * @date 2022/5/7 15:30
     */
    @Override
    public Boolean updatePassword(UserPasswordDTO userPasswordDTO) {
        if (userMapper.updatePassword(userPasswordDTO) < 1) {
            throw new ServiceException(StatusEnum.CODE_600.getStatus(), "密码错误");
        } else {
            return true;
        }
    }

    /**
     * 发送验证码
     *
     * @param email 邮箱
     * @param type  类型
     * @author 石一歌
     * @date 2022/5/20 14:59
     */
    @Override
    public void sendEmailCode(String email, Integer type, String username) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("email", email);
        User one = getOne(queryWrapper);
        if (BeanUtil.isEmpty(one) && !type.equals(ValidationEnum.Register.getType())) {
            throw new ServiceException(-1, "该邮箱未被使用!");
        } else if (BeanUtil.isNotEmpty(one) && type.equals(ValidationEnum.Register.getType())) {
            throw new ServiceException(-1, "该邮箱已经被注册!");
        }

        DateTime now = DateUtil.date();
        QueryWrapper<Validation> queryWrapperTwo = new QueryWrapper<>();
        queryWrapperTwo.eq("email", email);
        queryWrapperTwo.eq("type", type);
        queryWrapperTwo.ge("expiration_time", now);
        Validation validation = validationServiceImpl.getOne(queryWrapperTwo);
        if (BeanUtil.isNotEmpty(validation)) {
            throw new ServiceException(-1, "当前,您的验证码仍然有效,请不要重复发送!");
        }
        MimeMessage mimeMessage = javaMailSender.createMimeMessage();
        MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage);
        String content = "<p>\n" +
                "    尊敬的用户:\n" +
                "</p>\n" +
                "<p>\n" +
                "    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 您好,您的本次登录验证码为 <strong>{}</strong>,请妥善保管,切勿发送给他人,有效期为5分钟,请尽快通过<a href=\"服务器地址\">测试链接</a>登录系统。\n" +
                "</p>";
        String contentTwo = "<p>\n" +
                "    尊敬的用户:\n" +
                "</p>\n" +
                "<p>\n" +
                "    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 欢迎注册脚手架系统,本次验证码为 <strong>{}</strong>,请妥善保管,切勿发送给他人,有效期为5分钟。\n" +
                "</p>";
        String contentThree = "<p>\n" +
                "    尊敬的用户:\n" +
                "</p>\n" +
                "<p>\n" +
                "    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 您好,本次验证码为 <strong>{}</strong>,请妥善保管,切勿发送给他人,有效期为5分钟,请尽快重置密码。\n" +
                "</p>";
        String code = RandomUtil.randomString(6);

        try {
            mimeMessageHelper.setFrom(from);
            mimeMessageHelper.setTo(email);
            mimeMessageHelper.setSentDate(now);
            if (ValidationEnum.Login.getType().equals(type)) {
                mimeMessageHelper.setSubject("【脚手架系统】登录验证码");
                mimeMessageHelper.setText(StrUtil.format(content, code), true);
            } else if (ValidationEnum.Register.getType().equals(type)) {
                mimeMessageHelper.setSubject("【脚手架系统】注册验证码");
                mimeMessageHelper.setText(StrUtil.format(contentTwo, code), true);
            } else if (ValidationEnum.FORGET_PASS.getType().equals(type)) {
                mimeMessageHelper.setSubject("【脚手架系统】重置验证码");
                mimeMessageHelper.setText(StrUtil.format(contentThree, code), true);
            }
            javaMailSender.send(mimeMessage);
        } catch (MessagingException e) {
            throw new ServiceException(StatusEnum.CODE_500.getStatus(), "验证码发送失败!");
        }
        if (!type.equals(ValidationEnum.Register.getType())) {
            username = one.getUsername();
        }
        validationServiceImpl.saveValidation(Validation.builder()
                .username(username)
                .email(email)
                .code(code)
                .type(type)
                .expirationTime(LocalDateTimeUtil.of(DateUtil.offsetMinute(now, 5)))
                .build());
    }

    /**
     * 重置密码
     *
     * @param userDTO 用户信息
     * @author 石一歌
     * @date 2022/5/20 14:59
     */
    @Override
    public void resetPassword(UserDTO userDTO) {
        QueryWrapper<Validation> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("email", userDTO.getEmail());
        queryWrapper.eq("type", ValidationEnum.FORGET_PASS.getType());
        queryWrapper.eq("code", userDTO.getCode());
        queryWrapper.ge("expiration_time", DateUtil.date());
        Validation validation = validationServiceImpl.getOne(queryWrapper);
        if (ObjectUtil.isNull(validation.getId())) {
            throw new ServiceException(-1, "验证码已过期,请重新发送。");
        }
        QueryWrapper<User> queryWrapperTwo = new QueryWrapper<>();
        queryWrapperTwo.eq("email", userDTO.getEmail());
        User user = userMapper.selectOne(queryWrapperTwo);
        if (BeanUtil.isNotEmpty(user)) {
            user.setPassword("123456");
            userMapper.updateById(user);
        } else {
            throw new ServiceException(-1, "用户不存在!");
        }
    }

    /**
     * 设置权限
     *
     * @param userDTO 用户dto
     * @author 石一歌
     * @date 2022/5/7 15:29
     */
    private void setRolesAndMenus(UserDTO userDTO) {
        HashSet<Menu> menuSet = new HashSet<>();
        ArrayList<Role> roleList = new ArrayList<>();
        // 用户id查找userRole表
        // 角色id添加到roleList
        List<UserRole> userRoles = userRoleServiceImpl.getUserRole(userDTO.getId());
        userRoles.forEach(v -> roleList.add(roleServiceImpl.getById(v.getRoleId())));
        // 设置角色
        userDTO.setRoles(roleList);

        List<Integer> roles = userRoles.stream().map(UserRole::getRoleId).distinct().collect(Collectors.toList());
        // 角色id查找roleMenu表
        // 菜单添加到menuSet
        for (Integer roleId : roles) {
            List<RoleMenu> roleMenus = roleMenuServiceImpl.getRoleMenu(roleId);
            roleMenus.forEach(v -> menuSet.add(menuServiceImpl.getById(v.getMenuId())));
        }
        // 设置二级菜单
        List<Menu> parent = menuSet.stream().filter(menu -> ObjectUtil.isNull(menu.getPid())).collect(Collectors.toList());
        // 子菜单排序
        List<Menu> children = menuSet.stream().sorted(Comparator.comparing(Menu::getSortNum)).collect(Collectors.toList());
        for (Menu parentMenu : parent) {
            parentMenu.setChildren(children.stream().filter(childMenu -> parentMenu.getId().equals(childMenu.getPid())).collect(Collectors.toList()));
        }
        // 菜单排序
        List<Menu> menuList = parent.stream().sorted(Comparator.comparing(Menu::getSortNum)).collect(Collectors.toList());
        // 设置菜单
        userDTO.setMenus(menuList);
    }
}

UserMapper

public interface UserMapper extends BaseMapper<User> {
    /**
     * 统计用户地址
     *
     * @return List<AddressDTO>
     * @author 石一歌
     * @date 2022/4/22 16:49
     */
    @Select("select count(id) count, address from sys_user GROUP BY address")
    List<AddressDTO> countAddress();

    /**
     * 更新密码
     *
     * @param userPasswordDTO 密码dto
     * @return int
     * @author 石一歌
     * @date 2022/5/7 14:54
     */
    @Update("update sys_user set password = #{newPassword} where username = #{username} and password = #{password}")
    int updatePassword(UserPasswordDTO userPasswordDTO);
}

ValidationController

@RestController
@RequestMapping("/validation")
public class ValidationController {

    @Resource
    private IValidationService validationServiceImpl;

    /**
     * 保存验证码
     *
     * @param validation 验证码
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:43
     */
    @PostMapping
    public Result<?> save(@RequestBody Validation validation) {
        return validationServiceImpl.saveOrUpdate(validation) ? Result.success() : Result.error();
    }

    /**
     * 删除验证码
     *
     * @param id 验证码id
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:43
     */
    @DeleteMapping("/{id}")
    public Result<?> delete(@PathVariable Integer id) {
        return validationServiceImpl.removeById(id) ? Result.success() : Result.error();
    }

    /**
     * 批量删除验证码
     *
     * @param ids id数组
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:43
     */
    @PostMapping("/del/batch")
    public Result<?> deleteBatch(@RequestBody List<Integer> ids) {
        return validationServiceImpl.removeByIds(ids) ? Result.success() : Result.error();
    }

    /**
     * TODO
     *
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:43
     */
    @GetMapping
    public Result<?> findAll() {
        return CollUtil.isNotEmpty(validationServiceImpl.list()) ? Result.success(validationServiceImpl.list()) : Result.error();
    }

    /**
     * 根据id查找验证码
     *
     * @param id 验证码id
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:43
     */
    @GetMapping("/{id}")
    public Result<?> findOne(@PathVariable Integer id) {
        return BeanUtil.isNotEmpty(validationServiceImpl.getById(id)) ? Result.success(validationServiceImpl.getById(id)) : Result.error();
    }

    /**
     * 分页查找
     *
     * @param pageNum  分页页码
     * @param pageSize 分页大小
     * @param username 用户名
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:43
     */
    @GetMapping("/page")
    public Result<?> findPage(@RequestParam Integer pageNum,
                              @RequestParam Integer pageSize,
                              @RequestParam(defaultValue = "") String username,
                              @RequestParam(defaultValue = "0") Integer type) {
        IPage<Validation> page = new Page<>(pageNum, pageSize);
        QueryWrapper<Validation> queryWrapper = new QueryWrapper<>();
        if (StrUtil.isNotBlank(username)) {
            queryWrapper.like("username", username);
        }
        if (type != null && type != 0) {
            queryWrapper.like("type", type);
        }
        queryWrapper.orderByDesc("id");
        return ObjectUtil.isNotEmpty(validationServiceImpl.page(page, queryWrapper)) ? Result.success(validationServiceImpl.page(page, queryWrapper)) : Result.error();
    }

    /**
     * excel导出
     *
     * @param response 响应
     * @author 石一歌
     * @date 2022/5/20 14:43
     */
    @GetMapping("/export")
    public void exportExcel(HttpServletResponse response) throws IOException {
        List<Validation> list = validationServiceImpl.list();
        ExcelWriter writer = ExcelUtil.getWriter(true);
        writer.write(list, true);
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        String fileName = URLEncoder.encode("xx信息", "UTF-8");
        response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
        ServletOutputStream out = response.getOutputStream();
        writer.flush(out, true);
        writer.close();
        IoUtil.close(System.out);
    }

    /**
     * excel导入
     *
     * @param file 文件
     * @return Result<?>
     * @author 石一歌
     * @date 2022/5/20 14:43
     */

    @PostMapping("/import")
    public Result<?> importExcel(MultipartFile file) throws IOException {
        InputStream inputStream = file.getInputStream();
        ExcelReader reader = ExcelUtil.getReader(inputStream);
        List<Validation> list = reader.readAll(Validation.class);
        return validationServiceImpl.saveBatch(list) ? Result.success() : Result.error();
    }

}

Login.vue

<script setup lang="ts">
import { reactive, ref } from "vue";
import { Lock, User, Message } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import request from "@/utils/request";
import router, { setRoutes } from "@/router";
import Vcode from "vue3-puzzle-vcode";
const user = reactive({
  username: "",
  password: "",
  email: "",
  code: "",
});
const ruleAccountRef = ref();
const ruleEmailRef = ref();
const rules = reactive({
  username: [
    { required: true, message: "请输入用户名", trigger: "blur" },
    { min: 3, max: 10, message: "长度在 3 到 5 个字符", trigger: "blur" },
  ],
  password: [
    { required: true, message: "请输入密码", trigger: "blur" },
    { min: 1, max: 20, message: "长度在 1 到 20 个字符", trigger: "blur" },
  ],
  email: [
    { required: true, message: "请输入邮箱", trigger: "blur" },
    { min: 3, max: 20, message: "长度在 3 到 20 个字符", trigger: "blur" },
  ],
  code: [
    { required: true, message: "请输入验证码", trigger: "blur" },
    { min: 6, max: 6, message: "长度为6个字符", trigger: "blur" },
  ],
});
const loginAccount = async (formEl: {
  validate: (arg0: (valid: any, fields: any) => void) => any;
}) => {
  if (!formEl) return;
  if (!isCheck.value) {
    ElMessage.error("请进行人机验证");
    return;
  }
  await formEl.validate((valid) => {
    if (valid) {
      request.post("/user/loginAccount", user).then((res) => {
        if (res.status === 200) {
          localStorage.setItem("user", JSON.stringify(res.data));
          localStorage.setItem("menus", JSON.stringify(res.data.menus));
          setRoutes();
          router.push("/");
          ElMessage.success("登录成功");
        } else {
          ElMessage.error(res.statusText);
        }
      });
    }
  });
};
const loginEmail = async (formEl: {
  validate: (arg0: (valid: any, fields: any) => void) => any;
}) => {
  if (!formEl) return;
  await formEl.validate((valid) => {
    if (valid) {
      request.post("/user/loginEmail", user).then((res) => {
        if (res.status === 200) {
          localStorage.setItem("user", JSON.stringify(res.data));
          localStorage.setItem("menus", JSON.stringify(res.data.menus));
          setRoutes();
          router.push("/");
          ElMessage.success("登录成功");
        } else {
          ElMessage.error(res.statusText);
        }
      });
    }
  });
};
const activeName = ref("first");
const sendEmailCode = (type: any) => {
  if (
    !/^\w+((.\w+)|(-\W+))@[A-Za-z0-9]+((.|-)[A-Za-z0-9]+).[A-Za-z0-9]+$/.test(
      user.email
    )
  ) {
    ElMessage.warning("请输入正确的邮箱");
  } else {
    request
      .get(
        "/user/sendEmailCode/" + user.email + "/" + user.username + "/" + type
      )
      .then((res) => {
        if (res.status === 200) {
          ElMessage.success("发送成功");
        } else {
          ElMessage.error(res.statusText);
        }
      });
  }
};
const dialogPasswordVisible = ref(false);
const passwordBack = () => {
  dialogPasswordVisible.value = false;
  request.post("/user/resetPassword", user).then((res) => {
    if (res.status === 200) {
      ElMessage.success("重置成功,密码为123456,请尽快修改密码");
    } else {
      ElMessage.error(res.statusText);
    }
  });
};
const handlePassword = () => {
  dialogPasswordVisible.value = true;
  user.email = "";
  user.code = "";
};
const reset = () => {
  dialogPasswordVisible.value = false;
  user.email = "";
  user.code = "";
};
const isCheck = ref(false);
const isShow = ref(false);

const onShow = () => {
  isShow.value = true;
};

const onClose = () => {
  isShow.value = false;
};

const onSuccess = () => {
  onClose();
  isCheck.value = true;
};
</script>

<template>
  <div class="wrapper">
    <el-tabs
      v-model="activeName"
      type="card"
      class="demo-tabs"
      style="
        margin: 200px auto;
        background: white;
        width: 485px;
        height: 350px;
        border-radius: 10px;
        overflow: hidden;
      "
    >
      <el-tab-pane label="账号登录" name="first">
        <div
          style="
            margin: auto;
            background-color: #fff;
            width: 300px;
            height: 400px;
            padding: 20px;
            border-radius: 10px;
          "
        >
          <el-form :model="user" :rules="rules" ref="ruleAccountRef">
            <el-form-item prop="username">
              <el-input
                placeholder="输入账号"
                :prefix-icon="User"
                style="margin: 10px 0"
                v-model="user.username"
              >
              </el-input>
            </el-form-item>
            <el-form-item prop="password">
              <el-input
                placeholder="输入密码"
                :prefix-icon="Lock"
                style="margin: 10px 0"
                show-password
                v-model="user.password"
              >
              </el-input>
            </el-form-item>
            <el-form-item prop="password">
              <el-button
                style="width: 100%"
                type="info"
                autocomplete="off"
                @click="onShow"
                >人机验证
              </el-button>
            </el-form-item>
            <el-form-item style="margin: 10px 0; text-align: center">
              <el-button
                style="width: 100%"
                type="primary"
                autocomplete="off"
                @click="loginAccount(ruleAccountRef)"
                >登 录
              </el-button>
            </el-form-item>
            <el-form-item>
              <el-button
                type="text"
                autocomplete="off"
                @click="router.push('/register')"
                >前往注册 >></el-button
              ><el-button
                type="text"
                autocomplete="off"
                @click="handlePassword()"
                style="position: absolute; right: 0"
                >找回密码
              </el-button>
            </el-form-item>
          </el-form>
        </div></el-tab-pane
      >
      <el-tab-pane label="邮箱登录" name="second">
        <div
          style="
            margin: auto;
            background-color: #fff;
            width: 300px;
            height: 250px;
            padding: 20px;
            border-radius: 10px;
          "
        >
          <el-form :model="user" :rules="rules" ref="ruleEmailRef">
            <el-form-item prop="email">
              <el-input
                placeholder="输入邮箱"
                :prefix-icon="Message"
                style="margin: 10px 0"
                v-model="user.email"
              >
              </el-input>
            </el-form-item>
            <el-form-item prop="code">
              <el-input
                placeholder="输入验证码"
                :prefix-icon="Lock"
                style="margin: 10px 0; width: 170px"
                v-model="user.code"
              >
              </el-input>
              <el-button
                type="warning"
                autocomplete="off"
                size="small"
                class="ml-5"
                @click="sendEmailCode(1)"
                >获取验证码</el-button
              >
            </el-form-item>
            <el-form-item style="margin: 10px 0; text-align: center">
              <el-button
                style="width: 100%"
                type="primary"
                autocomplete="off"
                @click="loginEmail(ruleEmailRef)"
                >登 录
              </el-button>
            </el-form-item>
            <el-form-item>
              <el-button
                type="text"
                autocomplete="off"
                @click="router.push('/register')"
                >前往注册 >></el-button
              >
              <el-button
                type="text"
                autocomplete="off"
                @click="handlePassword()"
                style="position: absolute; right: 0"
                >找回密码
              </el-button>
            </el-form-item>
          </el-form>
        </div></el-tab-pane
      >
    </el-tabs>
    <el-dialog title="找回密码" v-model="dialogPasswordVisible" width="25%">
      <el-form :model="user" :rules="rules" ref="ruleEmailRef">
        <el-form-item prop="email">
          <el-input
            placeholder="输入邮箱"
            :prefix-icon="Message"
            style="margin: 10px 50px"
            v-model="user.email"
          >
          </el-input>
        </el-form-item>
        <el-form-item prop="code" style="margin: 0 50px">
          <el-input
            placeholder="输入验证码"
            :prefix-icon="Lock"
            style="width: 160px"
            v-model="user.code"
          >
          </el-input>
          <el-button
            type="warning"
            autocomplete="off"
            size="small"
            class="ml-5"
            style="width: 70px"
            @click="sendEmailCode(3)"
            >获取验证码</el-button
          >
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="reset()">取 消</el-button>
          <el-button type="primary" @click="passwordBack()">确 定</el-button>
        </span>
      </template>
    </el-dialog>
    <Vcode :show="isShow" @success="onSuccess" @close="onClose" />
  </div>
</template>

<style scoped>
.wrapper {
  height: 100vh;
  background-image: linear-gradient(to bottom right, #fc466b, #3f5efb);
  overflow: hidden;
}
</style>

Register.vue

<script setup lang="ts">
import { reactive, ref } from "vue";
import { Lock, User, Message } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import request from "@/utils/request";
import router from "@/router";
const user = reactive({
  username: "",
  password: "",
  confirmPassword: "",
  email: "",
  code: "",
});
const ruleFormRef = ref();
const rules = reactive({
  username: [
    { required: true, message: "请输入用户名", trigger: "blur" },
    { min: 3, max: 10, message: "长度在 3 到 5 个字符", trigger: "blur" },
  ],
  password: [
    { required: true, message: "请输入密码", trigger: "blur" },
    { min: 1, max: 20, message: "长度在 1 到 20 个字符", trigger: "blur" },
  ],
  confirmPassword: [
    { required: true, message: "请再次输入密码", trigger: "blur" },
    { min: 1, max: 20, message: "长度在 1 到 20 个字符", trigger: "blur" },
  ],
  email: [
    { required: true, message: "请输入邮箱", trigger: "blur" },
    { min: 3, max: 20, message: "长度在 3 到 20 个字符", trigger: "blur" },
  ],
  code: [
    { required: true, message: "请输入验证码", trigger: "blur" },
    { min: 6, max: 6, message: "长度为6个字符", trigger: "blur" },
  ],
});
const register = async (formEl: {
  validate: (arg0: (valid: any, fields: any) => void) => any;
}) => {
  if (!formEl) return;
  await formEl.validate((valid) => {
    if (valid) {
      if (user.password !== user.confirmPassword) {
        ElMessage.error("两次输入的密码不一致");
        return false;
      }
      request.post("/user/register", user).then((res) => {
        if (res.status === 200) {
          ElMessage.success("注册成功");
        } else {
          ElMessage.error(res.statusText);
        }
      });
    }
  });
};
const sendEmailCode = (type: any) => {
  if (
    !/^\w+((.\w+)|(-\W+))@[A-Za-z0-9]+((.|-)[A-Za-z0-9]+).[A-Za-z0-9]+$/.test(
      user.email
    )
  ) {
    ElMessage.warning("请输入正确的邮箱");
  } else {
    request
      .get(
        "/user/sendEmailCode/" + user.email + "/" + user.username + "/" + type
      )
      .then((res) => {
        if (res.status === 200) {
          ElMessage.success("发送成功");
        } else {
          ElMessage.error(res.statusText);
        }
      });
  }
};
</script>

<template>
  <div class="wrapper">
    <div
      style="
        margin: 100px auto;
        background-color: #fff;
        width: 350px;
        height: 530px;
        padding: 20px;
        border-radius: 10px;
      "
    >
      <div style="margin: 20px 0; text-align: center; font-size: 24px">
        <b>注 册</b>
      </div>

      <el-form :model="user" :rules="rules" ref="ruleFormRef">
        <el-form-item prop="username">
          <el-input
            placeholder="输入账号"
            :prefix-icon="User"
            style="margin: 10px 0"
            v-model="user.username"
          >
          </el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input
            placeholder="输入密码"
            :prefix-icon="Lock"
            style="margin: 10px 0"
            show-password
            v-model="user.password"
          >
          </el-input>
        </el-form-item>
        <el-form-item prop="confirmPassword">
          <el-input
            placeholder="确认密码"
            :prefix-icon="Lock"
            style="margin: 10px 0"
            show-password
            v-model="user.confirmPassword"
          >
          </el-input>
        </el-form-item>
        <el-form-item prop="email">
          <el-input
            placeholder="输入邮箱"
            :prefix-icon="Message"
            style="margin: 10px 0"
            v-model="user.email"
          >
          </el-input>
        </el-form-item>
        <el-form-item prop="code">
          <el-input
            placeholder="输入验证码"
            :prefix-icon="Lock"
            style="margin: 10px 0; width: 170px"
            v-model="user.code"
          >
          </el-input>
          <el-button
            type="warning"
            autocomplete="off"
            size="small"
            class="ml-5"
            @click="sendEmailCode(2)"
            >获取验证码</el-button
          >
        </el-form-item>
        <el-form-item style="margin: 10px 0; text-align: center">
          <el-button
            style="width: 100%"
            type="primary"
            autocomplete="off"
            @click="register(ruleFormRef)"
            >注 册
          </el-button>
        </el-form-item>
        <el-form-item style="margin: 20px 0; text-align: center">
          <el-button
            style="width: 100%"
            type="warning"
            autocomplete="off"
            @click="router.push('/login')"
            >登 录
          </el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<style scoped>
.wrapper {
  height: 100vh;
  background-image: linear-gradient(to bottom right, #fc466b, #3f5efb);
  overflow: hidden;
}
</style>

Validation.vue

<script setup lang="ts">
import { ref } from "vue";
import { Remove, Download, Upload, Search } from "@element-plus/icons-vue";
import request from "@/utils/request";
import { ElMessage } from "element-plus";
import { serverIp } from "../../public/config";
const tableData = ref([]);
const total = ref<number>(0);
const pageNum = ref<number>(1);
const pageSize = ref<number>(5);
const username = ref<string>("");
const type = ref<number>(0);
const multipleSelection = ref<any>([]);

const headerBg = ref("headerBg");
const load = () => {
  request
    .get("/validation/page", {
      params: {
        pageNum: pageNum.value,
        pageSize: pageSize.value,
        username: username.value,
        type: type.value,
      },
    })
    .then((res) => {
      if (res.status === 200) {
        tableData.value = res.data.records;
        total.value = res.data.total;
      } else {
        ElMessage.error("加载失败");
      }
    });
};
load();
const handleSelectionChange = (val: never[]) => {
  multipleSelection.value = val;
};
const del = (id: number) => {
  request.delete("/validation/" + id).then((res) => {
    if (res.status === 200) {
      ElMessage.success("删除成功");
      load();
    } else {
      ElMessage.error("删除失败");
    }
  });
};
const delBatch = () => {
  let ids: number[] = [];
  multipleSelection.value.forEach((element: any) => {
    ids.push(element.id);
  });
  request.post("/validation/del/batch", ids).then((res) => {
    if (res.status === 200) {
      ElMessage.success("批量删除成功");
      load();
    } else {
      ElMessage.error("批量删除失败");
    }
  });
};
const reset = () => {
  username.value = "";
  type.value = 0;
  load();
};
const handleSizeChange = (size: number) => {
  pageSize.value = size;
  load();
};
const handleCurrentChange = (num: number) => {
  pageNum.value = num;
  load();
};
const dataFormat = (data: string) => {
  if (data === null) return null;
  else return data.split("T").join(" ");
};
const exp = () => {
  window.open(`http://${serverIp}:8080/validation/export`);
};
const handleExcelImportSuccess = () => {
  ElMessage.success("导入成功");
};
const typeFormat = (typeId: number) => {
  if (typeId === 1) return "登录";
  if (typeId === 2) return "注册";
  if (typeId === 3) return "忘记密码";
};
const options = [
  { value: 0, label: "全部" },
  { value: 1, label: "登录" },
  { value: 2, label: "注册" },
  { value: 3, label: "忘记密码" },
];
</script>

<template>
  <el-main>
    <div style="margin: 10px 0">
      <el-input
        style="width: 200px"
        placeholder="请输入用户名"
        :suffix-icon="Search"
        class="ml-5"
        v-model="username"
      >
      </el-input>
      <el-select v-model="type" class="ml-5" placeholder="Select">
        <el-option
          v-for="item in options"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        />
      </el-select>
      <el-button class="ml-5" type="primary" @click="load"
        >搜索 <search style="width: 1em; height: 1em; margin-left: 8px"
      /></el-button>
      <el-button class="ml-5" type="warning" @click="reset"
        >清空
        <e-icon
          icon-name="fa fa-trash-o"
          style="width: 1em; height: 1em; margin-left: 8px"
        />
      </el-button>
    </div>

    <div style="margin: 10px 0; position: absolut">
      <el-popconfirm
        title="确定删除吗?"
        class="ml-5"
        confirm-button-text="确定"
        cancel-button-text="我再想想"
        icon-color="red"
        @confirm="delBatch"
      >
        <template #reference>
          <el-button type="danger">
            批量删除
            <remove style="width: 1em; height: 1em; margin-left: 8px" />
          </el-button>
        </template>
      </el-popconfirm>
      <el-upload
        :action="'http://' + serverIp + ':8080/log/import'"
        :show-file-list="false"
        accept="xlsx"
        :on-success="handleExcelImportSuccess"
        style="
          display: inline-block;
          margin: 0 10px;
          position: relative;
          top: 2px;
        "
      >
        <el-button type="primary">
          导入
          <upload style="width: 1em; height: 1em; margin-left: 8px" />
        </el-button>
      </el-upload>

      <el-button type="primary" @click="exp">
        导出
        <download style="width: 1em; height: 1em; margin-left: 8px" />
      </el-button>
    </div>

    <el-table
      :data="tableData"
      border
      stripe
      :header-cell-class-name="headerBg"
      @selection-change="handleSelectionChange"
    >
      <el-table-column type="selection" width="55" fixed></el-table-column>
      <el-table-column prop="id" label="ID" width="70"></el-table-column>
      <el-table-column prop="username" label="用户名"></el-table-column>
      <el-table-column prop="code" label="验证码" width="70"></el-table-column>
      <el-table-column prop="type" label="验证码类型" width="100">
        <template v-slot="scope">
          {{ typeFormat(scope.row.type) }}
        </template>
      </el-table-column>
      <el-table-column prop="email" label="邮箱"></el-table-column>
      <el-table-column prop="gmtModified" label="发送时间">
        <template v-slot="scope">
          {{ dataFormat(scope.row.gmtModified) }}
        </template>
      </el-table-column>
      <el-table-column prop="expirationTime" label="过期时间">
        <template v-slot="scope">
          {{ dataFormat(scope.row.expirationTime) }}
        </template>
      </el-table-column>
      <el-table-column label="操作" width="100" align="center" fixed="right">
        <template v-slot="scope">
          <el-popconfirm
            title="确定删除吗?"
            class="ml-5"
            confirm-button-text="确定"
            cancel-button-text="我再想想"
            icon-color="red"
            @confirm="del(scope.row.id)"
          >
            <template #reference>
              <el-button type="danger">
                删除
                <remove style="width: 1em; height: 1em; margin-left: 8px" />
              </el-button>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    <div style="padding: 10px 0">
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="pageNum"
        :page-sizes="[5, 10, 15, 20]"
        :page-size="pageSize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
      ></el-pagination>
    </div>
  </el-main>
</template>

<style scoped></style>

参考链接

posted @ 2022-05-21 23:24  Faetbwac  阅读(118)  评论(0编辑  收藏  举报