Day-19-项目1-待办事项应用

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-计时器应用,继续实战项目开发!