实战项目2:待办事项列表
项目介绍
在这个项目中,我们将创建一个功能完整的待办事项列表应用,该应用允许用户添加、删除、编辑和标记完成待办事项。这个项目将帮助我们学习JavaScript DOM操作、事件处理、本地存储和状态管理等核心概念。
项目需求
- 显示待办事项列表
- 提供输入框,允许用户添加新的待办事项
- 允许用户标记待办事项为已完成/未完成
- 允许用户编辑待办事项
- 允许用户删除待办事项
- 支持待办事项的筛选(全部、已完成、未完成)
- 显示待办事项的统计信息(总数、已完成数、未完成数)
- 支持清空所有已完成的待办事项
- 支持将待办事项保存到本地存储
- 页面布局简洁美观,响应式设计
技术栈
- HTML5
- CSS3
- JavaScript (ES6+)
项目结构
todo-list-project/
├── index.html # 主页面
├── styles.css # 样式文件
└── script.js # JavaScript文件实现步骤
1. 创建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="styles.css">
</head>
<body>
<div class="todo-container">
<h1>待办事项列表</h1>
<!-- 待办事项输入区域 -->
<div class="todo-input-section">
<input
type="text"
id="todo-input"
placeholder="添加新的待办事项..."
autocomplete="off"
>
<button id="add-btn">添加</button>
</div>
<!-- 待办事项列表 -->
<ul id="todo-list"></ul>
<!-- 操作和筛选区域 -->
<div class="todo-actions">
<div class="todo-stats">
<span id="total-count">0</span> 项待办,
<span id="completed-count">0</span> 项已完成
</div>
<div class="todo-filters">
<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>
<button id="clear-completed-btn">清空已完成</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>2. 添加CSS样式
接下来,我们需要添加CSS样式,使待办事项列表应用看起来更加美观。
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background-color: #f5f5f5;
color: #333;
line-height: 1.6;
}
.todo-container {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
padding: 20px;
max-width: 600px;
width: 100%;
margin: 50px auto;
}
h1 {
color: #2c3e50;
text-align: center;
margin-bottom: 20px;
font-size: 28px;
}
/* 输入区域样式 */
.todo-input-section {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
#todo-input {
flex: 1;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 16px;
transition: all 0.3s ease;
}
#todo-input:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 12px 20px;
font-size: 16px;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: bold;
}
button:hover {
background-color: #2980b9;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
button:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* 待办事项列表样式 */
#todo-list {
list-style: none;
margin-bottom: 20px;
}
.todo-item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #e0e0e0;
transition: all 0.3s ease;
background-color: #fafafa;
border-radius: 5px;
margin-bottom: 10px;
}
.todo-item:hover {
background-color: #f0f0f0;
transform: translateX(5px);
}
.todo-item.completed {
opacity: 0.7;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #95a5a6;
}
.todo-checkbox {
width: 20px;
height: 20px;
cursor: pointer;
margin-right: 15px;
}
.todo-text {
flex: 1;
font-size: 16px;
word-break: break-word;
}
.todo-edit-input {
flex: 1;
padding: 8px;
border: 2px solid #3498db;
border-radius: 5px;
font-size: 16px;
margin-right: 15px;
}
.todo-actions-btn {
display: flex;
gap: 5px;
}
.edit-btn, .delete-btn {
padding: 8px 12px;
font-size: 14px;
border-radius: 3px;
}
.edit-btn {
background-color: #f39c12;
}
.edit-btn:hover {
background-color: #e67e22;
}
.delete-btn {
background-color: #e74c3c;
}
.delete-btn:hover {
background-color: #c0392b;
}
/* 操作和筛选区域样式 */
.todo-actions {
display: flex;
flex-direction: column;
gap: 15px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
}
.todo-stats {
font-size: 14px;
color: #7f8c8d;
text-align: center;
}
.todo-filters {
display: flex;
justify-content: center;
gap: 10px;
}
.filter-btn {
background-color: #ecf0f1;
color: #34495e;
border: 1px solid #bdc3c7;
padding: 8px 15px;
font-size: 14px;
border-radius: 20px;
}
.filter-btn:hover {
background-color: #d5dbdb;
transform: none;
box-shadow: none;
}
.filter-btn.active {
background-color: #3498db;
color: white;
border-color: #3498db;
}
#clear-completed-btn {
background-color: #95a5a6;
padding: 8px 15px;
font-size: 14px;
margin: 0 auto;
}
#clear-completed-btn:hover {
background-color: #7f8c8d;
transform: none;
box-shadow: none;
}
/* 空状态样式 */
.empty-state {
text-align: center;
padding: 40px 20px;
color: #95a5a6;
font-style: italic;
}
/* 响应式设计 */
@media (max-width: 768px) {
.todo-container {
margin: 20px;
padding: 15px;
}
h1 {
font-size: 24px;
}
.todo-input-section {
flex-direction: column;
}
.todo-actions {
flex-direction: column;
}
.todo-filters {
flex-wrap: wrap;
}
}3. 编写JavaScript逻辑
最后,我们需要编写JavaScript逻辑,实现待办事项列表的所有功能。
// 获取DOM元素
const todoInput = document.getElementById('todo-input');
const addBtn = document.getElementById('add-btn');
const todoList = document.getElementById('todo-list');
const totalCount = document.getElementById('total-count');
const completedCount = document.getElementById('completed-count');
const filterBtns = document.querySelectorAll('.filter-btn');
const clearCompletedBtn = document.getElementById('clear-completed-btn');
// 初始化待办事项列表
let todos = JSON.parse(localStorage.getItem('todos')) || [];
let currentFilter = 'all';
// 渲染待办事项列表
function renderTodos() {
// 清空列表
todoList.innerHTML = '';
// 根据当前筛选条件过滤待办事项
const filteredTodos = todos.filter(todo => {
if (currentFilter === 'all') return true;
if (currentFilter === 'active') return !todo.completed;
if (currentFilter === 'completed') return todo.completed;
return true;
});
// 如果没有待办事项,显示空状态
if (filteredTodos.length === 0) {
const emptyState = document.createElement('div');
emptyState.className = 'empty-state';
emptyState.textContent = '暂无待办事项';
todoList.appendChild(emptyState);
return;
}
// 渲染每个待办事项
filteredTodos.forEach(todo => {
const todoItem = document.createElement('li');
todoItem.className = `todo-item ${todo.completed ? 'completed' : ''}`;
todoItem.dataset.id = todo.id;
todoItem.innerHTML = `
<input type="checkbox" class="todo-checkbox" ${todo.completed ? 'checked' : ''}>
<span class="todo-text">${todo.text}</span>
<input type="text" class="todo-edit-input" value="${todo.text}" style="display: none;">
<div class="todo-actions-btn">
<button class="edit-btn">编辑</button>
<button class="delete-btn">删除</button>
</div>
`;
todoList.appendChild(todoItem);
// 添加事件监听器
addTodoItemEventListeners(todoItem);
});
// 更新统计信息
updateStats();
}
// 添加待办事项项的事件监听器
function addTodoItemEventListeners(todoItem) {
const checkbox = todoItem.querySelector('.todo-checkbox');
const todoText = todoItem.querySelector('.todo-text');
const editInput = todoItem.querySelector('.todo-edit-input');
const editBtn = todoItem.querySelector('.edit-btn');
const deleteBtn = todoItem.querySelector('.delete-btn');
// 标记完成/未完成事件
checkbox.addEventListener('change', () => {
const id = parseInt(todoItem.dataset.id);
toggleTodoCompleted(id);
});
// 编辑按钮事件
editBtn.addEventListener('click', () => {
todoText.style.display = 'none';
editInput.style.display = 'block';
editInput.focus();
// 选中输入框内容
editInput.select();
});
// 编辑输入框事件
editInput.addEventListener('blur', () => {
saveEditedTodo(todoItem);
});
editInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
saveEditedTodo(todoItem);
}
});
// 删除按钮事件
deleteBtn.addEventListener('click', () => {
const id = parseInt(todoItem.dataset.id);
deleteTodo(id);
});
}
// 保存编辑后的待办事项
function saveEditedTodo(todoItem) {
const id = parseInt(todoItem.dataset.id);
const todoText = todoItem.querySelector('.todo-text');
const editInput = todoItem.querySelector('.todo-edit-input');
const newText = editInput.value.trim();
if (newText) {
editTodo(id, newText);
}
todoText.style.display = 'block';
editInput.style.display = 'none';
}
// 添加新的待办事项
function addTodo() {
const text = todoInput.value.trim();
if (text) {
const newTodo = {
id: Date.now(),
text: text,
completed: false
};
todos.push(newTodo);
saveTodos();
renderTodos();
// 清空输入框
todoInput.value = '';
todoInput.focus();
}
}
// 切换待办事项的完成状态
function toggleTodoCompleted(id) {
const todo = todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
saveTodos();
renderTodos();
}
}
// 编辑待办事项
function editTodo(id, newText) {
const todo = todos.find(t => t.id === id);
if (todo) {
todo.text = newText;
saveTodos();
renderTodos();
}
}
// 删除待办事项
function deleteTodo(id) {
todos = todos.filter(t => t.id !== id);
saveTodos();
renderTodos();
}
// 保存待办事项到本地存储
function saveTodos() {
localStorage.setItem('todos', JSON.stringify(todos));
}
// 更新统计信息
function updateStats() {
const total = todos.length;
const completed = todos.filter(t => t.completed).length;
totalCount.textContent = total;
completedCount.textContent = completed;
}
// 设置筛选条件
function setFilter(filter) {
currentFilter = filter;
// 更新筛选按钮的激活状态
filterBtns.forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.filter === filter) {
btn.classList.add('active');
}
});
// 重新渲染列表
renderTodos();
}
// 清空所有已完成的待办事项
function clearCompletedTodos() {
todos = todos.filter(t => !t.completed);
saveTodos();
renderTodos();
}
// 添加事件监听器
addBtn.addEventListener('click', addTodo);
// 按下回车键添加待办事项
todoInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
addTodo();
}
});
// 筛选按钮事件
filterBtns.forEach(btn => {
btn.addEventListener('click', () => {
setFilter(btn.dataset.filter);
});
});
// 清空已完成按钮事件
clearCompletedBtn.addEventListener('click', clearCompletedTodos);
// 初始渲染
renderTodos();功能说明
- 添加待办事项:在输入框中输入内容,点击"添加"按钮或按下回车键即可添加新的待办事项。
- 标记完成/未完成:点击复选框可以标记待办事项为已完成或未完成。
- 编辑待办事项:点击"编辑"按钮可以编辑待办事项的内容。
- 删除待办事项:点击"删除"按钮可以删除待办事项。
- 筛选待办事项:可以通过"全部"、"未完成"、"已完成"按钮筛选待办事项。
- 统计信息:显示待办事项的总数和已完成数。
- 清空已完成:点击"清空已完成"按钮可以删除所有已完成的待办事项。
- 本地存储:待办事项会自动保存到本地存储,刷新页面后数据不会丢失。
- 响应式设计:适配不同屏幕尺寸,在手机和电脑上都能正常使用。
扩展功能
我们可以进一步扩展待办事项列表的功能,例如:
- 支持待办事项的拖拽排序
- 添加待办事项的优先级设置
- 添加待办事项的截止日期
- 支持待办事项的分类
- 添加待办事项的搜索功能
- 支持待办事项的批量操作
- 添加深色/浅色主题切换
- 支持导出/导入待办事项
扩展功能示例:拖拽排序
// 添加拖拽排序功能
let draggedItem = null;
// 为待办事项添加拖拽属性
function addDragAndDrop() {
const todoItems = document.querySelectorAll('.todo-item');
todoItems.forEach(item => {
item.draggable = true;
// 拖拽开始事件
item.addEventListener('dragstart', (e) => {
draggedItem = item;
setTimeout(() => {
item.style.opacity = '0.5';
}, 0);
});
// 拖拽结束事件
item.addEventListener('dragend', () => {
draggedItem = null;
item.style.opacity = '1';
});
// 拖拽进入事件
item.addEventListener('dragover', (e) => {
e.preventDefault();
});
// 拖拽离开事件
item.addEventListener('dragleave', () => {
item.style.borderTop = '';
});
// 拖拽放置事件
item.addEventListener('drop', (e) => {
e.preventDefault();
const draggedId = parseInt(draggedItem.dataset.id);
const targetId = parseInt(item.dataset.id);
if (draggedId !== targetId) {
// 重新排序待办事项
const draggedIndex = todos.findIndex(t => t.id === draggedId);
const targetIndex = todos.findIndex(t => t.id === targetId);
const [removed] = todos.splice(draggedIndex, 1);
todos.splice(targetIndex, 0, removed);
saveTodos();
renderTodos();
}
});
});
}
// 在renderTodos函数的末尾添加拖拽排序功能
// addDragAndDrop();扩展功能示例:搜索功能
<!-- 在HTML中添加搜索框 -->
<div class="todo-search">
<input type="text" id="search-input" placeholder="搜索待办事项...">
</div>/* 在CSS中添加搜索框样式 */
.todo-search {
margin-bottom: 20px;
}
#search-input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 16px;
transition: all 0.3s ease;
}
#search-input:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}// 在JavaScript中添加搜索功能
const searchInput = document.getElementById('search-input');
let searchTerm = '';
// 更新搜索术语
searchInput.addEventListener('input', (e) => {
searchTerm = e.target.value.toLowerCase();
renderTodos();
});
// 修改renderTodos函数中的过滤逻辑
const filteredTodos = todos.filter(todo => {
const matchesSearch = todo.text.toLowerCase().includes(searchTerm);
const matchesFilter =
currentFilter === 'all' ||
(currentFilter === 'active' && !todo.completed) ||
(currentFilter === 'completed' && todo.completed);
return matchesSearch && matchesFilter;
});项目运行
- 创建上述三个文件(index.html, styles.css, script.js)
- 在浏览器中打开index.html文件
- 开始使用待办事项列表应用
项目总结
通过这个待办事项列表项目,我们学习了:
- 如何使用JavaScript获取和操作DOM元素
- 如何添加和处理各种事件
- 如何使用本地存储保存数据
- 如何实现状态管理
- 如何实现筛选和搜索功能
- 如何实现响应式设计
- 如何实现拖拽排序功能
这个项目涵盖了JavaScript开发中的许多重要概念,为我们后续开发更复杂的应用打下了基础。
练习
- 添加待办事项的优先级设置功能(高、中、低)
- 添加待办事项的截止日期功能
- 实现待办事项的拖拽排序功能
- 添加待办事项的搜索功能
- 实现深色/浅色主题切换功能
- 添加待办事项的分类功能
- 支持待办事项的导出/导入功能
- 添加待办事项的批量操作功能
扩展阅读
通过这个实战项目,我们已经掌握了JavaScript的核心概念和DOM操作,接下来我们将继续学习更复杂的JavaScript项目。