主页/jQuery教程/综合实战项目/jQuery项目一:交互式Todo列表应用

jQuery项目一:交互式Todo列表应用

12,517字
53–79 分钟

概览

目录

交互式Todo列表应用是一个经典的入门级实战项目,旨在综合运用jQuery的核心功能。本项目将实现任务的添加、删除、编辑、状态切换以及本地数据持久化。通过构建这个应用,可以深入理解jQuery选择器、DOM操作、事件处理、数据存储以及动画效果的综合应用,体会如何用简洁的代码构建完整的交互功能。jQuery在其中扮演了简化DOM操作和事件管理的核心角色,使开发者能够专注于业务逻辑的实现。

项目功能模块

一个完整的交互式Todo列表通常包含以下核心功能模块。

功能模块描述涉及的核心jQuery方法
任务展示将任务列表动态渲染到页面上$(container).html()$.each()
添加任务通过输入框和按钮向列表中添加新任务$(input).val()$(container).append()
删除任务删除单个任务,可配合动画效果$(element).remove().fadeOut()
编辑任务双击任务文本进入编辑模式,回车或失焦保存$(element).on('dblclick').replaceWith()
状态切换通过复选框标记任务为已完成或未完成,并更新样式$(checkbox).on('change').toggleClass()
本地存储使用 localStorage 保存任务数据,页面刷新后自动加载JSON.stringify()JSON.parse()
批量操作一键标记所有任务为已完成,或清除所有已完成任务$.each()$(selector).prop().remove()

项目初始化

构建Todo应用的第一步是搭建基础的HTML结构,并引入jQuery库。

示例:HTML结构与引入jQuery

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>jQuery 交互式 Todo 列表</title>
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <style>
        body { font-family: Arial, sans-serif; max-width: 500px; margin: 20px auto; }
        #todo-app { border: 1px solid #ddd; padding: 20px; border-radius: 5px; }
        #new-task { width: 70%; padding: 8px; margin-right: 5px; }
        #add-btn { padding: 8px 15px; cursor: pointer; }
        #task-list { list-style: none; padding: 0; margin-top: 20px; }
        #task-list li { padding: 10px; border-bottom: 1px solid #eee; display: flex; align-items: center; }
        #task-list li.completed span { text-decoration: line-through; color: #888; }
        .task-checkbox { margin-right: 10px; }
        .task-text { flex-grow: 1; cursor: pointer; }
        .delete-btn { background: red; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px; }
        .edit-input { flex-grow: 1; padding: 5px; margin-right: 5px; }
        .save-btn { background: green; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px; }
    </style>
</head>
<body>
    <div id="todo-app">
        <h2>我的任务清单</h2>
        <div>
            <input type="text" id="new-task" placeholder="添加新任务...">
            <button id="add-btn">添加</button>
            <button id="mark-all-btn">全部完成</button>
            <button id="clear-completed-btn">清除已完成</button>
        </div>
        <ul id="task-list">
            <!-- 任务列表将由 jQuery 动态生成 -->
        </ul>
    </div>

    <script>
        // jQuery 代码将在此处编写
    </script>
</body>
</html>

任务数据管理

使用JavaScript数组存储任务对象,每个对象包含 idtextcompleted 属性。id 用于唯一标识每个任务,便于后续的删除和更新操作。

语法

let tasks = [
    { id: 1, text: '学习 jQuery', completed: false },
    { id: 2, text: '编写 Todo 应用', completed: true }
];

渲染任务列表

封装一个 renderTasks 函数,根据 tasks 数组动态生成HTML并插入到 #task-list 中。每次数据变化(添加、删除、切换状态)后调用此函数刷新列表。

示例

<script>
    let tasks = [
        { id: 1, text: '学习 jQuery 选择器', completed: false },
        { id: 2, text: '练习事件处理', completed: true },
        { id: 3, text: '实现本地存储', completed: false }
    ];

    function renderTasks() {
        const $taskList = $('#task-list');
        $taskList.empty(); // 清空列表

        $.each(tasks, function(index, task) {
            const listItem = $('<li>').attr('data-id', task.id);
            if (task.completed) {
                listItem.addClass('completed');
            }

            const checkbox = $('<input>', {
                type: 'checkbox',
                class: 'task-checkbox',
                prop: { checked: task.completed }
            });

            const taskText = $('<span>', {
                class: 'task-text',
                text: task.text
            });

            const deleteBtn = $('<button>', {
                class: 'delete-btn',
                text: '删除'
            });

            listItem.append(checkbox, taskText, deleteBtn);
            $taskList.append(listItem);
        });
    }

    renderTasks(); // 初始渲染
</script>

添加新任务

监听“添加”按钮的点击事件,获取输入框的值,创建新任务对象并添加到 tasks 数组,然后重新渲染列表并清空输入框。

示例

<script>
    // ... 前面的代码保持不变

    $('#add-btn').on('click', function() {
        const newTaskText = $('#new-task').val().trim();
        if (newTaskText === '') {
            alert('任务内容不能为空!');
            return;
        }

        const newTask = {
            id: Date.now(), // 使用时间戳作为临时ID
            text: newTaskText,
            completed: false
        };
        tasks.push(newTask);
        renderTasks();
        $('#new-task').val('').focus(); // 清空输入并获取焦点
    });

    // 支持回车键添加
    $('#new-task').on('keypress', function(e) {
        if (e.which === 13) { // 回车键
            $('#add-btn').trigger('click');
        }
    });
</script>

删除任务

由于任务列表是动态生成的,需要使用事件委托将删除事件绑定到其父元素 #task-list 上。点击删除按钮时,获取对应 lidata-id 属性,从 tasks 数组中移除该任务,并重新渲染列表。可配合淡出动画提升体验。

示例

<script>
    // ... 前面的代码保持不变

    $('#task-list').on('click', '.delete-btn', function() {
        const $listItem = $(this).closest('li');
        const taskId = $listItem.data('id');

        // 添加淡出动画后移除
        $listItem.fadeOut(300, function() {
            tasks = tasks.filter(function(task) {
                return task.id !== taskId;
            });
            renderTasks(); // 重新渲染以恢复显示(但数据已删除)
        });
    });
</script>

切换任务状态

通过事件委托监听复选框的 change 事件,更新对应任务的 completed 属性,并切换父 licompleted 类以更新样式。

示例

<script>
    // ... 前面的代码保持不变

    $('#task-list').on('change', '.task-checkbox', function() {
        const $listItem = $(this).closest('li');
        const taskId = $listItem.data('id');
        const isChecked = $(this).prop('checked');

        tasks = tasks.map(function(task) {
            if (task.id === taskId) {
                task.completed = isChecked;
            }
            return task;
        });

        $listItem.toggleClass('completed', isChecked);
        // 注意:这里直接修改了样式,没有重新渲染全部,以提高性能
        // 但为了保持数据一致,也可以调用 renderTasks()
    });
</script>

编辑任务

双击任务文本时,将其替换为一个输入框和保存按钮,允许用户修改任务内容。保存时更新数据并重新渲染该项。

示例

<script>
    // ... 前面的代码保持不变

    $('#task-list').on('dblclick', '.task-text', function() {
        const $listItem = $(this).closest('li');
        const taskId = $listItem.data('id');
        const currentText = $(this).text();

        // 找到对应的任务对象
        const task = tasks.find(t => t.id === taskId);
        if (!task) return;

        // 创建编辑界面
        const $editInput = $('<input>', {
            type: 'text',
            class: 'edit-input',
            value: currentText
        });
        const $saveBtn = $('<button>', {
            class: 'save-btn',
            text: '保存'
        });

        // 临时替换内容
        $listItem.find('.task-text').replaceWith($editInput);
        $listItem.find('.delete-btn').before($saveBtn); // 在删除按钮前插入保存按钮
        $editInput.focus();
    });

    // 保存编辑(通过事件委托监听保存按钮)
    $('#task-list').on('click', '.save-btn', function() {
        const $listItem = $(this).closest('li');
        const taskId = $listItem.data('id');
        const $editInput = $listItem.find('.edit-input');
        const newText = $editInput.val().trim();

        if (newText === '') {
            alert('任务内容不能为空!');
            return;
        }

        tasks = tasks.map(function(task) {
            if (task.id === taskId) {
                task.text = newText;
            }
            return task;
        });

        renderTasks(); // 重新渲染整个列表以恢复显示
    });

    // 保存编辑(通过回车键)
    $('#task-list').on('keypress', '.edit-input', function(e) {
        if (e.which === 13) {
            $(this).siblings('.save-btn').trigger('click');
        }
    });

    // 失焦保存(可选,但注意与保存按钮的冲突处理)
    // $('#task-list').on('blur', '.edit-input', function() {
    //     $(this).siblings('.save-btn').trigger('click');
    // });
</script>

本地存储

利用 localStorage 实现数据的持久化。在添加、删除、编辑、切换状态后,将 tasks 数组保存到 localStorage。页面加载时,从 localStorage 读取数据并初始化。

示例

<script>
    // 封装存储函数
    function saveTasksToLocalStorage() {
        localStorage.setItem('todoTasks', JSON.stringify(tasks));
    }

    // 封装加载函数
    function loadTasksFromLocalStorage() {
        const storedTasks = localStorage.getItem('todoTasks');
        if (storedTasks) {
            tasks = JSON.parse(storedTasks);
        } else {
            tasks = []; // 默认空数组
        }
        renderTasks();
    }

    // 修改所有涉及 tasks 变化的地方,在变化后调用 saveTasksToLocalStorage()
    // 例如在添加任务后:
    $('#add-btn').on('click', function() {
        // ... 添加逻辑 ...
        saveTasksToLocalStorage();
    });

    // 在删除任务后:
    $('#task-list').on('click', '.delete-btn', function() {
        $listItem.fadeOut(300, function() {
            // ... 删除逻辑 ...
            saveTasksToLocalStorage();
            renderTasks(); // 重新渲染
        });
    });

    // 在切换状态后:
    $('#task-list').on('change', '.task-checkbox', function() {
        // ... 更新逻辑 ...
        saveTasksToLocalStorage();
    });

    // 在编辑保存后:
    $('#task-list').on('click', '.save-btn', function() {
        // ... 编辑逻辑 ...
        saveTasksToLocalStorage();
        renderTasks();
    });

    // 初始化时加载
    loadTasksFromLocalStorage();
</script>

批量操作

实现“全部完成”和“清除已完成”两个批量操作功能。

全部完成

将所有任务的 completed 属性设置为 true,更新本地存储并重新渲染。

清除已完成

tasks 数组中过滤掉所有 completedtrue 的任务,更新本地存储并重新渲染。

示例

<script>
    $('#mark-all-btn').on('click', function() {
        tasks = tasks.map(function(task) {
            task.completed = true;
            return task;
        });
        saveTasksToLocalStorage();
        renderTasks();
    });

    $('#clear-completed-btn').on('click', function() {
        tasks = tasks.filter(function(task) {
            return !task.completed;
        });
        saveTasksToLocalStorage();
        renderTasks();
    });
</script>

完整项目代码

将上述所有功能整合,形成一个完整的交互式Todo列表应用。以下为整合后的完整HTML文件。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>jQuery 交互式 Todo 列表 - 完整版</title>
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <style>
        body { font-family: Arial, sans-serif; max-width: 500px; margin: 20px auto; background: #f5f5f5; }
        #todo-app { background: white; border: 1px solid #ddd; padding: 20px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
        h2 { margin-top: 0; color: #333; }
        .input-area { display: flex; gap: 5px; margin-bottom: 15px; flex-wrap: wrap; }
        #new-task { flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 3px; min-width: 200px; }
        .btn { padding: 8px 15px; border: none; border-radius: 3px; cursor: pointer; background: #007bff; color: white; }
        .btn:hover { opacity: 0.9; }
        #mark-all-btn { background: #28a745; }
        #clear-completed-btn { background: #dc3545; }
        #task-list { list-style: none; padding: 0; margin: 0; }
        #task-list li { padding: 10px; border-bottom: 1px solid #eee; display: flex; align-items: center; animation: fadeIn 0.3s; }
        #task-list li:last-child { border-bottom: none; }
        #task-list li.completed .task-text { text-decoration: line-through; color: #888; }
        .task-checkbox { margin-right: 10px; cursor: pointer; }
        .task-text { flex-grow: 1; cursor: pointer; padding: 5px; }
        .delete-btn { background: #dc3545; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px; margin-left: 5px; }
        .edit-input { flex-grow: 1; padding: 5px; margin-right: 5px; border: 1px solid #ccc; border-radius: 3px; }
        .save-btn { background: #28a745; color: white; border: none; padding: 5px 10px; cursor: pointer; border-radius: 3px; }
        @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
    </style>
</head>
<body>
    <div id="todo-app">
        <h2>📋 我的任务清单</h2>
        <div class="input-area">
            <input type="text" id="new-task" placeholder="添加新任务...">
            <button id="add-btn" class="btn">添加</button>
            <button id="mark-all-btn" class="btn">全部完成</button>
            <button id="clear-completed-btn" class="btn">清除已完成</button>
        </div>
        <ul id="task-list"></ul>
    </div>

    <script>
        let tasks = [];

        function saveTasksToLocalStorage() {
            localStorage.setItem('todoTasks', JSON.stringify(tasks));
        }

        function loadTasksFromLocalStorage() {
            const storedTasks = localStorage.getItem('todoTasks');
            if (storedTasks) {
                tasks = JSON.parse(storedTasks);
            } else {
                tasks = [];
            }
            renderTasks();
        }

        function renderTasks() {
            const $taskList = $('#task-list');
            $taskList.empty();

            $.each(tasks, function(index, task) {
                const listItem = $('<li>').attr('data-id', task.id).addClass(task.completed ? 'completed' : '');
                const checkbox = $('<input>', { type: 'checkbox', class: 'task-checkbox', prop: { checked: task.completed } });
                const taskText = $('<span>', { class: 'task-text', text: task.text });
                const deleteBtn = $('<button>', { class: 'delete-btn', text: '删除' });
                listItem.append(checkbox, taskText, deleteBtn);
                $taskList.append(listItem);
            });
        }

        // 添加任务
        $('#add-btn').on('click', function() {
            const newTaskText = $('#new-task').val().trim();
            if (newTaskText === '') {
                alert('任务内容不能为空!');
                return;
            }
            const newTask = { id: Date.now(), text: newTaskText, completed: false };
            tasks.push(newTask);
            saveTasksToLocalStorage();
            renderTasks();
            $('#new-task').val('').focus();
        });

        // 回车添加
        $('#new-task').on('keypress', function(e) {
            if (e.which === 13) $('#add-btn').trigger('click');
        });

        // 删除任务(带动画)
        $('#task-list').on('click', '.delete-btn', function() {
            const $listItem = $(this).closest('li');
            const taskId = $listItem.data('id');
            $listItem.fadeOut(300, function() {
                tasks = tasks.filter(task => task.id !== taskId);
                saveTasksToLocalStorage();
                renderTasks(); // 重新渲染(但动画效果已过,此处为保持数据一致)
            });
        });

        // 切换任务状态
        $('#task-list').on('change', '.task-checkbox', function() {
            const $listItem = $(this).closest('li');
            const taskId = $listItem.data('id');
            const isChecked = $(this).prop('checked');
            tasks = tasks.map(task => {
                if (task.id === taskId) task.completed = isChecked;
                return task;
            });
            $listItem.toggleClass('completed', isChecked);
            saveTasksToLocalStorage();
        });

        // 双击编辑
        $('#task-list').on('dblclick', '.task-text', function() {
            const $listItem = $(this).closest('li');
            const taskId = $listItem.data('id');
            const currentText = $(this).text();
            const task = tasks.find(t => t.id === taskId);
            if (!task) return;

            const $editInput = $('<input>', { type: 'text', class: 'edit-input', value: currentText });
            const $saveBtn = $('<button>', { class: 'save-btn', text: '保存' });
            $listItem.find('.task-text').replaceWith($editInput);
            $listItem.find('.delete-btn').before($saveBtn);
            $editInput.focus();
        });

        // 保存编辑(点击保存按钮)
        $('#task-list').on('click', '.save-btn', function() {
            const $listItem = $(this).closest('li');
            const taskId = $listItem.data('id');
            const $editInput = $listItem.find('.edit-input');
            const newText = $editInput.val().trim();
            if (newText === '') {
                alert('任务内容不能为空!');
                return;
            }
            tasks = tasks.map(task => {
                if (task.id === taskId) task.text = newText;
                return task;
            });
            saveTasksToLocalStorage();
            renderTasks();
        });

        // 保存编辑(回车键)
        $('#task-list').on('keypress', '.edit-input', function(e) {
            if (e.which === 13) $(this).siblings('.save-btn').trigger('click');
        });

        // 全部完成
        $('#mark-all-btn').on('click', function() {
            tasks = tasks.map(task => { task.completed = true; return task; });
            saveTasksToLocalStorage();
            renderTasks();
        });

        // 清除已完成
        $('#clear-completed-btn').on('click', function() {
            tasks = tasks.filter(task => !task.completed);
            saveTasksToLocalStorage();
            renderTasks();
        });

        // 初始化
        loadTasksFromLocalStorage();
    </script>
</body>
</html>

版本变更记录

版本对 Todo 应用开发的影响
1.x基础的事件绑定(如 click())和 DOM 操作方法,适用于简单应用。
1.7+引入 .on().off() 方法,事件委托更为统一和高效,是构建动态列表的核心。
1.9+移除了一些已弃用的方法(如 .live()),推荐使用 .on() 进行事件委托。
3.xPromise 和 Deferred 的完善,可用于处理更复杂的异步流程(如与本地存储结合的回调)。
4.0移除对 IE < 11 的支持,使得可以使用更现代的 CSS 和 JavaScript 特性;源码迁移至 ES 模块,便于在现代构建工具中使用。

浏览器兼容性

基于 jQuery 3.x 或 4.x 版本,交互式Todo列表应用的兼容性如下。jQuery 4.x 已移除对 IE 10 及以下版本的支持。

浏览器类型最低兼容版本(基于 jQuery 3.x/4.x)
PC 端
Chrome60+ (当前及先前主要版本)
Edge15+ (基于 Chromium)
Firefox55+ (当前及先前主要版本)
Opera47+ (当前及先前主要版本)
Safari10+ (当前及先前主要版本)
移动端
Chrome Android100+ (当前版本)
Firefox for Android100+ (当前版本)
Opera Android64+ (当前版本)
Safari on iOS10+
Samsung Internet6.2+
WebView Android100+ (当前版本)
WebView on iOS10+

通过构建这个交互式Todo列表应用,可以系统性地实践jQuery的核心功能。从数据管理到动态渲染,从事件委托到本地存储,每个环节都体现了jQuery在简化DOM操作和事件处理方面的优势。此项目不仅巩固了基础语法,也为开发更复杂的交互功能奠定了坚实的实践基础。