将前端文件从html目录迁移到views目录,按功能模块组织 重构认证中间件和路由处理,简化页面权限控制 更新静态资源引用路径,统一使用/public前缀 添加学生仪表板页面,优化移动端显示 移除旧版html和js文件,更新样式和脚本
287 lines
9.9 KiB
JavaScript
287 lines
9.9 KiB
JavaScript
/**
|
||
* 认证模块管理器
|
||
* 处理登录、注册、注销及权限检查
|
||
*/
|
||
class AuthManager {
|
||
constructor() {
|
||
this.apiBase = '/api';
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
this.createNotificationContainer();
|
||
this.initEventListeners();
|
||
this.checkAuthStatus();
|
||
}
|
||
|
||
/**
|
||
* 创建通知容器
|
||
*/
|
||
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;
|
||
`;
|
||
document.body.appendChild(container);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查用户认证状态
|
||
*/
|
||
async checkAuthStatus() {
|
||
// 如果当前是公共页面,可以选择不检查,或者检查后更新UI
|
||
const currentPath = window.location.pathname;
|
||
const isAuthPage = currentPath.includes('/login') || currentPath.includes('/register');
|
||
|
||
try {
|
||
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 (isAuthPage || currentPath === '/') {
|
||
window.location.href = redirectUrl;
|
||
}
|
||
} else {
|
||
// 用户未登录,如果在受保护页面,跳转到登录页
|
||
// 注意:后端通常已经处理了重定向,这里是前端的额外保障
|
||
if (!isAuthPage && currentPath !== '/') {
|
||
// 可以在这里添加逻辑,但通常交给后端控制
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Auth check failed:', error);
|
||
}
|
||
}
|
||
|
||
getDashboardUrl(role) {
|
||
switch(role) {
|
||
case 'student': return '/student/dashboard';
|
||
case 'teacher': return '/teacher/dashboard';
|
||
case 'admin': return '/admin/dashboard';
|
||
default: return '/dashboard';
|
||
}
|
||
}
|
||
|
||
initEventListeners() {
|
||
// 登录表单
|
||
const loginForm = document.getElementById('loginForm');
|
||
if (loginForm) {
|
||
loginForm.addEventListener('submit', (e) => this.handleLogin(e));
|
||
}
|
||
|
||
// 注册表单
|
||
const registerForm = document.getElementById('registerForm');
|
||
if (registerForm) {
|
||
registerForm.addEventListener('submit', (e) => this.handleRegister(e));
|
||
|
||
// 角色选择联动
|
||
const roleSelect = document.getElementById('role');
|
||
if (roleSelect) {
|
||
roleSelect.addEventListener('change', (e) => this.handleRoleChange(e));
|
||
}
|
||
}
|
||
|
||
// 注销按钮 (可能有多个,例如在导航栏)
|
||
document.querySelectorAll('.btn-logout, #logoutBtn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => this.handleLogout(e));
|
||
});
|
||
}
|
||
|
||
handleRoleChange(e) {
|
||
const role = e.target.value;
|
||
const classField = document.getElementById('classField');
|
||
const classInput = document.getElementById('class');
|
||
|
||
if (classField && classInput) {
|
||
if (role === 'student' || role === 'teacher') {
|
||
classField.style.display = 'block';
|
||
classInput.required = true;
|
||
} else {
|
||
classField.style.display = 'none';
|
||
classInput.required = false;
|
||
classInput.value = ''; // 清空值
|
||
}
|
||
}
|
||
}
|
||
|
||
async handleLogin(e) {
|
||
e.preventDefault();
|
||
const form = e.target;
|
||
const submitBtn = form.querySelector('button[type="submit"]');
|
||
|
||
if (this.setLoading(submitBtn, true, '登录中...')) {
|
||
try {
|
||
const formData = new FormData(form);
|
||
const data = Object.fromEntries(formData.entries());
|
||
|
||
const response = await fetch(`${this.apiBase}/auth/login`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
this.showNotification('登录成功,正在跳转...', 'success');
|
||
setTimeout(() => {
|
||
window.location.href = this.getDashboardUrl(result.user.role);
|
||
}, 1000);
|
||
} else {
|
||
this.showNotification(result.message || '登录失败', 'error');
|
||
this.setLoading(submitBtn, false);
|
||
}
|
||
} catch (error) {
|
||
console.error('Login error:', error);
|
||
this.showNotification('网络错误,请稍后重试', 'error');
|
||
this.setLoading(submitBtn, false);
|
||
}
|
||
}
|
||
}
|
||
|
||
async handleRegister(e) {
|
||
e.preventDefault();
|
||
const form = e.target;
|
||
const submitBtn = 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, '注册中...')) {
|
||
try {
|
||
const response = await fetch(`${this.apiBase}/auth/register`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
this.showNotification('注册成功,请登录', 'success');
|
||
setTimeout(() => {
|
||
window.location.href = '/login';
|
||
}, 1500);
|
||
} else {
|
||
this.showNotification(result.message || '注册失败', 'error');
|
||
this.setLoading(submitBtn, false);
|
||
}
|
||
} catch (error) {
|
||
console.error('Register error:', error);
|
||
this.showNotification('网络错误,请稍后重试', 'error');
|
||
this.setLoading(submitBtn, false);
|
||
}
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
} catch (error) {
|
||
console.error('Logout error:', error);
|
||
// 即使出错也强制跳转到登录页
|
||
window.location.href = '/login';
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设置按钮加载状态
|
||
* @param {HTMLElement} btn 按钮元素
|
||
* @param {boolean} isLoading 是否正在加载
|
||
* @param {string} text 加载时的文本
|
||
* @returns {boolean} true表示状态设置成功
|
||
*/
|
||
setLoading(btn, isLoading, text = '') {
|
||
if (!btn) return false;
|
||
|
||
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}`;
|
||
btn.disabled = true;
|
||
} else {
|
||
btn.innerHTML = btn.dataset.originalText || btn.innerHTML;
|
||
delete btn.dataset.loading;
|
||
btn.disabled = false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 显示通知
|
||
* @param {string} message 消息内容
|
||
* @param {string} type 消息类型 'success' | 'error' | 'info'
|
||
*/
|
||
showNotification(message, type = 'info') {
|
||
const container = document.getElementById('notification-container');
|
||
if (!container) return;
|
||
|
||
const notification = document.createElement('div');
|
||
notification.className = `notification ${type}`;
|
||
|
||
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>
|
||
`;
|
||
|
||
container.appendChild(notification);
|
||
|
||
// 动画显示
|
||
requestAnimationFrame(() => {
|
||
notification.classList.add('show');
|
||
});
|
||
|
||
// 自动消失
|
||
setTimeout(() => {
|
||
notification.classList.remove('show');
|
||
notification.addEventListener('transitionend', () => {
|
||
notification.remove();
|
||
});
|
||
}, 3000);
|
||
}
|
||
}
|
||
|
||
// 初始化
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
window.authManager = new AuthManager();
|
||
}); |