Skip to content

实战项目:待办事项应用 ​

欢迎来到阶段 1 的实战项目!在这个项目中,你将综合运用所学的 JavaScript 基础知识,从零开始构建一个功能完整的待办事项应用。

📋 项目概述 ​

项目目标 ​

创建一个待办事项应用,支持:

  • ➕ 添加新任务
  • ✏️ 编辑任务内容
  • ✅ 标记任务完成/未完成
  • 🗑️ 删除任务
  • 🔍 过滤任务(全部/进行中/已完成)
  • 💾 数据持久化(localStorage)
  • 📊 显示任务统计信息

学习成果 ​

完成本项目后,你将能够:

  • ✅ 熟练操作 DOM 元素
  • ✅ 处理用户交互事件
  • ✅ 管理应用状态
  • ✅ 使用 localStorage 存储数据
  • ✅ 组织和结构化代码
  • ✅ 应用数组方法处理数据
  • ✅ 实现基本的错误处理和数据验证

技术栈 ​

  • HTML5
  • CSS3
  • JavaScript (ES6+)
  • localStorage API

预计时间 ​

  • 3-5 天(每天 2-3 小时)

🎨 项目效果展示 ​

┌─────────────────────────────────────────┐
│  ✓ 待办事项应用                          │
├─────────────────────────────────────────┤
│  ┌─────────────────────────────────┐   │
│  │ 输入新任务...            [添加]  │   │
│  └─────────────────────────────────┘   │
│                                         │
│  [全部] [进行中] [已完成]               │
│                                         │
│  ☐ 学习 JavaScript                [✏️][🗑️] │
│  ☑ 完成项目练习                   [✏️][🗑️] │
│  ☐ 阅读技术文档                   [✏️][🗑️] │
│                                         │
│  共 3 项 · 1 项已完成                   │
└─────────────────────────────────────────┘

📁 项目结构 ​

todo-app/
├── index.html          # HTML 结构
├── css/
│   └── style.css       # 样式文件
├── js/
│   ├── app.js          # 主应用逻辑
│   ├── storage.js      # 存储管理
│   └── utils.js        # 工具函数
└── README.md           # 项目说明

🚀 实现步骤 ​

步骤 1: 创建 HTML 结构 ​

创建 index.html 文件:

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>待办事项应用</title>
  <link rel="stylesheet" href="css/style.css">
</head>
<body>
  <!-- 应用容器 -->
  <div class="app-container">
    <!-- 头部 -->
    <header class="app-header">
      <h1>✓ 待办事项</h1>
    </header>

    <!-- 主内容区 -->
    <main class="app-main">
      <!-- 输入区域 -->
      <form id="todoForm" class="todo-form">
        <input 
          type="text" 
          id="todoInput" 
          class="todo-input" 
          placeholder="输入新任务..."
          autocomplete="off"
        >
        <button type="submit" class="btn btn-add">添加</button>
      </form>

      <!-- 过滤按钮 -->
      <div class="filter-buttons">
        <button class="btn-filter active" data-filter="all">全部</button>
        <button class="btn-filter" data-filter="active">进行中</button>
        <button class="btn-filter" data-filter="completed">已完成</button>
      </div>

      <!-- 待办列表 -->
      <ul id="todoList" class="todo-list">
        <!-- 动态生成的任务项 -->
      </ul>

      <!-- 空状态提示 -->
      <div id="emptyState" class="empty-state">
        <p>还没有任务,添加一个吧!</p>
      </div>
    </main>

    <!-- 底部统计 -->
    <footer class="app-footer">
      <p id="stats" class="stats">共 0 项</p>
      <button id="clearCompleted" class="btn btn-clear">清除已完成</button>
    </footer>
  </div>

  <!-- 加载 JavaScript -->
  <script src="js/utils.js"></script>
  <script src="js/storage.js"></script>
  <script src="js/app.js"></script>
</body>
</html>

关键点:

  • 使用语义化 HTML 标签
  • 为交互元素添加 ID 和类名
  • 表单结构便于事件处理
  • 空状态提示提升用户体验

步骤 2: 创建 CSS 样式 ​

创建 css/style.css 文件:

css
/* ===== 重置和基础样式 ===== */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 
    'Helvetica Neue', Arial, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 20px;
}

/* ===== 应用容器 ===== */
.app-container {
  width: 100%;
  max-width: 600px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
  overflow: hidden;
}

/* ===== 头部 ===== */
.app-header {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  padding: 30px;
  text-align: center;
}

.app-header h1 {
  font-size: 32px;
  font-weight: 600;
}

/* ===== 主内容区 ===== */
.app-main {
  padding: 30px;
}

/* ===== 输入表单 ===== */
.todo-form {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.todo-input {
  flex: 1;
  padding: 12px 16px;
  font-size: 16px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  transition: border-color 0.3s;
}

.todo-input:focus {
  outline: none;
  border-color: #667eea;
}

.btn {
  padding: 12px 24px;
  font-size: 16px;
  font-weight: 500;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s;
}

.btn-add {
  background: #667eea;
  color: white;
}

.btn-add:hover {
  background: #5568d3;
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}

/* ===== 过滤按钮 ===== */
.filter-buttons {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.btn-filter {
  flex: 1;
  padding: 10px;
  font-size: 14px;
  background: #f5f5f5;
  color: #666;
  border: 2px solid transparent;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.3s;
}

.btn-filter:hover {
  background: #e0e0e0;
}

.btn-filter.active {
  background: #667eea;
  color: white;
  border-color: #667eea;
}

/* ===== 待办列表 ===== */
.todo-list {
  list-style: none;
  max-height: 400px;
  overflow-y: auto;
}

.todo-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px;
  background: #f9f9f9;
  border-radius: 8px;
  margin-bottom: 10px;
  transition: all 0.3s;
}

.todo-item:hover {
  background: #f0f0f0;
  transform: translateX(4px);
}

.todo-item.completed {
  opacity: 0.6;
}

.todo-checkbox {
  width: 20px;
  height: 20px;
  cursor: pointer;
}

.todo-text {
  flex: 1;
  font-size: 16px;
  color: #333;
}

.todo-item.completed .todo-text {
  text-decoration: line-through;
  color: #999;
}

/* 编辑输入框 */
.todo-edit-input {
  flex: 1;
  padding: 8px 12px;
  font-size: 16px;
  border: 2px solid #667eea;
  border-radius: 6px;
}

.todo-edit-input:focus {
  outline: none;
}

/* 按钮组 */
.todo-actions {
  display: flex;
  gap: 8px;
}

.btn-icon {
  padding: 6px 12px;
  font-size: 14px;
  background: transparent;
  border: 1px solid #ddd;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.3s;
}

.btn-icon:hover {
  background: #f0f0f0;
}

.btn-edit:hover {
  background: #ffc107;
  border-color: #ffc107;
  color: white;
}

.btn-delete:hover {
  background: #f44336;
  border-color: #f44336;
  color: white;
}

.btn-save {
  background: #4caf50;
  color: white;
  border-color: #4caf50;
}

.btn-save:hover {
  background: #45a049;
}

.btn-cancel {
  background: #9e9e9e;
  color: white;
  border-color: #9e9e9e;
}

.btn-cancel:hover {
  background: #757575;
}

/* ===== 空状态 ===== */
.empty-state {
  text-align: center;
  padding: 40px 20px;
  color: #999;
  font-size: 16px;
  display: none;
}

.empty-state.show {
  display: block;
}

/* ===== 底部统计 ===== */
.app-footer {
  padding: 20px 30px;
  background: #f5f5f5;
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-top: 1px solid #e0e0e0;
}

.stats {
  color: #666;
  font-size: 14px;
}

.btn-clear {
  padding: 8px 16px;
  font-size: 14px;
  background: transparent;
  color: #f44336;
  border: 1px solid #f44336;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.3s;
}

.btn-clear:hover {
  background: #f44336;
  color: white;
}

/* ===== 滚动条样式 ===== */
.todo-list::-webkit-scrollbar {
  width: 8px;
}

.todo-list::-webkit-scrollbar-track {
  background: #f1f1f1;
  border-radius: 4px;
}

.todo-list::-webkit-scrollbar-thumb {
  background: #888;
  border-radius: 4px;
}

.todo-list::-webkit-scrollbar-thumb:hover {
  background: #555;
}

/* ===== 响应式设计 ===== */
@media (max-width: 480px) {
  .app-header h1 {
    font-size: 24px;
  }

  .todo-form {
    flex-direction: column;
  }

  .filter-buttons {
    flex-direction: column;
  }

  .app-footer {
    flex-direction: column;
    gap: 12px;
  }
}

关键点:

  • 使用现代 CSS 特性(Flexbox、渐变、过渡)
  • 响应式设计适配移动端
  • 良好的交互反馈(hover、active 状态)
  • 可访问性考虑(足够的对比度、合适的字体大小)

步骤 3: 创建工具函数 ​

创建 js/utils.js 文件:

javascript
/**
 * 工具函数模块
 * 提供通用的辅助函数
 */

console.log('加载 utils.js');

// 生成唯一 ID
function generateId() {
  return Date.now() + Math.random().toString(36).substr(2, 9);
}

// 转义 HTML(防止 XSS)
function escapeHTML(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

// 格式化日期
function formatDate(timestamp) {
  const date = new Date(timestamp);
  const now = new Date();
  const diffMs = now - date;
  const diffMins = Math.floor(diffMs / 60000);
  const diffHours = Math.floor(diffMs / 3600000);
  const diffDays = Math.floor(diffMs / 86400000);

  if (diffMins < 1) {
    return '刚刚';
  } else if (diffMins < 60) {
    return `${diffMins} 分钟前`;
  } else if (diffHours < 24) {
    return `${diffHours} 小时前`;
  } else if (diffDays < 7) {
    return `${diffDays} 天前`;
  } else {
    return date.toLocaleDateString('zh-CN');
  }
}

// 去除首尾空格并验证非空
function validateInput(text) {
  const trimmed = text.trim();
  return {
    valid: trimmed.length > 0,
    value: trimmed
  };
}

// 日志工具
const logger = {
  log(message, data) {
    console.log(`[TodoApp] ${message}`, data || '');
  },
  
  error(message, error) {
    console.error(`[TodoApp Error] ${message}`, error || '');
  },
  
  info(message) {
    console.info(`[TodoApp Info] ${message}`);
  }
};

console.log('utils.js 加载完成');

关键点:

  • 模块化组织代码
  • 添加日志追踪功能
  • 防止 XSS 攻击的安全措施
  • 输入验证确保数据质量

步骤 4: 创建存储管理模块 ​

创建 js/storage.js 文件:

javascript
/**
 * 存储管理模块
 * 负责数据的持久化存储
 */

console.log('加载 storage.js');

const Storage = {
  // 存储键名
  STORAGE_KEY: 'todos',
  
  /**
   * 获取所有待办事项
   * @returns {Array} 待办事项数组
   */
  getTodos() {
    try {
      const todosJSON = localStorage.getItem(this.STORAGE_KEY);
      const todos = todosJSON ? JSON.parse(todosJSON) : [];
      
      logger.log('从存储加载数据', `共 ${todos.length} 项`);
      return todos;
      
    } catch (error) {
      logger.error('加载数据失败', error);
      return [];
    }
  },
  
  /**
   * 保存所有待办事项
   * @param {Array} todos 待办事项数组
   */
  saveTodos(todos) {
    try {
      const todosJSON = JSON.stringify(todos);
      localStorage.setItem(this.STORAGE_KEY, todosJSON);
      
      logger.log('保存数据成功', `共 ${todos.length} 项`);
      
    } catch (error) {
      logger.error('保存数据失败', error);
      
      // 如果是配额超出错误,提示用户
      if (error.name === 'QuotaExceededError') {
        alert('存储空间不足,请清理一些数据!');
      }
    }
  },
  
  /**
   * 清空所有数据
   */
  clearAll() {
    try {
      localStorage.removeItem(this.STORAGE_KEY);
      logger.info('已清空所有数据');
      
    } catch (error) {
      logger.error('清空数据失败', error);
    }
  },
  
  /**
   * 导出数据(JSON 格式)
   * @returns {string} JSON 字符串
   */
  exportData() {
    const todos = this.getTodos();
    return JSON.stringify(todos, null, 2);
  },
  
  /**
   * 导入数据
   * @param {string} jsonData JSON 字符串
   * @returns {boolean} 是否成功
   */
  importData(jsonData) {
    try {
      const todos = JSON.parse(jsonData);
      
      // 验证数据格式
      if (!Array.isArray(todos)) {
        throw new Error('数据格式错误:不是数组');
      }
      
      this.saveTodos(todos);
      logger.log('导入数据成功', `共 ${todos.length} 项`);
      return true;
      
    } catch (error) {
      logger.error('导入数据失败', error);
      alert('导入失败:数据格式错误');
      return false;
    }
  }
};

console.log('storage.js 加载完成');

关键点:

  • 封装 localStorage 操作
  • 异常处理和错误恢复
  • 提供数据导入导出功能
  • 详细的日志记录

步骤 5: 创建主应用逻辑 ​

创建 js/app.js 文件:

javascript
/**
 * 待办事项应用主逻辑
 */

console.log('加载 app.js');

// 应用状态
const App = {
  // 数据
  todos: [],
  currentFilter: 'all', // 'all' | 'active' | 'completed'
  editingId: null,      // 正在编辑的任务 ID
  
  // DOM 元素(缓存)
  elements: {},
  
  /**
   * 初始化应用
   */
  init() {
    logger.info('初始化应用');
    
    // 缓存 DOM 元素
    this.cacheElements();
    
    // 加载数据
    this.loadData();
    
    // 绑定事件
    this.bindEvents();
    
    // 渲染界面
    this.render();
    
    logger.info('应用初始化完成');
  },
  
  /**
   * 缓存 DOM 元素
   */
  cacheElements() {
    this.elements = {
      // 表单
      form: document.getElementById('todoForm'),
      input: document.getElementById('todoInput'),
      
      // 列表
      list: document.getElementById('todoList'),
      emptyState: document.getElementById('emptyState'),
      
      // 过滤按钮
      filterButtons: document.querySelectorAll('.btn-filter'),
      
      // 底部
      stats: document.getElementById('stats'),
      clearCompleted: document.getElementById('clearCompleted')
    };
    
    logger.log('DOM 元素已缓存');
  },
  
  /**
   * 加载数据
   */
  loadData() {
    this.todos = Storage.getTodos();
    logger.log('数据已加载', `共 ${this.todos.length} 项`);
  },
  
  /**
   * 保存数据
   */
  saveData() {
    Storage.saveTodos(this.todos);
  },
  
  /**
   * 绑定事件监听器
   */
  bindEvents() {
    // 表单提交
    this.elements.form.addEventListener('submit', (e) => {
      e.preventDefault();
      this.handleAdd();
    });
    
    // 列表事件委托
    this.elements.list.addEventListener('click', (e) => {
      const target = e.target;
      const todoItem = target.closest('.todo-item');
      
      if (!todoItem) return;
      
      const id = todoItem.dataset.id;
      
      // 复选框点击
      if (target.classList.contains('todo-checkbox')) {
        this.handleToggle(id);
      }
      // 编辑按钮
      else if (target.classList.contains('btn-edit')) {
        this.handleEdit(id);
      }
      // 删除按钮
      else if (target.classList.contains('btn-delete')) {
        this.handleDelete(id);
      }
      // 保存按钮
      else if (target.classList.contains('btn-save')) {
        this.handleSave(id);
      }
      // 取消按钮
      else if (target.classList.contains('btn-cancel')) {
        this.handleCancelEdit(id);
      }
    });
    
    // 编辑输入框回车保存
    this.elements.list.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' && e.target.classList.contains('todo-edit-input')) {
        const todoItem = e.target.closest('.todo-item');
        if (todoItem) {
          this.handleSave(todoItem.dataset.id);
        }
      }
    });
    
    // 过滤按钮
    this.elements.filterButtons.forEach(button => {
      button.addEventListener('click', () => {
        this.handleFilter(button.dataset.filter);
      });
    });
    
    // 清除已完成
    this.elements.clearCompleted.addEventListener('click', () => {
      this.handleClearCompleted();
    });
    
    logger.log('事件监听器已绑定');
  },
  
  /**
   * 添加任务
   */
  handleAdd() {
    const input = this.elements.input;
    const validation = validateInput(input.value);
    
    if (!validation.valid) {
      logger.info('输入为空,忽略');
      return;
    }
    
    // 创建新任务
    const todo = {
      id: generateId(),
      text: validation.value,
      completed: false,
      createdAt: Date.now()
    };
    
    // 添加到数组开头
    this.todos.unshift(todo);
    
    // 保存和渲染
    this.saveData();
    this.render();
    
    // 清空输入框
    input.value = '';
    input.focus();
    
    logger.log('添加任务', todo);
  },
  
  /**
   * 切换完成状态
   */
  handleToggle(id) {
    const todo = this.todos.find(t => t.id === id);
    
    if (todo) {
      todo.completed = !todo.completed;
      this.saveData();
      this.render();
      
      logger.log('切换状态', { id, completed: todo.completed });
    }
  },
  
  /**
   * 进入编辑模式
   */
  handleEdit(id) {
    this.editingId = id;
    this.render();
    
    // 聚焦到编辑输入框
    const todoItem = this.elements.list.querySelector(`[data-id="${id}"]`);
    const editInput = todoItem?.querySelector('.todo-edit-input');
    
    if (editInput) {
      editInput.focus();
      editInput.select();
    }
    
    logger.log('进入编辑模式', id);
  },
  
  /**
   * 保存编辑
   */
  handleSave(id) {
    const todoItem = this.elements.list.querySelector(`[data-id="${id}"]`);
    const editInput = todoItem?.querySelector('.todo-edit-input');
    
    if (!editInput) return;
    
    const validation = validateInput(editInput.value);
    
    if (!validation.valid) {
      alert('任务内容不能为空!');
      editInput.focus();
      return;
    }
    
    // 更新任务文本
    const todo = this.todos.find(t => t.id === id);
    if (todo) {
      todo.text = validation.value;
      this.saveData();
    }
    
    // 退出编辑模式
    this.editingId = null;
    this.render();
    
    logger.log('保存编辑', { id, text: validation.value });
  },
  
  /**
   * 取消编辑
   */
  handleCancelEdit(id) {
    this.editingId = null;
    this.render();
    
    logger.log('取消编辑', id);
  },
  
  /**
   * 删除任务
   */
  handleDelete(id) {
    // 确认删除
    if (!confirm('确定要删除这个任务吗?')) {
      return;
    }
    
    // 从数组中删除
    this.todos = this.todos.filter(t => t.id !== id);
    
    // 如果删除的是正在编辑的任务,退出编辑模式
    if (this.editingId === id) {
      this.editingId = null;
    }
    
    this.saveData();
    this.render();
    
    logger.log('删除任务', id);
  },
  
  /**
   * 切换过滤器
   */
  handleFilter(filter) {
    this.currentFilter = filter;
    
    // 更新按钮状态
    this.elements.filterButtons.forEach(button => {
      if (button.dataset.filter === filter) {
        button.classList.add('active');
      } else {
        button.classList.remove('active');
      }
    });
    
    this.render();
    
    logger.log('切换过滤器', filter);
  },
  
  /**
   * 清除已完成任务
   */
  handleClearCompleted() {
    const completedCount = this.todos.filter(t => t.completed).length;
    
    if (completedCount === 0) {
      alert('没有已完成的任务!');
      return;
    }
    
    if (!confirm(`确定要清除 ${completedCount} 个已完成的任务吗?`)) {
      return;
    }
    
    this.todos = this.todos.filter(t => !t.completed);
    this.saveData();
    this.render();
    
    logger.log('清除已完成', `删除了 ${completedCount} 项`);
  },
  
  /**
   * 获取过滤后的任务
   */
  getFilteredTodos() {
    switch (this.currentFilter) {
      case 'active':
        return this.todos.filter(t => !t.completed);
      case 'completed':
        return this.todos.filter(t => t.completed);
      default:
        return this.todos;
    }
  },
  
  /**
   * 渲染任务项
   */
  renderTodoItem(todo) {
    const isEditing = this.editingId === todo.id;
    
    if (isEditing) {
      // 编辑模式
      return `
        <li class="todo-item ${todo.completed ? 'completed' : ''}" data-id="${todo.id}">
          <input 
            type="text" 
            class="todo-edit-input" 
            value="${escapeHTML(todo.text)}"
          >
          <div class="todo-actions">
            <button class="btn btn-icon btn-save">保存</button>
            <button class="btn btn-icon btn-cancel">取消</button>
          </div>
        </li>
      `;
    } else {
      // 正常模式
      return `
        <li class="todo-item ${todo.completed ? 'completed' : ''}" data-id="${todo.id}">
          <input 
            type="checkbox" 
            class="todo-checkbox" 
            ${todo.completed ? 'checked' : ''}
          >
          <span class="todo-text">${escapeHTML(todo.text)}</span>
          <div class="todo-actions">
            <button class="btn btn-icon btn-edit">✏️</button>
            <button class="btn btn-icon btn-delete">🗑️</button>
          </div>
        </li>
      `;
    }
  },
  
  /**
   * 渲染整个应用
   */
  render() {
    const filteredTodos = this.getFilteredTodos();
    
    // 渲染列表
    if (filteredTodos.length === 0) {
      this.elements.list.innerHTML = '';
      this.elements.emptyState.classList.add('show');
    } else {
      this.elements.emptyState.classList.remove('show');
      
      const html = filteredTodos
        .map(todo => this.renderTodoItem(todo))
        .join('');
      
      this.elements.list.innerHTML = html;
    }
    
    // 更新统计
    this.renderStats();
    
    logger.log('渲染完成', `显示 ${filteredTodos.length} 项`);
  },
  
  /**
   * 渲染统计信息
   */
  renderStats() {
    const total = this.todos.length;
    const completed = this.todos.filter(t => t.completed).length;
    const active = total - completed;
    
    let statsText = `共 ${total} 项`;
    
    if (completed > 0) {
      statsText += ` · ${completed} 项已完成`;
    }
    
    if (active > 0) {
      statsText += ` · ${active} 项进行中`;
    }
    
    this.elements.stats.textContent = statsText;
    
    // 清除按钮状态
    this.elements.clearCompleted.disabled = completed === 0;
  }
};

// DOM 加载完成后初始化应用
document.addEventListener('DOMContentLoaded', () => {
  App.init();
});

console.log('app.js 加载完成');

关键点:

  • 单一职责:每个方法只做一件事
  • 状态管理:集中管理应用状态
  • 事件委托:高效处理列表事件
  • 数据验证:确保输入合法性
  • 错误处理:优雅处理异常情况
  • 用户体验:即时反馈和确认对话框

✅ 功能验收清单 ​

完成项目后,检查以下功能是否正常工作:

基本功能 ​

  • [ ] 可以添加新任务
  • [ ] 可以标记任务完成/未完成
  • [ ] 可以删除任务
  • [ ] 空输入无法添加
  • [ ] 任务列表正确显示

编辑功能 ​

  • [ ] 可以进入编辑模式
  • [ ] 可以保存编辑内容
  • [ ] 可以取消编辑
  • [ ] 回车键保存编辑
  • [ ] 空内容无法保存

过滤功能 ​

  • [ ] "全部" 显示所有任务
  • [ ] "进行中" 只显示未完成任务
  • [ ] "已完成" 只显示已完成任务
  • [ ] 过滤按钮状态正确切换

数据持久化 ​

  • [ ] 刷新页面后数据仍然存在
  • [ ] 关闭浏览器重新打开,数据仍在
  • [ ] localStorage 中存储了正确的数据

统计信息 ​

  • [ ] 显示任务总数
  • [ ] 显示已完成数量
  • [ ] 显示进行中数量
  • [ ] 统计信息实时更新

其他功能 ​

  • [ ] 清除已完成任务功能正常
  • [ ] 空状态提示正确显示
  • [ ] 删除前有确认对话框
  • [ ] 界面美观,交互流畅

代码质量 ​

  • [ ] 代码结构清晰,易于理解
  • [ ] 有适当的注释
  • [ ] 遵循命名规范
  • [ ] 没有控制台错误

🎓 学习总结 ​

涵盖的知识点 ​

  1. DOM 操作

    • 元素选择(querySelector, getElementById)
    • 事件监听(addEventListener)
    • 动态创建和删除元素
    • 类名操作(classList)
  2. 数组方法

    • filter() - 过滤数据
    • find() - 查找元素
    • map() - 渲染列表
    • unshift() - 添加到开头
  3. 对象操作

    • 创建对象
    • 访问和修改属性
    • 解构赋值
  4. 函数

    • 箭头函数
    • 高阶函数
    • 回调函数
    • 模块化组织
  5. 浏览器 API

    • localStorage 存储
    • JSON 序列化
    • 表单处理
  6. 最佳实践

    • 代码组织和模块化
    • 事件委托
    • DOM 缓存
    • 输入验证
    • 错误处理

🚀 扩展建议 ​

完成基础功能后,可以尝试以下扩展:

初级扩展 ​

  1. 添加优先级

    • 为任务添加高/中/低优先级
    • 不同优先级使用不同颜色
  2. 任务排序

    • 按创建时间排序
    • 按优先级排序
    • 按字母顺序排序
  3. 搜索功能

    • 添加搜索框
    • 实时过滤匹配的任务

中级扩展 ​

  1. 分类标签

    • 为任务添加标签(工作、学习、生活等)
    • 按标签过滤
  2. 截止日期

    • 为任务设置截止日期
    • 显示即将到期的任务
    • 过期任务高亮显示
  3. 统计图表

    • 使用 Canvas 或 SVG 绘制完成率饼图
    • 显示每日完成任务数量趋势

高级扩展 ​

  1. 拖拽排序

    • 使用 HTML5 Drag and Drop API
    • 实现任务拖拽重排
  2. 数据导出

    • 导出为 JSON 文件
    • 导出为 CSV 文件
    • 打印功能
  3. 主题切换

    • 亮色/暗色主题
    • 保存用户偏好
  4. 多用户支持

    • 用户注册登录
    • 云端同步数据
    • 使用 Firebase 或自建后端

📝 常见问题 ​

Q1: 为什么刷新后数据消失? ​

A: 检查以下几点:

  1. 确保正确调用了 Storage.saveTodos()
  2. 检查浏览器控制台是否有错误
  3. 查看 localStorage 中是否有数据(开发者工具 → Application → Local Storage)

Q2: 编辑功能不工作? ​

A: 确保:

  1. 正确设置了 editingId
  2. 事件委托正确匹配了按钮类名
  3. 输入框的值正确绑定

Q3: 如何调试代码? ​

A: 使用以下方法:

  1. 在关键位置添加 console.log()
  2. 使用浏览器开发者工具断点调试
  3. 检查 Network 和 Console 面板

Q4: CSS 样式不生效? ​

A: 检查:

  1. CSS 文件路径是否正确
  2. 类名是否拼写正确
  3. 是否有样式冲突
  4. 浏览器缓存(硬刷新: Ctrl+Shift+R)

📚 下一步 ​

恭喜完成待办事项应用!现在你可以:

  1. 完成练习题 - 阶段 1 练习题
  2. 自我评估 - 回顾学习目标,检查掌握程度
  3. 分享作品 - 将项目部署到 GitHub Pages
  4. 继续学习 - 进入阶段 2:TypeScript 入门

📖 参考资源 ​

祝你学习愉快! 🎉

由 VitePress 构建,Markdown 教材原地维护。