- 后端从 Python FastAPI 重写为 Go Gin(端口 56789) - 多班级完全隔离 - 超级管理员独立登录 - 课代表作业管理、排行榜分项排行 - 角色加减分上下限可配置 - 家长改密功能(可开关) - 周度/月度重置功能 - MySQL 5.7 兼容 - 43轮代码审查+全部修复 - Apache 2.0 许可证
196 lines
7.0 KiB
JavaScript
196 lines
7.0 KiB
JavaScript
/**
|
|
* 多班级版班级管理系统 - 考勤管理页JS
|
|
*
|
|
* 开发者: Canglan
|
|
* 版权归属: Sea Network Technology Studio
|
|
*
|
|
* 版权所有 © Sea Network Technology Studio
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
let currentStatus = 'absent';
|
|
let studentsData = [];
|
|
let existingRecords = [];
|
|
|
|
// 考勤扣分配置映射(从后端配置注入)
|
|
const attendanceDeductionMap = {
|
|
absent: window.DEDUCTION_ATTENDANCE_ABSENT || 3,
|
|
late: window.DEDUCTION_ATTENDANCE_LATE || 1,
|
|
leave: window.DEDUCTION_ATTENDANCE_LEAVE || 0
|
|
};
|
|
|
|
// 初始化按钮文字
|
|
function initAttendanceButtons() {
|
|
const btnAbsent = document.getElementById('btnAbsent');
|
|
const btnLate = document.getElementById('btnLate');
|
|
const btnLeave = document.getElementById('btnLeave');
|
|
if (btnAbsent) btnAbsent.textContent = '缺勤(' + attendanceDeductionMap.absent + '分)';
|
|
if (btnLate) btnLate.textContent = '迟到(' + attendanceDeductionMap.late + '分)';
|
|
if (btnLeave) btnLeave.textContent = '请假(' + (attendanceDeductionMap.leave > 0 ? attendanceDeductionMap.leave + '分' : '不扣分') + ')';
|
|
if (attendanceDeductionMap.absent > 0) {
|
|
document.getElementById('customDeduction').value = attendanceDeductionMap.absent;
|
|
}
|
|
}
|
|
|
|
function selectStatus(btn) {
|
|
document.querySelectorAll('.status-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
currentStatus = btn.dataset.status;
|
|
const defaultDeduction = attendanceDeductionMap[currentStatus] || 0;
|
|
if (defaultDeduction > 0) {
|
|
document.getElementById('customDeduction').value = defaultDeduction;
|
|
} else {
|
|
document.getElementById('customDeduction').value = '';
|
|
}
|
|
}
|
|
|
|
async function loadStudents() {
|
|
const res = await apiGet('/api/admin/students', {page_size: 1000});
|
|
if (res && res.success) {
|
|
studentsData = res.data.students;
|
|
renderStudentGrid();
|
|
await loadExistingRecords();
|
|
} else {
|
|
document.getElementById('studentGrid').innerHTML = '<div style="text-align:center;padding:20px;color:#999;">加载学生列表失败</div>';
|
|
}
|
|
}
|
|
|
|
function renderStudentGrid() {
|
|
const currentSlot = document.getElementById('attendanceSlot').value;
|
|
let html = '';
|
|
studentsData.forEach(student => {
|
|
const hasRecord = existingRecords.some(r => r.student_id === student.student_id && r.slot === currentSlot);
|
|
html += `<div class="student-cell${hasRecord ? ' has-record' : ''}"
|
|
data-id="${student.student_id}"
|
|
data-name="${escapeHtml(student.name)}"
|
|
onclick="toggleStudent(this)">
|
|
<span class="student-cell-name">${escapeHtml(student.name)}</span>
|
|
<span class="student-cell-no">${escapeHtml(student.student_no)}</span>
|
|
</div>`;
|
|
});
|
|
if (studentsData.length === 0) {
|
|
html = '<div style="text-align:center;padding:20px;color:#999;width:100%;">暂无学生数据</div>';
|
|
}
|
|
document.getElementById('studentGrid').innerHTML = html;
|
|
}
|
|
|
|
function toggleStudent(cell) {
|
|
cell.classList.toggle('selected');
|
|
}
|
|
|
|
function selectAllStudents() {
|
|
document.querySelectorAll('.student-cell:not(.has-record)').forEach(cell => {
|
|
cell.classList.add('selected');
|
|
});
|
|
}
|
|
|
|
function deselectAllStudents() {
|
|
document.querySelectorAll('.student-cell').forEach(cell => {
|
|
cell.classList.remove('selected');
|
|
});
|
|
}
|
|
|
|
async function loadExistingRecords() {
|
|
const date = document.getElementById('attendanceDate').value;
|
|
const slot = document.getElementById('attendanceSlot').value;
|
|
const res = await apiGet('/api/admin/attendance/records', { date, slot });
|
|
if (res && res.success) {
|
|
existingRecords = res.data.records || [];
|
|
renderStudentGrid();
|
|
}
|
|
}
|
|
|
|
async function submitAttendance() {
|
|
const selectedCells = document.querySelectorAll('.student-cell.selected');
|
|
if (selectedCells.length === 0) {
|
|
showToast('请先选择有考勤异常的学生', 'warning');
|
|
return;
|
|
}
|
|
|
|
const date = document.getElementById('attendanceDate').value;
|
|
const slot = document.getElementById('attendanceSlot').value;
|
|
const reason = document.getElementById('attendanceReason').value;
|
|
const customDeduction = document.getElementById('customDeduction').value;
|
|
const customDeductionValue = customDeduction ? parseInt(customDeduction) : null;
|
|
|
|
const promises = [];
|
|
selectedCells.forEach(cell => {
|
|
const studentId = parseInt(cell.dataset.id);
|
|
const payload = {
|
|
student_id: studentId,
|
|
date: date,
|
|
slot: slot,
|
|
status: currentStatus,
|
|
reason: reason,
|
|
apply_deduction: true
|
|
};
|
|
if (customDeductionValue !== null && customDeductionValue > 0) {
|
|
payload.custom_deduction = customDeductionValue;
|
|
}
|
|
promises.push(apiPost('/api/admin/attendance', payload));
|
|
});
|
|
|
|
const results = await Promise.allSettled(promises);
|
|
const succeeded = results.filter(r => r.status === 'fulfilled' && r.value?.success).length;
|
|
const failed = results.length - succeeded;
|
|
|
|
if (failed === 0) {
|
|
showToast(`考勤提交成功(${succeeded}条)`);
|
|
} else {
|
|
showToast(`提交完成:成功${succeeded}条,失败${failed}条`, 'error');
|
|
}
|
|
|
|
deselectAllStudents();
|
|
await loadExistingRecords();
|
|
loadAttendanceRecords();
|
|
}
|
|
|
|
async function loadAttendanceRecords() {
|
|
const date = document.getElementById('attendanceDate').value;
|
|
const res = await apiGet('/api/admin/attendance/records', { date });
|
|
if (res && res.success) {
|
|
let html = '';
|
|
const records = res.data.records || [];
|
|
records.forEach(record => {
|
|
html += `<tr>
|
|
<td>${escapeHtml(record.student_no)}</td>
|
|
<td>${escapeHtml(record.student_name)}</td>
|
|
<td>${getStatusBadge(record.status, 'attendance')}</td>
|
|
<td>${escapeHtml(record.reason || '-')}</td>
|
|
<td>${escapeHtml(record.recorder_name || '-')}</td>
|
|
<td>${record.deduction_applied ? '已扣分' : '-'}</td>
|
|
</tr>`;
|
|
});
|
|
if (records.length === 0) {
|
|
html = '<tr><td colspan="6" style="text-align:center;">暂无考勤记录</td></tr>';
|
|
}
|
|
document.getElementById('attendanceList').innerHTML = html;
|
|
}
|
|
}
|
|
|
|
// 日期或时段变化时重新加载
|
|
document.getElementById('attendanceDate').addEventListener('change', function() {
|
|
loadExistingRecords();
|
|
loadAttendanceRecords();
|
|
});
|
|
document.getElementById('attendanceSlot').addEventListener('change', function() {
|
|
loadExistingRecords();
|
|
});
|
|
|
|
// 页面初始化
|
|
initAttendanceButtons();
|
|
loadStudents();
|
|
loadAttendanceRecords();
|
|
|
|
window.selectStatus = selectStatus;
|
|
window.loadStudents = loadStudents;
|
|
window.toggleStudent = toggleStudent;
|
|
window.selectAllStudents = selectAllStudents;
|
|
window.deselectAllStudents = deselectAllStudents;
|
|
window.submitAttendance = submitAttendance;
|
|
window.loadAttendanceRecords = loadAttendanceRecords;
|
|
|
|
})();
|