diff --git a/backend/backups/database-2025-12-22T15-15-32-013Z.sqlite b/backend/backups/database-2025-12-22T15-15-32-013Z.sqlite
new file mode 100644
index 0000000..58821b7
Binary files /dev/null and b/backend/backups/database-2025-12-22T15-15-32-013Z.sqlite differ
diff --git a/backend/config/database.js b/backend/config/database.js
index 4d99fba..515fdc5 100644
--- a/backend/config/database.js
+++ b/backend/config/database.js
@@ -1,36 +1,64 @@
-const mysql = require('mysql2/promise');
-require('dotenv').config();
+const sqlite3 = require('sqlite3').verbose();
+const path = require('path');
+const fs = require('fs');
-const pool = mysql.createPool({
- host: process.env.DB_HOST || 'localhost',
- user: process.env.DB_USER || 'root',
- password: process.env.DB_PASSWORD || '123456',
- database: process.env.DB_NAME || 'score_management',
- waitForConnections: true,
- connectionLimit: 10,
- queueLimit: 0
-});
+const dbPath = path.resolve(__dirname, '../database.sqlite');
+const db = new sqlite3.Database(dbPath);
-// 封装基本查询方法
-const query = async (sql, params) => {
- try {
- const [rows] = await pool.execute(sql, params);
- return rows;
- } catch (error) {
- console.error('Database query error:', error);
- throw error;
- }
+// Promisify query method
+const query = (sql, params = []) => {
+ return new Promise((resolve, reject) => {
+ // Handle SELECT vs INSERT/UPDATE/DELETE
+ const trimmedSql = sql.trim().toUpperCase();
+ if (trimmedSql.startsWith('SELECT') || trimmedSql.startsWith('PRAGMA')) {
+ db.all(sql, params, (err, rows) => {
+ if (err) {
+ console.error('Database query error:', err);
+ reject(err);
+ } else {
+ resolve(rows);
+ }
+ });
+ } else {
+ db.run(sql, params, function(err) {
+ if (err) {
+ console.error('Database execution error:', err);
+ reject(err);
+ } else {
+ // Normalize result to look like MySQL result
+ // this.lastID, this.changes
+ resolve({
+ insertId: this.lastID,
+ affectedRows: this.changes,
+ warningStatus: 0
+ });
+ }
+ });
+ }
+ });
+};
+
+// Mock Pool object for compatibility
+const pool = {
+ query: query,
+ execute: (sql, params) => query(sql, params).then(res => [res]), // Wrap in array for mysql2 compatibility
+ getConnection: () => Promise.resolve({
+ release: () => {},
+ query: query,
+ execute: (sql, params) => query(sql, params).then(res => [res]),
+ beginTransaction: () => query('BEGIN TRANSACTION'),
+ commit: () => query('COMMIT'),
+ rollback: () => query('ROLLBACK')
+ })
};
-// 测试连接
const testConnection = async () => {
try {
- const connection = await pool.getConnection();
- console.log('数据库连接成功');
- connection.release();
+ await query('SELECT 1');
+ console.log('SQLite 数据库连接成功');
return true;
} catch (error) {
- console.error('数据库连接失败:', error.message);
+ console.error('SQLite 数据库连接失败:', error.message);
return false;
}
};
@@ -39,4 +67,4 @@ module.exports = {
pool,
query,
testConnection
-};
\ No newline at end of file
+};
diff --git a/backend/controllers/adminController.js b/backend/controllers/adminController.js
index b9af3fc..2ec0c18 100644
--- a/backend/controllers/adminController.js
+++ b/backend/controllers/adminController.js
@@ -2,28 +2,25 @@ const AdminService = require('../services/adminService');
const { success, error } = require('../utils/response');
class AdminController {
+ static async getStats(req, res) {
+ try {
+ const stats = await AdminService.getStats();
+ success(res, stats, '获取统计信息成功');
+ } catch (err) {
+ console.error('Get Stats Error:', err);
+ error(res, '服务器错误');
+ }
+ }
+
static async getUsers(req, res) {
try {
const result = await AdminService.getUsers(req.query);
- success(res, result.data, '获取成功');
- // 注意:原来的响应结构是 { success, data, pagination }
- // 现在的 success 工具函数结构是 { success, message, data }
- // 我们可以稍微调整 success 调用,或者让前端适应
- // 为了兼容性,这里手动返回
- /*
res.json({
success: true,
+ message: '获取成功',
data: result.data,
pagination: result.pagination
});
- */
- // 或者修改 response.js 支持 extra 字段,这里简单处理:
- res.json({
- success: true,
- data: result.data,
- pagination: result.pagination
- });
-
} catch (err) {
console.error('Get Users Error:', err);
error(res, '服务器错误');
@@ -38,7 +35,10 @@ class AdminController {
return error(res, '请填写所有必填字段', 400);
}
- await AdminService.createUser(req.body);
+ await AdminService.createUser(req.body, {
+ user_id: req.session.user.id,
+ ip: req.ip
+ });
success(res, null, '创建用户成功');
} catch (err) {
if (err.message === '用户ID已存在') {
@@ -48,6 +48,287 @@ class AdminController {
error(res, '服务器错误');
}
}
+
+ static async updateUser(req, res) {
+ try {
+ const { id } = req.params;
+ const updateData = req.body;
+
+ await AdminService.updateUser(id, updateData, {
+ user_id: req.session.user.id,
+ ip: req.ip
+ });
+ success(res, null, '更新用户成功');
+ } catch (err) {
+ console.error('Update User Error:', err);
+ error(res, '服务器错误');
+ }
+ }
+
+ static async deleteUser(req, res) {
+ try {
+ const { id } = req.params;
+ await AdminService.deleteUser(id, {
+ user_id: req.session.user.id,
+ ip: req.ip
+ });
+ success(res, null, '删除用户成功');
+ } catch (err) {
+ console.error('Delete User Error:', err);
+ error(res, '服务器错误');
+ }
+ }
+
+ // Student Management
+ static async getStudents(req, res) {
+ try {
+ const result = await AdminService.getStudents(req.query);
+ res.json({
+ success: true,
+ message: '获取学生列表成功',
+ data: result.data,
+ pagination: result.pagination
+ });
+ } catch (err) {
+ console.error(err);
+ error(res, '服务器错误');
+ }
+ }
+
+ static async createStudent(req, res) {
+ try {
+ if (!req.body.id || !req.body.name) return error(res, 'ID和姓名必填', 400);
+ await AdminService.createStudent(req.body, {
+ user_id: req.session.user.id,
+ ip: req.ip
+ });
+ success(res, null, '创建学生成功');
+ } catch (err) {
+ console.error(err);
+ error(res, err.message || '创建失败', 400);
+ }
+ }
+
+ static async updateStudent(req, res) {
+ try {
+ await AdminService.updateStudent(req.params.id, req.body, {
+ user_id: req.session.user.id,
+ ip: req.ip
+ });
+ success(res, null, '更新学生成功');
+ } catch (err) {
+ console.error(err);
+ error(res, '更新失败');
+ }
+ }
+
+ static async deleteStudent(req, res) {
+ try {
+ await AdminService.deleteStudent(req.params.id, {
+ user_id: req.session.user.id,
+ ip: req.ip
+ });
+ success(res, null, '删除学生成功');
+ } catch (err) {
+ console.error(err);
+ error(res, '删除失败');
+ }
+ }
+
+ // Teacher Management
+ static async getTeachers(req, res) {
+ try {
+ const result = await AdminService.getTeachers(req.query);
+ res.json({
+ success: true,
+ message: '获取教师列表成功',
+ data: result.data,
+ pagination: result.pagination
+ });
+ } catch (err) {
+ console.error(err);
+ error(res, '服务器错误');
+ }
+ }
+
+ static async createTeacher(req, res) {
+ try {
+ if (!req.body.id || !req.body.name) return error(res, '工号和姓名必填', 400);
+ await AdminService.createTeacher(req.body, {
+ user_id: req.session.user.id,
+ ip: req.ip
+ });
+ success(res, null, '创建教师成功');
+ } catch (err) {
+ console.error(err);
+ error(res, err.message || '创建失败', 400);
+ }
+ }
+
+ static async updateTeacher(req, res) {
+ try {
+ await AdminService.updateTeacher(req.params.id, req.body, {
+ user_id: req.session.user.id,
+ ip: req.ip
+ });
+ success(res, null, '更新教师成功');
+ } catch (err) {
+ console.error(err);
+ error(res, '更新失败');
+ }
+ }
+
+ static async deleteTeacher(req, res) {
+ try {
+ await AdminService.deleteTeacher(req.params.id, {
+ user_id: req.session.user.id,
+ ip: req.ip
+ });
+ success(res, null, '删除教师成功');
+ } catch (err) {
+ console.error(err);
+ error(res, '删除失败');
+ }
+ }
+
+ // Grade Statistics
+ static async getGradeStats(req, res) {
+ try {
+ const stats = await AdminService.getGradeStats();
+ success(res, stats, '获取成绩统计成功');
+ } catch (err) {
+ console.error(err);
+ error(res, '服务器错误');
+ }
+ }
+
+ // System Settings
+ static async getSettings(req, res) {
+ try {
+ const settings = await AdminService.getSettings();
+ success(res, settings);
+ } catch (err) {
+ console.error(err);
+ error(res, '服务器错误');
+ }
+ }
+
+ static async saveSettings(req, res) {
+ try {
+ await AdminService.saveSettings(req.body, {
+ user_id: req.session.user.id,
+ ip: req.ip
+ });
+ success(res, null, '保存设置成功');
+ } catch (err) {
+ console.error(err);
+ error(res, '保存失败');
+ }
+ }
+
+ // Data Maintenance
+ static async backupDatabase(req, res) {
+ try {
+ const result = await AdminService.backupDatabase({
+ user_id: req.session.user.id,
+ ip: req.ip
+ });
+ success(res, result, '数据库备份成功');
+ } catch (err) {
+ console.error(err);
+ error(res, '备份失败');
+ }
+ }
+
+ static async clearCache(req, res) {
+ try {
+ await AdminService.clearCache({
+ user_id: req.session.user.id,
+ ip: req.ip
+ });
+ success(res, null, '缓存已清理');
+ } catch (err) {
+ console.error(err);
+ error(res, '清理失败');
+ }
+ }
+
+ static async resetStudentPasswords(req, res) {
+ try {
+ await AdminService.resetStudentPasswords({
+ user_id: req.session.user.id,
+ ip: req.ip
+ });
+ success(res, null, '所有学生密码已重置为 123456');
+ } catch (err) {
+ console.error(err);
+ error(res, '重置失败');
+ }
+ }
+
+ // Operation Logs
+ static async getOperationLogs(req, res) {
+ try {
+ const logs = await AdminService.getLogs(req.query);
+ success(res, logs);
+ } catch (err) {
+ console.error(err);
+ error(res, '获取日志失败');
+ }
+ }
+
+ // Data Export
+ static async exportStudents(req, res) {
+ try {
+ const data = await AdminService.exportStudents();
+ const csv = jsonToCsv(data, ['id', 'name', 'class', 'major', 'grade', 'contact_info']);
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8');
+ res.setHeader('Content-Disposition', 'attachment; filename=students.csv');
+ res.send(csv);
+ } catch (err) {
+ console.error(err);
+ error(res, '导出失败');
+ }
+ }
+
+ static async exportTeachers(req, res) {
+ try {
+ const data = await AdminService.exportTeachers();
+ const csv = jsonToCsv(data, ['id', 'name', 'department', 'title', 'contact_info', 'created_at']);
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8');
+ res.setHeader('Content-Disposition', 'attachment; filename=teachers.csv');
+ res.send(csv);
+ } catch (err) {
+ console.error(err);
+ error(res, '导出失败');
+ }
+ }
+
+ static async exportGrades(req, res) {
+ try {
+ const data = await AdminService.exportGrades();
+ const csv = jsonToCsv(data, ['student_id', 'student_name', 'course_code', 'course_name', 'total_score', 'grade_point', 'grade_level', 'teacher_name']);
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8');
+ res.setHeader('Content-Disposition', 'attachment; filename=grades.csv');
+ res.send(csv);
+ } catch (err) {
+ console.error(err);
+ error(res, '导出失败');
+ }
+ }
+}
+
+// Helper function
+function jsonToCsv(data, fields) {
+ if (!data || data.length === 0) return '';
+ const header = fields.join(',') + '\n';
+ const rows = data.map(row => {
+ return fields.map(field => {
+ const val = row[field] === null || row[field] === undefined ? '' : row[field];
+ return `"${String(val).replace(/"/g, '""')}"`;
+ }).join(',');
+ }).join('\n');
+ return '\ufeff' + header + rows; // Add BOM for Excel compatibility
}
module.exports = AdminController;
\ No newline at end of file
diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js
index 4589135..9a9a8c9 100644
--- a/backend/controllers/authController.js
+++ b/backend/controllers/authController.js
@@ -85,6 +85,26 @@ class AuthController {
error(res, '服务器错误');
}
}
+
+ static async updateProfile(req, res) {
+ try {
+ const userId = req.session.user.id;
+ const updateData = req.body;
+
+ const updatedUser = await AuthService.updateProfile(userId, updateData);
+
+ // 更新 Session 中的用户信息
+ req.session.user = {
+ ...req.session.user,
+ ...updatedUser
+ };
+
+ success(res, { user: req.session.user }, '资料更新成功');
+ } catch (err) {
+ console.error('Update Profile Error:', err);
+ error(res, '服务器错误');
+ }
+ }
}
module.exports = AuthController;
\ No newline at end of file
diff --git a/backend/controllers/teacherController.js b/backend/controllers/teacherController.js
index f1fc902..711b1d3 100644
--- a/backend/controllers/teacherController.js
+++ b/backend/controllers/teacherController.js
@@ -18,6 +18,68 @@ class TeacherController {
}
}
+ static async getClasses(req, res) {
+ try {
+ const classes = await TeacherService.getClasses();
+ success(res, { classes });
+ } catch (err) {
+ console.error('Get Classes Error:', err);
+ error(res, '服务器错误');
+ }
+ }
+
+ static async getMyClasses(req, res) {
+ try {
+ const teacherId = req.session.user.id;
+ const classes = await TeacherService.getTeacherClasses(teacherId);
+ success(res, { classes });
+ } catch (err) {
+ console.error('Get My Classes Error:', err);
+ error(res, '服务器错误');
+ }
+ }
+
+ static async createCourse(req, res) {
+ try {
+ const teacherId = req.session.user.id;
+ const courseId = await TeacherService.createCourse(teacherId, req.body);
+ success(res, { courseId }, '课程创建成功');
+ } catch (err) {
+ console.error('Create Course Error:', err);
+ error(res, '服务器错误');
+ }
+ }
+
+ static async updateCourse(req, res) {
+ try {
+ const teacherId = req.session.user.id;
+ const courseId = req.params.id;
+ await TeacherService.updateCourse(teacherId, courseId, req.body);
+ success(res, null, '课程更新成功');
+ } catch (err) {
+ if (err.message === '无权修改该课程或课程不存在') {
+ return error(res, err.message, 403);
+ }
+ console.error('Update Course Error:', err);
+ error(res, '服务器错误');
+ }
+ }
+
+ static async getGrades(req, res) {
+ try {
+ const teacherId = req.session.user.id;
+ const filters = {
+ courseId: req.query.courseId,
+ studentName: req.query.studentName
+ };
+ const grades = await TeacherService.getGrades(teacherId, filters);
+ success(res, { grades });
+ } catch (err) {
+ console.error('Get Grades Error:', err);
+ error(res, '服务器错误');
+ }
+ }
+
static async addScore(req, res) {
try {
const teacherId = req.session.user.id;
diff --git a/backend/database.sqlite b/backend/database.sqlite
new file mode 100644
index 0000000..8b85f52
Binary files /dev/null and b/backend/database.sqlite differ
diff --git a/backend/init_db.js b/backend/init_db.js
new file mode 100644
index 0000000..6fc978a
--- /dev/null
+++ b/backend/init_db.js
@@ -0,0 +1,343 @@
+const sqlite3 = require('sqlite3').verbose();
+const path = require('path');
+const bcrypt = require('bcryptjs');
+
+const dbPath = path.resolve(__dirname, 'database.sqlite');
+const db = new sqlite3.Database(dbPath);
+
+const run = (sql, params = []) => {
+ return new Promise((resolve, reject) => {
+ db.run(sql, params, function(err) {
+ if (err) reject(err);
+ else resolve(this);
+ });
+ });
+};
+
+const insertUser = async (user) => {
+ await run(
+ 'INSERT INTO users (id, name, password, role, class) VALUES (?, ?, ?, ?, ?)',
+ [user.id, user.name, user.password, user.role, user.class]
+ );
+};
+
+const insertStudent = async (student) => {
+ await run(
+ 'INSERT INTO students (id, name, class, major, grade, contact_info) VALUES (?, ?, ?, ?, ?, ?)',
+ [student.id, student.name, student.class, student.major, student.grade, student.contact_info]
+ );
+};
+
+const insertClass = async (cls) => {
+ return await run(
+ 'INSERT INTO classes (class_name, grade, major, teacher_id) VALUES (?, ?, ?, ?)',
+ [cls.class_name, cls.grade, cls.major, cls.teacher_id]
+ );
+};
+
+const insertCourse = async (course) => {
+ return await run(
+ 'INSERT INTO courses (course_code, course_name, credit, teacher_id, class_id, semester, academic_year, category) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
+ [course.course_code, course.course_name, course.credit, course.teacher_id, course.class_id, course.semester, course.academic_year, course.category]
+ );
+};
+
+const insertGrade = async (grade) => {
+ await run(
+ `INSERT INTO grades (
+ student_id, course_id, teacher_id,
+ usual_score, midterm_score, final_score, total_score,
+ grade_point, grade_level, remark
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ [
+ grade.student_id, grade.course_id, grade.teacher_id,
+ grade.usual_score, grade.midterm_score, grade.final_score, grade.total_score,
+ grade.grade_point, grade.grade_level, grade.remark
+ ]
+ );
+};
+
+// Helper to calculate grade point
+const calculateGradePoint = (score) => {
+ if (score >= 90) return 4.0;
+ if (score >= 85) return 3.7;
+ if (score >= 82) return 3.3;
+ if (score >= 78) return 3.0;
+ if (score >= 75) return 2.7;
+ if (score >= 72) return 2.3;
+ if (score >= 68) return 2.0;
+ if (score >= 64) return 1.5;
+ if (score >= 60) return 1.0;
+ return 0.0;
+};
+
+const calculateGradeLevel = (score) => {
+ if (score >= 90) return 'A';
+ if (score >= 80) return 'B';
+ if (score >= 70) return 'C';
+ if (score >= 60) return 'D';
+ return 'F';
+};
+
+const init = async () => {
+ console.log('开始初始化 SQLite 数据库...');
+ const hashedPassword = await bcrypt.hash('123456', 10);
+
+ try {
+ // Drop tables
+ await run('DROP TABLE IF EXISTS grades');
+ await run('DROP TABLE IF EXISTS courses');
+ await run('DROP TABLE IF EXISTS classes');
+ await run('DROP TABLE IF EXISTS students');
+ await run('DROP TABLE IF EXISTS users');
+ await run('DROP TABLE IF EXISTS operation_logs');
+
+ // Create tables
+ console.log('创建表结构...');
+
+ await run(`
+ CREATE TABLE users (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ password TEXT NOT NULL,
+ role TEXT NOT NULL,
+ class TEXT,
+ created_at TEXT DEFAULT (datetime('now', 'localtime'))
+ )
+ `);
+
+ await run(`
+ CREATE TABLE students (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ class TEXT,
+ major TEXT,
+ grade TEXT,
+ contact_info TEXT,
+ FOREIGN KEY (id) REFERENCES users(id) ON DELETE CASCADE
+ )
+ `);
+
+ await run(`
+ CREATE TABLE classes (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ class_name TEXT NOT NULL,
+ grade TEXT,
+ major TEXT,
+ teacher_id TEXT,
+ created_at TEXT DEFAULT (datetime('now', 'localtime'))
+ )
+ `);
+
+ await run(`
+ CREATE TABLE courses (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ course_code TEXT UNIQUE NOT NULL,
+ course_name TEXT NOT NULL,
+ credit REAL DEFAULT 2.0,
+ teacher_id TEXT NOT NULL,
+ class_id INTEGER NOT NULL,
+ semester TEXT,
+ academic_year TEXT,
+ category TEXT,
+ created_at TEXT DEFAULT (datetime('now', 'localtime'))
+ )
+ `);
+
+ await run(`
+ CREATE TABLE grades (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ student_id TEXT NOT NULL,
+ course_id INTEGER NOT NULL,
+ teacher_id TEXT NOT NULL,
+ usual_score REAL,
+ midterm_score REAL,
+ final_score REAL,
+ total_score REAL,
+ grade_point REAL,
+ grade_level TEXT,
+ remark TEXT,
+ created_at TEXT DEFAULT (datetime('now', 'localtime')),
+ UNIQUE(student_id, course_id)
+ )
+ `);
+
+ await run(`
+ CREATE TABLE operation_logs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT,
+ operation_type TEXT,
+ operation_target TEXT,
+ description TEXT,
+ ip_address TEXT,
+ created_at TEXT DEFAULT (datetime('now', 'localtime'))
+ )
+ `);
+
+ console.log('生成基础数据...');
+
+ // 1. Admin
+ await insertUser({
+ id: 'admin',
+ name: '系统管理员',
+ password: hashedPassword,
+ role: 'admin',
+ class: null
+ });
+
+ // 2. Teachers (20 teachers)
+ const teachers = [];
+ for (let i = 1; i <= 20; i++) {
+ const id = `T${1000 + i}`;
+ const name = `教师${String.fromCharCode(65 + (i % 26))}${i}`;
+ await insertUser({
+ id,
+ name,
+ password: hashedPassword,
+ role: 'teacher',
+ class: null
+ });
+ teachers.push(id);
+ }
+
+ // 3. Classes (10 classes)
+ const majors = ['软件工程', '计算机科学', '信息管理'];
+ const classIds = [];
+ const classes = [];
+
+ for (let i = 1; i <= 10; i++) {
+ const major = majors[i % 3];
+ const gradeYear = 2021 + Math.floor((i-1)/5); // 2021, 2022
+ const className = `${major.substr(0, 2)}${gradeYear}${String(i).padStart(2, '0')}`;
+ const teacherId = teachers[i % teachers.length];
+
+ const result = await insertClass({
+ class_name: className,
+ grade: String(gradeYear),
+ major: major,
+ teacher_id: teacherId
+ });
+ classIds.push(result.lastID);
+ classes.push({ id: result.lastID, name: className, year: gradeYear, major });
+ }
+
+ // 4. Students (50 per class -> 500 students)
+ console.log('生成学生数据...');
+ const students = [];
+ for (let clsIdx = 0; clsIdx < classes.length; clsIdx++) {
+ const cls = classes[clsIdx];
+ for (let i = 1; i <= 50; i++) {
+ const id = `${cls.year}${String(clsIdx + 1).padStart(2, '0')}${String(i).padStart(3, '0')}`;
+ const name = `学生${cls.name.substr(0,1)}${i}`;
+
+ await insertUser({
+ id,
+ name,
+ password: hashedPassword,
+ role: 'student',
+ class: cls.name
+ });
+
+ await insertStudent({
+ id,
+ name,
+ class: cls.name,
+ major: cls.major,
+ grade: String(cls.year),
+ contact_info: `13800${id.substr(0, 6)}`
+ });
+ students.push({ id, classId: cls.id });
+ }
+ }
+
+ // 5. Courses and Grades
+ console.log('生成课程和成绩数据...');
+ const courseNames = [
+ { name: '高等数学', credit: 4, category: '必修' },
+ { name: '大学英语', credit: 3, category: '必修' },
+ { name: '程序设计基础', credit: 4, category: '必修' },
+ { name: '数据结构', credit: 4, category: '必修' },
+ { name: '操作系统', credit: 3, category: '必修' },
+ { name: '计算机网络', credit: 3, category: '必修' },
+ { name: '数据库原理', credit: 3, category: '必修' },
+ { name: '软件工程导论', credit: 2, category: '必修' },
+ { name: 'Web开发技术', credit: 3, category: '选修' },
+ { name: '人工智能基础', credit: 2, category: '选修' },
+ { name: '大数据分析', credit: 2, category: '选修' },
+ { name: '音乐鉴赏', credit: 1, category: '通识' },
+ { name: '心理健康', credit: 1, category: '通识' },
+ { name: '职业规划', credit: 1, category: '通识' }
+ ];
+
+ const semesters = ['2021-2022-1', '2021-2022-2', '2022-2023-1', '2022-2023-2', '2023-2024-1'];
+
+ for (const cls of classes) {
+ // For each class, assign some courses
+ for (let semIdx = 0; semIdx < semesters.length; semIdx++) {
+ const semester = semesters[semIdx];
+ // Select random 5-8 courses for this semester
+ const semCourses = courseNames.sort(() => 0.5 - Math.random()).slice(0, 6);
+
+ for (const cTemplate of semCourses) {
+ const teacherId = teachers[Math.floor(Math.random() * teachers.length)];
+ const courseCode = `C${cls.id}${semIdx}${Math.floor(Math.random() * 1000)}`;
+
+ const result = await insertCourse({
+ course_code: courseCode,
+ course_name: cTemplate.name,
+ credit: cTemplate.credit,
+ teacher_id: teacherId,
+ class_id: cls.id,
+ semester: semester,
+ academic_year: semester.substring(0, 9),
+ category: cTemplate.category
+ });
+
+ const courseId = result.lastID;
+
+ // Generate grades for students in this class
+ const classStudents = students.filter(s => s.classId === cls.id);
+
+ // Batch insert could be faster, but let's keep it simple for now
+ const stmt = db.prepare(`INSERT INTO grades (
+ student_id, course_id, teacher_id,
+ usual_score, midterm_score, final_score, total_score,
+ grade_point, grade_level, remark
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
+
+ for (const stu of classStudents) {
+ // 90% chance to have a grade
+ if (Math.random() > 0.1) {
+ const totalScore = Math.floor(Math.random() * 40) + 60; // 60-100 mostly
+ // 10% chance to fail
+ const finalTotal = Math.random() > 0.1 ? totalScore : Math.floor(Math.random() * 59);
+
+ stmt.run([
+ stu.id,
+ courseId,
+ teacherId,
+ finalTotal, // usual
+ finalTotal, // midterm
+ finalTotal, // final
+ finalTotal, // total
+ calculateGradePoint(finalTotal),
+ calculateGradeLevel(finalTotal),
+ ''
+ ]);
+ }
+ }
+ stmt.finalize();
+ }
+ }
+ }
+
+ console.log('数据库初始化完成!');
+ db.close();
+
+ } catch (err) {
+ console.error('Initialization failed:', err);
+ process.exit(1);
+ }
+};
+
+init();
diff --git a/backend/models/Course.js b/backend/models/Course.js
index 9dea61f..572252c 100644
--- a/backend/models/Course.js
+++ b/backend/models/Course.js
@@ -2,10 +2,15 @@ const db = require('../config/database');
class Course {
static async findByTeacherId(teacherId) {
- return await db.query(
- 'SELECT * FROM courses WHERE teacher_id = ? ORDER BY course_code',
- [teacherId]
- );
+ const sql = `
+ SELECT c.*, cl.class_name,
+ (SELECT COUNT(*) FROM students s WHERE s.class = cl.class_name) as student_count
+ FROM courses c
+ LEFT JOIN classes cl ON c.class_id = cl.id
+ WHERE c.teacher_id = ?
+ ORDER BY c.course_code
+ `;
+ return await db.query(sql, [teacherId]);
}
static async findById(id) {
@@ -37,6 +42,28 @@ class Course {
`;
return await db.query(sql, [studentId]);
}
+
+ static async getClasses() {
+ return await db.query('SELECT * FROM classes');
+ }
+
+ static async create(data) {
+ const { course_name, course_code, credit, teacher_id, semester, class_id, category } = data;
+ const sql = `INSERT INTO courses (course_name, course_code, credit, teacher_id, semester, class_id, category) VALUES (?, ?, ?, ?, ?, ?, ?)`;
+ const result = await db.query(sql, [course_name, course_code, credit, teacher_id, semester, class_id, category]);
+ return result.insertId;
+ }
+
+ static async update(id, data) {
+ const { course_name, course_code, credit, semester, class_id } = data;
+ const sql = `
+ UPDATE courses
+ SET course_name = ?, course_code = ?, credit = ?, semester = ?, class_id = ?
+ WHERE id = ?
+ `;
+ const result = await db.query(sql, [course_name, course_code, credit, semester, class_id, id]);
+ return result.affectedRows > 0;
+ }
}
module.exports = Course;
diff --git a/backend/models/OperationLog.js b/backend/models/OperationLog.js
new file mode 100644
index 0000000..9233ca2
--- /dev/null
+++ b/backend/models/OperationLog.js
@@ -0,0 +1,28 @@
+const db = require('../config/database');
+
+class OperationLog {
+ static async add(logData) {
+ const { user_id, type, target, description, ip } = logData;
+ const sql = `
+ INSERT INTO operation_logs (user_id, operation_type, operation_target, description, ip_address)
+ VALUES (?, ?, ?, ?, ?)
+ `;
+ return await db.query(sql, [user_id, type, target, description, ip]);
+ }
+
+ static async findAll(params = {}) {
+ const limit = parseInt(params.limit) || 50;
+ const offset = parseInt(params.offset) || 0;
+
+ const sql = `
+ SELECT l.*, u.name as user_name
+ FROM operation_logs l
+ LEFT JOIN users u ON l.user_id = u.id
+ ORDER BY l.created_at DESC
+ LIMIT ? OFFSET ?
+ `;
+ return await db.query(sql, [limit, offset]);
+ }
+}
+
+module.exports = OperationLog;
diff --git a/backend/models/Score.js b/backend/models/Score.js
index 3b04309..3c4b20c 100644
--- a/backend/models/Score.js
+++ b/backend/models/Score.js
@@ -41,7 +41,7 @@ class Score {
const sql = `
INSERT INTO grades (student_id, course_id, teacher_id, final_score, total_score,
grade_point, grade_level, created_at, remark)
- VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), ?)
+ VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), ?)
`;
const result = await db.pool.execute(sql, [
studentId, courseId, teacherId, score, score, gradePoint, gradeLevel, remark
@@ -56,6 +56,78 @@ class Score {
);
return rows[0];
}
+
+ static async findByTeacher(teacherId, filters) {
+ let sql = `
+ SELECT g.*, s.name as student_name, c.course_name
+ FROM grades g
+ JOIN students s ON g.student_id = s.id
+ JOIN courses c ON g.course_id = c.id
+ WHERE c.teacher_id = ?
+ `;
+ const params = [teacherId];
+
+ if (filters.courseId) {
+ sql += ' AND g.course_id = ?';
+ params.push(filters.courseId);
+ }
+ if (filters.studentName) {
+ sql += ' AND (s.name LIKE ? OR s.id LIKE ?)';
+ params.push(`%${filters.studentName}%`, `%${filters.studentName}%`);
+ }
+ return await db.query(sql, params);
+ }
+
+ static async findCourseStudentsWithGrades(courseId, teacherId) {
+ const sql = `
+ SELECT s.id as student_id, s.name as student_name,
+ g.usual_score, g.midterm_score, g.final_score, g.total_score, g.grade_point, g.grade_level,
+ c.id as course_id, c.course_name
+ FROM students s
+ JOIN classes cl ON s.class = cl.class_name
+ JOIN courses c ON c.class_id = cl.id
+ LEFT JOIN grades g ON s.id = g.student_id AND g.course_id = c.id
+ WHERE c.id = ? AND c.teacher_id = ?
+ `;
+ return await db.query(sql, [courseId, teacherId]);
+ }
+
+ static async upsert(scoreData) {
+ const {
+ studentId, courseId, teacherId,
+ usual_score, midterm_score, final_score, score,
+ gradePoint, gradeLevel, remark
+ } = scoreData;
+
+ const existing = await this.findByStudentAndCourse(studentId, courseId);
+
+ // 处理参数:如果是 undefined 或空字符串,则设为 null
+ const sanitize = (val) => (val === undefined || val === '' || val === null) ? null : val;
+
+ const params = [
+ sanitize(usual_score),
+ sanitize(midterm_score),
+ sanitize(final_score),
+ sanitize(score),
+ sanitize(gradePoint),
+ sanitize(gradeLevel),
+ sanitize(remark)
+ ];
+
+ if (existing) {
+ const sql = `UPDATE grades SET usual_score=?, midterm_score=?, final_score=?, total_score=?, grade_point=?, grade_level=?, remark=? WHERE id=?`;
+ await db.query(sql, [...params, existing.id]);
+ return existing.id;
+ } else {
+ const sql = `INSERT INTO grades (student_id, course_id, teacher_id, usual_score, midterm_score, final_score, total_score, grade_point, grade_level, created_at, remark) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), ?)`;
+ const insertParams = [
+ studentId, courseId, teacherId,
+ ...params
+ ];
+ const result = await db.query(sql, insertParams);
+ return result.insertId;
+ }
+ }
}
module.exports = Score;
\ No newline at end of file
diff --git a/backend/models/Student.js b/backend/models/Student.js
index 2a48811..9653686 100644
--- a/backend/models/Student.js
+++ b/backend/models/Student.js
@@ -7,12 +7,33 @@ class Student {
}
static async create(studentData) {
- const { id, name, className } = studentData;
+ const { id, name, class: className, major, grade, contact_info } = studentData;
await db.query(
- 'INSERT INTO students (id, name, class) VALUES (?, ?, ?)',
- [id, name, className]
+ 'INSERT INTO students (id, name, class, major, grade, contact_info) VALUES (?, ?, ?, ?, ?, ?)',
+ [id, name, className, major, grade, contact_info]
);
}
+
+ static async update(id, data) {
+ const fields = [];
+ const values = [];
+ if (data.name) { fields.push('name = ?'); values.push(data.name); }
+ if (data.class) { fields.push('class = ?'); values.push(data.class); }
+ if (data.major !== undefined) { fields.push('major = ?'); values.push(data.major); }
+ if (data.grade !== undefined) { fields.push('grade = ?'); values.push(data.grade); }
+ if (data.contact_info !== undefined) { fields.push('contact_info = ?'); values.push(data.contact_info); }
+
+ if (fields.length === 0) return true;
+
+ values.push(id);
+ const sql = `UPDATE students SET ${fields.join(', ')} WHERE id = ?`;
+ await db.query(sql, values);
+ return true;
+ }
+
+ static async delete(id) {
+ await db.query('DELETE FROM students WHERE id = ?', [id]);
+ }
}
-module.exports = Student;
\ No newline at end of file
+module.exports = Student;
diff --git a/backend/models/SystemSetting.js b/backend/models/SystemSetting.js
new file mode 100644
index 0000000..ba0ebb7
--- /dev/null
+++ b/backend/models/SystemSetting.js
@@ -0,0 +1,32 @@
+const db = require('../config/database');
+
+class SystemSetting {
+ static async get(key) {
+ const rows = await db.query('SELECT value FROM system_settings WHERE key = ?', [key]);
+ return rows.length > 0 ? rows[0].value : null;
+ }
+
+ static async getAll() {
+ const rows = await db.query('SELECT key, value FROM system_settings');
+ const settings = {};
+ rows.forEach(row => {
+ settings[row.key] = row.value;
+ });
+ return settings;
+ }
+
+ static async set(key, value) {
+ return await db.query(
+ 'INSERT INTO system_settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
+ [key, value]
+ );
+ }
+
+ static async setMany(settings) {
+ for (const [key, value] of Object.entries(settings)) {
+ await this.set(key, String(value));
+ }
+ }
+}
+
+module.exports = SystemSetting;
diff --git a/backend/models/Teacher.js b/backend/models/Teacher.js
new file mode 100644
index 0000000..8dde743
--- /dev/null
+++ b/backend/models/Teacher.js
@@ -0,0 +1,38 @@
+const db = require('../config/database');
+
+class Teacher {
+ static async findById(id) {
+ const teachers = await db.query('SELECT * FROM teachers WHERE id = ?', [id]);
+ return teachers[0];
+ }
+
+ static async create(teacherData) {
+ const { id, name, department, title, contact_info } = teacherData;
+ await db.query(
+ 'INSERT INTO teachers (id, name, department, title, contact_info) VALUES (?, ?, ?, ?, ?)',
+ [id, name, department, title, contact_info]
+ );
+ }
+
+ static async update(id, data) {
+ const fields = [];
+ const values = [];
+ if (data.name) { fields.push('name = ?'); values.push(data.name); }
+ if (data.department) { fields.push('department = ?'); values.push(data.department); }
+ if (data.title !== undefined) { fields.push('title = ?'); values.push(data.title); }
+ if (data.contact_info !== undefined) { fields.push('contact_info = ?'); values.push(data.contact_info); }
+
+ if (fields.length === 0) return true;
+
+ values.push(id);
+ const sql = `UPDATE teachers SET ${fields.join(', ')} WHERE id = ?`;
+ await db.query(sql, values);
+ return true;
+ }
+
+ static async delete(id) {
+ await db.query('DELETE FROM teachers WHERE id = ?', [id]);
+ }
+}
+
+module.exports = Teacher;
diff --git a/backend/models/User.js b/backend/models/User.js
index 0177278..d6accaa 100644
--- a/backend/models/User.js
+++ b/backend/models/User.js
@@ -35,6 +35,27 @@ class User {
await db.query('UPDATE users SET password = ? WHERE id = ?', [hashedPassword, id]);
return true;
}
+
+ static async updateProfile(id, updateData) {
+ const fields = [];
+ const params = [];
+
+ if (updateData.name) {
+ fields.push('name = ?');
+ params.push(updateData.name);
+ }
+ if (updateData.class !== undefined) {
+ fields.push('class = ?');
+ params.push(updateData.class);
+ }
+
+ if (fields.length === 0) return false;
+
+ params.push(id);
+ const sql = `UPDATE users SET ${fields.join(', ')} WHERE id = ?`;
+ await db.query(sql, params);
+ return true;
+ }
}
module.exports = User;
\ No newline at end of file
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 8a956b6..98f8725 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -15,7 +15,8 @@
"express": "^4.18.2",
"express-mysql-session": "^3.0.0",
"express-session": "^1.17.3",
- "mysql2": "^3.6.0"
+ "mysql2": "^3.6.0",
+ "sqlite3": "^5.1.7"
},
"devDependencies": {
"nodemon": "^3.0.1"
@@ -24,6 +25,56 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@gar/promisify": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmmirror.com/@gar/promisify/-/promisify-1.1.3.tgz",
+ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@npmcli/fs": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/@npmcli/fs/-/fs-1.1.1.tgz",
+ "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "@gar/promisify": "^1.0.1",
+ "semver": "^7.3.5"
+ }
+ },
+ "node_modules/@npmcli/move-file": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@npmcli/move-file/-/move-file-1.1.2.tgz",
+ "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==",
+ "deprecated": "This functionality has been moved to @npmcli/fs",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "mkdirp": "^1.0.4",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@tootallnate/once/-/once-1.1.2.tgz",
+ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "license": "ISC",
+ "optional": true
+ },
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -37,6 +88,81 @@
"node": ">= 0.6"
}
},
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/agent-base/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/agent-base/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/agentkeepalive": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmmirror.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/aggregate-error": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/aggregate-error/-/aggregate-error-3.1.0.tgz",
+ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -51,6 +177,28 @@
"node": ">= 8"
}
},
+ "node_modules/aproba": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/aproba/-/aproba-2.1.0.tgz",
+ "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/are-we-there-yet": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
+ "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -70,7 +218,27 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
"license": "MIT"
},
"node_modules/bcryptjs": {
@@ -92,6 +260,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmmirror.com/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
@@ -120,7 +308,7 @@
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -140,6 +328,30 @@
"node": ">=8"
}
},
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -149,6 +361,49 @@
"node": ">= 0.8"
}
},
+ "node_modules/cacache": {
+ "version": "15.3.0",
+ "resolved": "https://registry.npmmirror.com/cacache/-/cacache-15.3.0.tgz",
+ "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "@npmcli/fs": "^1.0.0",
+ "@npmcli/move-file": "^1.0.1",
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "glob": "^7.1.4",
+ "infer-owner": "^1.0.4",
+ "lru-cache": "^6.0.0",
+ "minipass": "^3.1.1",
+ "minipass-collect": "^1.0.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.2",
+ "mkdirp": "^1.0.3",
+ "p-map": "^4.0.0",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^3.0.2",
+ "ssri": "^8.0.1",
+ "tar": "^6.0.2",
+ "unique-filename": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/cacache/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -203,13 +458,49 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/clean-stack": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/clean-stack/-/clean-stack-2.2.0.tgz",
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-support": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmmirror.com/color-support/-/color-support-1.1.3.tgz",
+ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "color-support": "bin.js"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
+ "node_modules/console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
+ "license": "ISC",
+ "optional": true
+ },
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -268,6 +559,37 @@
"ms": "2.0.0"
}
},
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -296,6 +618,15 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -328,6 +659,13 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -337,6 +675,55 @@
"node": ">= 0.8"
}
},
+ "node_modules/encoding": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmmirror.com/encoding/-/encoding-0.1.13.tgz",
+ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
+ "node_modules/encoding/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -382,6 +769,15 @@
"node": ">= 0.6"
}
},
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
@@ -511,6 +907,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "license": "MIT"
+ },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -560,6 +962,31 @@
"node": ">= 0.6"
}
},
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
+ "node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "license": "ISC",
+ "optional": true
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -584,6 +1011,27 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/gauge": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmmirror.com/gauge/-/gauge-4.0.4.tgz",
+ "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.3",
+ "console-control-strings": "^1.1.0",
+ "has-unicode": "^2.0.1",
+ "signal-exit": "^3.0.7",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wide-align": "^1.1.5"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
@@ -630,6 +1078,34 @@
"node": ">= 0.4"
}
},
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
@@ -655,6 +1131,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC",
+ "optional": true
+ },
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@@ -677,6 +1160,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
+ "license": "ISC",
+ "optional": true
+ },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -689,6 +1179,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/http-cache-semantics": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
+ "license": "BSD-2-Clause",
+ "optional": true
+ },
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -709,6 +1206,95 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/http-proxy-agent": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
+ "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tootallnate/once": "1",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/http-proxy-agent/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/http-proxy-agent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/https-proxy-agent/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/https-proxy-agent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -721,6 +1307,26 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
@@ -728,12 +1334,67 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/infer-owner": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/infer-owner/-/infer-owner-1.0.4.tgz",
+ "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
+ "node_modules/ip-address": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.1.0.tgz",
+ "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -766,6 +1427,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -779,6 +1450,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-lambda": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/is-lambda/-/is-lambda-1.0.1.tgz",
+ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -795,6 +1473,13 @@
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC",
+ "optional": true
+ },
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
@@ -825,6 +1510,47 @@
"url": "https://github.com/sponsors/wellwelwel"
}
},
+ "node_modules/make-fetch-happen": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmmirror.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz",
+ "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "agentkeepalive": "^4.1.3",
+ "cacache": "^15.2.0",
+ "http-cache-semantics": "^4.1.0",
+ "http-proxy-agent": "^4.0.1",
+ "https-proxy-agent": "^5.0.0",
+ "is-lambda": "^1.0.1",
+ "lru-cache": "^6.0.0",
+ "minipass": "^3.1.3",
+ "minipass-collect": "^1.0.2",
+ "minipass-fetch": "^1.3.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "negotiator": "^0.6.2",
+ "promise-retry": "^2.0.1",
+ "socks-proxy-agent": "^6.0.0",
+ "ssri": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -894,11 +1620,23 @@
"node": ">= 0.6"
}
},
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -907,6 +1645,128 @@
"node": "*"
}
},
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-collect": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/minipass-collect/-/minipass-collect-1.0.2.tgz",
+ "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-fetch": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmmirror.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz",
+ "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.1.0",
+ "minipass-sized": "^1.0.3",
+ "minizlib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "optionalDependencies": {
+ "encoding": "^0.1.12"
+ }
+ },
+ "node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmmirror.com/minipass-flush/-/minipass-flush-1.0.5.tgz",
+ "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-sized": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/minipass-sized/-/minipass-sized-1.0.3.tgz",
+ "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -961,6 +1821,12 @@
"node": ">=8.0.0"
}
},
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "license": "MIT"
+ },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -970,6 +1836,49 @@
"node": ">= 0.6"
}
},
+ "node_modules/node-abi": {
+ "version": "3.85.0",
+ "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.85.0.tgz",
+ "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "license": "MIT"
+ },
+ "node_modules/node-gyp": {
+ "version": "8.4.1",
+ "resolved": "https://registry.npmmirror.com/node-gyp/-/node-gyp-8.4.1.tgz",
+ "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "env-paths": "^2.2.0",
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.2.6",
+ "make-fetch-happen": "^9.1.0",
+ "nopt": "^5.0.0",
+ "npmlog": "^6.0.0",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.2",
+ "which": "^2.0.2"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
+ },
+ "engines": {
+ "node": ">= 10.12.0"
+ }
+ },
"node_modules/nodemon": {
"version": "3.1.11",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
@@ -1024,6 +1933,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/nopt": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/nopt/-/nopt-5.0.0.tgz",
+ "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "abbrev": "1"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -1034,6 +1959,23 @@
"node": ">=0.10.0"
}
},
+ "node_modules/npmlog": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmmirror.com/npmlog/-/npmlog-6.0.2.tgz",
+ "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "are-we-there-yet": "^3.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^4.0.3",
+ "set-blocking": "^2.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -1076,6 +2018,31 @@
"node": ">= 0.8"
}
},
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/p-map/-/p-map-4.0.0.tgz",
+ "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "aggregate-error": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -1085,6 +2052,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
@@ -1104,6 +2081,53 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/promise-inflight": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/promise-inflight/-/promise-inflight-1.0.1.tgz",
+ "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -1124,6 +2148,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/pump": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.3.tgz",
+ "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@@ -1172,6 +2206,35 @@
"node": ">= 0.8"
}
},
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -1185,6 +2248,33 @@
"node": ">=8.10.0"
}
},
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -1215,7 +2305,6 @@
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
- "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -1274,6 +2363,13 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC",
+ "optional": true
+ },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -1352,6 +2448,58 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmmirror.com/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
@@ -1365,6 +2513,96 @@
"node": ">=10"
}
},
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.8.7",
+ "resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.7.tgz",
+ "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ip-address": "^10.0.1",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmmirror.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz",
+ "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "agent-base": "^6.0.2",
+ "debug": "^4.3.3",
+ "socks": "^2.6.2"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/socks-proxy-agent/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socks-proxy-agent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/sqlite3": {
+ "version": "5.1.7",
+ "resolved": "https://registry.npmmirror.com/sqlite3/-/sqlite3-5.1.7.tgz",
+ "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "node-addon-api": "^7.0.0",
+ "prebuild-install": "^7.1.1",
+ "tar": "^6.1.11"
+ },
+ "optionalDependencies": {
+ "node-gyp": "8.x"
+ },
+ "peerDependencies": {
+ "node-gyp": "8.x"
+ },
+ "peerDependenciesMeta": {
+ "node-gyp": {
+ "optional": true
+ }
+ }
+ },
"node_modules/sqlstring": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
@@ -1374,6 +2612,19 @@
"node": ">= 0.6"
}
},
+ "node_modules/ssri": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmmirror.com/ssri/-/ssri-8.0.1.tgz",
+ "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -1383,6 +2634,52 @@
"node": ">= 0.8"
}
},
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -1396,6 +2693,66 @@
"node": ">=4"
}
},
+ "node_modules/tar": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+ "license": "ISC",
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-fs/node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar/node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -1428,6 +2785,18 @@
"nodetouch": "bin/nodetouch.js"
}
},
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -1460,6 +2829,26 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/unique-filename": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/unique-filename/-/unique-filename-1.1.1.tgz",
+ "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "unique-slug": "^2.0.0"
+ }
+ },
+ "node_modules/unique-slug": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/unique-slug/-/unique-slug-2.0.2.tgz",
+ "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4"
+ }
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -1469,6 +2858,12 @@
"node": ">= 0.8"
}
},
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -1486,6 +2881,44 @@
"engines": {
"node": ">= 0.8"
}
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wide-align": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/wide-align/-/wide-align-1.1.5.tgz",
+ "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "string-width": "^1.0.2 || 2 || 3 || 4"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC"
}
}
}
diff --git a/backend/package.json b/backend/package.json
index a6e01ca..1ca49a0 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -8,14 +8,15 @@
"dev": "nodemon server.js"
},
"dependencies": {
- "express": "^4.18.2",
- "cors": "^2.8.5",
- "express-session": "^1.17.3",
- "express-mysql-session": "^3.0.0",
- "mysql2": "^3.6.0",
"bcryptjs": "^2.4.3",
+ "body-parser": "^1.20.2",
+ "cors": "^2.8.5",
"dotenv": "^16.3.1",
- "body-parser": "^1.20.2"
+ "express": "^4.18.2",
+ "express-mysql-session": "^3.0.0",
+ "express-session": "^1.17.3",
+ "mysql2": "^3.6.0",
+ "sqlite3": "^5.1.7"
},
"devDependencies": {
"nodemon": "^3.0.1"
@@ -23,4 +24,4 @@
"engines": {
"node": ">=14.0.0"
}
-}
\ No newline at end of file
+}
diff --git a/backend/routes/admin.js b/backend/routes/admin.js
index 1607657..ead3be6 100644
--- a/backend/routes/admin.js
+++ b/backend/routes/admin.js
@@ -3,7 +3,42 @@ const router = express.Router();
const AdminController = require('../controllers/adminController');
const { requireAuth, requireRole } = require('../middleware/auth');
+router.get('/stats', requireAuth, requireRole(['admin']), AdminController.getStats);
router.get('/users', requireAuth, requireRole(['admin']), AdminController.getUsers);
router.post('/users', requireAuth, requireRole(['admin']), AdminController.createUser);
+router.put('/users/:id', requireAuth, requireRole(['admin']), AdminController.updateUser);
+router.delete('/users/:id', requireAuth, requireRole(['admin']), AdminController.deleteUser);
+
+// Student Management
+router.get('/students', requireAuth, requireRole(['admin']), AdminController.getStudents);
+router.post('/students', requireAuth, requireRole(['admin']), AdminController.createStudent);
+router.put('/students/:id', requireAuth, requireRole(['admin']), AdminController.updateStudent);
+router.delete('/students/:id', requireAuth, requireRole(['admin']), AdminController.deleteStudent);
+
+// Teacher Management
+router.get('/teachers', requireAuth, requireRole(['admin']), AdminController.getTeachers);
+router.post('/teachers', requireAuth, requireRole(['admin']), AdminController.createTeacher);
+router.put('/teachers/:id', requireAuth, requireRole(['admin']), AdminController.updateTeacher);
+router.delete('/teachers/:id', requireAuth, requireRole(['admin']), AdminController.deleteTeacher);
+
+// Grade Statistics
+router.get('/grade-stats', requireAuth, requireRole(['admin']), AdminController.getGradeStats);
+
+// System Settings
+router.get('/settings', requireAuth, requireRole(['admin']), AdminController.getSettings);
+router.post('/settings', requireAuth, requireRole(['admin']), AdminController.saveSettings);
+
+// Data Maintenance
+router.post('/maintenance/backup', requireAuth, requireRole(['admin']), AdminController.backupDatabase);
+router.post('/maintenance/clear-cache', requireAuth, requireRole(['admin']), AdminController.clearCache);
+router.post('/maintenance/reset-passwords', requireAuth, requireRole(['admin']), AdminController.resetStudentPasswords);
+
+// Data Export
+router.get('/export/students', requireAuth, requireRole(['admin']), AdminController.exportStudents);
+router.get('/export/teachers', requireAuth, requireRole(['admin']), AdminController.exportTeachers);
+router.get('/export/grades', requireAuth, requireRole(['admin']), AdminController.exportGrades);
+
+// Operation Logs
+router.get('/logs', requireAuth, requireRole(['admin']), AdminController.getOperationLogs);
module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
index a5d57a2..0ed96b5 100644
--- a/backend/routes/auth.js
+++ b/backend/routes/auth.js
@@ -8,5 +8,6 @@ router.post('/register', AuthController.register);
router.post('/logout', AuthController.logout);
router.get('/me', AuthController.getCurrentUser);
router.put('/update-password', requireAuth, AuthController.updatePassword);
+router.put('/update-profile', requireAuth, AuthController.updateProfile);
module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/teacher.js b/backend/routes/teacher.js
index 0a7f48a..5e32ea6 100644
--- a/backend/routes/teacher.js
+++ b/backend/routes/teacher.js
@@ -4,6 +4,11 @@ const TeacherController = require('../controllers/teacherController');
const { requireAuth, requireRole } = require('../middleware/auth');
router.get('/courses', requireAuth, requireRole(['teacher']), TeacherController.getCourses);
+router.get('/classes', requireAuth, requireRole(['teacher']), TeacherController.getClasses);
+router.get('/my-classes', requireAuth, requireRole(['teacher']), TeacherController.getMyClasses);
+router.post('/courses', requireAuth, requireRole(['teacher']), TeacherController.createCourse);
+router.put('/courses/:id', requireAuth, requireRole(['teacher']), TeacherController.updateCourse);
+router.get('/grades', requireAuth, requireRole(['teacher']), TeacherController.getGrades);
router.post('/grades', requireAuth, requireRole(['teacher']), TeacherController.addScore);
module.exports = router;
\ No newline at end of file
diff --git a/backend/scripts/fix_teachers.js b/backend/scripts/fix_teachers.js
new file mode 100644
index 0000000..ad5cb02
--- /dev/null
+++ b/backend/scripts/fix_teachers.js
@@ -0,0 +1,31 @@
+const db = require('../config/database');
+
+async function fixTeachers() {
+ const departments = ['计算机学院', '软件学院', '信息工程学院', '理学院', '外国语学院'];
+ const titles = ['教授', '副教授', '讲师', '助教'];
+
+ try {
+ const teachers = await db.query('SELECT id FROM teachers');
+ for (let i = 0; i < teachers.length; i++) {
+ const dept = departments[i % departments.length];
+ const title = titles[i % titles.length];
+ const contact = `139${Math.floor(Math.random() * 90000000 + 10000000)}`;
+
+ await db.query(
+ 'UPDATE teachers SET department = ?, title = ?, contact_info = ? WHERE id = ?',
+ [dept, title, contact, teachers[i].id]
+ );
+
+ // Also sync back to users table's class field if needed (though we'll use teachers table now)
+ await db.query(
+ 'UPDATE users SET class = ? WHERE id = ?',
+ [dept, teachers[i].id]
+ );
+ }
+ console.log('教师信息修复完成!');
+ } catch (err) {
+ console.error('修复失败:', err);
+ }
+}
+
+fixTeachers();
diff --git a/backend/scripts/migrate_v2.js b/backend/scripts/migrate_v2.js
new file mode 100644
index 0000000..eb084b2
--- /dev/null
+++ b/backend/scripts/migrate_v2.js
@@ -0,0 +1,48 @@
+const db = require('../config/database');
+
+async function migrate() {
+ try {
+ console.log('Starting migration v2...');
+
+ // 1. System Settings Table
+ await db.query(`
+ CREATE TABLE IF NOT EXISTS system_settings (
+ key TEXT PRIMARY KEY,
+ value TEXT
+ )
+ `);
+
+ // Default settings
+ const defaultSettings = [
+ ['system_name', '学校成绩管理系统'],
+ ['current_semester', '2023-2024-2'],
+ ['allow_course_selection', '1'],
+ ['allow_grade_check', '1']
+ ];
+
+ for (const [key, value] of defaultSettings) {
+ await db.query('INSERT OR IGNORE INTO system_settings (key, value) VALUES (?, ?)', [key, value]);
+ }
+
+ // 2. Operation Logs Table
+ await db.query(`
+ CREATE TABLE IF NOT EXISTS operation_logs (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT,
+ operation_type TEXT,
+ operation_target TEXT,
+ description TEXT,
+ ip_address TEXT,
+ created_at TEXT DEFAULT (datetime('now', 'localtime'))
+ )
+ `);
+
+ console.log('Migration v2 completed successfully.');
+ process.exit(0);
+ } catch (error) {
+ console.error('Migration v2 failed:', error);
+ process.exit(1);
+ }
+}
+
+migrate();
diff --git a/backend/scripts/migrate_v3.js b/backend/scripts/migrate_v3.js
new file mode 100644
index 0000000..fee0325
--- /dev/null
+++ b/backend/scripts/migrate_v3.js
@@ -0,0 +1,33 @@
+const db = require('../config/database');
+
+async function migrate() {
+ console.log('开始执行 v3 迁移: 创建 teachers 表...');
+
+ try {
+ await db.query(`
+ CREATE TABLE IF NOT EXISTS teachers (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ department TEXT,
+ title TEXT,
+ contact_info TEXT,
+ FOREIGN KEY (id) REFERENCES users(id) ON DELETE CASCADE
+ )
+ `);
+
+ // 从 users 表中迁移现有的教师数据
+ const teachers = await db.query('SELECT id, name, class FROM users WHERE role = "teacher"');
+ for (const t of teachers) {
+ await db.query(
+ 'INSERT OR IGNORE INTO teachers (id, name, department) VALUES (?, ?, ?)',
+ [t.id, t.name, t.class]
+ );
+ }
+
+ console.log('v3 迁移成功!');
+ } catch (err) {
+ console.error('v3 迁移失败:', err);
+ }
+}
+
+migrate();
diff --git a/backend/server.js b/backend/server.js
index abd23d3..9615aa2 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -1,7 +1,7 @@
const express = require('express');
const cors = require('cors');
const session = require('express-session');
-const MySQLStore = require('express-mysql-session')(session);
+// const MySQLStore = require('express-mysql-session')(session);
const path = require('path');
require('dotenv').config();
@@ -28,6 +28,7 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Session
+/*
const sessionStore = new MySQLStore({
expiration: 86400000,
createDatabaseTable: true,
@@ -40,11 +41,12 @@ const sessionStore = new MySQLStore({
}
}
}, db.pool);
+*/
app.use(session({
key: 'session_cookie',
secret: process.env.SESSION_SECRET || 'your-secret-key',
- store: sessionStore,
+ // store: sessionStore, // Use MemoryStore for SQLite migration simplification
resave: false,
saveUninitialized: false,
cookie: {
@@ -112,14 +114,20 @@ teacherPageRouter.use(requirePageAuth, requirePageRole(['teacher']));
teacherPageRouter.get('/dashboard', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/teacher/dashboard.html')));
teacherPageRouter.get('/grade_entry', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/teacher/grade_entry.html')));
teacherPageRouter.get('/grade_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/teacher/grade_management.html')));
+teacherPageRouter.get('/profile', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/teacher/profile.html')));
app.use('/teacher', teacherPageRouter);
// Admin Pages
const adminPageRouter = express.Router();
adminPageRouter.use(requirePageAuth, requirePageRole(['admin']));
adminPageRouter.get('/dashboard', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/dashboard.html')));
-adminPageRouter.get('/student_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/student_management.html')));
adminPageRouter.get('/user_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/user_management.html')));
+adminPageRouter.get('/student_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/student_management.html')));
+adminPageRouter.get('/teacher_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/teacher_management.html')));
+adminPageRouter.get('/grade_statistics', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/grade_statistics.html')));
+adminPageRouter.get('/system_settings', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/system_settings.html')));
+adminPageRouter.get('/data_export', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/data_export.html')));
+adminPageRouter.get('/operation_logs', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/operation_logs.html')));
app.use('/admin', adminPageRouter);
// --- API Routes ---
diff --git a/backend/services/adminService.js b/backend/services/adminService.js
index acc072b..dbabb7c 100644
--- a/backend/services/adminService.js
+++ b/backend/services/adminService.js
@@ -1,10 +1,17 @@
const db = require('../config/database');
const bcrypt = require('bcryptjs');
const User = require('../models/User');
+const Student = require('../models/Student');
+const Teacher = require('../models/Teacher');
+const SystemSetting = require('../models/SystemSetting');
+const OperationLog = require('../models/OperationLog');
class AdminService {
static async getUsers(params) {
- const { page = 1, limit = 10, search = '', role = '' } = params;
+ const page = parseInt(params.page) || 1;
+ const limit = parseInt(params.limit) || 10;
+ const search = params.search || '';
+ const role = params.role || '';
const offset = (page - 1) * limit;
let queryStr = 'SELECT id, name, role, class, created_at FROM users WHERE 1=1';
@@ -28,9 +35,9 @@ class AdminService {
// Data
queryStr += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
- queryParams.push(parseInt(limit), parseInt(offset));
+ const dataQueryParams = [...queryParams, limit, offset];
- const users = await db.query(queryStr, queryParams);
+ const users = await db.query(queryStr, dataQueryParams);
return {
data: users,
@@ -43,8 +50,22 @@ class AdminService {
};
}
- static async createUser(userData) {
- const { id } = userData;
+ static async getStats() {
+ const usersCount = await db.query('SELECT COUNT(*) as count FROM users');
+ const studentsCount = await db.query('SELECT COUNT(*) as count FROM students');
+ const teachersCount = await db.query('SELECT COUNT(*) as count FROM users WHERE role = "teacher"');
+ const coursesCount = await db.query('SELECT COUNT(*) as count FROM courses');
+
+ return {
+ users: usersCount[0].count,
+ students: studentsCount[0].count,
+ teachers: teachersCount[0].count,
+ courses: coursesCount[0].count
+ };
+ }
+
+ static async createUser(userData, operator) {
+ const { id, role, name } = userData;
// 检查 ID
const existingUser = await User.findById(id);
@@ -53,7 +74,445 @@ class AdminService {
}
// 创建用户
- return await User.create(userData);
+ const userId = await User.create(userData);
+
+ // 如果是学生,同时创建学生记录
+ if (role === 'student') {
+ await Student.create(userData);
+ } else if (role === 'teacher') {
+ await Teacher.create({
+ id,
+ name,
+ department: userData.class || ''
+ });
+ }
+
+ if (operator) {
+ await OperationLog.add({
+ user_id: operator.user_id,
+ type: '用户管理',
+ target: `user:${id}`,
+ description: `创建了${role === 'student' ? '学生' : (role === 'teacher' ? '教师' : '管理员')}用户: ${name}(${id})`,
+ ip: operator.ip
+ });
+ }
+
+ return userId;
+ }
+
+ static async updateUser(id, userData, operator) {
+ // 如果修改密码
+ if (userData.password) {
+ const salt = await bcrypt.genSalt(10);
+ userData.password = await bcrypt.hash(userData.password, salt);
+ } else {
+ delete userData.password;
+ }
+
+ const fields = [];
+ const values = [];
+
+ if (userData.name) { fields.push('name = ?'); values.push(userData.name); }
+ if (userData.role) { fields.push('role = ?'); values.push(userData.role); }
+ if (userData.class !== undefined) { fields.push('class = ?'); values.push(userData.class); } // class can be empty string
+ if (userData.password) { fields.push('password = ?'); values.push(userData.password); }
+
+ if (fields.length === 0) return true;
+
+ values.push(id);
+ const sql = `UPDATE users SET ${fields.join(', ')} WHERE id = ?`;
+
+ const result = await db.query(sql, values);
+
+ if (operator) {
+ await OperationLog.add({
+ user_id: operator.user_id,
+ type: '用户管理',
+ target: `user:${id}`,
+ description: `修改了用户信息: ${id}`,
+ ip: operator.ip
+ });
+ }
+
+ return result.affectedRows > 0;
+ }
+
+ static async deleteUser(id, operator) {
+ // 删除用户
+ await Student.delete(id);
+ await Teacher.delete(id);
+ const result = await db.query('DELETE FROM users WHERE id = ?', [id]);
+
+ if (operator) {
+ await OperationLog.add({
+ user_id: operator.user_id,
+ type: '用户管理',
+ target: `user:${id}`,
+ description: `删除了用户: ${id}`,
+ ip: operator.ip
+ });
+ }
+
+ return result.affectedRows > 0;
+ }
+
+ // ================= Student Management =================
+ static async getStudents(params) {
+ const page = parseInt(params.page) || 1;
+ const limit = parseInt(params.limit) || 10;
+ const search = params.search || '';
+ const offset = (page - 1) * limit;
+
+ let queryStr = 'SELECT * FROM students WHERE 1=1';
+ let queryParams = [];
+
+ if (search) {
+ queryStr += ' AND (id LIKE ? OR name LIKE ? OR class LIKE ?)';
+ const searchTerm = `%${search}%`;
+ queryParams.push(searchTerm, searchTerm, searchTerm);
+ }
+
+ // Count
+ const countSql = queryStr.replace('SELECT *', 'SELECT COUNT(*) as total');
+ const countRows = await db.query(countSql, queryParams);
+ const total = countRows[0].total;
+
+ // Data
+ queryStr += ' ORDER BY id ASC LIMIT ? OFFSET ?';
+ const dataQueryParams = [...queryParams, limit, offset];
+
+ const students = await db.query(queryStr, dataQueryParams);
+
+ return {
+ data: students,
+ pagination: {
+ page: parseInt(page),
+ limit: parseInt(limit),
+ total,
+ pages: Math.ceil(total / limit)
+ }
+ };
+ }
+
+ static async createStudent(studentData, operator) {
+ const { id, name, class: className } = studentData;
+
+ // Check ID
+ const existing = await User.findById(id);
+ if (existing) throw new Error('学号已存在');
+
+ // Create User first
+ await User.create({
+ id,
+ name,
+ password: id, // Default password
+ role: 'student',
+ class: className
+ });
+
+ // Create Student
+ await Student.create(studentData);
+
+ if (operator) {
+ await OperationLog.add({
+ user_id: operator.user_id,
+ type: '学生管理',
+ target: `student:${id}`,
+ description: `创建了学生: ${name}(${id})`,
+ ip: operator.ip
+ });
+ }
+
+ return id;
+ }
+
+ static async updateStudent(id, data, operator) {
+ // Update students table
+ await Student.update(id, data);
+
+ // Sync User table (name, class)
+ const userFields = [];
+ const userValues = [];
+ if (data.name) { userFields.push('name = ?'); userValues.push(data.name); }
+ if (data.class) { userFields.push('class = ?'); userValues.push(data.class); }
+
+ if (userFields.length > 0) {
+ userValues.push(id);
+ await db.query(`UPDATE users SET ${userFields.join(', ')} WHERE id = ?`, userValues);
+ }
+
+ if (operator) {
+ await OperationLog.add({
+ user_id: operator.user_id,
+ type: '学生管理',
+ target: `student:${id}`,
+ description: `修改了学生信息: ${id}`,
+ ip: operator.ip
+ });
+ }
+
+ return true;
+ }
+
+ static async deleteStudent(id, operator) {
+ await Student.delete(id);
+ await db.query('DELETE FROM users WHERE id = ?', [id]);
+
+ if (operator) {
+ await OperationLog.add({
+ user_id: operator.user_id,
+ type: '学生管理',
+ target: `student:${id}`,
+ description: `删除了学生: ${id}`,
+ ip: operator.ip
+ });
+ }
+
+ return true;
+ }
+
+ // ================= Teacher Management =================
+ static async getTeachers(params) {
+ const page = parseInt(params.page) || 1;
+ const limit = parseInt(params.limit) || 10;
+ const search = params.search || '';
+ const offset = (page - 1) * limit;
+
+ let queryStr = 'SELECT t.*, u.created_at FROM teachers t JOIN users u ON t.id = u.id WHERE 1=1';
+ let queryParams = [];
+
+ if (search) {
+ queryStr += ' AND (t.id LIKE ? OR t.name LIKE ? OR t.department LIKE ?)';
+ const searchTerm = `%${search}%`;
+ queryParams.push(searchTerm, searchTerm, searchTerm);
+ }
+
+ // Count
+ const countSql = `SELECT COUNT(*) as total FROM teachers t WHERE 1=1 ${search ? 'AND (t.id LIKE ? OR t.name LIKE ? OR t.department LIKE ?)' : ''}`;
+ const countRows = await db.query(countSql, queryParams);
+ const total = countRows[0].total;
+
+ // Data
+ queryStr += ' ORDER BY t.id ASC LIMIT ? OFFSET ?';
+ const dataQueryParams = [...queryParams, limit, offset];
+
+ const teachers = await db.query(queryStr, dataQueryParams);
+
+ return {
+ data: teachers,
+ pagination: {
+ page: parseInt(page),
+ limit: parseInt(limit),
+ total,
+ pages: Math.ceil(total / limit)
+ }
+ };
+ }
+
+ static async createTeacher(teacherData, operator) {
+ const { id, name, department } = teacherData;
+
+ // Check ID
+ const existing = await User.findById(id);
+ if (existing) throw new Error('工号已存在');
+
+ // Create User
+ await User.create({
+ id,
+ name,
+ password: id, // Default password
+ role: 'teacher',
+ class: department // Use class field for department in user table
+ });
+
+ // Create Teacher
+ await Teacher.create(teacherData);
+
+ if (operator) {
+ await OperationLog.add({
+ user_id: operator.user_id,
+ type: '教师管理',
+ target: `teacher:${id}`,
+ description: `创建了教师: ${name}(${id})`,
+ ip: operator.ip
+ });
+ }
+
+ return id;
+ }
+
+ static async updateTeacher(id, data, operator) {
+ await Teacher.update(id, data);
+
+ // Sync User table (name, department)
+ const userFields = [];
+ const userValues = [];
+ if (data.name) { userFields.push('name = ?'); userValues.push(data.name); }
+ if (data.department) { userFields.push('class = ?'); userValues.push(data.department); }
+
+ if (userFields.length > 0) {
+ userValues.push(id);
+ await db.query(`UPDATE users SET ${userFields.join(', ')} WHERE id = ?`, userValues);
+ }
+
+ if (operator) {
+ await OperationLog.add({
+ user_id: operator.user_id,
+ type: '教师管理',
+ target: `teacher:${id}`,
+ description: `修改了教师信息: ${id}`,
+ ip: operator.ip
+ });
+ }
+
+ return true;
+ }
+
+ static async deleteTeacher(id, operator) {
+ await Teacher.delete(id);
+ await db.query('DELETE FROM users WHERE id = ?', [id]);
+
+ if (operator) {
+ await OperationLog.add({
+ user_id: operator.user_id,
+ type: '教师管理',
+ target: `teacher:${id}`,
+ description: `删除了教师: ${id}`,
+ ip: operator.ip
+ });
+ }
+
+ return true;
+ }
+
+ // ================= Grade Statistics =================
+ static async getGradeStats() {
+ const sql = `
+ SELECT
+ c.course_code,
+ c.course_name,
+ u.name as teacher_name,
+ COUNT(g.student_id) as student_count,
+ AVG(g.total_score) as avg_score,
+ MAX(g.total_score) as max_score,
+ MIN(g.total_score) as min_score,
+ SUM(CASE WHEN g.total_score >= 60 THEN 1 ELSE 0 END) as pass_count
+ FROM courses c
+ LEFT JOIN grades g ON c.id = g.course_id
+ LEFT JOIN users u ON c.teacher_id = u.id
+ GROUP BY c.id
+ ORDER BY avg_score DESC
+ `;
+
+ const rows = await db.query(sql);
+
+ return rows.map(row => ({
+ ...row,
+ pass_rate: row.student_count > 0 ? ((row.pass_count / row.student_count) * 100).toFixed(1) : 0,
+ avg_score: row.avg_score ? Number(row.avg_score).toFixed(1) : 0
+ }));
+ }
+
+ // ================= System Settings =================
+ static async getSettings() {
+ return await SystemSetting.getAll();
+ }
+
+ static async saveSettings(settings, operator) {
+ await SystemSetting.setMany(settings);
+ await OperationLog.add({
+ user_id: operator.user_id,
+ type: '系统设置',
+ target: 'system_settings',
+ description: '修改了系统基础设置',
+ ip: operator.ip
+ });
+ return true;
+ }
+
+ // ================= Data Maintenance =================
+ static async backupDatabase(operator) {
+ const path = require('path');
+ const fs = require('fs');
+ const source = path.resolve(__dirname, '../database.sqlite');
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
+ const backupDir = path.resolve(__dirname, '../backups');
+ const target = path.resolve(backupDir, `database-${timestamp}.sqlite`);
+
+ if (!fs.existsSync(backupDir)) {
+ fs.mkdirSync(backupDir);
+ }
+
+ fs.copyFileSync(source, target);
+
+ await OperationLog.add({
+ user_id: operator.user_id,
+ type: '数据维护',
+ target: 'database',
+ description: `手动备份数据库: ${path.basename(target)}`,
+ ip: operator.ip
+ });
+
+ return { filename: path.basename(target) };
+ }
+
+ static async clearCache(operator) {
+ await OperationLog.add({
+ user_id: operator.user_id,
+ type: '数据维护',
+ target: 'cache',
+ description: '清理系统缓存',
+ ip: operator.ip
+ });
+ return true;
+ }
+
+ static async resetStudentPasswords(operator) {
+ const hashedPassword = await bcrypt.hash('123456', 10);
+ await db.query('UPDATE users SET password = ? WHERE role = "student"', [hashedPassword]);
+
+ await OperationLog.add({
+ user_id: operator.user_id,
+ type: '数据维护',
+ target: 'users',
+ description: '重置了所有学生的密码为 123456',
+ ip: operator.ip
+ });
+
+ return true;
+ }
+
+ // ================= Operation Logs =================
+ static async getLogs(params) {
+ return await OperationLog.findAll(params);
+ }
+
+ // ================= Data Export =================
+ static async exportStudents() {
+ return await db.query('SELECT * FROM students');
+ }
+
+ static async exportTeachers() {
+ return await db.query('SELECT t.*, u.created_at FROM teachers t JOIN users u ON t.id = u.id');
+ }
+
+ static async exportGrades() {
+ const sql = `
+ SELECT
+ g.student_id,
+ s.name as student_name,
+ c.course_code,
+ c.course_name,
+ g.total_score,
+ g.grade_point,
+ g.grade_level,
+ u.name as teacher_name
+ FROM grades g
+ JOIN students s ON g.student_id = s.id
+ JOIN courses c ON g.course_id = c.id
+ JOIN users u ON g.teacher_id = u.id
+ `;
+ return await db.query(sql);
}
}
diff --git a/backend/services/authService.js b/backend/services/authService.js
index be818f3..9cd80cd 100644
--- a/backend/services/authService.js
+++ b/backend/services/authService.js
@@ -70,6 +70,36 @@ class AuthService {
return await User.updatePassword(userId, newPassword);
}
+
+ static async updateProfile(userId, updateData) {
+ await User.updateProfile(userId, updateData);
+ const updatedUser = await User.findById(userId);
+
+ // 如果是学生,同步更新 students 表
+ if (updatedUser.role === 'student') {
+ const studentFields = [];
+ const studentParams = [];
+ if (updateData.name) {
+ studentFields.push('name = ?');
+ studentParams.push(updateData.name);
+ }
+ if (updateData.class) {
+ studentFields.push('class = ?');
+ studentParams.push(updateData.class);
+ }
+ if (studentFields.length > 0) {
+ studentParams.push(userId);
+ await db.query(`UPDATE students SET ${studentFields.join(', ')} WHERE id = ?`, studentParams);
+ }
+ }
+
+ return {
+ id: updatedUser.id,
+ name: updatedUser.name,
+ role: updatedUser.role,
+ class: updatedUser.class
+ };
+ }
}
module.exports = AuthService;
diff --git a/backend/services/teacherService.js b/backend/services/teacherService.js
index ac73c3e..9b90bdd 100644
--- a/backend/services/teacherService.js
+++ b/backend/services/teacherService.js
@@ -7,8 +7,39 @@ class TeacherService {
return await Course.findByTeacherId(teacherId);
}
+ static async getClasses() {
+ return await Course.getClasses();
+ }
+
+ static async getTeacherClasses(teacherId) {
+ const sql = `SELECT * FROM classes WHERE teacher_id = ?`;
+ const db = require('../config/database');
+ return await db.query(sql, [teacherId]);
+ }
+
+ static async createCourse(teacherId, courseData) {
+ return await Course.create({ ...courseData, teacher_id: teacherId });
+ }
+
+ static async updateCourse(teacherId, courseId, courseData) {
+ // Verify ownership
+ const course = await Course.findById(courseId);
+ if (!course || course.teacher_id != teacherId) {
+ throw new Error('无权修改该课程或课程不存在');
+ }
+ return await Course.update(courseId, courseData);
+ }
+
+ static async getGrades(teacherId, filters) {
+ if (filters.courseId) {
+ return await Score.findCourseStudentsWithGrades(filters.courseId, teacherId);
+ } else {
+ return await Score.findByTeacher(teacherId, filters);
+ }
+ }
+
static async addScore(teacherId, scoreData) {
- const { studentId, courseId, score } = scoreData;
+ let { studentId, courseId, score, usual_score, midterm_score, final_score } = scoreData;
// 验证学生
const student = await Student.findById(studentId);
@@ -16,13 +47,12 @@ class TeacherService {
throw new Error('学生不存在');
}
- // 验证课程(可选:验证是否是该教师的课程)
- // const course = await Course.findById(courseId);
-
- // 检查重复
- const existingScore = await Score.findByStudentAndCourse(studentId, courseId);
- if (existingScore) {
- throw new Error('该学生此课程成绩已存在');
+ // 如果没有总分但有平时/期中/期末分,尝试计算总分 (30% + 30% + 40%)
+ if ((score === undefined || score === '') && (usual_score || midterm_score || final_score)) {
+ const u = parseFloat(usual_score) || 0;
+ const m = parseFloat(midterm_score) || 0;
+ const f = parseFloat(final_score) || 0;
+ score = (u * 0.3 + m * 0.3 + f * 0.4).toFixed(1);
}
// 计算绩点和等级
@@ -30,19 +60,33 @@ class TeacherService {
let gradePoint = 0;
let gradeLevel = 'F';
- if (numericScore >= 90) { gradePoint = 4.0; gradeLevel = 'A'; }
- else if (numericScore >= 80) { gradePoint = 3.0; gradeLevel = 'B'; }
- else if (numericScore >= 70) { gradePoint = 2.0; gradeLevel = 'C'; }
- else if (numericScore >= 60) { gradePoint = 1.0; gradeLevel = 'D'; }
+ if (!isNaN(numericScore)) {
+ if (numericScore >= 90) { gradePoint = 4.0; gradeLevel = 'A'; }
+ else if (numericScore >= 85) { gradePoint = 3.7; gradeLevel = 'A-'; }
+ else if (numericScore >= 82) { gradePoint = 3.3; gradeLevel = 'B+'; }
+ else if (numericScore >= 78) { gradePoint = 3.0; gradeLevel = 'B'; }
+ else if (numericScore >= 75) { gradePoint = 2.7; gradeLevel = 'B-'; }
+ else if (numericScore >= 72) { gradePoint = 2.3; gradeLevel = 'C+'; }
+ else if (numericScore >= 68) { gradePoint = 2.0; gradeLevel = 'C'; }
+ else if (numericScore >= 64) { gradePoint = 1.5; gradeLevel = 'C-'; }
+ else if (numericScore >= 60) { gradePoint = 1.0; gradeLevel = 'D'; }
+ } else {
+ gradePoint = null;
+ gradeLevel = null;
+ }
const fullScoreData = {
...scoreData,
+ score, // 更新后的总分
teacherId,
gradePoint,
- gradeLevel
+ gradeLevel,
+ usual_score,
+ midterm_score,
+ final_score
};
- const gradeId = await Score.create(fullScoreData);
+ const gradeId = await Score.upsert(fullScoreData);
return gradeId;
}
}
diff --git a/database.sqlite b/database.sqlite
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/public/js/admin.js b/frontend/public/js/admin.js
index b7eae66..faefaec 100644
--- a/frontend/public/js/admin.js
+++ b/frontend/public/js/admin.js
@@ -1,583 +1,979 @@
-class AdminDashboard {
+/**
+ * 管理员端功能管理
+ */
+class AdminManager {
constructor() {
- // 动态设置API基础URL,支持file:///协议和localhost:3000访问
- this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api';
- this.currentUser = null;
- this.stats = {};
- this.users = [];
- this.students = [];
- this.teachers = [];
+ this.apiBase = '/api/admin';
this.init();
}
-
+
async init() {
- // 检查登录状? if (!await this.checkAuth()) {
- window.location.href = '/login';
- return;
- }
-
- // 加载用户信息
+ this.updateCurrentTime();
+ setInterval(() => this.updateCurrentTime(), 1000);
await this.loadUserInfo();
- // 加载统计数据
- await this.loadStats();
-
- // 加载用户数据
- await this.loadUsers();
-
- // 绑定事件
- this.bindEvents();
-
- // 更新界面
- this.updateUI();
-
- // 初始化图? this.initCharts();
- }
+ // 页面路由逻辑
+ const path = window.location.pathname;
+ if (path.includes('/dashboard')) {
+ this.initDashboard();
+ } else if (path.includes('/user_management')) {
+ this.initUserManagement();
+ } else if (path.includes('/student_management')) {
+ this.initStudentManagement();
+ } else if (path.includes('/teacher_management')) {
+ this.initTeacherManagement();
+ } else if (path.includes('/grade_statistics')) {
+ this.initGradeStatistics();
+ } else if (path.includes('/system_settings')) {
+ this.initSystemSettings();
+ } else if (path.includes('/data_export')) {
+ this.initDataExport();
+ } else if (path.includes('/operation_logs')) {
+ this.initOperationLogs();
+ }
- async checkAuth() {
- try {
- const response = await fetch(`${this.apiBase}/auth/me`, {
- credentials: 'include'
+ // Logout
+ const logoutBtn = document.getElementById('logoutBtn');
+ if (logoutBtn) {
+ logoutBtn.addEventListener('click', async (e) => {
+ e.preventDefault();
+ if (confirm('确定要退出登录吗?')) {
+ try {
+ const res = await fetch('/api/auth/logout', { method: 'POST' });
+ const result = await res.json();
+ if (result.success) {
+ alert('退出登录成功');
+ window.location.href = '/login';
+ } else {
+ alert(result.message || '退出登录失败');
+ }
+ } catch (e) {
+ console.error('Logout failed', e);
+ alert('退出登录出错: ' + e.message);
+ }
+ }
});
-
- if (!response.ok) {
- return false;
- }
-
- const data = await response.json();
- return data.success && data.user.role === 'admin';
- } catch (error) {
- console.error('认证检查失?', error);
- return false;
+ }
+ }
+
+ updateCurrentTime() {
+ const timeElement = document.getElementById('currentTime');
+ if (timeElement) {
+ const now = new Date();
+ const options = {
+ year: 'numeric', month: 'long', day: 'numeric',
+ hour: '2-digit', minute: '2-digit', second: '2-digit'
+ };
+ timeElement.textContent = now.toLocaleString('zh-CN', options);
}
}
async loadUserInfo() {
try {
- const response = await fetch(`${this.apiBase}/auth/me`, {
- credentials: 'include'
- });
-
- if (response.ok) {
- const data = await response.json();
- if (data.success) {
- this.currentUser = data.user;
- }
+ const res = await fetch('/api/auth/me');
+ const result = await res.json();
+ if (result.success && result.data.user) {
+ const user = result.data.user;
+ this.user = user;
+
+ const nameEls = document.querySelectorAll('#adminName, #userName');
+ nameEls.forEach(el => el.textContent = user.name || '管理员');
}
} catch (error) {
- console.error('加载用户信息失败:', error);
+ console.error('Load user info failed:', error);
}
}
-
- async loadStats() {
+
+ // ================= Dashboard =================
+ async initDashboard() {
try {
- const response = await fetch(`${this.apiBase}/admin/stats`, {
- credentials: 'include'
- });
+ const res = await fetch(`${this.apiBase}/stats`);
+ const result = await res.json();
- if (response.ok) {
- const data = await response.json();
- if (data.success) {
- this.stats = data.stats;
- this.updateStatsUI();
- }
+ if (result.success) {
+ const stats = result.data;
+ if (document.getElementById('totalUsers')) document.getElementById('totalUsers').textContent = stats.users;
+ if (document.getElementById('totalStudents')) document.getElementById('totalStudents').textContent = stats.students;
+ if (document.getElementById('totalTeachers')) document.getElementById('totalTeachers').textContent = stats.teachers;
+ if (document.getElementById('totalCourses')) document.getElementById('totalCourses').textContent = stats.courses;
}
- } catch (error) {
- console.error('加载统计数据失败:', error);
- this.showNotification('加载统计数据失败', 'error');
+ } catch (e) {
+ console.error('Load stats failed', e);
}
}
- async loadUsers() {
- try {
- const response = await fetch(`${this.apiBase}/admin/users`, {
- credentials: 'include'
- });
-
- if (response.ok) {
- const data = await response.json();
- if (data.success) {
- this.users = data.users;
- this.renderUsersTable();
- }
- }
- } catch (error) {
- console.error('加载用户数据失败:', error);
- this.showNotification('加载用户数据失败', 'error');
- }
- }
-
- updateStatsUI() {
- // 更新统计卡片
- const statElements = {
- 'totalUsers': 'totalUsers',
- 'totalStudents': 'totalStudents',
- 'totalTeachers': 'totalTeachers',
- 'totalCourses': 'totalCourses',
- 'totalGrades': 'totalGrades',
- 'avgScore': 'avgScore'
- };
-
- Object.entries(statElements).forEach(([key, elementId]) => {
- const element = document.getElementById(elementId);
- if (element && this.stats[key] !== undefined) {
- element.textContent = this.stats[key];
- }
+ // ================= User Management =================
+ async initUserManagement() {
+ this.currentPage = 1;
+ this.pageSize = 10;
+
+ // Bind Filter Events
+ document.getElementById('search').addEventListener('input', () => this.loadUsers());
+ document.getElementById('roleFilter').addEventListener('change', () => this.loadUsers());
+
+ // Bind Modal Events
+ const modalEl = document.getElementById('userModal');
+ this.userModal = new bootstrap.Modal(modalEl);
+
+ document.getElementById('addUserBtn').addEventListener('click', () => {
+ document.getElementById('userForm').reset();
+ document.getElementById('userModalTitle').textContent = '新增用户';
+ document.getElementById('isEdit').value = 'false';
+ document.getElementById('userId').readOnly = false;
+ this.userModal.show();
});
-
- // 更新时间
- const timeElement = document.getElementById('currentTime');
- if (timeElement) {
- timeElement.textContent = new Date().toLocaleString();
+
+ document.getElementById('saveUserBtn').addEventListener('click', () => this.saveUser());
+
+ // Initial Load
+ await this.loadUsers();
+ }
+
+ async loadUsers() {
+ const search = document.getElementById('search').value;
+ const role = document.getElementById('roleFilter').value;
+ const tbody = document.getElementById('userTableBody');
+
+ tbody.innerHTML = '
加载中... ';
+
+ try {
+ const query = new URLSearchParams({
+ page: this.currentPage,
+ limit: this.pageSize,
+ search,
+ role
+ });
+
+ const res = await fetch(`${this.apiBase}/users?${query}`);
+ const result = await res.json();
+
+ if (result.success) {
+ this.renderUserTable(result.data, result.pagination);
+ } else {
+ tbody.innerHTML = `${result.message} `;
+ }
+ } catch (e) {
+ console.error(e);
+ tbody.innerHTML = '加载失败 ';
}
}
-
- renderUsersTable() {
- const tableBody = document.getElementById('usersTableBody');
- if (!tableBody) return;
-
- if (this.users.length === 0) {
- tableBody.innerHTML = `
-
-
-
-
-
- `;
+
+ renderUserTable(users, pagination) {
+ const tbody = document.getElementById('userTableBody');
+
+ if (users.length === 0) {
+ tbody.innerHTML = '暂无用户 ';
return;
}
-
- tableBody.innerHTML = this.users.map(user => {
- const roleClass = this.getRoleClass(user.role);
- return `
-
-
- ${user.user_id}
- ${user.full_name}
- ${user.role}
- ${user.class_name || 'N/A'}
- ${user.email || 'N/A'}
-
-
-
- 编辑
-
-
- 删除
-
-
-
-
- `;
- }).join('');
- }
-
- getRoleClass(role) {
- switch (role) {
- case 'admin': return 'role-admin';
- case 'teacher': return 'role-teacher';
- case 'student': return 'role-student';
- default: return 'role-default';
- }
- }
-
- bindEvents() {
- // 导航菜单点击
- document.querySelectorAll('.nav-link').forEach(link => {
- link.addEventListener('click', (e) => {
- e.preventDefault();
- const page = link.dataset.page;
- this.loadPage(page);
+
+ tbody.innerHTML = users.map(u => `
+
+ ${u.id}
+ ${u.name}
+ ${this.getRoleName(u.role)}
+ ${u.class || '-'}
+ ${new Date(u.created_at).toLocaleDateString()}
+
+
+
+
+
+
+
+
+
+ `).join('');
+
+ // Bind Action Buttons
+ document.querySelectorAll('.btn-edit-user').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const user = JSON.parse(btn.dataset.user);
+ this.openEditUserModal(user);
});
});
-
- // 搜索按钮
- document.getElementById('searchBtn')?.addEventListener('click', () => {
- this.handleSearch();
- });
-
- // 重置按钮
- document.getElementById('resetBtn')?.addEventListener('click', () => {
- this.resetFilters();
- });
-
- // 添加用户按钮
- document.getElementById('addUserBtn')?.addEventListener('click', () => {
- this.addUser();
- });
-
- // 导出按钮
- document.getElementById('exportBtn')?.addEventListener('click', () => {
- this.exportUsers();
- });
-
- // 批量删除按钮
- document.getElementById('batchDeleteBtn')?.addEventListener('click', () => {
- this.batchDeleteUsers();
- });
-
- // 表格操作按钮事件委托
- document.addEventListener('click', (e) => {
- if (e.target.closest('.btn-edit')) {
- const userId = e.target.closest('.btn-edit').dataset.id;
- this.editUser(userId);
- }
-
- if (e.target.closest('.btn-delete')) {
- const userId = e.target.closest('.btn-delete').dataset.id;
- this.deleteUser(userId);
- }
- });
-
- // 退出登? document.getElementById('logoutBtn')?.addEventListener('click', () => {
- this.handleLogout();
- });
-
- // 刷新按钮
- document.getElementById('refreshBtn')?.addEventListener('click', () => {
- this.refreshData();
+
+ document.querySelectorAll('.btn-delete-user').forEach(btn => {
+ btn.addEventListener('click', () => {
+ if(confirm('确定要删除该用户吗?此操作不可恢复。')) {
+ this.deleteUser(btn.dataset.id);
+ }
+ });
});
+
+ this.renderPagination(pagination);
}
-
- async loadPage(page) {
- // 这里可以实现页面切换逻辑
- // 暂时使用简单跳? switch (page) {
- case 'users':
- window.location.href = '/admin/user_management';
- break;
- case 'students':
- window.location.href = '/admin/student_management';
- break;
- case 'teachers':
- // 可以跳转到教师管理页? break;
- case 'grades':
- window.location.href = '/teacher/grade_management';
- break;
- case 'settings':
- // 可以跳转到系统设置页? break;
+
+ getRoleBadgeColor(role) {
+ switch(role) {
+ case 'admin': return 'danger';
+ case 'teacher': return 'info';
+ case 'student': return 'success';
+ default: return 'secondary';
}
}
-
- handleSearch() {
- const userId = document.getElementById('userIdFilter')?.value || '';
- const name = document.getElementById('nameFilter')?.value || '';
- const role = document.getElementById('roleFilter')?.value || '';
- const className = document.getElementById('classFilter')?.value || '';
-
- // 这里可以实现搜索逻辑
- this.showNotification('搜索功能待实?, 'info');
+
+ getRoleName(role) {
+ switch(role) {
+ case 'admin': return '管理员';
+ case 'teacher': return '教师';
+ case 'student': return '学生';
+ default: role;
+ }
}
-
- resetFilters() {
- document.getElementById('userIdFilter').value = '';
- document.getElementById('nameFilter').value = '';
- document.getElementById('roleFilter').value = '';
- document.getElementById('classFilter').value = '';
+
+ renderPagination(pagination) {
+ const el = document.getElementById('pagination');
+ let html = '';
- // 重新加载数据
+ if (pagination.page > 1) {
+ html += `上一页 `;
+ } else {
+ html += `上一页 `;
+ }
+
+ for (let i = 1; i <= pagination.pages; i++) {
+ html += ``;
+ }
+
+ if (pagination.page < pagination.pages) {
+ html += `下一页 `;
+ } else {
+ html += `下一页 `;
+ }
+
+ el.innerHTML = html;
+ }
+
+ changePage(page) {
+ this.currentPage = page;
this.loadUsers();
}
-
- async addUser() {
- // 这里可以打开添加用户模态框
- const userData = {
- user_id: prompt('请输入用户ID:'),
- full_name: prompt('请输入姓?'),
- role: prompt('请输入角?(admin/teacher/student):'),
- email: prompt('请输入邮?'),
- class_name: prompt('请输入班?(学生/教师可?:')
- };
-
- if (!userData.user_id || !userData.full_name || !userData.role) {
- this.showNotification('用户ID、姓名和角色为必填项', 'error');
+
+ openEditUserModal(user) {
+ document.getElementById('userForm').reset();
+ document.getElementById('userModalTitle').textContent = '编辑用户';
+ document.getElementById('isEdit').value = 'true';
+ document.getElementById('userId').value = user.id;
+ document.getElementById('userId').readOnly = true;
+ document.getElementById('userNameInput').value = user.name;
+ document.getElementById('userRole').value = user.role;
+ document.getElementById('userClass').value = user.class || '';
+ this.userModal.show();
+ }
+
+ async saveUser() {
+ const form = document.getElementById('userForm');
+ const formData = new FormData(form);
+ const data = Object.fromEntries(formData.entries());
+ const isEdit = data.isEdit === 'true';
+
+ // Validation
+ if (!data.id || !data.name || !data.role) {
+ alert('请填写必填字段');
return;
}
-
- try {
- const response = await fetch(`${this.apiBase}/admin/users`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify(userData)
- });
-
- const data = await response.json();
- if (data.success) {
- this.showNotification('用户添加成功', 'success');
- await this.loadUsers();
- } else {
- this.showNotification(data.message || '添加失败', 'error');
- }
- } catch (error) {
- console.error('添加用户失败:', error);
- this.showNotification('添加用户失败', 'error');
- }
- }
-
- async exportUsers() {
- try {
- const response = await fetch(`${this.apiBase}/admin/users/export`, {
- credentials: 'include'
- });
-
- if (response.ok) {
- const blob = await response.blob();
- const url = window.URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `用户列表_${new Date().toISOString().split('T')[0]}.xlsx`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- window.URL.revokeObjectURL(url);
- }
- } catch (error) {
- console.error('导出失败:', error);
- this.showNotification('导出失败', 'error');
- }
- }
-
- async batchDeleteUsers() {
- const checkboxes = document.querySelectorAll('.user-checkbox:checked');
- if (checkboxes.length === 0) {
- this.showNotification('请选择要删除的用户', 'warning');
+
+ if (data.role === 'student' && !data.class) {
+ alert('学生角色必须填写班级');
return;
}
-
- if (!confirm(`确定要删除选中?${checkboxes.length} 个用户吗?`)) {
- return;
- }
-
- const userIds = Array.from(checkboxes).map(cb => cb.dataset.id);
try {
- const response = await fetch(`${this.apiBase}/admin/users/batch`, {
- method: 'DELETE',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({ userIds })
- });
-
- const data = await response.json();
- if (data.success) {
- this.showNotification(`成功删除 ${userIds.length} 个用户`, 'success');
- await this.loadUsers();
+ let res;
+ if (isEdit) {
+ res = await fetch(`${this.apiBase}/users/${data.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ });
} else {
- this.showNotification(data.message || '删除失败', 'error');
+ res = await fetch(`${this.apiBase}/users`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ });
}
- } catch (error) {
- console.error('批量删除失败:', error);
- this.showNotification('批量删除失败', 'error');
- }
- }
-
- async editUser(userId) {
- const user = this.users.find(u => u.id == userId);
- if (!user) return;
-
- // 这里可以打开编辑模态框
- const newName = prompt('请输入新的姓?', user.full_name);
- if (newName === null) return;
-
- const newRole = prompt('请输入新的角?', user.role);
- if (newRole === null) return;
-
- try {
- const response = await fetch(`${this.apiBase}/admin/users/${userId}`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({
- full_name: newName,
- role: newRole,
- email: user.email,
- class_name: user.class_name
- })
- });
- const data = await response.json();
- if (data.success) {
- this.showNotification('用户更新成功', 'success');
- await this.loadUsers();
+ const result = await res.json();
+
+ if (result.success) {
+ alert(isEdit ? '更新成功' : '创建成功');
+ this.userModal.hide();
+ this.loadUsers();
} else {
- this.showNotification(data.message || '更新失败', 'error');
+ alert(result.message || '操作失败');
}
- } catch (error) {
- console.error('更新用户失败:', error);
- this.showNotification('更新用户失败', 'error');
+ } catch (e) {
+ console.error(e);
+ alert('系统错误');
}
}
-
- async deleteUser(userId) {
- if (!confirm('确定要删除这个用户吗?)) {
- return;
- }
-
+
+ async deleteUser(id) {
try {
- const response = await fetch(`${this.apiBase}/admin/users/${userId}`, {
- method: 'DELETE',
- credentials: 'include'
- });
+ const res = await fetch(`${this.apiBase}/users/${id}`, { method: 'DELETE' });
+ const result = await res.json();
- const data = await response.json();
- if (data.success) {
- this.showNotification('用户删除成功', 'success');
- await this.loadUsers();
+ if (result.success) {
+ alert('删除成功');
+ this.loadUsers();
} else {
- this.showNotification(data.message || '删除失败', 'error');
+ alert(result.message || '删除失败');
}
- } catch (error) {
- console.error('删除用户失败:', error);
- this.showNotification('删除用户失败', 'error');
+ } catch (e) {
+ console.error(e);
+ alert('系统错误');
}
}
- async handleLogout() {
- try {
- const response = await fetch(`${this.apiBase}/auth/logout`, {
- method: 'POST',
- credentials: 'include'
- });
-
- if (response.ok) {
- window.location.href = '/login';
- }
- } catch (error) {
- console.error('退出登录失?', error);
- }
- }
-
- async refreshData() {
- await this.loadStats();
- await this.loadUsers();
- this.showNotification('数据已刷?, 'success');
- }
-
- updateUI() {
- // 更新用户信息
- if (this.currentUser) {
- const userInfoElements = document.querySelectorAll('.user-info');
- userInfoElements.forEach(el => {
- el.textContent = `${this.currentUser.full_name} (${this.currentUser.role})`;
- });
- }
- }
-
- async initCharts() {
- // 加载Chart.js? await this.loadChartLibrary();
+ // ================= Student Management =================
+ async initStudentManagement() {
+ this.studentCurrentPage = 1;
+ this.studentPageSize = 10;
- // 初始化用户分布饼? this.initUserDistributionChart();
+ // Bind Filter Events
+ document.getElementById('studentSearch').addEventListener('input', () => this.loadStudents());
- // 初始化成绩分布柱状图
- this.initGradeDistributionChart();
- }
-
- showNotification(message, type = 'info') {
- // 创建通知元素
- const notification = document.createElement('div');
- notification.className = `notification notification-${type}`;
- notification.innerHTML = `
-
- ${message}
- ×
- `;
+ // Bind Modal Events
+ const modalEl = document.getElementById('studentModal');
+ this.studentModal = new bootstrap.Modal(modalEl);
- // 添加到页? document.body.appendChild(notification);
-
- // 添加关闭事件
- notification.querySelector('.notification-close').addEventListener('click', () => {
- notification.remove();
+ document.getElementById('addStudentBtn').addEventListener('click', () => {
+ document.getElementById('studentForm').reset();
+ document.getElementById('studentModalTitle').textContent = '新增学生';
+ document.getElementById('studentIsEdit').value = 'false';
+ document.getElementById('studentId').readOnly = false;
+ this.studentModal.show();
});
- // 自动移除
- setTimeout(() => {
- if (notification.parentNode) {
- notification.remove();
- }
- }, 5000);
- }
-
- async loadChartLibrary() {
- if (typeof Chart !== 'undefined') return;
-
- return new Promise((resolve, reject) => {
- const script = document.createElement('script');
- script.src = 'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js';
- script.onload = resolve;
- script.onerror = reject;
- document.head.appendChild(script);
- });
- }
-
- initUserDistributionChart() {
- const ctx = document.getElementById('userDistributionChart');
- if (!ctx) return;
-
- // 模拟数据
- const data = {
- labels: ['学生', '教师', '管理?],
- datasets: [{
- data: [this.stats.totalStudents || 100, this.stats.totalTeachers || 20, 1],
- backgroundColor: [
- 'rgba(54, 162, 235, 0.8)',
- 'rgba(255, 206, 86, 0.8)',
- 'rgba(255, 99, 132, 0.8)'
- ]
- }]
- };
+ document.getElementById('saveStudentBtn').addEventListener('click', () => this.saveStudent());
- new Chart(ctx, {
- type: 'pie',
- data: data,
- options: {
- responsive: true,
- plugins: {
- legend: {
- position: 'bottom'
+ // Initial Load
+ await this.loadStudents();
+ }
+
+ async loadStudents() {
+ const search = document.getElementById('studentSearch').value;
+ const tbody = document.getElementById('studentTableBody');
+
+ tbody.innerHTML = '加载中... ';
+
+ try {
+ const query = new URLSearchParams({
+ page: this.studentCurrentPage,
+ limit: this.studentPageSize,
+ search
+ });
+
+ const res = await fetch(`${this.apiBase}/students?${query}`);
+ const result = await res.json();
+
+ if (result.success) {
+ this.renderStudentTable(result.data, result.pagination);
+ } else {
+ tbody.innerHTML = `${result.message} `;
+ }
+ } catch (e) {
+ console.error(e);
+ tbody.innerHTML = '加载失败 ';
+ }
+ }
+
+ renderStudentTable(students, pagination) {
+ const tbody = document.getElementById('studentTableBody');
+
+ if (students.length === 0) {
+ tbody.innerHTML = '暂无学生 ';
+ return;
+ }
+
+ tbody.innerHTML = students.map(s => `
+
+ ${s.id}
+ ${s.name}
+ ${s.class || '-'}
+ ${s.major || '-'}
+ ${s.grade || '-'}
+ ${s.contact_info || '-'}
+
+
+
+
+
+
+
+
+
+ `).join('');
+
+ // Bind Action Buttons
+ document.querySelectorAll('.btn-edit-student').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const student = JSON.parse(btn.dataset.student);
+ this.openEditStudentModal(student);
+ });
+ });
+
+ document.querySelectorAll('.btn-delete-student').forEach(btn => {
+ btn.addEventListener('click', () => {
+ if(confirm('确定要删除该学生吗?这将同时删除其用户账号。')) {
+ this.deleteStudent(btn.dataset.id);
+ }
+ });
+ });
+
+ this.renderStudentPagination(pagination);
+ }
+
+ renderStudentPagination(pagination) {
+ const el = document.getElementById('studentPagination');
+ let html = '';
+
+ if (pagination.page > 1) {
+ html += `上一页 `;
+ } else {
+ html += `上一页 `;
+ }
+
+ for (let i = 1; i <= pagination.pages; i++) {
+ html += ``;
+ }
+
+ if (pagination.page < pagination.pages) {
+ html += `下一页 `;
+ } else {
+ html += `下一页 `;
+ }
+
+ el.innerHTML = html;
+ }
+
+ changeStudentPage(page) {
+ this.studentCurrentPage = page;
+ this.loadStudents();
+ }
+
+ openEditStudentModal(student) {
+ document.getElementById('studentForm').reset();
+ document.getElementById('studentModalTitle').textContent = '编辑学生';
+ document.getElementById('studentIsEdit').value = 'true';
+ document.getElementById('studentId').value = student.id;
+ document.getElementById('studentId').readOnly = true;
+ document.getElementById('studentNameInput').value = student.name;
+ document.getElementById('studentClass').value = student.class || '';
+ document.getElementById('studentMajor').value = student.major || '';
+ document.getElementById('studentGrade').value = student.grade || '';
+ document.getElementById('studentContact').value = student.contact_info || '';
+ this.studentModal.show();
+ }
+
+ async saveStudent() {
+ const form = document.getElementById('studentForm');
+ const formData = new FormData(form);
+ const data = Object.fromEntries(formData.entries());
+ const isEdit = data.isEdit === 'true';
+
+ // Validation
+ if (!data.id || !data.name || !data.class) {
+ alert('学号、姓名和班级为必填项');
+ return;
+ }
+
+ try {
+ let res;
+ if (isEdit) {
+ res = await fetch(`${this.apiBase}/students/${data.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ });
+ } else {
+ res = await fetch(`${this.apiBase}/students`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ });
+ }
+
+ const result = await res.json();
+
+ if (result.success) {
+ alert(isEdit ? '更新成功' : '创建成功');
+ this.studentModal.hide();
+ this.loadStudents();
+ } else {
+ alert(result.message || '操作失败');
+ }
+ } catch (e) {
+ console.error(e);
+ alert('系统错误');
+ }
+ }
+
+ async deleteStudent(id) {
+ try {
+ const res = await fetch(`${this.apiBase}/students/${id}`, { method: 'DELETE' });
+ const result = await res.json();
+
+ if (result.success) {
+ alert('删除成功');
+ this.loadStudents();
+ } else {
+ alert(result.message || '删除失败');
+ }
+ } catch (e) {
+ console.error(e);
+ alert('系统错误');
+ }
+ }
+
+ // ================= Teacher Management =================
+ async initTeacherManagement() {
+ this.teacherCurrentPage = 1;
+ this.teacherPageSize = 10;
+
+ // Bind Filter Events
+ document.getElementById('teacherSearch').addEventListener('input', () => this.loadTeachers());
+
+ // Bind Modal Events
+ const modalEl = document.getElementById('teacherModal');
+ this.teacherModal = new bootstrap.Modal(modalEl);
+
+ document.getElementById('addTeacherBtn').addEventListener('click', () => {
+ document.getElementById('teacherForm').reset();
+ document.getElementById('teacherModalTitle').textContent = '新增教师';
+ document.getElementById('teacherIsEdit').value = 'false';
+ document.getElementById('teacherId').readOnly = false;
+ this.teacherModal.show();
+ });
+
+ document.getElementById('saveTeacherBtn').addEventListener('click', () => this.saveTeacher());
+
+ // Initial Load
+ await this.loadTeachers();
+ }
+
+ async loadTeachers() {
+ const search = document.getElementById('teacherSearch').value;
+ const tbody = document.getElementById('teacherTableBody');
+
+ tbody.innerHTML = '加载中... ';
+
+ try {
+ const query = new URLSearchParams({
+ page: this.teacherCurrentPage,
+ limit: this.teacherPageSize,
+ search
+ });
+
+ const res = await fetch(`${this.apiBase}/teachers?${query}`);
+ const result = await res.json();
+
+ if (result.success) {
+ this.renderTeacherTable(result.data, result.pagination);
+ } else {
+ tbody.innerHTML = `${result.message} `;
+ }
+ } catch (e) {
+ console.error(e);
+ tbody.innerHTML = '加载失败 ';
+ }
+ }
+
+ renderTeacherTable(teachers, pagination) {
+ const tbody = document.getElementById('teacherTableBody');
+
+ if (teachers.length === 0) {
+ tbody.innerHTML = '暂无教师 ';
+ return;
+ }
+
+ tbody.innerHTML = teachers.map(t => `
+
+ ${t.id}
+ ${t.name}
+ ${t.title || '-'}
+ ${t.department || '-'}
+ ${t.contact_info || '-'}
+
+
+
+
+
+
+
+
+
+ `).join('');
+
+ // Bind Action Buttons
+ document.querySelectorAll('.btn-edit-teacher').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const teacher = JSON.parse(btn.dataset.teacher);
+ this.openEditTeacherModal(teacher);
+ });
+ });
+
+ document.querySelectorAll('.btn-delete-teacher').forEach(btn => {
+ btn.addEventListener('click', () => {
+ if(confirm('确定要删除该教师吗?这将同时删除其用户账号。')) {
+ this.deleteTeacher(btn.dataset.id);
+ }
+ });
+ });
+
+ this.renderTeacherPagination(pagination);
+ }
+
+ renderTeacherPagination(pagination) {
+ const el = document.getElementById('teacherPagination');
+ let html = '';
+
+ if (pagination.page > 1) {
+ html += `上一页 `;
+ } else {
+ html += `上一页 `;
+ }
+
+ for (let i = 1; i <= pagination.pages; i++) {
+ html += ``;
+ }
+
+ if (pagination.page < pagination.pages) {
+ html += `下一页 `;
+ } else {
+ html += `下一页 `;
+ }
+
+ el.innerHTML = html;
+ }
+
+ changeTeacherPage(page) {
+ this.teacherCurrentPage = page;
+ this.loadTeachers();
+ }
+
+ openEditTeacherModal(teacher) {
+ document.getElementById('teacherForm').reset();
+ document.getElementById('teacherModalTitle').textContent = '编辑教师';
+ document.getElementById('teacherIsEdit').value = 'true';
+ document.getElementById('teacherId').value = teacher.id;
+ document.getElementById('teacherId').readOnly = true;
+ document.getElementById('teacherNameInput').value = teacher.name;
+ document.getElementById('teacherDepartment').value = teacher.department || '';
+ document.getElementById('teacherTitle').value = teacher.title || '';
+ document.getElementById('teacherContact').value = teacher.contact_info || '';
+ this.teacherModal.show();
+ }
+
+ async saveTeacher() {
+ const form = document.getElementById('teacherForm');
+ const formData = new FormData(form);
+ const data = Object.fromEntries(formData.entries());
+ const isEdit = data.isEdit === 'true';
+
+ // Validation
+ if (!data.id || !data.name) {
+ alert('工号和姓名为必填项');
+ return;
+ }
+
+ try {
+ let res;
+ if (isEdit) {
+ res = await fetch(`${this.apiBase}/teachers/${data.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ });
+ } else {
+ res = await fetch(`${this.apiBase}/teachers`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ });
+ }
+
+ const result = await res.json();
+
+ if (result.success) {
+ alert(isEdit ? '更新成功' : '创建成功');
+ this.teacherModal.hide();
+ this.loadTeachers();
+ } else {
+ alert(result.message || '操作失败');
+ }
+ } catch (e) {
+ console.error(e);
+ alert('系统错误');
+ }
+ }
+
+ async deleteTeacher(id) {
+ try {
+ const res = await fetch(`${this.apiBase}/teachers/${id}`, { method: 'DELETE' });
+ const result = await res.json();
+
+ if (result.success) {
+ alert('删除成功');
+ this.loadTeachers();
+ } else {
+ alert(result.message || '删除失败');
+ }
+ } catch (e) {
+ console.error(e);
+ alert('系统错误');
+ }
+ }
+
+ // ================= Grade Statistics =================
+ async initGradeStatistics() {
+ try {
+ const res = await fetch(`${this.apiBase}/grade-stats`);
+ const result = await res.json();
+
+ if (result.success) {
+ const stats = result.data;
+ this.renderGradeStatsTable(stats);
+ this.renderGradeCharts(stats);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ renderGradeStatsTable(stats) {
+ const tbody = document.getElementById('gradeStatsBody');
+ if (!tbody) return;
+
+ if (stats.length === 0) {
+ tbody.innerHTML = '暂无数据 ';
+ return;
+ }
+
+ tbody.innerHTML = stats.map(s => `
+
+ ${s.course_code || '-'}
+ ${s.course_name}
+ ${s.teacher_name || '未分配'}
+ ${s.student_count}
+ ${s.avg_score}
+ ${s.max_score || '-'}
+ ${s.min_score || '-'}
+
+
+
+
+ `).join('');
+ }
+
+ renderGradeCharts(stats) {
+ if (!window.Chart) return;
+
+ // 1. Course Average Scores Bar Chart
+ const ctxBar = document.getElementById('courseAvgChart');
+ if (ctxBar) {
+ new Chart(ctxBar, {
+ type: 'bar',
+ data: {
+ labels: stats.map(s => s.course_name),
+ datasets: [{
+ label: '平均分',
+ data: stats.map(s => s.avg_score),
+ backgroundColor: 'rgba(78, 115, 223, 0.5)',
+ borderColor: 'rgba(78, 115, 223, 1)',
+ borderWidth: 1
+ }]
+ },
+ options: {
+ responsive: true,
+ scales: {
+ y: { beginAtZero: true, max: 100 }
}
}
- }
- });
- }
-
- initGradeDistributionChart() {
- const ctx = document.getElementById('gradeDistributionChart');
- if (!ctx) return;
+ });
+ }
- // 模拟数据
- const data = {
- labels: ['A', 'B', 'C', 'D', 'F'],
- datasets: [{
- label: '成绩分布',
- data: [25, 35, 20, 15, 5],
- backgroundColor: [
- 'rgba(75, 192, 192, 0.8)',
- 'rgba(54, 162, 235, 0.8)',
- 'rgba(255, 206, 86, 0.8)',
- 'rgba(255, 159, 64, 0.8)',
- 'rgba(255, 99, 132, 0.8)'
- ]
- }]
- };
-
- new Chart(ctx, {
- type: 'bar',
- data: data,
- options: {
- responsive: true,
- scales: {
- y: {
- beginAtZero: true,
- ticks: {
- stepSize: 10
- }
+ // 2. Overall Pass Rate Pie Chart
+ const ctxPie = document.getElementById('passRateChart');
+ if (ctxPie) {
+ const totalStudents = stats.reduce((sum, s) => sum + s.student_count, 0);
+ const totalPass = stats.reduce((sum, s) => sum + parseInt(s.pass_count), 0);
+ const totalFail = totalStudents - totalPass;
+
+ new Chart(ctxPie, {
+ type: 'doughnut',
+ data: {
+ labels: ['及格', '不及格'],
+ datasets: [{
+ data: [totalPass, totalFail],
+ backgroundColor: ['#1cc88a', '#e74a3b'],
+ hoverOffset: 4
+ }]
+ },
+ options: {
+ responsive: true,
+ plugins: {
+ legend: { position: 'bottom' }
}
}
+ });
+ }
+ }
+
+ // ================= System Settings =================
+ async initSystemSettings() {
+ const form = document.getElementById('basicSettingsForm');
+ if (!form) return;
+
+ // Load current settings
+ try {
+ const response = await fetch('/api/admin/settings');
+ const result = await response.json();
+ if (result.success) {
+ const settings = result.data;
+ const nameInput = form.querySelector('input[type="text"]');
+ const semesterSelect = form.querySelector('select');
+ const courseSwitch = document.getElementById('courseSelectionSwitch');
+ const gradeSwitch = document.getElementById('gradeCheckSwitch');
+
+ if (nameInput) nameInput.value = settings.system_name || '';
+ if (semesterSelect) semesterSelect.value = settings.current_semester || '';
+ if (courseSwitch) courseSwitch.checked = settings.allow_course_selection === '1';
+ if (gradeSwitch) gradeSwitch.checked = settings.allow_grade_check === '1';
+ }
+ } catch (err) {
+ console.error('加载设置失败:', err);
+ }
+
+ form.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const settings = {
+ system_name: form.querySelector('input[type="text"]').value,
+ current_semester: form.querySelector('select').value,
+ allow_course_selection: document.getElementById('courseSelectionSwitch').checked ? '1' : '0',
+ allow_grade_check: document.getElementById('gradeCheckSwitch').checked ? '1' : '0'
+ };
+
+ try {
+ const response = await fetch('/api/admin/settings', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(settings)
+ });
+ const result = await response.json();
+ if (result.success) {
+ alert('系统设置已保存');
+ } else {
+ alert('保存失败: ' + result.message);
+ }
+ } catch (err) {
+ alert('保存出错');
}
});
+
+ // Data Maintenance Buttons
+ const backupBtn = document.querySelector('.btn-outline-primary i.fa-database')?.parentElement;
+ const clearCacheBtn = document.querySelector('.btn-outline-warning i.fa-trash-alt')?.parentElement;
+ const resetPassBtn = document.querySelector('.btn-outline-danger i.fa-history')?.parentElement;
+
+ if (backupBtn) {
+ backupBtn.addEventListener('click', async () => {
+ if (!confirm('确定要立即备份数据库吗?')) return;
+ try {
+ const res = await fetch('/api/admin/maintenance/backup', { method: 'POST' });
+ const result = await res.json();
+ if (result.success) alert('备份成功!文件已保存到 backups 目录: ' + result.data.filename);
+ else alert('备份失败: ' + result.message);
+ } catch (err) { alert('请求失败'); }
+ });
+ }
+
+ if (clearCacheBtn) {
+ clearCacheBtn.addEventListener('click', async () => {
+ try {
+ const res = await fetch('/api/admin/maintenance/clear-cache', { method: 'POST' });
+ const result = await res.json();
+ if (result.success) alert('系统缓存已清理');
+ else alert('清理失败');
+ } catch (err) { alert('请求失败'); }
+ });
+ }
+
+ if (resetPassBtn) {
+ resetPassBtn.addEventListener('click', async () => {
+ if (!confirm('确定要重置所有学生密码为 123456 吗?此操作不可撤销!')) return;
+ try {
+ const res = await fetch('/api/admin/maintenance/reset-passwords', { method: 'POST' });
+ const result = await res.json();
+ if (result.success) alert('所有学生密码已重置为 123456');
+ else alert('重置失败');
+ } catch (err) { alert('请求失败'); }
+ });
+ }
+ }
+
+ // ================= Data Export =================
+ async initDataExport() {
+ const studentExportBtn = document.getElementById('exportStudentsBtn');
+ const teacherExportBtn = document.getElementById('exportTeachersBtn');
+ const gradeExportBtn = document.getElementById('exportGradesBtn');
+
+ if (studentExportBtn) {
+ studentExportBtn.addEventListener('click', () => {
+ window.location.href = '/api/admin/export/students';
+ });
+ }
+
+ if (teacherExportBtn) {
+ teacherExportBtn.addEventListener('click', () => {
+ window.location.href = '/api/admin/export/teachers';
+ });
+ }
+
+ if (gradeExportBtn) {
+ gradeExportBtn.addEventListener('click', () => {
+ window.location.href = '/api/admin/export/grades';
+ });
+ }
+ }
+
+ // ================= Operation Logs =================
+ async initOperationLogs() {
+ const tbody = document.getElementById('logsTableBody');
+ if (!tbody) return;
+
+ try {
+ const response = await fetch('/api/admin/logs');
+ const result = await response.json();
+ if (result.success) {
+ const logs = result.data;
+ if (logs.length === 0) {
+ tbody.innerHTML = '暂无操作日志 ';
+ return;
+ }
+ tbody.innerHTML = logs.map(log => `
+
+ ${log.created_at}
+ ${log.user_id} (${log.user_name || '未知'})
+ ${log.operation_type}
+ ${log.description}
+ ${log.ip_address || '-'}
+
+ `).join('');
+ }
+ } catch (err) {
+ console.error('获取日志失败:', err);
+ tbody.innerHTML = '加载失败 ';
+ }
}
}
+document.addEventListener('DOMContentLoaded', () => {
+ window.adminManager = new AdminManager();
+});
diff --git a/frontend/public/js/auth.js b/frontend/public/js/auth.js
index 41c3d2c..724826a 100644
--- a/frontend/public/js/auth.js
+++ b/frontend/public/js/auth.js
@@ -196,11 +196,14 @@ class AuthManager {
});
const result = await response.json();
if (result.success) {
+ alert('退出登录成功');
window.location.href = '/login';
+ } else {
+ alert(result.message || '退出登录失败');
}
} catch (error) {
console.error('Logout error:', error);
- window.location.href = '/login';
+ alert('退出登录出错');
}
}
}
diff --git a/frontend/public/js/teacher.js b/frontend/public/js/teacher.js
index 83b0540..deb9740 100644
--- a/frontend/public/js/teacher.js
+++ b/frontend/public/js/teacher.js
@@ -7,10 +7,24 @@ class TeacherManager {
this.init();
}
- init() {
- this.initDashboard();
+ async init() {
this.updateCurrentTime();
setInterval(() => this.updateCurrentTime(), 1000);
+ await this.loadUserInfo();
+
+ // 页面路由逻辑
+ if (document.getElementById('courseList')) {
+ this.initDashboard();
+ }
+ if (document.getElementById('studentTableBody') && document.getElementById('courseSelect')) {
+ this.initGradeEntry();
+ }
+ if (document.getElementById('gradeTableBody')) {
+ this.initGradeManagement();
+ }
+ if (document.getElementById('profileForm')) {
+ this.initProfilePage();
+ }
}
updateCurrentTime() {
@@ -24,27 +38,100 @@ class TeacherManager {
timeElement.textContent = now.toLocaleString('zh-CN', options);
}
}
+
+ async loadUserInfo() {
+ try {
+ const res = await fetch('/api/auth/me');
+ const result = await res.json();
+ if (result.success && result.data.user) {
+ const user = result.data.user;
+ this.user = user;
+
+ // Update Sidebar
+ const nameEls = document.querySelectorAll('#teacherName, #userName, #profileName');
+ const idEls = document.querySelectorAll('#teacherId, #profileId, #inputTeacherId');
+
+ nameEls.forEach(el => el.textContent = user.name);
+ idEls.forEach(el => {
+ if (el.tagName === 'INPUT') el.value = user.id;
+ else el.textContent = user.id;
+ });
+
+ // Profile Page Specific
+ const inputName = document.getElementById('inputName');
+ const inputClass = document.getElementById('inputClass');
+ if (inputName) inputName.value = user.name;
+ if (inputClass) inputClass.value = user.class || '';
+ }
+ } catch (error) {
+ console.error('Load user info failed:', error);
+ }
+ }
+ // ================= Dashboard =================
async initDashboard() {
- // 检查是否在仪表板页面
- if (!document.getElementById('courseList')) return;
+ await Promise.all([
+ this.loadCourses(),
+ this.loadManagedClasses()
+ ]);
+ this.initAddCourse();
+ this.initEditCourse();
+ }
+
+ async loadManagedClasses() {
+ const managedClassesSection = document.getElementById('managedClassesSection');
+ const managedClassList = document.getElementById('managedClassList');
+ const classCountEl = document.getElementById('classCount');
+ if (!managedClassList) return;
+
+ try {
+ const res = await fetch(`${this.apiBase}/my-classes`);
+ const result = await res.json();
+
+ if (result.success && result.data.classes.length > 0) {
+ const classes = result.data.classes;
+ classCountEl.textContent = classes.length;
+ managedClassesSection.style.display = 'block';
+
+ managedClassList.innerHTML = classes.map(c => `
+
+
+
+
+
+
+
+
${c.class_name}
+
${c.major || '专业未设置'} | ${c.grade}级
+
+
+
+
+ `).join('');
+ } else {
+ classCountEl.textContent = '0';
+ managedClassesSection.style.display = 'none';
+ }
+ } catch (e) {
+ console.error('Load managed classes failed', e);
+ }
+ }
+
+ async loadCourses() {
try {
const response = await fetch(`${this.apiBase}/courses`);
const result = await response.json();
if (result.success) {
- this.renderDashboard(result.data.courses);
- } else {
- if (window.authManager) {
- window.authManager.showNotification(result.message || '获取课程失败', 'error');
- }
+ this.courses = result.data.courses;
+ this.renderDashboard(this.courses);
}
} catch (error) {
- console.error('Fetch teacher data failed:', error);
+ console.error('Fetch courses failed:', error);
}
}
-
+
renderDashboard(courses) {
const courseList = document.getElementById('courseList');
if (!courseList) return;
@@ -57,16 +144,22 @@ class TeacherManager {
courseList.innerHTML = courses.map(course => `
-
-
-
+
+
+
${course.course_code || 'CODE'}
-
${course.credit} 学分
+
+
+
+
+ ${course.credit} 学分
+
${course.course_name}
- 学生人数: ${course.student_count || 0}
+ ${course.class_name || '班级未指定'}
`).join('');
- // 更新统计数据
document.getElementById('courseCount').textContent = courses.length;
+ // Calculate total students
const totalStudents = courses.reduce((sum, c) => sum + (c.student_count || 0), 0);
document.getElementById('totalStudents').textContent = totalStudents;
}
-}
-// 初始化
-document.addEventListener('DOMContentLoaded', () => {
- window.teacherManager = new TeacherManager();
-
- // 从 Session 获取用户信息并更新 UI
- fetch('/api/auth/me')
- .then(res => res.json())
- .then(result => {
- if (result.success && result.data.user) {
- const user = result.data.user;
- const nameEl = document.getElementById('userName');
- const teacherNameEl = document.getElementById('teacherName');
+ initAddCourse() {
+ const btn = document.getElementById('addCourseBtn');
+ const modalEl = document.getElementById('addCourseModal');
+ const saveBtn = document.getElementById('saveCourseBtn');
+
+ if (!btn || !modalEl) return;
+
+ const modal = new bootstrap.Modal(modalEl);
+
+ btn.addEventListener('click', async () => {
+ // Load classes
+ try {
+ const res = await fetch(`${this.apiBase}/classes`);
+ const result = await res.json();
+ if (result.success) {
+ const select = document.getElementById('classSelect');
+ select.innerHTML = '
请选择班级... ' +
+ result.data.classes.map(c => `
${c.class_name} `).join('');
+ }
+ } catch (e) {
+ console.error('Load classes failed', e);
+ }
+ modal.show();
+ });
+
+ saveBtn.addEventListener('click', async () => {
+ const form = document.getElementById('addCourseForm');
+ const formData = new FormData(form);
+ const data = Object.fromEntries(formData.entries());
+
+ try {
+ const res = await fetch(`${this.apiBase}/courses`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ });
+ const result = await res.json();
- if (nameEl) nameEl.textContent = user.name;
- if (teacherNameEl) teacherNameEl.textContent = user.name;
+ if (result.success) {
+ alert('课程创建成功');
+ modal.hide();
+ this.loadCourses();
+ form.reset();
+ } else {
+ alert(result.message || '创建失败');
+ }
+ } catch (e) {
+ console.error('Create course failed', e);
+ alert('系统错误');
}
});
-});
\ No newline at end of file
+ }
+
+ initEditCourse() {
+ const modalEl = document.getElementById('editCourseModal');
+ if (!modalEl) return;
+
+ const modal = new bootstrap.Modal(modalEl);
+ const updateBtn = document.getElementById('updateCourseBtn');
+
+ // Use event delegation for dynamically created edit buttons
+ document.addEventListener('click', async (e) => {
+ const btn = e.target.closest('.btn-edit-course');
+ if (!btn) return;
+
+ const courseId = btn.dataset.id;
+ if (!this.courses) return;
+ const courseData = this.courses.find(c => c.id == courseId);
+
+ if (!courseData) return;
+
+ // Fill form
+ document.getElementById('editCourseId').value = courseData.id;
+ document.getElementById('editCourseName').value = courseData.course_name;
+ document.getElementById('editCourseCode').value = courseData.course_code;
+ document.getElementById('editCourseCredit').value = courseData.credit;
+ document.getElementById('editSemester').value = courseData.semester;
+
+ // Load classes and select current one
+ try {
+ const res = await fetch(`${this.apiBase}/classes`);
+ const result = await res.json();
+ if (result.success) {
+ const select = document.getElementById('editClassSelect');
+ select.innerHTML = '
请选择班级... ' +
+ result.data.classes.map(c => `
${c.class_name} `).join('');
+ }
+ } catch (e) {
+ console.error('Load classes failed', e);
+ }
+
+ modal.show();
+ });
+
+ updateBtn.addEventListener('click', async () => {
+ const form = document.getElementById('editCourseForm');
+ const formData = new FormData(form);
+ const data = Object.fromEntries(formData.entries());
+ const courseId = data.id;
+
+ try {
+ const res = await fetch(`${this.apiBase}/courses/${courseId}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ });
+ const result = await res.json();
+
+ if (result.success) {
+ alert('课程更新成功');
+ modal.hide();
+ this.loadCourses();
+ } else {
+ alert(result.message || '更新失败');
+ }
+ } catch (e) {
+ console.error('Update course failed', e);
+ alert('系统错误');
+ }
+ });
+ }
+
+ // ================= Grade Entry =================
+ async initGradeEntry() {
+ const courseSelect = document.getElementById('courseSelect');
+
+ // Load Courses for Select
+ try {
+ const response = await fetch(`${this.apiBase}/courses`);
+ const result = await response.json();
+ if (result.success) {
+ courseSelect.innerHTML = '
请选择课程... ' +
+ result.data.courses.map(c => `
${c.course_name} (${c.class_name || '未指定班级'}) `).join('');
+
+ // Check URL params
+ const params = new URLSearchParams(window.location.search);
+ const courseId = params.get('courseId');
+ if (courseId) {
+ courseSelect.value = courseId;
+ this.loadStudentsForGradeEntry(courseId);
+ }
+ }
+ } catch (e) { console.error(e); }
+
+ courseSelect.addEventListener('change', (e) => {
+ if (e.target.value) {
+ this.loadStudentsForGradeEntry(e.target.value);
+ } else {
+ document.getElementById('studentTableBody').innerHTML = '
请选择课程以加载学生列表 ';
+ }
+ });
+ }
+
+ async loadStudentsForGradeEntry(courseId) {
+ const tbody = document.getElementById('studentTableBody');
+ tbody.innerHTML = '
加载中...
';
+
+ // Since we don't have a direct "get students by course" API that returns grades yet,
+ // we might need to rely on what we have.
+ // Actually, we need to fetch students enrolled in the class of this course.
+ // AND fetch their existing grades for this course.
+ // For simplicity, let's assume we have an endpoint or we modify `getGradeStatistics` or similar?
+ // No, we should probably add `GET /api/teacher/course/:id/students`?
+ // Or just use `GET /api/teacher/grades?courseId=X`.
+
+ // Let's assume `GET /api/teacher/grades?courseId=X` returns the list of students with their grades (or null if no grade).
+ // I need to implement this backend logic if it's missing.
+ // For now, I'll simulate or try to use what I have.
+ // I'll add `getCourseGrades` to TeacherController.
+
+ // Wait, I can't modify backend endlessly.
+ // Let's check `TeacherController.js` again.
+ // It has `addScore`.
+ // It does NOT have `getCourseGrades`.
+ // I need to add it to support this view.
+
+ try {
+ // Placeholder: I will add this endpoint in next step.
+ const res = await fetch(`${this.apiBase}/grades?courseId=${courseId}`);
+ const result = await res.json();
+
+ if (result.success) {
+ this.renderGradeEntryTable(result.data.grades);
+ } else {
+ tbody.innerHTML = `
${result.message} `;
+ }
+ } catch (e) {
+ tbody.innerHTML = `
加载失败: ${e.message} `;
+ }
+ }
+
+ renderGradeEntryTable(grades) {
+ const tbody = document.getElementById('studentTableBody');
+ if (!grades || grades.length === 0) {
+ tbody.innerHTML = '
该课程暂无学生 ';
+ return;
+ }
+
+ tbody.innerHTML = grades.map(g => `
+
+ ${g.student_id}
+ ${g.student_name}
+
+
+
+ ${g.total_score || '-'}
+
+
+ 保存
+
+
+
+ `).join('');
+
+ // Bind save events
+ document.querySelectorAll('.save-grade-btn').forEach(btn => {
+ btn.addEventListener('click', (e) => this.saveGrade(e.target.closest('tr'), btn.dataset.studentId));
+ });
+ }
+
+ async saveGrade(row, studentId) {
+ const inputs = row.querySelectorAll('input');
+ const usual = inputs[0].value;
+ const midterm = inputs[1].value;
+ const final = inputs[2].value;
+ const courseId = document.getElementById('courseSelect').value;
+
+ // Simple calculation for total score preview (Backend should do the real one)
+ const total = (usual * 0.3 + midterm * 0.3 + final * 0.4).toFixed(1); // Example weights
+
+ try {
+ const res = await fetch(`${this.apiBase}/grades`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ studentId,
+ courseId,
+ usual_score: usual,
+ midterm_score: midterm,
+ final_score: final,
+ score: total // Passing total score as 'score' for compatibility with existing API
+ })
+ });
+
+ const result = await res.json();
+ if (result.success) {
+ // Update total cell
+ row.cells[5].textContent = total;
+ // Show success feedback
+ const btn = row.querySelector('.save-grade-btn');
+ const originalText = btn.innerHTML;
+ btn.innerHTML = '
已保存';
+ btn.classList.remove('btn-primary');
+ btn.classList.add('btn-success');
+ setTimeout(() => {
+ btn.innerHTML = originalText;
+ btn.classList.add('btn-primary');
+ btn.classList.remove('btn-success');
+ }, 2000);
+ } else {
+ alert(result.message || '保存失败');
+ }
+ } catch (e) {
+ console.error(e);
+ alert('保存失败');
+ }
+ }
+
+ // ================= Grade Management =================
+ async initGradeManagement() {
+ // Similar logic to Grade Entry but Read-Only or Filter focused
+ // Fetch courses for filter
+ try {
+ const response = await fetch(`${this.apiBase}/courses`);
+ const result = await response.json();
+ if (result.success) {
+ const select = document.getElementById('courseSelectFilter');
+ select.innerHTML = '
全部课程 ' +
+ result.data.courses.map(c => `
${c.course_name} `).join('');
+ }
+ } catch (e) { console.error(e); }
+
+ document.getElementById('searchBtn').addEventListener('click', () => this.searchGrades());
+
+ // Check URL params
+ const params = new URLSearchParams(window.location.search);
+ if (params.get('courseId')) {
+ document.getElementById('courseSelectFilter').value = params.get('courseId');
+ this.searchGrades();
+ }
+ }
+
+ async searchGrades() {
+ const courseId = document.getElementById('courseSelectFilter').value;
+ const studentName = document.getElementById('studentNameFilter').value;
+ const tbody = document.getElementById('gradeTableBody');
+
+ tbody.innerHTML = '
';
+
+ try {
+ let url = `${this.apiBase}/grades?`;
+ if (courseId) url += `courseId=${courseId}&`;
+ if (studentName) url += `studentName=${studentName}`;
+
+ const res = await fetch(url);
+ const result = await res.json();
+
+ if (result.success && result.data.grades) {
+ if (result.data.grades.length === 0) {
+ tbody.innerHTML = '
未找到相关成绩记录 ';
+ return;
+ }
+
+ tbody.innerHTML = result.data.grades.map(g => `
+
+ ${g.course_name || '-'}
+ ${g.student_id || '-'}
+ ${g.student_name || '-'}
+ ${g.total_score || '-'}
+ ${g.grade_point || '-'}
+ ${g.grade_level || '-'}
+
+
+ 修改
+
+
+
+ `).join('');
+ }
+ } catch (e) {
+ tbody.innerHTML = `
查询失败 `;
+ }
+ }
+
+ getBadgeColor(level) {
+ if (!level) return 'secondary';
+ if (level.startsWith('A')) return 'success';
+ if (level.startsWith('B')) return 'info';
+ if (level.startsWith('C')) return 'warning';
+ if (level.startsWith('F')) return 'danger';
+ return 'primary';
+ }
+
+ // ================= Profile =================
+ initProfilePage() {
+ const saveProfileBtn = document.getElementById('saveProfileBtn');
+ if (saveProfileBtn) {
+ saveProfileBtn.addEventListener('click', async () => {
+ const name = document.getElementById('inputName').value;
+ const className = document.getElementById('inputClass').value;
+ try {
+ const res = await fetch('/api/auth/update-profile', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name, class: className })
+ });
+ const result = await res.json();
+ if (result.success) {
+ alert('资料更新成功');
+ // 更新侧边栏和顶栏
+ const nameEls = document.querySelectorAll('#teacherName, #userName, #profileName');
+ nameEls.forEach(el => el.textContent = name);
+ } else {
+ alert(result.message || '更新失败');
+ }
+ } catch (e) {
+ console.error('Update profile failed', e);
+ alert('系统错误');
+ }
+ });
+ }
+
+ const passwordForm = document.getElementById('passwordForm');
+ if (passwordForm) {
+ passwordForm.addEventListener('submit', async (e) => {
+ e.preventDefault();
+
+ const oldPassword = document.getElementById('oldPassword').value;
+ const newPassword = document.getElementById('newPassword').value;
+ const confirmPassword = document.getElementById('confirmPassword').value;
+ const errorEl = document.getElementById('passwordError');
+
+ // Hide previous error
+ errorEl.style.display = 'none';
+
+ // Basic validation
+ if (newPassword !== confirmPassword) {
+ errorEl.textContent = '两次输入的新密码不一致';
+ errorEl.style.display = 'block';
+ return;
+ }
+
+ if (newPassword.length < 6) {
+ errorEl.textContent = '新密码长度至少为 6 位';
+ errorEl.style.display = 'block';
+ return;
+ }
+
+ try {
+ const res = await fetch('/api/auth/update-password', {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ oldPassword, newPassword })
+ });
+
+ const result = await res.json();
+ if (result.success) {
+ alert('密码修改成功,请重新登录');
+ // Logout and redirect
+ await fetch('/api/auth/logout', { method: 'POST' });
+ window.location.href = '/login';
+ } else {
+ errorEl.textContent = result.message || '修改失败';
+ errorEl.style.display = 'block';
+ }
+ } catch (err) {
+ console.error(err);
+ errorEl.textContent = '服务器错误,请稍后再试';
+ errorEl.style.display = 'block';
+ }
+ });
+ }
+ }
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ window.teacherManager = new TeacherManager();
+});
diff --git a/frontend/views/admin/dashboard.html b/frontend/views/admin/dashboard.html
index 4b1604e..7199f2b 100644
--- a/frontend/views/admin/dashboard.html
+++ b/frontend/views/admin/dashboard.html
@@ -3,340 +3,334 @@
-
学生成绩管理系统 - 管理员仪表板
-
-
-
-
-
-
-
-
-
- XX学校成绩管理系统
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
1,248
-
总用户数
-
- 12 本周新增
-
-
-
-
-
-
-
-
-
-
3,567
-
学生总数
-
- 45 本周新增
-
-
-
-
-
-
-
-
-
-
-
-
89
-
课程总数
-
- 3 本周新增
-
-
-
-
-
-
-
-
-
-
-
-
用户管理
-
- 管理所有用户账户,包括添加、编辑、删除用户,设置用户角色和权限?
-
进入管理
-
-
-
-
-
-
-
学生管理
-
- 管理学生信息,包括学籍管理、班级分配、信息维护和批量导入导出?
-
进入管理
-
-
-
-
-
-
-
教师管理
-
- 管理教师信息,包括教师分配、课程安排、权限设置和绩效考核?
-
进入管理
-
-
-
-
-
-
-
成绩统计
-
- 查看全校成绩统计,生成分析报告,支持图表展示和数据导出?
-
查看统计
-
-
-
-
-
-
-
- 系统状?
-
-
-
-
-
数据库服?/div>
-
运行正常 | 响应时间: 12ms
-
-
-
-
-
-
Web服务?/div>
-
运行正常 | 在线用户: 156
-
-
-
-
-
-
文件存储
-
使用? 65% | 剩余: 35GB
-
-
-
-
-
-
备份服务
-
上次备份: 2天前 | 建议立即备份
-
-
-
-
-
-
-
-
-
- 最近活?
-
-
-
-
-
-
-
新增学生用户
-
10分钟?| 操作? 管理?/div>
-
-
-
-
-
-
-
-
修改教师信息
-
1小时?| 操作? 管理?/div>
-
-
-
-
-
-
-
-
生成成绩统计报告
-
3小时?| 操作? 系统
-
-
-
-
-
-
-
-
导出用户数据
-
5小时?| 操作? 管理?/div>
-
-
-
-
-
-
-
-
系统设置更新
-
1天前 | 操作? 管理?/div>
-
-
-
-
-
-
-
-
+ .sidebar-header {
+ padding: 2rem 1.5rem;
+ text-align: center;
+ border-bottom: 1px solid rgba(255,255,255,0.1);
+ }
+
+ .sidebar-brand {
+ font-size: 1.25rem;
+ font-weight: 700;
+ color: white;
+ text-decoration: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ }
+
+ .user-profile {
+ padding: 2rem 1rem;
+ text-align: center;
+ }
+
+ .user-avatar {
+ width: 80px;
+ height: 80px;
+ background: rgba(255,255,255,0.2);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto 1rem;
+ font-size: 2rem;
+ }
+
+ .user-info h6 { margin-bottom: 0.25rem; font-weight: 600; }
+ .user-info p { font-size: 0.85rem; opacity: 0.8; margin-bottom: 0; }
+
+ .nav-menu { padding: 1rem 0; }
+ .nav-item { padding: 0.25rem 1rem; }
+
+ .nav-link {
+ color: rgba(255,255,255,0.8);
+ padding: 0.8rem 1.25rem;
+ border-radius: 0.5rem;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ transition: all 0.2s;
+ text-decoration: none;
+ }
+
+ .nav-link:hover, .nav-link.active {
+ color: white;
+ background: rgba(255,255,255,0.15);
+ }
+
+ .nav-link i { width: 20px; text-align: center; }
+
+ /* 主内容区 */
+ .main-content {
+ margin-left: var(--sidebar-width);
+ padding: 2rem;
+ min-height: 100vh;
+ }
+
+ .top-navbar {
+ background: white;
+ padding: 1rem 2rem;
+ border-radius: 1rem;
+ box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.1);
+ margin-bottom: 2rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .page-heading h4 { margin-bottom: 0; font-weight: 700; color: #333; }
+
+ .stat-card {
+ transition: transform 0.2s;
+ }
+ .stat-card:hover {
+ transform: translateY(-5px);
+ }
+
+ @media (max-width: 992px) {
+ .sidebar { left: -var(--sidebar-width); }
+ .main-content { margin-left: 0; }
+ .sidebar.active { left: 0; }
+ }
+
+
+
+
+
+
+
+
+
+
+
+