v1.7版本更新
This commit is contained in:
@@ -147,165 +147,8 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var currentEditUserId = null;
|
||||
var currentResetUserId = null;
|
||||
|
||||
async function loadAdmins() {
|
||||
const res = await apiGet('/api/admin/list');
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.admins.forEach(admin => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(admin.username)}</td>
|
||||
<td>${escapeHtml(admin.real_name)}</td>
|
||||
<td>${escapeHtml(admin.role_type)}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="showEditAdminModal(${admin.user_id}, '${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}', '${escapeHtml(admin.role_type)}')">编辑</button>
|
||||
<button class="btn btn-sm btn-warning" onclick="resetAdminPassword(${admin.user_id}, '${escapeHtml(admin.real_name)}')">重置密码</button>
|
||||
<button class="btn btn-sm btn-info" onclick="unlockUser('${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}')">解锁</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteAdmin(${admin.user_id}, '${escapeHtml(admin.real_name)}')">删除</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.admins.length === 0) {
|
||||
html = '<tr><td colspan="4" style="text-align:center;">暂无管理员</td></tr>';
|
||||
}
|
||||
document.getElementById('adminList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
function showAddAdminModal() {
|
||||
document.getElementById('addAdminModal').style.display = 'flex';
|
||||
document.getElementById('adminUsername').value = '';
|
||||
document.getElementById('adminRealName').value = '';
|
||||
document.getElementById('adminPassword').value = '';
|
||||
document.getElementById('adminRole').value = '';
|
||||
}
|
||||
|
||||
async function submitAddAdmin() {
|
||||
const username = document.getElementById('adminUsername').value.trim();
|
||||
const realName = document.getElementById('adminRealName').value.trim();
|
||||
const password = document.getElementById('adminPassword').value;
|
||||
const roleType = document.getElementById('adminRole').value;
|
||||
if (!username || !realName || !roleType) {
|
||||
showToast('请填写完整信息', 'warning');
|
||||
return;
|
||||
}
|
||||
const res = await apiPost('/api/admin/add', {
|
||||
username: username,
|
||||
real_name: realName,
|
||||
password: password || undefined,
|
||||
role_type: roleType
|
||||
});
|
||||
if (res && res.success) {
|
||||
let msg = `管理员 ${res.data.username} 添加成功`;
|
||||
if (res.data.password) msg += `,密码: ${res.data.password}`;
|
||||
showToast(msg);
|
||||
closeModal('addAdminModal');
|
||||
loadAdmins();
|
||||
} else {
|
||||
showToast(res?.message || '添加失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showEditAdminModal(userId, username, realName, roleType) {
|
||||
currentEditUserId = userId;
|
||||
document.getElementById('editAdminUserId').value = userId;
|
||||
document.getElementById('editAdminUsername').value = username;
|
||||
document.getElementById('editAdminRealName').value = realName;
|
||||
document.getElementById('editAdminRole').value = roleType;
|
||||
document.getElementById('editAdminModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function submitEditAdmin() {
|
||||
if (!currentEditUserId) return;
|
||||
|
||||
const roleType = document.getElementById('editAdminRole').value;
|
||||
if (!roleType) {
|
||||
showToast('请选择角色', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPut(`/api/admin/update/${currentEditUserId}`, {
|
||||
real_name: document.getElementById('editAdminRealName').value,
|
||||
role_type: roleType
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('管理员更新成功');
|
||||
closeModal('editAdminModal');
|
||||
loadAdmins();
|
||||
} else {
|
||||
showToast(res?.message || '更新失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAdmin(userId, realName) {
|
||||
if (!confirm(`确定要删除管理员 "${realName}" 吗?此操作不可恢复。`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiDelete(`/api/admin/delete/${userId}`);
|
||||
if (res && res.success) {
|
||||
showToast('管理员删除成功');
|
||||
loadAdmins();
|
||||
} else {
|
||||
showToast(res?.message || '删除失败', 'error');
|
||||
}
|
||||
}
|
||||
function resetAdminPassword(userId, realName) {
|
||||
currentResetUserId = userId;
|
||||
document.getElementById('resetPasswordUserId').value = userId;
|
||||
document.getElementById('resetPasswordAdminName').value = realName;
|
||||
document.getElementById('newPassword').value = '';
|
||||
document.getElementById('resetPasswordModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function unlockUser(username, realName) {
|
||||
if (!confirm(`确定要解除用户 "${realName}" 的登录锁定吗?\n(适用于多次登录失败被禁止登录的情况)`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/unlock-user', {
|
||||
username: username
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '解锁成功');
|
||||
} else {
|
||||
showToast(res?.message || '解锁失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function submitResetPassword() {
|
||||
if (!currentResetUserId) return;
|
||||
|
||||
const newPassword = document.getElementById('newPassword').value;
|
||||
if (!newPassword || newPassword.length < 6) {
|
||||
showToast('密码长度至少6位', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost(`/api/admin/reset-password/${currentResetUserId}`, {
|
||||
new_password: newPassword
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('密码重置成功');
|
||||
closeModal('resetPasswordModal');
|
||||
} else {
|
||||
showToast(res?.message || '密码重置失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) modal.style.display = 'none';
|
||||
}
|
||||
|
||||
loadAdmins();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/modules/admin-mgmt.js"></script>
|
||||
<script src="/assets/js/admins.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
|
||||
@@ -87,196 +87,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
.student-cell-name { font-size: 14px; font-weight: 500; }
|
||||
.student-cell-no { font-size: 11px; color: #999; }
|
||||
</style>
|
||||
<script>
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
// 加载当天已有考勤记录(用于标记 .has-record)
|
||||
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(); // 重新渲染以标记 has-record
|
||||
}
|
||||
}
|
||||
|
||||
// 提交考勤
|
||||
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();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/attendance-manage.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
|
||||
@@ -58,139 +58,10 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var selectedStudentIds = [];
|
||||
|
||||
async function loadStudents() {
|
||||
const res = await apiGet('/api/admin/students', {page_size: 1000});
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.students.forEach(student => {
|
||||
html += `<tr>
|
||||
<td><input type="checkbox" class="student-checkbox" data-id="${student.student_id}"></td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td><a href="/admin/history.php?student_id=${student.student_id}" style="color: #3498db; text-decoration: none;">${escapeHtml(student.name)}</a></td>
|
||||
<td>${student.total_points}</td>
|
||||
<td><button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button></td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.students.length === 0) {
|
||||
html = '<tr><td colspan="5" style="text-align:center;">暂无学生数据</td></tr>';
|
||||
}
|
||||
document.getElementById('studentList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
const selectAll = document.getElementById('selectAll');
|
||||
document.querySelectorAll('.student-checkbox').forEach(cb => {
|
||||
cb.checked = selectAll.checked;
|
||||
});
|
||||
}
|
||||
|
||||
function showSinglePointsModal(studentId, studentName) {
|
||||
selectedStudentIds = [studentId];
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`;
|
||||
document.getElementById('pointsChange').value = '';
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// 导出德育分记录(格式:学号 姓名 分数 加分历史 减分记录)
|
||||
async function exportMoralityRecords() {
|
||||
showToast('正在导出德育分记录...', 'info');
|
||||
|
||||
try {
|
||||
// 获取所有学生
|
||||
const studentsRes = await apiGet('/api/admin/students', { page_size: 1000 });
|
||||
if (!studentsRes || !studentsRes.success) {
|
||||
showToast('获取学生列表失败', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const students = studentsRes.data.students;
|
||||
if (students.length === 0) {
|
||||
showToast('没有找到学生', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取所有历史记录(获取全部,API限制最大1000条)
|
||||
const historyRes = await apiGet('/api/admin/conduct/history', { page: 1, page_size: 1000 });
|
||||
if (!historyRes || !historyRes.success) {
|
||||
showToast('获取历史记录失败', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const allRecords = historyRes.data.records || [];
|
||||
|
||||
// 按学生ID分组历史记录
|
||||
const recordsByStudent = {};
|
||||
allRecords.forEach(record => {
|
||||
const sid = record.student_id;
|
||||
if (!recordsByStudent[sid]) {
|
||||
recordsByStudent[sid] = [];
|
||||
}
|
||||
recordsByStudent[sid].push(record);
|
||||
});
|
||||
|
||||
// 构建学生记录
|
||||
const studentRecords = [];
|
||||
for (const student of students) {
|
||||
const studentRecords_list = recordsByStudent[student.student_id] || [];
|
||||
const positiveRecords = studentRecords_list.filter(r => r.points_change > 0).map(r => `${r.reason}(+${r.points_change})`);
|
||||
const negativeRecords = studentRecords_list.filter(r => r.points_change < 0).map(r => `${r.reason}(${r.points_change})`);
|
||||
|
||||
studentRecords.push({
|
||||
student_no: student.student_no,
|
||||
name: student.name,
|
||||
total_points: student.total_points || 0,
|
||||
positive_history: positiveRecords.join('; '),
|
||||
negative_history: negativeRecords.join('; ')
|
||||
});
|
||||
}
|
||||
|
||||
// CSV字段转义函数
|
||||
function escapeCsvField(field) {
|
||||
if (field === null || field === undefined) return '';
|
||||
// 移除换行符和回车符
|
||||
let str = String(field).replace(/[\r\n]+/g, ' ');
|
||||
// 转义双引号
|
||||
str = str.replace(/"/g, '""');
|
||||
// 如果包含逗号、分号、双引号或空格,用双引号包裹
|
||||
if (/[\,\;\"\s]/.test(str)) {
|
||||
str = '"' + str + '"';
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
// 构建CSV内容
|
||||
let csv = '\uFEFF'; // BOM for UTF-8
|
||||
csv += '学号,姓名,分数,加分历史,减分记录\n';
|
||||
studentRecords.forEach(s => {
|
||||
csv += `${escapeCsvField(s.student_no)},${escapeCsvField(s.name)},${escapeCsvField(s.total_points)},${escapeCsvField(s.positive_history)},${escapeCsvField(s.negative_history)}\n`;
|
||||
});
|
||||
|
||||
// 下载文件
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `德育分记录_${new Date().toISOString().slice(0,10)}.csv`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
showToast(`导出成功,共${studentRecords.length}名学生`);
|
||||
} catch (err) {
|
||||
showToast('导出失败:' + err.message, 'error');
|
||||
console.error('导出失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
loadStudents();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/modules/utils.js"></script>
|
||||
<script src="/assets/js/modules/points-mgmt.js"></script>
|
||||
<script src="/assets/js/conduct.js"></script>
|
||||
|
||||
<!-- 批量加减分模态框 -->
|
||||
<div id="batchPointsModal" class="modal">
|
||||
|
||||
@@ -52,79 +52,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var totalStudents = 0;
|
||||
|
||||
async function loadDashboard() {
|
||||
const studentsRes = await apiGet('/api/admin/students');
|
||||
if (studentsRes && studentsRes.success) {
|
||||
document.getElementById('dashboardStats').innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">学生总数</div>
|
||||
<div class="stat-value">${studentsRes.data.total || 0}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
let quickActions = '';
|
||||
if ('<?php echo $role; ?>' === '班主任' || '<?php echo $role; ?>' === '班长' || '<?php echo $role; ?>' === '劳动委员' || '<?php echo $role; ?>' === '志愿委员') {
|
||||
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>';
|
||||
}
|
||||
if ('<?php echo $role; ?>' === '班主任') {
|
||||
quickActions += '<button class="btn btn-success" onclick="location.href=\'/admin/students.php\'">导入学生</button>';
|
||||
quickActions += '<button class="btn btn-secondary" onclick="location.href=\'/admin/conduct.php\'">导出德育分记录</button>';
|
||||
}
|
||||
document.getElementById('quickActions').innerHTML = quickActions || '<p>暂无快捷操作</p>';
|
||||
|
||||
const rankingRes = await apiGet('/api/student/ranking', { limit: 100 });
|
||||
if (rankingRes && rankingRes.success) {
|
||||
totalStudents = rankingRes.data.total_students || 0;
|
||||
let html = '';
|
||||
rankingRes.data.ranking.forEach((student, index) => {
|
||||
const rank = index + 1;
|
||||
html += `<tr>
|
||||
<td>${rank}</td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td>${escapeHtml(student.name)}</td>
|
||||
<td>${student.total_points}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (rankingRes.data.ranking.length === 0) {
|
||||
html = '<tr><td colspan="4" style="text-align:center;">暂无数据</td></tr>';
|
||||
}
|
||||
document.getElementById('rankingList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('percentileFilter').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') applyPercentileFilter();
|
||||
});
|
||||
|
||||
function applyPercentileFilter() {
|
||||
const input = document.getElementById('percentileFilter');
|
||||
const percentile = parseInt(input.value);
|
||||
if (isNaN(percentile) || percentile < 1 || percentile > 100) {
|
||||
showToast('请输入 1-100 之间的整数', 'error');
|
||||
return;
|
||||
}
|
||||
const rows = document.getElementById('rankingList').querySelectorAll('tr');
|
||||
if (rows.length === 0) return;
|
||||
const showCount = Math.max(1, Math.floor(totalStudents * (percentile / 100)));
|
||||
rows.forEach(function(row, index) {
|
||||
row.style.display = index < showCount ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function resetPercentileFilter() {
|
||||
document.getElementById('percentileFilter').value = 100;
|
||||
const rows = document.getElementById('rankingList').querySelectorAll('tr');
|
||||
rows.forEach(function(row) {
|
||||
row.style.display = '';
|
||||
});
|
||||
}
|
||||
|
||||
loadDashboard();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script>window.PAGE_CONFIG = { role: '<?php echo $role; ?>' };</script>
|
||||
<script src="/assets/js/dashboard.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
@@ -44,13 +44,19 @@ include __DIR__ . '/../includes/header.php';
|
||||
<div class="filter-group">
|
||||
<label>扣分类型</label>
|
||||
<select id="historyRelatedType">
|
||||
<option value="">全部</option>
|
||||
<option value="">全部类型</option>
|
||||
<option value="manual">手动加减分</option>
|
||||
<option value="homework">作业</option>
|
||||
<option value="attendance">考勤</option>
|
||||
<option value="homework">作业扣分</option>
|
||||
<option value="attendance">考勤扣分</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="loadHistory(1)">查询</button>
|
||||
<div class="filter-group" style="min-width:auto;">
|
||||
<label> </label>
|
||||
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;font-size:13px;">
|
||||
<input type="checkbox" id="historyGrouped" onchange="loadHistory(1)"> 批次合并
|
||||
</label>
|
||||
</div>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<button class="btn btn-secondary" onclick="exportHistoryRecords()">导出历史记录</button>
|
||||
<?php endif; ?>
|
||||
@@ -59,7 +65,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<tr id="historyTableHead">
|
||||
<th>时间</th>
|
||||
<th>学生</th>
|
||||
<th>分数变动</th>
|
||||
@@ -77,156 +83,10 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var currentHistoryPage = 1;
|
||||
var totalHistoryPages = 1;
|
||||
var currentUserId = <?php echo intval($_SESSION['user_id']); ?>;
|
||||
|
||||
async function loadStudentsForSelect() {
|
||||
const res = await apiGet('/api/admin/students', {page_size: 1000});
|
||||
if (res && res.success) {
|
||||
let html = '<option value="">全部</option>';
|
||||
res.data.students.forEach(s => {
|
||||
html += `<option value="${s.student_id}">${escapeHtml(s.student_no)} - ${escapeHtml(s.name)}</option>`;
|
||||
});
|
||||
document.getElementById('historyStudentId').innerHTML = html;
|
||||
}
|
||||
}
|
||||
async function loadHistory(page = 1) {
|
||||
currentHistoryPage = page;
|
||||
const startDate = document.getElementById('historyStartDate').value;
|
||||
const endDate = document.getElementById('historyEndDate').value;
|
||||
const studentId = document.getElementById('historyStudentId').value;
|
||||
|
||||
const relatedType = document.getElementById('historyRelatedType').value;
|
||||
|
||||
const params = {
|
||||
page, page_size: 20,
|
||||
start_date: startDate,
|
||||
end_date: endDate
|
||||
};
|
||||
if (studentId) params.student_id = studentId;
|
||||
if (relatedType) params.related_type = relatedType;
|
||||
|
||||
const res = await apiGet('/api/admin/conduct/history', params);
|
||||
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.records.forEach(record => {
|
||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||
const revokedStyle = record.is_revoked == 1 ? ' style="opacity:0.5; text-decoration:line-through;"' : '';
|
||||
html += `<tr${revokedStyle}>
|
||||
<td>${formatDateTime(record.created_at)}</td>
|
||||
<td>${escapeHtml(record.student_name)}</td>
|
||||
<td class="${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
||||
<td>${escapeHtml(record.reason)}</td>
|
||||
<td>${escapeHtml(record.recorder_name)}</td>`;
|
||||
<?php if ($role === '班主任'): ?>
|
||||
if (record.is_revoked == 1) {
|
||||
const revokerInfo = record.revoker_name ? `由 ${escapeHtml(record.revoker_name)} 撤销` : '已撤销';
|
||||
html += `<td><span class="text-muted" style="margin-right:4px;">${revokerInfo}</span><button class="btn btn-sm btn-secondary" onclick="restoreRecord(${record.record_id})">反撤销</button></td>`;
|
||||
} else {
|
||||
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||
}
|
||||
<?php elseif ($role === '班长'): ?>
|
||||
if (record.is_revoked == 1) {
|
||||
const revokerInfo = record.revoker_name ? `由 ${escapeHtml(record.revoker_name)} 撤销` : '已撤销';
|
||||
html += `<td><span class="text-muted">${revokerInfo}</span></td>`;
|
||||
} else {
|
||||
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||
}
|
||||
<?php elseif ($role === '考勤委员'): ?>
|
||||
if (record.is_revoked == 1) {
|
||||
html += `<td><span class="text-muted">已撤销</span></td>`;
|
||||
} else if (record.recorder_id == currentUserId) {
|
||||
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||
} else {
|
||||
html += `<td><span class="text-muted">-</span></td>`;
|
||||
}
|
||||
<?php endif; ?>
|
||||
html += `</tr>`;
|
||||
});
|
||||
|
||||
if (res.data.records.length === 0) {
|
||||
const colSpan = <?php echo ($role === '班主任' || $role === '班长' || $role === '考勤委员') ? '6' : '5'; ?>;
|
||||
html = `<tr><td colspan="${colSpan}" style="text-align:center;">暂无记录</td></tr>`;
|
||||
}
|
||||
|
||||
document.getElementById('historyList').innerHTML = html;
|
||||
|
||||
totalHistoryPages = res.data.total_pages || 1;
|
||||
renderHistoryPagination();
|
||||
}
|
||||
}
|
||||
|
||||
function renderHistoryPagination() {
|
||||
renderSmartPagination('historyPagination', currentHistoryPage, totalHistoryPages, function(page) {
|
||||
loadHistory(page);
|
||||
});
|
||||
}
|
||||
|
||||
// 导出历史记录
|
||||
async function exportHistoryRecords() {
|
||||
const startDate = document.getElementById('historyStartDate').value;
|
||||
const endDate = document.getElementById('historyEndDate').value;
|
||||
const studentId = document.getElementById('historyStudentId').value;
|
||||
|
||||
showToast('正在导出历史记录...', 'info');
|
||||
|
||||
try {
|
||||
const relatedType = document.getElementById('historyRelatedType').value;
|
||||
const params = { page: 1, page_size: 1000 };
|
||||
if (startDate) params.start_date = startDate;
|
||||
if (endDate) params.end_date = endDate;
|
||||
if (studentId) params.student_id = studentId;
|
||||
if (relatedType) params.related_type = relatedType;
|
||||
|
||||
const res = await apiGet('/api/admin/conduct/history', params);
|
||||
if (res && res.success && res.data.records) {
|
||||
const records = res.data.records;
|
||||
if (records.length === 0) {
|
||||
showToast('没有找到记录', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建CSV内容
|
||||
let csv = '\uFEFF'; // BOM for UTF-8
|
||||
csv += '时间,学号,姓名,分数变动,原因,操作人\n';
|
||||
records.forEach(r => {
|
||||
csv += `${r.created_at || ''},${r.student_no || ''},${r.student_name || ''},${r.points_change > 0 ? '+' : ''}${r.points_change},${(r.reason || '').replace(/,/g, ';')},${r.recorder_name || ''}\n`;
|
||||
});
|
||||
|
||||
// 下载文件
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `历史记录_${new Date().toISOString().slice(0,10)}.csv`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
showToast(`导出成功,共${records.length}条记录`);
|
||||
} else {
|
||||
showToast('导出失败:' + (res?.message || '未知错误'), 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('导出失败:' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadStudentsForSelect().then(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const preStudentId = urlParams.get('student_id');
|
||||
if (preStudentId) {
|
||||
document.getElementById('historyStudentId').value = preStudentId;
|
||||
loadHistory();
|
||||
} else {
|
||||
loadHistory();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script>window.PAGE_CONFIG = { role: '<?php echo $role; ?>', userId: <?php echo intval($_SESSION['user_id']); ?> };</script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/modules/utils.js"></script>
|
||||
<script src="/assets/js/modules/points-mgmt.js"></script>
|
||||
<script src="/assets/js/history.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
@@ -81,6 +81,16 @@ include __DIR__ . '/../includes/header.php';
|
||||
<button type="button" class="btn btn-sm" onclick="selectDeductionType(0, '')">自定义</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($role === '学习委员'): ?>
|
||||
<div class="form-group">
|
||||
<label>具体作业</label>
|
||||
<input type="text" id="hwTitle" placeholder="选填,如:第三章练习">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>缴交时间</label>
|
||||
<input type="date" id="hwDeadline">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="form-group">
|
||||
<label>分数变动</label>
|
||||
<input type="number" id="pointsChange" required min="-5" max="5" step="1" placeholder="正数加分,负数扣分">
|
||||
@@ -101,118 +111,10 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var selectedStudentIds = [];
|
||||
const hwRole = '<?php echo $role; ?>';
|
||||
|
||||
// 初始化扣分配置
|
||||
const hwMaxPoints = hwRole === '班主任' ? 100 : 5;
|
||||
const hwNotSubmit = window.DEDUCTION_HOMEWORK_NOT_SUBMIT || 2;
|
||||
const hwLate = window.DEDUCTION_HOMEWORK_LATE || 1;
|
||||
|
||||
// 更新页面中的配置值显示
|
||||
document.querySelectorAll('.hw-not-submit').forEach(el => el.textContent = hwNotSubmit);
|
||||
document.querySelectorAll('.hw-late').forEach(el => el.textContent = hwLate);
|
||||
document.querySelectorAll('.hw-max').forEach(el => el.textContent = hwMaxPoints);
|
||||
|
||||
// 更新输入框的 min/max
|
||||
document.getElementById('pointsChange').setAttribute('min', -hwMaxPoints);
|
||||
document.getElementById('pointsChange').setAttribute('max', hwMaxPoints);
|
||||
|
||||
// 加载科目列表(学习委员)
|
||||
async function loadSubjectsForHomework() {
|
||||
if (hwRole !== '学习委员') return;
|
||||
const subjectSelect = document.getElementById('hwSubjectSelect');
|
||||
if (!subjectSelect) return;
|
||||
const res = await apiGet('/api/subject/list');
|
||||
if (res && res.success && res.data && res.data.subjects) {
|
||||
let html = '<option value="">不选择科目</option>';
|
||||
res.data.subjects.forEach(s => {
|
||||
html += `<option value="${escapeHtml(s.subject_name)}">${escapeHtml(s.subject_name)}</option>`;
|
||||
});
|
||||
subjectSelect.innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStudents() {
|
||||
const res = await apiGet('/api/admin/students', {page_size: 1000});
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.students.forEach(student => {
|
||||
html += `<tr>
|
||||
<td><input type="checkbox" class="student-checkbox" data-id="${student.student_id}"></td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td>${escapeHtml(student.name)}</td>
|
||||
<td>${student.total_points}</td>
|
||||
<td><button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button></td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.students.length === 0) {
|
||||
html = '<tr><td colspan="5" style="text-align:center;">暂无学生数据</td></tr>';
|
||||
}
|
||||
document.getElementById('studentList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
function showSinglePointsModal(studentId, studentName) {
|
||||
selectedStudentIds = [studentId];
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`;
|
||||
document.getElementById('pointsChange').value = '';
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function showBatchPointsModal() {
|
||||
selectedStudentIds = [];
|
||||
document.querySelectorAll('.student-checkbox:checked').forEach(cb => {
|
||||
selectedStudentIds.push(parseInt(cb.dataset.id));
|
||||
});
|
||||
if (selectedStudentIds.length === 0) {
|
||||
showToast('请先选择学生', 'warning');
|
||||
return;
|
||||
}
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `已选择 ${selectedStudentIds.length} 名学生`;
|
||||
document.getElementById('pointsChange').value = '';
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
||||
}
|
||||
function selectDeductionType(points, reason) {
|
||||
document.getElementById('pointsChange').value = points;
|
||||
if (points !== 0) {
|
||||
document.getElementById('pointsReason').value = reason;
|
||||
} else {
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('pointsReason').focus();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmitPoints() {
|
||||
const pointsChange = parseInt(document.getElementById('pointsChange').value);
|
||||
if (isNaN(pointsChange) || pointsChange === 0) {
|
||||
showToast('请输入有效的加减分值', 'warning');
|
||||
return;
|
||||
}
|
||||
if (Math.abs(pointsChange) > hwMaxPoints) {
|
||||
showToast(`每次加减分不超过${hwMaxPoints}分`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 学习委员附加科目前缀
|
||||
if (hwRole === '学习委员') {
|
||||
const subjectSelect = document.getElementById('hwSubjectSelect');
|
||||
const subjectName = subjectSelect ? subjectSelect.value : '';
|
||||
const reasonEl = document.getElementById('pointsReason');
|
||||
if (subjectName && !reasonEl.value.startsWith('[')) {
|
||||
reasonEl.value = `[${subjectName}] ${reasonEl.value}`;
|
||||
}
|
||||
}
|
||||
|
||||
submitBatchPoints();
|
||||
}
|
||||
|
||||
loadStudents();
|
||||
loadSubjectsForHomework();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script>window.PAGE_CONFIG = { role: '<?php echo $role; ?>' };</script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/modules/utils.js"></script>
|
||||
<script src="/assets/js/modules/points-mgmt.js"></script>
|
||||
<script src="/assets/js/homework-manage.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
@@ -77,6 +77,5 @@ document.getElementById('passwordForm').addEventListener('submit', async (e) =>
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
@@ -196,279 +196,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var archiveSemesterId = null;
|
||||
var archivePage = 1;
|
||||
var archiveTotalPages = 1;
|
||||
var associateSemesterId = null;
|
||||
|
||||
function fillSemesterDates(type) {
|
||||
var now = new Date();
|
||||
var currentYear = now.getFullYear();
|
||||
var currentMonth = now.getMonth() + 1;
|
||||
var startDateInput = document.getElementById('semesterStartDate');
|
||||
var endDateInput = document.getElementById('semesterEndDate');
|
||||
|
||||
if (type === 'upper') {
|
||||
var year = currentMonth >= 6 ? currentYear : currentYear - 1;
|
||||
var endYear = year + 1;
|
||||
var febDay = 28;
|
||||
if ((endYear % 4 === 0 && endYear % 100 !== 0) || endYear % 400 === 0) {
|
||||
febDay = 29;
|
||||
}
|
||||
startDateInput.value = year + '-09-01';
|
||||
endDateInput.value = endYear + '-02-' + febDay;
|
||||
} else if (type === 'lower') {
|
||||
startDateInput.value = currentYear + '-03-01';
|
||||
endDateInput.value = currentYear + '-07-15';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSemesters() {
|
||||
const res = await apiGet('/api/semester/list');
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
const semesters = res.data || [];
|
||||
semesters.forEach(sem => {
|
||||
let statusText = '';
|
||||
let statusClass = '';
|
||||
if (sem.is_archived) {
|
||||
statusText = '已归档';
|
||||
statusClass = 'status-badge status-not_submitted';
|
||||
} else if (sem.is_active) {
|
||||
statusText = '当前学期';
|
||||
statusClass = 'status-badge status-submitted';
|
||||
} else {
|
||||
statusText = '未激活';
|
||||
statusClass = 'status-badge status-late';
|
||||
}
|
||||
|
||||
let actions = '';
|
||||
const startDate = sem.start_date || '';
|
||||
const endDate = sem.end_date || '';
|
||||
if (!sem.is_archived) {
|
||||
actions += `<button class="btn btn-sm" style="border:1px solid #667eea;color:#667eea;" onclick="showEditSemesterModal(${sem.semester_id}, '${escapeHtml(sem.semester_name)}', '${startDate}', '${endDate}')">编辑</button> `;
|
||||
if (!sem.is_active) {
|
||||
actions += `<button class="btn btn-sm btn-primary" onclick="activateSemester(${sem.semester_id})">激活</button> `;
|
||||
}
|
||||
actions += `<button class="btn btn-sm" style="border:1px solid #2ecc71;color:#2ecc71;" onclick="showAssociateConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}', '${startDate}', '${endDate}')">关联数据</button> `;
|
||||
actions += `<button class="btn btn-sm btn-warning" onclick="showArchiveConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">归档</button> `;
|
||||
}
|
||||
if (sem.is_archived) {
|
||||
actions += `<button class="btn btn-sm btn-secondary" onclick="viewArchiveData(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">查看归档</button>`;
|
||||
}
|
||||
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(sem.semester_name)}</td>
|
||||
<td>${formatDate(sem.start_date)}</td>
|
||||
<td>${formatDate(sem.end_date)}</td>
|
||||
<td><span class="${statusClass}">${statusText}</span></td>
|
||||
<td>${formatDateTime(sem.created_at)}</td>
|
||||
<td>${actions}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (semesters.length === 0) {
|
||||
html = '<tr><td colspan="6" style="text-align:center;">暂无学期,请点击上方按钮创建新学期</td></tr>';
|
||||
}
|
||||
document.getElementById('semesterList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateSemesterModal() {
|
||||
document.getElementById('semesterName').value = '';
|
||||
document.getElementById('semesterStartDate').value = '';
|
||||
document.getElementById('semesterEndDate').value = '';
|
||||
document.getElementById('createSemesterModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function submitCreateSemester() {
|
||||
const name = document.getElementById('semesterName').value.trim();
|
||||
const startDate = document.getElementById('semesterStartDate').value;
|
||||
const endDate = document.getElementById('semesterEndDate').value;
|
||||
|
||||
if (!name) {
|
||||
showToast('请输入学期名称', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/semester/create', {
|
||||
semester_name: name,
|
||||
start_date: startDate || null,
|
||||
end_date: endDate || null
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '学期创建成功');
|
||||
closeModal('createSemesterModal');
|
||||
loadSemesters();
|
||||
} else {
|
||||
showToast(res?.message || '创建失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function activateSemester(semesterId) {
|
||||
if (!confirm('确认将此学期设为当前活跃学期?其他学期将被设为非活跃。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPut(`/api/semester/activate/${semesterId}`);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '已设为当前学期');
|
||||
loadSemesters();
|
||||
} else {
|
||||
showToast(res?.message || '操作失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showEditSemesterModal(id, name, startDate, endDate) {
|
||||
document.getElementById('editSemesterId').value = id;
|
||||
document.getElementById('editSemesterName').value = name;
|
||||
document.getElementById('editSemesterStartDate').value = startDate || '';
|
||||
document.getElementById('editSemesterEndDate').value = endDate || '';
|
||||
document.getElementById('editSemesterModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function submitEditSemester() {
|
||||
const id = document.getElementById('editSemesterId').value;
|
||||
const name = document.getElementById('editSemesterName').value.trim();
|
||||
const startDate = document.getElementById('editSemesterStartDate').value;
|
||||
const endDate = document.getElementById('editSemesterEndDate').value;
|
||||
|
||||
if (!name) {
|
||||
showToast('请输入学期名称', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { semester_name: name };
|
||||
if (startDate) data.start_date = startDate;
|
||||
if (endDate) data.end_date = endDate;
|
||||
|
||||
const res = await apiPut(`/api/semester/update/${id}`, data);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '更新成功');
|
||||
closeModal('editSemesterModal');
|
||||
loadSemesters();
|
||||
} else {
|
||||
showToast(res?.message || '更新失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSemester() {
|
||||
const id = document.getElementById('editSemesterId').value;
|
||||
if (!confirm('确定要删除该学期吗?如果学期已有归档数据则无法删除。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiDelete(`/api/semester/delete/${id}`);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '删除成功');
|
||||
closeModal('editSemesterModal');
|
||||
loadSemesters();
|
||||
} else {
|
||||
showToast(res?.message || '删除失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showAssociateConfirm(id, name, startDate, endDate) {
|
||||
associateSemesterId = id;
|
||||
const dateRange = startDate ? `${startDate} ~ ${endDate || '至今'}` : '未设置日期范围';
|
||||
document.getElementById('associateConfirmText').innerHTML =
|
||||
`即将关联 <strong>${dateRange}</strong> 内的所有未分配学期的操行分记录和考勤记录到学期 "<strong>${name}</strong>"。`;
|
||||
document.getElementById('associateConfirmModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function confirmAssociate() {
|
||||
if (!associateSemesterId) return;
|
||||
|
||||
const res = await apiPost(`/api/semester/${associateSemesterId}/associate`);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '关联成功');
|
||||
closeModal('associateConfirmModal');
|
||||
associateSemesterId = null;
|
||||
} else {
|
||||
showToast(res?.message || '关联失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showArchiveConfirm(semesterId, semesterName) {
|
||||
archiveSemesterId = semesterId;
|
||||
document.getElementById('archiveResetScores').checked = false;
|
||||
document.getElementById('archiveConfirmText').innerHTML =
|
||||
`确定要归档学期 "<strong>${semesterName}</strong>" 吗?<br>归档后将保存所有学生的当前操行分快照,该学期数据将变为只读。`;
|
||||
document.getElementById('archiveConfirmModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function confirmArchive() {
|
||||
if (!archiveSemesterId) return;
|
||||
|
||||
const resetScores = document.getElementById('archiveResetScores').checked;
|
||||
const url = `/api/semester/archive/${archiveSemesterId}?reset_scores=${resetScores}`;
|
||||
|
||||
const res = await apiPost(url);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '归档成功');
|
||||
closeModal('archiveConfirmModal');
|
||||
archiveSemesterId = null;
|
||||
loadSemesters();
|
||||
} else {
|
||||
showToast(res?.message || '归档失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function viewArchiveData(semesterId, semesterName, page) {
|
||||
page = page || 1;
|
||||
archivePage = page;
|
||||
document.getElementById('archiveDataTitle').textContent = `归档数据 - ${semesterName}`;
|
||||
|
||||
const res = await apiGet(`/api/semester/archive/${semesterId}/records`, {
|
||||
page: page, page_size: 50
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
const data = res.data || {};
|
||||
const archives = data.archives || [];
|
||||
let html = '';
|
||||
archives.forEach(a => {
|
||||
html += `<tr>
|
||||
<td>${a.rank_position || '-'}</td>
|
||||
<td>${escapeHtml(a.student_no)}</td>
|
||||
<td>${escapeHtml(a.student_name)}</td>
|
||||
<td>${a.final_points}</td>
|
||||
<td>${a.attendance_present || 0}</td>
|
||||
<td>${a.attendance_absent || 0}</td>
|
||||
<td>${a.attendance_late || 0}</td>
|
||||
<td>${a.attendance_leave || 0}</td>
|
||||
<td>${a.homework_submitted || 0}</td>
|
||||
<td>${a.homework_not_submitted || 0}</td>
|
||||
<td>${a.homework_late || 0}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (archives.length === 0) {
|
||||
html = '<tr><td colspan="11" style="text-align:center;">暂无归档数据</td></tr>';
|
||||
}
|
||||
document.getElementById('archiveDataList').innerHTML = html;
|
||||
|
||||
archiveTotalPages = data.total_pages || 1;
|
||||
renderArchivePagination(semesterId, semesterName);
|
||||
document.getElementById('archiveDataModal').style.display = 'flex';
|
||||
} else {
|
||||
showToast(res?.message || '获取归档数据失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderArchivePagination(semesterId, semesterName) {
|
||||
renderSmartPagination('archivePagination', archivePage, archiveTotalPages, function(page) {
|
||||
viewArchiveData(semesterId, semesterName, page);
|
||||
});
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) modal.style.display = 'none';
|
||||
}
|
||||
|
||||
loadSemesters();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/semesters.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
|
||||
@@ -46,6 +46,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
<th><input type="checkbox" id="selectAll" onclick="toggleSelectAll()"></th>
|
||||
<th>学号</th>
|
||||
<th>姓名</th>
|
||||
<th>宿舍号</th>
|
||||
<th>操行分</th>
|
||||
<?php if ($role === '班主任'): ?><th>家长手机号</th><?php endif; ?>
|
||||
<th>操作</th>
|
||||
@@ -102,6 +103,10 @@ include __DIR__ . '/../includes/header.php';
|
||||
<input type="tel" id="parentPhone" placeholder="11位手机号">
|
||||
<small>填写后将自动创建家长账号(密码同学生初始密码123456)</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>宿舍号</label>
|
||||
<input type="text" id="addDormitoryNumber" placeholder="选填">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">确认添加</button>
|
||||
<button type="button" class="btn" onclick="closeModal('addStudentModal')">取消</button>
|
||||
@@ -131,6 +136,10 @@ include __DIR__ . '/../includes/header.php';
|
||||
<label>家长手机号</label>
|
||||
<input type="text" id="editStudentPhone" maxlength="20">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>宿舍号</label>
|
||||
<input type="text" id="editDormitoryNumber" placeholder="选填">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">保存修改</button>
|
||||
<button type="button" class="btn" onclick="closeModal('editStudentModal')">取消</button>
|
||||
@@ -161,97 +170,12 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const userRole = '<?php echo $role; ?>';
|
||||
var currentPage = 1;
|
||||
var totalPages = 1;
|
||||
|
||||
async function loadStudents(page = 1) {
|
||||
currentPage = page;
|
||||
const search = document.getElementById('searchInput').value;
|
||||
const res = await apiGet('/api/admin/students', { page, page_size: 20, search });
|
||||
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.students.forEach(student => {
|
||||
html += `<tr>
|
||||
<td><input type="checkbox" class="student-checkbox" data-id="${student.student_id}"></td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td><a href="/admin/history.php?student_id=${student.student_id}" style="color: #3498db; text-decoration: none;">${escapeHtml(student.name)}</a></td>
|
||||
<td>${student.total_points}</td>
|
||||
${userRole === '班主任' ? `<td>${student.parent_phone ? student.parent_phone.slice(0,3) + '******' + student.parent_phone.slice(-2) : '-'}</td>` : ''}
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button>
|
||||
${userRole === '班主任' ? `<button class="btn btn-sm btn-secondary" onclick="showEditStudentModal(${student.student_id}, '${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}', '${escapeHtml(student.parent_phone || '')}')">编辑</button>
|
||||
<button class="btn btn-sm btn-warning" onclick="showResetStudentPasswordModal(${student.student_id}, '${escapeHtml(student.name)}')">重置密码</button>
|
||||
<button class="btn btn-sm btn-info" onclick="unlockStudent('${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}')">解锁</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteStudent(${student.student_id}, '${escapeHtml(student.name)}')">删除</button>` : ''}
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
if (res.data.students.length === 0) {
|
||||
html = `<tr><td colspan="${userRole === '班主任' ? '6' : '5'}" style="text-align:center;">暂无学生数据</td></tr>`;
|
||||
}
|
||||
|
||||
document.getElementById('studentList').innerHTML = html;
|
||||
|
||||
totalPages = res.data.total_pages || 1;
|
||||
renderPagination();
|
||||
}
|
||||
}
|
||||
|
||||
function renderPagination() {
|
||||
renderSmartPagination('pagination', currentPage, totalPages, function(page) {
|
||||
loadStudents(page);
|
||||
});
|
||||
}
|
||||
|
||||
function showSinglePointsModal(studentId, studentName) {
|
||||
selectedStudentIds = [studentId];
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`;
|
||||
document.getElementById('pointsChange').value = '';
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
const selectAll = document.getElementById('selectAll');
|
||||
if (selectAll) {
|
||||
document.querySelectorAll('.student-checkbox').forEach(cb => {
|
||||
cb.checked = selectAll.checked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 解锁学生登录
|
||||
async function unlockStudent(studentNo, studentName) {
|
||||
if (!confirm(`确定要解除学生 "${studentName}" 的登录锁定吗?\n(适用于多次登录失败被禁止登录的情况)`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/unlock-user', {
|
||||
username: studentNo
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '解锁成功');
|
||||
} else {
|
||||
showToast(res?.message || '解锁失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载
|
||||
loadStudents();
|
||||
|
||||
// 搜索防抖
|
||||
let searchTimeout;
|
||||
document.getElementById('searchInput').addEventListener('input', () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => loadStudents(1), 500);
|
||||
});
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script>window.PAGE_CONFIG = { role: '<?php echo $role; ?>' };</script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/modules/utils.js"></script>
|
||||
<script src="/assets/js/modules/student-mgmt.js"></script>
|
||||
<script src="/assets/js/modules/points-mgmt.js"></script>
|
||||
<script src="/assets/js/students-manage.js"></script>
|
||||
|
||||
<!-- 批量加减分模态框(共用) -->
|
||||
<div id="batchPointsModal" class="modal">
|
||||
|
||||
@@ -129,108 +129,8 @@ include __DIR__ . '/../includes/header.php';
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
async function loadSubjects() {
|
||||
const res = await apiGet('/api/subject/list');
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.subjects.forEach(sub => {
|
||||
html += `
|
||||
<div class="subject-item">
|
||||
<span class="subject-name">${escapeHtml(sub.subject_name)}</span>
|
||||
<span class="subject-code">${escapeHtml(sub.subject_code || '')}</span>
|
||||
<span class="subject-status ${sub.is_active ? 'subject-status-active' : 'subject-status-inactive'}">
|
||||
${sub.is_active ? '启用' : '禁用'}
|
||||
</span>
|
||||
<button class="btn btn-sm btn-primary" onclick="showEditSubjectModal(${sub.subject_id}, '${escapeHtml(sub.subject_name)}', '${escapeHtml(sub.subject_code || '')}', ${sub.sort_order || 0})">编辑</button>
|
||||
<button class="btn btn-sm" onclick="toggleSubject(${sub.subject_id}, ${!sub.is_active})">
|
||||
${sub.is_active ? '禁用' : '启用'}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
if (res.data.subjects.length === 0) {
|
||||
html = '<p style="text-align:center;padding:40px;">暂无科目,请点击"添加科目"</p>';
|
||||
}
|
||||
document.getElementById('subjectList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleSubject(subjectId, enable) {
|
||||
const res = await apiPut(`/api/subject/update/${subjectId}`, { is_active: enable });
|
||||
if (res && res.success) {
|
||||
showToast(enable ? '科目已启用' : '科目已禁用');
|
||||
loadSubjects();
|
||||
} else {
|
||||
showToast(res?.message || '操作失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showAddSubjectModal() {
|
||||
document.getElementById('addSubjectModal').style.display = 'flex';
|
||||
document.getElementById('addSubjectForm')?.reset();
|
||||
}
|
||||
|
||||
async function submitAddSubject() {
|
||||
const subjectName = document.getElementById('subjectName').value.trim();
|
||||
const subjectCode = document.getElementById('subjectCode').value.trim();
|
||||
if (!subjectName) {
|
||||
showToast('请填写科目名称', 'warning');
|
||||
return;
|
||||
}
|
||||
const res = await apiPost('/api/subject/create', {
|
||||
subject_name: subjectName,
|
||||
subject_code: subjectCode
|
||||
});
|
||||
if (res && res.success) {
|
||||
showToast('科目添加成功');
|
||||
closeModal('addSubjectModal');
|
||||
loadSubjects();
|
||||
} else {
|
||||
showToast(res?.message || '添加失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showEditSubjectModal(subjectId, name, code, sortOrder) {
|
||||
document.getElementById('editSubjectId').value = subjectId;
|
||||
document.getElementById('editSubjectName').value = name;
|
||||
document.getElementById('editSubjectCode').value = code;
|
||||
document.getElementById('editSubjectSortOrder').value = sortOrder;
|
||||
document.getElementById('editSubjectModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function submitEditSubject() {
|
||||
const subjectId = document.getElementById('editSubjectId').value;
|
||||
const subjectName = document.getElementById('editSubjectName').value.trim();
|
||||
const subjectCode = document.getElementById('editSubjectCode').value.trim();
|
||||
const sortOrder = document.getElementById('editSubjectSortOrder').value;
|
||||
|
||||
if (!subjectName) {
|
||||
showToast('请填写科目名称', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { subject_name: subjectName };
|
||||
if (subjectCode) data.subject_code = subjectCode;
|
||||
if (sortOrder !== '') data.sort_order = parseInt(sortOrder);
|
||||
|
||||
const res = await apiPut(`/api/subject/update/${subjectId}`, data);
|
||||
if (res && res.success) {
|
||||
showToast('科目更新成功');
|
||||
closeModal('editSubjectModal');
|
||||
loadSubjects();
|
||||
} else {
|
||||
showToast(res?.message || '更新失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) modal.style.display = 'none';
|
||||
}
|
||||
|
||||
loadSubjects();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/modules/subject-mgmt.js"></script>
|
||||
<script src="/assets/js/subjects.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
Reference in New Issue
Block a user