feat: 添加学生个人中心页面和数据库备份功能

refactor(auth): 重构认证模块适配Bootstrap 5样式
feat(controller): 在登录响应中返回用户对象
feat(server): 添加学生个人中心路由
refactor(models): 重构学生和成绩模型结构
style: 更新登录和注册页面UI设计
chore: 添加数据库备份脚本和空备份文件
This commit is contained in:
祀梦
2025-12-21 22:34:29 +08:00
parent b9a975004b
commit e5a2a9d042
16 changed files with 1834 additions and 1651 deletions

View File

@@ -1,438 +1,135 @@
/**
* 学生端功能管理
*/
class StudentManager {
constructor() {
// 动态设置API基础URL支持file:///协议和localhost:3000访问
this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api';
this.apiBase = '/api/student';
this.init();
}
init() {
this.initDashboard();
this.initGradeDetails();
this.loadProfile();
this.updateCurrentTime();
setInterval(() => this.updateCurrentTime(), 1000);
}
updateCurrentTime() {
const timeElement = document.getElementById('currentTime');
if (timeElement) {
const now = new Date();
const options = {
year: 'numeric', month: 'long', day: 'numeric',
hour: '2-digit', minute: '2-digit', second: '2-digit'
};
timeElement.textContent = now.toLocaleString('zh-CN', options);
}
}
async initDashboard() {
const gradeList = document.getElementById('gradeList');
const statisticsElement = document.getElementById('statistics');
if (!gradeList) return;
// 检查是否在仪表板页面
if (!document.getElementById('gradesTableBody')) return;
try {
const response = await fetch(`${this.apiBase}/student/grades`, {
credentials: 'include'
});
const response = await fetch(`${this.apiBase}/grades`);
const result = await response.json();
if (response.status === 401) {
// 未登录重定向到登录<E799BB><E5BD95>?
this.showNotification('请先登录', 'error');
setTimeout(() => {
window.location.href = '/login';
}, 1500);
if (result.success) {
this.renderDashboard(result.data);
} else {
if (window.authManager) {
window.authManager.showNotification(result.message || '获取数据失败', 'error');
}
}
} catch (error) {
console.error('Fetch dashboard data failed:', error);
}
}
renderDashboard(data) {
const { grades, statistics } = data;
// 渲染统计数据
if (statistics) {
this.updateElement('gpaValue', statistics.gpa);
this.updateElement('courseCount', statistics.totalCourses);
this.updateElement('creditTotal', statistics.totalCredits);
// 班级排名如果后端没提供,可以显示为 '-' 或固定值
this.updateElement('classRank', statistics.classRank || '-');
}
// 渲染成绩表格
const tbody = document.getElementById('gradesTableBody');
if (tbody) {
if (!grades || grades.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-4 text-muted">暂无成绩记录</td></tr>';
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');
tbody.innerHTML = grades.map(grade => `
<tr>
<td>${grade.course_name}</td>
<td>${grade.course_code || '-'}</td>
<td>${grade.credit}</td>
<td>${grade.score}</td>
<td>${grade.grade_level || '-'}</td>
<td>${grade.grade_point || '-'}</td>
<td>
<span class="badge ${this.getScoreBadgeClass(grade.score)}">
${this.getScoreText(grade.score)}
</span>
</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="studentManager.viewDetails(${grade.id})">
<i class="fas fa-eye"></i> 详情
</button>
</td>
</tr>
`).join('');
}
}
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);
});
updateElement(id, value) {
const el = document.getElementById(id);
if (el) el.textContent = value;
}
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">总学<E680BB><E5ADA6>?/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">平均<E5B9B3><E59D87>?/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>
`;
getScoreBadgeClass(score) {
const s = parseFloat(score);
if (s >= 90) return 'bg-success';
if (s >= 80) return 'bg-info';
if (s >= 60) return 'bg-warning';
return 'bg-danger';
}
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) {
// 未登录重定向到登录<E799BB><E5BD95>?
this.showNotification('请先登录', 'error');
setTimeout(() => {
window.location.href = '/login';
}, 1500);
return;
}
const data = await response.json();
if (data.success) {
const profile = data.profile;
// 更新学生仪表板顶部信<E983A8><E4BFA1>?
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 || '未设<E69CAA><E8AEBE>?;
}
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 || '未设<EFBFBD><EFBFBD>?}</p>
</div>
</div>
<div class="detail-item">
<i class="fas fa-calendar-alt"></i>
<div>
<h4>入学年份</h4>
<p>${profile.enrollment_year || '未设<E69CAA><E8AEBE>?}</p>
</div>
</div>
</div>
`;
} else {
// API返回失败
this.showNotification(data.message || '获取个人信息失败', 'error');
}
} catch (error) {
console.error('加载个人信息错误:', error);
this.showNotification('网络错误请重试', 'error');
}
getScoreText(score) {
const s = parseFloat(score);
if (s >= 60) return '及格';
return '不及格';
}
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 = '不及<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} <20><>?
<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>学分<E5ADA6><E58886>?/span>
<strong>${grade.credit}</strong>
</div>
<div class="detail-row">
<span>学期<E5ADA6><E69C9F>?/span>
<strong>${grade.semester}</strong>
</div>
<div class="detail-row">
<span>考试日期<E697A5><E69C9F>?/span>
<strong>${new Date(grade.exam_date).toLocaleDateString()}</strong>
</div>
<div class="detail-row">
<span>等级<E7AD89><E7BAA7>?/span>
<strong class="grade-level-${grade.grade_level}">${grade.grade_level || '-'}</strong>
</div>
<div class="detail-row">
<span>绩点<E7BBA9><E782B9>?/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>姓名<E5A793><E5908D>?/span>
<strong>${grade.full_name}</strong>
</div>
<div class="detail-row">
<span>学号<E5ADA6><E58FB7>?/span>
<strong>${grade.student_number}</strong>
</div>
<div class="detail-row">
<span>班级<E78FAD><E7BAA7>?/span>
<strong>${grade.class_name}</strong>
</div>
<div class="detail-row">
<span>专业<E4B893><E4B89A>?/span>
<strong>${grade.major || '未设<E69CAA><E8AEBE>?}</strong>
</div>
</div>
<div class="detail-section">
<h3><i class="fas fa-chalkboard-teacher"></i> 教师信息</h3>
<div class="detail-row">
<span>任课教师<E69599><E5B888>?/span>
<strong>${grade.teacher_name}</strong>
</div>
<div class="detail-row">
<span>教师邮箱<E982AE><E7AEB1>?/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> 打印成绩<E68890><E7BBA9>?
</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延迟加<E8BF9F><E58AA0>?
this.loadChartLibrary().then(() => this.updateChart(grades));
return;
}
const courseNames = grades.map(g => g.course_name);
const scores = grades.map(g => g.score);
// 销毁现有图表实<E8A1A8><E5AE9E>?
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的通知系统或自己实<E5B7B1><E5AE9E>?
if (window.authManager && window.authManager.showNotification) {
window.authManager.showNotification(message, type);
} else {
alert(message);
}
viewDetails(id) {
// 实现查看详情逻辑,或者跳转到详情页
console.log('View details for score:', id);
}
}
// 初始化学生管理器
// 初始化
document.addEventListener('DOMContentLoaded', () => {
if (window.location.pathname.includes('/student/')) {
window.studentManager = new StudentManager();
}
});
window.studentManager = new StudentManager();
// 从 Session 获取用户信息并更新 UI
fetch('/api/auth/me')
.then(res => res.json())
.then(result => {
if (result.success && result.data.user) {
const user = result.data.user;
const nameEl = document.getElementById('userName');
const studentNameEl = document.getElementById('studentName');
const studentClassEl = document.getElementById('studentClass');
if (nameEl) nameEl.textContent = user.name;
if (studentNameEl) studentNameEl.textContent = user.name;
if (studentClassEl) studentClassEl.textContent = user.class || '未分配';
}
});
});