feat: 初始化义工时长管理系统
- 管理台:活动设置、时长登记、用户管理、数据导出(Excel/PNG) - 用户端:姓名查询个人义工时长与活动记录 - 技术栈:React + Vite + TailwindCSS / Node.js + Express + sql.js (SQLite) - 支持 Docker 部署,附带 Gitea Actions 自动部署工作流 - 响应式界面,兼容移动端与 PC 端 - 默认管理员账号:admin / admin123
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import initSqlJs, { SqlJsStatic, Database } from 'sql.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
let SQL: SqlJsStatic | null = null;
|
||||
let db: (Database & {
|
||||
prepareCompat(sql: string): StmtCompat;
|
||||
execCompat(sql: string): void;
|
||||
pragmaCompat(expr: string): void;
|
||||
save(): void;
|
||||
}) | null = null;
|
||||
let dbFilePath: string = '';
|
||||
|
||||
interface StmtCompat {
|
||||
run(...params: any[]): { changes: number; lastInsertRowid: number };
|
||||
get(...params: any[]): any;
|
||||
all(...params: any[]): any[];
|
||||
}
|
||||
|
||||
function wrapStmt(db: Database & any, sql: string): StmtCompat {
|
||||
const bindArray = (params: any[]): any[] => {
|
||||
return params.map(p => {
|
||||
if (p === undefined) return null;
|
||||
return p;
|
||||
});
|
||||
};
|
||||
return {
|
||||
run(...params) {
|
||||
const stmt = db.prepare(sql);
|
||||
try {
|
||||
stmt.bind(bindArray(params));
|
||||
stmt.step();
|
||||
const changes = db.getRowsModified();
|
||||
const lastInsertRowid = (db as any).lastInsertRowid?.() ?? 0;
|
||||
return { changes, lastInsertRowid };
|
||||
} finally {
|
||||
stmt.free();
|
||||
}
|
||||
},
|
||||
get(...params) {
|
||||
const stmt = db.prepare(sql);
|
||||
try {
|
||||
stmt.bind(bindArray(params));
|
||||
if (stmt.step()) return stmt.getAsObject();
|
||||
return undefined;
|
||||
} finally {
|
||||
stmt.free();
|
||||
}
|
||||
},
|
||||
all(...params) {
|
||||
const stmt = db.prepare(sql);
|
||||
const rows: any[] = [];
|
||||
try {
|
||||
stmt.bind(bindArray(params));
|
||||
while (stmt.step()) rows.push(stmt.getAsObject());
|
||||
return rows;
|
||||
} finally {
|
||||
stmt.free();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function initDb() {
|
||||
if (db) return db;
|
||||
SQL = await initSqlJs({
|
||||
locateFile: (file) => {
|
||||
const p = path.join(__dirname, '..', '..', 'node_modules', 'sql.js', 'dist', file);
|
||||
if (fs.existsSync(p)) return p;
|
||||
return path.join(require.resolve('sql.js').replace(/index\.js$/, 'dist'), file);
|
||||
},
|
||||
});
|
||||
|
||||
const dataDir = path.join(__dirname, '..', '..', 'data');
|
||||
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
||||
dbFilePath = path.join(dataDir, 'volunteer.db');
|
||||
|
||||
let rawDb: Database;
|
||||
if (fs.existsSync(dbFilePath)) {
|
||||
const buf = fs.readFileSync(dbFilePath);
|
||||
rawDb = new SQL.Database(new Uint8Array(buf));
|
||||
} else {
|
||||
rawDb = new SQL.Database();
|
||||
}
|
||||
|
||||
const extended = rawDb as any;
|
||||
extended.prepareCompat = function (sql: string) { return wrapStmt(extended, sql); };
|
||||
extended.execCompat = function (sql: string) { extended.exec(sql); };
|
||||
extended.pragmaCompat = function (expr: string) {
|
||||
if (/foreign_keys\s*=\s*ON/i.test(expr)) {
|
||||
// sql.js 默认开启 FK?尝试显式设置
|
||||
try { extended.exec('PRAGMA foreign_keys = ON;'); } catch {}
|
||||
}
|
||||
};
|
||||
extended.save = function () {
|
||||
const data = extended.export();
|
||||
fs.writeFileSync(dbFilePath, Buffer.from(data));
|
||||
};
|
||||
|
||||
db = extended;
|
||||
const d = extended;
|
||||
|
||||
const schemaPath = path.join(__dirname, 'schema.sql');
|
||||
const schema = fs.readFileSync(schemaPath, 'utf-8');
|
||||
try { d.execCompat(schema); d.save(); } catch (e) { console.error('Schema init error', e); }
|
||||
|
||||
const adminCount = d.prepareCompat('SELECT COUNT(*) as c FROM admins').get();
|
||||
if (!adminCount || adminCount.c === 0) {
|
||||
const hash = bcrypt.hashSync('admin123', 10);
|
||||
d.prepareCompat('INSERT INTO admins (username, password_hash) VALUES (?, ?)').run('admin', hash);
|
||||
d.save();
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
export function getDb() {
|
||||
if (!db) throw new Error('DB not initialized, call initDb() first');
|
||||
return db;
|
||||
}
|
||||
|
||||
export type DbInstance = NonNullable<typeof db>;
|
||||
@@ -0,0 +1,50 @@
|
||||
CREATE TABLE IF NOT EXISTS work_groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
class TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
volunteer_no TEXT,
|
||||
group_id INTEGER,
|
||||
FOREIGN KEY (group_id) REFERENCES work_groups(id) ON DELETE SET NULL,
|
||||
UNIQUE(class, name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activities (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT,
|
||||
start_time TEXT NOT NULL,
|
||||
end_time TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_groups (
|
||||
activity_id INTEGER NOT NULL,
|
||||
group_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (activity_id, group_id),
|
||||
FOREIGN KEY (activity_id) REFERENCES activities(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (group_id) REFERENCES work_groups(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hours (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
activity_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
hours REAL NOT NULL DEFAULT 0,
|
||||
remark TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
||||
FOREIGN KEY (activity_id) REFERENCES activities(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
UNIQUE(activity_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
Reference in New Issue
Block a user