Files
WebWork/frontend/public/js/teacher.js
祀梦 b1da021185 feat: 实现教师资料更新、操作日志和系统设置功能
新增教师资料更新功能,包括个人信息修改和密码更新
添加操作日志记录系统,记录用户关键操作
实现系统设置模块,支持动态配置系统参数
重构数据库模型,新增教师表和系统设置表
优化成绩录入逻辑,支持平时分、期中和期末成绩计算
添加数据导出功能,支持学生、教师和成绩数据导出
完善管理员后台,增加统计图表和操作日志查看
2025-12-22 23:30:01 +08:00

613 lines
26 KiB
JavaScript

/**
* 教师端功能管理
*/
class TeacherManager {
constructor() {
this.apiBase = '/api/teacher';
this.init();
}
async init() {
this.updateCurrentTime();
setInterval(() => this.updateCurrentTime(), 1000);
await this.loadUserInfo();
// 页面路由逻辑
if (document.getElementById('courseList')) {
this.initDashboard();
}
if (document.getElementById('studentTableBody') && document.getElementById('courseSelect')) {
this.initGradeEntry();
}
if (document.getElementById('gradeTableBody')) {
this.initGradeManagement();
}
if (document.getElementById('profileForm')) {
this.initProfilePage();
}
}
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;
// Update Sidebar
const nameEls = document.querySelectorAll('#teacherName, #userName, #profileName');
const idEls = document.querySelectorAll('#teacherId, #profileId, #inputTeacherId');
nameEls.forEach(el => el.textContent = user.name);
idEls.forEach(el => {
if (el.tagName === 'INPUT') el.value = user.id;
else el.textContent = user.id;
});
// Profile Page Specific
const inputName = document.getElementById('inputName');
const inputClass = document.getElementById('inputClass');
if (inputName) inputName.value = user.name;
if (inputClass) inputClass.value = user.class || '';
}
} catch (error) {
console.error('Load user info failed:', error);
}
}
// ================= Dashboard =================
async initDashboard() {
await Promise.all([
this.loadCourses(),
this.loadManagedClasses()
]);
this.initAddCourse();
this.initEditCourse();
}
async loadManagedClasses() {
const managedClassesSection = document.getElementById('managedClassesSection');
const managedClassList = document.getElementById('managedClassList');
const classCountEl = document.getElementById('classCount');
if (!managedClassList) return;
try {
const res = await fetch(`${this.apiBase}/my-classes`);
const result = await res.json();
if (result.success && result.data.classes.length > 0) {
const classes = result.data.classes;
classCountEl.textContent = classes.length;
managedClassesSection.style.display = 'block';
managedClassList.innerHTML = classes.map(c => `
<div class="col-md-6 col-xl-4 mb-3">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body d-flex align-items-center">
<div class="bg-info bg-opacity-10 text-info rounded p-3 me-3">
<i class="fas fa-users fa-lg"></i>
</div>
<div>
<h6 class="mb-1 fw-bold">${c.class_name}</h6>
<p class="text-muted small mb-0">${c.major || '专业未设置'} | ${c.grade}级</p>
</div>
</div>
</div>
</div>
`).join('');
} else {
classCountEl.textContent = '0';
managedClassesSection.style.display = 'none';
}
} catch (e) {
console.error('Load managed classes failed', e);
}
}
async loadCourses() {
try {
const response = await fetch(`${this.apiBase}/courses`);
const result = await response.json();
if (result.success) {
this.courses = result.data.courses;
this.renderDashboard(this.courses);
}
} catch (error) {
console.error('Fetch courses failed:', error);
}
}
renderDashboard(courses) {
const courseList = document.getElementById('courseList');
if (!courseList) return;
if (!courses || courses.length === 0) {
courseList.innerHTML = '<div class="col-12 text-center py-5 text-muted">暂无负责课程</div>';
return;
}
courseList.innerHTML = courses.map(course => `
<div class="col-md-6 col-xl-4 mb-4">
<div class="card h-100 border-0 shadow-sm course-card">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-start mb-3">
<span class="badge bg-primary bg-opacity-10 text-primary">
<i class="fas fa-book me-1"></i> ${course.course_code || 'CODE'}
</span>
<div class="d-flex gap-2">
<button class="btn btn-link text-secondary p-0 btn-edit-course"
data-id="${course.id}">
<i class="fas fa-edit"></i>
</button>
<span class="text-muted small">${course.credit} 学分</span>
</div>
</div>
<h5 class="card-title fw-bold mb-2">${course.course_name}</h5>
<p class="card-text text-secondary small mb-4">
<i class="fas fa-users me-1"></i> ${course.class_name || '班级未指定'}
</p>
<div class="d-grid gap-2">
<a href="/teacher/grade_entry?courseId=${course.id}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-edit me-1"></i> 成绩录入
</a>
<a href="/teacher/grade_management?courseId=${course.id}" class="btn btn-primary btn-sm">
<i class="fas fa-tasks me-1"></i> 成绩管理
</a>
</div>
</div>
</div>
</div>
`).join('');
document.getElementById('courseCount').textContent = courses.length;
// Calculate total students
const totalStudents = courses.reduce((sum, c) => sum + (c.student_count || 0), 0);
document.getElementById('totalStudents').textContent = totalStudents;
}
initAddCourse() {
const btn = document.getElementById('addCourseBtn');
const modalEl = document.getElementById('addCourseModal');
const saveBtn = document.getElementById('saveCourseBtn');
if (!btn || !modalEl) return;
const modal = new bootstrap.Modal(modalEl);
btn.addEventListener('click', async () => {
// Load classes
try {
const res = await fetch(`${this.apiBase}/classes`);
const result = await res.json();
if (result.success) {
const select = document.getElementById('classSelect');
select.innerHTML = '<option value="">请选择班级...</option>' +
result.data.classes.map(c => `<option value="${c.id}">${c.class_name}</option>`).join('');
}
} catch (e) {
console.error('Load classes failed', e);
}
modal.show();
});
saveBtn.addEventListener('click', async () => {
const form = document.getElementById('addCourseForm');
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
try {
const res = await fetch(`${this.apiBase}/courses`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await res.json();
if (result.success) {
alert('课程创建成功');
modal.hide();
this.loadCourses();
form.reset();
} else {
alert(result.message || '创建失败');
}
} catch (e) {
console.error('Create course failed', e);
alert('系统错误');
}
});
}
initEditCourse() {
const modalEl = document.getElementById('editCourseModal');
if (!modalEl) return;
const modal = new bootstrap.Modal(modalEl);
const updateBtn = document.getElementById('updateCourseBtn');
// Use event delegation for dynamically created edit buttons
document.addEventListener('click', async (e) => {
const btn = e.target.closest('.btn-edit-course');
if (!btn) return;
const courseId = btn.dataset.id;
if (!this.courses) return;
const courseData = this.courses.find(c => c.id == courseId);
if (!courseData) return;
// Fill form
document.getElementById('editCourseId').value = courseData.id;
document.getElementById('editCourseName').value = courseData.course_name;
document.getElementById('editCourseCode').value = courseData.course_code;
document.getElementById('editCourseCredit').value = courseData.credit;
document.getElementById('editSemester').value = courseData.semester;
// Load classes and select current one
try {
const res = await fetch(`${this.apiBase}/classes`);
const result = await res.json();
if (result.success) {
const select = document.getElementById('editClassSelect');
select.innerHTML = '<option value="">请选择班级...</option>' +
result.data.classes.map(c => `<option value="${c.id}" ${c.id == courseData.class_id ? 'selected' : ''}>${c.class_name}</option>`).join('');
}
} catch (e) {
console.error('Load classes failed', e);
}
modal.show();
});
updateBtn.addEventListener('click', async () => {
const form = document.getElementById('editCourseForm');
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
const courseId = data.id;
try {
const res = await fetch(`${this.apiBase}/courses/${courseId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await res.json();
if (result.success) {
alert('课程更新成功');
modal.hide();
this.loadCourses();
} else {
alert(result.message || '更新失败');
}
} catch (e) {
console.error('Update course failed', e);
alert('系统错误');
}
});
}
// ================= Grade Entry =================
async initGradeEntry() {
const courseSelect = document.getElementById('courseSelect');
// Load Courses for Select
try {
const response = await fetch(`${this.apiBase}/courses`);
const result = await response.json();
if (result.success) {
courseSelect.innerHTML = '<option value="">请选择课程...</option>' +
result.data.courses.map(c => `<option value="${c.id}">${c.course_name} (${c.class_name || '未指定班级'})</option>`).join('');
// Check URL params
const params = new URLSearchParams(window.location.search);
const courseId = params.get('courseId');
if (courseId) {
courseSelect.value = courseId;
this.loadStudentsForGradeEntry(courseId);
}
}
} catch (e) { console.error(e); }
courseSelect.addEventListener('change', (e) => {
if (e.target.value) {
this.loadStudentsForGradeEntry(e.target.value);
} else {
document.getElementById('studentTableBody').innerHTML = '<tr><td colspan="7" class="text-center py-5 text-muted">请选择课程以加载学生列表</td></tr>';
}
});
}
async loadStudentsForGradeEntry(courseId) {
const tbody = document.getElementById('studentTableBody');
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5"><div class="spinner-border text-primary" role="status"></div><div class="mt-2">加载中...</div></td></tr>';
// Since we don't have a direct "get students by course" API that returns grades yet,
// we might need to rely on what we have.
// Actually, we need to fetch students enrolled in the class of this course.
// AND fetch their existing grades for this course.
// For simplicity, let's assume we have an endpoint or we modify `getGradeStatistics` or similar?
// No, we should probably add `GET /api/teacher/course/:id/students`?
// Or just use `GET /api/teacher/grades?courseId=X`.
// Let's assume `GET /api/teacher/grades?courseId=X` returns the list of students with their grades (or null if no grade).
// I need to implement this backend logic if it's missing.
// For now, I'll simulate or try to use what I have.
// I'll add `getCourseGrades` to TeacherController.
// Wait, I can't modify backend endlessly.
// Let's check `TeacherController.js` again.
// It has `addScore`.
// It does NOT have `getCourseGrades`.
// I need to add it to support this view.
try {
// Placeholder: I will add this endpoint in next step.
const res = await fetch(`${this.apiBase}/grades?courseId=${courseId}`);
const result = await res.json();
if (result.success) {
this.renderGradeEntryTable(result.data.grades);
} else {
tbody.innerHTML = `<tr><td colspan="7" class="text-center text-danger">${result.message}</td></tr>`;
}
} catch (e) {
tbody.innerHTML = `<tr><td colspan="7" class="text-center text-danger">加载失败: ${e.message}</td></tr>`;
}
}
renderGradeEntryTable(grades) {
const tbody = document.getElementById('studentTableBody');
if (!grades || grades.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5 text-muted">该课程暂无学生</td></tr>';
return;
}
tbody.innerHTML = grades.map(g => `
<tr>
<td>${g.student_id}</td>
<td>${g.student_name}</td>
<td><input type="number" class="form-control form-control-sm" value="${g.usual_score || ''}" placeholder="平时"></td>
<td><input type="number" class="form-control form-control-sm" value="${g.midterm_score || ''}" placeholder="期中"></td>
<td><input type="number" class="form-control form-control-sm" value="${g.final_score || ''}" placeholder="期末"></td>
<td>${g.total_score || '-'}</td>
<td>
<button class="btn btn-sm btn-primary save-grade-btn" data-student-id="${g.student_id}">
<i class="fas fa-save"></i> 保存
</button>
</td>
</tr>
`).join('');
// Bind save events
document.querySelectorAll('.save-grade-btn').forEach(btn => {
btn.addEventListener('click', (e) => this.saveGrade(e.target.closest('tr'), btn.dataset.studentId));
});
}
async saveGrade(row, studentId) {
const inputs = row.querySelectorAll('input');
const usual = inputs[0].value;
const midterm = inputs[1].value;
const final = inputs[2].value;
const courseId = document.getElementById('courseSelect').value;
// Simple calculation for total score preview (Backend should do the real one)
const total = (usual * 0.3 + midterm * 0.3 + final * 0.4).toFixed(1); // Example weights
try {
const res = await fetch(`${this.apiBase}/grades`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
studentId,
courseId,
usual_score: usual,
midterm_score: midterm,
final_score: final,
score: total // Passing total score as 'score' for compatibility with existing API
})
});
const result = await res.json();
if (result.success) {
// Update total cell
row.cells[5].textContent = total;
// Show success feedback
const btn = row.querySelector('.save-grade-btn');
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i> 已保存';
btn.classList.remove('btn-primary');
btn.classList.add('btn-success');
setTimeout(() => {
btn.innerHTML = originalText;
btn.classList.add('btn-primary');
btn.classList.remove('btn-success');
}, 2000);
} else {
alert(result.message || '保存失败');
}
} catch (e) {
console.error(e);
alert('保存失败');
}
}
// ================= Grade Management =================
async initGradeManagement() {
// Similar logic to Grade Entry but Read-Only or Filter focused
// Fetch courses for filter
try {
const response = await fetch(`${this.apiBase}/courses`);
const result = await response.json();
if (result.success) {
const select = document.getElementById('courseSelectFilter');
select.innerHTML = '<option value="">全部课程</option>' +
result.data.courses.map(c => `<option value="${c.id}">${c.course_name}</option>`).join('');
}
} catch (e) { console.error(e); }
document.getElementById('searchBtn').addEventListener('click', () => this.searchGrades());
// Check URL params
const params = new URLSearchParams(window.location.search);
if (params.get('courseId')) {
document.getElementById('courseSelectFilter').value = params.get('courseId');
this.searchGrades();
}
}
async searchGrades() {
const courseId = document.getElementById('courseSelectFilter').value;
const studentName = document.getElementById('studentNameFilter').value;
const tbody = document.getElementById('gradeTableBody');
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5"><div class="spinner-border text-primary"></div></td></tr>';
try {
let url = `${this.apiBase}/grades?`;
if (courseId) url += `courseId=${courseId}&`;
if (studentName) url += `studentName=${studentName}`;
const res = await fetch(url);
const result = await res.json();
if (result.success && result.data.grades) {
if (result.data.grades.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5 text-muted">未找到相关成绩记录</td></tr>';
return;
}
tbody.innerHTML = result.data.grades.map(g => `
<tr>
<td>${g.course_name || '-'}</td>
<td>${g.student_id || '-'}</td>
<td>${g.student_name || '-'}</td>
<td>${g.total_score || '-'}</td>
<td>${g.grade_point || '-'}</td>
<td><span class="badge bg-${this.getBadgeColor(g.grade_level)}">${g.grade_level || '-'}</span></td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="window.location.href='/teacher/grade_entry?courseId=${g.course_id}'">
<i class="fas fa-edit"></i> 修改
</button>
</td>
</tr>
`).join('');
}
} catch (e) {
tbody.innerHTML = `<tr><td colspan="7" class="text-center text-danger">查询失败</td></tr>`;
}
}
getBadgeColor(level) {
if (!level) return 'secondary';
if (level.startsWith('A')) return 'success';
if (level.startsWith('B')) return 'info';
if (level.startsWith('C')) return 'warning';
if (level.startsWith('F')) return 'danger';
return 'primary';
}
// ================= Profile =================
initProfilePage() {
const saveProfileBtn = document.getElementById('saveProfileBtn');
if (saveProfileBtn) {
saveProfileBtn.addEventListener('click', async () => {
const name = document.getElementById('inputName').value;
const className = document.getElementById('inputClass').value;
try {
const res = await fetch('/api/auth/update-profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, class: className })
});
const result = await res.json();
if (result.success) {
alert('资料更新成功');
// 更新侧边栏和顶栏
const nameEls = document.querySelectorAll('#teacherName, #userName, #profileName');
nameEls.forEach(el => el.textContent = name);
} else {
alert(result.message || '更新失败');
}
} catch (e) {
console.error('Update profile failed', e);
alert('系统错误');
}
});
}
const passwordForm = document.getElementById('passwordForm');
if (passwordForm) {
passwordForm.addEventListener('submit', async (e) => {
e.preventDefault();
const oldPassword = document.getElementById('oldPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
const errorEl = document.getElementById('passwordError');
// Hide previous error
errorEl.style.display = 'none';
// Basic validation
if (newPassword !== confirmPassword) {
errorEl.textContent = '两次输入的新密码不一致';
errorEl.style.display = 'block';
return;
}
if (newPassword.length < 6) {
errorEl.textContent = '新密码长度至少为 6 位';
errorEl.style.display = 'block';
return;
}
try {
const res = await fetch('/api/auth/update-password', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ oldPassword, newPassword })
});
const result = await res.json();
if (result.success) {
alert('密码修改成功,请重新登录');
// Logout and redirect
await fetch('/api/auth/logout', { method: 'POST' });
window.location.href = '/login';
} else {
errorEl.textContent = result.message || '修改失败';
errorEl.style.display = 'block';
}
} catch (err) {
console.error(err);
errorEl.textContent = '服务器错误,请稍后再试';
errorEl.style.display = 'block';
}
});
}
}
}
document.addEventListener('DOMContentLoaded', () => {
window.teacherManager = new TeacherManager();
});