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 += `
  • ${i}
  • `; + } + + 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 += `
  • ${i}
  • `; + } + + 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 += `
  • ${i}
  • `; + } + + 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 || '-'} + +
    +
    +
    +
    + ${s.pass_rate}% +
    + + + `).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 => ``).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 => ``).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 => ``).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 => ``).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 @@ - 学生成绩管理系统 - 管理员仪表板 - - - - - - -
    - - -
    - - - - -
    -
    -
    -

    管理员仪表板

    - -
    -
    -
    - - -
    -
    -
    - -
    -
    -
    1,248
    -
    总用户数
    -
    - 12 本周新增 -
    -
    -
    - -
    -
    - -
    -
    -
    3,567
    -
    学生总数
    -
    - 45 本周新增 -
    -
    -
    - -
    -
    - -
    -
    -
    128
    -
    教师总数
    -
    - 无变?
    -
    -
    - -
    -
    - -
    -
    -
    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; } + } + + + + + + + +
    + +
    +
    +

    管理员仪表板

    +
    +
    +
    +
    +
    加载中...
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + +
    +
    +
    总用户数
    +

    ...

    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    学生总数
    +

    ...

    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    教师总数
    +

    ...

    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    课程总数
    +

    ...

    +
    +
    +
    +
    + + +
    快捷管理
    + +
    + + + + diff --git a/frontend/views/admin/data_export.html b/frontend/views/admin/data_export.html new file mode 100644 index 0000000..f83f998 --- /dev/null +++ b/frontend/views/admin/data_export.html @@ -0,0 +1,266 @@ + + + + + + 数据导出 - 管理员端 + + + + + + + + + + + + + +
    + +
    +
    +

    数据导出

    +
    +
    +
    +
    +
    加载中...
    +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    学生信息导出
    +

    导出所有学生的基本信息、班级、专业等数据。

    + +
    +
    + +
    +
    +
    + +
    +
    教师信息导出
    +

    导出所有教师的基本信息、职称、部门等数据。

    + +
    +
    + +
    +
    +
    + +
    +
    成绩数据导出
    +

    导出所有课程的成绩记录,包括平时、期末、总评。

    + +
    +
    +
    +
    + + + + + + diff --git a/frontend/views/admin/grade_statistics.html b/frontend/views/admin/grade_statistics.html new file mode 100644 index 0000000..3c50d5b --- /dev/null +++ b/frontend/views/admin/grade_statistics.html @@ -0,0 +1,279 @@ + + + + + + 成绩统计 - 管理员端 + + + + + + + + + + + + + +
    + +
    +
    +

    成绩统计

    +
    +
    +
    +
    +
    加载中...
    +
    +
    +
    + + +
    +
    +
    +
    +
    各课程平均分对比
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    全校及格率
    +
    +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    课程成绩详情
    +
    +
    +
    + + + + + + + + + + + + + + + + +
    课程代码课程名称任课教师选课人数平均分最高分最低分及格率
    加载中...
    +
    +
    +
    +
    + + + + + + + diff --git a/frontend/views/admin/operation_logs.html b/frontend/views/admin/operation_logs.html new file mode 100644 index 0000000..752f13b --- /dev/null +++ b/frontend/views/admin/operation_logs.html @@ -0,0 +1,245 @@ + + + + + + 操作日志 - 管理员端 + + + + + + + + + + + + + +
    + +
    +
    +

    操作日志

    +
    +
    +
    +
    +
    加载中...
    +
    +
    +
    + +
    +
    +
    + + + + + + + + + + + + + +
    操作时间操作人操作类型详情IP地址
    暂无日志数据
    +
    +
    +
    +
    + + + + + + diff --git a/frontend/views/admin/student_management.html b/frontend/views/admin/student_management.html index 55f95c1..220f571 100644 --- a/frontend/views/admin/student_management.html +++ b/frontend/views/admin/student_management.html @@ -3,439 +3,327 @@ - 学生管理 - XX学校成绩管理系统 - + 学生管理 - 管理员端 + + + + + + - - - -
    -
    -
    - - - - -
    -
    -
    - - - - - - -
    -
    - - + +
    +
    +
    +
    + +
    + +
    - -
    - - - -
    -
    - - + +
    +
    + + +
    +
    - - +
    + - + - - - - - + + + + - - - - - + +
    学号学号 姓名性别 班级联系电话邮箱入学时间操作专业年级联系方式操作
    - -

    正在加载学生数据...

    -
    加载中...
    - - - + +
    +
    + + +
    +
    - -
    -
    -

    © 2023 XX学校成绩管理系统. 版权所?

    -

    技术支? 计算机科学与技术学?/p> -

    -
    - - + + + diff --git a/frontend/views/admin/system_settings.html b/frontend/views/admin/system_settings.html new file mode 100644 index 0000000..49c01b4 --- /dev/null +++ b/frontend/views/admin/system_settings.html @@ -0,0 +1,290 @@ + + + + + + 系统设置 - 管理员端 + + + + + + + + + + + + + +
    + +
    +
    +

    系统设置

    +
    +
    +
    +
    +
    加载中...
    +
    +
    +
    + +
    + +
    +
    +
    +
    基础设置
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    数据维护
    +
    +
    +
    + 定期备份数据可以防止意外丢失。 +
    +
    + + + +
    +
    +
    +
    +
    +
    + + + + + + diff --git a/frontend/views/admin/teacher_management.html b/frontend/views/admin/teacher_management.html new file mode 100644 index 0000000..58d9d8a --- /dev/null +++ b/frontend/views/admin/teacher_management.html @@ -0,0 +1,316 @@ + + + + + + 教师管理 - 管理员端 + + + + + + + + + + + + + +
    + +
    +
    +

    教师管理

    +
    +
    +
    +
    +
    加载中...
    +
    +
    +
    + + +
    +
    +
    +
    + +
    + + +
    +
    +
    + +
    +
    +
    +
    + + +
    +
    +
    + + + + + + + + + + + + + + +
    工号姓名职称学院/部门联系方式操作
    加载中...
    +
    +
    + +
    +
    + + + + + + + + + diff --git a/frontend/views/admin/user_management.html b/frontend/views/admin/user_management.html index e95daea..4ecfb3d 100644 --- a/frontend/views/admin/user_management.html +++ b/frontend/views/admin/user_management.html @@ -3,492 +3,327 @@ - 用户管理 - XX学校成绩管理系统 - + 用户管理 - 管理员端 + + + + + + - - - -
    -
    -
    - - +
    + + +
    +
    - -
    -
    -

    © 2023 XX学校成绩管理系统. 版权所?

    -

    技术支? 信息技术中?| 联系电话: 010-12345678

    -
    -
    - - - + + + diff --git a/frontend/views/student/profile.html b/frontend/views/student/profile.html index 92351ed..1fad180 100644 --- a/frontend/views/student/profile.html +++ b/frontend/views/student/profile.html @@ -253,54 +253,65 @@
    -
    -
    - 基本信息 +
    +
    +
    基本信息
    -
    +
    - +
    - +
    - +
    - +
    - -
    -
    安全设置
    - -
    - - -
    -
    -
    - - -
    -
    - - -
    -
    -
    - +
    +
    +
    +
    + +
    +
    +
    修改密码
    +
    +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + +
    +
    @@ -342,26 +353,58 @@ updateTime(); setInterval(updateTime, 1000); + // 保存基本信息逻辑 + const saveProfileBtn = document.getElementById('saveProfileBtn'); + if (saveProfileBtn) { + saveProfileBtn.addEventListener('click', async () => { + const name = document.getElementById('profileNameInput').value; + const className = document.getElementById('profileClass').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('资料更新成功'); + // 更新侧边栏和顶栏 + document.getElementById('userName').textContent = name; + document.getElementById('studentName').textContent = name; + document.getElementById('profileName').textContent = name; + } else { + alert(result.message || '更新失败'); + } + } catch (e) { + console.error('Update profile failed', e); + alert('系统错误'); + } + }); + } + // 修改密码逻辑 - const savePasswordBtn = document.getElementById('savePasswordBtn'); - if (savePasswordBtn) { - savePasswordBtn.addEventListener('click', async () => { + 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; - - if (!oldPassword || !newPassword || !confirmPassword) { - alert('请填写所有密码字段'); - return; - } + const errorEl = document.getElementById('passwordError'); + + errorEl.style.display = 'none'; if (newPassword !== confirmPassword) { - alert('两次输入的新密码不一致'); + errorEl.textContent = '两次输入的新密码不一致'; + errorEl.style.display = 'block'; return; } if (newPassword.length < 6) { - alert('新密码长度至少为 6 位'); + errorEl.textContent = '新密码长度至少为 6 位'; + errorEl.style.display = 'block'; return; } @@ -377,17 +420,16 @@ const result = await response.json(); if (result.success) { alert('密码修改成功,请重新登录'); - // 登出并跳转到登录页 - fetch('/api/auth/logout', { method: 'POST' }) - .then(() => { - window.location.href = '/login'; - }); + await fetch('/api/auth/logout', { method: 'POST' }); + window.location.href = '/login'; } else { - alert(result.message || '修改失败'); + errorEl.textContent = result.message || '修改失败'; + errorEl.style.display = 'block'; } } catch (error) { console.error('Update password error:', error); - alert('网络错误,请稍后再试'); + errorEl.textContent = '网络错误,请稍后再试'; + errorEl.style.display = 'block'; } }); } diff --git a/frontend/views/teacher/dashboard.html b/frontend/views/teacher/dashboard.html index b8f4c79..749e657 100644 --- a/frontend/views/teacher/dashboard.html +++ b/frontend/views/teacher/dashboard.html @@ -201,19 +201,25 @@