将前端文件从html目录迁移到views目录,按功能模块组织 重构认证中间件和路由处理,简化页面权限控制 更新静态资源引用路径,统一使用/public前缀 添加学生仪表板页面,优化移动端显示 移除旧版html和js文件,更新样式和脚本
439 lines
17 KiB
JavaScript
439 lines
17 KiB
JavaScript
class StudentManager {
|
||
constructor() {
|
||
// 动æ€<C3A6>设置API基础URL,支æŒ<C3A6>file:///å<><C3A5>议和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) {
|
||
// 未登录,é‡<C3A9>定å<C5A1>‘到登录é¡?
|
||
this.showNotification('请先登录', 'error');
|
||
setTimeout(() => {
|
||
window.location.href = '/login';
|
||
}, 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 || '获å<C2B7>–æˆ<C3A6>绩失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('获å<C2B7>–æˆ<C3A6>绩错误:', error);
|
||
this.showNotification('网络错误,请é‡<C3A9>试', '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>æš‚æ— æˆ<C3A6>绩记录</h3>
|
||
<p>ä½ è¿˜æ²¡æœ‰ä»»ä½•æˆ<C3A6>绩记录</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
const tbody = gradeTable.querySelector('tbody');
|
||
tbody.innerHTML = '';
|
||
|
||
grades.forEach(grade => {
|
||
const row = document.createElement('tr');
|
||
|
||
// æ ¹æ<C2B9>®åˆ†æ•°è®¾ç½®é¢œè‰²
|
||
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">å¹³å<C2B3>‡åˆ?/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">å¹³å<C2B3>‡ç»©ç‚¹</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) {
|
||
// 未登录,é‡<C3A9>定å<C5A1>‘到登录é¡?
|
||
this.showNotification('请先登录', 'error');
|
||
setTimeout(() => {
|
||
window.location.href = '/login';
|
||
}, 1500);
|
||
return;
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
const profile = data.profile;
|
||
|
||
// æ›´æ–°å¦ç”Ÿä»ªè¡¨æ<C2A8>¿é¡¶éƒ¨ä¿¡æ<C2A1>?
|
||
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>å¦å<C2A6>·</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 || '获å<EFBFBD>–个人信æ<EFBFBD>¯å¤±è´¥', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('åŠ è½½ä¸ªäººä¿¡æ<EFBFBD>¯é”™è¯¯:', error);
|
||
this.showNotification('网络错误,请é‡<EFBFBD>试', '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('获å<EFBFBD>–æˆ<EFBFBD>绩详情失败', 'error');
|
||
setTimeout(() => window.history.back(), 1500);
|
||
}
|
||
} catch (error) {
|
||
console.error('获å<EFBFBD>–æˆ<EFBFBD>绩详情错误:', error);
|
||
this.showNotification('网络错误,请é‡<EFBFBD>试', 'error');
|
||
}
|
||
}
|
||
|
||
renderGradeDetails(grade) {
|
||
const container = document.getElementById('gradeDetails');
|
||
if (!container) return;
|
||
|
||
// 计算绩点æ<C2B9><C3A6>è¿°
|
||
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 = 'å<EFBFBD>Šæ ¼';
|
||
else gradeDescription = 'ä¸<EFBFBD>å<EFBFBD>Šæ ?;
|
||
|
||
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> 基本信æ<C2A1>¯</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> å¦ç”Ÿä¿¡æ<C2A1>¯</h3>
|
||
<div class="detail-row">
|
||
<span>å§“å<E2809C><C3A5>ï¼?/span>
|
||
<strong>${grade.full_name}</strong>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span>å¦å<C2A6>·ï¼?/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> 教师信æ<C2A1>¯</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> 打å<E2809C>°æˆ<C3A6>绩å<C2A9>?
|
||
</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);
|
||
|
||
// 销æ¯<C3A6>现有图表实ä¾?
|
||
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: 'å<EFBFBD>„ç§‘æˆ<EFBFBD>绩分布'
|
||
}
|
||
},
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
// åˆ<C3A5>始化å¦ç”Ÿç®¡ç<C2A1>†å™¨
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
if (window.location.pathname.includes('/student/')) {
|
||
window.studentManager = new StudentManager();
|
||
}
|
||
});
|