refactor(frontend): 重构前端目录结构并优化认证流程
将前端文件从html目录迁移到views目录,按功能模块组织 重构认证中间件和路由处理,简化页面权限控制 更新静态资源引用路径,统一使用/public前缀 添加学生仪表板页面,优化移动端显示 移除旧版html和js文件,更新样式和脚本
This commit is contained in:
287
frontend/public/js/auth.js
Normal file
287
frontend/public/js/auth.js
Normal file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 认证模块管理器
|
||||
* 处理登录、注册、注销及权限检查
|
||||
*/
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user