Files
WebWork/frontend/public/js/student.js
祀梦 bcf2c71fad refactor(frontend): 重构前端目录结构并优化认证流程
将前端文件从html目录迁移到views目录,按功能模块组织
重构认证中间件和路由处理,简化页面权限控制
更新静态资源引用路径,统一使用/public前缀
添加学生仪表板页面,优化移动端显示
移除旧版html和js文件,更新样式和脚本
2025-12-21 22:07:23 +08:00

439 lines
17 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
});