1005 lines
39 KiB
JavaScript
1005 lines
39 KiB
JavaScript
/**
|
|
* 管理员端功能管理
|
|
*/
|
|
class AdminManager {
|
|
constructor() {
|
|
this.apiBase = '/api/admin';
|
|
this.init();
|
|
}
|
|
|
|
async init() {
|
|
this.updateCurrentTime();
|
|
setInterval(() => this.updateCurrentTime(), 1000);
|
|
await this.loadUserInfo();
|
|
|
|
// 页面路由逻辑
|
|
const path = window.location.pathname;
|
|
if (path.includes('/dashboard')) {
|
|
this.initDashboard();
|
|
} else if (path.includes('/user_management')) {
|
|
this.initUserManagement();
|
|
} else if (path.includes('/student_management')) {
|
|
this.initStudentManagement();
|
|
} else if (path.includes('/teacher_management')) {
|
|
this.initTeacherManagement();
|
|
} else if (path.includes('/grade_statistics')) {
|
|
this.initGradeStatistics();
|
|
} else if (path.includes('/system_settings')) {
|
|
this.initSystemSettings();
|
|
} else if (path.includes('/data_export')) {
|
|
this.initDataExport();
|
|
} else if (path.includes('/operation_logs')) {
|
|
this.initOperationLogs();
|
|
}
|
|
|
|
// Logout
|
|
const logoutBtn = document.getElementById('logoutBtn');
|
|
if (logoutBtn) {
|
|
logoutBtn.addEventListener('click', async (e) => {
|
|
e.preventDefault();
|
|
if (confirm('确定要退出登录吗?')) {
|
|
try {
|
|
const res = await fetch('/api/auth/logout', { method: 'POST' });
|
|
const result = await res.json();
|
|
if (result.success) {
|
|
alert('退出登录成功');
|
|
window.location.href = '/login';
|
|
} else {
|
|
alert(result.message || '退出登录失败');
|
|
}
|
|
} catch (e) {
|
|
console.error('Logout failed', e);
|
|
alert('退出登录出错: ' + e.message);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
updateCurrentTime() {
|
|
const timeElement = document.getElementById('currentTime');
|
|
if (timeElement) {
|
|
const now = new Date();
|
|
const options = {
|
|
year: 'numeric', month: 'long', day: 'numeric',
|
|
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
|
};
|
|
timeElement.textContent = now.toLocaleString('zh-CN', options);
|
|
}
|
|
}
|
|
|
|
async loadUserInfo() {
|
|
try {
|
|
const res = await fetch('/api/auth/me');
|
|
const result = await res.json();
|
|
if (result.success && result.data.user) {
|
|
const user = result.data.user;
|
|
this.user = user;
|
|
|
|
const nameEls = document.querySelectorAll('#adminName, #userName');
|
|
nameEls.forEach(el => el.textContent = user.name || '管理员');
|
|
}
|
|
} catch (error) {
|
|
console.error('Load user info failed:', error);
|
|
}
|
|
}
|
|
|
|
// ================= Dashboard =================
|
|
async initDashboard() {
|
|
try {
|
|
const res = await fetch(`${this.apiBase}/stats`);
|
|
const result = await res.json();
|
|
|
|
if (result.success) {
|
|
const stats = result.data;
|
|
if (document.getElementById('totalUsers')) document.getElementById('totalUsers').textContent = stats.users;
|
|
if (document.getElementById('totalStudents')) document.getElementById('totalStudents').textContent = stats.students;
|
|
if (document.getElementById('totalTeachers')) document.getElementById('totalTeachers').textContent = stats.teachers;
|
|
if (document.getElementById('totalCourses')) document.getElementById('totalCourses').textContent = stats.courses;
|
|
}
|
|
} catch (e) {
|
|
console.error('Load stats failed', e);
|
|
}
|
|
}
|
|
|
|
// ================= User Management =================
|
|
async initUserManagement() {
|
|
this.currentPage = 1;
|
|
this.pageSize = 10;
|
|
|
|
// Bind Filter Events
|
|
document.getElementById('search').addEventListener('input', () => this.loadUsers());
|
|
document.getElementById('roleFilter').addEventListener('change', () => this.loadUsers());
|
|
|
|
// Bind Modal Events
|
|
const modalEl = document.getElementById('userModal');
|
|
this.userModal = new bootstrap.Modal(modalEl);
|
|
|
|
document.getElementById('addUserBtn').addEventListener('click', () => {
|
|
document.getElementById('userForm').reset();
|
|
document.getElementById('userModalTitle').textContent = '新增用户';
|
|
document.getElementById('isEdit').value = 'false';
|
|
document.getElementById('userId').readOnly = false;
|
|
this.userModal.show();
|
|
});
|
|
|
|
document.getElementById('saveUserBtn').addEventListener('click', () => this.saveUser());
|
|
|
|
// Initial Load
|
|
await this.loadUsers();
|
|
}
|
|
|
|
async loadUsers() {
|
|
const search = document.getElementById('search').value;
|
|
const role = document.getElementById('roleFilter').value;
|
|
const tbody = document.getElementById('userTableBody');
|
|
|
|
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-5">加载中...</td></tr>';
|
|
|
|
try {
|
|
const query = new URLSearchParams({
|
|
page: this.currentPage,
|
|
limit: this.pageSize,
|
|
search,
|
|
role
|
|
});
|
|
|
|
const res = await fetch(`${this.apiBase}/users?${query}`);
|
|
const result = await res.json();
|
|
|
|
if (result.success) {
|
|
this.renderUserTable(result.data, result.pagination);
|
|
} else {
|
|
tbody.innerHTML = `<tr><td colspan="6" class="text-center text-danger">${result.message}</td></tr>`;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-danger">加载失败</td></tr>';
|
|
}
|
|
}
|
|
|
|
renderUserTable(users, pagination) {
|
|
const tbody = document.getElementById('userTableBody');
|
|
|
|
if (users.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-5 text-muted">暂无用户</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = users.map(u => `
|
|
<tr>
|
|
<td class="ps-4 fw-bold">${u.id}</td>
|
|
<td>${u.name}</td>
|
|
<td><span class="badge bg-${this.getRoleBadgeColor(u.role)}">${this.getRoleName(u.role)}</span></td>
|
|
<td>${u.class || '-'}</td>
|
|
<td>${new Date(u.created_at).toLocaleDateString()}</td>
|
|
<td class="text-end pe-4">
|
|
<button class="btn btn-sm btn-outline-primary me-1 btn-edit-user" data-user='${JSON.stringify(u).replace(/'/g, "'")}'>
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger btn-delete-user" data-id="${u.id}">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
// Bind Action Buttons
|
|
document.querySelectorAll('.btn-edit-user').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const user = JSON.parse(btn.dataset.user);
|
|
this.openEditUserModal(user);
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.btn-delete-user').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
if(confirm('确定要删除该用户吗?此操作不可恢复。')) {
|
|
this.deleteUser(btn.dataset.id);
|
|
}
|
|
});
|
|
});
|
|
|
|
this.renderPagination(pagination);
|
|
}
|
|
|
|
getRoleBadgeColor(role) {
|
|
switch(role) {
|
|
case 'admin': return 'danger';
|
|
case 'teacher': return 'info';
|
|
case 'student': return 'success';
|
|
default: return 'secondary';
|
|
}
|
|
}
|
|
|
|
getRoleName(role) {
|
|
switch(role) {
|
|
case 'admin': return '管理员';
|
|
case 'teacher': return '教师';
|
|
case 'student': return '学生';
|
|
default: role;
|
|
}
|
|
}
|
|
|
|
renderGenericPagination(containerId, pagination, callbackName) {
|
|
const el = document.getElementById(containerId);
|
|
if (!el) return;
|
|
|
|
const { page, pages } = pagination;
|
|
if (pages <= 0) {
|
|
el.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
|
|
// Previous page
|
|
if (page > 1) {
|
|
html += `<li class="page-item"><a class="page-link" href="javascript:void(0)" onclick="window.adminManager.${callbackName}(${page - 1})">上一页</a></li>`;
|
|
} else {
|
|
html += `<li class="page-item disabled"><a class="page-link" href="javascript:void(0)">上一页</a></li>`;
|
|
}
|
|
|
|
// Page numbers with ellipsis
|
|
if (pages <= 7) {
|
|
for (let i = 1; i <= pages; i++) {
|
|
html += `<li class="page-item ${i === page ? 'active' : ''}"><a class="page-link" href="javascript:void(0)" onclick="window.adminManager.${callbackName}(${i})">${i}</a></li>`;
|
|
}
|
|
} else {
|
|
// First page
|
|
html += `<li class="page-item ${page === 1 ? 'active' : ''}"><a class="page-link" href="javascript:void(0)" onclick="window.adminManager.${callbackName}(1)">1</a></li>`;
|
|
|
|
if (page > 4) {
|
|
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
|
}
|
|
|
|
// Middle pages
|
|
let start = Math.max(2, page - 2);
|
|
let end = Math.min(pages - 1, page + 2);
|
|
|
|
if (page <= 4) {
|
|
start = 2;
|
|
end = 5;
|
|
} else if (page >= pages - 3) {
|
|
start = pages - 4;
|
|
end = pages - 1;
|
|
}
|
|
|
|
for (let i = start; i <= end; i++) {
|
|
html += `<li class="page-item ${i === page ? 'active' : ''}"><a class="page-link" href="javascript:void(0)" onclick="window.adminManager.${callbackName}(${i})">${i}</a></li>`;
|
|
}
|
|
|
|
if (page < pages - 3) {
|
|
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
|
}
|
|
|
|
// Last page
|
|
html += `<li class="page-item ${page === pages ? 'active' : ''}"><a class="page-link" href="javascript:void(0)" onclick="window.adminManager.${callbackName}(${pages})">${pages}</a></li>`;
|
|
}
|
|
|
|
// Next page
|
|
if (page < pages) {
|
|
html += `<li class="page-item"><a class="page-link" href="javascript:void(0)" onclick="window.adminManager.${callbackName}(${page + 1})">下一页</a></li>`;
|
|
} else {
|
|
html += `<li class="page-item disabled"><a class="page-link" href="javascript:void(0)">下一页</a></li>`;
|
|
}
|
|
|
|
// Jump to page
|
|
html += `
|
|
<li class="page-item ms-3 d-flex align-items-center">
|
|
<div class="input-group input-group-sm" style="width: auto;">
|
|
<span class="input-group-text bg-light border-end-0">跳转至</span>
|
|
<input type="number" class="form-control text-center" style="width: 60px;" min="1" max="${pages}" value="${page}"
|
|
onkeydown="if(event.keyCode==13) {
|
|
const p = parseInt(this.value);
|
|
if(p >= 1 && p <= ${pages}) window.adminManager.${callbackName}(p);
|
|
else alert('请输入正确的页码(1-${pages})');
|
|
}">
|
|
<span class="input-group-text bg-light border-start-0">页 / 共 ${pages} 页</span>
|
|
</div>
|
|
</li>
|
|
`;
|
|
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
renderPagination(pagination) {
|
|
this.renderGenericPagination('pagination', pagination, 'changePage');
|
|
}
|
|
|
|
changePage(page) {
|
|
this.currentPage = page;
|
|
this.loadUsers();
|
|
}
|
|
|
|
openEditUserModal(user) {
|
|
document.getElementById('userForm').reset();
|
|
document.getElementById('userModalTitle').textContent = '编辑用户';
|
|
document.getElementById('isEdit').value = 'true';
|
|
document.getElementById('userId').value = user.id;
|
|
document.getElementById('userId').readOnly = true;
|
|
document.getElementById('userNameInput').value = user.name;
|
|
document.getElementById('userRole').value = user.role;
|
|
document.getElementById('userClass').value = user.class || '';
|
|
this.userModal.show();
|
|
}
|
|
|
|
async saveUser() {
|
|
const form = document.getElementById('userForm');
|
|
const formData = new FormData(form);
|
|
const data = Object.fromEntries(formData.entries());
|
|
const isEdit = data.isEdit === 'true';
|
|
|
|
// Validation
|
|
if (!data.id || !data.name || !data.role) {
|
|
alert('请填写必填字段');
|
|
return;
|
|
}
|
|
|
|
if (data.role === 'student' && !data.class) {
|
|
alert('学生角色必须填写班级');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let res;
|
|
if (isEdit) {
|
|
res = await fetch(`${this.apiBase}/users/${data.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
} else {
|
|
res = await fetch(`${this.apiBase}/users`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
}
|
|
|
|
const result = await res.json();
|
|
|
|
if (result.success) {
|
|
alert(isEdit ? '更新成功' : '创建成功');
|
|
this.userModal.hide();
|
|
this.loadUsers();
|
|
} else {
|
|
alert(result.message || '操作失败');
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert('系统错误');
|
|
}
|
|
}
|
|
|
|
async deleteUser(id) {
|
|
try {
|
|
const res = await fetch(`${this.apiBase}/users/${id}`, { method: 'DELETE' });
|
|
const result = await res.json();
|
|
|
|
if (result.success) {
|
|
alert('删除成功');
|
|
this.loadUsers();
|
|
} else {
|
|
alert(result.message || '删除失败');
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert('系统错误');
|
|
}
|
|
}
|
|
|
|
// ================= Student Management =================
|
|
async initStudentManagement() {
|
|
this.studentCurrentPage = 1;
|
|
this.studentPageSize = 10;
|
|
|
|
// Bind Filter Events
|
|
document.getElementById('studentSearch').addEventListener('input', () => this.loadStudents());
|
|
|
|
// Bind Modal Events
|
|
const modalEl = document.getElementById('studentModal');
|
|
this.studentModal = new bootstrap.Modal(modalEl);
|
|
|
|
document.getElementById('addStudentBtn').addEventListener('click', () => {
|
|
document.getElementById('studentForm').reset();
|
|
document.getElementById('studentModalTitle').textContent = '新增学生';
|
|
document.getElementById('studentIsEdit').value = 'false';
|
|
document.getElementById('studentId').readOnly = false;
|
|
this.studentModal.show();
|
|
});
|
|
|
|
document.getElementById('saveStudentBtn').addEventListener('click', () => this.saveStudent());
|
|
|
|
// Initial Load
|
|
await this.loadStudents();
|
|
}
|
|
|
|
async loadStudents() {
|
|
const search = document.getElementById('studentSearch').value;
|
|
const tbody = document.getElementById('studentTableBody');
|
|
|
|
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5">加载中...</td></tr>';
|
|
|
|
try {
|
|
const query = new URLSearchParams({
|
|
page: this.studentCurrentPage,
|
|
limit: this.studentPageSize,
|
|
search
|
|
});
|
|
|
|
const res = await fetch(`${this.apiBase}/students?${query}`);
|
|
const result = await res.json();
|
|
|
|
if (result.success) {
|
|
this.renderStudentTable(result.data, result.pagination);
|
|
} else {
|
|
tbody.innerHTML = `<tr><td colspan="7" class="text-center text-danger">${result.message}</td></tr>`;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-danger">加载失败</td></tr>';
|
|
}
|
|
}
|
|
|
|
renderStudentTable(students, pagination) {
|
|
const tbody = document.getElementById('studentTableBody');
|
|
|
|
if (students.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5 text-muted">暂无学生</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = students.map(s => `
|
|
<tr>
|
|
<td class="ps-4 fw-bold">${s.id}</td>
|
|
<td>${s.name}</td>
|
|
<td>${s.class || '-'}</td>
|
|
<td>${s.major || '-'}</td>
|
|
<td>${s.grade || '-'}</td>
|
|
<td>${s.contact_info || '-'}</td>
|
|
<td class="text-end pe-4">
|
|
<button class="btn btn-sm btn-outline-primary me-1 btn-edit-student" data-student='${JSON.stringify(s).replace(/'/g, "'")}'>
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger btn-delete-student" data-id="${s.id}">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
// Bind Action Buttons
|
|
document.querySelectorAll('.btn-edit-student').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const student = JSON.parse(btn.dataset.student);
|
|
this.openEditStudentModal(student);
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.btn-delete-student').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
if(confirm('确定要删除该学生吗?这将同时删除其用户账号。')) {
|
|
this.deleteStudent(btn.dataset.id);
|
|
}
|
|
});
|
|
});
|
|
|
|
this.renderStudentPagination(pagination);
|
|
}
|
|
|
|
renderStudentPagination(pagination) {
|
|
this.renderGenericPagination('studentPagination', pagination, 'changeStudentPage');
|
|
}
|
|
|
|
changeStudentPage(page) {
|
|
this.studentCurrentPage = page;
|
|
this.loadStudents();
|
|
}
|
|
|
|
openEditStudentModal(student) {
|
|
document.getElementById('studentForm').reset();
|
|
document.getElementById('studentModalTitle').textContent = '编辑学生';
|
|
document.getElementById('studentIsEdit').value = 'true';
|
|
document.getElementById('studentId').value = student.id;
|
|
document.getElementById('studentId').readOnly = true;
|
|
document.getElementById('studentNameInput').value = student.name;
|
|
document.getElementById('studentClass').value = student.class || '';
|
|
document.getElementById('studentMajor').value = student.major || '';
|
|
document.getElementById('studentGrade').value = student.grade || '';
|
|
document.getElementById('studentContact').value = student.contact_info || '';
|
|
this.studentModal.show();
|
|
}
|
|
|
|
async saveStudent() {
|
|
const form = document.getElementById('studentForm');
|
|
const formData = new FormData(form);
|
|
const data = Object.fromEntries(formData.entries());
|
|
const isEdit = data.isEdit === 'true';
|
|
|
|
// Validation
|
|
if (!data.id || !data.name || !data.class) {
|
|
alert('学号、姓名和班级为必填项');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let res;
|
|
if (isEdit) {
|
|
res = await fetch(`${this.apiBase}/students/${data.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
} else {
|
|
res = await fetch(`${this.apiBase}/students`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
}
|
|
|
|
const result = await res.json();
|
|
|
|
if (result.success) {
|
|
alert(isEdit ? '更新成功' : '创建成功');
|
|
this.studentModal.hide();
|
|
this.loadStudents();
|
|
} else {
|
|
alert(result.message || '操作失败');
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert('系统错误');
|
|
}
|
|
}
|
|
|
|
async deleteStudent(id) {
|
|
try {
|
|
const res = await fetch(`${this.apiBase}/students/${id}`, { method: 'DELETE' });
|
|
const result = await res.json();
|
|
|
|
if (result.success) {
|
|
alert('删除成功');
|
|
this.loadStudents();
|
|
} else {
|
|
alert(result.message || '删除失败');
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert('系统错误');
|
|
}
|
|
}
|
|
|
|
// ================= Teacher Management =================
|
|
async initTeacherManagement() {
|
|
this.teacherCurrentPage = 1;
|
|
this.teacherPageSize = 10;
|
|
|
|
// Bind Filter Events
|
|
document.getElementById('teacherSearch').addEventListener('input', () => this.loadTeachers());
|
|
|
|
// Bind Modal Events
|
|
const modalEl = document.getElementById('teacherModal');
|
|
this.teacherModal = new bootstrap.Modal(modalEl);
|
|
|
|
document.getElementById('addTeacherBtn').addEventListener('click', () => {
|
|
document.getElementById('teacherForm').reset();
|
|
document.getElementById('teacherModalTitle').textContent = '新增教师';
|
|
document.getElementById('teacherIsEdit').value = 'false';
|
|
document.getElementById('teacherId').readOnly = false;
|
|
this.teacherModal.show();
|
|
});
|
|
|
|
document.getElementById('saveTeacherBtn').addEventListener('click', () => this.saveTeacher());
|
|
|
|
// Initial Load
|
|
await this.loadTeachers();
|
|
}
|
|
|
|
async loadTeachers() {
|
|
const search = document.getElementById('teacherSearch').value;
|
|
const tbody = document.getElementById('teacherTableBody');
|
|
|
|
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-5">加载中...</td></tr>';
|
|
|
|
try {
|
|
const query = new URLSearchParams({
|
|
page: this.teacherCurrentPage,
|
|
limit: this.teacherPageSize,
|
|
search
|
|
});
|
|
|
|
const res = await fetch(`${this.apiBase}/teachers?${query}`);
|
|
const result = await res.json();
|
|
|
|
if (result.success) {
|
|
this.renderTeacherTable(result.data, result.pagination);
|
|
} else {
|
|
tbody.innerHTML = `<tr><td colspan="6" class="text-center text-danger">${result.message}</td></tr>`;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-danger">加载失败</td></tr>';
|
|
}
|
|
}
|
|
|
|
renderTeacherTable(teachers, pagination) {
|
|
const tbody = document.getElementById('teacherTableBody');
|
|
|
|
if (teachers.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-5 text-muted">暂无教师</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = teachers.map(t => `
|
|
<tr>
|
|
<td class="ps-4 fw-bold">${t.id}</td>
|
|
<td>${t.name}</td>
|
|
<td>${t.title || '-'}</td>
|
|
<td>${t.department || '-'}</td>
|
|
<td>${t.contact_info || '-'}</td>
|
|
<td class="text-end pe-4">
|
|
<button class="btn btn-sm btn-outline-primary me-1 btn-edit-teacher" data-teacher='${JSON.stringify(t).replace(/'/g, "'")}'>
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger btn-delete-teacher" data-id="${t.id}">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
// Bind Action Buttons
|
|
document.querySelectorAll('.btn-edit-teacher').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const teacher = JSON.parse(btn.dataset.teacher);
|
|
this.openEditTeacherModal(teacher);
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.btn-delete-teacher').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
if(confirm('确定要删除该教师吗?这将同时删除其用户账号。')) {
|
|
this.deleteTeacher(btn.dataset.id);
|
|
}
|
|
});
|
|
});
|
|
|
|
this.renderTeacherPagination(pagination);
|
|
}
|
|
|
|
renderTeacherPagination(pagination) {
|
|
this.renderGenericPagination('teacherPagination', pagination, 'changeTeacherPage');
|
|
}
|
|
|
|
changeTeacherPage(page) {
|
|
this.teacherCurrentPage = page;
|
|
this.loadTeachers();
|
|
}
|
|
|
|
openEditTeacherModal(teacher) {
|
|
document.getElementById('teacherForm').reset();
|
|
document.getElementById('teacherModalTitle').textContent = '编辑教师';
|
|
document.getElementById('teacherIsEdit').value = 'true';
|
|
document.getElementById('teacherId').value = teacher.id;
|
|
document.getElementById('teacherId').readOnly = true;
|
|
document.getElementById('teacherNameInput').value = teacher.name;
|
|
document.getElementById('teacherDepartment').value = teacher.department || '';
|
|
document.getElementById('teacherTitle').value = teacher.title || '';
|
|
document.getElementById('teacherContact').value = teacher.contact_info || '';
|
|
this.teacherModal.show();
|
|
}
|
|
|
|
async saveTeacher() {
|
|
const form = document.getElementById('teacherForm');
|
|
const formData = new FormData(form);
|
|
const data = Object.fromEntries(formData.entries());
|
|
const isEdit = data.isEdit === 'true';
|
|
|
|
// Validation
|
|
if (!data.id || !data.name) {
|
|
alert('工号和姓名为必填项');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let res;
|
|
if (isEdit) {
|
|
res = await fetch(`${this.apiBase}/teachers/${data.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
} else {
|
|
res = await fetch(`${this.apiBase}/teachers`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
}
|
|
|
|
const result = await res.json();
|
|
|
|
if (result.success) {
|
|
alert(isEdit ? '更新成功' : '创建成功');
|
|
this.teacherModal.hide();
|
|
this.loadTeachers();
|
|
} else {
|
|
alert(result.message || '操作失败');
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert('系统错误');
|
|
}
|
|
}
|
|
|
|
async deleteTeacher(id) {
|
|
try {
|
|
const res = await fetch(`${this.apiBase}/teachers/${id}`, { method: 'DELETE' });
|
|
const result = await res.json();
|
|
|
|
if (result.success) {
|
|
alert('删除成功');
|
|
this.loadTeachers();
|
|
} else {
|
|
alert(result.message || '删除失败');
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert('系统错误');
|
|
}
|
|
}
|
|
|
|
// ================= Grade Statistics =================
|
|
async initGradeStatistics() {
|
|
try {
|
|
const res = await fetch(`${this.apiBase}/grade-stats`);
|
|
const result = await res.json();
|
|
|
|
if (result.success) {
|
|
const stats = result.data;
|
|
this.renderGradeStatsTable(stats);
|
|
this.renderGradeCharts(stats);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
renderGradeStatsTable(stats) {
|
|
const tbody = document.getElementById('gradeStatsBody');
|
|
if (!tbody) return;
|
|
|
|
if (stats.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-5 text-muted">暂无数据</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = stats.map(s => `
|
|
<tr>
|
|
<td class="ps-4 fw-bold">${s.course_code || '-'}</td>
|
|
<td>${s.course_name}</td>
|
|
<td>${s.teacher_name || '未分配'}</td>
|
|
<td>${s.student_count}</td>
|
|
<td><span class="fw-bold text-primary">${s.avg_score}</span></td>
|
|
<td>${s.max_score || '-'}</td>
|
|
<td>${s.min_score || '-'}</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="progress flex-grow-1 me-2" style="height: 6px;">
|
|
<div class="progress-bar bg-${s.pass_rate >= 60 ? 'success' : 'danger'}" style="width: ${s.pass_rate}%"></div>
|
|
</div>
|
|
<span class="small">${s.pass_rate}%</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
renderGradeCharts(stats) {
|
|
if (!window.Chart) return;
|
|
|
|
// 1. Course Average Scores Bar Chart
|
|
const ctxBar = document.getElementById('courseAvgChart');
|
|
if (ctxBar) {
|
|
new Chart(ctxBar, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: stats.map(s => s.course_name),
|
|
datasets: [{
|
|
label: '平均分',
|
|
data: stats.map(s => s.avg_score),
|
|
backgroundColor: 'rgba(78, 115, 223, 0.5)',
|
|
borderColor: 'rgba(78, 115, 223, 1)',
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
scales: {
|
|
y: { beginAtZero: true, max: 100 }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 2. Overall Pass Rate Pie Chart
|
|
const ctxPie = document.getElementById('passRateChart');
|
|
if (ctxPie) {
|
|
const totalStudents = stats.reduce((sum, s) => sum + s.student_count, 0);
|
|
const totalPass = stats.reduce((sum, s) => sum + parseInt(s.pass_count), 0);
|
|
const totalFail = totalStudents - totalPass;
|
|
|
|
new Chart(ctxPie, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: ['及格', '不及格'],
|
|
datasets: [{
|
|
data: [totalPass, totalFail],
|
|
backgroundColor: ['#1cc88a', '#e74a3b'],
|
|
hoverOffset: 4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: { position: 'bottom' }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ================= System Settings =================
|
|
async initSystemSettings() {
|
|
const form = document.getElementById('basicSettingsForm');
|
|
if (!form) return;
|
|
|
|
// Load current settings
|
|
try {
|
|
const response = await fetch('/api/admin/settings');
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
const settings = result.data;
|
|
const nameInput = form.querySelector('input[type="text"]');
|
|
const semesterSelect = form.querySelector('select');
|
|
const courseSwitch = document.getElementById('courseSelectionSwitch');
|
|
const gradeSwitch = document.getElementById('gradeCheckSwitch');
|
|
|
|
if (nameInput) nameInput.value = settings.system_name || '';
|
|
if (semesterSelect) semesterSelect.value = settings.current_semester || '';
|
|
if (courseSwitch) courseSwitch.checked = settings.allow_course_selection === '1';
|
|
if (gradeSwitch) gradeSwitch.checked = settings.allow_grade_check === '1';
|
|
}
|
|
} catch (err) {
|
|
console.error('加载设置失败:', err);
|
|
}
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const settings = {
|
|
system_name: form.querySelector('input[type="text"]').value,
|
|
current_semester: form.querySelector('select').value,
|
|
allow_course_selection: document.getElementById('courseSelectionSwitch').checked ? '1' : '0',
|
|
allow_grade_check: document.getElementById('gradeCheckSwitch').checked ? '1' : '0'
|
|
};
|
|
|
|
try {
|
|
const response = await fetch('/api/admin/settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(settings)
|
|
});
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
alert('系统设置已保存');
|
|
} else {
|
|
alert('保存失败: ' + result.message);
|
|
}
|
|
} catch (err) {
|
|
alert('保存出错');
|
|
}
|
|
});
|
|
|
|
// Data Maintenance Buttons
|
|
const backupBtn = document.querySelector('.btn-outline-primary i.fa-database')?.parentElement;
|
|
const clearCacheBtn = document.querySelector('.btn-outline-warning i.fa-trash-alt')?.parentElement;
|
|
const resetPassBtn = document.querySelector('.btn-outline-danger i.fa-history')?.parentElement;
|
|
|
|
if (backupBtn) {
|
|
backupBtn.addEventListener('click', async () => {
|
|
if (!confirm('确定要立即备份数据库吗?')) return;
|
|
try {
|
|
const res = await fetch('/api/admin/maintenance/backup', { method: 'POST' });
|
|
const result = await res.json();
|
|
if (result.success) alert('备份成功!文件已保存到 backups 目录: ' + result.data.filename);
|
|
else alert('备份失败: ' + result.message);
|
|
} catch (err) { alert('请求失败'); }
|
|
});
|
|
}
|
|
|
|
if (clearCacheBtn) {
|
|
clearCacheBtn.addEventListener('click', async () => {
|
|
try {
|
|
const res = await fetch('/api/admin/maintenance/clear-cache', { method: 'POST' });
|
|
const result = await res.json();
|
|
if (result.success) alert('系统缓存已清理');
|
|
else alert('清理失败');
|
|
} catch (err) { alert('请求失败'); }
|
|
});
|
|
}
|
|
|
|
if (resetPassBtn) {
|
|
resetPassBtn.addEventListener('click', async () => {
|
|
if (!confirm('确定要重置所有学生密码为 123456 吗?此操作不可撤销!')) return;
|
|
try {
|
|
const res = await fetch('/api/admin/maintenance/reset-passwords', { method: 'POST' });
|
|
const result = await res.json();
|
|
if (result.success) alert('所有学生密码已重置为 123456');
|
|
else alert('重置失败');
|
|
} catch (err) { alert('请求失败'); }
|
|
});
|
|
}
|
|
}
|
|
|
|
// ================= Data Export =================
|
|
async initDataExport() {
|
|
const studentExportBtn = document.getElementById('exportStudentsBtn');
|
|
const teacherExportBtn = document.getElementById('exportTeachersBtn');
|
|
const gradeExportBtn = document.getElementById('exportGradesBtn');
|
|
|
|
if (studentExportBtn) {
|
|
studentExportBtn.addEventListener('click', () => {
|
|
window.location.href = '/api/admin/export/students';
|
|
});
|
|
}
|
|
|
|
if (teacherExportBtn) {
|
|
teacherExportBtn.addEventListener('click', () => {
|
|
window.location.href = '/api/admin/export/teachers';
|
|
});
|
|
}
|
|
|
|
if (gradeExportBtn) {
|
|
gradeExportBtn.addEventListener('click', () => {
|
|
window.location.href = '/api/admin/export/grades';
|
|
});
|
|
}
|
|
}
|
|
|
|
// ================= Operation Logs =================
|
|
async initOperationLogs() {
|
|
const tbody = document.getElementById('logsTableBody');
|
|
if (!tbody) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/admin/logs');
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
const logs = result.data;
|
|
if (logs.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-4 text-muted">暂无操作日志</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = logs.map(log => `
|
|
<tr>
|
|
<td class="ps-4 text-muted">${log.created_at}</td>
|
|
<td class="fw-bold">${log.user_id} (${log.user_name || '未知'})</td>
|
|
<td><span class="badge bg-secondary">${log.operation_type}</span></td>
|
|
<td>${log.description}</td>
|
|
<td class="text-muted small">${log.ip_address || '-'}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
} catch (err) {
|
|
console.error('获取日志失败:', err);
|
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-4 text-danger">加载失败</td></tr>';
|
|
}
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.adminManager = new AdminManager();
|
|
});
|