主题
实战项目:待办事项应用
欢迎来到阶段 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 中存储了正确的数据
统计信息
- [ ] 显示任务总数
- [ ] 显示已完成数量
- [ ] 显示进行中数量
- [ ] 统计信息实时更新
其他功能
- [ ] 清除已完成任务功能正常
- [ ] 空状态提示正确显示
- [ ] 删除前有确认对话框
- [ ] 界面美观,交互流畅
代码质量
- [ ] 代码结构清晰,易于理解
- [ ] 有适当的注释
- [ ] 遵循命名规范
- [ ] 没有控制台错误
🎓 学习总结
涵盖的知识点
DOM 操作
- 元素选择(
querySelector,getElementById) - 事件监听(
addEventListener) - 动态创建和删除元素
- 类名操作(
classList)
- 元素选择(
数组方法
filter()- 过滤数据find()- 查找元素map()- 渲染列表unshift()- 添加到开头
对象操作
- 创建对象
- 访问和修改属性
- 解构赋值
函数
- 箭头函数
- 高阶函数
- 回调函数
- 模块化组织
浏览器 API
- localStorage 存储
- JSON 序列化
- 表单处理
最佳实践
- 代码组织和模块化
- 事件委托
- DOM 缓存
- 输入验证
- 错误处理
🚀 扩展建议
完成基础功能后,可以尝试以下扩展:
初级扩展
添加优先级
- 为任务添加高/中/低优先级
- 不同优先级使用不同颜色
任务排序
- 按创建时间排序
- 按优先级排序
- 按字母顺序排序
搜索功能
- 添加搜索框
- 实时过滤匹配的任务
中级扩展
分类标签
- 为任务添加标签(工作、学习、生活等)
- 按标签过滤
截止日期
- 为任务设置截止日期
- 显示即将到期的任务
- 过期任务高亮显示
统计图表
- 使用 Canvas 或 SVG 绘制完成率饼图
- 显示每日完成任务数量趋势
高级扩展
拖拽排序
- 使用 HTML5 Drag and Drop API
- 实现任务拖拽重排
数据导出
- 导出为 JSON 文件
- 导出为 CSV 文件
- 打印功能
主题切换
- 亮色/暗色主题
- 保存用户偏好
多用户支持
- 用户注册登录
- 云端同步数据
- 使用 Firebase 或自建后端
📝 常见问题
Q1: 为什么刷新后数据消失?
A: 检查以下几点:
- 确保正确调用了
Storage.saveTodos() - 检查浏览器控制台是否有错误
- 查看 localStorage 中是否有数据(开发者工具 → Application → Local Storage)
Q2: 编辑功能不工作?
A: 确保:
- 正确设置了
editingId - 事件委托正确匹配了按钮类名
- 输入框的值正确绑定
Q3: 如何调试代码?
A: 使用以下方法:
- 在关键位置添加
console.log() - 使用浏览器开发者工具断点调试
- 检查 Network 和 Console 面板
Q4: CSS 样式不生效?
A: 检查:
- CSS 文件路径是否正确
- 类名是否拼写正确
- 是否有样式冲突
- 浏览器缓存(硬刷新: Ctrl+Shift+R)
📚 下一步
恭喜完成待办事项应用!现在你可以:
- 完成练习题 - 阶段 1 练习题
- 自我评估 - 回顾学习目标,检查掌握程度
- 分享作品 - 将项目部署到 GitHub Pages
- 继续学习 - 进入阶段 2:TypeScript 入门
📖 参考资源
祝你学习愉快! 🎉