JavaScript案例(待办事项列表)
项目概述
使用原生JavaScript实现一个功能完善的待办事项列表应用,支持任务的添加、编辑、删除和完成状态切换,并通过localStorage实现数据持久化。
核心功能需求
- 任务管理:添加、编辑、删除任务
- 状态切换:标记任务完成/未完成,带删除线样式
- 数据持久化:使用localStorage保存任务数据
- 交互设计:支持回车添加任务,按钮操作
技术栈
- HTML5:页面结构
- CSS3:样式设计,包括完成状态样式
- JavaScript:DOM操作、事件处理、本地存储
- 本地存储:localStorage API
项目结构
todo-list/
├── index.html # 主页面
├── css/
│ └── style.css # 样式文件
├── js/└── app.js # 核心逻辑
实现思路
1. 数据结构设计
// 任务对象结构
{id: Number, // 唯一标识符content: String, // 任务内容completed: Boolean, // 完成状态createdAt: Date // 创建时间
}// 任务数组
let tasks = [];
2. 核心功能模块
- DOM操作模块:动态创建任务元素、更新任务列表
- 事件处理模块:添加、编辑、删除、状态切换事件
- 本地存储模块:保存、读取、更新本地数据
- 交互优化模块:回车添加、焦点管理、动画效果
3. 关键技术点实现
- 动态创建元素:使用document.createElement和appendChild
- 状态切换:通过classList.toggle(‘completed’)实现样式切换
- 本地存储:JSON.stringify和JSON.parse处理数据
- 事件委托:利用事件冒泡机制处理动态元素事件
代码:
- index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>JavaScript待办事项列表</title><link rel="stylesheet" href="css/style.css"><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head>
<body><div class="container"><header><h1>待办事项列表 <i class="fas fa-check-circle"></i></h1><p>管理你的任务,提高工作效率</p></header><div class="task-input"><input type="text" id="taskInput" placeholder="添加新任务..."><button id="addTaskBtn"><i class="fas fa-plus"></i> 添加任务</button></div><div class="task-stats"><span>剩余任务: <strong id="remainingTasks">0</strong></span><button id="clearCompletedBtn"><i class="fas fa-trash"></i> 清除已完成</button></div><ul id="taskList" class="task-list"><!-- 任务项将通过JavaScript动态生成 --><li class="task-item placeholder"><span>你的任务将显示在这里</span></li></ul></div><script src="js/app.js"></script>
</body>
</html>
- style.css
/* 基础样式重置 */
.container{background-color: #ebf2fa;
}
* {margin: 0;padding: 0;box-sizing: border-box;font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}body {background-color: #f5f5f5;color: #333;line-height: 1.6;
}.container {max-width: 800px;margin: 0 auto;padding: 20px;
}header {text-align: center;margin-bottom: 30px;padding-bottom: 20px;border-bottom: 1px solid #eee;
}header h1 {font-size: 2.5rem;margin-bottom: 10px;color: #2c3e50;
}header p {color: #7f8c8d;font-size: 1.1rem;
}/* 任务输入区域 */
.task-input {display: flex;gap: 10px;margin-bottom: 25px;
}.task-input input {flex: 1;padding: 12px 15px;border: 2px solid #ddd;border-radius: 6px;font-size: 1rem;transition: all 0.3s ease;
}.task-input input:focus {outline: none;border-color: #3498db;box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}.task-input button {padding: 12px 20px;background-color: #3498db;color: white;border: none;border-radius: 6px;cursor: pointer;font-size: 1rem;transition: background-color 0.3s ease;display: flex;align-items: center;gap: 8px;
}.task-input button:hover {background-color: #2980b9;
}/* 任务统计区域 */
.task-stats {display: flex;justify-content: space-between;align-items: center;margin-bottom: 20px;padding: 10px 15px;background-color: #fff;border-radius: 6px;box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}.task-stats span {color: #7f8c8d;font-size: 0.95rem;
}.task-stats strong {color: #2c3e50;
}.task-stats button {background-color: transparent;color: #e74c3c;border: none;cursor: pointer;font-size: 0.95rem;display: flex;align-items: center;gap: 5px;padding: 5px 10px;border-radius: 4px;transition: all 0.2s ease;
}.task-stats button:hover {background-color: rgba(231, 76, 60, 0.1);
}/* 任务列表 */
.task-list {list-style: none;margin-top: 20px;
}.task-item {background-color: white;padding: 15px;border-radius: 6px;margin-bottom: 10px;display: flex;align-items: center;gap: 15px;box-shadow: 0 2px 4px rgba(0,0,0,0.05);transition: transform 0.2s ease, box-shadow 0.2s ease;
}.task-item:hover {transform: translateY(-2px);box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}.task-item.placeholder {background-color: rgba(255,255,255,0.5);justify-content: center;color: #95a5a6;padding: 40px 20px;
}/* 任务复选框 */
.task-checkbox {width: 22px;height: 22px;border-radius: 50%;border: 2px solid #ddd;cursor: pointer;display: flex;align-items: center;justify-content: center;flex-shrink: 0;transition: all 0.3s ease;
}.task-checkbox:hover {border-color: #3498db;
}.task-checkbox.checked {background-color: #2ecc71;border-color: #2ecc71;
}/* 任务内容 */
.task-content {flex: 1;transition: color 0.3s ease, text-decoration 0.3s ease;font-size: 1.05rem;
}.task-item.completed .task-content {text-decoration: line-through;color: #95a5a6;
}/* 任务操作按钮 */
.task-actions {display: flex;gap: 8px;
}.task-btn {background: none;border: none;cursor: pointer;font-size: 1rem;padding: 5px;border-radius: 4px;transition: all 0.2s ease;display: flex;align-items: center;justify-content: center;
}.edit-btn {color: #f39c12;
}.edit-btn:hover {background-color: rgba(243, 156, 18, 0.1);
}.delete-btn {color: #e74c3c;
}.delete-btn:hover {background-color: rgba(231, 76, 60, 0.1);
}/* 响应式设计 */
@media (max-width: 600px) {.task-input {flex-direction: column;}.task-input button {width: 100%;justify-content: center;}.task-stats {flex-direction: column;align-items: stretch;gap: 10px;}.task-stats button {width: 100%;justify-content: center;}
}
- app.js
// 任务数据存储
let tasks = [];// DOM元素
const taskInput = document.getElementById('taskInput');
const addTaskBtn = document.getElementById('addTaskBtn');
const taskList = document.getElementById('taskList');
const remainingTasksEl = document.getElementById('remainingTasks');
const clearCompletedBtn = document.getElementById('clearCompletedBtn');
const placeholderEl = document.querySelector('.placeholder');// 初始化应用
function init() {// 从本地存储加载任务loadTasks();// 渲染任务列表renderTasks();// 绑定事件监听器bindEvents();
}// 从本地存储加载任务
function loadTasks() {const storedTasks = localStorage.getItem('tasks');if (storedTasks) {tasks = JSON.parse(storedTasks);}
}// 保存任务到本地存储
function saveTasks() {localStorage.setItem('tasks', JSON.stringify(tasks));
}// 渲染任务列表
function renderTasks() {// 清空任务列表taskList.innerHTML = '';// 检查是否有任务if (tasks.length === 0) {taskList.appendChild(placeholderEl);remainingTasksEl.textContent = '0';return;}// 隐藏占位符if (placeholderEl.parentNode === taskList) {taskList.removeChild(placeholderEl);}// 渲染每个任务tasks.forEach(task => {const taskElement = createTaskElement(task);taskList.appendChild(taskElement);});// 更新剩余任务数量updateRemainingTasks();
}// 创建任务元素
function createTaskElement(task) {const li = document.createElement('li');li.className = `task-item ${task.completed ? 'completed' : ''}`;li.dataset.taskId = task.id;li.innerHTML = `<div class="task-checkbox ${task.completed ? 'checked' : ''}">${task.completed ? '<i class="fas fa-check"></i>' : ''}</div><div class="task-content" contenteditable="false">${escapeHTML(task.content)}</div><div class="task-actions"><button class="task-btn edit-btn" title="编辑任务"><i class="fas fa-edit"></i></button><button class="task-btn delete-btn" title="删除任务"><i class="fas fa-trash"></i></button></div>`;return li;
}// 添加新任务
function addTask(content) {if (!content.trim()) return; // 忽略空任务const newTask = {id: generateId(),content: content.trim(),completed: false,createdAt: new Date().toISOString()};tasks.unshift(newTask); // 添加到数组开头saveTasks();renderTasks();// 清空输入框taskInput.value = '';// 聚焦输入框taskInput.focus();
}// 切换任务完成状态
function toggleTaskCompletion(taskId) {const task = tasks.find(t => t.id === taskId);if (task) {task.completed = !task.completed;saveTasks();renderTasks();}
}// 编辑任务内容
function editTask(taskId, newContent) {const task = tasks.find(t => t.id === taskId);if (task && newContent.trim()) {task.content = newContent.trim();saveTasks();renderTasks();}
}// 删除任务
function deleteTask(taskId) {tasks = tasks.filter(task => task.id !== taskId);saveTasks();renderTasks();
}// 清除所有已完成任务
function clearCompletedTasks() {tasks = tasks.filter(task => !task.completed);saveTasks();renderTasks();
}// 更新剩余任务数量
function updateRemainingTasks() {const remaining = tasks.filter(task => !task.completed).length;remainingTasksEl.textContent = remaining;
}// 绑定事件监听器
function bindEvents() {// 添加任务按钮点击事件addTaskBtn.addEventListener('click', () => {addTask(taskInput.value);});// 输入框回车事件taskInput.addEventListener('keypress', (e) => {if (e.key === 'Enter') {addTask(taskInput.value);}});// 任务列表事件委托taskList.addEventListener('click', (e) => {const taskItem = e.target.closest('.task-item');if (!taskItem || taskItem.classList.contains('placeholder')) return;const taskId = parseInt(taskItem.dataset.taskId);// 切换完成状态if (e.target.closest('.task-checkbox')) {toggleTaskCompletion(taskId);}// 删除任务if (e.target.closest('.delete-btn')) {deleteTask(taskId);}// 编辑任务if (e.target.closest('.edit-btn')) {const contentEl = taskItem.querySelector('.task-content');enableEditMode(contentEl, taskId);}});// 双击任务内容编辑taskList.addEventListener('dblclick', (e) => {const contentEl = e.target.closest('.task-content');if (contentEl) {const taskItem = contentEl.closest('.task-item');const taskId = parseInt(taskItem.dataset.taskId);enableEditMode(contentEl, taskId);}});// 清除已完成按钮点击事件clearCompletedBtn.addEventListener('click', clearCompletedTasks);
}// 启用编辑模式
function enableEditMode(contentEl, taskId) {// 移除所有任务的编辑状态document.querySelectorAll('.task-content').forEach(el => {el.contentEditable = 'false';el.classList.remove('editing');});// 设置当前任务为编辑状态contentEl.contentEditable = 'true';contentEl.classList.add('editing');contentEl.focus();// 保存编辑内容const saveEdit = (e) => {if (e.type === 'blur' || (e.type === 'keydown' && e.key === 'Enter')) {e.preventDefault();editTask(taskId, contentEl.textContent);contentEl.contentEditable = 'false';contentEl.classList.remove('editing');// 移除事件监听器contentEl.removeEventListener('blur', saveEdit);contentEl.removeEventListener('keydown', saveEdit);}};// 添加事件监听器contentEl.addEventListener('blur', saveEdit);contentEl.addEventListener('keydown', saveEdit);
}// 生成唯一ID
function generateId() {return Date.now() + Math.floor(Math.random() * 1000);
}// HTML转义
function escapeHTML(str) {return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
}// 初始化应用
document.addEventListener('DOMContentLoaded', init);
效果展示: