目录
项目介绍
这个项目是我在目前大一暑假,利用 Springboot+vue 简单编写的,目的就是个人对 GTD+OKR 的理解后,更好的去管理自己的任务计划,合理的安排时间,系统化的学习更多东西,同时可以做到定期复盘,邮件任务提醒,于是就产生了这个念头,利用几天去开发了出来(测试版).
这里有项目介绍看
大一暑假几天用SpringBoo+Vue简单开发任务管理系统(GTD+OKR简易测试版)_哔哩哔哩_bilibili
项目环境
基本环境
- NodoJs 16以上
- Maven
- JDK 8/11/17/21/22
编辑器
- VS Code
- IntelliJ IDEA
- 浏览器 开发者工具(F12)
前端
- Vue 3
- Axios
- Element - Plus
- 等等
后端
- SpringBoot 2
- MyBatis-Plus
- JWT
- Java-Mail 2.7版
- 等等
项目界面预览
在 Web 界面
在 Adobe XD
数据库设计
(我是随个人需求去设计的 并未遵循ER关系图,三范式等)
共 4 张数据表
任务状态表
总任务表
任务回收表
用户表
智能数据生成
由 Tasks 表为例
成功的界面
失败的界面
失败的原因
问题分析
因为数据已经存在
解决方法
需要全部删掉后在进行生成)
前端设计
目录结构
vue 3
app.vue
<script>
</script>
<template>
<router-view></router-view>
</template>
<style scoped>
</style>
Home.vue
<template>
<!-- 个人信息 -->
<div v-if="userContent_display" class="userRegisteroks ">
<h2 class="f-s-30 f-s-b s-c-blue-80 p-20 t-a-c">个人信息</h2>
<h2 class="f-s-20 f-s-b s-c-blue-80 p-10-0-10-60">编号:{{ this.userContent_Gerenxixi.userPid }}</h2>
<h2 class="f-s-20 f-s-b s-c-blue-80 p-10-0-10-60">身份:{{ this.userContent_Gerenxixi.userRole }}</h2>
<h2 class="f-s-20 f-s-b s-c-blue-80 p-10-0-10-60">姓名:{{ this.userContent_Gerenxixi.userName }}</h2>
<h2 class="f-s-20 f-s-b s-c-blue-80 p-10-0-10-60">邮箱:{{ this.userContent_Gerenxixi.userMail }}</h2>
<h2 class="f-s-20 f-s-b s-c-blue-80 p-10-0-10-60">注册时间:{{ this.userContent_Gerenxixi.userRegisterTime }}</h2>
<h2 class="f-s-20 f-s-b s-c-blue-80 p-10-0-10-60">最近登录:{{ this.userContent_Gerenxixi.userNextTime }}</h2>
<h2 class="f-s-20 f-s-b s-c-blue-80 p-10-0-10-60"></h2>
<div class="Task_Remove_Box">
<button type="button" @click="User_NO()" class="bg-c-blue-80 mar-0-auto">确定</button>
</div>
</div>
<!-- 说明bug -->
<div v-if="homeContent_display" class="userRegisterok ">
<p class="f-s-1 f-s-b s-c-blue-20 p-20">Version 1.0.1 Bate</p>
<h2 class="f-s-30 f-s-b s-c-blue-80 ">欢迎来到任务系统管理</h2>
<p class="f-s-18 f-s-b s-c-blue-80 p-30">由UP对 GTD+ORK 的理解后融合而成</p>
<div class="Task_Remove_Box">
<button type="button" @click="Go()" class="bg-c-blue-80">确定</button>
<button type="button" @click="NOs()" class="bg-c-pink-20">关闭窗口</button>
</div>
</div>
<!-- 自我刷新 -->
<div v-if="userLogin_b"
style="width: 300px; position: absolute; top:15%; left:50%; transform: translate(-50%,0 ); z-index: 9999;"
class="am-up-float">
<el-alert title="欢迎回来" type="success" description="专心做好做好每一件事" show-icon />
</div>
<!-- 待会追加一个 退出登录(删除token) -->
<div class="common-layout">
<el-container>
<el-header class="box-s am-home_down">
<el-menu class="el-menu-demo home_box " mode="horizontal" :ellipsis="false">
<h2 class="f-s-30 f-s-b s-c-blue-80 p-0-0-0-30">{{ systenContent.title }}</h2>
<div class="flex-grow" />
<el-menu-item index="1">
<router-link to="/alltasklist" class="f-s-20 f-s-b t-d-n home_box"><span
class="s-c-blue-60">总任务清单</span></router-link>
</el-menu-item>
<el-menu-item index="2">
<router-link to="/nexttask" class="f-s-20 f-s-b t-d-n home_box"><span
class="s-c-blue-60">下一步清单</span></router-link>
</el-menu-item>
<el-menu-item index="3">
<router-link to="/waittask" class="f-s-20 f-s-b t-d-n home_box"><span
class="s-c-blue-60">等待清单</span></router-link>
</el-menu-item>
<el-menu-item index="4">
<router-link to="/wastebox" class="f-s-20 f-s-b t-d-n home_box"><span
class="s-c-blue-60">回收箱</span></router-link>
</el-menu-item>
<el-menu-item index="5">
<router-link to="/daybook" class="f-s-20 f-s-b t-d-n home_box"><span
class="s-c-blue-60">日记</span></router-link>
</el-menu-item>
<el-sub-menu index="6">
<template #title><span class="f-s-20 f-s-b t-d-n home_box"><span
class="s-c-blue-60">设置</span></span></template>
<el-menu-item index="6-1">颜色设置</el-menu-item>
<el-menu-item index="6-2">界面排版</el-menu-item>
<el-menu-item index="6-3">切换模式</el-menu-item>
</el-sub-menu>
<el-sub-menu index="7">
<template #title>
<span v-if="userContent.username != null" class="f-s-20 f-s-b s-c-blue-80 h-g bg-dark-50 p-0-10-0-0" >{{ userContent.username }}</span>
<span v-else class="f-s-20 f-s-b s-c-blue-80 h-g bg-dark-50 p-0-10-0-0" >确定后显示用户名</span>
<img style="width: 50px;" class="w-50x50 b-r-50_"
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
alt="Element logo" />
</template>
<el-menu-item index="7-1" @click="User_YES()">个人信息</el-menu-item>
<el-menu-item index="7-2">导出清单</el-menu-item>
<el-menu-item index="7-3">
<router-link to="/login" class="t-d-n f-s-b all_task"
@click="userexit()">安全退出</router-link>
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-header>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</div>
</template>
<script>
import axios from 'axios';
export default {
beforeCreate() {
console.log("home页面加载");
},
data() {
return {
userContent_display: false,
homeContent_display: true,
userLogin_b: false,
systenContent: {
title: '任务管理系统',
},
userContent: {
username: '名称加载中',
},
userContent_Gerenxixi: {},
}
},
created() {
// 从localStorage中获取名为'userToken'的项
const token = localStorage.getItem('userToken');
axios.get('http://127.0.0.1/loginjwt', {
params: {
token: token
}
})
.then(res => {
console.log(res.data);
if (res.data > 0) {
// 存储用户id
localStorage.setItem('userId', res.data);
} else {
// 如果token不存在,可能需要用户重新登录
console.log('No token found.');
this.$router.push({ name: 'Login' });
}
})
.catch(error => {
console.error('Error:', error);
});
//请求基本信息
const userId = localStorage.getItem('userId');
console.log("home中用户的id" + userId);
axios.get('http://127.0.0.1/user', {
params: {
userId: userId
}
})
.then(res => {
console.log(res.data);
this.userContent.username = res.data.userName;
this.userContent_Gerenxixi = res.data;
//提示框
this.userLogin_b = true;
setTimeout(() => {
this.userLogin_b = false;
}, 2000);
})
.catch(error => {
console.error('Error:', error);
});
},
methods: {
userexit() {
console.log("安全退出");
localStorage.removeItem('userToken');
localStorage.removeItem('userId');
},
Go() {
this.homeContent_display = false;
location.href = 'http://localhost:5173'
},
NOs() {
this.homeContent_display = false;
},
User_YES() {
this.userContent_display = true;
},
User_NO() {
this.userContent_display = false;
},
}
}
</script>
<style scoped>
.userRegisterok {
box-shadow: 0 5px 5px rgba(204, 204, 204, 0.299);
padding: 40px 0;
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 420px;
background: rgb(255, 255, 255);
z-index: 9999;
}
.userRegisteroks {
box-shadow: 0 5px 5px rgba(204, 204, 204, 0.299);
padding: 40px 0;
border-radius: 20px;
display: flex;
justify-content: center;
flex-direction: column;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 420px;
background: rgb(255, 255, 255);
z-index: 9999;
}
/*
.userRegisterok p {
padding: 5px 0 30px 0;
font-weight: bold;
font-size: 20px;
color: rgb(50, 157, 251);
}
*/
.Task_Remove_Box {
width: 90%;
display: flex;
justify-content: center;
}
.Task_Remove_Box button {
margin: 0 5px;
padding: 10px 0;
border-radius: 30px;
width: 60%;
border: 0;
color: #fff;
font-weight: bold;
font-size: 18px;
}
.box-s {
border-radius: 0 0 20px 20px;
box-shadow: 0 5px 10px rgba(204, 204, 204, 0.474);
}
.home_box {
align-items: center;
}
.flex-grow {
flex-grow: 1;
}</style>
(需要更多的话 去vx公众号 : wmcode 去获取 (我不打广告只是公众号是作为我的云端链接))
Vue-Router
import { createRouter, createMemoryHistory } from 'vue-router';
import Home from './src/views/Home.vue'
import Login from './src/views/Login.vue'
import Register from './src/views/Register.vue'
import AllTaskList from './src/views/AllTaskList.vue'
import DayBook from './src/views/DayBook.vue'
import NextTask from './src/views/NextTask.vue'
import WaitTask from './src/views/WaitTask.vue'
import WasteBox from './src/views/WasteBox.vue'
const router = createRouter({
history: createMemoryHistory(),
routes: [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'Home',
component: Home,
children: [
{
// 空字符串 '' 表示它是默认的子路由
path: '',
name: 'AllTaskList',
component: AllTaskList
},
{
path: '/alltasklist',
name: 'AllTaskListDirect',
component: AllTaskList
},
{
path: '/daybook',
name: 'DayBook',
component: DayBook
},
{
path: '/nexttask',
name: 'NextTask',
component: NextTask
},
{
path: '/waittask',
name: 'WaitTask',
component: WaitTask
},
{
path: '/wastebox',
name: 'WasteBox',
component: WasteBox
}
]
},
{
path: '/register',
name: 'Register',// 名字要大写
component: Register
},
{
path: '/login',
name: 'Login',
component: Login
},
]
});
export default router;
Axios
细节节点
- 用Get/Post当异步发送到后端
- Get的格式
- Post的格式跟后端实体类映射的 接口,参数名 一定要一致!
Elemten-Plus
看着文档结合需求去修改就好了
等更多前端技术......
后端设计
(懒得搞了, 去vx公众号 : wmcode 去获取 (我不打广告只是公众号是作为我的云端链接))
Springboot 2
-
实体类
-
配置类
-
控制层
-
服务层
-
Dao层(Mapper)
-
工具
- 自定义响应类
-
package com.takem.util; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.ToString; @Data @AllArgsConstructor @NoArgsConstructor @ToString public class JsonResult { // 状态码 private Integer stateCode; // 响应信息 private String message; // 纯ok public static JsonResult ok() { JsonResult jsonResult = new JsonResult(); jsonResult.setStateCode(ServiceCode.OK.getValue()); return jsonResult; } }
package com.takem.util; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.DecodedJWT; import com.takem.entity.Users; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import java.util.Calendar; @Component public class JwtTokenUtil { private static final String TOKENKEY = "chenchen"; // 随机盐 private static final long EXPIRATION = 86400000; // token有效时间(毫秒) public String generateToken(Integer userId , String userName) { //生成Token Calendar instance = Calendar.getInstance(); instance.add(Calendar.SECOND, (int)EXPIRATION); String token = JWT.create()//生成令牌 .withClaim("userId", userId)//payload .withClaim("username", userName)//设置自定义用户名 .withExpiresAt(instance.getTime())//设置令牌的有效时间 .sign(Algorithm.HMAC256(TOKENKEY))//设置签名 保密 复杂 ; System.out.println(token);//输出令牌 return token; } // 验证token public Integer validateToken(String token) { if (token == null || token.isEmpty()) { System.out.println("Token is null or empty"); return 0; } try { // 检查token是否包含两个点号,这是JWT标准格式的一部分 if (token.split("\\.").length != 3) { System.out.println("Invalid token format"); return 0; } JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(TOKENKEY)).build(); try { DecodedJWT decodedJWT = jwtVerifier.verify(token); System.out.println("用户Id:" + decodedJWT.getClaim("userId").asInt()); System.out.println("用户名:" + decodedJWT.getClaim("username").asString()); System.out.println("过期时间:" + decodedJWT.getExpiresAt()); return decodedJWT.getClaim("userId").asInt(); }catch (Exception e){ if (e instanceof TokenExpiredException) { System.out.println("Token已过期"); } else if (e instanceof SignatureVerificationException) { System.out.println("签名验证失败"); } else if (e instanceof JWTVerificationException) { System.out.println("JWT验证失败: " + e.getMessage()); } else { System.out.print("未知错误: "); e.printStackTrace(); } } return 0; } catch (Exception e) { return 0; } } }
package com.takem.util; // 自定义状态码 public enum ServiceCode { OK(200), ERR_NOT_FOUND(404), ERR_CONFLICT(409), ERR_CUSTOM(1000); private Integer value; ServiceCode(Integer value) { this.value = value; } public Integer getValue() { return value; } }
-
时间类
-
package com.takem.util; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; public class TimeConverter { public String TimeZ8(String utcTimeStr){ // 解析UTC时间字符串为ZonedDateTime DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT.withZone(ZoneId.of("UTC")); ZonedDateTime utcTime = ZonedDateTime.parse(utcTimeStr, formatter); // 转换为东八区时间(中国标准时间) ZonedDateTime cstTime = utcTime.withZoneSameInstant(ZoneId.of("Asia/Shanghai")); // 打印结果 System.out.println("CST Time (ZonedDateTime): " + cstTime); return cstTime+""; } }
-
启动类
-
package com.takem; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.takem.mapper") public class TakeMApplication { public static void main(String[] args) { SpringApplication.run(TakeMApplication.class, args); } }
MyBatis-plus
- 增添数据
- 删除数据
- 软删除
- 硬删除
- 改动数据
- 查询数据
JWT
有效的token处理很重要,我方法是存在了Cookie跟用户登录时存储在本地内
这个的话我其实也做过相关的文章了,可以直接去跳转观看
Springboot | 零基础快速搭建JWT简单登录案例(一)
邮件服务
这个的话我其实也做过相关的文章了,可以直接去跳转观看
基于SpringBoot构造超简易QQ邮件服务发送(分离-图解-新手)
等技术......
项目部署
本地部署
这个的话我其实也做过相关的文章了,可以直接去跳转观看
新手快速部署Springboot 的Jar包 (图解-BuiId,Maven)
(到底啦~)
(需要更多的话 去vx公众号 : wmcode 去获取 (我不打广告只是公众号是作为我的云端链接))