实战项目2:待办事项列表

项目介绍

在这个项目中,我们将创建一个功能完整的待办事项列表应用,该应用允许用户添加、删除、编辑和标记完成待办事项。这个项目将帮助我们学习JavaScript DOM操作、事件处理、本地存储和状态管理等核心概念。

项目需求

  1. 显示待办事项列表
  2. 提供输入框,允许用户添加新的待办事项
  3. 允许用户标记待办事项为已完成/未完成
  4. 允许用户编辑待办事项
  5. 允许用户删除待办事项
  6. 支持待办事项的筛选(全部、已完成、未完成)
  7. 显示待办事项的统计信息(总数、已完成数、未完成数)
  8. 支持清空所有已完成的待办事项
  9. 支持将待办事项保存到本地存储
  10. 页面布局简洁美观,响应式设计

技术栈

  • 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();

功能说明

  1. 添加待办事项:在输入框中输入内容,点击"添加"按钮或按下回车键即可添加新的待办事项。
  2. 标记完成/未完成:点击复选框可以标记待办事项为已完成或未完成。
  3. 编辑待办事项:点击"编辑"按钮可以编辑待办事项的内容。
  4. 删除待办事项:点击"删除"按钮可以删除待办事项。
  5. 筛选待办事项:可以通过"全部"、"未完成"、"已完成"按钮筛选待办事项。
  6. 统计信息:显示待办事项的总数和已完成数。
  7. 清空已完成:点击"清空已完成"按钮可以删除所有已完成的待办事项。
  8. 本地存储:待办事项会自动保存到本地存储,刷新页面后数据不会丢失。
  9. 响应式设计:适配不同屏幕尺寸,在手机和电脑上都能正常使用。

扩展功能

我们可以进一步扩展待办事项列表的功能,例如:

  1. 支持待办事项的拖拽排序
  2. 添加待办事项的优先级设置
  3. 添加待办事项的截止日期
  4. 支持待办事项的分类
  5. 添加待办事项的搜索功能
  6. 支持待办事项的批量操作
  7. 添加深色/浅色主题切换
  8. 支持导出/导入待办事项

扩展功能示例:拖拽排序

// 添加拖拽排序功能
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;
});

项目运行

  1. 创建上述三个文件(index.html, styles.css, script.js)
  2. 在浏览器中打开index.html文件
  3. 开始使用待办事项列表应用

项目总结

通过这个待办事项列表项目,我们学习了:

  1. 如何使用JavaScript获取和操作DOM元素
  2. 如何添加和处理各种事件
  3. 如何使用本地存储保存数据
  4. 如何实现状态管理
  5. 如何实现筛选和搜索功能
  6. 如何实现响应式设计
  7. 如何实现拖拽排序功能

这个项目涵盖了JavaScript开发中的许多重要概念,为我们后续开发更复杂的应用打下了基础。

练习

  1. 添加待办事项的优先级设置功能(高、中、低)
  2. 添加待办事项的截止日期功能
  3. 实现待办事项的拖拽排序功能
  4. 添加待办事项的搜索功能
  5. 实现深色/浅色主题切换功能
  6. 添加待办事项的分类功能
  7. 支持待办事项的导出/导入功能
  8. 添加待办事项的批量操作功能

扩展阅读

通过这个实战项目,我们已经掌握了JavaScript的核心概念和DOM操作,接下来我们将继续学习更复杂的JavaScript项目。

« 上一篇 实战项目1:计数器 下一篇 » 实战项目3:计算器