Files
WebWork/frontend/public/js/student.js
祀梦 16802c85e5 feat(学生): 添加成绩分析功能及密码修改功能
- 新增成绩分析页面,包含GPA趋势图、成绩分布图和学分进度
- 实现学生密码修改功能,包括前端表单和后端验证逻辑
- 添加课程类别分析功能,展示不同类别课程的GPA表现
- 优化学生仪表板和课程页面导航链接
- 增加数据加载状态提示和错误处理
2025-12-22 21:07:21 +08:00

449 lines
18 KiB
JavaScript

/**
* 学生端功能管理
*/
class StudentManager {
constructor() {
this.apiBase = '/api/student';
this.init();
}
init() {
this.initDashboard();
this.initMyCourses();
this.initGradeAnalysis();
this.updateCurrentTime();
setInterval(() => this.updateCurrentTime(), 1000);
// 绑定刷新按钮
const refreshBtn = document.getElementById('refreshCourses');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => this.initMyCourses());
}
const refreshTrendBtn = document.getElementById('refreshTrend');
if (refreshTrendBtn) {
refreshTrendBtn.addEventListener('click', () => this.initGradeAnalysis());
}
}
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('gradesTableBody')) return;
try {
const response = await fetch(`${this.apiBase}/grades`);
const result = await response.json();
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);
}
}
async initMyCourses() {
// 检查是否在我的课程页面
const tbody = document.getElementById('coursesTableBody');
if (!tbody) return;
// 显示加载状态
tbody.innerHTML = `
<tr>
<td colspan="6" class="text-center py-5 text-muted">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
数据加载中...
</td>
</tr>
`;
try {
const response = await fetch(`${this.apiBase}/courses`);
const result = await response.json();
if (result.success) {
this.renderCourses(result.data);
} else {
tbody.innerHTML = `
<tr>
<td colspan="6" class="text-center py-5 text-danger">
<i class="fas fa-exclamation-circle me-2"></i>
${result.message || '获取课程失败'}
</td>
</tr>
`;
if (window.authManager) {
window.authManager.showNotification(result.message || '获取课程失败', 'error');
}
}
} catch (error) {
console.error('Fetch courses data failed:', error);
tbody.innerHTML = `
<tr>
<td colspan="6" class="text-center py-5 text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
网络错误,请稍后重试
</td>
</tr>
`;
}
}
renderCourses(courses) {
const tbody = document.getElementById('coursesTableBody');
if (!tbody) return;
if (!courses || courses.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-4 text-muted">暂无课程记录</td></tr>';
return;
}
tbody.innerHTML = courses.map(course => `
<tr>
<td>${course.course_code}</td>
<td>${course.course_name}</td>
<td>${course.credit}</td>
<td>${course.teacher_name}</td>
<td>${course.semester || '2023-2024 下学期'}</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="studentManager.viewCourseDetails(${course.id})">
<i class="fas fa-info-circle"></i> 课程详情
</button>
</td>
</tr>
`).join('');
}
async viewCourseDetails(courseId) {
try {
const response = await fetch(`${this.apiBase}/courses/${courseId}`);
const result = await response.json();
if (result.success) {
const course = result.data;
this.updateElement('modalCourseName', course.course_name);
this.updateElement('modalCourseCode', course.course_code);
this.updateElement('modalCourseCredit', course.credit);
this.updateElement('modalTeacherName', course.teacher_name);
this.updateElement('modalClassName', course.class_name);
this.updateElement('modalSemester', course.semester || '2023-2024 下学期');
this.updateElement('modalTeacherEmail', course.teacher_email || '暂无');
const modalEl = document.getElementById('courseDetailsModal');
if (modalEl) {
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
} else {
console.error('Course details modal element not found');
if (window.authManager) {
window.authManager.showNotification('无法打开课程详情:模态框组件缺失', 'error');
}
}
} else {
if (window.authManager) {
window.authManager.showNotification(result.message || '获取课程详情失败', 'error');
}
}
} catch (error) {
console.error('Fetch course details failed:', error);
if (window.authManager) {
window.authManager.showNotification('获取课程详情失败:网络错误', '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 = '<tr><td colspan="8" class="text-center py-4 text-muted">暂无成绩记录</td></tr>';
return;
}
tbody.innerHTML = grades.map(grade => `
<tr>
<td>${grade.course_name}</td>
<td>${grade.course_code || '-'}</td>
<td>${grade.credit}</td>
<td>${grade.score}</td>
<td>${grade.grade_level || '-'}</td>
<td>${grade.grade_point || '-'}</td>
<td>
<span class="badge ${this.getScoreBadgeClass(grade.score)}">
${this.getScoreText(grade.score)}
</span>
</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="studentManager.viewDetails(${grade.id})">
<i class="fas fa-eye"></i> 详情
</button>
</td>
</tr>
`).join('');
}
}
updateElement(id, value) {
const el = document.getElementById(id);
if (el) el.textContent = value;
}
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';
}
getScoreText(score) {
const s = parseFloat(score);
if (s >= 60) return '及格';
return '不及格';
}
async viewDetails(id) {
try {
const response = await fetch(`${this.apiBase}/grades/${id}`);
const result = await response.json();
if (result.success) {
const grade = result.data.grade;
this.updateElement('modalGradeCourseName', grade.course_name);
this.updateElement('modalGradeCourseCode', grade.course_code);
this.updateElement('modalTotalScore', grade.score);
this.updateElement('modalUsualScore', grade.usual_score || '-');
this.updateElement('modalMidtermScore', grade.midterm_score || '-');
this.updateElement('modalFinalScore', grade.final_score || '-');
this.updateElement('modalGradeCredit', grade.credit);
this.updateElement('modalGradeLevel', grade.grade_level || '-');
this.updateElement('modalGradePoint', grade.grade_point || '-');
this.updateElement('modalGradeTeacher', grade.teacher_name);
this.updateElement('modalGradeTime', new Date(grade.created_at).toLocaleString());
const remarkContainer = document.getElementById('modalRemarkContainer');
if (grade.remark) {
remarkContainer.style.display = 'block';
this.updateElement('modalRemark', grade.remark);
} else {
remarkContainer.style.display = 'none';
}
const modalEl = document.getElementById('gradeDetailsModal');
if (modalEl) {
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
} else {
console.error('Grade details modal element not found');
if (window.authManager) {
window.authManager.showNotification('无法打开成绩详情:模态框组件缺失', 'error');
}
}
} else {
if (window.authManager) {
window.authManager.showNotification(result.message || '获取成绩详情失败', 'error');
}
}
} catch (error) {
console.error('Fetch grade details failed:', error);
if (window.authManager) {
window.authManager.showNotification('获取成绩详情失败:网络错误', 'error');
}
}
}
async initGradeAnalysis() {
if (!document.getElementById('gpaTrendChart')) return;
try {
const response = await fetch(`${this.apiBase}/statistics`);
const result = await response.json();
if (result.success) {
this.renderAnalysisCharts(result.data);
this.renderCreditsInfo(result.data.credits, result.data.semesterCredits);
} else {
if (window.authManager) {
window.authManager.showNotification(result.message || '获取统计数据失败', 'error');
}
}
} catch (error) {
console.error('Fetch statistics failed:', error);
}
}
renderAnalysisCharts(data) {
try {
// 1. GPA 趋势图
const ctxGPA = document.getElementById('gpaTrendChart');
if (ctxGPA) {
const ctx = ctxGPA.getContext('2d');
if (this.gpaChart) this.gpaChart.destroy();
this.gpaChart = new Chart(ctx, {
type: 'line',
data: {
labels: (data.trend || []).map(t => t.semester),
datasets: [{
label: '学期 GPA',
data: (data.trend || []).map(t => t.gpa),
borderColor: '#4e73df',
backgroundColor: 'rgba(78, 115, 223, 0.05)',
tension: 0.3,
fill: true
}]
},
options: {
maintainAspectRatio: false,
scales: {
y: { beginAtZero: false, min: 0, max: 4.0 }
}
}
});
}
// 2. 成绩分布饼图
const ctxDist = document.getElementById('gradeDistributionChart');
if (ctxDist) {
const ctx = ctxDist.getContext('2d');
if (this.distChart) this.distChart.destroy();
this.distChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['A (优秀)', 'B (良好)', 'C (中等)', 'D (及格)', 'F (不及格)'],
datasets: [{
data: [
data.distribution.A || 0,
data.distribution.B || 0,
data.distribution.C || 0,
data.distribution.D || 0,
data.distribution.F || 0
],
backgroundColor: ['#1cc88a', '#36b9cc', '#f6c23e', '#fd7e14', '#e74a3b']
}]
},
options: {
maintainAspectRatio: false,
plugins: { legend: { position: 'bottom' } }
}
});
}
} catch (error) {
console.error('Render charts failed:', error);
}
// 3. 课程类别表现 (使用进度条列表展示)
this.renderCategoryPerformance(data.categories);
}
renderCategoryPerformance(categories) {
const container = document.getElementById('categoryPerformanceList');
if (!container || !categories) return;
container.innerHTML = categories.map(cat => {
const gpaValue = parseFloat(cat.gpa);
const percentage = (gpaValue / 4.0) * 100;
const totalCredits = parseFloat(cat.totalCredits || 0);
// 根据绩点选择颜色
let bgColor = 'bg-primary';
if (gpaValue >= 3.5) bgColor = 'bg-success';
else if (gpaValue < 2.0) bgColor = 'bg-danger';
else if (gpaValue < 3.0) bgColor = 'bg-warning';
return `
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-1">
<div>
<span class="fw-bold">${cat.category}</span>
<span class="text-muted small ms-2">(${totalCredits.toFixed(1)} 学分)</span>
</div>
<span class="fw-bold text-primary">${cat.gpa} GPA</span>
</div>
<div class="progress" style="height: 10px;">
<div class="progress-bar ${bgColor}" role="progressbar"
style="width: ${percentage}%"
aria-valuenow="${percentage}" aria-valuemin="0" aria-valuemax="100">
</div>
</div>
</div>
`;
}).join('');
}
renderCreditsInfo(credits, semesterCredits) {
// 总进度
const percent = Math.min(100, Math.round((credits.earned / credits.target) * 100));
const progressBar = document.getElementById('creditProgressBar');
const percentText = document.getElementById('creditPercent');
if (progressBar && percentText) {
progressBar.style.width = `${percent}%`;
percentText.textContent = `${percent}%`;
// 更新数值
this.updateElement('earnedCredits', credits.earned);
this.updateElement('targetCredits', credits.target);
}
// 学期学分列表
const listContainer = document.getElementById('semesterCreditsList');
if (listContainer) {
listContainer.innerHTML = semesterCredits.map(item => `
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-secondary small">${item.semester}</span>
<span class="fw-bold">${item.credits} 学分</span>
</div>
`).join('');
}
}
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
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 || '未分配';
}
});
});