init: Todo Monitor 初始提交

This commit is contained in:
2026-08-02 02:47:50 +08:00
commit a3a9972ab7
45 changed files with 7880 additions and 0 deletions
+1307
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
{
"name": "todomonitor-backend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "node --watch src/index.js",
"start": "node src/index.js"
},
"dependencies": {
"better-sqlite3": "^11.0.0",
"cors": "^2.8.5",
"express": "^4.21.0",
"js-yaml": "^4.1.0"
}
}
+65
View File
@@ -0,0 +1,65 @@
import Database from 'better-sqlite3'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const DB_PATH = path.join(__dirname, '..', 'todomonitor.db')
let db
export function getDb() {
if (!db) {
db = new Database(DB_PATH)
db.pragma('journal_mode = WAL')
db.pragma('foreign_keys = ON')
initTables()
}
return db
}
function initTables() {
db.exec(`
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL DEFAULT '',
x REAL NOT NULL DEFAULT 0,
y REAL NOT NULL DEFAULT 0,
width REAL NOT NULL DEFAULT 280,
height REAL NOT NULL DEFAULT 200,
color TEXT NOT NULL DEFAULT '#fef9c3',
group_id INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL DEFAULT '新分组',
x REAL NOT NULL DEFAULT 0,
y REAL NOT NULL DEFAULT 0,
width REAL NOT NULL DEFAULT 400,
height REAL NOT NULL DEFAULT 300,
color TEXT NOT NULL DEFAULT '#e2e8f0',
priority INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS meta (
key TEXT PRIMARY KEY,
value TEXT
);
`)
try {
db.exec('ALTER TABLE groups ADD COLUMN priority INTEGER NOT NULL DEFAULT 0')
} catch (_) {}
}
export function closeDb() {
if (db) {
db.close()
db = null
}
}
+38
View File
@@ -0,0 +1,38 @@
import express from 'express'
import cors from 'cors'
import notesRouter from './routes/notes.js'
import groupsRouter from './routes/groups.js'
import batchRouter from './routes/batch.js'
import { errorHandler, notFound } from './middleware/errorHandler.js'
import { closeDb } from './db.js'
const app = express()
const PORT = process.env.PORT || 3001
app.use(cors())
app.use(express.json({ limit: '1mb' }))
app.use('/api/notes', notesRouter)
app.use('/api/groups', groupsRouter)
app.use('/api/batch', batchRouter)
app.get('/api/health', (_req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() })
})
app.use(notFound)
app.use(errorHandler)
const server = app.listen(PORT, () => {
console.log(`[TodoMonitor Backend] 运行在 http://localhost:${PORT}`)
})
process.on('SIGTERM', () => {
closeDb()
server.close()
})
process.on('SIGINT', () => {
closeDb()
server.close()
})
+10
View File
@@ -0,0 +1,10 @@
export function errorHandler(err, _req, res, _next) {
console.error('[Error]', err.message)
res.status(err.status || 500).json({
error: err.message || '服务器内部错误'
})
}
export function notFound(_req, res) {
res.status(404).json({ error: '接口不存在' })
}
+27
View File
@@ -0,0 +1,27 @@
import { Router } from 'express'
import { getDb } from '../db.js'
const router = Router()
router.post('/', (req, res) => {
const db = getDb()
const { notes = [] } = req.body
const updateNote = db.prepare(`
UPDATE notes
SET x = ?, y = ?, width = ?, height = ?, group_id = ?,
updated_at = datetime('now', 'localtime')
WHERE id = ?
`)
const transaction = db.transaction((items) => {
for (const item of items) {
updateNote.run(item.x, item.y, item.width, item.height, item.group_id ?? null, item.id)
}
})
transaction(notes)
res.json({ success: true, count: notes.length })
})
export default router
+68
View File
@@ -0,0 +1,68 @@
import { Router } from 'express'
import { getDb } from '../db.js'
const router = Router()
router.get('/', (_req, res) => {
const db = getDb()
const groups = db.prepare('SELECT * FROM groups ORDER BY created_at ASC').all()
res.json(groups)
})
router.post('/', (req, res) => {
const db = getDb()
const { name = '新分组', x = 100, y = 100, width = 400, height = 300, color = '#e2e8f0', priority = 0 } = req.body
const stmt = db.prepare(`
INSERT INTO groups (name, x, y, width, height, color, priority)
VALUES (?, ?, ?, ?, ?, ?, ?)
`)
const result = stmt.run(name, x, y, width, height, color, priority)
const group = db.prepare('SELECT * FROM groups WHERE id = ?').get(result.lastInsertRowid)
res.status(201).json(group)
})
router.put('/:id', (req, res) => {
const db = getDb()
const { id } = req.params
const existing = db.prepare('SELECT * FROM groups WHERE id = ?').get(id)
if (!existing) {
return res.status(404).json({ error: '分组不存在' })
}
const name = req.body.name ?? existing.name
const x = req.body.x ?? existing.x
const y = req.body.y ?? existing.y
const width = req.body.width ?? existing.width
const height = req.body.height ?? existing.height
const color = req.body.color ?? existing.color
const priority = req.body.priority ?? existing.priority
db.prepare(`
UPDATE groups
SET name = ?, x = ?, y = ?, width = ?, height = ?, color = ?, priority = ?,
updated_at = datetime('now', 'localtime')
WHERE id = ?
`).run(name, x, y, width, height, color, priority, id)
const updated = db.prepare('SELECT * FROM groups WHERE id = ?').get(id)
res.json(updated)
})
router.delete('/:id', (req, res) => {
const db = getDb()
const { id } = req.params
const existing = db.prepare('SELECT * FROM groups WHERE id = ?').get(id)
if (!existing) {
return res.status(404).json({ error: '分组不存在' })
}
db.prepare('UPDATE notes SET group_id = NULL WHERE group_id = ?').run(id)
db.prepare('DELETE FROM groups WHERE id = ?').run(id)
res.json({ success: true })
})
export default router
+67
View File
@@ -0,0 +1,67 @@
import { Router } from 'express'
import { getDb } from '../db.js'
const router = Router()
router.get('/', (_req, res) => {
const db = getDb()
const notes = db.prepare('SELECT * FROM notes ORDER BY updated_at DESC').all()
res.json(notes)
})
router.post('/', (req, res) => {
const db = getDb()
const { content = '', x = 200, y = 200, width = 280, height = 200, color = '#fef9c3', group_id = null } = req.body
const stmt = db.prepare(`
INSERT INTO notes (content, x, y, width, height, color, group_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
`)
const result = stmt.run(content, x, y, width, height, color, group_id)
const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(result.lastInsertRowid)
res.status(201).json(note)
})
router.put('/:id', (req, res) => {
const db = getDb()
const { id } = req.params
const existing = db.prepare('SELECT * FROM notes WHERE id = ?').get(id)
if (!existing) {
return res.status(404).json({ error: '便签不存在' })
}
const content = req.body.content ?? existing.content
const x = req.body.x ?? existing.x
const y = req.body.y ?? existing.y
const width = req.body.width ?? existing.width
const height = req.body.height ?? existing.height
const color = req.body.color ?? existing.color
const group_id = req.body.group_id !== undefined ? req.body.group_id : existing.group_id
db.prepare(`
UPDATE notes
SET content = ?, x = ?, y = ?, width = ?, height = ?, color = ?, group_id = ?,
updated_at = datetime('now', 'localtime')
WHERE id = ?
`).run(content, x, y, width, height, color, group_id, id)
const updated = db.prepare('SELECT * FROM notes WHERE id = ?').get(id)
res.json(updated)
})
router.delete('/:id', (req, res) => {
const db = getDb()
const { id } = req.params
const existing = db.prepare('SELECT * FROM notes WHERE id = ?').get(id)
if (!existing) {
return res.status(404).json({ error: '便签不存在' })
}
db.prepare('DELETE FROM notes WHERE id = ?').run(id)
res.json({ success: true })
})
export default router