refactor(frontend): 重构前端目录结构并优化认证流程
将前端文件从html目录迁移到views目录,按功能模块组织 重构认证中间件和路由处理,简化页面权限控制 更新静态资源引用路径,统一使用/public前缀 添加学生仪表板页面,优化移动端显示 移除旧版html和js文件,更新样式和脚本
This commit is contained in:
@@ -52,65 +52,72 @@ app.use(session({
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 静态文件服务
|
// 静态文件服务 - 只公开 public 目录
|
||||||
app.use(express.static(path.join(__dirname, '../frontend')));
|
app.use('/public', express.static(path.join(__dirname, '../frontend/public')));
|
||||||
|
|
||||||
// 重定向旧路径 /frontend/html/* 到 /html/*
|
// 页面认证中间件
|
||||||
app.get('/frontend/html/*', (req, res) => {
|
const requirePageAuth = (req, res, next) => {
|
||||||
const path = req.params[0];
|
if (!req.session.user) {
|
||||||
res.redirect(`/html/${path}`);
|
return res.redirect('/login');
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
const requirePageRole = (allowedRoles) => {
|
||||||
|
return (req, res, next) => {
|
||||||
|
if (!req.session.user) return res.redirect('/login');
|
||||||
|
if (!allowedRoles.includes(req.session.user.role)) {
|
||||||
|
return res.status(403).send('<h1>403 Forbidden - 权限不足</h1><a href="/dashboard">返回首页</a>');
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面路由
|
||||||
|
app.get('/', (req, res) => res.redirect('/login'));
|
||||||
|
app.get('/login', (req, res) => {
|
||||||
|
if (req.session.user) return res.redirect('/dashboard');
|
||||||
|
res.sendFile(path.join(__dirname, '../frontend/views/auth/login.html'));
|
||||||
|
});
|
||||||
|
app.get('/register', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/auth/register.html')));
|
||||||
|
|
||||||
|
app.get('/dashboard', requirePageAuth, (req, res) => {
|
||||||
|
const role = req.session.user?.role;
|
||||||
|
switch (role) {
|
||||||
|
case 'student': res.redirect('/student/dashboard'); break;
|
||||||
|
case 'teacher': res.redirect('/teacher/dashboard'); break;
|
||||||
|
case 'admin': res.redirect('/admin/dashboard'); break;
|
||||||
|
default: res.redirect('/login');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 路由
|
// 学生页面
|
||||||
|
app.get('/student/dashboard', requirePageAuth, requirePageRole(['student']), (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, '../frontend/views/student/dashboard.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 教师页面
|
||||||
|
const teacherRouter = express.Router();
|
||||||
|
teacherRouter.use(requirePageAuth, requirePageRole(['teacher']));
|
||||||
|
teacherRouter.get('/dashboard', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/teacher/dashboard.html')));
|
||||||
|
teacherRouter.get('/grade_entry', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/teacher/grade_entry.html')));
|
||||||
|
teacherRouter.get('/grade_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/teacher/grade_management.html')));
|
||||||
|
app.use('/teacher', teacherRouter);
|
||||||
|
|
||||||
|
// 管理员页面
|
||||||
|
const adminRouter = express.Router();
|
||||||
|
adminRouter.use(requirePageAuth, requirePageRole(['admin']));
|
||||||
|
adminRouter.get('/dashboard', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/dashboard.html')));
|
||||||
|
adminRouter.get('/student_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/student_management.html')));
|
||||||
|
adminRouter.get('/user_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/user_management.html')));
|
||||||
|
app.use('/admin', adminRouter);
|
||||||
|
|
||||||
|
// API 路由
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/student', studentRoutes);
|
app.use('/api/student', studentRoutes);
|
||||||
app.use('/api/teacher', teacherRoutes);
|
app.use('/api/teacher', teacherRoutes);
|
||||||
app.use('/api/admin', adminRoutes);
|
app.use('/api/admin', adminRoutes);
|
||||||
|
|
||||||
// 认证中间件
|
|
||||||
const { requireAuth, requireRole } = require('./middleware/auth');
|
|
||||||
|
|
||||||
// 页面路由
|
|
||||||
app.get('/', (req, res) => {
|
|
||||||
res.sendFile(path.join(__dirname, '../frontend/html/login.html'));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/dashboard', requireAuth, (req, res) => {
|
|
||||||
// 根据用户角色重定向到不同的仪表板
|
|
||||||
const role = req.session.user?.role;
|
|
||||||
|
|
||||||
switch (role) {
|
|
||||||
case 'student':
|
|
||||||
res.redirect('/html/student_dashboard.html');
|
|
||||||
break;
|
|
||||||
case 'teacher':
|
|
||||||
res.redirect('/html/teacher_dashboard.html');
|
|
||||||
break;
|
|
||||||
case 'admin':
|
|
||||||
res.redirect('/html/admin_dashboard.html');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// 如果没有角色信息,重定向到登录页面
|
|
||||||
res.redirect('/');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 学生页面路由
|
|
||||||
app.get('/student/*', requireAuth, requireRole(['student', 'admin', 'teacher']), (req, res, next) => {
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 教师页面路由
|
|
||||||
app.get('/teacher/*', requireAuth, requireRole(['teacher', 'admin']), (req, res, next) => {
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 管理员页面路由
|
|
||||||
app.get('/admin/*', requireAuth, requireRole(['admin']), (req, res, next) => {
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 404处理
|
// 404处理
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
res.status(404).json({ error: 'Not found' });
|
res.status(404).json({ error: 'Not found' });
|
||||||
|
|||||||
@@ -1,269 +0,0 @@
|
|||||||
class AuthManager {
|
|
||||||
constructor() {
|
|
||||||
// 动态设置API基础URL,支持file:///协议和localhost:3000访问
|
|
||||||
this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api';
|
|
||||||
this.initEventListeners();
|
|
||||||
this.checkAuthStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkAuthStatus() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${this.apiBase}/auth/me`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success && data.user) {
|
|
||||||
// 用户已登录,根据角色重定向到正确的仪表板
|
|
||||||
const userRole = data.user.role;
|
|
||||||
let redirectUrl = '/dashboard';
|
|
||||||
|
|
||||||
if (userRole === 'student') {
|
|
||||||
redirectUrl = '/frontend/html/student_dashboard.html';
|
|
||||||
} else if (userRole === 'teacher') {
|
|
||||||
redirectUrl = '/frontend/html/teacher_dashboard.html';
|
|
||||||
} else if (userRole === 'admin') {
|
|
||||||
redirectUrl = '/frontend/html/admin_dashboard.html';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果当前页面是登录或注册页面,则重定向到仪表板
|
|
||||||
const currentPath = window.location.pathname;
|
|
||||||
if (currentPath.includes('login.html') || currentPath.includes('register.html')) {
|
|
||||||
window.location.href = redirectUrl;
|
|
||||||
}
|
|
||||||
// 如果当前页面不是正确的仪表板页面,则重定向
|
|
||||||
else if (!currentPath.includes(redirectUrl) &&
|
|
||||||
currentPath !== '/' &&
|
|
||||||
!currentPath.includes('index.html')) {
|
|
||||||
window.location.href = redirectUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('用户未登录');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 logoutBtn = document.getElementById('logoutBtn');
|
|
||||||
if (logoutBtn) {
|
|
||||||
logoutBtn.addEventListener('click', (e) => this.handleLogout(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleLogin(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const form = e.target;
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const data = Object.fromEntries(formData.entries());
|
|
||||||
|
|
||||||
const submitBtn = form.querySelector('button[type="submit"]');
|
|
||||||
const originalText = submitBtn.innerHTML;
|
|
||||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 登录中...';
|
|
||||||
submitBtn.disabled = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
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');
|
|
||||||
|
|
||||||
// 根据用户角色跳转到不同的仪表板
|
|
||||||
const userRole = result.user?.role;
|
|
||||||
let redirectUrl = '/dashboard';
|
|
||||||
|
|
||||||
if (userRole === 'student') {
|
|
||||||
redirectUrl = '/frontend/html/student_dashboard.html';
|
|
||||||
} else if (userRole === 'teacher') {
|
|
||||||
redirectUrl = '/frontend/html/teacher_dashboard.html';
|
|
||||||
} else if (userRole === 'admin') {
|
|
||||||
redirectUrl = '/frontend/html/admin_dashboard.html';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 延迟跳转以显示通知
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = redirectUrl;
|
|
||||||
}, 1500);
|
|
||||||
} else {
|
|
||||||
this.showNotification(result.message || '登录失败', 'error');
|
|
||||||
submitBtn.innerHTML = originalText;
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('登录错误:', error);
|
|
||||||
this.showNotification('网络错误,请重试', 'error');
|
|
||||||
submitBtn.innerHTML = originalText;
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleRegister(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const form = e.target;
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const data = Object.fromEntries(formData.entries());
|
|
||||||
|
|
||||||
// 验证密码
|
|
||||||
if (data.password !== data.confirmPassword) {
|
|
||||||
this.showNotification('两次输入的密码不一致', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitBtn = form.querySelector('button[type="submit"]');
|
|
||||||
const originalText = submitBtn.innerHTML;
|
|
||||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 注册中...';
|
|
||||||
submitBtn.disabled = 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 = '/html/login.html';
|
|
||||||
}, 1500);
|
|
||||||
} else {
|
|
||||||
this.showNotification(result.message || '注册失败', 'error');
|
|
||||||
submitBtn.innerHTML = originalText;
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('注册错误:', error);
|
|
||||||
this.showNotification('网络错误,请重试', 'error');
|
|
||||||
submitBtn.innerHTML = originalText;
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleLogout(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!confirm('确定要退出登录吗?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = '/';
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
|
||||||
this.showNotification('退出登录失败', 'error');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('退出登录错误:', error);
|
|
||||||
this.showNotification('网络错误,请重试', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showNotification(message, type = 'info') {
|
|
||||||
// 移除现有的通知
|
|
||||||
const existingNotification = document.querySelector('.notification');
|
|
||||||
if (existingNotification) {
|
|
||||||
existingNotification.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新的通知
|
|
||||||
const notification = document.createElement('div');
|
|
||||||
notification.className = `notification notification-${type}`;
|
|
||||||
notification.innerHTML = `
|
|
||||||
<i class="fas fa-${type === 'success' ? 'check-circle' : 'exclamation-circle'}"></i>
|
|
||||||
<span>${message}</span>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 样式
|
|
||||||
notification.style.cssText = `
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'};
|
|
||||||
color: white;
|
|
||||||
padding: 15px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
z-index: 1000;
|
|
||||||
animation: slideIn 0.3s ease;
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.body.appendChild(notification);
|
|
||||||
|
|
||||||
// 3秒后自动移除
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.style.animation = 'slideOut 0.3s ease';
|
|
||||||
setTimeout(() => notification.remove(), 300);
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
// 添加动画关键帧
|
|
||||||
if (!document.querySelector('#notification-styles')) {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'notification-styles';
|
|
||||||
style.textContent = `
|
|
||||||
@keyframes slideIn {
|
|
||||||
from {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes slideOut {
|
|
||||||
from {
|
|
||||||
transform: translateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化认证管理器
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
window.authManager = new AuthManager();
|
|
||||||
});
|
|
||||||
47
frontend/public/css/notification.css
Normal file
47
frontend/public/css/notification.css
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/* 通知消息样式 */
|
||||||
|
.notification {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 15px 25px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
transform: translateX(120%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
border-left: 4px solid #4e73df;
|
||||||
|
max-width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.show {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.success {
|
||||||
|
border-left-color: #2ecc71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.error {
|
||||||
|
border-left-color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification i {
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.success i {
|
||||||
|
color: #2ecc71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.error i {
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
@@ -1147,4 +1147,51 @@ tr:hover {
|
|||||||
.action-buttons {
|
.action-buttons {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}/ | ||||||