🎯 课程目标

完成本课程后,你将能够:

  • 综合运用HTML、CSS和JavaScript开发完整应用
  • 实现待办事项的增删改查功能
  • 使用localStorage实现数据持久化存储
  • 实现任务的分类、筛选和搜索功能
  • 掌握项目开发和代码组织的最佳实践

📖 项目概述

待办事项应用(Todo App)是最经典的练手项目之一,它涵盖了前端开发的核心技能:表单处理、列表渲染、事件处理、状态管理和本地存储。通过这个项目,你将学会如何构建一个功能完整的交互式Web应用。

✨ 功能需求

  • 添加任务:输入任务内容,按回车或点击按钮添加
  • 完成状态:点击任务切换完成/未完成状态
  • 删除任务:提供删除按钮移除任务
  • 编辑任务:支持修改已添加的任务
  • 筛选功能:显示全部/已完成/未完成的任务
  • 数据持久化:刷新页面后数据不丢失

💻 项目结构

todo-app/ ├── index.html # 主页面结构 ├── style.css # 样式文件 └── app.js # 逻辑代码 <!-- index.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="style.css"> </head> <body> <div class="container"> <h1>待办事项</h1> <!-- 输入区域 --> <div class="input-section"> <input type="text" id="todoInput" placeholder="添加新任务..."> <input type="date" id="dueDate"> <select id="priority"> <option value="low">低优先级</option> <option value="medium" selected>中优先级</option> <option value="high">高优先级</option> </select> <button id="addBtn">添加</button> </div> <!-- 筛选区域 --> <div class="filter-section"> <button class="filter-btn active" data-filter="all">全部</button> <button class="filter-btn" data-filter="active">未完成</button> <button class="filter-btn" data-filter="completed">已完成</button> <span id="taskCount">0 个任务</span> </div> <!-- 任务列表 --> <ul id="todoList"></ul> </div> <script src="app.js"></script> </body> </html>

🎨 样式设计

/* style.css */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; } .container { max-width: 600px; margin: 0 auto; background: white; border-radius: 15px; padding: 30px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); } h1 { text-align: center; color: #333; margin-bottom: 25px; } .input-section { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; } .input-section input[type="text"] { flex: 1; min-width: 200px; padding: 12px 15px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; } .input-section input[type="date"], .input-section select { padding: 12px; border: 2px solid #ddd; border-radius: 8px; } .input-section button { padding: 12px 25px; background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; font-weight: bold; } .filter-section { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; } .filter-btn { padding: 8px 16px; border: 2px solid #667eea; background: white; color: #667eea; border-radius: 20px; cursor: pointer; transition: all 0.3s; } .filter-btn.active, .filter-btn:hover { background: #667eea; color: white; } #taskCount { margin-left: auto; color: #666; } #todoList { list-style: none; } .todo-item { display: flex; align-items: center; padding: 15px; border-bottom: 1px solid #eee; transition: background 0.3s; } .todo-item:hover { background: #f8f9fa; } .todo-item.completed .todo-text { text-decoration: line-through; color: #999; } .todo-checkbox { width: 20px; height: 20px; margin-right: 15px; cursor: pointer; } .todo-text { flex: 1; font-size: 16px; cursor: pointer; } .priority-badge { padding: 4px 8px; border-radius: 4px; font-size: 12px; margin-right: 10px; } .priority-low { background: #e8f5e9; color: #4caf50; } .priority-medium { background: #fff3e0; color: #ff9800; } .priority-high { background: #ffebee; color: #f44336; } .due-date { color: #666; font-size: 14px; margin-right: 10px; } .delete-btn { padding: 6px 12px; background: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer; margin-left: 10px; } .delete-btn:hover { background: #cc0000; } .empty-message { text-align: center; color: #999; padding: 30px; font-size: 18px; }

⚡ JavaScript实现

// app.js // 获取DOM元素 const todoInput = document.getElementById('todoInput'); const dueDateInput = document.getElementById('dueDate'); const prioritySelect = document.getElementById('priority'); const addBtn = document.getElementById('addBtn'); const todoList = document.getElementById('todoList'); const filterBtns = document.querySelectorAll('.filter-btn'); const taskCount = document.getElementById('taskCount'); // 状态管理 let todos = JSON.parse(localStorage.getItem('todos')) || []; let currentFilter = 'all'; // 初始化 function init() { renderTodos(); updateTaskCount(); setMinDate(); } // 设置最小日期为今天 function setMinDate() { const today = new Date().toISOString().split('T')[0]; dueDateInput.min = today; } // 添加任务 function addTodo() { const text = todoInput.value.trim(); if (!text) { alert('请输入任务内容'); return; } const todo = { id: Date.now(), text: text, completed: false, priority: prioritySelect.value, dueDate: dueDateInput.value, createdAt: new Date().toISOString() }; todos.unshift(todo); saveTodos(); renderTodos(); updateTaskCount(); // 清空输入框 todoInput.value = ''; dueDateInput.value = ''; } // 删除任务 function deleteTodo(id) { todos = todos.filter(todo => todo.id !== id); saveTodos(); renderTodos(); updateTaskCount(); } // 切换完成状态 function toggleTodo(id) { const todo = todos.find(todo => todo.id === id); if (todo) { todo.completed = !todo.completed; saveTodos(); renderTodos(); updateTaskCount(); } } // 保存到localStorage function saveTodos() { localStorage.setItem('todos', JSON.stringify(todos)); } // 渲染任务列表 function renderTodos() { const filteredTodos = filterTodos(todos); if (filteredTodos.length === 0) { todoList.innerHTML = '<li class="empty-message">暂无任务</li>'; return; } todoList.innerHTML = filteredTodos.map(todo => ` <li class="todo-item ${todo.completed ? 'completed' : ''}"> <input type="checkbox" class="todo-checkbox" ${todo.completed ? 'checked' : ''}> <span class="todo-text">${escapeHtml(todo.text)}</span> <span class="priority-badge priority-${todo.priority}"> ${getPriorityText(todo.priority)} </span> ${todo.dueDate ? `<span class="due-date">📅 ${formatDate(todo.dueDate)}</span>` : ''} <button class="delete-btn" data-id="${todo.id}">删除</button> </li> `).join(''); // 绑定事件 bindEvents(); } // 筛选任务 function filterTodos(todoList) { switch (currentFilter) { case 'active': return todoList.filter(todo => !todo.completed); case 'completed': return todoList.filter(todo => todo.completed); default: return todoList; } } // 绑定事件 function bindEvents() { // 复选框点击 document.querySelectorAll('.todo-checkbox').forEach(checkbox => { checkbox.addEventListener('change', (e) => { const id = parseInt(e.target.closest('.todo-item') .querySelector('.delete-btn').dataset.id); toggleTodo(id); }); }); // 删除按钮点击 document.querySelectorAll('.delete-btn').forEach(btn => { btn.addEventListener('click', (e) => { const id = parseInt(e.target.dataset.id); deleteTodo(id); }); }); // 任务文本点击(可以添加编辑功能) document.querySelectorAll('.todo-text').forEach(span => { span.addEventListener('click', () => { // TODO: 实现编辑功能 }); }); } // 更新任务计数 function updateTaskCount() { const activeCount = todos.filter(todo => !todo.completed).length; taskCount.textContent = `${activeCount} 个未完成任务`; } // 筛选按钮事件 filterBtns.forEach(btn => { btn.addEventListener('click', () => { filterBtns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentFilter = btn.dataset.filter; renderTodos(); }); }); // 工具函数 function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function getPriorityText(priority) { const map = { low: '低', medium: '中', high: '高' }; return map[priority] || '中'; } function formatDate(dateStr) { const date = new Date(dateStr); return `${date.getMonth() + 1}/${date.getDate()}`; } // 事件监听 addBtn.addEventListener('click', addTodo); todoInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') addTodo(); }); // 初始化 init();

🚀 功能扩展

编辑任务

点击任务文本时,将文本转换为输入框,允许用户编辑任务内容。

批量操作

添加"标记全部完成"和"清除已完成"按钮,实现批量管理。

搜索功能

添加搜索框,实时过滤显示匹配的任务。

分类标签

支持添加自定义标签或分类,便于任务组织管理。

⚠️ 常见问题

问题原因分析解决方案
数据不保存localStorage存储失败检查浏览器是否支持localStorage
日期显示错误时区问题使用日期格式化函数处理
XSS攻击风险直接插入用户输入的HTML使用escapeHtml转义特殊字符
任务顺序错乱ID不是按时间顺序生成使用Date.now()确保唯一且递增

✅ 课后练习

练习要求:请独立完成以下练习任务:

  1. 练习 1:完成本课的基础待办应用,确保所有功能正常工作
  2. 练习 2:添加任务编辑功能,点击任务可修改内容
  3. 选做练习:添加任务分类和标签功能,支持按标签筛选