pin_drop当前位置:知识文库 ❯ 图文
实战项目:项目09:任务管理应用 - 详细教程与实战指南
一、项目简介
任务管理应用是提升个人和团队效率的重要工具,也是前端CRUD(创建、读取、更新、删除)操作的典型应用场景。一个优秀的任务管理应用需要提供直观的任务创建和编辑体验、灵活的筛选和分类机制、以及可靠的数据持久化方案。
本项目将构建一个功能完善的任务管理应用,包含任务列表、添加/编辑/删除任务、任务状态切换、优先级标记、分类筛选、本地存储和拖拽排序等核心功能。项目实现了完整的CRUD操作和localStorage持久化,确保数据在刷新页面后不会丢失。
二、需求分析
功能需求
任务列表:展示所有任务,显示标题、描述、优先级、分类、状态、创建时间
添加任务:弹窗表单,输入标题、描述、优先级、分类
编辑任务:点击编辑按钮修改任务信息
删除任务:点击删除按钮移除任务,带确认提示
状态切换:点击复选框切换完成/未完成状态
优先级标记:高/中/低三级优先级,颜色区分
分类筛选:按分类和状态筛选任务
本地存储:任务数据自动保存到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">📋</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">⚠</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 ? '✓' : '') + '</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>📁 ' + task.category + '</span>' +
'<span>📅 ' + formatDate(task.createdAt) + '</span>' +
'</div>' +
'</div>' +
'<div class="task-actions">' +
'<button class="action-btn edit" data-id="' + task.id + '" aria-label="编辑任务">✎</button>' +
'<button class="action-btn delete" data-id="' + task.id + '" aria-label="删除任务">🗑</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变量,方便统一切换。
本文涉及AI创作
内容由AI创作,请仔细甄别