feat(学生): 添加成绩分析功能及密码修改功能

- 新增成绩分析页面,包含GPA趋势图、成绩分布图和学分进度
- 实现学生密码修改功能,包括前端表单和后端验证逻辑
- 添加课程类别分析功能,展示不同类别课程的GPA表现
- 优化学生仪表板和课程页面导航链接
- 增加数据加载状态提示和错误处理
This commit is contained in:
祀梦
2025-12-22 21:07:21 +08:00
parent ac6029dac8
commit 16802c85e5
13 changed files with 819 additions and 14 deletions

View File

@@ -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 = `
<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();
@@ -64,12 +80,28 @@ class StudentManager {
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>
`;
}
}
@@ -113,8 +145,16 @@ class StudentManager {
this.updateElement('modalSemester', course.semester || '2023-2024 下学期');
this.updateElement('modalTeacherEmail', course.teacher_email || '暂无');
const modal = new bootstrap.Modal(document.getElementById('courseDetailsModal'));
modal.show();
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');
@@ -122,6 +162,9 @@ class StudentManager {
}
} catch (error) {
console.error('Fetch course details failed:', error);
if (window.authManager) {
window.authManager.showNotification('获取课程详情失败:网络错误', 'error');
}
}
}
@@ -214,8 +257,16 @@ class StudentManager {
remarkContainer.style.display = 'none';
}
const modal = new bootstrap.Modal(document.getElementById('gradeDetailsModal'));
modal.show();
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');
@@ -223,6 +274,154 @@ class StudentManager {
}
} 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('');
}
}
}