init: Todo Monitor 初始提交
This commit is contained in:
Generated
+1307
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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: '接口不存在' })
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user