feat(学生): 添加成绩分析功能及密码修改功能
- 新增成绩分析页面,包含GPA趋势图、成绩分布图和学分进度 - 实现学生密码修改功能,包括前端表单和后端验证逻辑 - 添加课程类别分析功能,展示不同类别课程的GPA表现 - 优化学生仪表板和课程页面导航链接 - 增加数据加载状态提示和错误处理
This commit is contained in:
@@ -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('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user