feat: 多班级版 v2.0 - Go后端重写 + 43轮代码审查

- 后端从 Python FastAPI 重写为 Go Gin(端口 56789)
- 多班级完全隔离
- 超级管理员独立登录
- 课代表作业管理、排行榜分项排行
- 角色加减分上下限可配置
- 家长改密功能(可开关)
- 周度/月度重置功能
- MySQL 5.7 兼容
- 43轮代码审查+全部修复
- Apache 2.0 许可证
This commit is contained in:
2026-06-22 10:06:10 +08:00
parent 4084afc53c
commit d6dec878bd
214 changed files with 12622 additions and 9725 deletions

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 管理端样式
* 多班级版班级管理系统 - 管理端样式
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 全局样式
* 多班级版班级管理系统 - 全局样式
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -691,6 +691,10 @@ tr:hover {
background: #ed8936;
}
.toast-info {
background: var(--color-primary);
}
@keyframes fadeInUp {
from {
opacity: 0;

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 管理员管理页JS
* 多班级版班级管理系统 - 管理员管理页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 考勤管理页JS
* 多班级版班级管理系统 - 考勤管理页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio

View File

@@ -0,0 +1,159 @@
/**
* 多班级版班级管理系统 - 课代表作业管理JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
*
* 版权所有 © Sea Network Technology Studio
*/
(function() {
'use strict';
var currentPage = 1;
var pageSize = 20;
var currentAssignmentId = null;
async function loadHomework(page) {
var res = await apiGet('/api/cadre/homework', { page: page, page_size: pageSize });
if (res && res.success && res.data) {
var items = res.data.items || res.data.records || [];
var total = res.data.total || 0;
var html = '';
if (items.length === 0) {
html = '<tr><td colspan="5" style="text-align:center;">暂无作业记录</td></tr>';
} else {
items.forEach(function(item) {
html += '<tr>' +
'<td>' + escapeHtml(item.title || '-') + '</td>' +
'<td>' + escapeHtml(item.subject_name || '-') + '</td>' +
'<td>' + formatDate(item.deadline) + '</td>' +
'<td>' + escapeHtml(item.description || '-') + '</td>' +
'<td><button class="btn btn-sm btn-outline" onclick="showAbsentModal(' + item.assignment_id + ')">登记缺交</button></td>' +
'</tr>';
});
}
document.getElementById('homeworkList').innerHTML = html;
var totalPages = Math.ceil(total / pageSize);
if (totalPages > 1) {
document.getElementById('pagination').style.display = 'flex';
document.getElementById('pageInfo').textContent = page + ' / ' + totalPages;
document.getElementById('prevBtn').disabled = page <= 1;
document.getElementById('nextBtn').disabled = page >= totalPages;
} else {
document.getElementById('pagination').style.display = 'none';
}
}
}
window.changePage = function(delta) {
currentPage += delta;
loadHomework(currentPage);
};
window.showPublishModal = function() {
document.getElementById('publishForm').reset();
document.getElementById('hwDeadline').value = new Date().toISOString().split('T')[0];
document.getElementById('publishModal').style.display = 'flex';
};
window.submitHomework = async function() {
var title = document.getElementById('hwTitle').value.trim();
var deadline = document.getElementById('hwDeadline').value;
var description = document.getElementById('hwDescription').value.trim();
if (!title) {
showToast('请填写作业标题', 'error');
return;
}
if (!deadline) {
showToast('请选择截止日期', 'error');
return;
}
var res = await apiPost('/api/cadre/homework', {
title: title,
deadline: deadline,
description: description
});
if (res && res.success) {
showToast('作业发布成功');
closeModal('publishModal');
loadHomework(currentPage);
} else {
showToast(res && res.message ? res.message : '发布失败', 'error');
}
};
window.showAbsentModal = async function(assignmentId) {
currentAssignmentId = assignmentId;
var res = await apiGet('/api/admin/students', { page_size: 1000 });
if (res && res.success && res.data) {
var students = res.data.students || res.data.items || [];
var html = '<div class="form-group"><label>选择缺交学生</label></div>';
if (students.length === 0) {
html += '<p style="text-align:center;padding:20px;">暂无学生数据</p>';
} else {
html += '<div class="table-wrapper"><table class="table"><thead><tr>' +
'<th><input type="checkbox" id="selectAllAbsent" onchange="toggleAllAbsent(this)"></th>' +
'<th>学号</th><th>姓名</th></tr></thead><tbody>';
students.forEach(function(s) {
html += '<tr>' +
'<td><input type="checkbox" class="absent-checkbox" data-id="' + s.student_id + '"></td>' +
'<td>' + escapeHtml(s.student_no) + '</td>' +
'<td>' + escapeHtml(s.name) + '</td>' +
'</tr>';
});
html += '</tbody></table></div>';
}
document.getElementById('absentStudentList').innerHTML = html;
document.getElementById('absentModal').style.display = 'flex';
} else {
showToast('获取学生列表失败', 'error');
}
};
window.toggleAllAbsent = function(el) {
var checkboxes = document.querySelectorAll('.absent-checkbox');
checkboxes.forEach(function(cb) { cb.checked = el.checked; });
};
window.submitAbsent = async function() {
var checkboxes = document.querySelectorAll('.absent-checkbox:checked');
if (checkboxes.length === 0) {
showToast('请选择至少一名缺交学生', 'error');
return;
}
var studentIds = [];
checkboxes.forEach(function(cb) {
studentIds.push(parseInt(cb.getAttribute('data-id')));
});
var hwDeduct = window.DEDUCTION_HOMEWORK_NOT_SUBMIT || 2;
var res = await apiPost('/api/cadre/conduct/add', {
student_ids: studentIds,
points_change: -hwDeduct,
reason: '作业未提交',
related_type: 'homework'
});
if (res && res.success) {
showToast('已登记 ' + studentIds.length + ' 名学生缺交');
closeModal('absentModal');
} else {
showToast(res && res.message ? res.message : '提交失败', 'error');
}
};
window.closeModal = function(id) {
document.getElementById(id).style.display = 'none';
};
document.addEventListener('DOMContentLoaded', function() {
loadHomework(currentPage);
});
})();

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 公共JS
* 多班级版班级管理系统 - 公共JS
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -18,7 +18,7 @@ function getUserInfo() {
if (!userStr) return null;
try {
return JSON.parse(userStr);
} catch {
} catch (e) {
return null;
}
}
@@ -191,11 +191,13 @@ async function logout() {
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '\x26amp;')
.replace(/</g, '\x26lt;')
.replace(/>/g, '\x26gt;')
.replace(/"/g, '\x26quot;')
.replace(/'/g, '\x26#x27;');
.replace(/&/g, '&amp;')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, '&#x27;')
.replace(/`/g, '&#x60;')
.replace(/\//g, '&#x2F;');
}
/**
@@ -383,21 +385,19 @@ document.addEventListener('click', function(e) {
}
});
// 全局textarea键盘事件Enter提交表单Ctrl+Enter换行
// 全局textarea键盘事件Ctrl+Enter提交表单Enter换行(默认行为)
document.addEventListener('keydown', function(e) {
if (e.target.tagName !== 'TEXTAREA') return;
if (e.key === 'Enter' && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
// Enter键提交表单
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
// Ctrl+Enter / Cmd+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保持默认换行行为不拦截
});
window.selectDeductionType = function(points, reason) {

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 操行分管理页JS
* 多班级版班级管理系统 - 操行分管理页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
@@ -20,7 +20,7 @@ async function loadStudents() {
<td>${escapeHtml(student.student_no)}</td>
<td><a href="/admin/history.php?student_id=${student.student_id}" class="link">${escapeHtml(student.name)}</a></td>
<td>${student.total_points}</td>
<td><button class="btn btn-sm btn-outline" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button></td>
<td><button class="btn btn-sm btn-outline js-single-points" data-student-id="${student.student_id}" data-student-name="${escapeHtml(student.name)}">加减分</button></td>
</tr>`;
});
if (res.data.students.length === 0) {
@@ -39,7 +39,7 @@ function showSinglePointsModal(studentId, studentName) {
}
async function exportMoralityRecords() {
showToast('正在导出德育分记录...', 'info');
showToast('正在导出操行分记录...', 'info');
try {
const studentsRes = await apiGet('/api/admin/students', { page_size: 1000 });
@@ -54,13 +54,20 @@ async function exportMoralityRecords() {
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 allRecords = [];
let page = 1;
let totalPages = 1;
do {
const historyRes = await apiGet('/api/admin/conduct/history', { page: page, page_size: 500 });
if (!historyRes || !historyRes.success) {
showToast('获取历史记录失败', 'error');
return;
}
const records = historyRes.data.records || [];
allRecords.push(...records);
totalPages = historyRes.data.total_pages || 1;
page++;
} while (page <= totalPages);
const recordsByStudent = {};
allRecords.forEach(record => {
@@ -90,7 +97,7 @@ async function exportMoralityRecords() {
if (field === null || field === undefined) return '';
let str = String(field).replace(/[\r\n]+/g, ' ');
str = str.replace(/"/g, '""');
if (/[\,\;\"\s]/.test(str)) {
if (/[\,\"\s]/.test(str)) {
str = '"' + str + '"';
}
return str;
@@ -106,7 +113,7 @@ async function exportMoralityRecords() {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `德育分记录_${new Date().toISOString().slice(0,10)}.csv`;
link.download = `操行分记录_${new Date().toISOString().slice(0,10)}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
@@ -119,7 +126,7 @@ async function exportMoralityRecords() {
}
}
// 宿舍集体加分相关
var dormitoryStudentIds = [];
let dormitoryStudentIds = [];
async function showDormitoryPointsModal() {
dormitoryStudentIds = [];
@@ -196,6 +203,11 @@ async function submitDormitoryPoints() {
return;
}
if (Math.abs(pointsChange) > 100) {
showToast('分值绝对值不能超过100', 'error');
return;
}
if (!reason.trim()) {
showToast('请填写原因', 'error');
return;
@@ -204,7 +216,8 @@ async function submitDormitoryPoints() {
const data = {
student_ids: dormitoryStudentIds,
points_change: pointsChange,
reason: reason
reason: reason,
related_type: 'manual'
};
const res = await apiPost('/api/admin/conduct/add', data);
@@ -220,6 +233,16 @@ async function submitDormitoryPoints() {
loadStudents();
document.getElementById('studentList').addEventListener('click', function(e) {
const btn = e.target.closest('.js-single-points');
if (btn) {
showSinglePointsModal(
parseInt(btn.dataset.studentId, 10),
btn.dataset.studentName
);
}
});
window.loadStudents = loadStudents;
window.showSinglePointsModal = showSinglePointsModal;
window.exportMoralityRecords = exportMoralityRecords;

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 管理端首页JS
* 多班级版班级管理系统 - 管理端首页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 历史记录页JS
* 多班级版班级管理系统 - 历史记录页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
@@ -15,11 +15,14 @@ const currentUserId = window.PAGE_CONFIG.userId;
let currentHistoryPage = 1;
let totalHistoryPages = 1;
function escapeHtml(str) {
if (!str) return '';
var el = document.createElement('span');
el.appendChild(document.createTextNode(str));
return el.innerHTML;
function getTypeLabel(relatedType) {
if (!relatedType) return '操行';
switch (relatedType) {
case 'conduct': return '操行';
case 'homework': return '作业';
case 'attendance': return '考勤';
default: return relatedType;
}
}
async function loadStudentsForSelect() {
@@ -35,9 +38,9 @@ async function loadStudentsForSelect() {
// 加载科目下拉列表
async function loadSubjectsForFilter() {
var subjectSelect = document.getElementById('historySubjectFilter');
let subjectSelect = document.getElementById('historySubjectFilter');
if (!subjectSelect) return;
var res = await apiGet('/api/subject/list', { is_active: true });
let res = await apiGet('/api/subject/list', { is_active: true });
if (res && res.success && res.data && res.data.subjects) {
let html = '<option value="">全部科目</option>';
res.data.subjects.forEach(s => {
@@ -49,16 +52,16 @@ async function loadSubjectsForFilter() {
// 筛选学生时自动取消合并记录
function onStudentFilterChange() {
var studentId = document.getElementById('historyStudentId').value;
let studentId = document.getElementById('historyStudentId').value;
if (studentId) {
var grouped = document.getElementById('historyGrouped');
let grouped = document.getElementById('historyGrouped');
if (grouped) grouped.checked = false;
}
}
// 科目筛选变化时,取消扣分类型筛选(互斥)
function onSubjectFilterChange() {
var subjectVal = document.getElementById('historySubjectFilter').value;
let subjectVal = document.getElementById('historySubjectFilter').value;
if (subjectVal) {
document.getElementById('historyReasonFilter').value = '';
}
@@ -66,8 +69,8 @@ function onSubjectFilterChange() {
// 折叠/展开筛选面板
function toggleFilterPanel() {
var panel = document.getElementById('advancedFilters');
var btn = document.getElementById('filterToggleBtn');
let panel = document.getElementById('advancedFilters');
let btn = document.getElementById('filterToggleBtn');
if (!panel || !btn) return;
if (panel.style.display === 'none') {
panel.style.display = 'block';
@@ -81,19 +84,19 @@ function toggleFilterPanel() {
async function loadHistory(page) {
page = page || 1;
currentHistoryPage = page;
var startDate = document.getElementById('historyStartDate').value;
var endDate = document.getElementById('historyEndDate').value;
var studentId = document.getElementById('historyStudentId').value;
var reasonFilter = document.getElementById('historyReasonFilter').value;
var subjectFilter = document.getElementById('historySubjectFilter').value;
var reasonSearch = document.getElementById('historyReasonSearch').value.trim();
var isGrouped = document.getElementById('historyGrouped').checked;
var statusFilter = document.getElementById('historyStatusFilter') ? document.getElementById('historyStatusFilter').value : '';
let startDate = document.getElementById('historyStartDate').value;
let endDate = document.getElementById('historyEndDate').value;
let studentId = document.getElementById('historyStudentId').value;
let reasonFilter = document.getElementById('historyReasonFilter').value;
let subjectFilter = document.getElementById('historySubjectFilter').value;
let reasonSearch = document.getElementById('historyReasonSearch').value.trim();
let isGrouped = document.getElementById('historyGrouped').checked;
let statusFilter = document.getElementById('historyStatusFilter') ? document.getElementById('historyStatusFilter').value : '';
// 筛选学生时强制取消合并
if (studentId) isGrouped = false;
var params = {
let params = {
page: page, page_size: 20,
start_date: startDate,
end_date: endDate
@@ -111,37 +114,38 @@ async function loadHistory(page) {
if (isGrouped) params.grouped = true;
if (statusFilter !== '') params.is_revoked = parseInt(statusFilter);
var res = await apiGet('/api/admin/conduct/history', params);
let res = await apiGet('/api/admin/conduct/history', params);
if (res && res.success) {
var nowrapStyle = ' style="white-space:nowrap;min-width:80px;"';
var headHtml = '';
let nowrapStyle = ' style="white-space:nowrap;min-width:80px;"';
let headHtml = '';
if (isGrouped) {
headHtml = '<th>时间</th><th>原因</th><th>分值</th><th' + nowrapStyle + '>操作人</th><th>涉及学生</th>';
headHtml = '<th>类型</th><th>分值</th><th>原因</th><th>学生名单</th><th' + nowrapStyle + '>操作人</th><th>时间</th>';
if (role === '班主任' || role === '班长') {
headHtml += '<th>操作</th>';
}
} else {
headHtml = '<th>时间</th><th>学生</th><th>分数变动</th><th>原因</th><th' + nowrapStyle + '>操作人</th>';
headHtml = '<th>类型</th><th>分值</th><th>原因</th><th>学生</th><th' + nowrapStyle + '>操作人</th><th>时间</th>';
if (role === '班主任' || role === '班长' || role === '考勤委员') {
headHtml += '<th>操作</th>';
}
}
document.getElementById('historyTableHead').innerHTML = headHtml;
var html = '';
let html = '';
if (isGrouped) {
res.data.records.forEach(function(record) {
var pointsClass = record.points_change > 0 ? 'plus' : 'minus';
var names = record.student_names || '';
var allRevoked = record.all_revoked;
var revokedStyle = allRevoked ? ' style="opacity:0.5;text-decoration:line-through;"' : '';
let pointsClass = record.points_change > 0 ? 'plus' : 'minus';
let names = record.student_names || '';
let allRevoked = record.all_revoked;
let revokedStyle = allRevoked ? ' style="opacity:0.5;text-decoration:line-through;"' : '';
html += '<tr' + revokedStyle + '>' +
'<td class="history-time">' + formatDateTime(record.created_at) + '</td>' +
'<td class="history-reason">' + escapeHtml(record.reason) + '</td>' +
'<td>' + escapeHtml(getTypeLabel(record.related_type)) + '</td>' +
'<td class="' + pointsClass + '">' + (record.points_change > 0 ? '+' : '') + record.points_change + '&times;' + record.student_count + '</td>' +
'<td class="history-reason">' + escapeHtml(record.reason) + '</td>' +
'<td class="history-students">' + escapeHtml(names) + '</td>' +
'<td>' + escapeHtml(record.recorder_name || '') + '</td>' +
'<td class="history-students">' + escapeHtml(names) + '</td>';
'<td class="history-time">' + formatDateTime(record.created_at) + '</td>';
if (role === '班主任' || role === '班长') {
if (allRevoked) {
html += '<td><span class="text-muted">已撤销</span></td>';
@@ -152,29 +156,30 @@ async function loadHistory(page) {
html += '</tr>';
});
if (res.data.records.length === 0) {
var colSpan = (role === '班主任' || role === '班长') ? 6 : 5;
let colSpan = (role === '班主任' || role === '班长') ? 7 : 6;
html = '<tr><td colspan="' + colSpan + '" style="text-align:center;">暂无记录</td></tr>';
}
} else {
res.data.records.forEach(function(record) {
var pointsClass = record.points_change > 0 ? 'plus' : 'minus';
var revokedStyle = record.is_revoked == 1 ? ' style="opacity:0.5;text-decoration:line-through;"' : '';
let pointsClass = record.points_change > 0 ? 'plus' : 'minus';
let revokedStyle = record.is_revoked == 1 ? ' style="opacity:0.5;text-decoration:line-through;"' : '';
html += '<tr' + revokedStyle + '>' +
'<td class="history-time">' + formatDateTime(record.created_at) + '</td>' +
'<td>' + escapeHtml(record.student_name) + '</td>' +
'<td>' + escapeHtml(getTypeLabel(record.related_type)) + '</td>' +
'<td class="' + pointsClass + '">' + (record.points_change > 0 ? '+' : '') + record.points_change + '</td>' +
'<td class="history-reason">' + escapeHtml(record.reason) + '</td>' +
'<td>' + escapeHtml(record.recorder_name) + '</td>';
'<td>' + escapeHtml(record.student_name) + '</td>' +
'<td>' + escapeHtml(record.recorder_name) + '</td>' +
'<td class="history-time">' + formatDateTime(record.created_at) + '</td>';
if (role === '班主任') {
if (record.is_revoked == 1) {
var revokerInfo = record.revoker_name ? '由 ' + escapeHtml(record.revoker_name) + ' 撤销' : '已撤销';
let 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-outline" onclick="restoreRecord(' + record.record_id + ')">反撤销</button></td>';
} else {
html += '<td><button class="btn btn-sm btn-outline" onclick="revokeRecord(' + record.record_id + ')">撤销</button></td>';
}
} else if (role === '班长') {
if (record.is_revoked == 1) {
var revokerInfo = record.revoker_name ? '由 ' + escapeHtml(record.revoker_name) + ' 撤销' : '已撤销';
let 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-outline" onclick="revokeRecord(' + record.record_id + ')">撤销</button></td>';
@@ -192,7 +197,7 @@ async function loadHistory(page) {
});
if (res.data.records.length === 0) {
var colSpan = (role === '班主任' || role === '班长' || role === '考勤委员') ? 6 : 5;
let colSpan = (role === '班主任' || role === '班长' || role === '考勤委员') ? 7 : 6;
html = '<tr><td colspan="' + colSpan + '" style="text-align:center;">暂无记录</td></tr>';
}
}
@@ -211,17 +216,17 @@ function renderHistoryPagination() {
}
async function exportHistoryRecords() {
var startDate = document.getElementById('historyStartDate').value;
var endDate = document.getElementById('historyEndDate').value;
var studentId = document.getElementById('historyStudentId').value;
let startDate = document.getElementById('historyStartDate').value;
let endDate = document.getElementById('historyEndDate').value;
let studentId = document.getElementById('historyStudentId').value;
showToast('正在导出历史记录...', 'info');
try {
var reasonFilter = document.getElementById('historyReasonFilter').value;
var subjectFilter = document.getElementById('historySubjectFilter').value;
var reasonSearch = document.getElementById('historyReasonSearch').value.trim();
var params = { page: 1, page_size: 1000 };
let reasonFilter = document.getElementById('historyReasonFilter').value;
let subjectFilter = document.getElementById('historySubjectFilter').value;
let reasonSearch = document.getElementById('historyReasonSearch').value.trim();
let params = { page: 1, page_size: 1000 };
if (startDate) params.start_date = startDate;
if (endDate) params.end_date = endDate;
if (studentId) params.student_id = studentId;
@@ -232,24 +237,31 @@ async function exportHistoryRecords() {
}
if (reasonSearch) params.reason_search = reasonSearch;
var res = await apiGet('/api/admin/conduct/history', params);
let res = await apiGet('/api/admin/conduct/history', params);
if (res && res.success && res.data.records) {
var records = res.data.records;
let records = res.data.records;
if (records.length === 0) {
showToast('没有找到记录', 'warning');
return;
}
var csv = '\uFEFF';
function csvField(val) {
let s = String(val == null ? '' : val);
if (s.indexOf(',') >= 0 || s.indexOf('"') >= 0 || s.indexOf('\n') >= 0) {
return '"' + s.replace(/"/g, '""') + '"';
}
return s;
}
let csv = '\uFEFF';
csv += '时间,学号,姓名,分数变动,原因,操作人\n';
records.forEach(function(r) {
if (r.is_revoked == 1) return;
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';
csv += csvField(r.created_at) + ',' + csvField(r.student_no) + ',' + csvField(r.student_name) + ',' + csvField((r.points_change > 0 ? '+' : '') + r.points_change) + ',' + csvField(r.reason) + ',' + csvField(r.recorder_name) + '\n';
});
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
let blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
let url = URL.createObjectURL(blob);
let link = document.createElement('a');
link.href = url;
link.download = '历史记录_' + new Date().toISOString().slice(0,10) + '.csv';
document.body.appendChild(link);
@@ -273,21 +285,21 @@ async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt)
showToast('正在批量撤销...', 'info');
try {
var params = {
let params = {
page: 1, page_size: 1000,
start_date: document.getElementById('historyStartDate').value,
end_date: document.getElementById('historyEndDate').value,
reason_prefix: reason.substring(0, 4),
reason_prefix: reason,
grouped: false
};
var res = await apiGet('/api/admin/conduct/history', params);
let res = await apiGet('/api/admin/conduct/history', params);
if (!res || !res.success || !res.data.records) {
showToast('查询记录失败', 'error');
return;
}
var matchedIds = [];
let matchedIds = [];
res.data.records.forEach(function(r) {
if (r.reason === reason && r.points_change === pointsChange && r.is_revoked == 0) {
matchedIds.push(r.record_id);
@@ -299,7 +311,7 @@ async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt)
return;
}
var revokeRes = await apiPost('/api/admin/conduct/batch-revoke', { record_ids: matchedIds });
let revokeRes = await apiPost('/api/admin/conduct/batch-revoke', { record_ids: matchedIds });
if (revokeRes && revokeRes.success) {
showToast('批量撤销完成: ' + (revokeRes.data ? revokeRes.data.success_count : 0) + '条成功');
loadHistory(currentHistoryPage);
@@ -313,8 +325,8 @@ async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt)
// 初始化:并行加载学生和科目列表,然后加载历史记录
Promise.all([loadStudentsForSelect(), loadSubjectsForFilter()]).then(function() {
var urlParams = new URLSearchParams(window.location.search);
var preStudentId = urlParams.get('student_id');
let urlParams = new URLSearchParams(window.location.search);
let preStudentId = urlParams.get('student_id');
if (preStudentId) {
document.getElementById('historyStudentId').value = preStudentId;
onStudentFilterChange();

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 作业扣分页JS
* 多班级版班级管理系统 - 作业扣分页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
@@ -187,7 +187,7 @@ async function submitAddSubject() {
}
async function toggleSubjectStatus(subjectId, enable) {
const res = await apiPut(`/api/subject/update/${subjectId}`, { is_active: enable });
const res = await apiPut(`/api/subject/toggle/${subjectId}`, { is_active: enable });
if (res && res.success) {
showToast(enable ? '科目已启用' : '科目已禁用');
loadSubjectList();

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 管理员管理函数
* 多班级版班级管理系统 - 管理员管理函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 模态框工具函数
* 多班级版班级管理系统 - 模态框工具函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 加减分管理函数
* 多班级版班级管理系统 - 加减分管理函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 学生管理函数
* 多班级版班级管理系统 - 学生管理函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -32,7 +32,7 @@
const res = await apiPost('/api/admin/students', {
student_no: studentNo,
name: name,
parent_phone: parentPhone,
parent_account: parentPhone,
dormitory_number: document.getElementById('addDormitoryNumber').value.trim()
});
@@ -68,7 +68,7 @@
const res = await apiPut(`/api/admin/students/${studentId}`, {
name: name,
parent_phone: phone || null,
parent_account: phone || null,
dormitory_number: document.getElementById('editDormitoryNumber').value.trim()
});
@@ -146,14 +146,14 @@
const students = data.students || [];
let html = '<h4>预览数据</h4><div class="table-wrapper"><table><thead><tr>';
html += '<th>学号</th><th>姓名</th><th>家长手机号</th><th>宿舍号</th><th>初始密码</th>';
html += '<th>学号</th><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.parent_account || '')}</td>
<td>${escapeHtml(s.dormitory_number || '-')}</td>
<td>${escapeHtml(s.password || '123456')}</td>
</tr>`;

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 科目管理函数
* 多班级版班级管理系统 - 科目管理函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 通用工具函数
* 多班级版班级管理系统 - 通用工具函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 家长端JS
* 多班级版班级管理系统 - 家长端JS
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -0,0 +1,59 @@
/**
* 多班级版班级管理系统 - 排行榜JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
*
* 版权所有 © Sea Network Technology Studio
*/
(function() {
'use strict';
let currentType = 'conduct';
async function loadRankings(type) {
const res = await apiGet('/api/admin/rankings', { type: type, limit: 50 });
if (res && res.success && res.data) {
const rankings = res.data.ranking || [];
let html = '';
if (rankings.length === 0) {
html = '<tr><td colspan="4" style="text-align:center;">暂无排行数据</td></tr>';
} else {
rankings.forEach(function(item, index) {
let rankClass = '';
if (index === 0) rankClass = 'rank-gold';
else if (index === 1) rankClass = 'rank-silver';
else if (index === 2) rankClass = 'rank-bronze';
let pointsText = Number(item.points !== undefined ? item.points : (item.total_points || 0));
if (pointsText > 0) {
pointsText = '+' + pointsText;
}
html += '<tr>' +
'<td><span class="rank-badge ' + rankClass + '">' + (index + 1) + '</span></td>' +
'<td>' + escapeHtml(item.student_no || '-') + '</td>' +
'<td>' + escapeHtml(item.name || '-') + '</td>' +
'<td><span class="record-points ' + (pointsText > 0 ? 'plus' : (pointsText < 0 ? 'minus' : '')) + '">' + pointsText + '</span></td>' +
'</tr>';
});
}
document.getElementById('rankingList').innerHTML = html;
} else {
document.getElementById('rankingList').innerHTML = '<tr><td colspan="4" style="text-align:center;">加载失败</td></tr>';
}
}
window.switchTab = function(type, btn) {
currentType = type;
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
loadRankings(type);
};
document.addEventListener('DOMContentLoaded', function() {
loadRankings(currentType);
});
})();

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 学期管理页JS
* 多班级版班级管理系统 - 学期管理页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
@@ -41,7 +41,7 @@ async function loadSemesters() {
const res = await apiGet('/api/semester/list');
if (res && res.success) {
let html = '';
const semesters = res.data || [];
const semesters = res.data.semesters || [];
semesters.forEach(sem => {
let statusText = '';
let statusClass = '';
@@ -252,7 +252,7 @@ async function viewArchiveData(semesterId, semesterName, page) {
if (res && res.success) {
const data = res.data || {};
const archives = data.archives || [];
const archives = data.items || [];
let html = '';
archives.forEach(a => {
html += `<tr>
@@ -288,6 +288,80 @@ function renderArchivePagination(semesterId, semesterName) {
});
}
// ========== 周期重置功能 ==========
let pendingPeriodType = null;
let periodArchivesType = null;
let periodArchivesPage = 1;
let periodArchivesTotalPages = 1;
function confirmPeriodReset(periodType) {
pendingPeriodType = periodType;
const label = periodType === 'weekly' ? '本周' : '本月';
document.getElementById('periodResetText').innerHTML =
`确定要执行 <strong>${label}重置</strong> 吗?<br>将保存当前所有学生的操行分快照,然后将所有学生操行分重置为初始值。`;
document.getElementById('periodResetModal').style.display = 'flex';
}
async function executePeriodReset() {
if (!pendingPeriodType) return;
const res = await apiPost('/api/semester/period-reset', { period: pendingPeriodType });
if (res && res.success) {
showToast(res.message || '重置成功');
closeModal('periodResetModal');
pendingPeriodType = null;
} else {
showToast(res?.message || '重置失败', 'error');
}
}
async function showPeriodArchives(type, page) {
var periodType = type || periodArchivesType;
page = page || 1;
periodArchivesType = periodType;
periodArchivesPage = page;
const label = periodType === 'weekly' ? '周' : '月';
document.getElementById('periodArchivesTitle').textContent = label + '归档数据';
const res = await apiGet('/api/semester/period-archives', {
period: periodType,
page: page,
page_size: 50
});
if (res && res.success) {
const data = res.data || {};
const archives = data.items || [];
let html = '';
archives.forEach(function(a) {
const resetByLabel = a.reset_by === 'auto' ? '自动' : '手动';
html += '<tr>' +
'<td>' + escapeHtml(a.period_label) + '</td>' +
'<td>' + (a.rank_position || '-') + '</td>' +
'<td>' + escapeHtml(a.student_no) + '</td>' +
'<td>' + escapeHtml(a.student_name) + '</td>' +
'<td>' + a.final_points + '</td>' +
'<td>' + resetByLabel + '</td>' +
'<td>' + formatDateTime(a.archived_at) + '</td>' +
'</tr>';
});
if (archives.length === 0) {
html = '<tr><td colspan="7" style="text-align:center;">暂无归档数据</td></tr>';
}
document.getElementById('periodArchivesList').innerHTML = html;
periodArchivesTotalPages = data.total_pages || 1;
renderSmartPagination('periodArchivePagination', periodArchivesPage, periodArchivesTotalPages, function(p) {
showPeriodArchives(periodArchivesType, p);
});
document.getElementById('periodArchivesModal').style.display = 'flex';
} else {
showToast(res?.message || '获取归档数据失败', 'error');
}
}
loadSemesters();
window.fillSemesterDates = fillSemesterDates;
@@ -302,5 +376,8 @@ window.confirmAssociate = confirmAssociate;
window.showArchiveConfirm = showArchiveConfirm;
window.confirmArchive = confirmArchive;
window.viewArchiveData = viewArchiveData;
window.confirmPeriodReset = confirmPeriodReset;
window.executePeriodReset = executePeriodReset;
window.showPeriodArchives = showPeriodArchives;
})();

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 学生端作业情况JS
* 多班级版班级管理系统 - 学生端作业情况JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 学生端JS
* 多班级版班级管理系统 - 学生端JS
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 学生管理页JS
* 多班级版班级管理系统 - 学生管理页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
@@ -28,13 +28,13 @@ async function loadStudents(page = 1) {
<td><a href="/admin/history.php?student_id=${student.student_id}" class="link">${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>` : ''}
${userRole === '班主任' ? `<td>${student.parent_account ? student.parent_account.slice(0,3) + '******' + student.parent_account.slice(-2) : '-'}</td>` : ''}
<td>
<div class="action-dropdown">
<button class="btn btn-sm btn-outline" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button>
${userRole === '班主任' ? `<button class="btn btn-sm action-dropdown-toggle" onclick="toggleActionDropdown(this)">更多 ▼</button>
<div class="action-dropdown-menu">
<a onclick="showEditStudentModal(${student.student_id}, '${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}', '${escapeHtml(student.parent_phone || '')}', '${escapeHtml(student.dormitory_number || '')}')">编辑</a>
<a onclick="showEditStudentModal(${student.student_id}, '${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}', '${escapeHtml(student.parent_account || '')}', '${escapeHtml(student.dormitory_number || '')}')">编辑</a>
<a onclick="showResetStudentPasswordModal(${student.student_id}, '${escapeHtml(student.name)}')">重置密码</a>
<a onclick="unlockStudent('${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}')">解锁</a>
<a class="danger" onclick="deleteStudent(${student.student_id}, '${escapeHtml(student.name)}')">删除</a>

View File

@@ -1,49 +1,23 @@
{
"_comment1": "================================================",
"_comment2": "班级操行分管理系统 - 学生批量导入模板",
"_comment3": "开发者: Canglan | 版权: Sea Network Technology Studio",
"_comment4": "================================================",
"_comment5": "字段说明:",
"_comment6": " student_no - 必填,学生学号,唯一标识",
"_comment7": " name - 必填,学生姓名",
"_comment8": " parent_phone - 可选家长手机号11位手机号",
"_comment9": " dormitory_number - 可选,宿舍号(支持字母数字组合,如 301-A",
"_comment10": " password - 可选,初始密码,不填则默认 123456",
"_comment11": "================================================",
"_comment12": "导入规则:",
"_comment13": " 1. 学生操行分初始值 = 60分",
"_comment14": " 2. 学生账号 = 学号,密码 = 指定的password或123456",
"_comment15": " 3. 家长账号 = 手机号若parent_phone有值密码 = 指定的password或123456",
"_comment16": " 4. 家长姓名默认显示为 '学生姓名家长'",
"_comment17": "================================================",
"students": [
{
"student_no": "20240001",
"name": "张三",
"parent_phone": "13800138001",
"dormitory_number": "301-A",
"password": "123456"
},
{
"student_no": "20240002",
"name": "李四",
"parent_phone": "13800138002",
"dormitory_number": "205",
"password": "123456"
},
{
"student_no": "20240003",
"name": "王五",
"parent_phone": "",
"dormitory_number": "",
"password": ""
},
{
"student_no": "20240004",
"name": "赵六",
"parent_phone": "13800138004",
"dormitory_number": "102-B",
"password": ""
}
]
}
"students": [
{
"student_no": "2025001",
"name": "张三",
"parent_account": "13800138001",
"dormitory_number": "A301",
"password": "123456"
},
{
"student_no": "2025002",
"name": "李四",
"parent_account": "13800138002",
"dormitory_number": "A302"
},
{
"student_no": "2025003",
"name": "王五",
"parent_account": "",
"dormitory_number": "B101"
}
]
}