Day 19: 项目1-待办事项应用
🎯 Day 19 – 待办事项应用
📂 分类: 09-项目实战
🔥 难度: ⭐⭐⭐☆☆
🎯 学习目标
- ✅ 综合运用前18天学到的知识构建完整应用
- ✅ 掌握DOM操作、事件处理、数据持久化的综合应用
- ✅ 学会使用localStorage存储数据
- ✅ 理解MVC(Model-View-Controller)设计模式
- ✅ 培养项目化思维和代码组织能力
💡 项目概述
项目简介
**待办事项应用(Todo App)**是编程入门的经典项目,它能让你综合运用:
- HTML结构设计
- CSS样式美化
- JavaScript逻辑处理
- DOM操作
- 事件处理
- 数据存储
核心功能
- [ ] 添加新的待办事项
- [ ] 标记事项为完成/未完成
- [ ] 删除待办事项
- [ ] 编辑事项内容
- [ ] 过滤显示(全部/进行中/已完成)
- [ ] 数据持久化(刷新不丢失)
- [ ] 清空所有已完成事项
技术栈
- HTML5: 语义化标签
- CSS3: Flexbox布局、过渡动画
- JavaScript ES6+: 箭头函数、模板字符串、数组方法
- Web API: localStorage、DOM API
- 设计模式: MVC(模型-视图-控制器)
📝 项目结构
todo-app/
├── index.html # 主页面
├── style.css # 样式文件
└── script.js # JavaScript逻辑
🎮 完整代码
1. HTML (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">
<header>
<h1>📝 我的待办事项</h1>
<p class="date" id="currentDate"></p>
</header>
<!-- 输入区域 -->
<div class="input-section">
<input
type="text"
id="todoInput"
placeholder="添加新的待办事项..."
autocomplete="off"
>
<button id="addBtn" class="btn-add">
<span>+</span>
</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>
</div>
<!-- 统计信息 -->
<div class="stats">
<span id="totalCount">共 0 项</span>
<button id="clearCompleted" class="clear-btn">清除已完成</button>
</div>
<!-- 待办列表 -->
<ul id="todoList" class="todo-list"></ul>
<!-- 空状态提示 -->
<div id="emptyState" class="empty-state">
<p>🎉 太棒了!所有事项都完成了</p>
<small>或者添加一些新的待办事项吧</small>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
2. CSS (style.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;
}
/* 容器 */
.container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 100%;
padding: 30px;
}
/* 头部 */
header {
text-align: center;
margin-bottom: 30px;
}
header h1 {
color: #333;
font-size: 28px;
margin-bottom: 10px;
}
.date {
color: #666;
font-size: 14px;
}
/* 输入区域 */
.input-section {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
#todoInput {
flex: 1;
padding: 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 16px;
transition: border-color 0.3s;
}
#todoInput:focus {
outline: none;
border-color: #667eea;
}
.btn-add {
background: #667eea;
color: white;
border: none;
border-radius: 10px;
width: 50px;
font-size: 24px;
cursor: pointer;
transition: all 0.3s;
}
.btn-add:hover {
background: #5568d3;
transform: scale(1.05);
}
/* 过滤按钮 */
.filter-section {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.filter-btn {
flex: 1;
padding: 10px;
border: 2px solid #e0e0e0;
background: white;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.filter-btn:hover {
border-color: #667eea;
}
.filter-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
/* 统计信息 */
.stats {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
font-size: 14px;
color: #666;
}
.clear-btn {
background: none;
border: none;
color: #f56565;
cursor: pointer;
font-size: 14px;
}
.clear-btn:hover {
text-decoration: underline;
}
/* 待办列表 */
.todo-list {
list-style: none;
}
.todo-item {
display: flex;
align-items: center;
padding: 15px;
background: #f7f7f7;
border-radius: 10px;
margin-bottom: 10px;
transition: all 0.3s;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.todo-item:hover {
background: #f0f0f0;
}
.todo-item.completed {
opacity: 0.6;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #999;
}
/* 复选框 */
.todo-item input[type="checkbox"] {
width: 20px;
height: 20px;
margin-right: 15px;
cursor: pointer;
}
/* 待办文本 */
.todo-text {
flex: 1;
font-size: 16px;
color: #333;
word-break: break-word;
}
/* 删除按钮 */
.delete-btn {
background: #f56565;
color: white;
border: none;
border-radius: 6px;
padding: 6px 12px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s;
}
.delete-btn:hover {
background: #e53e3e;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 40px 20px;
color: #999;
}
.empty-state p {
font-size: 18px;
margin-bottom: 10px;
}
.empty-state small {
font-size: 14px;
}
/* 响应式 */
@media (max-width: 480px) {
.container {
padding: 20px;
}
header h1 {
font-size: 24px;
}
.filter-section {
flex-direction: column;
}
}
3. JavaScript (script.js)
// ========== 数据模型 ==========
class TodoModel {
constructor() {
this.todos = this.loadFromStorage();
}
// 从 localStorage 加载数据
loadFromStorage() {
const stored = localStorage.getItem('todos');
return stored ? JSON.parse(stored) : [];
}
// 保存到 localStorage
saveToStorage() {
localStorage.setItem('todos', JSON.stringify(this.todos));
}
// 添加待办事项
addTodo(text) {
const todo = {
id: Date.now(),
text: text,
completed: false,
createdAt: new Date().toISOString()
};
this.todos.push(todo);
this.saveToStorage();
return todo;
}
// 切换完成状态
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.saveToStorage();
}
return todo;
}
// 删除待办事项
deleteTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
this.saveToStorage();
}
// 编辑待办事项
editTodo(id, newText) {
const todo = this.todos.find(t => t.id === id);
if (todo && newText.trim()) {
todo.text = newText.trim();
this.saveToStorage();
}
}
// 清除已完成
clearCompleted() {
this.todos = this.todos.filter(t => !t.completed);
this.saveToStorage();
}
// 获取过滤后的列表
getFilteredTodos(filter) {
switch(filter) {
case 'active':
return this.todos.filter(t => !t.completed);
case 'completed':
return this.todos.filter(t => t.completed);
default:
return this.todos;
}
}
// 获取统计信息
getStats() {
const total = this.todos.length;
const completed = this.todos.filter(t => t.completed).length;
const active = total - completed;
return { total, completed, active };
}
}
// ========== 视图渲染 ==========
class TodoView {
constructor() {
this.todoList = document.getElementById('todoList');
this.todoInput = document.getElementById('todoInput');
this.emptyState = document.getElementById('emptyState');
this.totalCount = document.getElementById('totalCount');
}
// 渲染待办列表
renderTodos(todos) {
this.todoList.innerHTML = '';
if (todos.length === 0) {
this.emptyState.style.display = 'block';
} else {
this.emptyState.style.display = 'none';
todos.forEach(todo => {
const li = this.createTodoElement(todo);
this.todoList.appendChild(li);
});
}
}
// 创建待办事项元素
createTodoElement(todo) {
const li = document.createElement('li');
li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
li.dataset.id = todo.id;
li.innerHTML = `
<input type="checkbox" ${todo.completed ? 'checked' : ''}>
<span class="todo-text">${this.escapeHtml(todo.text)}</span>
<button class="delete-btn">删除</button>
`;
return li;
}
// 更新统计信息
updateStats(stats) {
this.totalCount.textContent = `共 ${stats.total} 项 (${stats.active} 进行中)`;
}
// 获取输入值
getInputValue() {
return this.todoInput.value.trim();
}
// 清空输入框
clearInput() {
this.todoInput.value = '';
}
// HTML转义(防止XSS)
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// ========== 控制器 ==========
class TodoController {
constructor(model, view) {
this.model = model;
this.view = view;
this.currentFilter = 'all';
this.initEventListeners();
this.render();
}
// 初始化事件监听
initEventListeners() {
// 添加按钮
document.getElementById('addBtn').addEventListener('click', () => {
this.handleAddTodo();
});
// 回车添加
this.view.todoInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.handleAddTodo();
}
});
// 列表点击事件(委托)
this.view.todoList.addEventListener('click', (e) => {
const todoItem = e.target.closest('.todo-item');
if (!todoItem) return;
const id = parseInt(todoItem.dataset.id);
if (e.target.type === 'checkbox') {
this.handleToggleTodo(id);
} else if (e.target.classList.contains('delete-btn')) {
this.handleDeleteTodo(id);
}
});
// 过滤按钮
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
this.handleFilterChange(btn);
});
});
// 清除已完成
document.getElementById('clearCompleted').addEventListener('click', () => {
this.handleClearCompleted();
});
}
// 处理添加待办
handleAddTodo() {
const text = this.view.getInputValue();
if (!text) {
alert('请输入待办事项!');
return;
}
this.model.addTodo(text);
this.view.clearInput();
this.render();
}
// 处理切换状态
handleToggleTodo(id) {
this.model.toggleTodo(id);
this.render();
}
// 处理删除
handleDeleteTodo(id) {
if (confirm('确定要删除这个待办事项吗?')) {
this.model.deleteTodo(id);
this.render();
}
}
// 处理过滤切换
handleFilterChange(btn) {
// 更新按钮状态
document.querySelectorAll('.filter-btn').forEach(b => {
b.classList.remove('active');
});
btn.classList.add('active');
// 更新过滤器
this.currentFilter = btn.dataset.filter;
this.render();
}
// 处理清除已完成
handleClearCompleted() {
const stats = this.model.getStats();
if (stats.completed === 0) {
alert('没有已完成的待办事项!');
return;
}
if (confirm(`确定要清除 ${stats.completed} 个已完成的事项吗?`)) {
this.model.clearCompleted();
this.render();
}
}
// 渲染视图
render() {
const filteredTodos = this.model.getFilteredTodos(this.currentFilter);
this.view.renderTodos(filteredTodos);
this.view.updateStats(this.model.getStats());
}
}
// ========== 应用初始化 ==========
// 显示当前日期
function displayCurrentDate() {
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
};
const today = new Date().toLocaleDateString('zh-CN', options);
document.getElementById('currentDate').textContent = today;
}
// 启动应用
document.addEventListener('DOMContentLoaded', () => {
displayCurrentDate();
const model = new TodoModel();
const view = new TodoView();
const controller = new TodoController(model, view);
console.log('🎉 待办事项应用已启动!');
console.log('💡 提示:所有数据都保存在本地,刷新页面不会丢失');
});
🎯 项目亮点
1. MVC架构设计
┌─────────────┐
│ View │ ← 负责UI渲染和用户交互
│ (视图层) │
└──────┬──────┘
│
↓
┌─────────────┐
│ Controller │ ← 处理业务逻辑和事件
│ (控制层) │
└──────┬──────┘
│
↓
┌─────────────┐
│ Model │ ← 管理数据和状态
│ (模型层) │
└─────────────┘
2. 数据持久化
使用 localStorage 保存数据:
// 保存
localStorage.setItem('todos', JSON.stringify(todos));
// 读取
const todos = JSON.parse(localStorage.getItem('todos'));
3. 事件委托
使用事件委托优化性能:
todoList.addEventListener('click', (e) => {
if (e.target.type === 'checkbox') {
// 处理复选框
}
});
4. 安全防护
HTML转义防止XSS攻击:
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
✍️ 练习任务
练习1:基础功能(30分钟)
- [ ] 创建项目文件结构
- [ ] 实现添加待办事项功能
- [ ] 实现删除待办事项功能
- [ ] 实现标记完成功能
- [ ] 测试基本功能是否正常
练习2:进阶功能(45分钟)
- [ ] 添加过滤功能(全部/进行中/已完成)
- [ ] 实现数据持久化(localStorage)
- [ ] 添加统计信息显示
- [ ] 实现清除已完成功能
- [ ] 添加空状态提示
练习3:UI优化(30分钟)
- [ ] 添加添加动画效果
- [ ] 优化移动端响应式布局
- [ ] 添加hover效果
- [ ] 美化按钮和输入框
- [ ] 添加日期显示
练习4:功能扩展(60分钟)
- [ ] 添加编辑待办事项功能(双击编辑)
- [ ] 拖拽排序功能(使用HTML5 Drag API)
- [ ] 优先级标记(高/中/低)
- [ ] 截止日期设置
- [ ] 导出待办列表为JSON
🎓 挑战项目
挑战:高级待办应用
目标:实现一个功能完整的待办事项应用
功能要求:
- [x] 基础CRUD操作
- [x] 数据持久化
- [x] 过滤功能
- [ ] 待办事项分类(工作/生活/学习)
- [ ] 优先级设置(高/中/低)
- [ ] 截止日期提醒
- [ ] 搜索功能
- [ ] 拖拽排序
- [ ] 暗黑模式切换
- [ ] 数据导出/导入
技术要求:
- 使用ES6+语法
- 代码结构清晰(MVC)
- 有良好的注释
- 响应式设计
💡 常见问题 FAQ
Q1: localStorage有大小限制吗?
A: 有,通常限制在5-10MB。对于文本数据来说足够了。如果需要存储大量数据,考虑使用IndexedDB。
Q2: 如何防止XSS攻击?
A: 在显示用户输入的内容时,要进行HTML转义:
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
Q3: 事件委托有什么好处?
A:
- 减少事件监听器数量
- 动态添加的元素也能响应事件
- 提高性能
Q4: 如何优化移动端体验?
A:
- 添加viewport meta标签
- 使用Flexbox布局
- 按钮尺寸至少44x44px
- 避免hover效果(移动端没有)
Q5: 代码可以进一步优化吗?
A: 可以考虑:
- 使用虚拟滚动(大量数据时)
- 添加防抖/节流
- 使用Web Worker处理大数据
- 添加单元测试
📚 拓展学习
推荐资源
在线教程:
相关项目:
- 笔记应用
- 番茄钟应用
- 账单管理应用
进阶主题:
- IndexedDB(大容量存储)
- Service Worker(离线应用)
- PWA(渐进式Web应用)
🎯 总结
今天我们学到了:
- ✅ MVC设计模式的应用
- ✅ localStorage数据持久化
- ✅ 事件委托优化性能
- ✅ 综合运用DOM操作
- ✅ 完整项目开发流程
关键概念:
- 📦 数据模型:管理应用状态
- 🎨 视图渲染:展示UI
- 🎮 控制器:处理逻辑
学习时间: 2026-03-17
难度: ⭐⭐⭐☆☆
预计用时: 3-4 小时
关键词: 待办事项、MVC、localStorage、DOM操作
相关标签: #09-项目实战
📌 下一步: 学习 Day 20: 项目2-计时器应用,继续实战项目开发!