springboot+vue项目:工具箱
常用账号管理:工作相关账号、游戏账号、各平台账号
加班调休管理:公司没有对应的系统,需要自己记录加班调休情况。
待办事项:方便记录待办,以提醒还有哪些事情没有办理。
待实现功能:
1、点击侧边栏菜单,实现PageMain所在位置的内容(页面)更换(已完成)
2、在AccountPage页面实现查询、新增、编辑、删除功能。(已完成)
3、在AccountPage页面实现分页 + 关键字模糊查询功能。(已完成)
4、将数据库中的密码全部加密,前端进行转换显示明文密码。
5、实现用户登录、退出登录功能。
6、实现多表联查功能。
7、实现批量导入,模糊查询结果导出(已完成)
8、发现新问题,同一局域网,A、B两台电脑都可以访问项目,A进行CURD操作时,B的页面数据不会实时更新。(使用WebSocket解决)
001 || 环境
- JDK:1.8
- MySQL:8.0.37
- Maven:3.9.8
- SpringBoot:2.7.18
- MyBatisPlus:3.5.4
- Vue:2.7.16
- ElementUI:2.15.14
002 || 功能介绍
(1)首页:目前只有使用ElementUi制作的轮播图
(2)账号管理:
- mybatisplus的第三方插件对数据库中account数据的分页
- 根据名称、账号、备注进行关键字模糊查询结果展示。
- 每条记录的编辑(遮罩层表单更新)、删除(逻辑删除)
- 单条记录通过遮罩层表单进行添加
- 根据csv文件进行数据的批量导入,只需要准备name、username、password、comment四列
- 可以进行批量导出csv文件,也可以对模糊查询之后的结果集进行导出(csv文件)
(3)加班调休
- 使用vue的timeline(时间线)显示加班调休的历史记录,可通过遮罩层表单进行调休/加班的记录,登记之后,剩余可用调休时长会变更。
(4)加密解密
- 使用jasypt对输入框中的内容进行加密/解密,点击赋值,可将下方结果复制到剪贴板。点击清空,可以同时清空输入框和结果的内容。
(5)日期时间
- 显示日历、当前系统时间、距离下班/下次上班时间。
003 || 配置
Maven依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.harley</groupId>
<artifactId>AccountManager</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.15</version>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.4</version>
</dependency>
<!-- Development Tools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<!-- Security and Encryption -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<!-- 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
<!-- 加密解密 -->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- csv导入导出 -->
<dependency>
<groupId>net.sf.supercsv</groupId>
<artifactId>super-csv</artifactId>
<version>2.4.0</version> <!-- 确保选择稳定版本 -->
</dependency>
</dependencies>
</project>
application.yaml
application.yml
server:
port: 10086
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/accountmanager?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: '!QAZ2wsx'
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
#logging:
# level:
# root: debug
# org.springframework: debug
# com.baomidou.mybatisplus: debug
mybatis-plus:
mapper-location: classpath:mapper/*.xml
global-config:
# 配置逻辑删除
db-config:
# 删除为0
logic-delete-value: 0
# 存在为1
logic-not-delete-value: 1
jasypt:
encryptor:
password: harley
004 || 数据库
DDL
-- 创建数据库
create database accountmanager;
-- 切换数据库
use accountmanager;
-- 账号表
create table account
(
id int auto_increment comment 'PK' primary key,
name varchar(300) null comment '名称',
username varchar(100) not null comment '用户名',
password varchar(200) not null comment '密码',
comment text null comment '备注',
deleted int default 1 null comment '逻辑删除标志(0:删除,1:存在)',
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间'
)
comment '账号信息表' collate = utf8mb4_bin;
-- 加班调休表
create table `overtime_and_leave` (
`id` int not null auto_increment,
`user_id` int not null, -- 用户id
`type` enum('overtime', 'leave') not null, -- 类型:加班或调休
`date` date not null, -- 日期
`duration` decimal(5,2) not null, -- 时长(小时)
`created_at` timestamp default current_timestamp, -- 创建时间
primary key (`id`)
);
005 || 代码生成器
直接启动main方法,根据提示输入数据库链接、用户名、密码、作者、包名、模块名、表名......可自动生成entity、mapper、mapper.xml、service、serviceImpl、controller代码
com.harley.common.CodeGenerator
package com.harley.common;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import java.util.Collections;
import java.util.Scanner;
/**
* @author harley
* @since 2024-12-09 22:00:02
*/
public class CodeGenerator {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 获取数据库连接信息
System.out.println("请输入数据库URL:");
String dbUrl = scanner.nextLine();
System.out.println("请输入数据库用户名:");
String dbUsername = scanner.nextLine();
System.out.println("请输入数据库密码:");
String dbPassword = scanner.nextLine();
// 获取其他配置信息
System.out.println("请输入作者名字:");
String author = scanner.nextLine();
System.out.println("请输入父包名(例如:com.yourcompany):");
String parentPackage = scanner.nextLine();
System.out.println("请输入模块名(例如:module-name):");
String moduleName = scanner.nextLine();
System.out.println("请输入需要生成的表名(多个表以逗号分隔):");
String tableNames = scanner.nextLine();
FastAutoGenerator.create(dbUrl, dbUsername, dbPassword)
.globalConfig(builder -> {
builder.author(author) // 设置作者名
.outputDir(System.getProperty("user.dir") + "/src/main/java") // 设置输出目录
.dateType(DateType.ONLY_DATE);
})
.packageConfig(builder -> {
builder.parent(parentPackage) // 设置父包名
.moduleName(moduleName) // 设置模块名
.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/mapper")); // 设置mapper.xml生成路径
})
.strategyConfig(builder -> {
builder.addInclude(tableNames.split(",")) // 设置需要生成的表名
.entityBuilder() // 实体类生成策略
.enableLombok() // 开启 Lombok 模型
.logicDeleteColumnName("deleted") // 逻辑删除字段名
.controllerBuilder() // 控制器生成策略
.enableRestStyle() // REST 风格控制器
.enableHyphenStyle() // 使用连字符命名风格
.serviceBuilder() // Service 生成策略
.formatServiceFileName("%sService") // 格式化 service 文件名
.formatServiceImplFileName("%sServiceImpl") // 格式化 serviceImpl 文件名
.mapperBuilder() // Mapper 生成策略
.enableBaseColumnList() // 启用 BaseColumnList
.enableBaseResultMap(); // 启用 BaseResultMap
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用 FreeMarker 引擎,默认是 Velocity
.execute(); // 执行代码生成
System.out.println("代码生成完成!");
scanner.close();
}
}
006 || 账号管理
分页 + 模糊查询:详见 https://www.cnblogs.com/houhuilinblogs/p/18244117
批量导入、导出
(1)Config
com.harley.config.MybatisPlusConfig
package com.harley.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 指定数据库类型
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 如果配置多个插件, 切记分页最后添加
// 如果有多数据源可以不配具体类型, 否则都建议配上具体的 DbType
return interceptor;
}
}
(2)Entity
com.harley.entity.Account
package com.harley.entity;
import com.baomidou.mybatisplus.annotation.*;
import java.io.Serializable;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Getter;
import lombok.Setter;
/**
* <p>
* 账号信息表
* </p>
*
* @author harley
* @since 2024-12-10
*/
@Getter
@Setter
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
/**
* PK
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 名称
*/
private String name;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
private String comment;
// 是否删除(0:删除,1:存在)
@TableLogic
private Integer deleted;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}
(3)Mapper
com.harley.mapper.AccountMapper
package com.harley.mapper;
import com.harley.entity.Account;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* <p>
* 账号信息表 Mapper 接口
* </p>
*
* @author harley
* @since 2024-12-10
*/
@Mapper
public interface AccountMapper extends BaseMapper<Account> {
}
src/main/resources/mapper/AccountMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.harley.mapper.AccountMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.harley.entity.Account">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="username" property="username" />
<result column="password" property="password" />
<result column="comment" property="comment" />
<result column="deleted" property="deleted" />
<result column="update_time" property="updateTime" />
<result column="create_time" property="createTime" />
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, name, username, password, comment, deleted, update_time, create_time
</sql>
</mapper>
(4)Service
com.harley.service.AccountService
package com.harley.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.harley.entity.Account;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 账号信息表 服务类
* </p>
*
* @author harley
* @since 2024-12-10
*/
public interface AccountService extends IService<Account> {
IPage<Account> getRecordPage(Integer pageNum,Integer PageSize,String keyword);
}
com.harley.service.CsvImportService
package com.harley.service;
import com.harley.entity.Account;
import java.io.InputStream;
import java.util.List;
public interface CsvImportService {
List<Account> parseCsv(InputStream inputStream) throws Exception;
}
com.harley.service.CsvExportService
package com.harley.service;
import com.harley.entity.Account;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
public interface CsvExportService {
void exportUsersToCsv(HttpServletResponse response, List<Account> accounts) throws Exception;
}
(5)ServiceImpl
com.harley.service.impl.AccountServiceImpl
package com.harley.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.harley.entity.Account;
import com.harley.mapper.AccountMapper;
import com.harley.service.AccountService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* 账号信息表 服务实现类
* </p>
*
* @author harley
* @since 2024-12-10
*/
@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
public IPage<Account> getRecordPage(Integer pageNum, Integer pageSize, String keyword) {
Page<Account> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<Account> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.like(StringUtils.isNotBlank(keyword),Account::getName,keyword)
.or()
.like(StringUtils.isNotBlank(keyword),Account::getUsername,keyword)
.or()
.like(StringUtils.isNotBlank(keyword),Account::getComment,keyword)
.orderByDesc(Account::getUpdateTime);
return accountMapper.selectPage(page,lambdaQueryWrapper);
}
}
批量导入csv
com.harley.service.impl.CsvImportServiceImpl
package com.harley.service.impl;
import com.harley.entity.Account;
import com.harley.service.CsvImportService;
import org.springframework.stereotype.Service;
import org.supercsv.cellprocessor.constraint.NotNull;
import org.supercsv.cellprocessor.ift.CellProcessor;
import org.supercsv.io.CsvBeanReader;
import org.supercsv.prefs.CsvPreference;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@Service
public class CsvImportServiceImpl implements CsvImportService {
@Override
public List<Account> parseCsv(InputStream inputStream) throws Exception {
ArrayList<Account> accounts = new ArrayList<>();
CellProcessor[] processors = getProcessors();
try (CsvBeanReader beanReader = new CsvBeanReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8), CsvPreference.STANDARD_PREFERENCE)){
String[] header = beanReader.getHeader(true);// 忽略第一行标题
Account account;
while((account = beanReader.read(Account.class,header,processors))!=null){
accounts.add(account);
}
}
return accounts;
}
private CellProcessor[] getProcessors() {
return new CellProcessor[]{
new NotNull(), // 名称
new NotNull(), // 账号
new NotNull(), // 密码
new NotNull() // 备注
};
}
}
批量导出csv
com.harley.service.impl.CsvExportServiceImpl
package com.harley.service.impl;
import com.harley.entity.Account;
import com.harley.service.CsvExportService;
import org.springframework.stereotype.Service;
import org.supercsv.io.CsvBeanWriter;
import org.supercsv.prefs.CsvPreference;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@Service
public class CsvExportServiceImpl implements CsvExportService {
@Override
public void exportUsersToCsv(HttpServletResponse response, List<Account> accounts) throws Exception {
response.setContentType("text/csv; charset=UTF-8");
response.setHeader("Content-Disposition","attachment;filename=accounts.csv");
try (CsvBeanWriter beanWriter = new CsvBeanWriter(response.getWriter(),
CsvPreference.STANDARD_PREFERENCE)){
String[] header = new String[]{"name","username", "password", "comment"};
for (Account account : accounts){
beanWriter.write(account,header);
}
}
}
}
(6)Controller
com.harley.controller.AccountController
package com.harley.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.harley.entity.Account;
import com.harley.service.AccountService;
import com.harley.service.CsvExportService;
import com.harley.service.CsvImportService;
import com.harley.utils.JasyptUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* <p>
* 账号信息表 前端控制器
* </p>
*
* @author harley
* @since 2024-12-10
*/
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
private AccountService accountService;
@Autowired
private JasyptUtils jasyptUtils;
@Autowired
private CsvImportService csvImportService;
@Autowired
private CsvExportService csvExportService;
@GetMapping("/getRecordPage")
public IPage<Account> getRecordPage(@RequestParam(defaultValue = "0") Integer pageNum,
@RequestParam(defaultValue = "5") Integer pageSize,
@RequestParam(value = "keyword", defaultValue = "", required = false) String keyword){
return accountService.getRecordPage(pageNum,pageSize,keyword);
}
@PostMapping("/addAccount")
public boolean addAccount(@RequestBody Account account){
return accountService.save(account);
}
@GetMapping("/del")
public boolean delAccount(@RequestParam Integer id){
return accountService.removeById(id);
}
@PutMapping("/updateAccount/{id}")
public String updateAccount(@PathVariable Integer id, @RequestBody Account account){
account.setId(id);
return accountService.updateById(account) ? "更新成功" : "更新失败";
}
@PostMapping("/import")
public ResponseEntity<String> importCsv(@RequestParam("file") MultipartFile file){
try {
List<Account> accounts = csvImportService.parseCsv(file.getInputStream());
accountService.saveBatch(accounts);
return ResponseEntity.ok("Import successful");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Import failed: " + e.getMessage());
}
}
@GetMapping("/export")
public void exportCsv(@RequestParam(value = "keyword",defaultValue = "", required = false) String keyword,HttpServletResponse response) throws Exception {
System.out.println(keyword);
LambdaQueryWrapper<Account> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(StringUtils.isNotBlank(keyword),Account::getName,keyword)
.or()
.like(StringUtils.isNotBlank(keyword),Account::getUsername,keyword)
.or()
.like(StringUtils.isNotBlank(keyword),Account::getComment,keyword)
.orderByDesc(Account::getUpdateTime);
List<Account> accounts = accountService.list(queryWrapper);
// 只会将该表所有数据都导出来的
csvExportService.exportUsersToCsv(response, accounts);
}
}
(7)AccountPage.vue
src/components/views/AccountPage.vue
<template>
<div>
<div class="page-main">
<!-- 搜索框和按钮 -->
<div class="search-bar">
<el-input placeholder="请输入搜索内容" v-model="query.keyword" style="width:500px;" @keydown.enter.native.prevent="fetchAccounts" clearable>
<template #append>
<el-button type="primary" icon="el-icon-search" @click="fetchAccounts"></el-button>
</template>
</el-input>
<el-tooltip content="新增" placement="top" style="margin-left: 30px;">
<el-button size="mini" type="success" @click="showDialog('add',row=null)" circle><i class="el-icon-plus"></i></el-button>
</el-tooltip>
<el-tooltip content="批量导入" placement="top">
<el-upload action="/api/account/import" :on-success="handleSuccess" :show-file-list="false">
<el-button size="mini" type="primary" circle><i class="el-icon-upload"></i></el-button>
</el-upload>
</el-tooltip>
<el-tooltip content="导出" placement="top">
<el-button @click="handleExport" size="mini" type="primary" circle><i class="el-icon-download"></i></el-button>
</el-tooltip>
</div>
</div>
<el-table :data="accounts" style="width: 100%">
<el-table-column prop="id" label="序号" v-if="false" width="auto"></el-table-column>
<el-table-column prop="name" label="名称" width="400">
<template slot-scope="scope">{{ scope.row.name || 'N/A'}}</template> // use || to handle null value
</el-table-column>
<el-table-column prop="username" label="账号" width="400"></el-table-column>
<el-table-column prop="password" label="密码">
<template slot-scope="scope">
<div class="password-cell">
<span :class="{ 'masked-password': !scope.row.showPassword }">{{ getPassword(scope.row) }}</span>
<el-button type="text" @click="togglePasswordVisibility(scope.$index)">
<i :class="[scope.row.showPassword? 'el-icon-view' : 'el-icon-lock']"></i>
</el-button>
</div>
</template>
</el-table-column>
<el-table-column prop="comment" label="备注">
<template slot-scope="scope">{{ scope.row.comment || 'N/A'}}</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间"></el-table-column>
<el-table-column prop="updateTime" label="更新时间"></el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-tooltip content="编辑" placement="top">
<el-button size="mini" type="primary" @click="showDialog('edit',scope.row)" icon="el-icon-edit" circle></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button size="mini" type="danger" @click="deleteAccount(scope.row.id)" icon="el-icon-delete" circle></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<el-pagination
style="text-align: center"
:page-sizes="[5,10]"
:current-page="query.pageNum"
:page-size="query.pageSize"
:total="query.total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handlePageNumChange"
@size-change="handlePageSizeChange"
>
</el-pagination>
<!-- 新增/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="30%">
<el-form :model="form" :rules="rules" ref="form" label-width="100px">
<el-form-item label="编号" prop="id">
<el-input v-model="form.id" disabled></el-input>
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="账号" prop="username">
<el-input v-model="form.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="form.password"></el-input>
</el-form-item>
<el-form-item label="备注" prop="comment">
<el-input v-model="form.comment"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button> <!-- 点击取消按钮,遮罩层隐藏 -->
<el-button type="primary" @click="handleSubmit">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'AccountPage',
components: {
},
data() {
return {
accounts: [],
editVDialogVisible: false,
selectedAccount: null,
dialogVisible: false, // 控制对话框的显示与隐藏,默认为隐藏
dialogTitle: '', // 对话框标题
form: {
id: null,
name: '',
username: '',
password: '',
comment: '',
createTime: '',
updateTime: '',
},// 表单数据
rules: {
name: [{ required: false, message: '请输入名称', trigger: 'blur' }],
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
comment: [{ required: false, message: '请输入备注', trigger: 'blur' }],
}, // 表单验证规则
currentRow: null, // 当前编辑的行数据
query: {
pageNum: 0,
pageSize: 10,
total: 0,
keyword: '', // 搜索关键字
}, // 分页信息
};
},
created() {
this.fetchAccounts();
},
methods: {
async fetchAccounts() {
try {
const response = await axios.get('/api/account/getRecordPage', {
params: {
pageNum: this.query.pageNum,
pageSize: this.query.pageSize,
keyword: this.query.keyword,
}
}); // 如果设置了代理,这里不需要完整的URL
this.accounts = response.data.records.map(record => ({
id: record.id,
name: record.name,
username: record.username,
password: record.password,
comment: record.comment,
createTime: record.createTime,
updateTime: record.updateTime,
showPassword: false
}));
this.query.total = response.data.total;
this.query.pageNum = response.data.current;
this.query.pageSize = response.data.size;
} catch (error) {
console.error('There was an error fetching the accounts!', error);
}
},
getPassword(row){
return row.showPassword? row.password : '•'.repeat(8);
},
togglePasswordVisibility(index) {
this.$set(this.accounts[index],'showPassword',!this.accounts[index].showPassword);
},
showDialog(type, row = null) {
this.dialogVisible = true;
this.dialogTitle = type === 'add' ? '新增记录' : '编辑记录';
this.currentRow = row;
if (type === 'add') {
this.$refs.form && this.$refs.form.resetFields();
} else if (row) {
this.form = { ...row }; // 使用展开运算符复制对象以避免引用问题
}
},
async deleteAccount(id){
try{
const response = await axios.get(`/api/account/del?id=${id}`);
console.log('已删除: ' + response.data)
await this.fetchAccounts();
}catch(error){
console.error('删除失败', error);
}
},
handleSubmit() {
if (this.dialogTitle === '新增记录') {
this.$refs.form.validate((valid) => {
if (valid) {
axios.post('/api/account/addAccount', this.form)
.then(response => {
console.log('新增成功: ' + response.data);
this.$message({
message: '新增账号',
type: 'success'
});
this.dialogVisible = false;
this.accounts.push({ ...this.form }); // 使用展开运算符复制对象以避免引用问题
this.fetchAccounts();
})
.catch(error => {
console.error('新增账号失败', error);
this.$message({
message: '新增账号失败',
type: 'error'
});
});
} else {
console.log('表单验证失败!!');
return false;
}
});
} else if(this.dialogTitle === '编辑记录'){
this.$refs.form.validate((valid) => {
if (valid) {
axios.put(`/api/account/updateAccount/${this.form.id}`, this.form)
.then(response => {
console.log('更新成功: ' + response.data);
this.$message({
message: response.data,
type: 'success'
});
this.dialogVisible = false;
const index = this.accounts.findIndex(account => account.id === this.form.id);
if (index !== -1) {
this.$set(this.accounts,index, { ...this.form });
}
this.fetchAccounts();
})
.catch(error => {
console.error('更新失败', error);
this.$message({
message: '更新失败',
type: 'error'
});
});
} else {
console.log('表单验证失败!!');
return false;
}
});
}
},
handlePageNumChange(val){
console.log('当前页码: ' + val);
this.query.pageNum = val;
this.fetchAccounts();
},
handlePageSizeChange(val){
console.log('每页条数: ' + val);
this.query.pageSize = val;
this.fetchAccounts();
},
handleExport(){
window.location.href = `/api/account/export?keyword=${this.query.keyword}`;
console.log('导出成功');
this.fetchAccounts();
},
handleSuccess() {
this.$message({
message: '导入成功',
type: 'success'
});
this.fetchAccounts();
},
}
};
</script>
<style scoped>
.search-bar{
display: flex;
align-items: center;
}
.search-bar .el-tooltip + .el-tooltip{
margin-left: 30px;
}
</style>
导入时,准备的csv文件,注意第一行的标题列,需要按照如下进行配置
007 || 待办事项
勾选状态,则数据库中该待办的状态更改为completed(且待办事项加删除线),反之则为pending状态。
遗留问题:
- 勾选了两条待办,点击其中一个待办时,会造成另外一个勾选的待办状态变更。
- 切换到【账号管理】(其他菜单项)然后再切换回来,completed状态的待办前面的复选框并不是勾选状态。或者可以设计为鼠标悬浮到待办一行时,显示复选框,反之隐藏。
构想:
- 通过背景颜色来区分已完成的待办和未完成的待办
通过复选框对待办事项的状态进行改变的方式存在bug,暂时换成以下方式处理待办。
点击编辑按钮,即可对待办事项内容进行修改,同时编辑按钮会变成保存按钮,提交表单。将修改同步到数据库。
(1)数据库
-- auto-generated definition
create table todo(
id char(32) default (replace(uuid(), _utf8mb4'-', _utf8mb4'')) not null primary key,
description text,
status enum ('pending', 'completed') default 'pending',
created_at timestamp default CURRENT_TIMESTAMP,
updated_at timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP
);
(2)实体类
com.harley.entity.Todo
package com.harley.entity;
import java.io.Serializable;
import java.util.Date;
import lombok.Getter;
import lombok.Setter;
/**
* <p>
*
* </p>
*
* @author harley
* @since 2024-12-27
*/
@Getter
@Setter
public class Todo implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String description;
private String status;
private Date createdAt;
private Date updatedAt;
}
(3)Mapper
com.harley.mapper.TodoMapper
package com.harley.mapper;
import com.harley.entity.Todo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author harley
* @since 2024-12-27
*/
public interface TodoMapper extends BaseMapper<Todo> {
}
src/main/resources/mapper/TodoMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.harley.mapper.TodoMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.harley.entity.Todo">
<id column="id" property="id" />
<result column="description" property="description" />
<result column="status" property="status" />
<result column="created_at" property="createdAt" />
<result column="updated_at" property="updatedAt" />
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, description, status, created_at, updated_at
</sql>
</mapper>
(4)Service
com.harley.service.TodoService
package com.harley.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.harley.entity.Todo;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 服务类
* </p>
*
* @author harley
* @since 2024-12-27
*/
public interface TodoService extends IService<Todo> {
IPage<Todo> getRecordPage(Integer pageNum, Integer pageSize, String keyword);
void updateStatusById(String id,String status);
}
(5)ServiceImpl
com.harley.service.impl.TodoServiceImpl
package com.harley.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.harley.entity.Todo;
import com.harley.mapper.TodoMapper;
import com.harley.service.TodoService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* <p>
* 服务实现类
* </p>
*
* @author harley
* @since 2024-12-27
*/
@Service
public class TodoServiceImpl extends ServiceImpl<TodoMapper, Todo> implements TodoService {
@Autowired
private TodoMapper todoMapper;
@Override
public IPage<Todo> getRecordPage(Integer pageNum, Integer pageSize, String keyword) {
Page<Todo> page = new Page<>(pageNum,pageSize);
LambdaQueryWrapper<Todo> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper
.like(StringUtils.isNotBlank(keyword),Todo::getDescription,keyword)
.orderByDesc(Todo::getCreatedAt);
return todoMapper.selectPage(page, lambdaQueryWrapper);
}
@Override
public void updateStatusById(String id, String status) {
Todo todo = todoMapper.selectById(id);
if(todo != null){
todo.setStatus(status);
todo.setUpdatedAt(new Date());
todoMapper.updateById(todo);
}
}
}
(6)Controller
com.harley.controller.TodoController
package com.harley.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.harley.entity.Todo;
import com.harley.service.TodoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/**
* <p>
* 前端控制器
* </p>
*
* @author harley
* @since 2024-12-27
*/
@RestController
@RequestMapping("/todo")
public class TodoController {
@Autowired
private TodoService todoService;
@GetMapping("/getRecordPage")
public IPage<Todo> getRecordPage(@RequestParam(defaultValue = "0") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(value = "keyword", defaultValue = "", required = false) String keyword){
return todoService.getRecordPage(pageNum,pageSize,keyword);
}
@PostMapping("/addTodo")
public String addTodo(@RequestBody Todo todo){
return todoService.save(todo)?"新增成功":"新增失败";
}
@GetMapping("/deleteTodo")
public boolean deleteTodo(@RequestParam String id){
return todoService.removeById(id);
}
@PutMapping("/updateStatus/{id}")
public ResponseEntity<String> updateStatus(@PathVariable String id,@RequestBody Todo todo){
todo.setId(id);
return todoService.updateById(todo) ? ResponseEntity.ok("update successfully.") : ResponseEntity.ok("update failed.");
}
}
(7)TodoPage.vue
src\components\views\ToDoPage.vue
<template>
<div>
<div class="page-main">
<!-- 搜索框和按钮 -->
<div class="search-bar">
<el-form :model="form" ref="form" :rules="rules" label-width="80px">
<el-input placeholder="请输入待办事项" v-model="form.description" style="width:100%;" @keydown.enter.native.prevent="addTodo" clearable>
<template #append>
<el-button type="primary" icon="el-icon-plus" @click="addTodo">添加待办</el-button>
</template>
</el-input>
</el-form>
</div>
</div>
<el-table :data="records" style="width: 100%">
<el-table-column label="序号" prop="id" v-if="false" width="auto"></el-table-column>
<el-table-column label="待办" prop="description">
<template slot-scope="scope">
<span v-if="!scope.row.editable" :class="{ completed: scope.row.status === 'completed' }">{{ scope.row.description }}</span>
<el-input v-else v-model="scope.row.description" placeholder="请输入待办事项"></el-input>
</template>
</el-table-column>
<el-table-column label="操作" width="400">
<template slot-scope="scope">
<el-select v-model="scope.row.status" placeholder="请选择" size="mini" @change="updateStatus(scope.row)" style="width: 80px;">
<el-option type="success" label="待办" value="pending"></el-option>
<el-option label="完成" value="completed"></el-option>
</el-select>
<el-button type="primary" size="mini" style="margin-left: 10px;" @click="toggleEdit(scope.row)">
{{ scope.row.editable ? '保存' : '编辑' }}
</el-button>
<el-button type="danger" @click="deleteTodo(scope.row.id)" class="el-icon-delete" size="mini" style="margin-left: 10px;"></el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
style="text-align: center"
:page-sizes="[5,10]"
:current-page="query.pageNum"
:page-size="query.pageSize"
:total="query.total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handlePageNumChange"
@size-change="handlePageSizeChange"
>
</el-pagination>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'ToDoPage',
data() {
return {
records: [],
// 分页信息
query: {
pageNum: 0,
pageSize: 10,
total: 0,
keyword: '', // 搜索关键字
},
form: {
description: '',
},
rules: {
description: [
{ required: true, message: '请输入待办事项', trigger: 'blur' },
],
},
selectedRows: [],
}
},
created() {
this.getRecords();
},
mounted() {
// this.initializeSelection();
},
methods: {
async getRecords(){
const response = await axios.get('/api/todo/getRecordPage', {
params: {
pageNum: this.query.pageNum,
pageSize: this.query.pageSize,
keyword: this.query.keyword,
}});
this.records = response.data.records.map(record => ({
id: record.id,
description: record.description,
status: record.status,
editable: false
}));
this.query.total = response.data.total;
this.query.pageNum = response.data.current;
this.query.pageSize = response.data.size;
},
addTodo() {
console.log(this.description);
axios.post('/api/todo/addTodo',this.form)
.then(response => {
console.log('新增待办: ' + response.data);
this.$message({
message: '新增待办',
type: 'success'
})
this.form.description = '';
this.getRecords();
})
},
// 分页
handlePageNumChange(val){
console.log('当前页码: ' + val);
this.query.pageNum = val;
this.getRecords();
},
handlePageSizeChange(val){
console.log('每页条数: ' + val);
this.query.pageSize = val;
this.getRecords();
},
// handleSelectionChange(selection) {
// console.log('Selection changed: ', selection);
// this.records.forEach(record => {
// record._checked = selection.some(item => item.id === record.id);
// record.status = record._checked ? 'completed' : 'pending';
// console.log(`Record ID ${record.id} status updated to ${record.status}`);
// });
// // 准备要发送的数据
// const updateData = this.records.map(record => ({
// id: record.id,
// status: record.status
// }));
// console.log('Preparing to send update data:', updateData)
// // 发送批量更新请求到后端
// this.updateStatus(updateData);
// },
async updateStatus(row) {
row.editable = false;
try{
console.log('Sending update request to server: ', row);
await axios.put(`/api/todo/updateStatus/${row.id}`, row);
}catch(error){
console.error('Failed to update status: ', error);
}
},
// handleRowSelection(row, checked) {
// this.$refs.multipleTable.toggleRowSelection(row,checked);
// },
// initializeSelection() {
// // 遍历所有记录,并自动选中status为'completed'的行
// this.records.forEach(row => {
// if (row.status === 'completed') {
// this.multipleTable.toggleRowSelection(row, true);
// }
// });
// },
deleteTodo(id){
console.log('删除待办: ' + id);
axios.get(`/api/todo/deleteTodo?id=${id}`)
.then(response => {
console.log('删除待办: ' + response.data);
this.$message({
message: '删除待办',
type: 'success'
})
this.getRecords();
})
},
toggleEdit(row) {
if(row.editable){
this.updateStatus(row)
}else{
row.editable = true;
}
},
},
}
</script>
<style scoped>
.completed {
text-decoration: line-through;
color: gray;
}
</style>
008 || 加班/调休
(1)Entity
com.harley.entity.OvertimeAndLeave
package com.harley.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* <p>
*
* </p>
*
* @author harley
* @since 2024-12-20
*/
@Data
@TableName("overtime_and_leave")
public class OvertimeAndLeave implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String type;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
private Date date;
private BigDecimal duration;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
private Date createdAt;
}
(2)Mapper
com.harley.mapper.OvertimeAndLeaveMapper
package com.harley.mapper;
import com.harley.entity.OvertimeAndLeave;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author harley
* @since 2024-12-20
*/
public interface OvertimeAndLeaveMapper extends BaseMapper<OvertimeAndLeave> {
}
src/main/resources/mapper/OvertimeAndLeaveMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.harley.mapper.OvertimeAndLeaveMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.harley.entity.OvertimeAndLeave">
<id column="id" property="id" />
<result column="type" property="type" />
<result column="date" property="date" />
<result column="duration" property="duration" />
<result column="created_at" property="createdAt" />
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, type, date, duration, created_at
</sql>
</mapper>
(3)Service
com.harley.service.OvertimeAndLeaveService
package com.harley.service;
import com.harley.entity.OvertimeAndLeave;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 服务类
* </p>
*
* @author harley
* @since 2024-12-20
*/
public interface OvertimeAndLeaveService extends IService<OvertimeAndLeave> {
}
(4)ServiceImpl
com.harley.service.impl.OvertimeAndLeaveServiceImpl
package com.harley.service.impl;
import com.harley.entity.OvertimeAndLeave;
import com.harley.mapper.OvertimeAndLeaveMapper;
import com.harley.service.OvertimeAndLeaveService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 服务实现类
* </p>
*
* @author harley
* @since 2024-12-20
*/
@Service
public class OvertimeAndLeaveServiceImpl extends ServiceImpl<OvertimeAndLeaveMapper, OvertimeAndLeave> implements OvertimeAndLeaveService {
}
(5)Controller
com.harley.controller.OvertimeAndLeaveController
package com.harley.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.harley.entity.Account;
import com.harley.entity.OvertimeAndLeave;
import com.harley.service.OvertimeAndLeaveService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* <p>
* 前端控制器
* </p>
*
* @author harley
* @since 2024-12-20
*/
@RestController
@RequestMapping("/overtime-and-leave")
public class OvertimeAndLeaveController {
@Autowired
private OvertimeAndLeaveService overtimeAndLeaveService;
@GetMapping("/getTimeline")
public List<OvertimeAndLeave> getTimeline() {
LambdaQueryWrapper<OvertimeAndLeave> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.orderByDesc(OvertimeAndLeave::getDate);
return overtimeAndLeaveService.list(lambdaQueryWrapper);
}
@GetMapping("/leave-balance")
public Map<String, BigDecimal> getLeaveBalance() {
// 计算总加班时长
BigDecimal totalOvertime = overtimeAndLeaveService.list()
.stream()
.filter(ol -> "overtime".equals(ol.getType()))
.map(OvertimeAndLeave::getDuration)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 计算已使用调休时长
BigDecimal usedLeave = overtimeAndLeaveService.list()
.stream()
.filter(ol -> "leave".equals(ol.getType()))
.map(OvertimeAndLeave::getDuration)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Map<String, BigDecimal> stringBigDecimalMap = new HashMap<>();
stringBigDecimalMap.put("totalOvertime", totalOvertime);
stringBigDecimalMap.put("usedLeave", usedLeave);
return stringBigDecimalMap;
}
@PostMapping("/addRecord")
public boolean addRecord(@RequestBody OvertimeAndLeave overtimeAndLeave){
return overtimeAndLeaveService.save(overtimeAndLeave);
}
}
(6)OvertimePage.vue
src/components/views/OvertimePage.vue
<template>
<div id="app">
<div slot="header">
剩余可用调休时长{{ remainingLeave }} 小时<!-- 登记加班/调休 -->
<el-tooltip content="登记加班/调休" placement="top">
<el-button size="mini" type="primary" style="margin-left: 20px;" @click="showDialog = true" icon="el-icon-plus" circle></el-button>
</el-tooltip>
</div>
<el-scrollbar class="custom-scrollbar" style="height: 666px;">
<el-timeline>
<el-timeline-item
v-for="(activity, index) in activities"
:key="index"
:timestamp="getFormattedDate(activity.date)">
<i :class="getIconClass(activity.type)"></i>
{{ activity.type === 'overtime' ? '加班' : '调休' }} - {{ activity.duration }} 小时
</el-timeline-item>
</el-timeline>
</el-scrollbar>
<!-- 对话框 -->
<el-dialog :visible.sync="showDialog" title="登记加班/调休">
<el-form ref="form" :model="form">
<el-form-item label="类型" :label-width="formLabelWidth">
<el-select v-model="form.type" placeholder="请选择类型">
<el-option label="加班" value="overtime"></el-option>
<el-option label="调休" value="leave"></el-option>
</el-select>
</el-form-item>
<el-form-item label="日期" :label-width="formLabelWidth">
<el-date-picker v-model="form.date" type="date" placeholder="选择日期"></el-date-picker>
</el-form-item>
<el-form-item label="时长" :label-width="formLabelWidth">
<el-input-number v-model="form.duration" :min="0.5" :step="0.5"></el-input-number>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'OvertimePage',
data() {
return {
activities: [],
showDialog: false,
form: {
type: '',
date: '',
duration: 0
},
formLabelWidth: '120px',
totalOvertime: 0.0,
usedLeave: 0.0,
};
},
computed: {
remainingLeave() {
return this.totalOvertime - this.usedLeave;
},
},
methods: {
async fetchActivities() {
const response = axios.get('/api/overtime-and-leave/getTimeline');
response.then(response => {
this.activities = response.data;
})
},
fetchLeaveBalance(){
axios.get('/api/overtime-and-leave/leave-balance')
.then(response => {
this.totalOvertime = response.data.totalOvertime;
this.usedLeave = response.data.usedLeave;
})
.catch(error => console.error('Error fetching leave balance:', error));
},
handleSubmit() {
this.$refs['form'].validate((valid) => {
if (valid) {
axios.post('/api/overtime-and-leave/addRecord', {
...this.form,
date: new Date(this.form.date).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }).replace(/\//g, '-')
},{
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
console.log('登记成功: ' + response.data);
this.$message({
message: '登记成功',
type: 'success'
});
this.showDialog = false;
this.fetchActivities();
this.fetchLeaveBalance();
})
.catch(error => {
console.error('登记失败', error);
this.$message({
message: '登记失败',
type: 'error'
});
});
} else {
console.log('验证失败');
return false;
}
});
},
getFormattedDate(dateString) {
const options = { year: 'numeric', month: 'long', day: 'numeric' };
const daysOfWeek = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const date = new Date(dateString);
if (isNaN(date)) {
return '无效日期';
}
const formattedDate = date.toLocaleDateString('zh-CN', options);
const dayOfWeek = daysOfWeek[date.getDay()];
return `${formattedDate} (${dayOfWeek})`;
},
getIconClass(type){
if (type === 'overtime') {
return 'el-icon-time'; // 或者 'el-icon-clock'
} else if (type === 'leave') {
return 'el-icon-sunny'; // 或者 'el-icon-date'
}
return ''; // 默认情况下不显示图标
}
},
mounted() {
this.fetchActivities();
this.fetchLeaveBalance();
}
};
</script>
<style scoped>
/* 自定义样式 */
.custom-scrollbar {
width: 100% !important;
overflow: hidden !important;
}
.custom-scrollbar .el-scrollbar__wrap {
overflow-x: hidden !important; /* 强制隐藏水平滚动 */
overflow-y: auto !important; /* 保持垂直滚动 */
}
.custom-scrollbar .el-scrollbar__view {
white-space: normal !important;
word-break: break-all !important; /* 确保长单词和URL等能够换行 */
}
.custom-scrollbar * {
box-sizing: border-box !important;
}
</style>
009 || 加密解密
(1)Utils
com.harley.utils.JasyptUtils
package com.harley.utils;
import lombok.extern.slf4j.Slf4j;
import org.jasypt.encryption.StringEncryptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class JasyptUtils {
@Autowired
private StringEncryptor encryptor;
public String encrypt(String str){
System.out.println("============ origin Str : " + str);
String encryptedStr = encryptor.encrypt(str);
System.out.println("============ encrypted Str : " + encryptedStr);
return encryptedStr;
}
public String decrypt(String deStr){
System.out.println("============ decrypt deStr : " + deStr);
String originStr = encryptor.decrypt(deStr);
System.out.println("============ origin str : " + originStr);
return originStr;
}
}
(2)controller
com.harley.controller.TypeTransformController
package com.harley.controller;
import com.harley.utils.JasyptUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@Controller
public class TypeTransformController {
@Resource
private JasyptUtils jasyptUtils;
@ResponseBody
@GetMapping(value = "/encrypt")
public String encrypt(@RequestParam String str){
System.out.println(str);
return jasyptUtils.encrypt(str);
}
@ResponseBody
@GetMapping(value = "/decrypt")
public String decrypt(@RequestParam String str) {
System.out.println("接收到"+str);
return jasyptUtils.decrypt(str);
}
}
(3)EncryptPage.vue
src/components/views/EncryptPage.vue
<template>
<div class="encryption-tool">
<el-row :gutter="20" style="margin-bottom: 10px;">
<el-col :span="16">
<el-input v-model="inputText" placeholder="请输入要加密/解密的内容"></el-input>
</el-col>
<el-col :span="1.5">
<el-tooltip content="加密" placement="top">
<el-button type="primary" @click="encrypt">加密</el-button>
</el-tooltip>
</el-col>
<el-col :span="1.5">
<el-tooltip content="解密" placement="top">
<el-button type="success" @click="decrypt">解密</el-button>
</el-tooltip>
</el-col>
<el-col :span="1.5">
<el-tooltip content="点击复制结果到剪贴板" placement="top">
<el-button type="info" @click="copyToClipboard">复制</el-button>
</el-tooltip>
</el-col>
<el-col :span="1.5">
<el-tooltip content="清空输入框及结果" placement="top">
<el-button type="danger" @click="clear">清空</el-button>
</el-tooltip>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-input
type="textarea"
:rows="10"
placeholder="加密或解密后的结果会显示在这里"
v-model="outputText"
readonly
></el-input>
</el-col>
</el-row>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'EncryptPage',
data() {
return {
inputText: '',
outputText: ''
}
},
methods: {
encrypt() {
axios.get(`/api/encrypt?str=${this.inputText}`).then(response => {
this.outputText = response.data
})
},
decrypt() {
const finalInputText = encodeURIComponent(this.inputText);
// this.inputText.replace("+","%2B").replace("/","%2F")
console.log(finalInputText)
axios.get(`/api/decrypt?str=${finalInputText}`).then(response => {
this.outputText = response.data
})
},
clear() {
this.inputText = ''
this.outputText = ''
},
copyToClipboard() {
// 检查浏览器是否支持 Clipboard API
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
// 使用 Clipboard API 复制文本
navigator.clipboard.writeText(this.outputText).then(() => {
this.$message({
message: '复制成功',
type: 'success'
});
}).catch(err => {
this.$message.error('复制失败');
console.error('Failed to copy: ', err);
});
} else {
// 如果不支持 Clipboard API,则回退到其他方法
this.fallbackCopyTextToClipboard(this.outputText);
}
},
fallbackCopyTextToClipboard(text) {
// 创建一个临时的 textarea 元素用于复制文本
const textArea = document.createElement("textarea");
textArea.value = text;
// 将 textarea 移动到屏幕外
textArea.style.position = 'fixed';
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.width = '2em';
textArea.style.height = '2em';
textArea.style.padding = '0';
textArea.style.border = 'none';
textArea.style.outline = 'none';
textArea.style.boxShadow = 'none';
textArea.style.background = 'transparent';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
const msg = successful ? '成功' : '失败';
this.$message({
message: `复制 ${msg}`,
type: successful ? 'success' : 'error'
});
} catch (err) {
this.$message.error('复制失败');
console.error('Fallback failed: ', err);
}
document.body.removeChild(textArea);
},
}
}
</script>
<style scoped>
.encryption-tool {
padding: 20px;
}
</style>
010 || 日期时间
(1)CalendarPage.vue
src/components/views/CalendarPage.vue
<template>
<!-- <h3>实时系统时间</h3> -->
<!-- <p><span id="nowTime">{{ formattedTime }}, </span>{{ timeMessage }}</p> -->
<div>
<p><span id="nowTime">{{ formattedTime }}, </span>{{ timeMessage }}</p>
<!-- <el-date-picker v-model="selectedDate" type="date" placeholder="选择日期时间"></el-date-picker> -->
<!-- <p>已选日期:{{ selectedDate }}</p> -->
<el-calendar>
<template v-slot="{ date }">
<div class="calendar-cell">
<!-- 格里高利历日期 -->
<span>{{ date.getDate() }}</span> <!-- 日期 -->
<!-- 农历日期 -->
<span v-if="getLunarDate(date)" class="lunar-date">{{ getLunarDate(date) }}</span> <!-- 农历日期 -->
</div>
</template>
</el-calendar>
</div>
</template>
<script>
import { format, intervalToDuration, setHours, setMinutes, setSeconds, addDays } from 'date-fns';
import Lunar from 'lunar';
export default {
name: 'CalendarPage',
data() {
return {
// selectedDate: new Date(),
currentTime: new Date(),
timeMessage: '',
};
},
created() {
// this.updateTimeMessage();
// 每秒更新一次时间
this.timer = setInterval(() => {
this.currentTime = new Date();
this.updateTimeMessage();
}, 1000);
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer);
}
},
computed: {
// 格式化时间字符串
formattedTime() {
// const options = { hour: '2-digit', minute: '2-digit', second: '2-digit' };
// return this.currentTime.toLocaleTimeString('zh-CN', options);
return format(this.currentTime, 'HH:mm:ss');
}
},
methods: {
// 更新当前时间的方法
updateTime() {
this.currentTime = new Date();
},
// 更新时间信息的方法
updateTimeMessage() {
const now = this.currentTime;
const startOfWorkDay = setHours(setMinutes(setSeconds(new Date(), 0), 0), 9); // 9:00
const endOfWorkDay = setHours(setMinutes(setSeconds(new Date(), 0), 0), 18); // 18:00
const nextWorkDayStart = setHours(setMinutes(setSeconds(addDays(new Date(), 1), 0), 0), 9); // 第二天 9:00
let duration;
if (now < startOfWorkDay) {
duration = intervalToDuration({ start: now, end: startOfWorkDay });
this.timeMessage = `距离上班还有 ${this.formatDuration(duration)}`;
} else if (now >= startOfWorkDay && now < endOfWorkDay) {
duration = intervalToDuration({ start: now, end: endOfWorkDay });
this.timeMessage = `距离下班还有 ${this.formatDuration(duration)}`;
} else {
duration = intervalToDuration({ start: now, end: nextWorkDayStart });
this.timeMessage = `已下班, 距离下次(第二天9:00)上班还有 ${this.formatDuration(duration)}`;
}
},
// 格式化时间间隔的方法
formatDuration(duration) {
const hours = String(duration.hours || 0).padStart(2, '0');
const minutes = String(duration.minutes || 0).padStart(2, '0');
const seconds = String(duration.seconds || 0).padStart(2, '0');
return `${hours} 小时 ${minutes} 分钟 ${seconds} 秒`;
},
// 返回农历字符串表示形式
getLunarDate(date) {
const lunarDate = new Lunar(date);
return lunarDate.toString(); // 返回农历字符串表示形式
},
}
};
</script>
<style scoped>
#nowTime {
font-size: 2em;
font-weight: bold;
}
.lunar-date {
/* 农历样式的自定义 */
font-size: smaller;
color: gray;
}
</style>
用于在使用postman调用接口时,将返回内容按照表格的形式返回。
com.harley.utils.PrettyTable
package com.harley.utils;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class PrettyTable {
private List<String> headers = new ArrayList<>();
private List<List<String>> data = new ArrayList<>();
public PrettyTable(String... headers) {
this.headers.addAll(Arrays.asList(headers));
}
public void addRow(String... row) {
data.add(Arrays.asList(row));
}
private int getMaxSize(int column) {
int maxSize = headers.get(column).length();
for (List<String> row : data) {
if (row.get(column).length() > maxSize)
maxSize = row.get(column).length();
}
return maxSize;
}
private String formatRow(List<String> row) {
StringBuilder result = new StringBuilder();
result.append("|");
for (int i = 0; i < row.size(); i++) {
result.append(StringUtils.center(row.get(i), getMaxSize(i) + 2));
result.append("|");
}
result.append("\n");
return result.toString();
}
private String formatRule() {
StringBuilder result = new StringBuilder();
result.append("+");
for (int i = 0; i < headers.size(); i++) {
for (int j = 0; j < getMaxSize(i) + 2; j++) {
result.append("-");
}
result.append("+");
}
result.append("\n");
return result.toString();
}
public String toString() {
StringBuilder result = new StringBuilder();
result.append("\r\n");
result.append(formatRule());
result.append(formatRow(headers));
result.append(formatRule());
for (List<String> row : data) {
result.append(formatRow(row));
}
result.append(formatRule());
return result.toString();
}
}
011 || 前端
页面构图
(1)创建项目&安装模块
创建vue项目,并安装所需模块
# 使用vue-cli方式创建vue项目, 选择vue2.x
vue create account-vue
# 安装所需的模块
npm install element-ui axios date-dns lunar --save
(2)配置main.js
在
main.js
中引入Element ui
,并启用
import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import '@/assets/styles/global.css'
Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app') // 挂载到id为app的元素上
console.log('Vue version:',Vue.version)
console.log('ElementUI version:',ElementUI.version)
console.log('欢迎使用由Harley开发的账号管理系统')
(3)配置路由
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
proxy: {
'/api': {
target: 'http://localhost:10086',
changeOrigin: true,
pathRewrite: {
'^/api': ''
},
},
}
}
})
(4)配置global.css
/* src/assets/styles/global.css */
*{
margin: 0;
padding: 0;
}
(5)App.vue
<template>
<div id="app">
<PageIndex/>
</div>
</template>
<script>
import PageIndex from './components/PageIndex.vue';
export default {
name: 'App',
components: {
PageIndex
},
created() {
document.title = '侯惠林的工具箱';
}
}
</script>
<style>
#app {
height: 100%;
}
</style>
src/components/PageIndex.page
<template>
<el-container style="height: 100%; border: 1px solid #eee">
<el-aside :width="aside_width" style="background-color: rgb(238, 241, 246);height: 100%;margin:-1px 0 0 -1px;">
<PageAside :isCollapse="isCollapse" @menu-change="handleMenuChange"/>
</el-aside>
<el-container style="height: 100%">
<el-header style="text-align: right; font-size: 12px;height: 100%;border-bottom: rgba(168, 168, 168, 0.3) 1px solid;">
<PageHeader @doCollapse="doCollapse" :collapseIcon="collapse_icon"/>
</el-header>
<el-main>
<component :is="activeComponent" />
</el-main>
</el-container>
</el-container>
</template>
<script>
import PageAside from './PageAside.vue'
import PageHeader from './PageHeader.vue'
import AccountPage from './views/AccountPage.vue'
import CalendarPage from './views/CalendarPage.vue';
import HomePage from './views/HomePage.vue';
import EncryptPage from './views/EncryptPage.vue';
import OvertimePage from './views/OvertimePage.vue';
export default {
name: "PageIndex",
components:{PageAside,PageHeader,AccountPage,CalendarPage,HomePage,EncryptPage,OvertimePage},
data(){
return {
isCollapse: false,
aside_width: '200px',
collapse_icon: 'el-icon-s-fold',
activeComponent: 'HomePage'
}
},
methods:{
doCollapse(){
console.log("doCollapse隐藏侧边栏")
this.isCollapse = !this.isCollapse
if(!this.isCollapse){
this.aside_width = '200px'
this.collapse_icon = 'el-icon-s-fold'
}else{
this.aside_width = '64px'
this.collapse_icon = 'el-icon-s-unfold'
}
},
handleMenuChange(menuItem) {
switch (menuItem) {
case 'home':
this.activeComponent = 'HomePage'
break
case 'calendar':
this.activeComponent = 'CalendarPage'
break
case 'account':
this.activeComponent = 'AccountPage'
break
case 'encrypt':
this.activeComponent = 'EncryptPage'
break
case 'overtime':
this.activeComponent = 'OvertimePage'
break
default:
this.activeComponent = 'HomePage'
}
}
},
computed: {
}
};
</script>
<style scoped>
.el-header {
/* background-color: #B3C0D1; */
color: #333;
line-height: 60px;
}
.el-main{
padding: 5px;
}
.el-aside {
color: #333;
}
</style>
src/components/PageAside.page
<!-- PageAside.vue -->
<template>
<el-menu style="height: 100vh;" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b" :collapse="isCollapse" :collapse-transition="false" :default-active="activeMenu" @select="handleSelect">
<el-menu-item index="home"><i class="el-icon-s-home"></i><span slot="title"> 首页</span></el-menu-item>
<el-menu-item index="account"><i class="el-icon-user"></i><span slot="title"> 账号管理</span></el-menu-item>
<el-menu-item index="overtime"><i class="el-icon-time"></i><span slot="title"> 加班调休</span></el-menu-item>
<el-menu-item index="encrypt"><i class="el-icon-lock"></i><span slot="title"> 加密解密</span></el-menu-item>
<el-menu-item index="calendar"><i class="el-icon-time"></i><span slot="title"> 日期时间</span></el-menu-item>
</el-menu>
</template>
<script>
export default {
name: 'PageAside',
data() {
return {
activeMenu: 'home'
}
},
props:{
isCollapse: Boolean
},
methods: {
handleSelect(key) {
console.log(key);
this.$emit('menu-change', key);
},
}
};
</script>
<style scoped>
</style>
src/components/PageHeader.vue
<!-- PageHeader.vue -->
<template>
<div style="display: flex;line-height: 60px;">
<div style="margin-top: 8px;">
<i :class="collapseIcon" style="font-size: 20px;cursor: pointer;" @click="collapse"></i>
</div>
<div style="flex: 1; text-align: center; font-size: 30px;">
<!-- <span>欢迎来到账号管理系统</span> -->
</div>
<el-dropdown>
<span style="cursor: pointer;">Harley</span><i class="el-icon-arrow-down" style="margin-left: 5px;cursor: pointer;"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native = "toUser">个人中心</el-dropdown-item>
<el-dropdown-item @click.native = "logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script>
export default {
name: 'PageHeader',
props:{
collapseIcon:String
},
methods:{
toUser(){
console.log('to_user')
},
logout(){
console.log('log_out')
},
collapse(){
// 将子组件的值传给父组件
this.$emit('doCollapse')
}
}
};
</script>
<style scoped>
</style>
099 || Q&A
(1)数据库中是正常的datetime类型数据,在vue页面显示为2024-12-18T09:17:45.000+00:00
Spring Boot默认使用Jackson库来序列化和反序列化JSON数据。默认情况下Jackson会将日期序列化为
ISO 8601
格式。可以在
application.yml
中进行如下配置,以显示类似于:2024-12-19 17:29:00
的时间格式
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南