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