常用账号管理:工作相关账号、游戏账号、各平台账号

加班调休管理:公司没有对应的系统,需要自己记录加班调休情况。

待办事项:方便记录待办,以提醒还有哪些事情没有办理。

待实现功能:

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状态。

遗留问题:

  1. 勾选了两条待办,点击其中一个待办时,会造成另外一个勾选的待办状态变更。
  2. 切换到【账号管理】(其他菜单项)然后再切换回来,completed状态的待办前面的复选框并不是勾选状态。或者可以设计为鼠标悬浮到待办一行时,显示复选框,反之隐藏。

构想:

  1. 通过背景颜色来区分已完成的待办和未完成的待办

通过复选框对待办事项的状态进行改变的方式存在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

springboot+vue项目:工具箱的更多相关文章

  1. .gitignore 标准模板 -适用于SpringBoot+Vue项目 -Idea+VSCode开发

    .gitignore 标准模板 -适用于SpringBoot+Vue项目 node_modules/ target/ !.mvn/wrapper/maven-wrapper.jar ### STS # ...

  2. Docker部署Springboot+Vue项目

    1 docker使用nginx部署vue项目 1.1 打包vue项目 npm run build vue项目路径下会增加一个dist文件夹,里面就是网页文件 1.2 使用docker 拉取nginx ...

  3. 【占坑】IDEA从github 导入并运行 SpringBoot + VUE项目

    最近工程实践的项目内容是开发一个类似于博客和bbs论坛的系统,在github上找了一个类似的项目可以照着写一写.所以这里先占着坑,等把后端的数据库连接学完了再来填坑. github项目链接:githu ...

  4. linux下部署springboot vue项目

    使用的工具是 XFTP5 XSHELL5 docker pull gmaslowski/jdk 拉取jdk docker images 查询下载的镜像ID (如:390b58b1be42) docke ...

  5. SpringBoot+Vue项目上手

    博客 https://gitee.com/RoadsideParty/White-Jotter-Vue?_from=gitee_search UI框架 https://at-ui.github.io/ ...

  6. Springboot项目与vue项目整合打包

    我的环境 * JDK 1.8 * maven 3.6.0 * node环境 1.为什么需要前后端项目开发时分离,部署时合并? 在一些公司,部署实施人员的技术无法和互联网公司的运维团队相比,由于各种不定 ...

  7. springboot部署多个vue项目

    在springboot下部署多个vue项目,只需要将vue打包成静态文件后,将其放在resources的静态文件夹下即可. 如下图:static目录下有三个vue的静态文件夹,分别为运营后台(admi ...

  8. SpringBoot+Vue前后端分离项目,maven package自动打包整合

    起因:看过Dubbo管控台的都知道,人家是个前后端分离的项目,可是一条打包命令能让两个项目整合在一起,我早想这样玩玩了. 1. 建立个maven父项目 next 这个作为父工程,next Finish ...

  9. SpringBoot + Vue + nginx项目部署(零基础带你部署)

    一.环境.工具 jdk1.8 maven spring-boot idea VSVode vue 百度网盘(vue+springboot+nginx源码): 链接:https://pan.baidu. ...

  10. docker 运行jenkins及vue项目与springboot项目(三.jenkins的使用及自动打包vue项目)

    docker 运行jenkins及vue项目与springboot项目: 一.安装docker 二.docker运行jenkins为自动打包运行做准备 三.jenkins的使用及自动打包vue项目 四 ...

随机推荐

  1. orangepi zero3 使用dd命令进行SD卡系统备份与还原

    1. 使用dd命令备份整个sd卡 首先使用 df -h命令查看sd卡挂载名,如下所示,sd卡挂载为 /dev/sdc meng@meng:~/桌面/code$ df -h 文件系统 大小 已用 可用 ...

  2. 如何解决Git合并冲突?

    讲个故事先: 一个晴朗的日子,Alex 把远程版本库的修改拉到他的本地版本库. 他修改了名为 abc.txt 的文件,将其暂存(staged),提交(committed),最后推送(pushed)回远 ...

  3. Docker安装开源版obs对象存储服务minio,并后台运行

    ​​>Minio 是一个基于Apache License v2.0开源协议的对象存储服务,虽然轻量,却拥有着不错的性能.它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据. 例如 ...

  4. docker直接运行vue3源代码npm run dev

    ​有套代码,需要在服务器直接run dev,docker build玩起来. 步骤: 将自己的代码上传到服务器,本例:/home/flow/ruoyi-ui cd到项目根目录 ruoyi-ui,新建D ...

  5. blender low poly + unity 3d游戏制作

    会是一个有趣的方向,适合独立游戏制作人,独立动画电影制作人.

  6. .NET 9 增强 OpenAPI 规范

    在 .NET 9 的更新中,微软增强了原生 OpenAPI.这一变化表明 .NET 正在更加拥抱开放标准,同时让开发者体验更加轻松高效.本文将探讨为何进行这一更改.OpenAPI 的优势,以及如何在 ...

  7. [转]CMake:相关概念与使用入门

    CMake:相关概念与使用入门(一) CMake:搜索文件和指定头文件目录(三) CMake 子工程添加 根目录中他文件夹里的cpp文件 翻译 搜索 复制

  8. 今天记录一下管理系统中预览pdf的方法

    在管理系统中,有很多需要预览文件的操作,既方便用户查看又可以不用打开新的页面,我发现一个不错的方法,记录一下 <el-dialog title="" :visible.syn ...

  9. Solution -「AGC 058D」Yet Another ABC String

    \[\mathfrak{Defining~\LaTeX~macros\dots} \newcommand{\chr}[1]{\underline{\texttt{#1}}} \] \(\mathscr ...

  10. Solution Set -「LOCAL」冲刺省选 Round XXVII

    \(\mathscr{Summary}\)   还行,B 题挺不错,C 题就省选来说有点水(? \(\mathscr{Solution}\) \(\mathscr{A-}\) 分裂   初始时,你有一 ...