新增教师资料更新功能,包括个人信息修改和密码更新 添加操作日志记录系统,记录用户关键操作 实现系统设置模块,支持动态配置系统参数 重构数据库模型,新增教师表和系统设置表 优化成绩录入逻辑,支持平时分、期中和期末成绩计算 添加数据导出功能,支持学生、教师和成绩数据导出 完善管理员后台,增加统计图表和操作日志查看
277 lines
9.8 KiB
JavaScript
277 lines
9.8 KiB
JavaScript
/**
|
|
* 认证模块管理器
|
|
* 处理登录、注册、注销及权限检查
|
|
* 适配 Bootstrap 5 样式
|
|
*/
|
|
class AuthManager {
|
|
constructor() {
|
|
this.apiBase = '/api';
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.createNotificationContainer();
|
|
this.initEventListeners();
|
|
this.checkAuthStatus();
|
|
}
|
|
|
|
/**
|
|
* 创建通知容器 (适配 Bootstrap Toast)
|
|
*/
|
|
createNotificationContainer() {
|
|
if (!document.getElementById('notification-container')) {
|
|
const container = document.createElement('div');
|
|
container.id = 'notification-container';
|
|
container.className = 'toast-container position-fixed top-0 end-0 p-3';
|
|
container.style.zIndex = '9999';
|
|
document.body.appendChild(container);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 检查用户认证状态
|
|
*/
|
|
async checkAuthStatus() {
|
|
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.data && data.data.user) {
|
|
const redirectUrl = this.getDashboardUrl(data.data.user.role);
|
|
if (isAuthPage || currentPath === '/') {
|
|
window.location.href = redirectUrl;
|
|
}
|
|
}
|
|
} 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 = 'flex'; // 配合 Bootstrap input-group
|
|
classInput.required = true;
|
|
} else {
|
|
classField.style.display = 'none';
|
|
classInput.required = false;
|
|
classInput.value = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
async handleLogin(e) {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
const submitBtn = document.getElementById('loginBtn') || 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');
|
|
|
|
// 获取用户信息,优先从 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.setLoading(submitBtn, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
async handleRegister(e) {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
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)) {
|
|
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) {
|
|
alert('退出登录成功');
|
|
window.location.href = '/login';
|
|
} else {
|
|
alert(result.message || '退出登录失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('Logout error:', error);
|
|
alert('退出登录出错');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 设置按钮加载状态
|
|
*/
|
|
setLoading(btn, isLoading) {
|
|
if (!btn) return false;
|
|
|
|
const spinner = btn.querySelector('.spinner-border');
|
|
if (isLoading) {
|
|
if (btn.disabled) return false;
|
|
btn.disabled = true;
|
|
if (spinner) spinner.classList.remove('d-none');
|
|
} else {
|
|
btn.disabled = false;
|
|
if (spinner) spinner.classList.add('d-none');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 显示通知 (Bootstrap 5 Toast)
|
|
*/
|
|
showNotification(message, type = 'info') {
|
|
const container = document.getElementById('notification-container');
|
|
if (!container) return;
|
|
|
|
const iconMap = {
|
|
success: 'fa-check-circle',
|
|
error: 'fa-exclamation-circle',
|
|
warning: 'fa-exclamation-triangle',
|
|
info: 'fa-info-circle'
|
|
};
|
|
|
|
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();
|
|
|
|
toastElement.addEventListener('hidden.bs.toast', () => {
|
|
toastElement.remove();
|
|
});
|
|
}
|
|
}
|
|
|
|
// 初始化认证管理器
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.authManager = new AuthManager();
|
|
}); |