v1.7版本更新
This commit is contained in:
@@ -206,6 +206,21 @@
|
||||
color: #4338ca;
|
||||
}
|
||||
|
||||
.student-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: #e8f4f8;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
margin: 2px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
.student-tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.student-cell {
|
||||
width: calc(100% / 4 - 10px);
|
||||
@@ -217,3 +232,8 @@
|
||||
width: calc(100% / 3 - 10px);
|
||||
}
|
||||
}
|
||||
|
||||
.preserve-newlines {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@@ -473,6 +473,11 @@ tr:hover {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
min-height: 60px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* ========== 复选框组 ========== */
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
|
||||
@@ -1,388 +1,14 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理端JS
|
||||
* admin.js - 管理端公共函数库
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
* 此文件已拆分为独立模块,各模块文件位于 /assets/js/modules/ 目录
|
||||
* 各页面通过引用对应模块获取所需功能
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
* 模块列表:
|
||||
* - modules/modal-utils.js - 模态框工具函数
|
||||
* - modules/utils.js - 通用工具函数(escapeHtml, toggleSelectAll等)
|
||||
* - modules/student-mgmt.js - 学生管理函数
|
||||
* - modules/admin-mgmt.js - 管理员管理函数
|
||||
* - modules/subject-mgmt.js - 科目管理函数
|
||||
* - modules/points-mgmt.js - 加减分管理函数
|
||||
*/
|
||||
|
||||
// 全局变量(使用 var 避免与页面级 let 重复声明冲突)
|
||||
var selectedStudentIds = [];
|
||||
var currentPage = 1;
|
||||
var totalPages = 1;
|
||||
var currentHistoryPage = 1;
|
||||
|
||||
// 显示批量加减分模态框
|
||||
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';
|
||||
}
|
||||
|
||||
// 提交批量加减分
|
||||
async function submitBatchPoints() {
|
||||
const pointsChange = parseInt(document.getElementById('pointsChange').value);
|
||||
const reason = document.getElementById('pointsReason').value;
|
||||
|
||||
if (isNaN(pointsChange) || pointsChange === 0) {
|
||||
showToast('分值不能为0', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reason.trim()) {
|
||||
showToast('请填写原因', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/conduct/add', {
|
||||
student_ids: selectedStudentIds,
|
||||
points_change: pointsChange,
|
||||
reason: reason
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast(`操作成功: ${res.data.success_count} 人成功`);
|
||||
closeModal('batchPointsModal');
|
||||
loadStudents();
|
||||
if (typeof loadConductStudents === 'function') loadConductStudents();
|
||||
} else {
|
||||
showToast(res?.message || '操作失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示导入模态框
|
||||
function showImportModal() {
|
||||
document.getElementById('importModal').style.display = 'flex';
|
||||
document.getElementById('importPreview').style.display = 'none';
|
||||
document.getElementById('importPreview').innerHTML = '';
|
||||
document.getElementById('importBtn').style.display = 'none';
|
||||
document.getElementById('importFile').value = '';
|
||||
}
|
||||
|
||||
// 预览导入文件
|
||||
function previewImportFile() {
|
||||
const file = document.getElementById('importFile').files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
try {
|
||||
const data = JSON.parse(e.target.result);
|
||||
const students = data.students || [];
|
||||
|
||||
let html = '<h4>预览数据</h4><div class="table-wrapper"><table><thead><tr>';
|
||||
html += '<th>学号</th><th>姓名</th><th>家长手机号</th><th>初始密码</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
students.forEach(s => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(s.student_no || '')}</td>
|
||||
<td>${escapeHtml(s.name || '')}</td>
|
||||
<td>${escapeHtml(s.parent_phone || '')}</td>
|
||||
<td>${escapeHtml(s.password || '123456')}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += `</tbody></table></div><p>共 ${students.length} 条记录,初始操行分默认为60分</p>`;
|
||||
document.getElementById('importPreview').innerHTML = html;
|
||||
document.getElementById('importPreview').style.display = 'block';
|
||||
document.getElementById('importBtn').style.display = 'inline-block';
|
||||
} catch (error) {
|
||||
showToast('JSON格式错误', 'error');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
// 执行导入
|
||||
async function doImport() {
|
||||
const file = document.getElementById('importFile').files[0];
|
||||
if (!file) {
|
||||
showToast('请选择文件', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const token = getToken();
|
||||
const response = await fetch(`${API_BASE_URL}/api/admin/students/import`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast(result.message);
|
||||
closeModal('importModal');
|
||||
loadStudents();
|
||||
|
||||
// 显示详细导入结果
|
||||
if (result.data && result.data.results) {
|
||||
const failedList = result.data.results.filter(r => !r.success);
|
||||
if (failedList.length > 0) {
|
||||
let detail = '失败详情:\n';
|
||||
failedList.forEach(r => {
|
||||
detail += `${r.student_no || '未知'}: ${r.error}\n`;
|
||||
});
|
||||
alert(detail);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showToast(result.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示新增学生模态框
|
||||
function showAddStudentModal() {
|
||||
document.getElementById('addStudentModal').style.display = 'flex';
|
||||
document.getElementById('addStudentForm').reset();
|
||||
}
|
||||
|
||||
// 提交新增学生
|
||||
async function submitAddStudent() {
|
||||
const studentNo = document.getElementById('studentNo').value.trim();
|
||||
const name = document.getElementById('studentName').value.trim();
|
||||
const parentPhone = document.getElementById('parentPhone').value.trim();
|
||||
|
||||
if (!studentNo || !name) {
|
||||
showToast('请填写学号和姓名', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/students', {
|
||||
student_no: studentNo,
|
||||
name: name,
|
||||
parent_phone: parentPhone
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('学生添加成功');
|
||||
closeModal('addStudentModal');
|
||||
loadStudents();
|
||||
} else {
|
||||
showToast(res?.message || '添加失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示添加管理员模态框
|
||||
function showAddAdminModal() {
|
||||
document.getElementById('addAdminModal').style.display = 'flex';
|
||||
document.getElementById('addAdminForm')?.reset();
|
||||
}
|
||||
|
||||
// 提交添加管理员
|
||||
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 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');
|
||||
}
|
||||
}
|
||||
|
||||
// 撤销扣分记录
|
||||
async function revokeRecord(recordId) {
|
||||
if (!confirm('确定要撤销这条记录吗?撤销后学生分数将恢复。')) return;
|
||||
|
||||
const res = await apiPost('/api/admin/conduct/revoke', { record_id: recordId });
|
||||
if (res && res.success) {
|
||||
showToast('撤销成功');
|
||||
loadHistory(currentHistoryPage);
|
||||
} else {
|
||||
showToast(res?.message || '撤销失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 反撤销(恢复)记录
|
||||
async function restoreRecord(recordId) {
|
||||
if (!confirm('确定要反撤销这条记录吗?分数变动将重新生效。')) return;
|
||||
|
||||
const res = await apiPost('/api/admin/conduct/restore', { record_id: recordId });
|
||||
if (res && res.success) {
|
||||
showToast('反撤销成功');
|
||||
loadHistory(currentHistoryPage);
|
||||
} else {
|
||||
showToast(res?.message || '反撤销失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
function closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// HTML转义
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/[&<>]/g, function(m) {
|
||||
if (m === '&') return '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
// 全选功能
|
||||
function toggleSelectAll() {
|
||||
const selectAll = document.getElementById('selectAll');
|
||||
if (selectAll) {
|
||||
document.querySelectorAll('.student-checkbox').forEach(cb => {
|
||||
cb.checked = selectAll.checked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定文件选择事件
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('importFile');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', previewImportFile);
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 学生编辑/删除/重置密码 =====
|
||||
|
||||
function showEditStudentModal(studentId, studentNo, name, phone) {
|
||||
document.getElementById('editStudentId').value = studentId;
|
||||
document.getElementById('editStudentNo').value = studentNo;
|
||||
document.getElementById('editStudentName').value = name;
|
||||
document.getElementById('editStudentPhone').value = phone || '';
|
||||
document.getElementById('editStudentModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function submitEditStudent() {
|
||||
const studentId = document.getElementById('editStudentId').value;
|
||||
const name = document.getElementById('editStudentName').value.trim();
|
||||
const phone = document.getElementById('editStudentPhone').value.trim();
|
||||
|
||||
if (!name) {
|
||||
showToast('请输入姓名', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPut(`/api/admin/students/${studentId}`, {
|
||||
name: name,
|
||||
parent_phone: phone || null
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('学生信息更新成功');
|
||||
closeModal('editStudentModal');
|
||||
location.reload();
|
||||
} else {
|
||||
showToast(res?.message || '更新失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showResetStudentPasswordModal(studentId, name) {
|
||||
document.getElementById('resetStudentId').value = studentId;
|
||||
document.getElementById('resetStudentInfo').textContent = `正在重置学生 "${name}" 的密码`;
|
||||
document.getElementById('newStudentPassword').value = '';
|
||||
document.getElementById('resetStudentPasswordModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function submitResetStudentPassword() {
|
||||
const studentId = document.getElementById('resetStudentId').value;
|
||||
const newPassword = document.getElementById('newStudentPassword').value;
|
||||
|
||||
if (!newPassword || newPassword.length < 6) {
|
||||
showToast('密码至少6位', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost(`/api/admin/students/reset-password/${studentId}`, {
|
||||
new_password: newPassword
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('密码重置成功');
|
||||
closeModal('resetStudentPasswordModal');
|
||||
} else {
|
||||
showToast(res?.message || '重置失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteStudent(studentId, name) {
|
||||
if (!confirm(`确定要删除学生 "${name}" 吗?删除后学生账号将被禁用。`)) return;
|
||||
|
||||
const res = await apiDelete(`/api/admin/students/${studentId}`);
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('学生删除成功');
|
||||
location.reload();
|
||||
} else {
|
||||
showToast(res?.message || '删除失败', 'error');
|
||||
}
|
||||
}
|
||||
141
frontend/assets/js/admins.js
Normal file
141
frontend/assets/js/admins.js
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理员管理页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
let currentEditUserId = null;
|
||||
let 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 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');
|
||||
}
|
||||
}
|
||||
|
||||
loadAdmins();
|
||||
|
||||
window.loadAdmins = loadAdmins;
|
||||
window.showEditAdminModal = showEditAdminModal;
|
||||
window.submitEditAdmin = submitEditAdmin;
|
||||
window.deleteAdmin = deleteAdmin;
|
||||
window.resetAdminPassword = resetAdminPassword;
|
||||
window.unlockUser = unlockUser;
|
||||
window.submitResetPassword = submitResetPassword;
|
||||
|
||||
})();
|
||||
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;
|
||||
|
||||
})();
|
||||
@@ -334,4 +334,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', logout);
|
||||
}
|
||||
});
|
||||
|
||||
// 全局textarea键盘事件:Enter提交表单,Ctrl+Enter换行
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.target.tagName !== 'TEXTAREA') return;
|
||||
|
||||
if (e.key === 'Enter' && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
|
||||
// Enter键提交表单
|
||||
e.preventDefault();
|
||||
var form = e.target.closest('form');
|
||||
if (form) {
|
||||
// 触发form的submit事件
|
||||
var submitEvent = new Event('submit', { cancelable: true, bubbles: true });
|
||||
form.dispatchEvent(submitEvent);
|
||||
}
|
||||
}
|
||||
// Ctrl+Enter和Shift+Enter保持默认换行行为(不拦截)
|
||||
});
|
||||
128
frontend/assets/js/conduct.js
Normal file
128
frontend/assets/js/conduct.js
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 操行分管理页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
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 showSinglePointsModal(studentId, studentName) {
|
||||
window.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;
|
||||
}
|
||||
|
||||
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 || [];
|
||||
|
||||
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('; ')
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let csv = '\uFEFF';
|
||||
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();
|
||||
|
||||
window.loadStudents = loadStudents;
|
||||
window.showSinglePointsModal = showSinglePointsModal;
|
||||
window.exportMoralityRecords = exportMoralityRecords;
|
||||
|
||||
})();
|
||||
90
frontend/assets/js/dashboard.js
Normal file
90
frontend/assets/js/dashboard.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理端首页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const role = window.PAGE_CONFIG.role;
|
||||
let 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 (role === '班主任' || role === '班长' || role === '劳动委员' || role === '志愿委员') {
|
||||
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>';
|
||||
}
|
||||
if (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;
|
||||
}
|
||||
}
|
||||
|
||||
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 = '';
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('percentileFilter').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') applyPercentileFilter();
|
||||
});
|
||||
|
||||
loadDashboard();
|
||||
|
||||
window.loadDashboard = loadDashboard;
|
||||
window.applyPercentileFilter = applyPercentileFilter;
|
||||
window.resetPercentileFilter = resetPercentileFilter;
|
||||
|
||||
})();
|
||||
199
frontend/assets/js/history.js
Normal file
199
frontend/assets/js/history.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 历史记录页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const role = window.PAGE_CONFIG.role;
|
||||
const currentUserId = window.PAGE_CONFIG.userId;
|
||||
let currentHistoryPage = 1;
|
||||
let totalHistoryPages = 1;
|
||||
|
||||
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 isGrouped = document.getElementById('historyGrouped').checked;
|
||||
|
||||
const params = {
|
||||
page, page_size: 20,
|
||||
start_date: startDate,
|
||||
end_date: endDate
|
||||
};
|
||||
if (studentId) params.student_id = studentId;
|
||||
if (relatedType && !isGrouped) params.related_type = relatedType;
|
||||
if (isGrouped) params.grouped = true;
|
||||
|
||||
const res = await apiGet('/api/admin/conduct/history', params);
|
||||
|
||||
if (res && res.success) {
|
||||
let headHtml = '';
|
||||
if (isGrouped) {
|
||||
headHtml = '<th>时间</th><th>原因</th><th>分值</th><th>操作人</th><th>涉及学生</th>';
|
||||
} else {
|
||||
headHtml = '<th>时间</th><th>学生</th><th>分数变动</th><th>原因</th><th>操作人</th>';
|
||||
if (role === '班主任' || role === '班长' || role === '考勤委员') {
|
||||
headHtml += '<th>操作</th>';
|
||||
}
|
||||
}
|
||||
document.getElementById('historyTableHead').innerHTML = headHtml;
|
||||
|
||||
let html = '';
|
||||
if (isGrouped) {
|
||||
res.data.records.forEach(record => {
|
||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||
const names = record.student_names ? record.student_names.split(', ') : [];
|
||||
let tagsHtml = '<div class="student-tags-container">';
|
||||
names.forEach(name => {
|
||||
tagsHtml += `<span class="student-tag">${escapeHtml(name)}</span>`;
|
||||
});
|
||||
tagsHtml += '</div>';
|
||||
html += `<tr>
|
||||
<td>${formatDateTime(record.created_at)}</td>
|
||||
<td class="preserve-newlines">${escapeHtml(record.reason)}</td>
|
||||
<td class="${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}×${record.student_count}</td>
|
||||
<td>${escapeHtml(record.recorder_name || '')}</td>
|
||||
<td>${tagsHtml}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.records.length === 0) {
|
||||
html = '<tr><td colspan="5" style="text-align:center;">暂无记录</td></tr>';
|
||||
}
|
||||
} else {
|
||||
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 class="preserve-newlines">${escapeHtml(record.reason)}</td>
|
||||
<td>${escapeHtml(record.recorder_name)}</td>`;
|
||||
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>`;
|
||||
}
|
||||
} else if (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>`;
|
||||
}
|
||||
} else if (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>`;
|
||||
}
|
||||
}
|
||||
html += `</tr>`;
|
||||
});
|
||||
|
||||
if (res.data.records.length === 0) {
|
||||
const colSpan = (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;
|
||||
}
|
||||
|
||||
let csv = '\uFEFF';
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
window.loadHistory = loadHistory;
|
||||
window.loadStudentsForSelect = loadStudentsForSelect;
|
||||
window.exportHistoryRecords = exportHistoryRecords;
|
||||
|
||||
})();
|
||||
127
frontend/assets/js/homework-manage.js
Normal file
127
frontend/assets/js/homework-manage.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 作业管理页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const hwRole = window.PAGE_CONFIG.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) {
|
||||
window.selectedStudentIds = [studentId];
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`;
|
||||
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 hwTitle = document.getElementById('hwTitle').value.trim();
|
||||
const hwDeadline = document.getElementById('hwDeadline').value;
|
||||
const reasonEl = document.getElementById('pointsReason');
|
||||
|
||||
let prefix = '';
|
||||
if (subjectName) {
|
||||
prefix = `[${subjectName}]`;
|
||||
}
|
||||
if (hwTitle) {
|
||||
prefix += `[${hwTitle}]`;
|
||||
}
|
||||
if (hwDeadline) {
|
||||
prefix += ` 缴交:${hwDeadline}`;
|
||||
}
|
||||
if (prefix) {
|
||||
reasonEl.value = prefix + ' ' + reasonEl.value;
|
||||
}
|
||||
}
|
||||
|
||||
submitBatchPoints();
|
||||
}
|
||||
|
||||
loadStudents();
|
||||
loadSubjectsForHomework();
|
||||
|
||||
window.loadStudents = loadStudents;
|
||||
window.showSinglePointsModal = showSinglePointsModal;
|
||||
window.selectDeductionType = selectDeductionType;
|
||||
window.handleSubmitPoints = handleSubmitPoints;
|
||||
|
||||
})();
|
||||
53
frontend/assets/js/modules/admin-mgmt.js
Normal file
53
frontend/assets/js/modules/admin-mgmt.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理员管理函数
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 显示添加管理员模态框
|
||||
function showAddAdminModal() {
|
||||
document.getElementById('addAdminModal').style.display = 'flex';
|
||||
document.getElementById('addAdminForm')?.reset();
|
||||
}
|
||||
|
||||
// 提交添加管理员
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
window.showAddAdminModal = showAddAdminModal;
|
||||
window.submitAddAdmin = submitAddAdmin;
|
||||
})();
|
||||
24
frontend/assets/js/modules/modal-utils.js
Normal file
24
frontend/assets/js/modules/modal-utils.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 模态框工具函数
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 关闭模态框
|
||||
function closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
window.closeModal = closeModal;
|
||||
})();
|
||||
98
frontend/assets/js/modules/points-mgmt.js
Normal file
98
frontend/assets/js/modules/points-mgmt.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 加减分管理函数
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 全局变量
|
||||
var selectedStudentIds = [];
|
||||
var currentHistoryPage = 1;
|
||||
|
||||
// 显示批量加减分模态框
|
||||
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';
|
||||
}
|
||||
|
||||
// 提交批量加减分
|
||||
async function submitBatchPoints() {
|
||||
const pointsChange = parseInt(document.getElementById('pointsChange').value);
|
||||
const reason = document.getElementById('pointsReason').value;
|
||||
|
||||
if (isNaN(pointsChange) || pointsChange === 0) {
|
||||
showToast('分值不能为0', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reason.trim()) {
|
||||
showToast('请填写原因', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/conduct/add', {
|
||||
student_ids: selectedStudentIds,
|
||||
points_change: pointsChange,
|
||||
reason: reason
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast(`操作成功: ${res.data.success_count} 人成功`);
|
||||
closeModal('batchPointsModal');
|
||||
loadStudents();
|
||||
if (typeof loadConductStudents === 'function') loadConductStudents();
|
||||
} else {
|
||||
showToast(res?.message || '操作失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 撤销扣分记录
|
||||
async function revokeRecord(recordId) {
|
||||
if (!confirm('确定要撤销这条记录吗?撤销后学生分数将恢复。')) return;
|
||||
|
||||
const res = await apiPost('/api/admin/conduct/revoke', { record_id: recordId });
|
||||
if (res && res.success) {
|
||||
showToast('撤销成功');
|
||||
loadHistory(currentHistoryPage);
|
||||
} else {
|
||||
showToast(res?.message || '撤销失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 反撤销(恢复)记录
|
||||
async function restoreRecord(recordId) {
|
||||
if (!confirm('确定要反撤销这条记录吗?分数变动将重新生效。')) return;
|
||||
|
||||
const res = await apiPost('/api/admin/conduct/restore', { record_id: recordId });
|
||||
if (res && res.success) {
|
||||
showToast('反撤销成功');
|
||||
loadHistory(currentHistoryPage);
|
||||
} else {
|
||||
showToast(res?.message || '反撤销失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
window.showBatchPointsModal = showBatchPointsModal;
|
||||
window.submitBatchPoints = submitBatchPoints;
|
||||
window.revokeRecord = revokeRecord;
|
||||
window.restoreRecord = restoreRecord;
|
||||
})();
|
||||
233
frontend/assets/js/modules/student-mgmt.js
Normal file
233
frontend/assets/js/modules/student-mgmt.js
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 学生管理函数
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 显示新增学生模态框
|
||||
function showAddStudentModal() {
|
||||
document.getElementById('addStudentModal').style.display = 'flex';
|
||||
document.getElementById('addStudentForm').reset();
|
||||
}
|
||||
|
||||
// 提交新增学生
|
||||
async function submitAddStudent() {
|
||||
const studentNo = document.getElementById('studentNo').value.trim();
|
||||
const name = document.getElementById('studentName').value.trim();
|
||||
const parentPhone = document.getElementById('parentPhone').value.trim();
|
||||
|
||||
if (!studentNo || !name) {
|
||||
showToast('请填写学号和姓名', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/students', {
|
||||
student_no: studentNo,
|
||||
name: name,
|
||||
parent_phone: parentPhone,
|
||||
dormitory_number: document.getElementById('addDormitoryNumber').value.trim()
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('学生添加成功');
|
||||
closeModal('addStudentModal');
|
||||
loadStudents();
|
||||
} else {
|
||||
showToast(res?.message || '添加失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示编辑学生模态框
|
||||
function showEditStudentModal(studentId, studentNo, name, phone, dormitoryNumber) {
|
||||
document.getElementById('editStudentId').value = studentId;
|
||||
document.getElementById('editStudentNo').value = studentNo;
|
||||
document.getElementById('editStudentName').value = name;
|
||||
document.getElementById('editStudentPhone').value = phone || '';
|
||||
document.getElementById('editDormitoryNumber').value = dormitoryNumber || '';
|
||||
document.getElementById('editStudentModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// 提交编辑学生
|
||||
async function submitEditStudent() {
|
||||
const studentId = document.getElementById('editStudentId').value;
|
||||
const name = document.getElementById('editStudentName').value.trim();
|
||||
const phone = document.getElementById('editStudentPhone').value.trim();
|
||||
|
||||
if (!name) {
|
||||
showToast('请输入姓名', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPut(`/api/admin/students/${studentId}`, {
|
||||
name: name,
|
||||
parent_phone: phone || null,
|
||||
dormitory_number: document.getElementById('editDormitoryNumber').value.trim()
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('学生信息更新成功');
|
||||
closeModal('editStudentModal');
|
||||
location.reload();
|
||||
} else {
|
||||
showToast(res?.message || '更新失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示重置学生密码模态框
|
||||
function showResetStudentPasswordModal(studentId, name) {
|
||||
document.getElementById('resetStudentId').value = studentId;
|
||||
document.getElementById('resetStudentInfo').textContent = `正在重置学生 "${name}" 的密码`;
|
||||
document.getElementById('newStudentPassword').value = '';
|
||||
document.getElementById('resetStudentPasswordModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// 提交重置学生密码
|
||||
async function submitResetStudentPassword() {
|
||||
const studentId = document.getElementById('resetStudentId').value;
|
||||
const newPassword = document.getElementById('newStudentPassword').value;
|
||||
|
||||
if (!newPassword || newPassword.length < 6) {
|
||||
showToast('密码至少6位', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost(`/api/admin/students/reset-password/${studentId}`, {
|
||||
new_password: newPassword
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('密码重置成功');
|
||||
closeModal('resetStudentPasswordModal');
|
||||
} else {
|
||||
showToast(res?.message || '重置失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 删除学生
|
||||
async function deleteStudent(studentId, name) {
|
||||
if (!confirm(`确定要删除学生 "${name}" 吗?删除后学生账号将被禁用。`)) return;
|
||||
|
||||
const res = await apiDelete(`/api/admin/students/${studentId}`);
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('学生删除成功');
|
||||
location.reload();
|
||||
} else {
|
||||
showToast(res?.message || '删除失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示导入模态框
|
||||
function showImportModal() {
|
||||
document.getElementById('importModal').style.display = 'flex';
|
||||
document.getElementById('importPreview').style.display = 'none';
|
||||
document.getElementById('importPreview').innerHTML = '';
|
||||
document.getElementById('importBtn').style.display = 'none';
|
||||
document.getElementById('importFile').value = '';
|
||||
}
|
||||
|
||||
// 预览导入文件
|
||||
function previewImportFile() {
|
||||
const file = document.getElementById('importFile').files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
try {
|
||||
const data = JSON.parse(e.target.result);
|
||||
const students = data.students || [];
|
||||
|
||||
let html = '<h4>预览数据</h4><div class="table-wrapper"><table><thead><tr>';
|
||||
html += '<th>学号</th><th>姓名</th><th>家长手机号</th><th>初始密码</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
students.forEach(s => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(s.student_no || '')}</td>
|
||||
<td>${escapeHtml(s.name || '')}</td>
|
||||
<td>${escapeHtml(s.parent_phone || '')}</td>
|
||||
<td>${escapeHtml(s.password || '123456')}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += `</tbody></table></div><p>共 ${students.length} 条记录,初始操行分默认为60分</p>`;
|
||||
document.getElementById('importPreview').innerHTML = html;
|
||||
document.getElementById('importPreview').style.display = 'block';
|
||||
document.getElementById('importBtn').style.display = 'inline-block';
|
||||
} catch (error) {
|
||||
showToast('JSON格式错误', 'error');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
// 执行导入
|
||||
async function doImport() {
|
||||
const file = document.getElementById('importFile').files[0];
|
||||
if (!file) {
|
||||
showToast('请选择文件', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const token = getToken();
|
||||
const response = await fetch(`${API_BASE_URL}/api/admin/students/import`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast(result.message);
|
||||
closeModal('importModal');
|
||||
loadStudents();
|
||||
|
||||
// 显示详细导入结果
|
||||
if (result.data && result.data.results) {
|
||||
const failedList = result.data.results.filter(r => !r.success);
|
||||
if (failedList.length > 0) {
|
||||
let detail = '失败详情:\n';
|
||||
failedList.forEach(r => {
|
||||
detail += `${r.student_no || '未知'}: ${r.error}\n`;
|
||||
});
|
||||
alert(detail);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showToast(result.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定文件选择事件
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('importFile');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', previewImportFile);
|
||||
}
|
||||
});
|
||||
|
||||
window.showAddStudentModal = showAddStudentModal;
|
||||
window.submitAddStudent = submitAddStudent;
|
||||
window.showEditStudentModal = showEditStudentModal;
|
||||
window.submitEditStudent = submitEditStudent;
|
||||
window.showResetStudentPasswordModal = showResetStudentPasswordModal;
|
||||
window.submitResetStudentPassword = submitResetStudentPassword;
|
||||
window.deleteStudent = deleteStudent;
|
||||
window.showImportModal = showImportModal;
|
||||
window.previewImportFile = previewImportFile;
|
||||
window.doImport = doImport;
|
||||
})();
|
||||
47
frontend/assets/js/modules/subject-mgmt.js
Normal file
47
frontend/assets/js/modules/subject-mgmt.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 科目管理函数
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 显示添加科目模态框
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
window.showAddSubjectModal = showAddSubjectModal;
|
||||
window.submitAddSubject = submitAddSubject;
|
||||
})();
|
||||
38
frontend/assets/js/modules/utils.js
Normal file
38
frontend/assets/js/modules/utils.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 通用工具函数
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// HTML转义
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/[&<>]/g, function(m) {
|
||||
if (m === '&') return '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
// 全选功能
|
||||
function toggleSelectAll() {
|
||||
const selectAll = document.getElementById('selectAll');
|
||||
if (selectAll) {
|
||||
document.querySelectorAll('.student-checkbox').forEach(cb => {
|
||||
cb.checked = selectAll.checked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.escapeHtml = escapeHtml;
|
||||
window.toggleSelectAll = toggleSelectAll;
|
||||
})();
|
||||
293
frontend/assets/js/semesters.js
Normal file
293
frontend/assets/js/semesters.js
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 学期管理页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
let archiveSemesterId = null;
|
||||
let archivePage = 1;
|
||||
let archiveTotalPages = 1;
|
||||
let associateSemesterId = null;
|
||||
|
||||
function fillSemesterDates(type) {
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear();
|
||||
const currentMonth = now.getMonth() + 1;
|
||||
const startDateInput = document.getElementById('semesterStartDate');
|
||||
const endDateInput = document.getElementById('semesterEndDate');
|
||||
|
||||
if (type === 'upper') {
|
||||
const year = currentMonth >= 6 ? currentYear : currentYear - 1;
|
||||
const endYear = year + 1;
|
||||
let 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);
|
||||
});
|
||||
}
|
||||
|
||||
loadSemesters();
|
||||
|
||||
window.fillSemesterDates = fillSemesterDates;
|
||||
window.showCreateSemesterModal = showCreateSemesterModal;
|
||||
window.submitCreateSemester = submitCreateSemester;
|
||||
window.activateSemester = activateSemester;
|
||||
window.showEditSemesterModal = showEditSemesterModal;
|
||||
window.submitEditSemester = submitEditSemester;
|
||||
window.deleteSemester = deleteSemester;
|
||||
window.showAssociateConfirm = showAssociateConfirm;
|
||||
window.confirmAssociate = confirmAssociate;
|
||||
window.showArchiveConfirm = showArchiveConfirm;
|
||||
window.confirmArchive = confirmArchive;
|
||||
window.viewArchiveData = viewArchiveData;
|
||||
|
||||
})();
|
||||
38
frontend/assets/js/student-homework.js
Normal file
38
frontend/assets/js/student-homework.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 学生端作业情况JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const STUDENT_ID = window.PAGE_CONFIG.studentId;
|
||||
|
||||
async function loadHomework() {
|
||||
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.homework.forEach(hw => {
|
||||
const pointsDisplay = hw.points ? hw.points + '分' : '-';
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(hw.subject_name)}</td>
|
||||
<td>${hw.deadline || hw.created_at}</td>
|
||||
<td>${pointsDisplay}</td>
|
||||
<td>${escapeHtml(hw.comments || '-')}</td>
|
||||
<td>${escapeHtml(hw.title)}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.homework.length === 0) {
|
||||
html = '<tr><td colspan="5" style="text-align:center;">暂无作业</td></tr>';
|
||||
}
|
||||
document.getElementById('homeworkList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
loadHomework();
|
||||
|
||||
})();
|
||||
95
frontend/assets/js/students-manage.js
Normal file
95
frontend/assets/js/students-manage.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 学生管理页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const userRole = window.PAGE_CONFIG.role;
|
||||
let currentPage = 1;
|
||||
let 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>${escapeHtml(student.dormitory_number || '-')}</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 || '')}', '${escapeHtml(student.dormitory_number || '')}')">编辑</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 === '班主任' ? '7' : '6'}" 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) {
|
||||
window.selectedStudentIds = [studentId];
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`;
|
||||
document.getElementById('pointsChange').value = '';
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
window.loadStudents = loadStudents;
|
||||
window.showSinglePointsModal = showSinglePointsModal;
|
||||
window.unlockStudent = unlockStudent;
|
||||
|
||||
})();
|
||||
102
frontend/assets/js/subjects.js
Normal file
102
frontend/assets/js/subjects.js
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 科目管理页JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
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>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteSubject(${sub.subject_id})">删除</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');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSubject(subjectId) {
|
||||
if (!confirm('确定要删除该科目吗?')) return;
|
||||
const res = await apiDelete('/api/subject/delete/' + subjectId);
|
||||
if (res && res.success) {
|
||||
showToast('科目删除成功');
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
loadSubjects();
|
||||
|
||||
window.loadSubjects = loadSubjects;
|
||||
window.toggleSubject = toggleSubject;
|
||||
window.deleteSubject = deleteSubject;
|
||||
window.showEditSubjectModal = showEditSubjectModal;
|
||||
window.submitEditSubject = submitEditSubject;
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user