学期 GPA 趋势
+ +成绩等第分布
+学分完成进度
+已获得 0 / 目标 160 学分
++
From 16802c85e53c2c3d11c95e594e0905b44579a193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=80=E6=A2=A6?= <3501646051@qq.com> Date: Mon, 22 Dec 2025 21:07:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AD=A6=E7=94=9F):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=88=90=E7=BB=A9=E5=88=86=E6=9E=90=E5=8A=9F=E8=83=BD=E5=8F=8A?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E4=BF=AE=E6=94=B9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增成绩分析页面,包含GPA趋势图、成绩分布图和学分进度 - 实现学生密码修改功能,包括前端表单和后端验证逻辑 - 添加课程类别分析功能,展示不同类别课程的GPA表现 - 优化学生仪表板和课程页面导航链接 - 增加数据加载状态提示和错误处理 --- backend/controllers/authController.js | 20 ++ backend/controllers/studentController.js | 14 + backend/models/User.js | 7 + backend/routes/auth.js | 2 + backend/routes/student.js | 1 + backend/server.js | 3 + backend/services/authService.js | 18 +- backend/services/studentService.js | 95 ++++++ frontend/public/js/student.js | 207 ++++++++++++- frontend/views/student/dashboard.html | 58 +++- frontend/views/student/grade_analysis.html | 341 +++++++++++++++++++++ frontend/views/student/my_courses.html | 5 +- frontend/views/student/profile.html | 62 +++- 13 files changed, 819 insertions(+), 14 deletions(-) create mode 100644 frontend/views/student/grade_analysis.html diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index 5aa9d07..4589135 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -65,6 +65,26 @@ class AuthController { res.json({ success: false, message: '未登录' }); } } + + static async updatePassword(req, res) { + try { + const userId = req.session.user.id; + const { oldPassword, newPassword } = req.body; + + if (!oldPassword || !newPassword) { + return error(res, '请提供原密码和新密码', 400); + } + + await AuthService.updatePassword(userId, oldPassword, newPassword); + success(res, null, '密码修改成功'); + } catch (err) { + if (err.message === '原密码错误' || err.message === '用户不存在') { + return error(res, err.message, 400); + } + console.error('Update Password Error:', err); + error(res, '服务器错误'); + } + } } module.exports = AuthController; \ No newline at end of file diff --git a/backend/controllers/studentController.js b/backend/controllers/studentController.js index ac4f2e9..15c30d5 100644 --- a/backend/controllers/studentController.js +++ b/backend/controllers/studentController.js @@ -58,6 +58,20 @@ class StudentController { error(res, '服务器错误'); } } + + static async getGradeStatistics(req, res) { + try { + const userId = req.session.user.id; + const data = await StudentService.getGradeStatistics(userId); + success(res, data); + } catch (err) { + if (err.message === '学生信息不存在') { + return error(res, err.message, 404); + } + console.error('Get Grade Statistics Error:', err); + error(res, '服务器错误'); + } + } } module.exports = StudentController; diff --git a/backend/models/User.js b/backend/models/User.js index e4d5bf9..0177278 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -28,6 +28,13 @@ class User { static async verifyPassword(plainPassword, hashedPassword) { return await bcrypt.compare(plainPassword, hashedPassword); } + + static async updatePassword(id, newPassword) { + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(newPassword, salt); + await db.query('UPDATE users SET password = ? WHERE id = ?', [hashedPassword, id]); + return true; + } } module.exports = User; \ No newline at end of file diff --git a/backend/routes/auth.js b/backend/routes/auth.js index b3983d8..a5d57a2 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,10 +1,12 @@ const express = require('express'); const router = express.Router(); const AuthController = require('../controllers/authController'); +const { requireAuth } = require('../middleware/auth'); router.post('/login', AuthController.login); router.post('/register', AuthController.register); router.post('/logout', AuthController.logout); router.get('/me', AuthController.getCurrentUser); +router.put('/update-password', requireAuth, AuthController.updatePassword); module.exports = router; \ No newline at end of file diff --git a/backend/routes/student.js b/backend/routes/student.js index ce1ba53..973200d 100644 --- a/backend/routes/student.js +++ b/backend/routes/student.js @@ -5,6 +5,7 @@ const { requireAuth, requireRole } = require('../middleware/auth'); router.get('/courses', requireAuth, requireRole(['student']), StudentController.getCourses); router.get('/courses/:id', requireAuth, requireRole(['student']), StudentController.getCourseDetails); +router.get('/statistics', requireAuth, requireRole(['student']), StudentController.getGradeStatistics); router.get('/grades', requireAuth, requireRole(['student']), StudentController.getGrades); router.get('/grades/:id', requireAuth, requireRole(['student']), StudentController.getGradeDetails); diff --git a/backend/server.js b/backend/server.js index cd3c47c..abd23d3 100644 --- a/backend/server.js +++ b/backend/server.js @@ -99,6 +99,9 @@ app.get('/student/dashboard', requirePageAuth, requirePageRole(['student']), (re app.get('/student/my-courses', requirePageAuth, requirePageRole(['student']), (req, res) => { res.sendFile(path.join(__dirname, '../frontend/views/student/my_courses.html')); }); +app.get('/student/grade-analysis', requirePageAuth, requirePageRole(['student']), (req, res) => { + res.sendFile(path.join(__dirname, '../frontend/views/student/grade_analysis.html')); +}); app.get('/student/profile', requirePageAuth, requirePageRole(['student']), (req, res) => { res.sendFile(path.join(__dirname, '../frontend/views/student/profile.html')); }); diff --git a/backend/services/authService.js b/backend/services/authService.js index 9ae8c4d..be818f3 100644 --- a/backend/services/authService.js +++ b/backend/services/authService.js @@ -1,5 +1,6 @@ const User = require('../models/User'); const Student = require('../models/Student'); +const db = require('../config/database'); class AuthService { static async login(id, password, role) { @@ -55,7 +56,20 @@ class AuthService { return newUser; } + + static async updatePassword(userId, oldPassword, newPassword) { + const user = await User.findById(userId); + if (!user) { + throw new Error('用户不存在'); + } + + const isValid = await User.verifyPassword(oldPassword, user.password); + if (!isValid) { + throw new Error('原密码错误'); + } + + return await User.updatePassword(userId, newPassword); + } } -const db = require('../config/database'); // 补充引用 -module.exports = AuthService; \ No newline at end of file +module.exports = AuthService; diff --git a/backend/services/studentService.js b/backend/services/studentService.js index 32da19a..fbdbd17 100644 --- a/backend/services/studentService.js +++ b/backend/services/studentService.js @@ -1,6 +1,7 @@ const Score = require('../models/Score'); const Student = require('../models/Student'); const Course = require('../models/Course'); +const db = require('../config/database'); class StudentService { static async getStudentCourses(userId) { @@ -65,6 +66,100 @@ class StudentService { } return grade; } + + static async getGradeStatistics(userId) { + const student = await Student.findById(userId); + if (!student) { + throw new Error('学生信息不存在'); + } + + const sql = ` + SELECT g.total_score as score, g.grade_point, g.grade_level, + c.course_name, CAST(c.credit AS CHAR) as credit, c.semester, c.academic_year, c.category + FROM grades g + JOIN courses c ON g.course_id = c.id + WHERE g.student_id = ? + ORDER BY c.semester ASC + `; + const grades = await db.query(sql, [userId]); + + // 1. 学期 GPA 趋势 + const semesterGPA = {}; + // 5. 课程类别分析 + const categoryStats = {}; + + grades.forEach(g => { + // console.log('Processing grade:', g); + // 学期统计 + const semester = g.semester || '未知学期'; + if (!semesterGPA[semester]) { + semesterGPA[semester] = { totalGP: 0, totalCredits: 0 }; + } + const credit = parseFloat(g.credit || 0); + semesterGPA[semester].totalGP += parseFloat(g.grade_point || 0) * credit; + semesterGPA[semester].totalCredits += credit; + + // 类别统计 + const catName = g.category || '其他'; + if (!categoryStats[catName]) { + categoryStats[catName] = { totalGP: 0, totalCredits: 0 }; + } + categoryStats[catName].totalGP += parseFloat(g.grade_point || 0) * credit; + categoryStats[catName].totalCredits += credit; + }); + + const trend = Object.keys(semesterGPA).map(semester => ({ + semester, + gpa: (semesterGPA[semester].totalGP / semesterGPA[semester].totalCredits).toFixed(2) + })); + + const categories = Object.keys(categoryStats).map(cat => { + const stats = categoryStats[cat]; + const gpa = stats.totalCredits > 0 ? (stats.totalGP / stats.totalCredits).toFixed(2) : '0.00'; + return { + category: cat, + gpa: gpa, + totalCredits: Number(stats.totalCredits) || 0 + }; + }); + + // 2. 成绩等第分布 + const distribution = { + 'A': 0, 'B': 0, 'C': 0, 'D': 0, 'F': 0 + }; + grades.forEach(g => { + if (distribution[g.grade_level] !== undefined) { + distribution[g.grade_level]++; + } + }); + + // 3. 学分进度 + let earnedCredits = 0; + grades.forEach(g => { + const score = parseFloat(g.score || 0); + const credit = parseFloat(g.credit || 0); + if (score >= 60) { + earnedCredits += credit; + } + }); + + // 4. 每学期学分详情 + const semesterCredits = Object.keys(semesterGPA).map(semester => ({ + semester, + credits: semesterGPA[semester].totalCredits + })); + + return { + trend, + distribution, + categories, + credits: { + earned: earnedCredits, + target: 160 // 假设目标学分为 160 + }, + semesterCredits + }; + } } module.exports = StudentService; diff --git a/frontend/public/js/student.js b/frontend/public/js/student.js index 32b924c..d7342ce 100644 --- a/frontend/public/js/student.js +++ b/frontend/public/js/student.js @@ -10,6 +10,7 @@ class StudentManager { init() { this.initDashboard(); this.initMyCourses(); + this.initGradeAnalysis(); this.updateCurrentTime(); setInterval(() => this.updateCurrentTime(), 1000); @@ -18,6 +19,11 @@ class StudentManager { if (refreshBtn) { refreshBtn.addEventListener('click', () => this.initMyCourses()); } + + const refreshTrendBtn = document.getElementById('refreshTrend'); + if (refreshTrendBtn) { + refreshTrendBtn.addEventListener('click', () => this.initGradeAnalysis()); + } } updateCurrentTime() { @@ -57,6 +63,16 @@ class StudentManager { const tbody = document.getElementById('coursesTableBody'); if (!tbody) return; + // 显示加载状态 + tbody.innerHTML = ` +
已获得 0 / 目标 160 学分
+