feat: 多班级版班级管理系统 v2.0
技术栈:Go (Gin + GORM) + PHP + MySQL 5.7 + Redis 主要功能: - 多班级完全隔离(class_id 贯穿全系统) - 后端从 Python FastAPI 重写为 Go Gin(端口 56789) - 超级管理员独立登录(env 配置路径,默认账密 admin/Admin123) - 科任老师/课代表新角色 - 课代表作业管理页面 - 排行榜分项排行(操行分/考勤/作业) - 角色加减分上下限由班主任配置 - 家长改密功能(可开关) - 班级角色按需开关 - 宿舍号格式:南0-000 - 周度/月度重置功能 - MySQL 5.7 兼容 - Nginx 反向代理部署 开发者: Canglan 版权归属: Sea Network Technology Studio 许可证: Apache License 2.0
This commit is contained in:
195
frontend/assets/js/attendance-manage.js
Normal file
195
frontend/assets/js/attendance-manage.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 多班级版班级管理系统 - 考勤管理页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;
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user