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,6 +1,7 @@
/**
* 认证模块管理器
* 处理登录、注册、注销及权限检查
* 适配 Bootstrap 5 样式
*/
class AuthManager {
constructor() {
@@ -15,21 +16,14 @@ class AuthManager {
}
/**
* 创建通知容器
* 创建通知容器 (适配 Bootstrap Toast)
*/
createNotificationContainer() {
if (!document.getElementById('notification-container')) {
const container = document.createElement('div');
container.id = 'notification-container';
container.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 10px;
`;
container.className = 'toast-container position-fixed top-0 end-0 p-3';
container.style.zIndex = '9999';
document.body.appendChild(container);
}
}
@@ -38,7 +32,6 @@ class AuthManager {
* 检查用户认证状态
*/
async checkAuthStatus() {
// 如果当前是公共页面可以选择不检查或者检查后更新UI
const currentPath = window.location.pathname;
const isAuthPage = currentPath.includes('/login') || currentPath.includes('/register');
@@ -46,20 +39,11 @@ class AuthManager {
const response = await fetch(`${this.apiBase}/auth/me`);
const data = await response.json();
if (data.success && data.user) {
// 用户已登录
const redirectUrl = this.getDashboardUrl(data.user.role);
// 如果在登录/注册页,跳转到仪表板
if (data.success && data.data && data.data.user) {
const redirectUrl = this.getDashboardUrl(data.data.user.role);
if (isAuthPage || currentPath === '/') {
window.location.href = redirectUrl;
}
} else {
// 用户未登录,如果在受保护页面,跳转到登录页
// 注意:后端通常已经处理了重定向,这里是前端的额外保障
if (!isAuthPage && currentPath !== '/') {
// 可以在这里添加逻辑,但通常交给后端控制
}
}
} catch (error) {
console.error('Auth check failed:', error);
@@ -94,7 +78,7 @@ class AuthManager {
}
}
// 注销按钮 (可能有多个,例如在导航栏)
// 注销按钮
document.querySelectorAll('.btn-logout, #logoutBtn').forEach(btn => {
btn.addEventListener('click', (e) => this.handleLogout(e));
});
@@ -107,12 +91,12 @@ class AuthManager {
if (classField && classInput) {
if (role === 'student' || role === 'teacher') {
classField.style.display = 'block';
classField.style.display = 'flex'; // 配合 Bootstrap input-group
classInput.required = true;
} else {
classField.style.display = 'none';
classInput.required = false;
classInput.value = ''; // 清空值
classInput.value = '';
}
}
}
@@ -120,9 +104,9 @@ class AuthManager {
async handleLogin(e) {
e.preventDefault();
const form = e.target;
const submitBtn = form.querySelector('button[type="submit"]');
const submitBtn = document.getElementById('loginBtn') || form.querySelector('button[type="submit"]');
if (this.setLoading(submitBtn, true, '登录中...')) {
if (this.setLoading(submitBtn, true)) {
try {
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
@@ -137,16 +121,27 @@ class AuthManager {
if (result.success) {
this.showNotification('登录成功,正在跳转...', 'success');
setTimeout(() => {
window.location.href = this.getDashboardUrl(result.user.role);
}, 1000);
// 获取用户信息,优先从 result.data.user 获取,兼容旧格式 result.data 或 result.user
const user = (result.data && result.data.user) || result.data || result.user;
const role = user ? user.role : null;
if (role) {
setTimeout(() => {
window.location.href = this.getDashboardUrl(role);
}, 1000);
} else {
console.error('Login success but role not found:', result);
this.showNotification('登录状态异常,请重试', 'error');
this.setLoading(submitBtn, false);
}
} else {
this.showNotification(result.message || '登录失败', 'error');
this.setLoading(submitBtn, false);
}
} catch (error) {
console.error('Login error:', error);
this.showNotification('网络错误,请稍后重试', 'error');
this.showNotification('服务器连接失败,请稍后重试', 'error');
this.setLoading(submitBtn, false);
}
}
@@ -155,19 +150,17 @@ class AuthManager {
async handleRegister(e) {
e.preventDefault();
const form = e.target;
const submitBtn = form.querySelector('button[type="submit"]');
const submitBtn = document.getElementById('registerBtn') || form.querySelector('button[type="submit"]');
// 获取数据
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// 简单验证
if (data.password !== data.confirmPassword) {
this.showNotification('两次输入的密码不一致', 'error');
return;
}
if (this.setLoading(submitBtn, true, '注册中...')) {
if (this.setLoading(submitBtn, true)) {
try {
const response = await fetch(`${this.apiBase}/auth/register`, {
method: 'POST',
@@ -188,7 +181,7 @@ class AuthManager {
}
} catch (error) {
console.error('Register error:', error);
this.showNotification('网络错误,请稍后重试', 'error');
this.showNotification('服务器连接失败,请稍后重试', 'error');
this.setLoading(submitBtn, false);
}
}
@@ -196,24 +189,17 @@ class AuthManager {
async handleLogout(e) {
e.preventDefault();
if (confirm('确定要退出登录吗?')) {
try {
const response = await fetch(`${this.apiBase}/auth/logout`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
this.showNotification('已退出登录', 'success');
setTimeout(() => {
window.location.href = '/login';
}, 1000);
window.location.href = '/login';
}
} catch (error) {
console.error('Logout error:', error);
// 即使出错也强制跳转到登录页
window.location.href = '/login';
}
}
@@ -221,67 +207,68 @@ class AuthManager {
/**
* 设置按钮加载状态
* @param {HTMLElement} btn 按钮元素
* @param {boolean} isLoading 是否正在加载
* @param {string} text 加载时的文本
* @returns {boolean} true表示状态设置成功
*/
setLoading(btn, isLoading, text = '') {
setLoading(btn, isLoading) {
if (!btn) return false;
const spinner = btn.querySelector('.spinner-border');
if (isLoading) {
if (btn.dataset.loading) return false; // 防止重复提交
btn.dataset.loading = 'true';
btn.dataset.originalText = btn.innerHTML;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${text}`;
if (btn.disabled) return false;
btn.disabled = true;
if (spinner) spinner.classList.remove('d-none');
} else {
btn.innerHTML = btn.dataset.originalText || btn.innerHTML;
delete btn.dataset.loading;
btn.disabled = false;
if (spinner) spinner.classList.add('d-none');
}
return true;
}
/**
* 显示通知
* @param {string} message 消息内容
* @param {string} type 消息类型 'success' | 'error' | 'info'
* 显示通知 (Bootstrap 5 Toast)
*/
showNotification(message, type = 'info') {
const container = document.getElementById('notification-container');
if (!container) return;
const notification = document.createElement('div');
notification.className = `notification ${type}`;
const iconMap = {
success: 'fa-check-circle',
error: 'fa-exclamation-circle',
warning: 'fa-exclamation-triangle',
info: 'fa-info-circle'
};
let icon = 'info-circle';
if (type === 'success') icon = 'check-circle';
if (type === 'error') icon = 'exclamation-circle';
notification.innerHTML = `
<i class="fas fa-${icon}"></i>
<span class="notification-content">${message}</span>
const bgMap = {
success: 'bg-success',
error: 'bg-danger',
warning: 'bg-warning',
info: 'bg-info'
};
const toastId = 'toast-' + Date.now();
const toastHtml = `
<div id="${toastId}" class="toast align-items-center text-white ${bgMap[type]} border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
<i class="fas ${iconMap[type]} me-2"></i>
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', toastHtml);
const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement, { delay: 3000 });
toast.show();
container.appendChild(notification);
// 动画显示
requestAnimationFrame(() => {
notification.classList.add('show');
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
// 自动消失
setTimeout(() => {
notification.classList.remove('show');
notification.addEventListener('transitionend', () => {
notification.remove();
});
}, 3000);
}
}
// 初始化
// 初始化认证管理器
document.addEventListener('DOMContentLoaded', () => {
window.authManager = new AuthManager();
});