- 管理台:活动设置、时长登记、用户管理、数据导出(Excel/PNG) - 用户端:姓名查询个人义工时长与活动记录 - 技术栈:React + Vite + TailwindCSS / Node.js + Express + sql.js (SQLite) - 支持 Docker 部署,附带 Gitea Actions 自动部署工作流 - 响应式界面,兼容移动端与 PC 端 - 默认管理员账号:admin / admin123
135 lines
5.2 KiB
TypeScript
135 lines
5.2 KiB
TypeScript
import { Router, Request, Response } from 'express';
|
|
import { getDb } from '../db';
|
|
import { authMiddleware, AuthRequest } from '../middleware/auth';
|
|
|
|
const router = Router();
|
|
|
|
router.get('/', (req: Request, res: Response) => {
|
|
const db = getDb();
|
|
const list = db.prepareCompat(`
|
|
SELECT a.*,
|
|
GROUP_CONCAT(g.name) AS groups_name,
|
|
GROUP_CONCAT(g.id) AS groups_id
|
|
FROM activities a
|
|
LEFT JOIN activity_groups ag ON a.id = ag.activity_id
|
|
LEFT JOIN work_groups g ON ag.group_id = g.id
|
|
GROUP BY a.id
|
|
ORDER BY a.start_time DESC
|
|
`).all().map((item: any) => ({
|
|
...item,
|
|
groups: (item.groups_id ? String(item.groups_id).split(',').map(Number) : []),
|
|
groups_name_list: (item.groups_name ? String(item.groups_name).split(',') : []),
|
|
}));
|
|
res.json(list);
|
|
});
|
|
|
|
router.get('/:id', (req: Request, res: Response) => {
|
|
const id = Number(req.params.id);
|
|
const db = getDb();
|
|
const act = db.prepareCompat('SELECT * FROM activities WHERE id = ?').get(id) as any;
|
|
if (!act) return res.status(404).json({ error: '活动不存在' });
|
|
const groups = db.prepareCompat(`
|
|
SELECT g.id, g.name FROM activity_groups ag
|
|
JOIN work_groups g ON ag.group_id = g.id
|
|
WHERE ag.activity_id = ?
|
|
`).all(id);
|
|
res.json({ ...act, groups: groups.map((g: any) => g.id), groups_name_list: groups.map((g: any) => g.name) });
|
|
});
|
|
|
|
router.post('/', authMiddleware, (req: AuthRequest, res: Response) => {
|
|
const { title, content, start_time, end_time, groups } = req.body || {};
|
|
if (!title || !String(title).trim()) return res.status(400).json({ error: '活动标题不能为空' });
|
|
if (!start_time || !end_time) return res.status(400).json({ error: '活动时间不能为空' });
|
|
|
|
const db = getDb();
|
|
try {
|
|
db.execCompat('BEGIN');
|
|
const info = db.prepareCompat('INSERT INTO activities (title, content, start_time, end_time) VALUES (?,?,?,?)')
|
|
.run(String(title).trim(), content || '', start_time, end_time);
|
|
const activityId = Number(info.lastInsertRowid);
|
|
const insertAG = db.prepareCompat('INSERT OR IGNORE INTO activity_groups (activity_id, group_id) VALUES (?,?)');
|
|
if (Array.isArray(groups)) {
|
|
for (const gid of groups) insertAG.run(activityId, Number(gid));
|
|
}
|
|
db.execCompat('COMMIT');
|
|
db.save();
|
|
res.json({ id: activityId });
|
|
} catch (e: any) {
|
|
try { db.execCompat('ROLLBACK'); } catch {}
|
|
res.status(500).json({ error: e.message });
|
|
}
|
|
});
|
|
|
|
router.put('/:id', authMiddleware, (req: AuthRequest, res: Response) => {
|
|
const id = Number(req.params.id);
|
|
const { title, content, start_time, end_time, groups } = req.body || {};
|
|
if (!title || !String(title).trim()) return res.status(400).json({ error: '活动标题不能为空' });
|
|
if (!start_time || !end_time) return res.status(400).json({ error: '活动时间不能为空' });
|
|
|
|
const db = getDb();
|
|
try {
|
|
db.execCompat('BEGIN');
|
|
db.prepareCompat('UPDATE activities SET title=?, content=?, start_time=?, end_time=? WHERE id=?')
|
|
.run(String(title).trim(), content || '', start_time, end_time, id);
|
|
db.prepareCompat('DELETE FROM activity_groups WHERE activity_id = ?').run(id);
|
|
const insertAG = db.prepareCompat('INSERT OR IGNORE INTO activity_groups (activity_id, group_id) VALUES (?,?)');
|
|
if (Array.isArray(groups)) {
|
|
for (const gid of groups) insertAG.run(id, Number(gid));
|
|
}
|
|
db.execCompat('COMMIT');
|
|
db.save();
|
|
res.json({ ok: true });
|
|
} catch (e: any) {
|
|
try { db.execCompat('ROLLBACK'); } catch {}
|
|
res.status(500).json({ error: e.message });
|
|
}
|
|
});
|
|
|
|
router.delete('/:id', authMiddleware, (req: AuthRequest, res: Response) => {
|
|
const id = Number(req.params.id);
|
|
const db = getDb();
|
|
db.prepareCompat('DELETE FROM activities WHERE id = ?').run(id);
|
|
db.save();
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
router.get('/:id/participants', (req: Request, res: Response) => {
|
|
const id = Number(req.params.id);
|
|
const db = getDb();
|
|
const act = db.prepareCompat('SELECT * FROM activities WHERE id = ?').get(id);
|
|
if (!act) return res.status(404).json({ error: '活动不存在' });
|
|
|
|
const groupIds = db.prepareCompat('SELECT group_id FROM activity_groups WHERE activity_id = ?').all(id).map((r: any) => r.group_id);
|
|
let users: any[];
|
|
if (groupIds.length === 0) {
|
|
users = db.prepareCompat(`
|
|
SELECT u.id, u.class, u.name, u.volunteer_no, g.name AS group_name
|
|
FROM users u LEFT JOIN work_groups g ON u.group_id = g.id
|
|
ORDER BY u.class, u.name
|
|
`).all();
|
|
} else {
|
|
const ph = groupIds.map(() => '?').join(',');
|
|
users = db.prepareCompat(`
|
|
SELECT u.id, u.class, u.name, u.volunteer_no, g.name AS group_name
|
|
FROM users u
|
|
LEFT JOIN work_groups g ON u.group_id = g.id
|
|
WHERE u.group_id IN (${ph}) OR u.group_id IS NULL
|
|
ORDER BY u.class, u.name
|
|
`).all(...groupIds);
|
|
}
|
|
|
|
const hoursRows = db.prepareCompat('SELECT * FROM hours WHERE activity_id = ?').all(id);
|
|
const hoursMap: Record<number, any> = {};
|
|
for (const h of hoursRows) hoursMap[(h as any).user_id] = h;
|
|
|
|
const list = users.map((u: any) => ({
|
|
...u,
|
|
hours: hoursMap[u.id]?.hours ?? 0,
|
|
remark: hoursMap[u.id]?.remark ?? '',
|
|
hours_id: hoursMap[u.id]?.id ?? null,
|
|
}));
|
|
res.json({ activity: act, list });
|
|
});
|
|
|
|
export default router;
|