Files
WebWork/frontend/public/js/admin.js
祀梦 bcf2c71fad refactor(frontend): 重构前端目录结构并优化认证流程
将前端文件从html目录迁移到views目录,按功能模块组织
重构认证中间件和路由处理,简化页面权限控制
更新静态资源引用路径,统一使用/public前缀
添加学生仪表板页面,优化移动端显示
移除旧版html和js文件,更新样式和脚本
2025-12-21 22:07:23 +08:00

584 lines
19 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class AdminDashboard {
constructor() {
// 动æ€<C3A6>设置API基础URL,支æŒ<C3A6>file:///å<><C3A5>è®®åŒlocalhost:3000访问
this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api';
this.currentUser = null;
this.stats = {};
this.users = [];
this.students = [];
this.teachers = [];
this.init();
}
async init() {
// 检查登录状� if (!await this.checkAuth()) {
window.location.href = '/login';
return;
}
// 加载用户信æ<C2A1>¯
await this.loadUserInfo();
// 加载统计数æ<C2B0>®
await this.loadStats();
// 加载用户数æ<C2B0>®
await this.loadUsers();
// 绑定事件
this.bindEvents();
// æ´æ°ç•Œé<C592>¢
this.updateUI();
// åˆ<C3A5>å§åŒå¾è¡? this.initCharts();
}
async checkAuth() {
try {
const response = await fetch(`${this.apiBase}/auth/me`, {
credentials: 'include'
});
if (!response.ok) {
return false;
}
const data = await response.json();
return data.success && data.user.role === 'admin';
} catch (error) {
console.error('认è¯<C3A8>检查失è´?', error);
return false;
}
}
async loadUserInfo() {
try {
const response = await fetch(`${this.apiBase}/auth/me`, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.success) {
this.currentUser = data.user;
}
}
} catch (error) {
console.error('加载用户信æ<C2A1>¯å¤±è´¥:', error);
}
}
async loadStats() {
try {
const response = await fetch(`${this.apiBase}/admin/stats`, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.success) {
this.stats = data.stats;
this.updateStatsUI();
}
}
} catch (error) {
console.error('加载统计数æ<C2B0>®å¤±è´¥:', error);
this.showNotification('加载统计数æ<C2B0>®å¤±è´¥', 'error');
}
}
async loadUsers() {
try {
const response = await fetch(`${this.apiBase}/admin/users`, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.success) {
this.users = data.users;
this.renderUsersTable();
}
}
} catch (error) {
console.error('加载用户数æ<C2B0>®å¤±è´¥:', error);
this.showNotification('加载用户数æ<C2B0>®å¤±è´¥', 'error');
}
}
updateStatsUI() {
// æ´æ°ç»Ÿè®¡å<C2A1>¡ç‰‡
const statElements = {
'totalUsers': 'totalUsers',
'totalStudents': 'totalStudents',
'totalTeachers': 'totalTeachers',
'totalCourses': 'totalCourses',
'totalGrades': 'totalGrades',
'avgScore': 'avgScore'
};
Object.entries(statElements).forEach(([key, elementId]) => {
const element = document.getElementById(elementId);
if (element && this.stats[key] !== undefined) {
element.textContent = this.stats[key];
}
});
// æ›´æ–°æ—¶é—´
const timeElement = document.getElementById('currentTime');
if (timeElement) {
timeElement.textContent = new Date().toLocaleString();
}
}
renderUsersTable() {
const tableBody = document.getElementById('usersTableBody');
if (!tableBody) return;
if (this.users.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="7" class="text-center">
<div class="no-data">
<i class="fas fa-info-circle"></i>
<p>æšæ— ç”¨æˆ·æ•°æ<C2B0>®</p>
</div>
</td>
</tr>
`;
return;
}
tableBody.innerHTML = this.users.map(user => {
const roleClass = this.getRoleClass(user.role);
return `
<tr>
<td><input type="checkbox" class="user-checkbox" data-id="${user.id}"></td>
<td>${user.user_id}</td>
<td>${user.full_name}</td>
<td><span class="role-badge ${roleClass}">${user.role}</span></td>
<td>${user.class_name || 'N/A'}</td>
<td>${user.email || 'N/A'}</td>
<td>
<div class="action-buttons">
<button class="btn-edit" data-id="${user.id}">
<i class="fas fa-edit"></i> 编辑
</button>
<button class="btn-delete" data-id="${user.id}">
<i class="fas fa-trash"></i> 删除
</button>
</div>
</td>
</tr>
`;
}).join('');
}
getRoleClass(role) {
switch (role) {
case 'admin': return 'role-admin';
case 'teacher': return 'role-teacher';
case 'student': return 'role-student';
default: return 'role-default';
}
}
bindEvents() {
// 导航è<C2AA>œå<C593>•ç¹å‡»
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const page = link.dataset.page;
this.loadPage(page);
});
});
// æ<>œç´¢æŒ‰é®
document.getElementById('searchBtn')?.addEventListener('click', () => {
this.handleSearch();
});
// é‡<C3A9>置按é®
document.getElementById('resetBtn')?.addEventListener('click', () => {
this.resetFilters();
});
// 添加用户按钮
document.getElementById('addUserBtn')?.addEventListener('click', () => {
this.addUser();
});
// 导出按钮
document.getElementById('exportBtn')?.addEventListener('click', () => {
this.exportUsers();
});
// 批é‡<C3A9>删除按é®
document.getElementById('batchDeleteBtn')?.addEventListener('click', () => {
this.batchDeleteUsers();
});
// 表格æ“<C3A6>作按é®äºä»¶å§”托
document.addEventListener('click', (e) => {
if (e.target.closest('.btn-edit')) {
const userId = e.target.closest('.btn-edit').dataset.id;
this.editUser(userId);
}
if (e.target.closest('.btn-delete')) {
const userId = e.target.closest('.btn-delete').dataset.id;
this.deleteUser(userId);
}
});
// 退出登� document.getElementById('logoutBtn')?.addEventListener('click', () => {
this.handleLogout();
});
// 刷新按钮
document.getElementById('refreshBtn')?.addEventListener('click', () => {
this.refreshData();
});
}
async loadPage(page) {
// 这里å<C592>¯ä»¥å®žçް页é<C2B5>¢åˆ‡æ<E280A1>¢é€»è¾
// æšæ—¶ä½¿ç”¨ç®€å<E282AC>•è·³è½? switch (page) {
case 'users':
window.location.href = '/admin/user_management';
break;
case 'students':
window.location.href = '/admin/student_management';
break;
case 'teachers':
// å<>¯ä»¥è·³è½¬åˆ°æ•™å¸ˆç®¡ç<C2A1>†é¡µé<C2B5>? break;
case 'grades':
window.location.href = '/teacher/grade_management';
break;
case 'settings':
// å<>¯ä»¥è·³è½¬åˆ°ç³»ç»Ÿè®¾ç½®é¡µé<C2B5>? break;
}
}
handleSearch() {
const userId = document.getElementById('userIdFilter')?.value || '';
const name = document.getElementById('nameFilter')?.value || '';
const role = document.getElementById('roleFilter')?.value || '';
const className = document.getElementById('classFilter')?.value || '';
// 这里å<C592>¯ä»¥å®žçްæ<C2B0>œç´¢é€»è¾
this.showNotification(<>œç´¢åŠŸèƒ½å¾…å®žçŽ?, 'info');
}
resetFilters() {
document.getElementById('userIdFilter').value = '';
document.getElementById('nameFilter').value = '';
document.getElementById('roleFilter').value = '';
document.getElementById('classFilter').value = '';
// é‡<C3A9>æ°åŠ è½½æ•°æ<C2B0>®
this.loadUsers();
}
async addUser() {
// 这里å<C592>¯ä»¥æ‰“开添加用户模æ€<C3A6>框
const userData = {
user_id: prompt('请è¾å¥ç¨æˆ·ID:'),
full_name: prompt('请è¾å¥å§å<EFBFBD>?'),
role: prompt('请è¾å¥è§è?(admin/teacher/student):'),
email: prompt('请è¾å¥é®ç®?'),
class_name: prompt('请è¾å¥ç<EFBFBD>­çº?(å­¦çŸ/æå¸ˆå<EFBFBD>¯é?:')
};
if (!userData.user_id || !userData.full_name || !userData.role) {
this.showNotification('ç¨æˆ·IDã<EFBFBD>å§å<EFBFBD><EFBFBD>åŒè§è²ä¸ºå¿å¡«é¡¹', 'error');
return;
}
try {
const response = await fetch(`${this.apiBase}/admin/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(userData)
});
const data = await response.json();
if (data.success) {
this.showNotification('ç¨æˆ·æ·»åŠ æˆ<EFBFBD>功', 'success');
await this.loadUsers();
} else {
this.showNotification(data.message || '添加失败', 'error');
}
} catch (error) {
console.error('æ·»åŠ ç¨æˆ·å¤±è´¥:', error);
this.showNotification('æ·»åŠ ç¨æˆ·å¤±è´¥', 'error');
}
}
async exportUsers() {
try {
const response = await fetch(`${this.apiBase}/admin/users/export`, {
credentials: 'include'
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `用户列表_${new Date().toISOString().split('T')[0]}.xlsx`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
} catch (error) {
console.error('导åºå¤±è´¥:', error);
this.showNotification('导åºå¤±è´¥', 'error');
}
}
async batchDeleteUsers() {
const checkboxes = document.querySelectorAll('.user-checkbox:checked');
if (checkboxes.length === 0) {
this.showNotification('è¯·éæ©è¦<EFBFBD>删é¤çšç¨æˆ·', 'warning');
return;
}
if (!confirm(`确定è¦<C3A8>删除选中çš?${checkboxes.length} 个用户å<C2B7>—?`)) {
return;
}
const userIds = Array.from(checkboxes).map(cb => cb.dataset.id);
try {
const response = await fetch(`${this.apiBase}/admin/users/batch`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ userIds })
});
const data = await response.json();
if (data.success) {
this.showNotification(`æˆ<C3A6>功删除 ${userIds.length} 个用户`, 'success');
await this.loadUsers();
} else {
this.showNotification(data.message || '删é¤å¤±è´¥', 'error');
}
} catch (error) {
console.error('æ¹é<EFBFBD>删é¤å¤±è´¥:', error);
this.showNotification('æ¹é<EFBFBD>删é¤å¤±è´¥', 'error');
}
}
async editUser(userId) {
const user = this.users.find(u => u.id == userId);
if (!user) return;
// 这里å<C592>¯ä»¥æ‰“å¼€ç¼è¾æ¨¡æ€<C3A6>框
const newName = prompt('请è¾å¥æ°çšå§å<EFBFBD>?', user.full_name);
if (newName === null) return;
const newRole = prompt('请è¾å¥æ°çšè§è?', user.role);
if (newRole === null) return;
try {
const response = await fetch(`${this.apiBase}/admin/users/${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
full_name: newName,
role: newRole,
email: user.email,
class_name: user.class_name
})
});
const data = await response.json();
if (data.success) {
this.showNotification('ç¨æˆ·æ´æ°æˆ<EFBFBD>功', 'success');
await this.loadUsers();
} else {
this.showNotification(data.message || 'æ´æ°å¤±è´¥', 'error');
}
} catch (error) {
console.error('æ´æ°ç¨æˆ·å¤±è´¥:', error);
this.showNotification('æ´æ°ç¨æˆ·å¤±è´¥', 'error');
}
}
async deleteUser(userId) {
if (!confirm('确定è¦<EFBFBD>删é¤è¿ä¸ªç¨æˆ·å<EFBFBD>ï¼?)) {
return;
}
try {
const response = await fetch(`${this.apiBase}/admin/users/${userId}`, {
method: 'DELETE',
credentials: 'include'
});
const data = await response.json();
if (data.success) {
this.showNotification('用户删除æˆ<C3A6>功', 'success');
await this.loadUsers();
} else {
this.showNotification(data.message || '删除失败', 'error');
}
} catch (error) {
console.error('删除用户失败:', error);
this.showNotification('删除用户失败', 'error');
}
}
async handleLogout() {
try {
const response = await fetch(`${this.apiBase}/auth/logout`, {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
window.location.href = '/login';
}
} catch (error) {
console.error('退出登录失�', error);
}
}
async refreshData() {
await this.loadStats();
await this.loadUsers();
this.showNotification('æ•°æ<C2B0>®å·²åˆ·æ?, 'success');
}
updateUI() {
// æ´æ°ç”¨æˆ·ä¿¡æ<C2A1>¯
if (this.currentUser) {
const userInfoElements = document.querySelectorAll('.user-info');
userInfoElements.forEach(el => {
el.textContent = `${this.currentUser.full_name} (${this.currentUser.role})`;
});
}
}
async initCharts() {
// 加载Chart.js� await this.loadChartLibrary();
// åˆ<C3A5>å§åŒç”¨æˆ·åˆ†å¸ƒé¥¼å? this.initUserDistributionChart();
// åˆ<C3A5>å§åŒæˆ<C3A6>绩分布柱状å¾
this.initGradeDistributionChart();
}
showNotification(message, type = 'info') {
// 创建通知元素
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : type === 'warning' ? 'exclamation-triangle' : 'info-circle'}"></i>
<span>${message}</span>
<button class="notification-close">&times;</button>
`;
// 添加到页é<C2B5>? document.body.appendChild(notification);
// 添加关闭事件
notification.querySelector('.notification-close').addEventListener('click', () => {
notification.remove();
});
// 自动移除
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 5000);
}
async loadChartLibrary() {
if (typeof Chart !== 'undefined') return;
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
initUserDistributionChart() {
const ctx = document.getElementById('userDistributionChart');
if (!ctx) return;
// æ¨¡æŸæ•°æ<C2B0>®
const data = {
labels: ['学生', '教师', '管ç<C2A1>†å?],
datasets: [{
data: [this.stats.totalStudents || 100, this.stats.totalTeachers || 20, 1],
backgroundColor: [
'rgba(54, 162, 235, 0.8)',
'rgba(255, 206, 86, 0.8)',
'rgba(255, 99, 132, 0.8)'
]
}]
};
new Chart(ctx, {
type: 'pie',
data: data,
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
initGradeDistributionChart() {
const ctx = document.getElementById('gradeDistributionChart');
if (!ctx) return;
// æ¨¡æŸæ•°æ<C2B0>®
const data = {
labels: ['A', 'B', 'C', 'D', 'F'],
datasets: [{
label: 'æˆ<EFBFBD>绩åˆå¸ƒ',
data: [25, 35, 20, 15, 5],
backgroundColor: [
'rgba(75, 192, 192, 0.8)',
'rgba(54, 162, 235, 0.8)',
'rgba(255, 206, 86, 0.8)',
'rgba(255, 159, 64, 0.8)',
'rgba(255, 99, 132, 0.8)'
]
}]
};
new Chart(ctx, {
type: 'bar',
data: data,
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 10
}
}
}
}
});
}
}