Files

338 lines
7.4 KiB
HTML
Raw Permalink Normal View History

2026-07-16 15:42:31 +08:00
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>待办清单</title>
<style>
:root {
color-scheme: light;
font-family: Arial, Helvetica, sans-serif;
background: #f5f7fb;
color: #172033;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 48px 18px;
}
main {
width: min(760px, 100%);
background: #ffffff;
border: 1px solid #dfe5ef;
border-radius: 8px;
box-shadow: 0 16px 40px rgba(23, 32, 51, 0.08);
overflow: hidden;
}
header {
padding: 28px 30px 18px;
border-bottom: 1px solid #e8edf5;
}
h1 {
margin: 0 0 8px;
font-size: 28px;
line-height: 1.2;
}
.status {
min-height: 20px;
color: #657189;
font-size: 14px;
}
.composer {
display: flex;
gap: 10px;
padding: 20px 30px;
border-bottom: 1px solid #e8edf5;
background: #fbfcff;
}
input[type="text"] {
flex: 1;
min-width: 0;
height: 42px;
border: 1px solid #cbd5e1;
border-radius: 6px;
padding: 0 12px;
font-size: 15px;
outline: none;
}
input[type="text"]:focus {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}
button {
height: 42px;
border: 0;
border-radius: 6px;
padding: 0 14px;
background: #2563eb;
color: #ffffff;
font-size: 14px;
cursor: pointer;
}
button.secondary {
background: #e8edf5;
color: #172033;
}
button.danger {
background: #dc2626;
}
button:disabled {
opacity: 0.55;
cursor: not-allowed;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
li {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 12px;
align-items: center;
padding: 16px 30px;
border-bottom: 1px solid #edf1f7;
}
li:last-child {
border-bottom: 0;
}
.task {
min-width: 0;
font-size: 16px;
overflow-wrap: anywhere;
}
.task.done {
color: #7b879c;
text-decoration: line-through;
}
.actions {
display: flex;
gap: 8px;
}
.empty {
padding: 34px 30px;
color: #657189;
text-align: center;
}
@media (max-width: 560px) {
body {
padding: 18px 10px;
}
header,
.composer,
li,
.empty {
padding-left: 16px;
padding-right: 16px;
}
.composer {
flex-direction: column;
}
li {
grid-template-columns: auto 1fr;
}
.actions {
grid-column: 1 / -1;
justify-content: flex-end;
}
}
</style>
</head>
<body>
<main>
<header>
<h1>待办清单</h1>
<div id="status" class="status">正在连接...</div>
</header>
<form id="todoForm" class="composer">
<input id="taskInput" type="text" placeholder="写下一件要做的事" autocomplete="off" maxlength="200">
<button type="submit">添加</button>
</form>
<ul id="todoList"></ul>
</main>
<script>
const API_URL = "/api";
const statusEl = document.getElementById("status");
const formEl = document.getElementById("todoForm");
const inputEl = document.getElementById("taskInput");
const listEl = document.getElementById("todoList");
let todos = [];
function setStatus(message) {
statusEl.textContent = message;
}
async function api(path, options = {}) {
const response = await fetch(`${API_URL}${path}`, {
headers: {
"Content-Type": "application/json",
...(options.headers || {})
},
...options
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: response.statusText }));
throw new Error(error.detail || "Request failed");
}
return response.json();
}
function renderTodos() {
listEl.innerHTML = "";
if (todos.length === 0) {
const empty = document.createElement("li");
empty.className = "empty";
empty.textContent = "暂时没有任务。";
listEl.appendChild(empty);
return;
}
for (const todo of todos) {
const item = document.createElement("li");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = todo.completion;
checkbox.addEventListener("change", () => updateTodo(todo.id, { completion: checkbox.checked }));
const task = document.createElement("div");
task.className = todo.completion ? "task done" : "task";
task.textContent = todo.task;
const actions = document.createElement("div");
actions.className = "actions";
const editButton = document.createElement("button");
editButton.type = "button";
editButton.className = "secondary";
editButton.textContent = "编辑";
editButton.addEventListener("click", () => editTodo(todo));
const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.className = "danger";
deleteButton.textContent = "删除";
deleteButton.addEventListener("click", () => deleteTodo(todo.id));
actions.append(editButton, deleteButton);
item.append(checkbox, task, actions);
listEl.appendChild(item);
}
}
async function loadTodos() {
try {
setStatus("正在加载...");
todos = await api("/todos");
renderTodos();
const completed = todos.filter((todo) => todo.completion).length;
setStatus(`共 ${todos.length} 项,已完成 ${completed} 项`);
} catch (error) {
setStatus(`连接失败:${error.message}`);
}
}
async function createTodo(task) {
await api("/todos", {
method: "POST",
body: JSON.stringify({ task })
});
await loadTodos();
}
async function updateTodo(id, updateData) {
await api(`/todos/${id}`, {
method: "PATCH",
body: JSON.stringify(updateData)
});
await loadTodos();
}
async function editTodo(todo) {
const task = prompt("修改任务", todo.task);
if (task === null) {
return;
}
const trimmed = task.trim();
if (trimmed === "") {
setStatus("任务内容不能为空。 ");
return;
}
await updateTodo(todo.id, { task: trimmed });
}
async function deleteTodo(id) {
await api(`/todos/${id}`, { method: "DELETE" });
await loadTodos();
}
formEl.addEventListener("submit", async (event) => {
event.preventDefault();
const task = inputEl.value.trim();
if (task === "") {
setStatus("任务内容不能为空。 ");
return;
}
formEl.querySelector("button").disabled = true;
try {
await createTodo(task);
inputEl.value = "";
} catch (error) {
setStatus(`添加失败:${error.message}`);
} finally {
formEl.querySelector("button").disabled = false;
inputEl.focus();
}
});
loadTodos();
</script>
</body>
</html>