feat: 多班级版 v2.0 - Go后端重写 + 43轮代码审查
- 后端从 Python FastAPI 重写为 Go Gin(端口 56789) - 多班级完全隔离 - 超级管理员独立登录 - 课代表作业管理、排行榜分项排行 - 角色加减分上下限可配置 - 家长改密功能(可开关) - 周度/月度重置功能 - MySQL 5.7 兼容 - 43轮代码审查+全部修复 - Apache 2.0 许可证
This commit is contained in:
@@ -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 + '×' + 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();
|
||||
|
||||
Reference in New Issue
Block a user