pin_drop当前位置:知识文库 ❯ 图文

实战项目:项目09:任务管理应用 - 详细教程与实战指南

一、项目简介

任务管理应用是提升个人和团队效率的重要工具,也是前端CRUD(创建、读取、更新、删除)操作的典型应用场景。一个优秀的任务管理应用需要提供直观的任务创建和编辑体验、灵活的筛选和分类机制、以及可靠的数据持久化方案。

本项目将构建一个功能完善的任务管理应用,包含任务列表添加/编辑/删除任务任务状态切换优先级标记分类筛选本地存储拖拽排序等核心功能。项目实现了完整的CRUD操作和localStorage持久化,确保数据在刷新页面后不会丢失。


二、需求分析

功能需求

  • 任务列表:展示所有任务,显示标题、描述、优先级、分类、状态、创建时间

  • 添加任务:弹窗表单,输入标题、描述、优先级、分类

  • 编辑任务:点击编辑按钮修改任务信息

  • 删除任务:点击删除按钮移除任务,带确认提示

  • 状态切换:点击复选框切换完成/未完成状态

  • 优先级标记:高/中/低三级优先级,颜色区分

  • 分类筛选:按分类和状态筛选任务

  • 本地存储:任务数据自动保存到localStorage

  • 拖拽排序:拖拽任务调整顺序

交互需求

  • 添加/编辑任务使用模态框表单

  • 任务完成时有划线效果

  • 删除任务有确认步骤

  • 筛选切换时列表平滑更新

  • 拖拽时有视觉反馈


三、技术选型

技术领域 选型方案 选择理由
数据持久化 localStorage 浏览器原生API,无需后端
模态框 自定义实现 完全控制样式和交互
拖拽排序 HTML5 Drag & Drop 原生API,无需外部库
状态管理 内存对象 + localStorage 简单可靠的数据管理
筛选逻辑 前端过滤 数据量小,前端过滤即可
日期格式化 自定义函数 轻量无依赖

四、页面结构设计

代码示例

<body>
├── 应用头部
│   ├── 标题和统计
│   └── 添加任务按钮
├── 筛选栏
│   ├── 状态筛选(全部/待办/已完成)
│   └── 分类筛选
├── 任务列表
│   └── 任务卡片(可拖拽)
│       ├── 复选框(状态切换)
│       ├── 任务信息
│       ├── 优先级标签
│       └── 操作按钮(编辑/删除)
├── 模态框(添加/编辑任务)
│   └── 表单
└── 删除确认对话框

五、完整代码实现

以下是任务管理应用的完整HTML代码实现,包含CSS样式和JavaScript交互逻辑:

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>任务管理</title>
    <style>
        :root {
            --color-primary: #6366f1;
            --color-primary-light: #818cf8;
            --color-primary-dark: #4f46e5;
            --color-success: #22c55e;
            --color-warning: #f59e0b;
            --color-danger: #ef4444;
            --color-bg: #f8fafc;
            --color-bg-white: #ffffff;
            --color-text: #1e293b;
            --color-text-secondary: #64748b;
            --color-text-light: #94a3b8;
            --color-border: #e2e8f0;
            --color-shadow: rgba(0, 0, 0, 0.06);
            --radius: 12px;
            --radius-sm: 8px;
            --transition: 0.3s ease;
        }

        *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans SC", sans-serif;
            background: var(--color-bg);
            color: var(--color-text);
            line-height: 1.6;
            min-height: 100vh;
        }
        a { text-decoration: none; }
        button { cursor: pointer; font-family: inherit; }

        /* ========== 应用头部 ========== */
        .app-header {
            background: var(--color-bg-white);
            border-bottom: 1px solid var(--color-border);
            padding: 24px;
        }

        .header-inner {
            max-width: 800px;
            margin: 0 auto;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .app-title {
            font-size: 1.5rem;
            font-weight: 800;
        }

        .app-stats {
            font-size: 0.85rem;
            color: var(--color-text-secondary);
            margin-top: 2px;
        }

        .btn-add {
            padding: 10px 24px;
            background: var(--color-primary);
            color: white;
            border: none;
            border-radius: var(--radius-sm);
            font-size: 0.92rem;
            font-weight: 600;
            display: flex;
            align-items: center;
            gap: 6px;
            transition: all var(--transition);
        }

        .btn-add:hover { background: var(--color-primary-dark); transform: translateY(-1px); }

        /* ========== 筛选栏 ========== */
        .filter-bar {
            max-width: 800px;
            margin: 20px auto 0;
            padding: 0 24px;
            display: flex;
            gap: 12px;
            flex-wrap: wrap;
            align-items: center;
        }

        .filter-group {
            display: flex;
            gap: 4px;
            background: var(--color-bg-white);
            border-radius: var(--radius-sm);
            padding: 4px;
            border: 1px solid var(--color-border);
        }

        .filter-btn {
            padding: 6px 14px;
            border: none;
            border-radius: 6px;
            font-size: 0.82rem;
            font-weight: 500;
            background: transparent;
            color: var(--color-text-secondary);
            transition: all var(--transition);
        }

        .filter-btn:hover { color: var(--color-text); }

        .filter-btn.active {
            background: var(--color-primary);
            color: white;
        }

        .category-select {
            padding: 6px 14px;
            border: 1px solid var(--color-border);
            border-radius: 6px;
            font-size: 0.82rem;
            background: var(--color-bg-white);
            color: var(--color-text);
            font-family: inherit;
        }

        .category-select:focus { outline: none; border-color: var(--color-primary); }

        /* ========== 任务列表 ========== */
        .task-list-container {
            max-width: 800px;
            margin: 16px auto 0;
            padding: 0 24px 40px;
        }

        .task-list {
            display: flex;
            flex-direction: column;
            gap: 8px;
        }

        .task-item {
            background: var(--color-bg-white);
            border-radius: var(--radius);
            padding: 16px 20px;
            display: flex;
            align-items: flex-start;
            gap: 14px;
            border: 1px solid var(--color-border);
            transition: all var(--transition);
            cursor: grab;
        }

        .task-item:hover {
            box-shadow: 0 4px 12px var(--color-shadow);
        }

        .task-item.dragging {
            opacity: 0.5;
            transform: rotate(2deg);
        }

        .task-item.drag-over {
            border-color: var(--color-primary);
            background: rgba(99, 102, 241, 0.03);
        }

        /* 自定义复选框 */
        .task-checkbox {
            position: relative;
            width: 22px;
            height: 22px;
            flex-shrink: 0;
            margin-top: 2px;
        }

        .task-checkbox input {
            position: absolute;
            opacity: 0;
            width: 100%;
            height: 100%;
            cursor: pointer;
            z-index: 1;
        }

        .checkmark {
            width: 22px;
            height: 22px;
            border: 2px solid var(--color-border);
            border-radius: 6px;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all var(--transition);
            font-size: 12px;
            color: white;
        }

        .task-checkbox input:checked + .checkmark {
            background: var(--color-success);
            border-color: var(--color-success);
        }

        .task-checkbox input:focus-visible + .checkmark {
            box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.2);
        }

        .task-content {
            flex: 1;
            min-width: 0;
        }

        .task-title-row {
            display: flex;
            align-items: center;
            gap: 8px;
            margin-bottom: 4px;
        }

        .task-title {
            font-size: 0.95rem;
            font-weight: 600;
            transition: all var(--transition);
        }

        .task-item.completed .task-title {
            text-decoration: line-through;
            color: var(--color-text-light);
        }

        .priority-badge {
            padding: 1px 8px;
            border-radius: 10px;
            font-size: 0.68rem;
            font-weight: 600;
            text-transform: uppercase;
        }

        .priority-high { background: #fef2f2; color: var(--color-danger); }
        .priority-medium { background: #fffbeb; color: var(--color-warning); }
        .priority-low { background: #f0fdf4; color: var(--color-success); }

        .task-desc {
            font-size: 0.82rem;
            color: var(--color-text-secondary);
            margin-bottom: 6px;
        }

        .task-item.completed .task-desc {
            color: var(--color-text-light);
        }

        .task-meta {
            display: flex;
            gap: 12px;
            font-size: 0.72rem;
            color: var(--color-text-light);
        }

        .task-meta span {
            display: flex;
            align-items: center;
            gap: 4px;
        }

        .task-actions {
            display: flex;
            gap: 4px;
            flex-shrink: 0;
        }

        .action-btn {
            width: 32px;
            height: 32px;
            border: none;
            border-radius: 6px;
            background: transparent;
            color: var(--color-text-light);
            font-size: 14px;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all var(--transition);
        }

        .action-btn:hover { background: var(--color-bg); }
        .action-btn.edit:hover { color: var(--color-primary); }
        .action-btn.delete:hover { color: var(--color-danger); }

        /* 空状态 */
        .empty-state {
            text-align: center;
            padding: 60px 20px;
            color: var(--color-text-light);
        }

        .empty-state .empty-icon { font-size: 3rem; margin-bottom: 12px; }
        .empty-state h3 { font-size: 1.1rem; color: var(--color-text-secondary); margin-bottom: 4px; }

        /* ========== 模态框 ========== */
        .modal-overlay {
            position: fixed;
            inset: 0;
            background: rgba(0, 0, 0, 0.5);
            z-index: 1000;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 24px;
            opacity: 0;
            visibility: hidden;
            transition: all var(--transition);
        }

        .modal-overlay.open {
            opacity: 1;
            visibility: visible;
        }

        .modal {
            background: var(--color-bg-white);
            border-radius: var(--radius);
            width: 100%;
            max-width: 500px;
            padding: 28px;
            transform: translateY(20px);
            transition: transform var(--transition);
        }

        .modal-overlay.open .modal {
            transform: translateY(0);
        }

        .modal h2 {
            font-size: 1.2rem;
            font-weight: 700;
            margin-bottom: 20px;
        }

        .form-group {
            margin-bottom: 16px;
        }

        .form-group label {
            display: block;
            font-size: 0.85rem;
            font-weight: 600;
            margin-bottom: 6px;
        }

        .form-group label .required { color: var(--color-danger); }

        .form-input, .form-textarea, .form-select {
            width: 100%;
            padding: 10px 14px;
            border: 2px solid var(--color-border);
            border-radius: var(--radius-sm);
            font-size: 0.9rem;
            font-family: inherit;
            transition: all var(--transition);
            background: var(--color-bg-white);
            color: var(--color-text);
        }

        .form-input:focus, .form-textarea:focus, .form-select:focus {
            outline: none;
            border-color: var(--color-primary);
        }

        .form-textarea { resize: vertical; min-height: 80px; }

        .form-row {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 12px;
        }

        .modal-actions {
            display: flex;
            justify-content: flex-end;
            gap: 10px;
            margin-top: 24px;
        }

        .btn-cancel {
            padding: 10px 20px;
            background: var(--color-bg);
            border: none;
            border-radius: var(--radius-sm);
            font-size: 0.9rem;
            color: var(--color-text-secondary);
        }

        .btn-cancel:hover { background: var(--color-border); }

        .btn-save {
            padding: 10px 24px;
            background: var(--color-primary);
            color: white;
            border: none;
            border-radius: var(--radius-sm);
            font-size: 0.9rem;
            font-weight: 600;
        }

        .btn-save:hover { background: var(--color-primary-dark); }

        /* ========== 删除确认 ========== */
        .confirm-overlay {
            position: fixed;
            inset: 0;
            background: rgba(0, 0, 0, 0.5);
            z-index: 1100;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 24px;
            opacity: 0;
            visibility: hidden;
            transition: all var(--transition);
        }

        .confirm-overlay.open { opacity: 1; visibility: visible; }

        .confirm-dialog {
            background: var(--color-bg-white);
            border-radius: var(--radius);
            padding: 28px;
            max-width: 400px;
            text-align: center;
        }

        .confirm-dialog .confirm-icon {
            font-size: 2.5rem;
            margin-bottom: 12px;
        }

        .confirm-dialog h3 { font-size: 1.1rem; margin-bottom: 8px; }
        .confirm-dialog p { font-size: 0.88rem; color: var(--color-text-secondary); margin-bottom: 20px; }

        .confirm-actions {
            display: flex;
            gap: 10px;
            justify-content: center;
        }

        .btn-confirm-delete {
            padding: 10px 24px;
            background: var(--color-danger);
            color: white;
            border: none;
            border-radius: var(--radius-sm);
            font-size: 0.9rem;
            font-weight: 600;
        }

        .btn-confirm-delete:hover { background: #dc2626; }

        /* ========== 响应式 ========== */
        @media (max-width: 600px) {
            .header-inner { flex-direction: column; gap: 12px; text-align: center; }
            .filter-bar { flex-direction: column; }
            .form-row { grid-template-columns: 1fr; }
            .task-item { padding: 14px 16px; }
        }
    </style>
</head>
<body>
    <!-- 应用头部 -->
    <header class="app-header">
        <div class="header-inner">
            <div>
                <div class="app-title">任务管理</div>
                <div class="app-stats" id="appStats">0 个任务</div>
            </div>
            <button class="btn-add" id="addTaskBtn">+ 添加任务</button>
        </div>
    </header>

    <!-- 筛选栏 -->
    <div class="filter-bar">
        <div class="filter-group" id="statusFilter">
            <button class="filter-btn active" data-status="all">全部</button>
            <button class="filter-btn" data-status="pending">待办</button>
            <button class="filter-btn" data-status="completed">已完成</button>
        </div>
        <select class="category-select" id="categoryFilter" aria-label="分类筛选">
            <option value="all">全部分类</option>
            <option value="工作">工作</option>
            <option value="学习">学习</option>
            <option value="生活">生活</option>
            <option value="健康">健康</option>
        </select>
    </div>

    <!-- 任务列表 -->
    <div class="task-list-container">
        <div class="task-list" id="taskList"></div>
        <div class="empty-state" id="emptyState" style="display:none;">
            <div class="empty-icon">&#128203;</div>
            <h3>暂无任务</h3>
            <p>点击"添加任务"开始规划你的待办事项</p>
        </div>
    </div>

    <!-- 添加/编辑模态框 -->
    <div class="modal-overlay" id="taskModal">
        <div class="modal">
            <h2 id="modalTitle">添加任务</h2>
            <div class="form-group">
                <label for="taskTitle">任务标题 <span class="required">*</span></label>
                <input type="text" class="form-input" id="taskTitleInput" placeholder="输入任务标题" maxlength="100">
            </div>
            <div class="form-group">
                <label for="taskDesc">任务描述</label>
                <textarea class="form-textarea" id="taskDescInput" placeholder="输入任务描述(选填)" maxlength="500"></textarea>
            </div>
            <div class="form-row">
                <div class="form-group">
                    <label for="taskPriority">优先级</label>
                    <select class="form-select" id="taskPriority">
                        <option value="low">低</option>
                        <option value="medium" selected>中</option>
                        <option value="high">高</option>
                    </select>
                </div>
                <div class="form-group">
                    <label for="taskCategory">分类</label>
                    <select class="form-select" id="taskCategory">
                        <option value="工作">工作</option>
                        <option value="学习">学习</option>
                        <option value="生活">生活</option>
                        <option value="健康">健康</option>
                    </select>
                </div>
            </div>
            <div class="modal-actions">
                <button class="btn-cancel" id="cancelBtn">取消</button>
                <button class="btn-save" id="saveBtn">保存</button>
            </div>
        </div>
    </div>

    <!-- 删除确认 -->
    <div class="confirm-overlay" id="confirmOverlay">
        <div class="confirm-dialog">
            <div class="confirm-icon">&#9888;</div>
            <h3>确认删除</h3>
            <p>删除后无法恢复,确定要删除这个任务吗?</p>
            <div class="confirm-actions">
                <button class="btn-cancel" id="confirmCancel">取消</button>
                <button class="btn-confirm-delete" id="confirmDelete">删除</button>
            </div>
        </div>
    </div>

    <script>
        /* ========== 数据管理 ========== */
        var STORAGE_KEY = 'task-manager-data';
        var tasks = [];
        var editingTaskId = null;
        var deletingTaskId = null;
        var filterStatus = 'all';
        var filterCategory = 'all';

        function loadTasks() {
            try {
                var saved = localStorage.getItem(STORAGE_KEY);
                if (saved) tasks = JSON.parse(saved);
            } catch (e) {
                tasks = [];
            }

            // 初始化示例数据
            if (tasks.length === 0) {
                tasks = [
                    { id: Date.now(), title: '完成项目方案设计', desc: '整理需求文档,输出技术方案和时间排期', priority: 'high', category: '工作', completed: false, createdAt: Date.now() - 86400000 },
                    { id: Date.now() + 1, title: '阅读《JavaScript高级程序设计》第8章', desc: '重点理解BOM和DOM相关内容', priority: 'medium', category: '学习', completed: false, createdAt: Date.now() - 43200000 },
                    { id: Date.now() + 2, title: '每周跑步3次', desc: '每次至少5公里,保持运动习惯', priority: 'low', category: '健康', completed: false, createdAt: Date.now() - 21600000 },
                    { id: Date.now() + 3, title: '整理房间', desc: '清理书桌和衣柜,捐赠不需要的物品', priority: 'low', category: '生活', completed: true, createdAt: Date.now() - 172800000 },
                    { id: Date.now() + 4, title: '准备周会演示材料', desc: '汇总本周工作进展和下周计划', priority: 'high', category: '工作', completed: false, createdAt: Date.now() - 3600000 }
                ];
                saveTasks();
            }
        }

        function saveTasks() {
            try {
                localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
            } catch (e) {}
        }

        /* ========== 渲染任务列表 ========== */
        function getFilteredTasks() {
            return tasks.filter(function(task) {
                var statusMatch = filterStatus === 'all' ||
                    (filterStatus === 'pending' && !task.completed) ||
                    (filterStatus === 'completed' && task.completed);
                var categoryMatch = filterCategory === 'all' || task.category === filterCategory;
                return statusMatch && categoryMatch;
            });
        }

        function renderTasks() {
            var filtered = getFilteredTasks();
            var listEl = document.getElementById('taskList');
            var emptyEl = document.getElementById('emptyState');

            // 更新统计
            var total = tasks.length;
            var completed = tasks.filter(function(t) { return t.completed; }).length;
            document.getElementById('appStats').textContent = total + ' 个任务,' + completed + ' 个已完成';

            if (filtered.length === 0) {
                listEl.innerHTML = '';
                emptyEl.style.display = 'block';
                return;
            }

            emptyEl.style.display = 'none';

            var priorityLabels = { high: '高', medium: '中', low: '低' };

            listEl.innerHTML = filtered.map(function(task) {
                return '<div class="task-item' + (task.completed ? ' completed' : '') + '" data-id="' + task.id + '" draggable="true">' +
                    '<div class="task-checkbox">' +
                    '<input type="checkbox" ' + (task.completed ? 'checked' : '') + ' aria-label="标记完成">' +
                    '<div class="checkmark">' + (task.completed ? '&#10003;' : '') + '</div>' +
                    '</div>' +
                    '<div class="task-content">' +
                    '<div class="task-title-row">' +
                    '<span class="task-title">' + escapeHtml(task.title) + '</span>' +
                    '<span class="priority-badge priority-' + task.priority + '">' + priorityLabels[task.priority] + '</span>' +
                    '</div>' +
                    (task.desc ? '<div class="task-desc">' + escapeHtml(task.desc) + '</div>' : '') +
                    '<div class="task-meta">' +
                    '<span>&#128193; ' + task.category + '</span>' +
                    '<span>&#128197; ' + formatDate(task.createdAt) + '</span>' +
                    '</div>' +
                    '</div>' +
                    '<div class="task-actions">' +
                    '<button class="action-btn edit" data-id="' + task.id + '" aria-label="编辑任务">&#9998;</button>' +
                    '<button class="action-btn delete" data-id="' + task.id + '" aria-label="删除任务">&#128465;</button>' +
                    '</div>' +
                    '</div>';
            }).join('');

            bindTaskEvents();
        }

        function escapeHtml(text) {
            var div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML;
        }

        function formatDate(timestamp) {
            var d = new Date(timestamp);
            var month = d.getMonth() + 1;
            var day = d.getDate();
            var hours = String(d.getHours()).padStart(2, '0');
            var minutes = String(d.getMinutes()).padStart(2, '0');
            return month + '/' + day + ' ' + hours + ':' + minutes;
        }

        /* ========== 事件绑定 ========== */
        function bindTaskEvents() {
            // 复选框
            document.querySelectorAll('.task-checkbox input').forEach(function(cb) {
                cb.addEventListener('change', function() {
                    var id = parseInt(this.closest('.task-item').dataset.id);
                    var task = tasks.find(function(t) { return t.id === id; });
                    if (task) {
                        task.completed = this.checked;
                        saveTasks();
                        renderTasks();
                    }
                });
            });

            // 编辑按钮
            document.querySelectorAll('.action-btn.edit').forEach(function(btn) {
                btn.addEventListener('click', function(e) {
                    e.stopPropagation();
                    var id = parseInt(this.dataset.id);
                    openEditModal(id);
                });
            });

            // 删除按钮
            document.querySelectorAll('.action-btn.delete').forEach(function(btn) {
                btn.addEventListener('click', function(e) {
                    e.stopPropagation();
                    deletingTaskId = parseInt(this.dataset.id);
                    document.getElementById('confirmOverlay').classList.add('open');
                });
            });

            // 拖拽排序
            document.querySelectorAll('.task-item').forEach(function(item) {
                item.addEventListener('dragstart', handleDragStart);
                item.addEventListener('dragend', handleDragEnd);
                item.addEventListener('dragover', handleDragOver);
                item.addEventListener('drop', handleDrop);
                item.addEventListener('dragenter', handleDragEnter);
                item.addEventListener('dragleave', handleDragLeave);
            });
        }

        /* ========== 拖拽排序 ========== */
        var draggedItem = null;

        function handleDragStart(e) {
            draggedItem = this;
            this.classList.add('dragging');
            e.dataTransfer.effectAllowed = 'move';
            e.dataTransfer.setData('text/plain', this.dataset.id);
        }

        function handleDragEnd() {
            this.classList.remove('dragging');
            document.querySelectorAll('.task-item').forEach(function(item) {
                item.classList.remove('drag-over');
            });
        }

        function handleDragOver(e) {
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
        }

        function handleDragEnter(e) {
            e.preventDefault();
            if (this !== draggedItem) {
                this.classList.add('drag-over');
            }
        }

        function handleDragLeave() {
            this.classList.remove('drag-over');
        }

        function handleDrop(e) {
            e.preventDefault();
            this.classList.remove('drag-over');

            if (this === draggedItem) return;

            var fromId = parseInt(draggedItem.dataset.id);
            var toId = parseInt(this.dataset.id);
            var fromIndex = tasks.findIndex(function(t) { return t.id === fromId; });
            var toIndex = tasks.findIndex(function(t) { return t.id === toId; });

            if (fromIndex === -1 || toIndex === -1) return;

            var movedTask = tasks.splice(fromIndex, 1)[0];
            tasks.splice(toIndex, 0, movedTask);
            saveTasks();
            renderTasks();
        }

        /* ========== 模态框操作 ========== */
        var taskModal = document.getElementById('taskModal');

        function openAddModal() {
            editingTaskId = null;
            document.getElementById('modalTitle').textContent = '添加任务';
            document.getElementById('taskTitleInput').value = '';
            document.getElementById('taskDescInput').value = '';
            document.getElementById('taskPriority').value = 'medium';
            document.getElementById('taskCategory').value = '工作';
            taskModal.classList.add('open');
            document.getElementById('taskTitleInput').focus();
        }

        function openEditModal(id) {
            var task = tasks.find(function(t) { return t.id === id; });
            if (!task) return;

            editingTaskId = id;
            document.getElementById('modalTitle').textContent = '编辑任务';
            document.getElementById('taskTitleInput').value = task.title;
            document.getElementById('taskDescInput').value = task.desc || '';
            document.getElementById('taskPriority').value = task.priority;
            document.getElementById('taskCategory').value = task.category;
            taskModal.classList.add('open');
            document.getElementById('taskTitleInput').focus();
        }

        function closeModal() {
            taskModal.classList.remove('open');
            editingTaskId = null;
        }

        function saveTask() {
            var title = document.getElementById('taskTitleInput').value.trim();
            if (!title) {
                document.getElementById('taskTitleInput').style.borderColor = 'var(--color-danger)';
                return;
            }

            var desc = document.getElementById('taskDescInput').value.trim();
            var priority = document.getElementById('taskPriority').value;
            var category = document.getElementById('taskCategory').value;

            if (editingTaskId) {
                var task = tasks.find(function(t) { return t.id === editingTaskId; });
                if (task) {
                    task.title = title;
                    task.desc = desc;
                    task.priority = priority;
                    task.category = category;
                }
            } else {
                tasks.unshift({
                    id: Date.now(),
                    title: title,
                    desc: desc,
                    priority: priority,
                    category: category,
                    completed: false,
                    createdAt: Date.now()
                });
            }

            saveTasks();
            renderTasks();
            closeModal();
        }

        document.getElementById('addTaskBtn').addEventListener('click', openAddModal);
        document.getElementById('cancelBtn').addEventListener('click', closeModal);
        document.getElementById('saveBtn').addEventListener('click', saveTask);

        taskModal.addEventListener('click', function(e) {
            if (e.target === taskModal) closeModal();
        });

        // 输入时清除错误状态
        document.getElementById('taskTitleInput').addEventListener('input', function() {
            this.style.borderColor = '';
        });

        /* ========== 删除确认 ========== */
        document.getElementById('confirmCancel').addEventListener('click', function() {
            document.getElementById('confirmOverlay').classList.remove('open');
            deletingTaskId = null;
        });

        document.getElementById('confirmDelete').addEventListener('click', function() {
            if (deletingTaskId) {
                tasks = tasks.filter(function(t) { return t.id !== deletingTaskId; });
                saveTasks();
                renderTasks();
            }
            document.getElementById('confirmOverlay').classList.remove('open');
            deletingTaskId = null;
        });

        document.getElementById('confirmOverlay').addEventListener('click', function(e) {
            if (e.target === this) {
                this.classList.remove('open');
                deletingTaskId = null;
            }
        });

        /* ========== 筛选 ========== */
        document.getElementById('statusFilter').addEventListener('click', function(e) {
            var btn = e.target.closest('.filter-btn');
            if (!btn) return;
            this.querySelectorAll('.filter-btn').forEach(function(b) { b.classList.remove('active'); });
            btn.classList.add('active');
            filterStatus = btn.dataset.status;
            renderTasks();
        });

        document.getElementById('categoryFilter').addEventListener('change', function() {
            filterCategory = this.value;
            renderTasks();
        });

        /* ========== 键盘快捷键 ========== */
        document.addEventListener('keydown', function(e) {
            if (e.key === 'Escape') {
                closeModal();
                document.getElementById('confirmOverlay').classList.remove('open');
            }
            if (e.key === 'Enter' && taskModal.classList.contains('open')) {
                if (e.target.tagName !== 'TEXTAREA') saveTask();
            }
        });

        /* ========== 初始化 ========== */
        loadTasks();
        renderTasks();
    </script>
</body>
</html>

六、功能说明

1. CRUD操作

  • 创建:点击"添加任务"按钮打开模态框,填写标题、描述、优先级和分类后保存。新任务添加到列表顶部。

  • 读取:任务列表从localStorage加载,渲染时根据筛选条件过滤显示。

  • 更新:点击编辑按钮打开预填充的模态框,修改后保存。复选框切换完成状态也是更新操作。

  • 删除:点击删除按钮弹出确认对话框,确认后从数组中移除并保存。

2. 任务状态切换

每个任务左侧有自定义复选框,点击切换完成/未完成状态。已完成的任务标题显示删除线,描述文字颜色变浅,视觉上与待办任务区分。状态变更立即保存到localStorage。

3. 优先级标记

三级优先级(高/中/低)用不同颜色的标签区分:高优先级红色中优先级黄色低优先级绿色。标签显示在任务标题右侧,一目了然。

4. 分类筛选

筛选栏提供两个维度的筛选:状态筛选(全部/待办/已完成)和分类筛选(工作/学习/生活/健康)。两个筛选条件可以组合使用,实时更新列表。

5. 本地存储持久化

所有任务数据自动保存到localStorage,键名为 task-manager-data。每次增删改操作后都会调用 saveTasks() 持久化数据。页面加载时从localStorage恢复数据。首次使用时生成5条示例任务。

6. 拖拽排序

使用HTML5原生Drag and Drop API实现任务排序。拖拽开始时添加半透明和旋转效果,拖拽到目标位置时显示蓝色边框提示。放下后重新排列任务数组并保存。


七、响应式适配

  • 桌面端(>600px):头部水平布局,表单双列

  • 手机端(<=600px):头部垂直布局,表单单列,筛选栏垂直排列


八、无障碍优化

  • 复选框有 aria-label="标记完成"

  • 编辑和删除按钮有 aria-label

  • 分类筛选下拉框有 aria-label

  • 自定义复选框支持键盘操作焦点样式

  • 模态框ESC键关闭

  • 颜色对比度满足WCAG标准


九、性能优化

  • 数据操作后立即保存,避免批量写入

  • 渲染使用 innerHTML 批量更新DOM

  • 事件绑定在每次渲染后执行,避免内存泄漏

  • HTML转义防止XSS攻击

  • 拖拽使用原生API,无需外部库


十、扩展建议

  • 子任务:支持任务下添加子任务,层级管理

  • 截止日期:添加截止日期和逾期提醒

  • 标签系统:支持自定义标签,多标签筛选

  • 搜索功能:添加任务搜索框

  • 批量操作:支持多选批量完成/删除

  • 数据导出:支持导出为JSON/CSV

  • 主题切换:添加暗色模式

  • 云同步:对接后端API实现多设备同步

  • 番茄钟:集成番茄工作法计时器

  • 统计面板:添加任务完成率统计图表

小贴士

任务管理应用的核心价值在于帮助用户高效管理待办事项。关键设计原则包括:直观的CRUD操作、可靠的数据持久化、灵活的筛选机制。使用localStorage可以在无后端的情况下实现数据持久化,但需要注意存储空间限制(通常5-10MB)。对于更复杂的需求,可以考虑IndexedDB或对接后端API。

常见问题

localStorage的存储限制是多少?

localStorage的存储限制通常为5-10MB(不同浏览器略有差异)。对于任务管理应用来说,这个空间足够存储数千条任务数据。如果数据量更大,可以考虑使用IndexedDB(支持数百MB)或对接后端数据库。

如何实现跨设备同步?

跨设备同步需要后端支持。常见方案包括:1)使用Firebase、Supabase等BaaS服务,无需自建后端;2)自建RESTful API,任务数据通过HTTP请求同步;3)使用WebSocket实现实时同步。前端需要在每次数据变更后调用同步API,并在页面加载时拉取最新数据。

拖拽排序在移动端如何支持?

HTML5 Drag and Drop API在移动端支持有限。移动端拖拽需要使用touch事件(touchstart、touchmove、touchend)手动实现,或使用第三方库如SortableJS、Dragula等。这些库提供了跨平台的拖拽支持,兼容桌面和移动端。

如何防止XSS攻击?

本项目使用escapeHtml()函数对用户输入进行HTML转义,将特殊字符(如<、>、&、引号)转换为HTML实体。这样即使用户输入恶意脚本,也会被当作纯文本显示而不会执行。另外,使用textContent而非innerHTML设置用户输入也是防止XSS的有效方法。

如何添加暗色模式?

暗色模式可以通过CSS变量实现:1)定义两套颜色变量(亮色/暗色);2)使用prefers-color-scheme媒体查询自动切换;3)添加手动切换按钮,通过给body添加class切换主题;4)将用户偏好保存到localStorage。关键是将所有颜色值都使用CSS变量,方便统一切换。

标签: 任务管理 CRUD操作 localStorage 拖拽排序 模态框 前端应用 JavaScript

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 实战项目:项目08:天气应用界面 - 详细教程与实战指南 下一篇: 实战项目:项目10:个人作品集网站 - 详细教程与实战指南

poll相关推荐