一、项目背景与架构设计

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. 架构设计详解

采用前后端分离+分层架构,核心设计原则为“高内聚、低耦合”,各层职责严格划分:

  1. 后端分层
            
  • Controller层:接收前端请求,参数校验,返回统一响应(不处理业务逻辑);
  • Service层:核心业务逻辑处理,分为接口(Service)和实现类(ServiceImpl),便于后续扩展;
  • Mapper层:数据访问层,通过MyBatis-Plus注解/XML实现数据库交互;
  • Entity层:数据库实体映射,与表结构一一对应;
  • DTO/VO层:数据传输对象(前端传入参数封装)/视图对象(后端返回数据封装),避免直接暴露实体类;
  • Config层:全局配置(跨域、MyBatis-Plus、日志等);
  • Exception层:全局异常处理,统一捕获业务异常、系统异常。
  1. 前端分层
            
  • View层:页面组件(.vue文件),负责UI展示;
  • API层:封装Axios请求,统一管理接口地址,便于维护;
  • Utils层:工具函数(时间格式化、数据校验等);
  • Store层:Pinia(Vue3推荐状态管理工具),存储全局状态(如用户信息)。
  1. 交互流程:前端通过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 =

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐