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">© 献给最爱的青哥哥</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">© 献给最爱的青哥哥</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>© 献给最爱的青哥哥</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);
}
}
MenuController
@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();
}
}
MenuServiceImpl
@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;
}
Menu
@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>
Menu.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';
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" +
" 您好,您的本次登录验证码为 <strong>{}</strong>,请妥善保管,切勿发送给他人,有效期为5分钟,请尽快通过<a href=\"服务器地址\">测试链接</a>登录系统。\n" +
"</p>";
String contentTwo = "<p>\n" +
" 尊敬的用户:\n" +
"</p>\n" +
"<p>\n" +
" 欢迎注册脚手架系统,本次验证码为 <strong>{}</strong>,请妥善保管,切勿发送给他人,有效期为5分钟。\n" +
"</p>";
String contentThree = "<p>\n" +
" 尊敬的用户:\n" +
"</p>\n" +
"<p>\n" +
" 您好,本次验证码为 <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>
参考链接
- Vue3+vite 按需自动引入elementUI (按照官方方法配置失败:Failed to resolve import "element-plus")
- 带你体验Vue2和Vue3开发组件有什么区别
- element plus menu不能嵌套 el-sub-menu 不能使用 解决方案
- vue3+vite 按需自动引入element plus报错(按照官方方法配置失败:Failed to resolve import “element-plus“)
- vite + ts 快速搭建 vue3 项目 以及介绍相关特性
- Vite + Vue3 + TS,配置别名 alias
- 使用vite搭建vue3项目(二) 引入vue-router
- element plus 引入icon 三种方法
- vite+vue3 按需引入element-plus 及element-icon图标的配置
- vue2到vue3中插槽slot变化详解---从slot,slot-scope到v-slot的变化
- Vue3+Vite+TS后台项目 ~ 5.搭建页面架构
- Pinia 快速入门
- Vue + Ts 封装axios 及 全局方法挂载
- vite+vue3+ts父子组件传值,及属性监听watch用法
- 使用Typescript重构axios(一)——写在最前面
- Vue3 Element Plus 动态图标
- vue3.x element-plus 实现省市区三级联动
- vue element-china-area-data使用详解
- vue3 中添加wangEditor富文本编辑器
- wangEditor4 + vue3.0 创建一个基础的 富文本编辑器
- vue3项目中简单的wangEnduit富文本编辑器封装
- vue3+ts中封装自己的富文本组件
- Java如何通过IP获得真实地址
- 从0开始带你手撸一套SpringBoot+Vue后台管理系统(2022年最新版)