使用 HTML+JS 实现一个高颜值待办清单:支持搜索高亮、分类筛选、本地存储

前言

在这篇文章中,我将介绍如何使用纯HTML和JavaScript实现一个功能完整的待办事项清单。这个项目不仅界面美观,还包含了许多实用的功能特性。

功能特点

  • 🎨 清新的渐变背景和现代化UI设计
  •  📊 实时统计:总任务、已完成、待完成数量
  •  🔍 搜索功能:支持关键字高亮显示
  • 📑 分类管理:支持工作、个人、购物、学习等多种分类
  • ⭐ 优先级标记:高、中、低三级优先级,通过左边框颜色区分
  •  💾 本地存储:自动保存所有待办事项
  • 🔄 状态切换:支持待办事项完成状态切换
  • 🗑️ 删除功能:可随时删除不需要的待办事项

效果预览

核心功能实现

1. 搜索高亮功能

/**
 * 高亮显示搜索关键字
 * 使用正则表达式匹配关键字并添加高亮样式
 */
 function highlightText(text, searchText) {
            if (!searchText) return text;
            const regex = new RegExp(`(${searchText})`, 'gi');
            return text.replace(regex, '<span class="highlight">$1</span>');
  }

这个函数通过正则表达式匹配搜索关键字,并用带有高亮样式的span标签包裹匹配文本

2. 本地数据存储

/**
 * 保存数据到localStorage
 */
function saveTodos() {
    localStorage.setItem('todos', JSON.stringify(todos));
    updateStats();
}

/**
 * 初始化数据
 */
let todos = JSON.parse(localStorage.getItem('todos')) || [];

使用localStorage实现数据持久化,确保刷新页面后数据不会丢失

3. 统计功能实现

/**
 * 更新统计数据
 */
function updateStats() {
    const total = todos.length;
    const completed = todos.filter(todo => todo.completed).length;
    const pending = total - completed;

    document.getElementById('totalTasks').textContent = total;
    document.getElementById('completedTasks').textContent = completed;
    document.getElementById('pendingTasks').textContent = pending;
}

通过数组方法快速统计各类任务数量

4. 添加待办事项

/**
 * 添加新的待办事项
 */
  function addTodo() {
            hideAllErrors();
            
            const todoText = document.getElementById('todoInput').value.trim();
            const category = document.getElementById('todoCategory').value;
            const priority = document.getElementById('todoPriority').value;

            let hasError = false;

            if (todoText === '') {
                document.getElementById('textError').style.display = 'block';
                hasError = true;
            }
            if (category === '') {
                document.getElementById('categoryError').style.display = 'block';
                hasError = true;
            }
            if (priority === '') {
                document.getElementById('priorityError').style.display = 'block';
                hasError = true;
            }

            if (hasError) {
                return;
            }

            const todo = {
                id: Date.now(),
                text: todoText,
                completed: false,
                category: category,
                priority: priority,
                createdAt: formatDateTime(new Date())
            };

            todos.push(todo);
            saveTodos();
            renderTodos();
            
            // 清空所有输入
            document.getElementById('todoInput').value = '';
            document.getElementById('todoCategory').value = '';
            document.getElementById('todoPriority').value = '';
            hideAllErrors();
        }

    todos.push(todo);
    saveTodos();
    renderTodos();
    resetInputs();
}

包含输入验证、数据创建和保存的完整流程

5. 渲染待办列表

/**
 * 渲染待办事项列表
 */
function renderTodos() {
    const todoList = document.getElementById('todoList');
    const searchText = document.getElementById('searchInput').value.toLowerCase();
    const categoryFilter = document.getElementById('categoryFilter').value;
    const priorityFilter = document.getElementById('priorityFilter').value;

    // 过滤待办事项
    let filteredTodos = todos.filter(todo => {
        const matchesSearch = todo.text.toLowerCase().includes(searchText);
        const matchesCategory = categoryFilter === 'all' || todo.category === categoryFilter;
        const matchesPriority = priorityFilter === 'all' || todo.priority === priorityFilter;
        return matchesSearch && matchesCategory && matchesPriority;
    });

    // 渲染列表
    todoList.innerHTML = '';
    if (filteredTodos.length === 0) {
        todoList.innerHTML = '<div class="no-data">暂无数据</div>';
        return;
    }

    filteredTodos.forEach(todo => {
        // ... 创建列表项
    });
}

实现了搜索、筛选和渲染的综合功能

6. 日期格式化

/**
 * 格式化日期时间
 */
function formatDateTime(date) {
    const pad = (num) => String(num).padStart(2, '0');
    const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
    const weekDay = weekDays[date.getDay()];
    return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${weekDay} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}

自定义日期格式化,包含年月日、星期和时分秒。

样式设计要点

使用渐变背景创造现代感

background: linear-gradient(120deg, #84fab0, #8fd3f4);

 优先级标识使用左边框色彩区分

.priority-high { border-left: 4px solid #ff6b6b; }
.priority-medium { border-left: 4px solid #ffd93d; }
.priority-low { border-left: 4px solid #6ed89b; }

可扩展功能

  • 添加截止日期功能
  • 支持任务拖拽排序
  • 添加标签系统
  • 数据导出导入
  • 支持任务编辑
  • 添加提醒功能

总结

这个项目展示了如何使用纯HTML和JavaScript实现一个功能完整的待办清单。通过合理的代码组织和功能模块化,我们实现了一个既实用又美观的Web应用。项目中的很多实现方式,如搜索高亮、本地存储、状态管理等,都是前端开发中常用的技术点,值得学习和借鉴。

完整代码

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>增强版待办事项清单</title>
    <style>
        html, body {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100vh;
            background: linear-gradient(120deg, #84fab0, #8fd3f4);
            overflow: hidden; /* 禁止body滚动 */
        }

        .container {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 800px;
            height: 80vh; /* 固定高度 */
            background: white;
            border-radius: 15px;
            padding: 30px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
            box-sizing: border-box; /* 确保padding包含在高度内 */
            display: flex;
            flex-direction: column;
        }

        .todo-list {
            flex: 1;
            overflow-y: auto; /* 只在列表区域滚动 */
            margin: 0;
            padding: 0;
            list-style: none;
        }

        /* 美化滚动条 */
        .todo-list::-webkit-scrollbar {
            width: 6px;
        }

        .todo-list::-webkit-scrollbar-track {
            background: #f1f1f1;
            border-radius: 3px;
        }

        .todo-list::-webkit-scrollbar-thumb {
            background: #84fab0;
            border-radius: 3px;
        }

        /* 移除container的滚动 */
        .container {
            overflow: visible;
        }

        .page-title {
            text-align: center;
            color: #2c3e50;
            margin-bottom: 15px;
            font-size: 1.8em;
            padding: 5px 0;
            font-weight: bold;
        }

        .stats {
            display: flex;
            justify-content: space-around;
            margin: 10px 0 15px 0;
            padding: 12px;
            background: #f8f9fa;
            border-radius: 12px;
        }

        .stat-item {
            text-align: center;
        }

        .stat-number {
            font-size: 24px;
            font-weight: bold;
            color: #2c3e50;
        }

        .todo-item {
            display: flex;
            align-items: center;
            padding: 12px 20px;
            background: #f8f9fa;
            margin-bottom: 10px;
            border-radius: 12px;
            animation: slideIn 0.3s ease;
            gap: 15px;
            width: calc(100% - 40px);
            box-sizing: border-box;
        }

        .todo-item:hover {
            transform: translateX(5px);
            transition: transform 0.3s ease;
        }

        .todo-content {
            flex: 1;
            display: flex;
            flex-direction: column;
            gap: 8px;
        }

        .todo-text {
            font-size: 1em;
            color: #2c3e50;
        }

        .todo-details {
            display: flex;
            gap: 15px;
            color: #666;
            font-size: 0.85em;
        }

        .category-tag {
            padding: 4px 12px;
            border-radius: 15px;
            background: #e0e0e0;
            font-size: 0.9em;
        }

        .input-section {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 12px;
            margin-bottom: 15px;
            display: flex;
            gap: 15px;
            position: relative;
        }

        .input-section input[type="text"],
        .input-section select,
        .input-section button {
            height: 45px;
            padding: 0 15px;
            border-radius: 8px;
            border: 1px solid #e0e0e0;
        }

        .input-section button {
            background: #84fab0;
            border: none;
            color: #2c3e50;
            cursor: pointer;
        }

        .search-box {
            width: 100%;
            padding: 12px;
            margin: 10px 0;
            border-radius: 8px;
            border: 1px solid #e0e0e0;
        }

        .delete-btn {
            background: #ff6b6b;
            padding: 8px 16px;
            border: none;
            border-radius: 6px;
            color: white;
            cursor: pointer;
        }

        .filters {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
        }

        .priority-high {
            border-left: 4px solid #ff6b6b;
        }

        .priority-medium {
            border-left: 4px solid #ffd93d;
        }

        .priority-low {
            border-left: 4px solid #6ed89b;
        }

        .todo-item.completed {
            background: #e8f5e9;
            text-decoration: line-through;
            color: #666;
        }

        .todo-item input[type="checkbox"] {
            margin-right: 15px;
            width: 20px;
            height: 20px;
            cursor: pointer;
        }

        .no-data {
            text-align: center;
            padding: 20px;
            color: #666;
        }

        .error-text {
            color: #ff6b6b;
            font-size: 14px;
            margin-top: 5px;
            display: none;
        }

        .btn {
            height: 45px;
            padding: 0 15px;
            border-radius: 8px;
            border: none;
            cursor: pointer;
        }

        .btn-add {
            background: #84fab0;
            color: #2c3e50;
        }

        .btn-reset {
            background: #e0e0e0;
            color: #666;
        }

        /* 添加高亮样式 */
        .highlight {
            background-color: red;
            padding: 0 2px;
            color:#fff;
            border-radius: 2px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="page-title">增强版待办事项清单</div>
        
        <div class="stats">
            <div class="stat-item">
                <div class="stat-number" id="totalTasks">0</div>
                <div>总任务</div>
            </div>
            <div class="stat-item">
                <div class="stat-number" id="completedTasks">0</div>
                <div>已完成</div>
            </div>
            <div class="stat-item">
                <div class="stat-number" id="pendingTasks">0</div>
                <div>待完成</div>
            </div>
        </div>

        <input type="text" class="search-box" id="searchInput" placeholder="搜索待办事项..." autocomplete="off">

        <div class="filters">
            <select id="categoryFilter">
                <option value="all">所有分类</option>
                <option value="work">工作</option>
                <option value="personal">个人</option>
                <option value="shopping">购物</option>
                <option value="study">学习</option>
            </select>
            <select id="priorityFilter">
                <option value="all">所有优先级</option>
                <option value="high">高</option>
                <option value="medium">中</option>
                <option value="low">低</option>
            </select>
        </div>

        <div class="input-section">
            <div>
                <input type="text" id="todoInput" placeholder="添加新的待办事项..." autocomplete="off">
                <div id="textError" class="error-text">请填写待办事项内容</div>
            </div>
            <div>
                <select id="todoCategory">
                    <option value="">- 请选择 -</option>
                    <option value="personal">个人</option>
                    <option value="work">工作</option>
                    <option value="shopping">购物</option>
                    <option value="study">学习</option>
                </select>
                <div id="categoryError" class="error-text">请选择分类</div>
            </div>
            <div>
                <select id="todoPriority">
                    <option value="">- 请选择 -</option>
                    <option value="medium">中优先级</option>
                    <option value="high">高优先级</option>
                    <option value="low">低优先级</option>
                </select>
                <div id="priorityError" class="error-text">请选择优先级</div>
            </div>
            <button class="btn btn-add" onclick="addTodo()">添加</button>
            <button class="btn btn-reset" onclick="resetInputs()">重置</button>
        </div>

        <ul id="todoList" class="todo-list"></ul>
    </div>

    <script>
        let todos = JSON.parse(localStorage.getItem('todos')) || [];

        function updateStats() {
            const total = todos.length;
            const completed = todos.filter(todo => todo.completed).length;
            const pending = total - completed;

            document.getElementById('totalTasks').textContent = total;
            document.getElementById('completedTasks').textContent = completed;
            document.getElementById('pendingTasks').textContent = pending;
        }

        function saveTodos() {
            localStorage.setItem('todos', JSON.stringify(todos));
            updateStats();
        }

        function formatDateTime(date) {
            const pad = (num) => String(num).padStart(2, '0');
            const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
            const weekDay = weekDays[date.getDay()];
            return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${weekDay} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
        }

        function hideAllErrors() {
            document.getElementById('textError').style.display = 'none';
            document.getElementById('categoryError').style.display = 'none';
            document.getElementById('priorityError').style.display = 'none';
        }

        function addTodo() {
            hideAllErrors();
            
            const todoText = document.getElementById('todoInput').value.trim();
            const category = document.getElementById('todoCategory').value;
            const priority = document.getElementById('todoPriority').value;

            let hasError = false;

            if (todoText === '') {
                document.getElementById('textError').style.display = 'block';
                hasError = true;
            }
            if (category === '') {
                document.getElementById('categoryError').style.display = 'block';
                hasError = true;
            }
            if (priority === '') {
                document.getElementById('priorityError').style.display = 'block';
                hasError = true;
            }

            if (hasError) {
                return;
            }

            const todo = {
                id: Date.now(),
                text: todoText,
                completed: false,
                category: category,
                priority: priority,
                createdAt: formatDateTime(new Date())
            };

            todos.push(todo);
            saveTodos();
            renderTodos();
            
            // 清空所有输入
            document.getElementById('todoInput').value = '';
            document.getElementById('todoCategory').value = '';
            document.getElementById('todoPriority').value = '';
            hideAllErrors();
        }

        function deleteTodo(id) {
            todos = todos.filter(todo => todo.id !== id);
            saveTodos();
            renderTodos();
        }

        function toggleComplete(id) {
            const todo = todos.find(todo => todo.id === id);
            if (todo) {
                todo.completed = !todo.completed;
                saveTodos();
                renderTodos();
            }
        }

        function highlightText(text, searchText) {
            if (!searchText) return text;
            const regex = new RegExp(`(${searchText})`, 'gi');
            return text.replace(regex, '<span class="highlight">$1</span>');
        }

        function renderTodos() {
            const todoList = document.getElementById('todoList');
            const searchText = document.getElementById('searchInput').value.toLowerCase();
            const categoryFilter = document.getElementById('categoryFilter').value;
            const priorityFilter = document.getElementById('priorityFilter').value;

            let filteredTodos = todos.filter(todo => {
                const matchesSearch = todo.text.toLowerCase().includes(searchText);
                const matchesCategory = categoryFilter === 'all' || todo.category === categoryFilter;
                const matchesPriority = priorityFilter === 'all' || todo.priority === priorityFilter;
                return matchesSearch && matchesCategory && matchesPriority;
            });

            todoList.innerHTML = '';

            if (filteredTodos.length === 0) {
                todoList.innerHTML = '<div class="no-data">暂无数据</div>';
                return;
            }

            filteredTodos.forEach(todo => {
                const li = document.createElement('li');
                li.className = `todo-item priority-${todo.priority} ${todo.completed ? 'completed' : ''}`;
                
                // 使用highlightText函数处理待办事项文本
                const highlightedText = highlightText(todo.text, searchText);
                
                li.innerHTML = `
                    <input type="checkbox" ${todo.completed ? 'checked' : ''} onchange="toggleComplete(${todo.id})">
                    <div class="todo-content">
                        <div class="todo-text">${highlightedText}</div>
                        <div class="todo-details">
                            <span class="category-tag">${todo.category}</span>
                            <span>${todo.createdAt}</span>
                        </div>
                    </div>
                    <button class="delete-btn" onclick="deleteTodo(${todo.id})">删除</button>
                `;

                todoList.appendChild(li);
            });
        }

        // 初始化
        document.getElementById('searchInput').addEventListener('input', renderTodos);
        document.getElementById('categoryFilter').addEventListener('change', renderTodos);
        document.getElementById('priorityFilter').addEventListener('change', renderTodos);
        document.getElementById('todoInput').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') addTodo();
        });

        // 添加输入事件监听器,在用户输入时隐藏错误提示
        document.getElementById('todoInput').addEventListener('input', () => {
            document.getElementById('textError').style.display = 'none';
        });
        document.getElementById('todoCategory').addEventListener('change', () => {
            document.getElementById('categoryError').style.display = 'none';
        });
        document.getElementById('todoPriority').addEventListener('change', () => {
            document.getElementById('priorityError').style.display = 'none';
        });

        // 首次加载
        renderTodos();
        updateStats();

        function resetInputs() {
            document.getElementById('todoInput').value = '';
            document.getElementById('todoCategory').value = '';
            document.getElementById('todoPriority').value = '';
            hideAllErrors();
        }
    </script>
</body>
</html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端切图仔001

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值