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

407 lines
14 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 TeacherDashboard {
constructor() {
// 动æ€<C3A6>设置API基础URL,支æŒ<C3A6>file:///å<><C3A5>è®®åŒlocalhost:3000访问
this.apiBase = window.location.protocol === 'file:' ? 'http://localhost:3000/api' : '/api';
this.currentUser = null;
this.courses = [];
this.grades = [];
this.init();
}
async init() {
// 检查登录状� if (!await this.checkAuth()) {
window.location.href = '/login';
return;
}
// 加载用户信æ<C2A1>¯
await this.loadUserInfo();
// åŠ è½½è¯¾ç¨æ•°æ<C2B0>®
await this.loadCourses();
// 加载æˆ<C3A6>绩数æ<C2B0>®
await this.loadGrades();
// 绑定事件
this.bindEvents();
// æ´æ°ç•Œé<C592>¢
this.updateUI();
}
async checkAuth() {
try {
const response = await fetch(`${this.apiBase}/auth/check`, {
credentials: 'include'
});
if (!response.ok) {
return false;
}
const data = await response.json();
return data.success && data.user.role === 'teacher';
} 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 loadCourses() {
try {
const response = await fetch(`${this.apiBase}/teacher/courses`, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.success) {
this.courses = data.courses;
this.populateCourseSelectors();
}
}
} catch (error) {
console.error('加载课程失败:', error);
this.showNotification('加载课程失败', 'error');
}
}
async loadGrades(filters = {}) {
try {
const queryParams = new URLSearchParams(filters).toString();
const url = `${this.apiBase}/teacher/grades${queryParams ? '?' + queryParams : ''}`;
const response = await fetch(url, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.success) {
this.grades = data.grades;
this.renderGradesTable();
}
}
} catch (error) {
console.error('加载æˆ<C3A6>绩失败:', error);
this.showNotification('加载æˆ<C3A6>绩失败', 'error');
}
}
populateCourseSelectors() {
// 填充课程选择� const courseSelectors = document.querySelectorAll('.course-selector');
courseSelectors.forEach(select => {
select.innerHTML = '<option value="">请选择课程</option>';
this.courses.forEach(course => {
const option = document.createElement('option');
option.value = course.id;
option.textContent = `${course.course_code} - ${course.course_name}`;
select.appendChild(option);
});
});
}
renderGradesTable() {
const tableBody = document.getElementById('gradesTableBody');
if (!tableBody) return;
if (this.grades.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="9" class="text-center">
<div class="no-data">
<i class="fas fa-info-circle"></i>
<p>æšæ— æˆ<C3A6>绩数æ<C2B0>®</p>
</div>
</td>
</tr>
`;
return;
}
tableBody.innerHTML = this.grades.map(grade => {
const gradeClass = this.getGradeClass(grade.score);
return `
<tr>
<td><input type="checkbox" class="grade-checkbox" data-id="${grade.id}"></td>
<td>${grade.student_id}</td>
<td>${grade.full_name}</td>
<td>${grade.class_name}</td>
<td>${grade.course_code}</td>
<td>${grade.course_name}</td>
<td class="grade-cell ${gradeClass}">
<span class="grade-score">${grade.score}</span>
<span class="grade-level">${grade.grade_level}</span>
</td>
<td>${grade.exam_date ? new Date(grade.exam_date).toLocaleDateString() : '未设�}</td>
<td>
<div class="action-buttons">
<button class="btn-edit" data-id="${grade.id}">
<i class="fas fa-edit"></i> 编辑
</button>
<button class="btn-delete" data-id="${grade.id}">
<i class="fas fa-trash"></i> 删除
</button>
</div>
</td>
</tr>
`;
}).join('');
// æ´æ°ç»Ÿè®¡ä¿¡æ<C2A1>¯
this.updateStats();
}
getGradeClass(score) {
if (score >= 90) return 'grade-excellent';
if (score >= 80) return 'grade-good';
if (score >= 70) return 'grade-medium';
if (score >= 60) return 'grade-pass';
return 'grade-fail';
}
updateStats() {
if (this.grades.length === 0) return;
const totalStudents = new Set(this.grades.map(g => g.student_id)).size;
const avgScore = this.grades.reduce((sum, g) => sum + g.score, 0) / this.grades.length;
const passRate = (this.grades.filter(g => g.score >= 60).length / this.grades.length * 100).toFixed(1);
document.getElementById('totalStudents').textContent = totalStudents;
document.getElementById('avgScore').textContent = avgScore.toFixed(1);
document.getElementById('passRate').textContent = `${passRate}%`;
}
bindEvents() {
// æ<>œç´¢æŒ‰é®
document.getElementById('searchBtn')?.addEventListener('click', () => {
this.handleSearch();
});
// é‡<C3A9>置按é®
document.getElementById('resetBtn')?.addEventListener('click', () => {
this.resetFilters();
});
// 导出按钮
document.getElementById('exportBtn')?.addEventListener('click', () => {
this.exportGrades();
});
// 批é‡<C3A9>删除按é®
document.getElementById('batchDeleteBtn')?.addEventListener('click', () => {
this.batchDeleteGrades();
});
// 表格æ“<C3A6>作按é®äºä»¶å§”托
document.addEventListener('click', (e) => {
if (e.target.closest('.btn-edit')) {
const gradeId = e.target.closest('.btn-edit').dataset.id;
this.editGrade(gradeId);
}
if (e.target.closest('.btn-delete')) {
const gradeId = e.target.closest('.btn-delete').dataset.id;
this.deleteGrade(gradeId);
}
});
// 退出登� document.getElementById('logoutBtn')?.addEventListener('click', () => {
this.handleLogout();
});
}
handleSearch() {
const className = document.getElementById('classFilter')?.value || '';
const courseId = document.getElementById('courseFilter')?.value || '';
this.loadGrades({ class_name: className, course_id: courseId });
}
resetFilters() {
document.getElementById('classFilter').value = '';
document.getElementById('courseFilter').value = '';
this.loadGrades();
}
async exportGrades() {
try {
const response = await fetch(`${this.apiBase}/teacher/grades/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 = `æˆ<C3A6>绩报表_${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 batchDeleteGrades() {
const checkboxes = document.querySelectorAll('.grade-checkbox:checked');
if (checkboxes.length === 0) {
this.showNotification('è¯·éæ©è¦<EFBFBD>删é¤çšæˆ<EFBFBD>绩', 'warning');
return;
}
if (!confirm(`确定è¦<C3A8>删除选中çš?${checkboxes.length} æ<>¡æˆ<C3A6>绩记录å<E280A2>—?`)) {
return;
}
const gradeIds = Array.from(checkboxes).map(cb => cb.dataset.id);
try {
const response = await fetch(`${this.apiBase}/teacher/grades/batch`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ gradeIds })
});
const data = await response.json();
if (data.success) {
this.showNotification(`æˆ<C3A6>功删除 ${gradeIds.length} æ<>¡æˆ<C3A6>绩记录`, 'success');
await this.loadGrades();
} else {
this.showNotification(data.message || '删é¤å¤±è´¥', 'error');
}
} catch (error) {
console.error('æ¹é<EFBFBD>删é¤å¤±è´¥:', error);
this.showNotification('æ¹é<EFBFBD>删é¤å¤±è´¥', 'error');
}
}
async editGrade(gradeId) {
const grade = this.grades.find(g => g.id == gradeId);
if (!grade) return;
// 这里å<C592>¯ä»¥æ‰“å¼€ç¼è¾æ¨¡æ€<C3A6>框
// æšæ—¶ä½¿ç”¨ç®€å<E282AC>•æ<E280A2><C3A6>示框
const newScore = prompt('请è¾å¥æ°çšåˆæ?', grade.score);
if (newScore === null) return;
const numericScore = parseFloat(newScore);
if (isNaN(numericScore) || numericScore < 0 || numericScore > 100) {
this.showNotification('请è¾å?-100ä¹é´çšæœæˆåˆæ?, 'error');
return;
}
try {
const response = await fetch(`${this.apiBase}/teacher/grades/${gradeId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
score: numericScore,
examDate: grade.exam_date,
remark: grade.remark
})
});
const data = await response.json();
if (data.success) {
this.showNotification('æˆ<C3A6>ç»©æ´æ°æˆ<C3A6>功', 'success');
await this.loadGrades();
} else {
this.showNotification(data.message || '更新失败', 'error');
}
} catch (error) {
console.error('æ´æ°æˆ<C3A6>绩失败:', error);
this.showNotification('æ´æ°æˆ<C3A6>绩失败', 'error');
}
}
async deleteGrade(gradeId) {
if (!confirm('确定è¦<C3A8>删除这æ<E284A2>¡æˆ<C3A6>绩记录å<E280A2>—ï¼?)) {
return;
}
try {
const response = await fetch(`${this.apiBase}/teacher/grades/${gradeId}`, {
method: 'DELETE',
credentials: 'include'
});
const data = await response.json();
if (data.success) {
this.showNotification('æˆ<C3A6>绩删除æˆ<C3A6>功', 'success');
await this.loadGrades();
} else {
this.showNotification(data.message || '删除失败', 'error');
}
} catch (error) {
console.error('删除æˆ<C3A6>绩失败:', error);
this.showNotification('删除æˆ<C3A6>绩失败', '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);
}
}
updateUI() {
// æ´æ°ç”¨æˆ·ä¿¡æ<C2A1>¯
if (this.currentUser) {
const userInfoElements = document.querySelectorAll('.user-info');
userInfoElements.forEach(el => {
el.textContent = `${this.currentUser.full_name} (${this.currentUser.role})`;
});
}
}
showNotification(message, type = 'info') {
// 使用AuthManager的通知系统æˆç®€å<E282AC>•alert
if (typeof AuthManager !== 'undefined' && AuthManager.showNotification) {
AuthManager.showNotification(message, type);
} else {
alert(`${type}: ${message}`);
}
}
}
// 页é<C2B5>¢åŠ è½½å®Œæˆ<C3A6>å<EFBFBD>Žåˆ<C3A5>å§åŒ
document.addEventListener('DOMContentLoaded', () => {
if (window.location.pathname.includes('/teacher/')) {
new TeacherDashboard();
}
});