438 lines
17 KiB
JavaScript
438 lines
17 KiB
JavaScript
class StudentManager {
|
||
constructor() {
|
||
// 动态设置API基础URL,支持file:///协议和localhost:3000访问
|
||
this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api';
|
||
this.initDashboard();
|
||
this.initGradeDetails();
|
||
this.loadProfile();
|
||
}
|
||
|
||
async initDashboard() {
|
||
const gradeList = document.getElementById('gradeList');
|
||
const statisticsElement = document.getElementById('statistics');
|
||
|
||
if (!gradeList) return;
|
||
|
||
try {
|
||
const response = await fetch(`${this.apiBase}/student/grades`, {
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (response.status === 401) {
|
||
// 未登录,重定向到登录页
|
||
this.showNotification('请先登录', 'error');
|
||
setTimeout(() => {
|
||
window.location.href = '/html/login.html';
|
||
}, 1500);
|
||
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');
|
||
}
|
||
}
|
||
|
||
renderGrades(grades) {
|
||
const gradeList = document.getElementById('gradeList');
|
||
const gradeTable = document.getElementById('gradeTable');
|
||
|
||
if (!gradeTable) return;
|
||
|
||
if (grades.length === 0) {
|
||
gradeList.innerHTML = `
|
||
<div class="empty-state">
|
||
<i class="fas fa-clipboard-list fa-3x"></i>
|
||
<h3>暂无成绩记录</h3>
|
||
<p>你还没有任何成绩记录</p>
|
||
</div>
|
||
`;
|
||
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 = `
|
||
<td>${grade.course_code}</td>
|
||
<td>${grade.course_name}</td>
|
||
<td>${grade.credit}</td>
|
||
<td class="${scoreClass}">
|
||
<span class="grade-badge">${grade.score}</span>
|
||
</td>
|
||
<td>${grade.grade_level || '-'}</td>
|
||
<td>${grade.grade_point || '-'}</td>
|
||
<td>${grade.teacher_name}</td>
|
||
<td>${new Date(grade.exam_date).toLocaleDateString()}</td>
|
||
<td>
|
||
<a href="/html/student/details.html?id=${grade.id}"
|
||
class="btn btn-sm btn-secondary">
|
||
<i class="fas fa-eye"></i> 查看
|
||
</a>
|
||
</td>
|
||
`;
|
||
|
||
tbody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
renderStatistics(statistics) {
|
||
const element = document.getElementById('statistics');
|
||
if (!element) return;
|
||
|
||
element.innerHTML = `
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-icon student">
|
||
<i class="fas fa-book"></i>
|
||
</div>
|
||
<div class="stat-value">${statistics.totalCourses}</div>
|
||
<div class="stat-label">总课程数</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon course">
|
||
<i class="fas fa-star"></i>
|
||
</div>
|
||
<div class="stat-value">${statistics.totalCredits}</div>
|
||
<div class="stat-label">总学分</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon grade">
|
||
<i class="fas fa-chart-line"></i>
|
||
</div>
|
||
<div class="stat-value">${statistics.averageScore}</div>
|
||
<div class="stat-label">平均分</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-icon teacher">
|
||
<i class="fas fa-graduation-cap"></i>
|
||
</div>
|
||
<div class="stat-value">${statistics.gpa}</div>
|
||
<div class="stat-label">平均绩点</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
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 = '/html/login.html';
|
||
}, 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 = `
|
||
<div class="profile-header">
|
||
<div class="profile-avatar">
|
||
<i class="fas fa-user-graduate"></i>
|
||
</div>
|
||
<div class="profile-info">
|
||
<h2>${profile.full_name}</h2>
|
||
<p class="profile-role">
|
||
<i class="fas fa-user-tag"></i> 学生
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="profile-details">
|
||
<div class="detail-item">
|
||
<i class="fas fa-id-card"></i>
|
||
<div>
|
||
<h4>学号</h4>
|
||
<p>${profile.student_id}</p>
|
||
</div>
|
||
</div>
|
||
<div class="detail-item">
|
||
<i class="fas fa-users"></i>
|
||
<div>
|
||
<h4>班级</h4>
|
||
<p>${profile.class_name}</p>
|
||
</div>
|
||
</div>
|
||
<div class="detail-item">
|
||
<i class="fas fa-book"></i>
|
||
<div>
|
||
<h4>专业</h4>
|
||
<p>${profile.major || '未设置'}</p>
|
||
</div>
|
||
</div>
|
||
<div class="detail-item">
|
||
<i class="fas fa-calendar-alt"></i>
|
||
<div>
|
||
<h4>入学年份</h4>
|
||
<p>${profile.enrollment_year || '未设置'}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
// API返回失败
|
||
this.showNotification(data.message || '获取个人信息失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('加载个人信息错误:', error);
|
||
this.showNotification('网络错误,请重试', 'error');
|
||
}
|
||
}
|
||
|
||
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 = `
|
||
<div class="grade-detail-card">
|
||
<div class="grade-header">
|
||
<h2>${grade.course_name} (${grade.course_code})</h2>
|
||
<div class="grade-score ${grade.score >= 60 ? 'score-pass' : 'score-fail'}">
|
||
${grade.score} 分
|
||
<span class="grade-description">${gradeDescription}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grade-details-grid">
|
||
<div class="detail-section">
|
||
<h3><i class="fas fa-info-circle"></i> 基本信息</h3>
|
||
<div class="detail-row">
|
||
<span>学分:</span>
|
||
<strong>${grade.credit}</strong>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span>学期:</span>
|
||
<strong>${grade.semester}</strong>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span>考试日期:</span>
|
||
<strong>${new Date(grade.exam_date).toLocaleDateString()}</strong>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span>等级:</span>
|
||
<strong class="grade-level-${grade.grade_level}">${grade.grade_level || '-'}</strong>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span>绩点:</span>
|
||
<strong>${grade.grade_point || '-'}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h3><i class="fas fa-user-graduate"></i> 学生信息</h3>
|
||
<div class="detail-row">
|
||
<span>姓名:</span>
|
||
<strong>${grade.full_name}</strong>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span>学号:</span>
|
||
<strong>${grade.student_number}</strong>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span>班级:</span>
|
||
<strong>${grade.class_name}</strong>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span>专业:</span>
|
||
<strong>${grade.major || '未设置'}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h3><i class="fas fa-chalkboard-teacher"></i> 教师信息</h3>
|
||
<div class="detail-row">
|
||
<span>任课教师:</span>
|
||
<strong>${grade.teacher_name}</strong>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span>教师邮箱:</span>
|
||
<strong>${grade.teacher_email}</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
${grade.remark ? `
|
||
<div class="remark-section">
|
||
<h3><i class="fas fa-comment"></i> 备注</h3>
|
||
<p>${grade.remark}</p>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div class="grade-actions">
|
||
<button onclick="window.print()" class="btn btn-secondary">
|
||
<i class="fas fa-print"></i> 打印成绩单
|
||
</button>
|
||
<button onclick="window.history.back()" class="btn btn-primary">
|
||
<i class="fas fa-arrow-left"></i> 返回
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 初始化学生管理器
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
if (window.location.pathname.includes('/student/')) {
|
||
window.studentManager = new StudentManager();
|
||
}
|
||
}); |