From e5a2a9d0423aa7d6ac20db87d2214b1586c82fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=80=E6=A2=A6?= <3501646051@qq.com> Date: Sun, 21 Dec 2025 22:34:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AD=A6=E7=94=9F?= =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E4=B8=AD=E5=BF=83=E9=A1=B5=E9=9D=A2=E5=92=8C?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E5=A4=87=E4=BB=BD=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(auth): 重构认证模块适配Bootstrap 5样式 feat(controller): 在登录响应中返回用户对象 feat(server): 添加学生个人中心路由 refactor(models): 重构学生和成绩模型结构 style: 更新登录和注册页面UI设计 chore: 添加数据库备份脚本和空备份文件 --- backend/controllers/authController.js | 2 +- backend/models/Score.js | 26 +- backend/models/Student.js | 15 +- backend/scripts/backup.js | 62 ++ backend/server.js | 3 + database/backup_2025-12-21T14-30-15-873Z.sql | 0 database/backup_2025-12-21T14-30-37-825Z.sql | 177 +++++ frontend/public/js/auth.js | 149 ++--- frontend/public/js/student.js | 523 +++------------ frontend/public/js/teacher.js | 488 +++----------- frontend/views/auth/login.html | 215 ++++-- frontend/views/auth/register.html | 247 +++++-- frontend/views/student/dashboard.html | 543 ++++++++++----- frontend/views/student/profile.html | 347 ++++++++++ frontend/views/teacher/dashboard.html | 663 +++++++------------ scripts/start.bat | 25 +- 16 files changed, 1834 insertions(+), 1651 deletions(-) create mode 100644 backend/scripts/backup.js create mode 100644 database/backup_2025-12-21T14-30-15-873Z.sql create mode 100644 database/backup_2025-12-21T14-30-37-825Z.sql create mode 100644 frontend/views/student/profile.html diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index b811c2f..5aa9d07 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -14,7 +14,7 @@ class AuthController { // 设置 Session req.session.user = user; - success(res, user, '登录成功'); + success(res, { user }, '登录成功'); } catch (err) { if (err.message === '用户名或密码错误') { return error(res, err.message, 401); diff --git a/backend/models/Score.js b/backend/models/Score.js index 378eeb5..3f4fbd3 100644 --- a/backend/models/Score.js +++ b/backend/models/Score.js @@ -3,9 +3,11 @@ const db = require('../config/database'); class Score { static async findByStudentId(studentId) { const sql = ` - SELECT s.*, c.course_code, c.course_name, c.credit, + SELECT s.id, s.student_id, s.course_id, s.teacher_id, + s.total_score as score, s.grade_point, s.grade_level, s.created_at, + c.course_code, c.course_name, c.credit, u.name as teacher_name - FROM scores s + FROM grades s JOIN courses c ON s.course_id = c.id JOIN users u ON s.teacher_id = u.id WHERE s.student_id = ? @@ -16,10 +18,13 @@ class Score { static async findDetailsById(scoreId, studentId) { const sql = ` - SELECT s.*, c.course_code, c.course_name, c.credit, c.semester, + SELECT s.id, s.student_id, s.course_id, s.teacher_id, + s.usual_score, s.midterm_score, s.final_score, s.total_score as score, + s.grade_point, s.grade_level, s.created_at, s.remark, + c.course_code, c.course_name, c.credit, c.semester, u.name as teacher_name, u.email as teacher_email, st.id as student_number, st.class as class_name - FROM scores s + FROM grades s JOIN courses c ON s.course_id = c.id JOIN users u ON s.teacher_id = u.id JOIN students st ON s.student_id = st.id @@ -30,22 +35,23 @@ class Score { } static async create(scoreData) { - const { studentId, courseId, teacherId, score, gradePoint, gradeLevel, examDate, remark } = scoreData; + const { studentId, courseId, teacherId, score, gradePoint, gradeLevel, remark } = scoreData; + // 既然TeacherService只传了score,我们默认把它作为 final_score 和 total_score + // 如果需要支持平时分/期中分,需要修改前端和Controller const sql = ` - INSERT INTO scores (student_id, course_id, teacher_id, score, + INSERT INTO grades (student_id, course_id, teacher_id, final_score, total_score, grade_point, grade_level, created_at, remark) - VALUES (?, ?, ?, ?, ?, ?, NOW(), ?) + VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), ?) `; - // 注意:这里用 created_at 替代了 examDate,如果数据库有 examDate 列可以改回去 const result = await db.pool.execute(sql, [ - studentId, courseId, teacherId, score, gradePoint, gradeLevel, remark + studentId, courseId, teacherId, score, score, gradePoint, gradeLevel, remark ]); return result[0].insertId; } static async findByStudentAndCourse(studentId, courseId) { const rows = await db.query( - 'SELECT * FROM scores WHERE student_id = ? AND course_id = ?', + 'SELECT * FROM grades WHERE student_id = ? AND course_id = ?', [studentId, courseId] ); return rows[0]; diff --git a/backend/models/Student.js b/backend/models/Student.js index 3322357..2a48811 100644 --- a/backend/models/Student.js +++ b/backend/models/Student.js @@ -1,21 +1,16 @@ const db = require('../config/database'); class Student { - static async findByUserId(userId) { - const students = await db.query('SELECT * FROM students WHERE user_id = ?', [userId]); - return students[0]; - } - - static async findById(studentId) { - const students = await db.query('SELECT * FROM students WHERE student_id = ?', [studentId]); + static async findById(id) { + const students = await db.query('SELECT * FROM students WHERE id = ?', [id]); return students[0]; } static async create(studentData) { - const { id, name, className, userId } = studentData; + const { id, name, className } = studentData; await db.query( - 'INSERT INTO students (id, name, class, user_id) VALUES (?, ?, ?, ?)', - [id, name, className, userId] + 'INSERT INTO students (id, name, class) VALUES (?, ?, ?)', + [id, name, className] ); } } diff --git a/backend/scripts/backup.js b/backend/scripts/backup.js new file mode 100644 index 0000000..4a700c8 --- /dev/null +++ b/backend/scripts/backup.js @@ -0,0 +1,62 @@ +const fs = require('fs'); +const path = require('path'); +const db = require('../config/database'); + +async function backup() { + console.log('开始备份数据库...'); + const backupDir = path.join(__dirname, '../../database'); + const filename = `backup_${new Date().toISOString().replace(/[:.]/g, '-')}.sql`; + const filepath = path.join(backupDir, filename); + + if (!fs.existsSync(backupDir)) { + fs.mkdirSync(backupDir, { recursive: true }); + } + + try { + const fileStream = fs.createWriteStream(filepath, { flags: 'a' }); + + // 获取所有表 + const tables = await db.query('SHOW TABLES'); + const tableNames = tables.map(t => Object.values(t)[0]); + + for (const tableName of tableNames) { + console.log(`正在备份表: ${tableName}`); + + // 1. 获取 CREATE TABLE 语句 + const createRows = await db.query(`SHOW CREATE TABLE ${tableName}`); + const createTableSql = createRows[0]['Create Table']; + + fileStream.write(`\n\n-- Table structure for table \`${tableName}\`\n`); + fileStream.write(`DROP TABLE IF EXISTS \`${tableName}\`;\n`); + fileStream.write(`${createTableSql};\n`); + + // 2. 获取数据 + const rows = await db.query(`SELECT * FROM ${tableName}`); + if (rows.length > 0) { + fileStream.write(`\n-- Dumping data for table \`${tableName}\`\n`); + fileStream.write(`INSERT INTO \`${tableName}\` VALUES\n`); + + const values = rows.map(row => { + const rowValues = Object.values(row).map(val => { + if (val === null) return 'NULL'; + if (typeof val === 'number') return val; + // 转义单引号和反斜杠 + return `'${String(val).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`; + }); + return `(${rowValues.join(', ')})`; + }); + + fileStream.write(values.join(',\n') + ';\n'); + } + } + + fileStream.end(); + console.log(`备份完成! 文件保存在: ${filepath}`); + process.exit(0); + } catch (error) { + console.error('备份失败:', error); + process.exit(1); + } +} + +backup(); \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 804f690..a8b9c89 100644 --- a/backend/server.js +++ b/backend/server.js @@ -96,6 +96,9 @@ app.get('/dashboard', requirePageAuth, (req, res) => { app.get('/student/dashboard', requirePageAuth, requirePageRole(['student']), (req, res) => { res.sendFile(path.join(__dirname, '../frontend/views/student/dashboard.html')); }); +app.get('/student/profile', requirePageAuth, requirePageRole(['student']), (req, res) => { + res.sendFile(path.join(__dirname, '../frontend/views/student/profile.html')); +}); // Teacher Pages const teacherPageRouter = express.Router(); diff --git a/database/backup_2025-12-21T14-30-15-873Z.sql b/database/backup_2025-12-21T14-30-15-873Z.sql new file mode 100644 index 0000000..e69de29 diff --git a/database/backup_2025-12-21T14-30-37-825Z.sql b/database/backup_2025-12-21T14-30-37-825Z.sql new file mode 100644 index 0000000..4dd7142 --- /dev/null +++ b/database/backup_2025-12-21T14-30-37-825Z.sql @@ -0,0 +1,177 @@ + + +-- Table structure for table `classes` +DROP TABLE IF EXISTS `classes`; +CREATE TABLE `classes` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '班级ID', + `class_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '班级名称', + `grade` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '年级', + `major` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '专业', + `teacher_id` int DEFAULT NULL COMMENT '班主任ID', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_class_name` (`class_name`) USING BTREE, + KEY `idx_teacher_id` (`teacher_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='班级表'; + +-- Dumping data for table `classes` +INSERT INTO `classes` VALUES +(1, '计算机2301', '2023', '计算机科学与技术', 2001, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'), +(2, '软件工程2302', '2023', '软件工程', 2002, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'); + + +-- Table structure for table `courses` +DROP TABLE IF EXISTS `courses`; +CREATE TABLE `courses` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '课程ID', + `course_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '课程代码', + `course_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '课程名称', + `credit` decimal(3,1) NOT NULL DEFAULT '2.0' COMMENT '学分', + `teacher_id` int NOT NULL COMMENT '授课教师ID', + `class_id` int NOT NULL COMMENT '授课班级ID', + `semester` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '学期', + `academic_year` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '学年', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `course_code` (`course_code`) USING BTREE, + KEY `idx_course_code` (`course_code`) USING BTREE, + KEY `idx_teacher_id` (`teacher_id`) USING BTREE, + KEY `idx_class_id` (`class_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='课程表'; + +-- Dumping data for table `courses` +INSERT INTO `courses` VALUES +(1, 'CS101', '高级程序设计', '4.0', 2001, 1, NULL, NULL, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'), +(2, 'SE201', '软件工程导论', '3.0', 2002, 2, NULL, NULL, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'), +(3, 'MAT101', '高等数学', '5.0', 2003, 1, NULL, NULL, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'); + + +-- Table structure for table `grades` +DROP TABLE IF EXISTS `grades`; +CREATE TABLE `grades` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '成绩ID', + `student_id` int NOT NULL COMMENT '学生ID', + `course_id` int NOT NULL COMMENT '课程ID', + `usual_score` decimal(5,2) DEFAULT NULL COMMENT '平时成绩', + `midterm_score` decimal(5,2) DEFAULT NULL COMMENT '期中成绩', + `final_score` decimal(5,2) DEFAULT NULL COMMENT '期末成绩', + `total_score` decimal(5,2) DEFAULT NULL COMMENT '总评成绩', + `grade_point` decimal(3,2) DEFAULT NULL COMMENT '绩点', + `grade_level` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '成绩等级(A/B/C/D/F)', + `teacher_id` int NOT NULL COMMENT '录入教师ID', + `remark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '备注', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_student_course` (`student_id`,`course_id`) USING BTREE, + KEY `idx_student_id` (`student_id`) USING BTREE, + KEY `idx_course_id` (`course_id`) USING BTREE, + KEY `idx_teacher_id` (`teacher_id`) USING BTREE, + KEY `idx_total_score` (`total_score`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='成绩表'; + +-- Dumping data for table `grades` +INSERT INTO `grades` VALUES +(1, 3001, 1, '90.00', '85.00', '88.00', '87.70', '3.00', NULL, 2001, NULL, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'), +(2, 3001, 3, '80.00', '75.00', '82.00', '79.30', '2.00', NULL, 2003, NULL, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'), +(3, 3002, 1, '95.00', '92.00', '94.00', '93.70', '4.00', NULL, 2001, NULL, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'), +(4, 3003, 2, '88.00', '80.00', '85.00', '84.40', '3.00', NULL, 2002, NULL, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'); + + +-- Table structure for table `operation_logs` +DROP TABLE IF EXISTS `operation_logs`; +CREATE TABLE `operation_logs` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '日志ID', + `user_id` int NOT NULL COMMENT '操作用户ID', + `operation_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作类型', + `operation_target` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作目标', + `operation_details` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '操作详情', + `ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'IP地址', + `user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '用户代理', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_user_id` (`user_id`) USING BTREE, + KEY `idx_operation_type` (`operation_type`) USING BTREE, + KEY `idx_created_at` (`created_at`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='操作日志表'; + + +-- Table structure for table `scores` +DROP TABLE IF EXISTS `scores`; +CREATE TABLE `scores` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '成绩记录ID', + `student_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '学生ID', + `course` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '课程名称', + `score` decimal(5,2) NOT NULL COMMENT '成绩', + `teacher_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '教师ID', + `class` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '班级', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_student_id` (`student_id`) USING BTREE, + KEY `idx_teacher_id` (`teacher_id`) USING BTREE, + KEY `idx_class` (`class`) USING BTREE, + CONSTRAINT `scores_ibfk_1` FOREIGN KEY (`student_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `scores_ibfk_2` FOREIGN KEY (`teacher_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `scores_chk_1` CHECK (((`score` >= 0) and (`score` <= 100))) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='成绩记录表'; + + +-- Table structure for table `sessions` +DROP TABLE IF EXISTS `sessions`; +CREATE TABLE `sessions` ( + `session_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `expires` int unsigned NOT NULL, + `data` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin, + PRIMARY KEY (`session_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC; + +-- Dumping data for table `sessions` +INSERT INTO `sessions` VALUES +('KY6QaavAiws7rkdEBFIFDoHefl2bxzlI', 1766334525, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T16:28:44.650Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"teststudent","name":"????","role":"student","class":"2023?1?"}}'), +('KaDFs4HogLmkjS0HAs6qki6g2FmE3sTL', 1766334465, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T16:27:45.314Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"teststudent","name":"????","role":"student","class":"2023?1?"}}'), +('P-_AKfysnsUwA6PypoYdyXCxa-_8TiCE', 1766413502, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-22T14:25:01.323Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"3001","name":"陈同学","role":"student","class":"计算机2301","studentInfo":{"id":"3001","name":"陈同学","class":"计算机2301"}}}'), +('QAhXDQ1FOlhU6RhaFOm3ghRtLOW4hBTd', 1766334812, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T16:33:31.987Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"teststudent","name":"????","role":"student","class":"2023?1?"}}'), +('SAuQyktAI9gAHpXbjARpe-9BL42pDRiV', 1766334764, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T16:32:43.695Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"teststudent","name":"????","role":"student","class":"2023?1?"}}'), +('XduN1lYhGPeIaLTHbLTNVnTCBtKUCkJR', 1766334689, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T16:31:28.994Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"teststudent","name":"????","role":"student","class":"2023?1?"}}'), +('Y59PFvvqK7M0DKZshc6ONTmFQjzGyMmV', 1766334426, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T16:27:05.673Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"teststudent","name":"????","role":"student","class":"2023?1?"}}'), +('rlscT2Pi2EAyLXHs1CNXyQmNSiW8vEo4', 1766334271, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T16:24:30.682Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"teststudent","name":"????","role":"student","class":"2023?1?"}}'), +('rsaOCJRjYQLPtUWlDmUFJgWcCYZbOCgJ', 1766410574, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T15:54:39.935Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"123","name":"经济局","role":"student","class":"123"}}'), +('wXxRpNTGY0wqLaHsebSAsw1I6Pb7Ed6w', 1766410584, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-22T13:35:43.191Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"567","name":"急急急","role":"teacher","class":"567"}}'); + + +-- Table structure for table `students` +DROP TABLE IF EXISTS `students`; +CREATE TABLE `students` ( + `id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '学生ID(与users表id一致)', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '姓名', + `class` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '班级', + PRIMARY KEY (`id`) USING BTREE, + CONSTRAINT `students_ibfk_1` FOREIGN KEY (`id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='学生详细信息表'; + +-- Dumping data for table `students` +INSERT INTO `students` VALUES +('3001', '陈同学', '计算机2301'), +('3002', '林同学', '计算机2301'), +('3003', '黄同学', '软件工程2302'), +('3004', '吴同学', '软件工程2302'); + + +-- Table structure for table `users` +DROP TABLE IF EXISTS `users`; +CREATE TABLE `users` ( + `id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户ID(学号/工号)', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '姓名', + `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码(bcrypt加密)', + `role` enum('student','teacher','admin') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色', + `class` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '班级(学生和教师需要,管理员为NULL)', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_role` (`role`) USING BTREE, + KEY `idx_class` (`class`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户表'; + +-- Dumping data for table `users` diff --git a/frontend/public/js/auth.js b/frontend/public/js/auth.js index ef33b8c..41c3d2c 100644 --- a/frontend/public/js/auth.js +++ b/frontend/public/js/auth.js @@ -1,6 +1,7 @@ /** * 认证模块管理器 * 处理登录、注册、注销及权限检查 + * 适配 Bootstrap 5 样式 */ class AuthManager { constructor() { @@ -15,21 +16,14 @@ class AuthManager { } /** - * 创建通知容器 + * 创建通知容器 (适配 Bootstrap Toast) */ createNotificationContainer() { if (!document.getElementById('notification-container')) { const container = document.createElement('div'); container.id = 'notification-container'; - container.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - z-index: 9999; - display: flex; - flex-direction: column; - gap: 10px; - `; + container.className = 'toast-container position-fixed top-0 end-0 p-3'; + container.style.zIndex = '9999'; document.body.appendChild(container); } } @@ -38,7 +32,6 @@ class AuthManager { * 检查用户认证状态 */ async checkAuthStatus() { - // 如果当前是公共页面,可以选择不检查,或者检查后更新UI const currentPath = window.location.pathname; const isAuthPage = currentPath.includes('/login') || currentPath.includes('/register'); @@ -46,20 +39,11 @@ class AuthManager { const response = await fetch(`${this.apiBase}/auth/me`); const data = await response.json(); - if (data.success && data.user) { - // 用户已登录 - const redirectUrl = this.getDashboardUrl(data.user.role); - - // 如果在登录/注册页,跳转到仪表板 + if (data.success && data.data && data.data.user) { + const redirectUrl = this.getDashboardUrl(data.data.user.role); if (isAuthPage || currentPath === '/') { window.location.href = redirectUrl; } - } else { - // 用户未登录,如果在受保护页面,跳转到登录页 - // 注意:后端通常已经处理了重定向,这里是前端的额外保障 - if (!isAuthPage && currentPath !== '/') { - // 可以在这里添加逻辑,但通常交给后端控制 - } } } catch (error) { console.error('Auth check failed:', error); @@ -94,7 +78,7 @@ class AuthManager { } } - // 注销按钮 (可能有多个,例如在导航栏) + // 注销按钮 document.querySelectorAll('.btn-logout, #logoutBtn').forEach(btn => { btn.addEventListener('click', (e) => this.handleLogout(e)); }); @@ -107,12 +91,12 @@ class AuthManager { if (classField && classInput) { if (role === 'student' || role === 'teacher') { - classField.style.display = 'block'; + classField.style.display = 'flex'; // 配合 Bootstrap input-group classInput.required = true; } else { classField.style.display = 'none'; classInput.required = false; - classInput.value = ''; // 清空值 + classInput.value = ''; } } } @@ -120,9 +104,9 @@ class AuthManager { async handleLogin(e) { e.preventDefault(); const form = e.target; - const submitBtn = form.querySelector('button[type="submit"]'); + const submitBtn = document.getElementById('loginBtn') || form.querySelector('button[type="submit"]'); - if (this.setLoading(submitBtn, true, '登录中...')) { + if (this.setLoading(submitBtn, true)) { try { const formData = new FormData(form); const data = Object.fromEntries(formData.entries()); @@ -137,16 +121,27 @@ class AuthManager { if (result.success) { this.showNotification('登录成功,正在跳转...', 'success'); - setTimeout(() => { - window.location.href = this.getDashboardUrl(result.user.role); - }, 1000); + + // 获取用户信息,优先从 result.data.user 获取,兼容旧格式 result.data 或 result.user + const user = (result.data && result.data.user) || result.data || result.user; + const role = user ? user.role : null; + + if (role) { + setTimeout(() => { + window.location.href = this.getDashboardUrl(role); + }, 1000); + } else { + console.error('Login success but role not found:', result); + this.showNotification('登录状态异常,请重试', 'error'); + this.setLoading(submitBtn, false); + } } else { this.showNotification(result.message || '登录失败', 'error'); this.setLoading(submitBtn, false); } } catch (error) { console.error('Login error:', error); - this.showNotification('网络错误,请稍后重试', 'error'); + this.showNotification('服务器连接失败,请稍后重试', 'error'); this.setLoading(submitBtn, false); } } @@ -155,19 +150,17 @@ class AuthManager { async handleRegister(e) { e.preventDefault(); const form = e.target; - const submitBtn = form.querySelector('button[type="submit"]'); + const submitBtn = document.getElementById('registerBtn') || form.querySelector('button[type="submit"]'); - // 获取数据 const formData = new FormData(form); const data = Object.fromEntries(formData.entries()); - // 简单验证 if (data.password !== data.confirmPassword) { this.showNotification('两次输入的密码不一致', 'error'); return; } - if (this.setLoading(submitBtn, true, '注册中...')) { + if (this.setLoading(submitBtn, true)) { try { const response = await fetch(`${this.apiBase}/auth/register`, { method: 'POST', @@ -188,7 +181,7 @@ class AuthManager { } } catch (error) { console.error('Register error:', error); - this.showNotification('网络错误,请稍后重试', 'error'); + this.showNotification('服务器连接失败,请稍后重试', 'error'); this.setLoading(submitBtn, false); } } @@ -196,24 +189,17 @@ class AuthManager { async handleLogout(e) { e.preventDefault(); - if (confirm('确定要退出登录吗?')) { try { const response = await fetch(`${this.apiBase}/auth/logout`, { method: 'POST' }); - const result = await response.json(); - if (result.success) { - this.showNotification('已退出登录', 'success'); - setTimeout(() => { - window.location.href = '/login'; - }, 1000); + window.location.href = '/login'; } } catch (error) { console.error('Logout error:', error); - // 即使出错也强制跳转到登录页 window.location.href = '/login'; } } @@ -221,67 +207,68 @@ class AuthManager { /** * 设置按钮加载状态 - * @param {HTMLElement} btn 按钮元素 - * @param {boolean} isLoading 是否正在加载 - * @param {string} text 加载时的文本 - * @returns {boolean} true表示状态设置成功 */ - setLoading(btn, isLoading, text = '') { + setLoading(btn, isLoading) { if (!btn) return false; + const spinner = btn.querySelector('.spinner-border'); if (isLoading) { - if (btn.dataset.loading) return false; // 防止重复提交 - btn.dataset.loading = 'true'; - btn.dataset.originalText = btn.innerHTML; - btn.innerHTML = ` ${text}`; + if (btn.disabled) return false; btn.disabled = true; + if (spinner) spinner.classList.remove('d-none'); } else { - btn.innerHTML = btn.dataset.originalText || btn.innerHTML; - delete btn.dataset.loading; btn.disabled = false; + if (spinner) spinner.classList.add('d-none'); } return true; } /** - * 显示通知 - * @param {string} message 消息内容 - * @param {string} type 消息类型 'success' | 'error' | 'info' + * 显示通知 (Bootstrap 5 Toast) */ showNotification(message, type = 'info') { const container = document.getElementById('notification-container'); if (!container) return; - const notification = document.createElement('div'); - notification.className = `notification ${type}`; + const iconMap = { + success: 'fa-check-circle', + error: 'fa-exclamation-circle', + warning: 'fa-exclamation-triangle', + info: 'fa-info-circle' + }; - let icon = 'info-circle'; - if (type === 'success') icon = 'check-circle'; - if (type === 'error') icon = 'exclamation-circle'; - - notification.innerHTML = ` - - ${message} + const bgMap = { + success: 'bg-success', + error: 'bg-danger', + warning: 'bg-warning', + info: 'bg-info' + }; + + const toastId = 'toast-' + Date.now(); + const toastHtml = ` + `; + + container.insertAdjacentHTML('beforeend', toastHtml); + const toastElement = document.getElementById(toastId); + const toast = new bootstrap.Toast(toastElement, { delay: 3000 }); + toast.show(); - container.appendChild(notification); - - // 动画显示 - requestAnimationFrame(() => { - notification.classList.add('show'); + toastElement.addEventListener('hidden.bs.toast', () => { + toastElement.remove(); }); - - // 自动消失 - setTimeout(() => { - notification.classList.remove('show'); - notification.addEventListener('transitionend', () => { - notification.remove(); - }); - }, 3000); } } -// 初始化 +// 初始化认证管理器 document.addEventListener('DOMContentLoaded', () => { window.authManager = new AuthManager(); }); \ No newline at end of file diff --git a/frontend/public/js/student.js b/frontend/public/js/student.js index aa743af..cfd19d8 100644 --- a/frontend/public/js/student.js +++ b/frontend/public/js/student.js @@ -1,438 +1,135 @@ +/** + * 学生端功能管理 + */ class StudentManager { constructor() { - // 动态设置API基础URL,支持file:///协议和localhost:3000访问 - this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api'; + this.apiBase = '/api/student'; + this.init(); + } + + init() { this.initDashboard(); - this.initGradeDetails(); - this.loadProfile(); + this.updateCurrentTime(); + setInterval(() => this.updateCurrentTime(), 1000); + } + + 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 initDashboard() { - const gradeList = document.getElementById('gradeList'); - const statisticsElement = document.getElementById('statistics'); - - if (!gradeList) return; + // 检查是否在仪表板页面 + if (!document.getElementById('gradesTableBody')) return; try { - const response = await fetch(`${this.apiBase}/student/grades`, { - credentials: 'include' - }); + const response = await fetch(`${this.apiBase}/grades`); + const result = await response.json(); - if (response.status === 401) { - // 未登录,重定向到登录? - this.showNotification('请先登录', 'error'); - setTimeout(() => { - window.location.href = '/login'; - }, 1500); + if (result.success) { + this.renderDashboard(result.data); + } else { + if (window.authManager) { + window.authManager.showNotification(result.message || '获取数据失败', 'error'); + } + } + } catch (error) { + console.error('Fetch dashboard data failed:', error); + } + } + + renderDashboard(data) { + const { grades, statistics } = data; + + // 渲染统计数据 + if (statistics) { + this.updateElement('gpaValue', statistics.gpa); + this.updateElement('courseCount', statistics.totalCourses); + this.updateElement('creditTotal', statistics.totalCredits); + // 班级排名如果后端没提供,可以显示为 '-' 或固定值 + this.updateElement('classRank', statistics.classRank || '-'); + } + + // 渲染成绩表格 + const tbody = document.getElementById('gradesTableBody'); + if (tbody) { + if (!grades || grades.length === 0) { + tbody.innerHTML = '暂无成绩记录'; return; } - const data = await response.json(); - - if (data.success) { - this.renderGrades(data.grades); - this.renderStatistics(data.statistics); - this.updateChart(data.grades); - } else { - this.showNotification(data.message || '获取成绩失败', 'error'); - } - } catch (error) { - console.error('获取成绩错误:', error); - this.showNotification('网络错误,请重试', 'error'); + tbody.innerHTML = grades.map(grade => ` + + ${grade.course_name} + ${grade.course_code || '-'} + ${grade.credit} + ${grade.score} + ${grade.grade_level || '-'} + ${grade.grade_point || '-'} + + + ${this.getScoreText(grade.score)} + + + + + + + `).join(''); } } - renderGrades(grades) { - const gradeList = document.getElementById('gradeList'); - const gradeTable = document.getElementById('gradeTable'); - - if (!gradeTable) return; - - if (grades.length === 0) { - gradeList.innerHTML = ` -
- -

暂无成绩记录

-

你还没有任何成绩记录

-
- `; - return; - } - - const tbody = gradeTable.querySelector('tbody'); - tbody.innerHTML = ''; - - grades.forEach(grade => { - const row = document.createElement('tr'); - - // 根据分数设置颜色 - let scoreClass = ''; - if (grade.score >= 90) scoreClass = 'grade-excellent'; - else if (grade.score >= 80) scoreClass = 'grade-good'; - else if (grade.score >= 60) scoreClass = 'grade-pass'; - else scoreClass = 'grade-fail'; - - row.innerHTML = ` - ${grade.course_code} - ${grade.course_name} - ${grade.credit} - - ${grade.score} - - ${grade.grade_level || '-'} - ${grade.grade_point || '-'} - ${grade.teacher_name} - ${new Date(grade.exam_date).toLocaleDateString()} - - - 查看 - - - `; - - tbody.appendChild(row); - }); + updateElement(id, value) { + const el = document.getElementById(id); + if (el) el.textContent = value; } - renderStatistics(statistics) { - const element = document.getElementById('statistics'); - if (!element) return; - - element.innerHTML = ` -
-
-
- -
-
${statistics.totalCourses}
-
总课程数
-
-
-
- -
-
${statistics.totalCredits}
-
总学?/div> -
-
-
- -
-
${statistics.averageScore}
-
平均?/div> -
-
-
- -
-
${statistics.gpa}
-
平均绩点
-
-
- `; + getScoreBadgeClass(score) { + const s = parseFloat(score); + if (s >= 90) return 'bg-success'; + if (s >= 80) return 'bg-info'; + if (s >= 60) return 'bg-warning'; + return 'bg-danger'; } - async loadProfile() { - const profileElement = document.getElementById('profileInfo'); - if (!profileElement) return; - - try { - const response = await fetch(`${this.apiBase}/student/profile`, { - credentials: 'include' - }); - - if (response.status === 401) { - // 未登录,重定向到登录? - this.showNotification('请先登录', 'error'); - setTimeout(() => { - window.location.href = '/login'; - }, 1500); - return; - } - - const data = await response.json(); - - if (data.success) { - const profile = data.profile; - - // 更新学生仪表板顶部信? - const userNameElement = document.getElementById('userName'); - const studentNameElement = document.getElementById('studentName'); - const studentClassElement = document.getElementById('studentClass'); - - if (userNameElement) { - userNameElement.textContent = profile.full_name || profile.username; - } - if (studentNameElement) { - studentNameElement.textContent = profile.full_name || profile.username; - } - if (studentClassElement) { - studentClassElement.textContent = profile.class_name || '未设?; - } - - profileElement.innerHTML = ` -
-
- -
-
-

${profile.full_name}

-

- 学生 -

-
-
-
-
- -
-

学号

-

${profile.student_id}

-
-
-
- -
-

班级

-

${profile.class_name}

-
-
-
- -
-

专业

-

${profile.major || '未设?}

-
-
-
- -
-

入学年份

-

${profile.enrollment_year || '未设?}

-
-
-
- `; - } else { - // API返回失败 - this.showNotification(data.message || '获取个人信息失败', 'error'); - } - } catch (error) { - console.error('加载个人信息错误:', error); - this.showNotification('网络错误,请重试', 'error'); - } + getScoreText(score) { + const s = parseFloat(score); + if (s >= 60) return '及格'; + return '不及格'; } - async initGradeDetails() { - const urlParams = new URLSearchParams(window.location.search); - const gradeId = urlParams.get('id'); - - if (!gradeId) return; - - try { - const response = await fetch(`${this.apiBase}/student/grades/${gradeId}`, { - credentials: 'include' - }); - const data = await response.json(); - - if (data.success) { - this.renderGradeDetails(data.grade); - } else { - this.showNotification('获取成绩详情失败', 'error'); - setTimeout(() => window.history.back(), 1500); - } - } catch (error) { - console.error('获取成绩详情错误:', error); - this.showNotification('网络错误,请重试', 'error'); - } - } - - renderGradeDetails(grade) { - const container = document.getElementById('gradeDetails'); - if (!container) return; - - // 计算绩点描述 - let gradeDescription = ''; - if (grade.score >= 90) gradeDescription = '优秀'; - else if (grade.score >= 80) gradeDescription = '良好'; - else if (grade.score >= 70) gradeDescription = '中等'; - else if (grade.score >= 60) gradeDescription = '及格'; - else gradeDescription = '不及?; - - container.innerHTML = ` -
-
-

${grade.course_name} (${grade.course_code})

-
- ${grade.score} ? - ${gradeDescription} -
-
- -
-
-

基本信息

-
- 学分?/span> - ${grade.credit} -
-
- 学期?/span> - ${grade.semester} -
-
- 考试日期?/span> - ${new Date(grade.exam_date).toLocaleDateString()} -
-
- 等级?/span> - ${grade.grade_level || '-'} -
-
- 绩点?/span> - ${grade.grade_point || '-'} -
-
- -
-

学生信息

-
- 姓名?/span> - ${grade.full_name} -
-
- 学号?/span> - ${grade.student_number} -
-
- 班级?/span> - ${grade.class_name} -
-
- 专业?/span> - ${grade.major || '未设?} -
-
- -
-

教师信息

-
- 任课教师?/span> - ${grade.teacher_name} -
-
- 教师邮箱?/span> - ${grade.teacher_email} -
-
-
- - ${grade.remark ? ` -
-

备注

-

${grade.remark}

-
- ` : ''} - -
- - -
-
- `; - } - - updateChart(grades) { - const ctx = document.getElementById('gradeChart'); - if (!ctx) return; - - if (typeof Chart === 'undefined') { - // 如果没有Chart.js,延迟加? - this.loadChartLibrary().then(() => this.updateChart(grades)); - return; - } - - const courseNames = grades.map(g => g.course_name); - const scores = grades.map(g => g.score); - - // 销毁现有图表实? - if (window.gradeChart instanceof Chart) { - window.gradeChart.destroy(); - } - - window.gradeChart = new Chart(ctx, { - type: 'bar', - data: { - labels: courseNames, - datasets: [{ - label: '分数', - data: scores, - backgroundColor: scores.map(score => { - if (score >= 90) return 'rgba(75, 192, 192, 0.7)'; - if (score >= 80) return 'rgba(54, 162, 235, 0.7)'; - if (score >= 60) return 'rgba(255, 206, 86, 0.7)'; - return 'rgba(255, 99, 132, 0.7)'; - }), - borderColor: scores.map(score => { - if (score >= 90) return 'rgb(75, 192, 192)'; - if (score >= 80) return 'rgb(54, 162, 235)'; - if (score >= 60) return 'rgb(255, 206, 86)'; - return 'rgb(255, 99, 132)'; - }), - borderWidth: 1 - }] - }, - options: { - responsive: true, - plugins: { - title: { - display: true, - text: '各科成绩分布' - } - }, - scales: { - y: { - beginAtZero: true, - max: 100 - } - } - } - }); - } - - async loadChartLibrary() { - return new Promise((resolve, reject) => { - if (typeof Chart !== 'undefined') { - resolve(); - return; - } - - const script = document.createElement('script'); - script.src = 'https://cdn.jsdelivr.net/npm/chart.js'; - script.onload = resolve; - script.onerror = reject; - document.head.appendChild(script); - }); - } - - showNotification(message, type = 'info') { - // 使用AuthManager的通知系统或自己实? - if (window.authManager && window.authManager.showNotification) { - window.authManager.showNotification(message, type); - } else { - alert(message); - } + viewDetails(id) { + // 实现查看详情逻辑,或者跳转到详情页 + console.log('View details for score:', id); } } -// 初始化学生管理器 +// 初始化 document.addEventListener('DOMContentLoaded', () => { - if (window.location.pathname.includes('/student/')) { - window.studentManager = new StudentManager(); - } -}); + window.studentManager = new StudentManager(); + + // 从 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 studentNameEl = document.getElementById('studentName'); + const studentClassEl = document.getElementById('studentClass'); + + if (nameEl) nameEl.textContent = user.name; + if (studentNameEl) studentNameEl.textContent = user.name; + if (studentClassEl) studentClassEl.textContent = user.class || '未分配'; + } + }); +}); \ No newline at end of file diff --git a/frontend/public/js/teacher.js b/frontend/public/js/teacher.js index e9535ee..83b0540 100644 --- a/frontend/public/js/teacher.js +++ b/frontend/public/js/teacher.js @@ -1,406 +1,108 @@ -class TeacherDashboard { +/** + * 教师端功能管理 + */ +class TeacherManager { constructor() { - // 动态设置API基础URL,支持file:///协议和localhost:3000访问 - this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api'; - this.currentUser = null; - this.courses = []; - this.grades = []; + this.apiBase = '/api/teacher'; this.init(); } - - async init() { - // 检查登录状? if (!await this.checkAuth()) { - window.location.href = '/login'; + + init() { + this.initDashboard(); + this.updateCurrentTime(); + setInterval(() => this.updateCurrentTime(), 1000); + } + + 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 initDashboard() { + // 检查是否在仪表板页面 + if (!document.getElementById('courseList')) return; + + 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'); + } + } + } catch (error) { + console.error('Fetch teacher data failed:', error); + } + } + + renderDashboard(courses) { + const courseList = document.getElementById('courseList'); + if (!courseList) return; + + if (!courses || courses.length === 0) { + courseList.innerHTML = '
暂无负责课程
'; return; } - - // 加载用户信息 - await this.loadUserInfo(); - // 加载课程数据 - await this.loadCourses(); - - // 加载成绩数据 - await this.loadGrades(); - - // 绑定事件 - this.bindEvents(); - - // 更新界面 - this.updateUI(); - } - - async checkAuth() { - try { - const response = await fetch(`${this.apiBase}/auth/check`, { - credentials: 'include' - }); - - if (!response.ok) { - return false; - } - - const data = await response.json(); - return data.success && data.user.role === 'teacher'; - } catch (error) { - console.error('认证检查失?', error); - return false; - } - } - - 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; - } - } - } catch (error) { - console.error('加载用户信息失败:', error); - } - } - - async loadCourses() { - try { - const response = await fetch(`${this.apiBase}/teacher/courses`, { - credentials: 'include' - }); - - if (response.ok) { - const data = await response.json(); - if (data.success) { - this.courses = data.courses; - this.populateCourseSelectors(); - } - } - } catch (error) { - console.error('加载课程失败:', error); - this.showNotification('加载课程失败', 'error'); - } - } - - async loadGrades(filters = {}) { - try { - const queryParams = new URLSearchParams(filters).toString(); - const url = `${this.apiBase}/teacher/grades${queryParams ? '?' + queryParams : ''}`; - - const response = await fetch(url, { - credentials: 'include' - }); - - if (response.ok) { - const data = await response.json(); - if (data.success) { - this.grades = data.grades; - this.renderGradesTable(); - } - } - } catch (error) { - console.error('加载成绩失败:', error); - this.showNotification('加载成绩失败', 'error'); - } - } - - populateCourseSelectors() { - // 填充课程选择? const courseSelectors = document.querySelectorAll('.course-selector'); - courseSelectors.forEach(select => { - select.innerHTML = ''; - this.courses.forEach(course => { - const option = document.createElement('option'); - option.value = course.id; - option.textContent = `${course.course_code} - ${course.course_name}`; - select.appendChild(option); - }); - }); - } - - renderGradesTable() { - const tableBody = document.getElementById('gradesTableBody'); - if (!tableBody) return; - - if (this.grades.length === 0) { - tableBody.innerHTML = ` - - -
- -

暂无成绩数据

+ courseList.innerHTML = courses.map(course => ` +
+
+
+
+ + ${course.course_code || 'CODE'} + + ${course.credit} 学分
- - - `; - return; - } - - tableBody.innerHTML = this.grades.map(grade => { - const gradeClass = this.getGradeClass(grade.score); - return ` - - - ${grade.student_id} - ${grade.full_name} - ${grade.class_name} - ${grade.course_code} - ${grade.course_name} - - ${grade.score} - ${grade.grade_level} - - ${grade.exam_date ? new Date(grade.exam_date).toLocaleDateString() : '未设?} - -
- - +
${course.course_name}
+

+ 学生人数: ${course.student_count || 0} +

+ - - - `; - }).join(''); - - // 更新统计信息 - this.updateStats(); - } - - getGradeClass(score) { - if (score >= 90) return 'grade-excellent'; - if (score >= 80) return 'grade-good'; - if (score >= 70) return 'grade-medium'; - if (score >= 60) return 'grade-pass'; - return 'grade-fail'; - } - - updateStats() { - if (this.grades.length === 0) return; - - const totalStudents = new Set(this.grades.map(g => g.student_id)).size; - const avgScore = this.grades.reduce((sum, g) => sum + g.score, 0) / this.grades.length; - const passRate = (this.grades.filter(g => g.score >= 60).length / this.grades.length * 100).toFixed(1); - +
+
+
+ `).join(''); + + // 更新统计数据 + document.getElementById('courseCount').textContent = courses.length; + const totalStudents = courses.reduce((sum, c) => sum + (c.student_count || 0), 0); document.getElementById('totalStudents').textContent = totalStudents; - document.getElementById('avgScore').textContent = avgScore.toFixed(1); - document.getElementById('passRate').textContent = `${passRate}%`; - } - - bindEvents() { - // 搜索按钮 - document.getElementById('searchBtn')?.addEventListener('click', () => { - this.handleSearch(); - }); - - // 重置按钮 - document.getElementById('resetBtn')?.addEventListener('click', () => { - this.resetFilters(); - }); - - // 导出按钮 - document.getElementById('exportBtn')?.addEventListener('click', () => { - this.exportGrades(); - }); - - // 批量删除按钮 - document.getElementById('batchDeleteBtn')?.addEventListener('click', () => { - this.batchDeleteGrades(); - }); - - // 表格操作按钮事件委托 - document.addEventListener('click', (e) => { - if (e.target.closest('.btn-edit')) { - const gradeId = e.target.closest('.btn-edit').dataset.id; - this.editGrade(gradeId); - } - - if (e.target.closest('.btn-delete')) { - const gradeId = e.target.closest('.btn-delete').dataset.id; - this.deleteGrade(gradeId); - } - }); - - // 退出登? document.getElementById('logoutBtn')?.addEventListener('click', () => { - this.handleLogout(); - }); - } - - handleSearch() { - const className = document.getElementById('classFilter')?.value || ''; - const courseId = document.getElementById('courseFilter')?.value || ''; - - this.loadGrades({ class_name: className, course_id: courseId }); - } - - resetFilters() { - document.getElementById('classFilter').value = ''; - document.getElementById('courseFilter').value = ''; - this.loadGrades(); - } - - async exportGrades() { - try { - const response = await fetch(`${this.apiBase}/teacher/grades/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 batchDeleteGrades() { - const checkboxes = document.querySelectorAll('.grade-checkbox:checked'); - if (checkboxes.length === 0) { - this.showNotification('请选择要删除的成绩', 'warning'); - return; - } - - if (!confirm(`确定要删除选中?${checkboxes.length} 条成绩记录吗?`)) { - return; - } - - const gradeIds = Array.from(checkboxes).map(cb => cb.dataset.id); - - try { - const response = await fetch(`${this.apiBase}/teacher/grades/batch`, { - method: 'DELETE', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - body: JSON.stringify({ gradeIds }) - }); - - const data = await response.json(); - if (data.success) { - this.showNotification(`成功删除 ${gradeIds.length} 条成绩记录`, 'success'); - await this.loadGrades(); - } else { - this.showNotification(data.message || '删除失败', 'error'); - } - } catch (error) { - console.error('批量删除失败:', error); - this.showNotification('批量删除失败', 'error'); - } - } - - async editGrade(gradeId) { - const grade = this.grades.find(g => g.id == gradeId); - if (!grade) return; - - // 这里可以打开编辑模态框 - // 暂时使用简单提示框 - const newScore = prompt('请输入新的分?', grade.score); - if (newScore === null) return; - - const numericScore = parseFloat(newScore); - if (isNaN(numericScore) || numericScore < 0 || numericScore > 100) { - this.showNotification('请输?-100之间的有效分?, 'error'); - return; - } - - try { - const response = await fetch(`${this.apiBase}/teacher/grades/${gradeId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - body: JSON.stringify({ - score: numericScore, - examDate: grade.exam_date, - remark: grade.remark - }) - }); - - const data = await response.json(); - if (data.success) { - this.showNotification('成绩更新成功', 'success'); - await this.loadGrades(); - } else { - this.showNotification(data.message || '更新失败', 'error'); - } - } catch (error) { - console.error('更新成绩失败:', error); - this.showNotification('更新成绩失败', 'error'); - } - } - - async deleteGrade(gradeId) { - if (!confirm('确定要删除这条成绩记录吗?)) { - return; - } - - try { - const response = await fetch(`${this.apiBase}/teacher/grades/${gradeId}`, { - method: 'DELETE', - credentials: 'include' - }); - - const data = await response.json(); - if (data.success) { - this.showNotification('成绩删除成功', 'success'); - await this.loadGrades(); - } else { - this.showNotification(data.message || '删除失败', 'error'); - } - } catch (error) { - console.error('删除成绩失败:', error); - this.showNotification('删除成绩失败', 'error'); - } - } - - 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); - } - } - - updateUI() { - // 更新用户信息 - if (this.currentUser) { - const userInfoElements = document.querySelectorAll('.user-info'); - userInfoElements.forEach(el => { - el.textContent = `${this.currentUser.full_name} (${this.currentUser.role})`; - }); - } - } - - showNotification(message, type = 'info') { - // 使用AuthManager的通知系统或简单alert - if (typeof AuthManager !== 'undefined' && AuthManager.showNotification) { - AuthManager.showNotification(message, type); - } else { - alert(`${type}: ${message}`); - } } } -// 页面加载完成后初始化 +// 初始化 document.addEventListener('DOMContentLoaded', () => { - if (window.location.pathname.includes('/teacher/')) { - new TeacherDashboard(); - } -}); + 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'); + + if (nameEl) nameEl.textContent = user.name; + if (teacherNameEl) teacherNameEl.textContent = user.name; + } + }); +}); \ No newline at end of file diff --git a/frontend/views/auth/login.html b/frontend/views/auth/login.html index 9bd53f5..10ce4c0 100644 --- a/frontend/views/auth/login.html +++ b/frontend/views/auth/login.html @@ -3,60 +3,177 @@ - 学生成绩管理系统 - 登录 - - + 登录 - 学生成绩管理系统 + + + + + + + -
-
-

学生成绩管理系统

-

请登录您的账户

-
- -
-
-
- - -
- -
- - -
- -
- - -
- -
- -
- - -
+ + + +
- + + + \ No newline at end of file diff --git a/frontend/views/auth/register.html b/frontend/views/auth/register.html index ed4df6b..0cebd74 100644 --- a/frontend/views/auth/register.html +++ b/frontend/views/auth/register.html @@ -3,84 +3,185 @@ - 学生成绩管理系统 - 注册 - - + 注册 - 学生成绩管理系统 + + + + + + + -
-
-

学生成绩管理系统

-

创建新账户

-
- -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - -
- -
- - -
+ + + +
- + + + \ No newline at end of file diff --git a/frontend/views/student/dashboard.html b/frontend/views/student/dashboard.html index 534c8e8..d10be6e 100644 --- a/frontend/views/student/dashboard.html +++ b/frontend/views/student/dashboard.html @@ -3,191 +3,386 @@ - 学生成绩管理系统 - 学生仪表?/title> - <link rel="stylesheet" href="/public/css/style.css"> - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> -</head> + <title>学生仪表板 - 成绩管理系统 + + + + + + + - - - -
- - - - -
-
-
-

学生仪表?/h1> - -

-
+ - - -
-
-
- -
-
3.75
-
平均绩点
-
- -
-
- -
-
8
-
已修课程
-
- -
-
- -
-
24
-
总学?/div> -
- -
-
- -
-
5
-
班级排名
-
+ - - -
-
-

本学期成?/h2> -
- - -
-

- -
- - - - - - - - - - - - - - - -
课程名称课程代码学分平时成绩期末成绩总成?/th> - 绩点操作
-
+ -
+ +
- + +
+ +
+
+

学生仪表板

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

平均绩点 (GPA)

+

0.00

+
+
+ +
+
+
+
+
+
+
+
+

已修课程

+

0

+
+
+ +
+
+
+
+
+
+
+
+

获得学分

+

0

+
+
+ +
+
+
+
+
+
+
+
+

班级排名

+

-

+
+
+ +
+
+
+
+
+ + +
+
+
近期成绩记录
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + +
课程名称课程代码学分分数等第绩点状态操作
+
+ 数据加载中... +
+
+
+
+ + + - + - + \ No newline at end of file diff --git a/frontend/views/student/profile.html b/frontend/views/student/profile.html new file mode 100644 index 0000000..0c840ed --- /dev/null +++ b/frontend/views/student/profile.html @@ -0,0 +1,347 @@ + + + + + + 个人中心 - 成绩管理系统 + + + + + + + + + + + + + +
+ +
+
+

个人中心

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

加载中...

+

学生账户

+
+ +
+
+
+
+ +
+
+
+ 基本信息 +
+
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+
安全设置
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/frontend/views/teacher/dashboard.html b/frontend/views/teacher/dashboard.html index 6dfbaa4..b8f4c79 100644 --- a/frontend/views/teacher/dashboard.html +++ b/frontend/views/teacher/dashboard.html @@ -3,505 +3,312 @@ - 学生成绩管理系统 - 教师仪表?/title> - <link rel="stylesheet" href="/public/css/style.css"> - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> -</head> - /* 仪表板布局 */ - .dashboard-container { - display: flex; - min-height: calc(100vh - 80px); + <title>教师仪表板 - 成绩管理系统 + + + + + + + - - - -
- - - - -
-
-
-

教师仪表?/h1> -