AI 生成: 一个简洁的待办事项 Web 应用,支持添加、完成、删除任务,并使用 localStorage 在浏览器本地保存数据。

This commit is contained in:
admin
2026-09-28 20:05:39 +08:00
commit 3cf7cbe814
4 changed files with 434 additions and 0 deletions
+44
View File
@@ -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
+174
View File
@@ -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();
})();
+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Simple Todo App</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="app">
<header class="app-header">
<h1>待办事项</h1>
<p class="subtitle">简单、快速、本地保存</p>
</header>
<form id="todo-form" class="todo-form">
<input
id="todo-input"
class="todo-input"
type="text"
placeholder="输入一个新任务..."
autocomplete="off"
maxlength="120"
/>
<button type="submit" class="add-btn">添加</button>
</form>
<section class="todo-stats">
<span id="todo-count">共 0 项</span>
<button id="clear-completed" class="clear-btn" type="button">清除已完成</button>
</section>
<ul id="todo-list" class="todo-list"></ul>
<p id="empty-state" class="empty-state">暂无任务,添加一个开始吧!</p>
</main>
<script src="app.js"></script>
</body>
</html>
+176
View File
@@ -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;
}