commit 3cf7cbe8149a30e3465a1e69d7a41ba7d72379bd Author: admin Date: Mon Sep 28 20:05:39 2026 +0800 AI 生成: 一个简洁的待办事项 Web 应用,支持添加、完成、删除任务,并使用 localStorage 在浏览器本地保存数据。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..475d529 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Simple Todo App + +一个简洁的待办事项 Web 应用,支持添加、完成、删除任务,数据保存在浏览器本地。 + +## 功能 + +- 添加任务 +- 标记任务为完成 / 未完成 +- 删除任务 +- 使用 localStorage 自动保存数据 +- 响应式界面,适合桌面和移动端 + +## 技术栈 + +- HTML5 +- CSS3 +- 原生 JavaScript + +## 目录结构 + +``` +simple-todo-app/ +├── index.html +├── style.css +├── app.js +└── README.md +``` + +## 使用方法 + +1. 下载或克隆本项目。 +2. 直接在浏览器中打开 `index.html`。 +3. 输入任务内容,点击“添加”或按回车即可创建任务。 +4. 点击任务左侧复选框可切换完成状态。 +5. 点击右侧删除按钮可删除任务。 + +## 数据存储 + +所有任务数据保存在浏览器的 `localStorage` 中,键名为 `simple-todo-app.tasks`。 +清除浏览器数据会同时清除任务。 + +## 许可 + +MIT diff --git a/app.js b/app.js new file mode 100644 index 0000000..2d85b15 --- /dev/null +++ b/app.js @@ -0,0 +1,174 @@ +// Simple Todo App +// 数据保存在 localStorage 中 + +(function () { + "use strict"; + + const STORAGE_KEY = "simple-todo-app.tasks"; + + // DOM 元素 + const form = document.getElementById("todo-form"); + const input = document.getElementById("todo-input"); + const list = document.getElementById("todo-list"); + const countEl = document.getElementById("todo-count"); + const emptyState = document.getElementById("empty-state"); + const clearCompletedBtn = document.getElementById("clear-completed"); + + /** + * 从 localStorage 读取任务列表 + * @returns {Array<{id: string, text: string, completed: boolean}>} + */ + function loadTasks() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return []; + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch (err) { + console.error("读取任务失败:", err); + return []; + } + } + + /** + * 保存任务列表到 localStorage + * @param {Array} tasks + */ + function saveTasks(tasks) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks)); + } catch (err) { + console.error("保存任务失败:", err); + } + } + + // 当前任务状态 + let tasks = loadTasks(); + + /** + * 生成唯一 ID + */ + function generateId() { + return Date.now().toString(36) + Math.random().toString(36).slice(2, 8); + } + + /** + * 渲染任务列表 + */ + function render() { + list.innerHTML = ""; + + tasks.forEach(function (task) { + const li = document.createElement("li"); + li.className = "todo-item" + (task.completed ? " completed" : ""); + li.dataset.id = task.id; + + // 复选框 + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = task.completed; + checkbox.addEventListener("change", function () { + toggleTask(task.id); + }); + + // 文本 + const span = document.createElement("span"); + span.className = "todo-text"; + span.textContent = task.text; + + // 删除按钮 + const delBtn = document.createElement("button"); + delBtn.className = "delete-btn"; + delBtn.type = "button"; + delBtn.setAttribute("aria-label", "删除任务"); + delBtn.textContent = "✕"; + delBtn.addEventListener("click", function () { + deleteTask(task.id); + }); + + li.appendChild(checkbox); + li.appendChild(span); + li.appendChild(delBtn); + list.appendChild(li); + }); + + // 空状态 + if (tasks.length === 0) { + emptyState.classList.remove("hidden"); + } else { + emptyState.classList.add("hidden"); + } + + // 统计 + const remaining = tasks.filter(function (t) { + return !t.completed; + }).length; + countEl.textContent = "共 " + tasks.length + " 项 · 未完成 " + remaining + " 项"; + } + + /** + * 添加任务 + */ + function addTask(text) { + const trimmed = text.trim(); + if (!trimmed) return; + + tasks.push({ + id: generateId(), + text: trimmed, + completed: false + }); + + saveTasks(tasks); + render(); + } + + /** + * 切换任务完成状态 + */ + function toggleTask(id) { + tasks = tasks.map(function (t) { + return t.id === id ? Object.assign({}, t, { completed: !t.completed }) : t; + }); + saveTasks(tasks); + render(); + } + + /** + * 删除任务 + */ + function deleteTask(id) { + tasks = tasks.filter(function (t) { + return t.id !== id; + }); + saveTasks(tasks); + render(); + } + + /** + * 清除已完成任务 + */ + function clearCompleted() { + tasks = tasks.filter(function (t) { + return !t.completed; + }); + saveTasks(tasks); + render(); + } + + // 事件绑定 + form.addEventListener("submit", function (e) { + e.preventDefault(); + addTask(input.value); + input.value = ""; + input.focus(); + }); + + clearCompletedBtn.addEventListener("click", clearCompleted); + + // 初始渲染 + render(); + + // 自动聚焦输入框 + input.focus(); +})(); diff --git a/index.html b/index.html new file mode 100644 index 0000000..9c2a99d --- /dev/null +++ b/index.html @@ -0,0 +1,40 @@ + + + + + + Simple Todo App + + + +
+
+

待办事项

+

简单、快速、本地保存

+
+ +
+ + +
+ +
+ 共 0 项 + +
+ + + +

暂无任务,添加一个开始吧!

+
+ + + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..c82e6e7 --- /dev/null +++ b/style.css @@ -0,0 +1,176 @@ +:root { + --bg: #f5f7fb; + --card: #ffffff; + --text: #1f2937; + --muted: #6b7280; + --primary: #2563eb; + --primary-hover: #1d4ed8; + --danger: #ef4444; + --border: #e5e7eb; + --done: #9ca3af; + --shadow: 0 10px 25px rgba(15, 23, 42, 0.08); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background: linear-gradient(180deg, #eef2ff 0%, var(--bg) 100%); + color: var(--text); + display: flex; + justify-content: center; + padding: 40px 16px; +} + +.app { + width: 100%; + max-width: 560px; + background: var(--card); + border-radius: 16px; + box-shadow: var(--shadow); + padding: 28px; +} + +.app-header h1 { + margin: 0; + font-size: 28px; + letter-spacing: -0.5px; +} + +.subtitle { + margin: 6px 0 22px; + color: var(--muted); + font-size: 14px; +} + +.todo-form { + display: flex; + gap: 10px; +} + +.todo-input { + flex: 1; + padding: 12px 14px; + border: 1px solid var(--border); + border-radius: 10px; + font-size: 15px; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.todo-input:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); +} + +.add-btn { + padding: 12px 18px; + border: none; + border-radius: 10px; + background: var(--primary); + color: #fff; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.add-btn:hover { + background: var(--primary-hover); +} + +.todo-stats { + display: flex; + justify-content: space-between; + align-items: center; + margin: 18px 0 10px; + color: var(--muted); + font-size: 13px; +} + +.clear-btn { + background: transparent; + border: none; + color: var(--muted); + cursor: pointer; + font-size: 13px; +} + +.clear-btn:hover { + color: var(--danger); +} + +.todo-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.todo-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 14px; + border: 1px solid var(--border); + border-radius: 10px; + background: #fff; + transition: background 0.2s, transform 0.1s; +} + +.todo-item:hover { + background: #fafafa; +} + +.todo-item input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--primary); + cursor: pointer; + flex-shrink: 0; +} + +.todo-text { + flex: 1; + font-size: 15px; + word-break: break-word; +} + +.todo-item.completed .todo-text { + text-decoration: line-through; + color: var(--done); +} + +.delete-btn { + border: none; + background: transparent; + color: var(--muted); + font-size: 18px; + line-height: 1; + cursor: pointer; + padding: 4px 8px; + border-radius: 6px; + transition: background 0.2s, color 0.2s; +} + +.delete-btn:hover { + background: #fee2e2; + color: var(--danger); +} + +.empty-state { + margin: 24px 0 0; + text-align: center; + color: var(--muted); + font-size: 14px; +} + +.empty-state.hidden { + display: none; +}