一、项目背景与架构设计
1. 需求定位与价值
本项目旨在开发一款轻量且可扩展的企业级待办事项管理系统,核心解决个人/团队日常任务跟踪、进度管控需求。除基础的“增删改查待办、标记完成状态”外,新增分页查询、按状态/日期筛选、数据导出、用户登录认证等进阶功能,同时兼顾代码规范性、可维护性和生产环境部署适配性,为新手提供全栈开发落地范本,为进阶开发者提供企业级改造思路。
2. 技术栈深度选型
结合“开发效率+性能+生态”三大核心因素,最终技术栈选型如下,各组件职责清晰、互补性强:
- 后端核心:Spring Boot 3.x(快速整合依赖、内置Tomcat容器,简化配置)+ MyBatis-Plus 3.5.x(ORM框架,简化CRUD操作,支持分页、条件查询等增强功能)
- 数据存储:MySQL 8.0(稳定可靠的关系型数据库,支持事务,适配中小规模数据存储)
- 前端核心:Vue 3(组合式API更灵活,响应式性能更优)+ Vite(替代Webpack,构建速度更快)+ Element Plus(企业级UI组件库,提供丰富表单、表格、弹窗组件)
- 辅助工具:Axios(前端HTTP请求封装)、Lombok(简化Java实体类代码)、SpringDoc(生成OpenAPI接口文档)、Docker(容器化部署)、Redis(缓存优化,可选)
3. 架构设计详解
采用前后端分离+分层架构,核心设计原则为“高内聚、低耦合”,各层职责严格划分:
- 后端分层:
- Controller层:接收前端请求,参数校验,返回统一响应(不处理业务逻辑);
- Service层:核心业务逻辑处理,分为接口(Service)和实现类(ServiceImpl),便于后续扩展;
- Mapper层:数据访问层,通过MyBatis-Plus注解/XML实现数据库交互;
- Entity层:数据库实体映射,与表结构一一对应;
- DTO/VO层:数据传输对象(前端传入参数封装)/视图对象(后端返回数据封装),避免直接暴露实体类;
- Config层:全局配置(跨域、MyBatis-Plus、日志等);
- Exception层:全局异常处理,统一捕获业务异常、系统异常。
- 前端分层:
- View层:页面组件(.vue文件),负责UI展示;
- API层:封装Axios请求,统一管理接口地址,便于维护;
- Utils层:工具函数(时间格式化、数据校验等);
- Store层:Pinia(Vue3推荐状态管理工具),存储全局状态(如用户信息)。
- 交互流程:前端通过Axios调用后端RESTful接口 → 后端Controller接收请求 → Service处理业务 → Mapper操作数据库 → 后端返回统一格式响应 → 前端解析响应并更新页面。
二、后端实现(企业级规范)
1. 项目初始化与依赖配置
通过 Spring Initializr 初始化项目,选择Spring Boot 3.2.x,核心依赖如下(pom.xml关键配置):
| xml <dependencies> <!-- Spring Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <!-- MySQL Driver --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- SpringDoc(接口文档) --> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.2.0</version> </dependency> <!-- 分页插件(MyBatis-Plus内置,需手动配置) --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-extension</artifactId> <version>3.5.5</version> </dependency> <!-- 数据校验(JSR380) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies> |
2. 数据库设计(规范化)
遵循数据库设计三大范式,避免数据冗余,同时添加索引优化查询性能。除核心待办表外,新增用户表支持登录认证:
| sql -- 创建数据库(指定字符集,避免中文乱码) CREATE DATABASE IF NOT EXISTS todo_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE todo_system; -- 用户表(支持登录认证) CREATE TABLE `sys_user` ( `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID', `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', `password` VARCHAR(100) NOT NULL COMMENT '加密后的密码', `nickname` VARCHAR(50) COMMENT '用户昵称', `status` TINYINT DEFAULT 1 COMMENT '状态:0禁用/1正常', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', INDEX idx_username (`username`) COMMENT '用户名索引,优化登录查询' ) COMMENT '系统用户表'; -- 待办事项表(关联用户ID,实现一人一待办) CREATE TABLE `todo` ( `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', `user_id` BIGINT NOT NULL COMMENT '关联用户ID', `content` VARCHAR(255) NOT NULL COMMENT '待办内容', `status` TINYINT DEFAULT 0 COMMENT '状态:0未完成/1已完成', `deadline` DATETIME COMMENT '截止时间', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', INDEX idx_user_id (`user_id`) COMMENT '用户ID索引,优化个人待办查询', INDEX idx_status_deadline (`status`, `deadline`) COMMENT '状态+截止时间索引,优化筛选查询', CONSTRAINT fk_todo_user FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ) COMMENT '待办事项表'; -- 初始化测试用户(密码:123456,加密方式:BCrypt) INSERT INTO `sys_user` (`username`, `password`, `nickname`) VALUES ('test', '$2a$10$EixZaYb4xU58Gpq1R0yWbeb00LU5qUaK6x8h8yR8JZ011yM7hX8H2a', '测试用户'); |
3. 核心配置文件(application.yml)
分环境配置(开发/生产),明确各模块配置,添加日志、数据校验、MyBatis-Plus等细节配置:
| yaml spring: # 环境激活(dev/prod) profiles: active: dev # 数据库配置 datasource: url: jdbc:mysql://localhost:3306/todo_system?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # 数据校验配置 validation: messages: basename: i18n/ValidationMessages # Jackson时间格式化 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai # 服务器配置 server: port: 8080 servlet: context-path: /api # 接口统一前缀,避免路径冲突 tomcat: max-threads: 100 # 最大线程数,优化并发 min-spare-threads: 10 # 最小空闲线程数 # MyBatis-Plus配置 mybatis-plus: configuration: map-underscore-to-camel-case: true # 下划线转驼峰 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发环境打印SQL call-setters-on-nulls: true # 查询结果为null时返回字段,便于前端处理 mapper-locations: classpath:mapper/**/*.xml # MapperXML文件路径 type-aliases-package: com.example.todosystem.entity # 实体类别名包 global-config: db-config: logic-delete-field: isDelete logic-delete-value: 1 logic-not-delete-value: 0 table-prefix: "" # 表名前缀(无则空) # SpringDoc接口文档配置 springdoc: api-docs: path: /doc/api-docs # 接口文档JSON路径 swagger-ui: path: /doc/swagger-ui.html # 接口文档UI路径 operationsSorter: method # 按请求方法排序(GET/POST/PUT/DELETE) packages-to-scan: com.example.todosystem.controller # 扫描的Controller包 # 日志配置 logging: level: root: info com.example.todosystem: debug # 项目包日志级别 org.springframework.web: warn com.baomidou.mybatisplus: warn pattern: console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n" file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n" file: name: logs/todo-system.log # 日志文件路径 max-size: 10MB # 单个日志文件大小 max-history: 30 # 日志保留天数 # 自定义配置(JWT相关,后续登录功能用) jwt: secret: todo-system-secret-key-2025 # 密钥(生产环境需加密存储) expire: 3600000 # 过期时间(1小时,单位:毫秒) |
4. 核心代码实现(分层规范)
(1)实体类(Entity)
使用Lombok简化getter/setter,添加MyBatis-Plus注解映射表结构,同时通过@TableField配置自动填充:
| java // 基础实体类(抽取公共字段,减少冗余) @Data public class BaseEntity implements Serializable { @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; } // 用户实体类 @Data @TableName("sys_user") public class SysUser extends BaseEntity { @TableId(type = IdType.AUTO) private Long id; @NotBlank(message = "用户名不能为空") private String username; @NotBlank(message = "密码不能为空") private String password; private String nickname; private Integer status; } // 待办实体类 @Data @TableName("todo") public class Todo extends BaseEntity { @TableId(type = IdType.AUTO) private Long id; @NotNull(message = "用户ID不能为空") private Long userId; @NotBlank(message = "待办内容不能为空") @Length(max = 255, message = "待办内容不能超过255字") private String content; private Integer status; private LocalDateTime deadline; } |
(2)自动填充配置(MetaObjectHandler)
实现MyBatis-Plus的MetaObjectHandler接口,自动填充createTime和updateTime,避免手动赋值:
| java @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { // 严格填充:只有字段为null时才填充 this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } } |
(3)DTO与VO(数据传输对象)
前端传入参数用DTO接收(避免直接使用实体类),后端返回数据用VO封装(隐藏敏感字段,如密码):
| java // 待办新增DTO(前端传入参数) @Data public class TodoAddDTO { @NotBlank(message = "待办内容不能为空") @Length(max = 255, message = "待办内容不能超过255字") private String content; private LocalDateTime deadline; // 截止时间(可选) } // 待办查询DTO(分页+筛选条件) @Data public class TodoQueryDTO { private Integer status; // 状态筛选(0未完成/1已完成) private LocalDateTime startDate; // 开始日期(筛选创建时间) private LocalDateTime endDate; // 结束日期(筛选创建时间) @PageableDefault(page = 0, size = 10, sort = "createTime", direction = Sort.Direction.DESC) private Pageable pageable; // 分页参数 } // 待办VO(后端返回数据,隐藏userId等无关字段) @Data public class TodoVO { private Long id; private String content; private Integer status; private String statusName; // 状态名称(0→未完成,1→已完成) private LocalDateTime deadline; private String deadlineStr; // 格式化后的截止时间 private LocalDateTime createTime; private String createTimeStr; // 格式化后的创建时间 // 状态名称转换(也可通过前端处理,后端处理更灵活) @Mapping(target = "statusName", expression = "java(todo.getStatus() == 0 ? \"未完成\" : \"已完成\")") // 时间格式化(后端统一处理,避免前端重复代码) @Mapping(target = "createTimeStr", expression = "java(com.example.todosystem.util.DateUtils.format(todo.getCreateTime(), \"yyyy-MM-dd HH:mm:ss\"))") @Mapping(target = "deadlineStr", expression = "java(todo.getDeadline() != null ? com.example.todosystem.util.DateUtils.format(todo.getDeadline(), \"yyyy-MM-dd HH:mm:ss\") : \"无\")") public abstract TodoVO convert(Todo todo); } |
(4)Mapper层(数据访问)
继承MyBatis-Plus的BaseMapper,复杂查询可通过XML实现:
| java // 用户Mapper public interface SysUserMapper extends BaseMapper<SysUser> { // 按用户名查询用户(登录用) @Select("SELECT * FROM sys_user WHERE username = #{username} AND status = 1") SysUser selectByUsername(@Param("username") String username); } // 待办Mapper(支持条件查询) public interface TodoMapper extends BaseMapper<Todo> { // 分页+条件查询个人待办 Page<Todo> selectTodoPage(@Param("page") Page<Todo> page, @Param("userId") Long userId, @Param("query") TodoQueryDTO query); } |
TodoMapper.xml(resources/mapper/TodoMapper.xml):
| 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.example.todosystem.mapper.TodoMapper"> <select id="selectTodoPage" resultType="com.example.todosystem.entity.Todo"> SELECT * FROM todo WHERE user_id = #{userId} <if test="query.status != null"> AND status = #{query.status} </if> <if test="query.startDate != null and query.endDate != null"> AND create_time BETWEEN #{query.startDate} AND #{query.endDate} </if> ORDER BY create_time DESC </select> </mapper> |
(5)Service层(业务逻辑)
分为接口和实现类,核心业务逻辑在此层实现,添加日志记录,同时处理事务:
| java // 待办Service接口 public interface TodoService extends IService<Todo> { // 新增待办 boolean addTodo(Long userId, TodoAddDTO dto); // 分页查询个人待办 Page<TodoVO> getTodoPage(Long userId, TodoQueryDTO query); // 切换待办状态 boolean toggleStatus(Long id, Long userId); // 删除待办 boolean deleteTodo(Long id, Long userId); // 导出个人待办(Excel) List<TodoVO> exportTodo(Long userId, TodoQueryDTO query); } // 待办Service实现类 @Slf4j @Service public class TodoServiceImpl extends ServiceImpl<TodoMapper, Todo> implements TodoService { @Autowired private TodoMapper todoMapper; @Autowired private ModelMapper modelMapper; // 用于DTO/VO/Entity转换(需导入modelmapper依赖) @Override @Transactional(rollbackFor = Exception.class) // 事务管理,异常回滚 public boolean addTodo(Long userId, TodoAddDTO dto) { log.info("用户{}新增待办:{}", userId, dto.getContent()); Todo todo = new Todo(); todo.setUserId(userId); todo.setContent(dto.getContent()); todo.setStatus(0); // 默认未完成 todo.setDeadline(dto.getDeadline()); return save(todo); } @Override public Page<TodoVO> getTodoPage(Long userId, TodoQueryDTO query) { log.info("用户{}分页查询待办,条件:{}", userId, query); // 分页查询 Page<Todo> todoPage = todoMapper.selectTodoPage( new Page<>(query.getPageable().getPageNumber(), query.getPageable().getPageSize()), userId, query ); // 转换为VO分页对象 return todoPage.convert(this::convertToVO); } @Override @Transactional(rollbackFor = Exception.class) public boolean toggleStatus(Long id, Long userId) { log.info("用户{}切换待办{}状态", userId, id); // 先查询待办是否存在,且属于当前用户(权限校验) Todo todo = getById(id); if (todo == null || !todo.getUserId().equals(userId)) { log.error("待办{}不存在或不属于用户{}", id, userId); throw new BusinessException("待办不存在或无操作权限"); } // 切换状态 todo.setStatus(todo.getStatus() == 0 ? 1 : 0); return updateById(todo); } @Override @Transactional(rollbackFor = Exception.class) public boolean deleteTodo(Long id, Long userId) { log.info("用户{}删除待办{}", userId, id); // 权限校验 Todo todo = getById(id); if (todo == null || !todo.getUserId().equals(userId)) { throw new BusinessException("待办不存在或无操作权限"); } return removeById(id); } @Override public List<TodoVO> exportTodo(Long userId, TodoQueryDTO query) { log.info("用户{}导出待办,条件:{}", userId, query); // 查询所有符合条件的待办 List<Todo> todoList = todoMapper.selectList( Wrappers.<Todo>lambdaQuery() .eq(Todo::getUserId, userId) .eq(query.getStatus() != null, Todo::getStatus, query.getStatus()) .between(query.getStartDate() != null && query.getEndDate() != null, Todo::getCreateTime, query.getStartDate(), query.getEndDate()) .orderByDesc(Todo::getCreateTime) ); // 转换为VO列表 return todoList.stream().map(this::convertToVO).collect(Collectors.toList()); } // 转换为VO对象 private TodoVO convertToVO(Todo todo) { return modelMapper.map(todo, TodoVO.class); } } |
(6)Controller层(接口暴露)
接收前端请求,参数校验,调用Service层,返回统一响应。添加SpringDoc注解生成接口文档:
| java @RestController @RequestMapping("/todo") @Tag(name = "待办事项接口", description = "待办的增删改查、分页筛选、导出") public class TodoController { @Autowired private TodoService todoService; @PostMapping @Operation(summary = "新增待办", description = "新增个人待办事项,需登录") public Result<Boolean> addTodo(@RequestBody @Valid TodoAddDTO dto, @RequestHeader("userId") Long userId) { // userId从请求头获取(登录后由JWT拦截器解析) boolean success = todoService.addTodo(userId, dto); return Result.ok(success, "新增成功"); } @GetMapping @Operation(summary = "分页查询待办", description = "支持按状态、日期筛选,分页返回") public Result<Page<TodoVO>> getTodoPage(TodoQueryDTO query, @RequestHeader("userId") Long userId) { Page<TodoVO> todoPage = todoService.getTodoPage(userId, query); return Result.ok(todoPage); } @PutMapping("/{id}/status") @Operation(summary = "切换待办状态", description = "将待办标记为完成/未完成") public Result<Boolean> toggleStatus(@PathVariable Long id, @RequestHeader("userId") Long userId) { boolean success = todoService.toggleStatus(id, userId); return Result.ok(success, "状态切换成功"); } @DeleteMapping("/{id}") @Operation(summary = "删除待办", description = "删除个人待办事项") public Result<Boolean> deleteTodo(@PathVariable Long id, @RequestHeader("userId") Long userId) { boolean success = todoService.deleteTodo(id, userId); return Result.ok(success, "删除成功"); } @GetMapping("/export") @Operation(summary = "导出待办", description = "导出个人符合条件的待办为Excel") public void exportTodo(TodoQueryDTO query, @RequestHeader("userId") Long userId, HttpServletResponse response) throws IOException { List<TodoVO> todoList = todoService.exportTodo(userId, query); // 导出Excel(使用EasyExcel,需导入依赖) ExcelUtils.exportExcel(response, "个人待办事项", "待办列表", TodoVO.class, todoList); } } |
(7)统一响应与异常处理
统一响应格式,便于前端解析;全局异常处理,避免接口直接抛出异常:
| java // 统一响应类 @Data public class Result<T> { private Integer code; // 200成功,500系统异常,400业务异常 private String msg; private T data; // 成功响应(带数据) public static <T> Result<T> ok(T data) { return new Result<>(200, "操作成功", data); } // 成功响应(带消息) public static <T> Result<T> ok(T data, String msg) { return new Result<>(200, msg, data); } // 失败响应(业务异常) public static <T> Result<T> error(String msg) { return new Result<>(400, msg, null); } // 系统异常 public static <T> Result<T> systemError(String msg) { return new Result<>(500, msg, null); } } // 自定义业务异常 public class BusinessException extends RuntimeException { public BusinessException(String message) { super(message); } } // 全局异常处理器 @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { // 处理自定义业务异常 @ExceptionHandler(BusinessException.class) public Result<Void> handleBusinessException(BusinessException e) { log.warn("业务异常:{}", e.getMessage()); return Result.error(e.getMessage()); } // 处理参数校验异常 @ExceptionHandler(MethodArgumentNotValidException.class) public Result<Void> handleValidationException(MethodArgumentNotValidException e) { // 获取校验失败信息 String msg = e.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining(";")); log.warn("参数校验异常:{}", msg); return Result.error(msg); } // 处理系统异常 @ExceptionHandler(Exception.class) public Result<Void> handleSystemException(Exception e) { log.error("系统异常:", e); return Result.systemError("服务器内部错误,请联系管理员"); } } |
(8)分页插件配置
配置MyBatis-Plus分页插件,支持分页查询:
| java @Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } } |
(9)接口文档配置(SpringDoc)
配置SpringDoc,生成可视化接口文档,便于前后端协作:
| java @Configuration public class SpringDocConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title("待办事项管理系统接口文档") .version("1.0") .description("Spring Boot+Vue全栈开发的待办事项管理系统接口文档,包含用户登录、待办增删改查等接口")) // 配置安全模式(JWT认证) .components(new Components() .addSecuritySchemes("bearerAuth", new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT"))); } } |
三、前端实现(Vue 3+Element Plus)
1. 项目初始化与依赖安装
使用Vite创建Vue 3项目,安装核心依赖:
| bash # 创建Vue项目(Vite) npm create vite@latest todo-front -- --template vue cd todo-front # 安装核心依赖 npm install axios element-plus @element-plus/icons-vue pinia pinia-plugin-persistedstate easyexcel-js vue-router # 启动项目 npm run dev |
2. 全局配置(main.js)
配置Element Plus、Axios、Pinia、路由等全局依赖:
| javascript import { createApp } from 'vue' import App from './App.vue' import router from './router' import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import * as ElementPlusIconsVue from '@element-plus/icons-vue' import axios from './utils/axios' // 创建Pinia实例(状态管理,持久化存储) const pinia = createPinia() pinia.use(piniaPluginPersistedstate) const app = createApp(App) // 全局注册Element Plus图标 for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } // 全局配置 app.use(ElementPlus) app.use(pinia) app.use(router) app.config.globalProperties.$axios = axios app.mount('#app') |
3. 路由配置(router/index.js)
配置路由,实现登录页与待办列表页的跳转,添加路由守卫校验登录状态:
| javascript import { createRouter, createWebHistory } from 'vue-router' import { useUserStore } from '@/stores/user' // 路由规则 const routes = [ { path: '/', redirect: '/login' }, { path: '/login', name: 'Login', component: () => import('@/views/Login.vue'), meta: { requiresAuth: false } // 无需登录 }, { path: '/todo', name: 'Todo', component: () => import('@/views/Todo.vue'), meta: { requiresAuth: true } // 需要登录 } ] const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes }) // 路由守卫:校验登录状态 router.beforeEach((to, from, next) => { const userStore = useUserStore() if (to.meta.requiresAuth && !userStore.token) { // 未登录,跳转到登录页 next('/login') } else { next() } }) export default router |
4. 状态管理(Pinia)
使用Pinia管理用户登录状态(token、userId等),持久化存储到本地:
| javascript // stores/user.js import { defineStore } from 'pinia' import { login as userLogin } from '@/api/user' export const useUserStore = defineStore('user', { state: () => ({ token: '', // JWT令牌 userId: '', // 用户ID nickname: '' // 用户昵称 }), persist: true, // 持久化存储(localStorage) actions: { // 登录动作 async login(userInfo) { const res = await userLogin(userInfo) this.token = res.data.token this.userId = res.data.userId this.nickname = res.data.nickname }, // 退出登录 logout() { this.token = '' this.userId = '' this.nickname = '' } } }) |
5. Axios封装(utils/axios.js)
封装Axios,统一处理请求拦截(添加token)、响应拦截(统一解析响应)、异常处理:
| javascript import axios from 'axios' import { ElMessage } from 'element-plus' import { useUserStore } from '@/stores/user' // 创建Axios实例 const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量获取基础地址 timeout: 5000 }) // 请求拦截器:添加token到请求头 service.interceptors.request.use( (config) => { const userStore = useUserStore() if (userStore.token) { config.headers['Authorization'] = `Bearer ${userStore.token}` config.headers['userId'] = userStore.userId } return config }, (error) => { ElMessage.error('请求异常:' + error.message) return Promise.reject(error) } ) // 响应拦截器:统一解析响应 service.interceptors.response.use( (response) => { const res = response.data if (res.code !== 200) { // 业务异常 ElMessage.error(res.msg || '操作失败') return Promise.reject(res) } return res }, (error) => { // 系统异常 let msg = '服务器内部错误' if (error.response) { const status = error.response.status if (status === 401) { msg = '未登录或登录已过期,请重新登录' // 跳转到登录页 const userStore = useUserStore() userStore.logout() window.location.href = '/login' } else if (status === 403) { msg = '无操作权限' } else if (status === 404) { msg = '接口不存在' } } ElMessage.error(msg) return Promise.reject(error) } ) export default service |
6. API封装(api/todo.js)
统一管理待办相关接口,便于维护:
| javascript import request from '@/utils/axios' // 新增待办 export const addTodo = (data) => { return request({ url: '/todo', method: 'post', data }) } // 分页查询待办 export const getTodoPage = (params) => { return request({ url: '/todo', method: 'get', params }) } // 切换待办状态 export const toggleTodoStatus = (id) => { return request({ url: `/todo/${id}/status`, method: 'put' }) } // 删除待办 export const deleteTodo = (id) => { return request({ url: `/todo/${id}`, method: 'delete' }) } // 导出待办 export const exportTodo = (params) => { return request({ url: '/todo/export', method: 'get', params, responseType: 'blob' // 导出文件需设置响应类型 }) } |
7. 核心页面实现
(1)登录页(Login.vue)
| vue <template> <div class="login-container"> <el-card class="login-card"> <h2 class="login-title">待办事项管理系统</h2> <el-form :model="loginForm" :rules="loginRules" ref="loginFormRef" label-width="80px"> <el-form-item label="用户名" prop="username"> <el-input v-model="loginForm.username" placeholder="请输入用户名" prefix-icon="User" /> </el-form-item> <el-form-item label="密码" prop="password"> <el-input v-model="loginForm.password" type="password" placeholder="请输入密码" prefix-icon="Lock" /> </el-form-item> <el-form-item> <el-button type="primary" class="login-btn" @click="handleLogin">登录</el-button> </el-form-item> </el-form> </el-card> </div> </template> <script setup> import { ref } from 'vue' import { useRouter } from 'vue-router' import { useUserStore } from '@/stores/user' import { login } from '@/api/user' const router = useRouter() const userStore = useUserStore() const loginFormRef = ref(null) // 登录表单数据 const loginForm = ref({ username: 'test', password: '123456' }) // 表单校验规则 const loginRules = ref({ username: [{ required: true, message: '请输入用户名', trigger: 'blur' }], password: [{ required: true, message: '请输入密码', trigger: 'blur' }] }) // 登录处理 const handleLogin = async () => { try { // 表单校验 await loginFormRef.value.validate() // 调用登录接口 await userStore.login(loginForm.value) // 登录成功,跳转到待办页面 router.push('/todo') } catch (error) { console.error('登录失败:', error) } } </script> <style scoped> .login-container { width: 100vw; height: 100vh; display: flex; justify-content: center; align-items: center; background-color: #f5f5f5; } .login-card { width: 400px; padding: 20px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); } .login-title { text-align: center; margin-bottom: 20px; color: #303133; } .login-btn { width: 100%; } </style> |
(2)待办列表页(Todo.vue)
| vue <template> <div class="todo-container"> <div class="todo-header"> <h2>个人待办事项管理</h2> <div class="user-info"> <span>欢迎您,{{ nickname }}</span> <el-button type="text" @click="handleLogout">退出登录</el-button> </div> </div> <!-- 筛选与新增区域 --> <el-card class="filter-add-card"> <div class="filter-form"> <el-form :model="queryForm" inline> <el-form-item label="状态"> <el-select v-model="queryForm.status" placeholder="全部状态"> <el-option label="全部" value=""></el-option> <el-option label="未完成" value="0"></el-option> <el-option label="已完成" value="1"></el-option> </el-select> </el-form-item> <el-form-item label="创建时间"> <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD HH:mm:ss" ></el-date-picker> </el-form-item> <el-form-item> <el-button type="primary" @click="getTodoList">查询</el-button> <el-button @click="resetQuery">重置</el-button> <el-button type="success" @click="handleExport">导出Excel</el-button> </el-form-item> </el-form> </div> <div class="add-todo"> <el-input v-model="addForm.content" placeholder="请输入待办内容(最多255字)" maxlength="255" show-word-limit style="width: 300px; margin-right: 10px;" /> <el-date-picker v-model="addForm.deadline" type="datetime" placeholder="选择截止时间(可选)" format="YYYY-MM-DD HH:mm" value-format="YYYY-MM-DD HH:mm:ss" style="margin-right: 10px;" /> <el-button type="primary" @click="addTodo">添加待办</el-button> </div> </el-card> <!-- 加载中提示 --> <el-loading v-if="loading" text="数据加载中..." fullscreen /> <!-- 待办列表 --> <el-table v-if="todoList.length > 0 && !loading" :data="todoList" border stripe :header-cell-style="{ background: '#f5f7fa' }" > <el-table-column label="序号" type="index" width="80"></el-table-column> <el-table-column prop="content" label="待办内容" min-width="300"></el-table-column> <el-table-column label="状态" width="120"> <template #default="scope"> <el-tag :type="scope.row.status === 0 ? 'warning' : 'success'"> {{ scope.row.statusName }} </el-tag> </template> </el-table-column> <el-table-column prop="deadlineStr" label="截止时间" width="200"></el-table-column> <el-table-column prop="createTimeStr" label="创建时间" width="200"></el-table-column> <el-table-column label="操作" width="200"> <template #default="scope"> <el-button type="success" size="small" @click="toggleStatus(scope.row.id)" icon="Check" :disabled="scope.row.status === 1" > 标记完成 </el-button> <el-button type="warning" size="small" @click="toggleStatus(scope.row.id)" icon="Refresh" :disabled="scope.row.status === 0" style="margin-left: 5px;" > 标记未完成 </el-button> <el-button type="danger" size="small" @click="deleteTodo(scope.row.id)" icon="Delete" style="margin-left: 5px;" > 删除 </el-button> </template> </el-table-column> </el-table> <!-- 空列表提示 --> <el-empty v-if="todoList.length === 0 && !loading" description="暂无待办事项,点击上方添加吧~" style="margin-top: 50px;" /> <!-- 分页组件 --> <el-pagination v-if="total > 0 && !loading" @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="currentPage" :page-sizes="[5, 10, 20, 50]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper" :total="total" style="margin-top: 20px; text-align: right;" ></el-pagination> </div> </template> <script setup> import { ref, onMounted, computed } from 'vue' import { useRouter } from 'vue-router' import { useUserStore } from '@/stores/user' import { addTodo, getTodoPage, toggleTodoStatus, deleteTodo, exportTodo } from '@/api/todo' import { ElMessage } from 'element-plus' import { formatDate } from '@/utils/date' const router = useRouter() const userStore = useUserStore() const nickname = computed(() => userStore.nickname) // 加载状态 const loading = ref(false) // 待办列表数据 const todoList = ref([]) // 总条数 const total = ref(0) // 分页参数 const currentPage = ref(1) const pageSize = |
所有评论(0)