v1.7版本更新

This commit is contained in:
2026-05-21 20:15:56 +08:00
parent 74a71ddaf5
commit cb0c367eb7
54 changed files with 2292 additions and 1785 deletions

View File

@@ -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;
}

View File

@@ -473,6 +473,11 @@ tr:hover {
margin-top: 4px;
}
.form-group textarea {
min-height: 60px;
resize: vertical;
}
/* ========== 复选框组 ========== */
.checkbox-group {
display: flex;

View File

@@ -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 '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
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');
}
}

View 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;
})();

View 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;
})();

View File

@@ -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保持默认换行行为不拦截
});

View 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;
})();

View 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;
})();

View 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;
})();

View 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;
})();

View 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;
})();

View 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;
})();

View 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;
})();

View 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;
})();

View 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;
})();

View 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;
})();

View 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;
})();

View 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();
})();

View 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;
})();

View 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;
})();