diff --git a/api/[campus]/[...path].js b/api/[campus]/[...path].js new file mode 100644 index 0000000..807bb72 --- /dev/null +++ b/api/[campus]/[...path].js @@ -0,0 +1,141 @@ +const db = require('../_db'); + +const ALLOWED_CAMPUSES = ['chencunnct', 'beijiaonct']; +const CAMPUS_NAMES = { chencunnct: '陈村校区', beijiaonct: '北滘校区' }; + +module.exports = async function handler(req, res) { + // 设置 CORS + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET,POST,DELETE,OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + return res.status(200).end(); + } + + const campus = req.query.campus; + if (!ALLOWED_CAMPUSES.includes(campus)) { + return res.status(404).json({ ok: false, msg: '校区不存在' }); + } + + const pathParts = (req.query.path || []); + const route = pathParts[0] || ''; + + // 路由分发 + if (route === 'register' && req.method === 'POST') { + return handleRegister(req, res, campus); + } + if (route === 'registrations' && req.method === 'GET') { + return handleGetRegistrations(req, res, campus); + } + if (route === 'stats' && req.method === 'GET') { + return handleGetStats(req, res, campus); + } + if (route === 'registrations' && req.method === 'DELETE') { + return handleDeleteRegistration(req, res, campus); + } + + return res.status(404).json({ ok: false, msg: '接口不存在' }); +}; + +// 报名 +async function handleRegister(req, res, campus) { + const { name, period, teacher } = req.body || {}; + + if (!name || !period || !teacher) { + return res.json({ ok: false, msg: '请填写完整信息' }); + } + + try { + const registrations = await db.getRegistrations(campus); + const id = await db.getNextId(campus); + const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false }) + .replace(/\//g, '-'); + + registrations.unshift({ + id, + name: name.trim(), + period, + teacher, + createdAt: now + }); + + await db.saveRegistrations(campus, registrations); + console.log('[报名·' + CAMPUS_NAMES[campus] + '] ' + name + ' - ' + period + ' - ' + teacher); + return res.json({ ok: true, msg: '报名成功' }); + } catch (err) { + console.error('[报名错误]', err); + return res.json({ ok: false, msg: '服务器错误,请稍后再试' }); + } +} + +// 获取报名列表 +async function handleGetRegistrations(req, res, campus) { + const password = req.query.password || req.headers['x-admin-password']; + if (!db.verifyPassword(password)) { + return res.status(401).json({ ok: false, msg: '密码错误' }); + } + + try { + const registrations = await db.getRegistrations(campus); + return res.json({ ok: true, data: registrations }); + } catch (err) { + console.error('[查询错误]', err); + return res.status(500).json({ ok: false, msg: '服务器错误' }); + } +} + +// 获取统计 +async function handleGetStats(req, res, campus) { + const password = req.query.password || req.headers['x-admin-password']; + if (!db.verifyPassword(password)) { + return res.status(401).json({ ok: false, msg: '密码错误' }); + } + + try { + const registrations = await db.getRegistrations(campus); + const periodStats = {}; + const teacherStats = {}; + + registrations.forEach(function(r) { + periodStats[r.period] = (periodStats[r.period] || 0) + 1; + teacherStats[r.teacher] = (teacherStats[r.teacher] || 0) + 1; + }); + + return res.json({ + ok: true, + data: { + total: registrations.length, + byPeriod: periodStats, + byTeacher: teacherStats + } + }); + } catch (err) { + console.error('[统计错误]', err); + return res.status(500).json({ ok: false, msg: '服务器错误' }); + } +} + +// 删除报名 +async function handleDeleteRegistration(req, res, campus) { + const password = req.query.password || req.headers['x-admin-password']; + if (!db.verifyPassword(password)) { + return res.status(401).json({ ok: false, msg: '密码错误' }); + } + + const idStr = req.query.path ? req.query.path[1] : null; + if (!idStr) { + return res.status(400).json({ ok: false, msg: '缺少 ID' }); + } + + try { + const registrations = await db.getRegistrations(campus); + const id = parseInt(idStr); + const filtered = registrations.filter(function(r) { return r.id !== id; }); + await db.saveRegistrations(campus, filtered); + return res.json({ ok: true, msg: '删除成功' }); + } catch (err) { + console.error('[删除错误]', err); + return res.status(500).json({ ok: false, msg: '服务器错误' }); + } +} diff --git a/api/_db.js b/api/_db.js new file mode 100644 index 0000000..de90391 --- /dev/null +++ b/api/_db.js @@ -0,0 +1,44 @@ +// Vercel KV 存储,带内存回退(首次部署未配置 KV 时也能运行) +let kv = null; +try { + const mod = require('@vercel/kv'); + kv = mod.kv; +} catch (e) { + // KV 未安装,使用内存回退 +} + +const ADMIN_PASSWORD = 'admin123'; + +// 内存存储(KV 不可用时的回退方案) +const memoryStore = {}; + +async function getRegistrations(campus) { + if (kv) { + return await kv.get(campus + ':registrations') || []; + } + return memoryStore[campus] || []; +} + +async function saveRegistrations(campus, data) { + if (kv) { + await kv.set(campus + ':registrations', data); + } else { + memoryStore[campus] = data; + } +} + +async function getNextId(campus) { + if (kv) { + const id = await kv.get(campus + ':nextId') || 1; + await kv.set(campus + ':nextId', id + 1); + return id; + } + if (!memoryStore[campus + ':nextId']) memoryStore[campus + ':nextId'] = 1; + return memoryStore[campus + ':nextId']++; +} + +function verifyPassword(password) { + return password === ADMIN_PASSWORD; +} + +module.exports = { getRegistrations, saveRegistrations, getNextId, verifyPassword }; diff --git a/package.json b/package.json index 2c4e7b4..c0f0ab5 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "type": "commonjs", "dependencies": { "express": "^4.22.2", - "sql.js": "^1.14.1" + "sql.js": "^1.14.1", + "@vercel/kv": "^3.0.1" } } diff --git a/public/beijiao/admin.html b/public/beijiaonct/admin.html similarity index 100% rename from public/beijiao/admin.html rename to public/beijiaonct/admin.html diff --git a/public/beijiao/css/style.css b/public/beijiaonct/css/style.css similarity index 100% rename from public/beijiao/css/style.css rename to public/beijiaonct/css/style.css diff --git a/public/beijiao/index.html b/public/beijiaonct/index.html similarity index 100% rename from public/beijiao/index.html rename to public/beijiaonct/index.html diff --git a/public/beijiao/js/app.js b/public/beijiaonct/js/app.js similarity index 100% rename from public/beijiao/js/app.js rename to public/beijiaonct/js/app.js diff --git a/public/chencun/admin.html b/public/chencunnct/admin.html similarity index 100% rename from public/chencun/admin.html rename to public/chencunnct/admin.html diff --git a/public/chencun/css/style.css b/public/chencunnct/css/style.css similarity index 100% rename from public/chencun/css/style.css rename to public/chencunnct/css/style.css diff --git a/public/chencun/index.html b/public/chencunnct/index.html similarity index 100% rename from public/chencun/index.html rename to public/chencunnct/index.html diff --git a/public/chencun/js/app.js b/public/chencunnct/js/app.js similarity index 100% rename from public/chencun/js/app.js rename to public/chencunnct/js/app.js diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..47e9ead --- /dev/null +++ b/public/index.html @@ -0,0 +1,54 @@ + + + + + + NCT 集训报名 · 选择校区 + + + +
+

🏫 选择校区

+

请选择您所在的校区

+ +
+ + diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..b982ca6 --- /dev/null +++ b/vercel.json @@ -0,0 +1,6 @@ +{ + "rewrites": [ + { "source": "/chencunnct/api/:path*", "destination": "/api/chencunnct/:path*" }, + { "source": "/beijiaonct/api/:path*", "destination": "/api/beijiaonct/:path*" } + ] +}