2025年9月24日
尝试搭建一个简易任务清单-DateGo
任务清单实现效果如下:

1. 首先,添加一个Node项目,并设置以下配置:

注意启动命令设置为:
start:node server.js2. 创建一个MySQL的数据库用于保存多台设备间存储的任务信息。
3. 在项目目录下,编写以下代码文件:
styles.css
:root {
--bg: #0f172a; /* slate-900 */
--panel: #111827; /* gray-900 */
--text: #e5e7eb; /* gray-200 */
--muted: #9ca3af; /* gray-400 */
--primary: #60a5fa; /* blue-400 */
--primary-hover: #3b82f6; /* blue-500 */
--accent: #22c55e; /* green-500 */
--danger: #ef4444; /* red-500 */
--border: #1f2937; /* gray-800 */
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Segoe UI Emoji";
background: linear-gradient(135deg, #0f172a 0%, #111827 60%, #0b1220 100%);
color: var(--text);
}
.app { max-width: 780px; margin: 40px auto; padding: 24px; background: rgba(17,24,39,0.6); border: 1px solid var(--border); border-radius: 16px; backdrop-filter: blur(6px); }
.header { display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: center; }
.header h1 { margin: 0; font-size: 28px; letter-spacing: 0.5px; }
.date-row { display: flex; gap: 8px; align-items: center; }
.date-row input[type="date"] { background: #0b1220; color: var(--text); border: 1px solid var(--border); padding: 8px 10px; border-radius: 8px; }
.date-title { grid-column: 1 / -1; color: var(--muted); font-size: 14px; }
.new-task { display: grid; grid-template-columns: 1fr auto; gap: 10px; margin-top: 16px; }
.new-task input { padding: 12px; border-radius: 10px; border: 1px solid var(--border); background: #0b1220; color: var(--text); }
.new-task .primary { padding: 10px 14px; border-radius: 10px; border: 1px solid var(--primary); background: var(--primary); color: #0b1220; cursor: pointer; }
.new-task .primary:hover { background: var(--primary-hover); }
.toolbar { display: flex; justify-content: space-between; align-items: center; margin: 14px 0; }
.filters { display: flex; gap: 6px; }
.filter { background: #0b1220; border: 1px solid var(--border); color: var(--text); padding: 8px 10px; border-radius: 999px; cursor: pointer; }
.filter.active { border-color: var(--primary); color: var(--primary); }
.actions { display: flex; gap: 8px; align-items: center; color: var(--muted); }
.actions .ghost { background: transparent; color: var(--muted); border: 1px dashed var(--border); padding: 8px 10px; border-radius: 999px; cursor: pointer; }
.actions .ghost:hover { color: var(--text); border-style: solid; }
.task-list { list-style: none; padding: 0; margin: 0; }
.task-item { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; padding: 10px; border-bottom: 1px solid var(--border); }
.task-item.dragging { background: rgba(96,165,250,0.08); }
.task-title { word-break: break-word; }
.task-title.completed { color: var(--muted); text-decoration: line-through; }
.icon-btn { background: transparent; border: none; color: var(--muted); cursor: pointer; padding: 6px; border-radius: 8px; }
.icon-btn:hover { color: var(--text); background: rgba(255,255,255,0.05); }
.icon-btn.delete { color: var(--danger); }
.icon-btn.edit { color: var(--accent); }
.hint { color: var(--muted); font-size: 12px; margin-top: 12px; }
/* 可访问性与拖拽辅助 */
.task-item[draggable="true"] { cursor: grab; }
.task-item[draggable="true"]:active { cursor: grabbing; }
.all-dates { margin-top: 24px; }
.section-title { font-size: 18px; margin: 8px 0 12px; color: var(--text); }
.all-dates-grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
.date-card { border: 1px solid var(--border); background: rgba(11,18,32,0.6); border-radius: 12px; padding: 12px; }
.date-card h3 { margin: 0 0 8px; font-size: 16px; color: var(--primary); }
.date-card ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.date-card li { display: flex; align-items: center; gap: 8px; color: var(--text); }
.date-card li.completed { color: var(--muted); text-decoration: line-through; }
.date-card .empty { color: var(--muted); font-size: 12px; }
span { font-size: 13px; }
index.html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>任务清单 · 实时记录</title>
<link rel="stylesheet" href="styles.css" />
<!-- 标签栏图标与主题色 -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="theme-color" content="#0b1220" />
</head>
<body>
<div class="app">
<header class="header">
<h1>任务清单</h1>
<div class="date-row">
<label for="datePicker">选择日期</label>
<input id="datePicker" type="date" />
</div>
<div id="dateTitle" class="date-title">——</div>
</header>
<section class="new-task">
<input id="newTaskInput" type="text" placeholder="添加新任务,按 Enter 确认" />
<button id="addBtn" class="primary">添加</button>
</section>
<section class="toolbar">
<div class="filters" role="tablist" aria-label="筛选任务">
<button class="filter active" data-filter="all" role="tab" aria-selected="true">全部</button>
<button class="filter" data-filter="active" role="tab" aria-selected="false">未完成</button>
<button class="filter" data-filter="completed" role="tab" aria-selected="false">已完成</button>
</div>
<div class="actions">
<span id="leftCount">0 项未完成</span>
<button id="clearCompleted" class="ghost">🧹</button>
</div>
</section>
<main>
<ul id="taskList" class="task-list" aria-live="polite"></ul>
</main>
<section class="all-dates">
<h2 class="section-title">所有日期任务</h2>
<div id="allDatesContainer" class="all-dates-grid"></div>
</section>
</div>
<script src="app.js"></script>
</body>
</html>
app.js
(function () {
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
// 在非安全上下文(http)下,可能没有 crypto.randomUUID,提供回退生成器
function genId() {
if (window.crypto && typeof window.crypto.randomUUID === 'function') {
return window.crypto.randomUUID();
}
return (Date.now().toString(36) + Math.random().toString(36).slice(2));
}
// 后端 API 基础与用户标识
const API_BASE = '/api';
const USER_KEY = 'datego_user_v1';
const DEFAULT_USER = 'shared'; // 统一同步码,所有设备默认使用这个值,可通过 ?user= 覆盖
let userId = null;
function getInitialUserId() {
const p = new URLSearchParams(location.search).get('user');
if (p) { localStorage.setItem(USER_KEY, p); return p; }
// 强制使用统一同步码,确保跨设备共享
localStorage.setItem(USER_KEY, DEFAULT_USER);
return DEFAULT_USER;
}
async function fetchJSON(url, opts) {
const res = await fetch(url, Object.assign({ headers: { 'Content-Type': 'application/json' } }, opts || {}));
if (!res.ok) throw new Error('Network ' + res.status);
return res.json();
}
async function loadAllFromServer() {
const data = await fetchJSON(`${API_BASE}/all?user=${encodeURIComponent(userId)}`);
state.tasksByDate = data.tasksByDate || {};
}
// 状态管理
const state = {
date: todayStr(),
filter: 'all',
tasksByDate: loadTasks(),
};
// 初始化
window.addEventListener('DOMContentLoaded', () => {
userId = getInitialUserId();
const datePicker = $('#datePicker');
datePicker.value = state.date;
updateDateTitle();
bindEvents();
loadAllFromServer().then(() => {
render();
}).catch((e) => {
console.error('加载云端数据失败', e);
render();
});
});
function todayStr() {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
function updateDateTitle() {
const dateTitle = $('#dateTitle');
const [y, m, d] = state.date.split('-');
dateTitle.textContent = `${y}年${m}月${d}日下列举需要完成的任务`;
}
function bindEvents() {
const datePicker = $('#datePicker');
const newTaskInput = $('#newTaskInput');
const addBtn = $('#addBtn');
const clearCompleted = $('#clearCompleted');
datePicker.addEventListener('change', () => {
state.date = datePicker.value || todayStr();
updateDateTitle();
render();
});
newTaskInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') addTask();
});
addBtn.addEventListener('click', addTask);
$('.filters').addEventListener('click', (e) => {
const btn = e.target.closest('.filter');
if (!btn) return;
state.filter = btn.dataset.filter;
$$('.filter').forEach(b => b.classList.toggle('active', b === btn));
render();
});
clearCompleted.addEventListener('click', () => {
const list = getTasksForDate();
const next = list.filter(t => !t.completed);
const delIds = list.filter(t => t.completed).map(t => t.id);
setTasksForDate(next);
render();
// 同步云端删除
Promise.all(delIds.map(id => fetch(`${API_BASE}/tasks/${encodeURIComponent(id)}`, { method: 'DELETE' }))).catch(err => {
console.error('批量删除失败', err);
});
});
// 跨标签页同步(本地存储),保留但不影响云端
window.addEventListener('storage', (e) => {
if (e.key === STORAGE_KEY) {
state.tasksByDate = JSON.parse(e.newValue || '{}');
render();
}
});
}
// 数据持久化
const STORAGE_KEY = 'datego_tasks_v1';
function loadTasks() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
} catch {
return {};
}
}
function saveTasks() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.tasksByDate));
}
function getTasksForDate() {
return state.tasksByDate[state.date] || [];
}
function setTasksForDate(list) {
state.tasksByDate[state.date] = list;
saveTasks();
}
// 任务操作
function addTask() {
const input = $('#newTaskInput');
const title = input.value.trim();
if (!title) return;
const list = getTasksForDate();
const task = { id: genId(), title, completed: false };
setTasksForDate([task, ...list]);
input.value = '';
render();
// 同步到后端
fetchJSON(`${API_BASE}/tasks`, {
method: 'POST',
body: JSON.stringify({ userId, date: state.date, title, id: task.id })
}).catch(err => {
console.error('保存到云端失败', err);
});
}
function toggleTask(id) {
const list = getTasksForDate();
const target = list.find(t => t.id === id);
const nextCompleted = target ? !target.completed : true;
const next = list.map(t => t.id === id ? { ...t, completed: nextCompleted } : t);
setTasksForDate(next);
render();
// 同步到后端
fetchJSON(`${API_BASE}/tasks/${encodeURIComponent(id)}`, {
method: 'PATCH',
body: JSON.stringify({ completed: nextCompleted })
}).catch(err => {
console.error('更新到云端失败', err);
});
}
function deleteTask(id) {
const list = getTasksForDate();
const next = list.filter(t => t.id !== id);
setTasksForDate(next);
render();
// 同步到后端
fetch(`${API_BASE}/tasks/${encodeURIComponent(id)}`, { method: 'DELETE' }).catch(err => {
console.error('删除到云端失败', err);
});
}
function editTask(id) {
const li = document.getElementById(`task-${id}`);
const titleEl = $('.task-title', li);
const old = titleEl.textContent;
const input = document.createElement('input');
input.type = 'text';
input.value = old;
input.className = 'edit-input';
input.style.padding = '8px';
input.style.borderRadius = '8px';
input.style.border = '1px solid var(--border)';
input.style.background = '#0b1220';
input.style.color = 'var(--text)';
const commit = () => {
const val = input.value.trim();
const list = getTasksForDate();
const next = list.map(t => t.id === id ? { ...t, title: val || old } : t);
setTasksForDate(next);
render();
// 同步到云端
fetchJSON(`${API_BASE}/tasks/${encodeURIComponent(id)}`, {
method: 'PATCH',
body: JSON.stringify({ title: val || old })
}).catch(err => {
console.error('更新标题到云端失败', err);
});
};
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') commit();
if (e.key === 'Escape') render();
});
input.addEventListener('blur', commit);
titleEl.replaceWith(input);
input.focus();
input.select();
}
// 拖拽排序
function enableDragAndDrop() {
const listEl = $('#taskList');
const items = $$('.task-item', listEl);
let draggingId = null;
items.forEach(item => {
item.draggable = true;
item.addEventListener('dragstart', () => {
draggingId = item.dataset.id;
item.classList.add('dragging');
});
item.addEventListener('dragend', () => {
item.classList.remove('dragging');
draggingId = null;
});
});
listEl.addEventListener('dragover', (e) => {
e.preventDefault();
const after = getDragAfterElement(listEl, e.clientY);
const draggingEl = $('#taskList .task-item.dragging');
if (!draggingEl) return;
if (after == null) listEl.appendChild(draggingEl);
else listEl.insertBefore(draggingEl, after);
});
listEl.addEventListener('drop', () => {
const ids = $$('#taskList .task-item').map(li => li.dataset.id);
const list = getTasksForDate();
const map = new Map(list.map(t => [t.id, t]));
const next = ids.map(id => map.get(id)).filter(Boolean);
setTasksForDate(next);
render();
// 同步排序到云端
fetchJSON(`${API_BASE}/tasks/order`, {
method: 'POST',
body: JSON.stringify({ userId, date: state.date, order: ids })
}).catch(err => {
console.error('更新排序失败', err);
});
});
}
function getDragAfterElement(container, y) {
const els = [...container.querySelectorAll('.task-item:not(.dragging)')];
return els.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY, element: null }).element;
}
function setTasksFor(date, list) {
state.tasksByDate[date] = list;
saveTasks();
}
function getTasks(date) {
return state.tasksByDate[date] || [];
}
function toggleTaskForDate(date, id) {
const list = getTasks(date);
const target = list.find(t => t.id === id);
const nextCompleted = target ? !target.completed : true;
const next = list.map(t => t.id === id ? { ...t, completed: nextCompleted } : t);
setTasksFor(date, next);
render();
// 同步到云端
fetchJSON(`${API_BASE}/tasks/${encodeURIComponent(id)}`, {
method: 'PATCH',
body: JSON.stringify({ completed: nextCompleted })
}).catch(err => {
console.error('更新到云端失败', err);
});
}
function deleteTaskForDate(date, id) {
const list = getTasks(date);
const next = list.filter(t => t.id !== id);
setTasksFor(date, next);
render();
// 同步到云端
fetch(`${API_BASE}/tasks/${encodeURIComponent(id)}`, { method: 'DELETE' }).catch(err => {
console.error('删除到云端失败', err);
});
}
function formatDateCn(dateStr) {
const [y, m, d] = dateStr.split('-');
return `${y}年${m}月${d}日`;
}
function renderAllDates() {
const container = document.getElementById('allDatesContainer');
if (!container) return;
const dates = Object.keys(state.tasksByDate).sort((a, b) => a.localeCompare(b));
if (dates.length === 0) {
container.innerHTML = `<div class="date-card"><h3>暂无日期</h3><div class="empty">当前没有任何任务,请先添加任务</div></div>`;
return;
}
container.innerHTML = dates.map(date => {
const tasks = getTasks(date);
const listHtml = tasks.length
? tasks.map(t => `
<li class="${t.completed ? 'completed' : ''}" data-id="${t.id}">
<input type="checkbox" ${t.completed ? 'checked' : ''} aria-label="完成任务切换" />
<span>${escapeHtml(t.title)}</span>
<button class="icon-btn delete" title="删除" aria-label="删除任务">🗑️</button>
</li>
`).join('')
: `<div class="empty">该日期暂无任务</div>`;
return `
<div class="date-card" data-date="${date}">
<h3>${formatDateCn(date)}</h3>
<ul>${listHtml}</ul>
</div>
`;
}).join('');
// 绑定事件
dates.forEach(date => {
const card = container.querySelector(`.date-card[data-date="${date}"]`);
if (!card) return;
const items = Array.from(card.querySelectorAll('ul li'));
items.forEach(li => {
const id = li.dataset.id;
const checkbox = li.querySelector('input[type="checkbox"]');
const delBtn = li.querySelector('.icon-btn.delete');
if (checkbox) checkbox.addEventListener('change', () => toggleTaskForDate(date, id));
if (delBtn) delBtn.addEventListener('click', () => deleteTaskForDate(date, id));
});
});
}
// 渲染
function render() {
const listEl = $('#taskList');
const list = getTasksForDate();
const filtered = list.filter(t => state.filter === 'all' ? true : state.filter === 'active' ? !t.completed : t.completed);
listEl.innerHTML = filtered.map(t => itemTemplate(t)).join('');
updateLeftCount();
enableDragAndDrop();
// 绑定行内事件
filtered.forEach(t => bindItemEvents(t.id));
// 渲染所有日期卡片
renderAllDates();
}
function updateLeftCount() {
const list = getTasksForDate();
const left = list.filter(t => !t.completed).length;
$('#leftCount').textContent = `${left} 项未完成`;
}
function itemTemplate(t) {
const completed = t.completed ? 'completed' : '';
return `
<li id="task-${t.id}" class="task-item" data-id="${t.id}">
<input type="checkbox" ${t.completed ? 'checked' : ''} aria-label="完成任务切换" />
<div class="task-title ${completed}" title="双击编辑">${escapeHtml(t.title)}</div>
<div class="ops">
<button class="icon-btn edit" title="编辑">✏️</button>
<button class="icon-btn delete" title="删除">🗑️</button>
</div>
</li>
`;
}
function bindItemEvents(id) {
const li = document.getElementById(`task-${id}`);
const checkbox = $('input[type="checkbox"]', li);
const titleEl = $('.task-title', li);
const editBtn = $('.icon-btn.edit', li);
const delBtn = $('.icon-btn.delete', li);
checkbox.addEventListener('change', () => toggleTask(id));
delBtn.addEventListener('click', () => deleteTask(id));
editBtn.addEventListener('click', () => editTask(id));
titleEl.addEventListener('dblclick', () => editTask(id));
}
function escapeHtml(str) {
return str.replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
}
})();
server.js
const http = require('http');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { URL } = require('url');
const mysql = require('mysql2/promise');
const port = process.env.PORT || 1818;
const root = __dirname;
const MIME = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.svg': 'image/svg+xml; charset=utf-8',
};
// 必填:MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DB
const pool = mysql.createPool({
host: process.env.MYSQL_HOST || '127.0.0.1',
user: process.env.MYSQL_USER || '项目名',
password: process.env.MYSQL_PASSWORD || 'XXXXXXX',
database: process.env.MYSQL_DB || '项目名',
port: process.env.MYSQL_PORT ? Number(process.env.MYSQL_PORT) : 3306,
connectionLimit: 10,
charset: 'utf8mb4',
});
async function initDb() {
const createSQL = `
CREATE TABLE IF NOT EXISTS tasks (
id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(128) NOT NULL,
date VARCHAR(10) NOT NULL,
title TEXT NOT NULL,
completed TINYINT(1) NOT NULL DEFAULT 0,
position INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
INDEX idx_user_date (user_id, date),
INDEX idx_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`;
const conn = await pool.getConnection();
try {
await conn.query(createSQL);
} finally {
conn.release();
}
}
function sendJSON(res, code, data) {
res.statusCode = code;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(JSON.stringify(data));
}
function notFound(res) {
res.statusCode = 404;
res.end('Not Found');
}
function methodNotAllowed(res) {
res.statusCode = 405;
res.end('Method Not Allowed');
}
function readBody(req) {
return new Promise((resolve, reject) => {
let raw = '';
req.on('data', (chunk) => {
raw += chunk;
if (raw.length > 1e6) {
req.destroy();
reject(new Error('Payload too large'));
}
});
req.on('end', () => {
if (!raw) return resolve({});
try { resolve(JSON.parse(raw)); } catch { resolve({}); }
});
req.on('error', reject);
});
}
function serveStatic(req, res, urlPath) {
let filePath = path.join(root, urlPath);
if (urlPath === '/' || urlPath === '') filePath = path.join(root, 'index.html');
fs.stat(filePath, (err, stat) => {
if (err || !stat.isFile()) return notFound(res);
const ext = path.extname(filePath).toLowerCase();
res.setHeader('Content-Type', MIME[ext] || 'application/octet-stream');
fs.createReadStream(filePath).pipe(res);
});
}
async function handleApi(req, res, url) {
// 路由分发
const pathname = url.pathname;
try {
if (pathname === '/api/tasks' && req.method === 'GET') {
const user = url.searchParams.get('user');
const date = url.searchParams.get('date');
if (!user || !date) return sendJSON(res, 400, { error: 'missing user or date' });
const [rows] = await pool.query(
'SELECT id, user_id AS userId, date, title, completed, position, created_at AS createdAt, updated_at AS updatedAt FROM tasks WHERE user_id=? AND date=? ORDER BY position ASC, created_at ASC',
[user, date]
);
return sendJSON(res, 200, { tasks: rows });
}
if (pathname === '/api/tasks' && req.method === 'POST') {
const body = await readBody(req);
const userId = body.userId;
const date = body.date;
let title = (body.title || '').toString().trim();
let id = (body.id || '').toString().trim();
if (!userId || !date || !title) return sendJSON(res, 400, { error: 'missing userId/date/title' });
if (!id) id = crypto.randomUUID();
const now = new Date();
const conn = await pool.getConnection();
try {
const [[{ next }]] = await conn.query('SELECT COALESCE(MAX(position), 0) + 1 AS next FROM tasks WHERE user_id=? AND date=?', [userId, date]);
await conn.query(
'INSERT INTO tasks (id, user_id, date, title, completed, position, created_at, updated_at) VALUES (?, ?, ?, ?, 0, ?, ?, ?)',
[id, userId, date, title, next, now, now]
);
} finally {
conn.release();
}
return sendJSON(res, 201, { id });
}
if (pathname.startsWith('/api/tasks/') && req.method === 'PATCH') {
const id = pathname.split('/').pop();
const body = await readBody(req);
const fields = [];
const values = [];
if (typeof body.title === 'string') { fields.push('title=?'); values.push(body.title); }
if (typeof body.completed === 'boolean') { fields.push('completed=?'); values.push(body.completed ? 1 : 0); }
if (fields.length === 0) return sendJSON(res, 400, { error: 'no updatable fields' });
fields.push('updated_at=?'); values.push(new Date());
values.push(id);
await pool.query(`UPDATE tasks SET ${fields.join(', ')} WHERE id=?`, values);
return sendJSON(res, 200, { ok: true });
}
if (pathname.startsWith('/api/tasks/') && req.method === 'DELETE') {
const id = pathname.split('/').pop();
await pool.query('DELETE FROM tasks WHERE id=?', [id]);
return sendJSON(res, 200, { ok: true });
}
if (pathname === '/api/dates' && req.method === 'GET') {
const user = url.searchParams.get('user');
if (!user) return sendJSON(res, 400, { error: 'missing user' });
const [rows] = await pool.query('SELECT DISTINCT date FROM tasks WHERE user_id=? ORDER BY date ASC', [user]);
return sendJSON(res, 200, { dates: rows.map(r => r.date) });
}
if (pathname === '/api/all' && req.method === 'GET') {
const user = url.searchParams.get('user');
if (!user) return sendJSON(res, 400, { error: 'missing user' });
const [rows] = await pool.query(
'SELECT id, user_id AS userId, date, title, completed, position, created_at AS createdAt, updated_at AS updatedAt FROM tasks WHERE user_id=? ORDER BY date ASC, position ASC, created_at ASC',
[user]
);
const map = {};
for (const r of rows) {
if (!map[r.date]) map[r.date] = [];
map[r.date].push({ id: r.id, title: r.title, completed: !!r.completed });
}
return sendJSON(res, 200, { tasksByDate: map });
}
if (pathname === '/api/tasks/order' && req.method === 'POST') {
const body = await readBody(req);
const userId = body.userId;
const date = body.date;
const order = Array.isArray(body.order) ? body.order : [];
if (!userId || !date || order.length === 0) return sendJSON(res, 400, { error: 'missing userId/date/order' });
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
for (let i = 0; i < order.length; i++) {
await conn.query('UPDATE tasks SET position=?, updated_at=? WHERE id=? AND user_id=? AND date=?', [i + 1, new Date(), order[i], userId, date]);
}
await conn.commit();
} catch (e) {
await conn.rollback();
throw e;
} finally {
conn.release();
}
return sendJSON(res, 200, { ok: true });
}
return notFound(res);
} catch (err) {
console.error('API error:', err);
return sendJSON(res, 500, { error: 'internal_error' });
}
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
if (url.pathname.startsWith('/api')) {
return handleApi(req, res, url);
}
// 静态资源
const urlPath = decodeURIComponent(url.pathname);
return serveStatic(req, res, urlPath);
});
initDb().then(() => {
server.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
}).catch((e) => {
console.error('Failed to init DB:', e);
process.exit(1);
});
