AI 生成: 一个简洁的待办事项 Web 应用,支持添加、完成、删除任务,并使用 localStorage 在浏览器本地保存数据。
This commit is contained in:
@@ -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
|
||||
@@ -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
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user