v2.5.1更新

This commit is contained in:
2026-05-29 20:16:25 +08:00
parent fe58ee1d23
commit b2c36cab2c
22 changed files with 347 additions and 166 deletions

View File

@@ -337,25 +337,40 @@ function toggleActionDropdown(el) {
var isOpen = menu.classList.contains('show');
// 先关闭所有
document.querySelectorAll('.action-dropdown-menu.show').forEach(function(m) {
m.classList.remove('show');
var toggle = m.closest('.action-dropdown').querySelector('.action-dropdown-toggle');
if (toggle) toggle.classList.remove('open');
});
closeAllDropdowns();
if (!isOpen) {
// 使用 fixed 定位,避免被 overflow 容器裁剪
var rect = el.getBoundingClientRect();
menu.style.position = 'fixed';
menu.style.bottom = 'auto';
menu.style.right = 'auto';
menu.style.left = rect.right - 120 + 'px'; // 120px = min-width
menu.style.top = (rect.top - 4) + 'px';
menu.style.transform = 'translateY(-100%)';
menu.classList.add('show');
el.classList.add('open');
}
}
function closeAllDropdowns() {
document.querySelectorAll('.action-dropdown-menu.show').forEach(function(m) {
m.classList.remove('show');
m.style.position = '';
m.style.left = '';
m.style.top = '';
m.style.transform = '';
var toggle = m.closest('.action-dropdown');
if (toggle) {
var btn = toggle.querySelector('.action-dropdown-toggle');
if (btn) btn.classList.remove('open');
}
});
}
document.addEventListener('click', function(e) {
if (!e.target.closest('.action-dropdown')) {
document.querySelectorAll('.action-dropdown-menu.show').forEach(function(m) {
m.classList.remove('show');
var toggle = m.closest('.action-dropdown').querySelector('.action-dropdown-toggle');
if (toggle) toggle.classList.remove('open');
});
closeAllDropdowns();
}
});

View File

@@ -14,9 +14,15 @@ const role = window.PAGE_CONFIG.role;
let totalStudents = 0;
async function loadDashboard() {
const studentsRes = await apiGet('/api/admin/students');
// 并行加载学生数据和学期信息
const [studentsRes, semesterRes] = await Promise.all([
apiGet('/api/admin/students'),
apiGet('/api/semester/active')
]);
let statsHtml = '';
if (studentsRes && studentsRes.success) {
document.getElementById('dashboardStats').innerHTML = `
statsHtml += `
<div class="stat-card">
<div class="stat-label">学生总数</div>
<div class="stat-value">${studentsRes.data.total || 0}</div>
@@ -24,6 +30,24 @@ async function loadDashboard() {
`;
}
// 显示学期信息和当前周数
if (semesterRes && semesterRes.success && semesterRes.data) {
const sem = semesterRes.data;
const weekNum = sem.current_week;
let semesterInfo = escapeHtml(sem.semester_name);
if (weekNum && weekNum > 0) {
semesterInfo += ` · 第${weekNum}`;
}
statsHtml += `
<div class="stat-card">
<div class="stat-label">当前学期</div>
<div class="stat-value" style="font-size:20px;">${semesterInfo}</div>
</div>
`;
}
document.getElementById('dashboardStats').innerHTML = statsHtml;
let quickActions = '';
if (role === '班主任' || role === '班长' || role === '学习委员' || role === '劳动委员' || role === '志愿委员') {
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>';

View File

@@ -15,41 +15,74 @@ 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;
}
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>`;
html += '<option value="' + s.student_id + '">' + escapeHtml(s.student_no) + ' - ' + escapeHtml(s.name) + '</option>';
});
document.getElementById('historyStudentId').innerHTML = html;
}
}
async function loadHistory(page = 1) {
// 筛选学生时自动取消合并记录
function onStudentFilterChange() {
var studentId = document.getElementById('historyStudentId').value;
if (studentId) {
var grouped = document.getElementById('historyGrouped');
if (grouped) grouped.checked = false;
}
}
// 折叠/展开筛选面板
function toggleFilterPanel() {
var panel = document.getElementById('advancedFilters');
var btn = document.getElementById('filterToggleBtn');
if (panel.style.display === 'none') {
panel.style.display = 'block';
btn.textContent = '收起筛选 ▲';
} else {
panel.style.display = 'none';
btn.textContent = '展开筛选 ▼';
}
}
async function loadHistory(page) {
page = page || 1;
currentHistoryPage = page;
const startDate = document.getElementById('historyStartDate').value;
const endDate = document.getElementById('historyEndDate').value;
const studentId = document.getElementById('historyStudentId').value;
const reasonFilter = document.getElementById('historyReasonFilter').value;
const isGrouped = document.getElementById('historyGrouped').checked;
const statusFilter = document.getElementById('historyStatusFilter')?.value;
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 isGrouped = document.getElementById('historyGrouped').checked;
var statusFilter = document.getElementById('historyStatusFilter') ? document.getElementById('historyStatusFilter').value : '';
const params = {
page, page_size: 20,
// 筛选学生时强制取消合并
if (studentId) isGrouped = false;
var params = {
page: page, page_size: 20,
start_date: startDate,
end_date: endDate
};
if (studentId) params.student_id = studentId;
if (reasonFilter) params.reason_prefix = reasonFilter;
if (isGrouped) params.grouped = true;
if (statusFilter !== undefined && statusFilter !== '') params.is_revoked = parseInt(statusFilter);
if (statusFilter !== '') params.is_revoked = parseInt(statusFilter);
const res = await apiGet('/api/admin/conduct/history', params);
var res = await apiGet('/api/admin/conduct/history', params);
if (res && res.success) {
const nowrapStyle = ' style="white-space: nowrap; min-width: 80px;"';
let headHtml = '';
var nowrapStyle = ' style="white-space:nowrap;min-width:80px;"';
var headHtml = '';
if (isGrouped) {
headHtml = '<th>时间</th><th>原因</th><th>分值</th><th' + nowrapStyle + '>操作人</th><th>涉及学生</th>';
if (role === '班主任' || role === '班长') {
@@ -63,71 +96,71 @@ async function loadHistory(page = 1) {
}
document.getElementById('historyTableHead').innerHTML = headHtml;
let html = '';
var html = '';
if (isGrouped) {
res.data.records.forEach(record => {
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
const names = record.student_names || '';
const allRevoked = record.all_revoked;
const revokedStyle = allRevoked ? ' style="opacity:0.5; text-decoration:line-through;"' : '';
html += `<tr${revokedStyle}>
<td class="history-time">${formatDateTime(record.created_at)}</td>
<td class="preserve-newlines history-reason">${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 class="history-students">${escapeHtml(names)}</td>`;
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;"' : '';
html += '<tr' + revokedStyle + '>' +
'<td class="history-time">' + formatDateTime(record.created_at) + '</td>' +
'<td class="history-reason">' + escapeHtml(record.reason) + '</td>' +
'<td class="' + pointsClass + '">' + (record.points_change > 0 ? '+' : '') + record.points_change + '&times;' + record.student_count + '</td>' +
'<td>' + escapeHtml(record.recorder_name || '') + '</td>' +
'<td class="history-students">' + escapeHtml(names) + '</td>';
if (role === '班主任' || role === '班长') {
if (allRevoked) {
html += `<td><span class="text-muted">已撤销</span></td>`;
html += '<td><span class="text-muted">已撤销</span></td>';
} else {
html += `<td><button class="btn btn-sm btn-outline-danger" onclick="batchRevokeGrouped('${escapeHtml(record.reason)}', ${record.points_change}, '${escapeHtml(record.recorder_name || '')}', '${formatDateTime(record.created_at)}')">批量撤销</button></td>`;
html += '<td><button class="btn btn-sm btn-outline" onclick="batchRevokeGrouped(\'' + escapeHtml(record.reason).replace(/'/g, "\\'") + '\',' + record.points_change + ',\'' + escapeHtml(record.recorder_name || '').replace(/'/g, "\\'") + '\',\'' + formatDateTime(record.created_at) + '\')">批量撤销</button></td>';
}
}
html += `</tr>`;
html += '</tr>';
});
if (res.data.records.length === 0) {
const colSpan = (role === '班主任' || role === '班长') ? 6 : 5;
var colSpan = (role === '班主任' || role === '班长') ? 6 : 5;
html = '<tr><td colspan="' + colSpan + '" 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 class="history-time">${formatDateTime(record.created_at)}</td>
<td class="history-students">${escapeHtml(record.student_name)}</td>
<td class="${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
<td class="preserve-newlines history-reason">${escapeHtml(record.reason)}</td>
<td>${escapeHtml(record.recorder_name)}</td>`;
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;"' : '';
html += '<tr' + revokedStyle + '>' +
'<td class="history-time">' + formatDateTime(record.created_at) + '</td>' +
'<td>' + escapeHtml(record.student_name) + '</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>';
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-outline" onclick="restoreRecord(${record.record_id})">反撤销</button></td>`;
var 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>`;
html += '<td><button class="btn btn-sm btn-outline" 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>`;
var 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>`;
html += '<td><button class="btn btn-sm btn-outline" onclick="revokeRecord(' + record.record_id + ')">撤销</button></td>';
}
} else if (role === '考勤委员') {
if (record.is_revoked == 1) {
html += `<td><span class="text-muted">已撤销</span></td>`;
html += '<td><span class="text-muted">已撤销</span></td>';
} else if (record.recorder_id == currentUserId) {
html += `<td><button class="btn btn-sm btn-outline" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
html += '<td><button class="btn btn-sm btn-outline" onclick="revokeRecord(' + record.record_id + ')">撤销</button></td>';
} else {
html += `<td><span class="text-muted">-</span></td>`;
html += '<td><span class="text-muted">-</span></td>';
}
}
html += `</tr>`;
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>`;
var colSpan = (role === '班主任' || role === '班长' || role === '考勤委员') ? 6 : 5;
html = '<tr><td colspan="' + colSpan + '" style="text-align:center;">暂无记录</td></tr>';
}
}
@@ -145,47 +178,47 @@ function renderHistoryPagination() {
}
async function exportHistoryRecords() {
const startDate = document.getElementById('historyStartDate').value;
const endDate = document.getElementById('historyEndDate').value;
const studentId = document.getElementById('historyStudentId').value;
var startDate = document.getElementById('historyStartDate').value;
var endDate = document.getElementById('historyEndDate').value;
var studentId = document.getElementById('historyStudentId').value;
showToast('正在导出历史记录...', 'info');
try {
const reasonFilter = document.getElementById('historyReasonFilter').value;
const params = { page: 1, page_size: 1000 };
var reasonFilter = document.getElementById('historyReasonFilter').value;
var 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 (reasonFilter) params.reason_prefix = reasonFilter;
const res = await apiGet('/api/admin/conduct/history', params);
var res = await apiGet('/api/admin/conduct/history', params);
if (res && res.success && res.data.records) {
const records = res.data.records;
var records = res.data.records;
if (records.length === 0) {
showToast('没有找到记录', 'warning');
return;
}
let csv = '\uFEFF';
var 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`;
records.forEach(function(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');
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
var url = URL.createObjectURL(blob);
var 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);
URL.revokeObjectURL(url);
showToast(`导出成功,共${records.length}条记录`);
showToast('导出成功,共' + records.length + '条记录');
} else {
showToast('导出失败:' + (res?.message || '未知错误'), 'error');
showToast('导出失败:' + (res && res.message || '未知错误'), 'error');
}
} catch (err) {
showToast('导出失败:' + err.message, 'error');
@@ -194,13 +227,12 @@ async function exportHistoryRecords() {
// 批量撤销合并记录(按条件查找并撤销)
async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt) {
if (!confirm(`确定要撤销所有"${reason}"(${pointsChange > 0 ? '+' : ''}${pointsChange}分)的记录吗?`)) return;
if (!confirm('确定要撤销所有"' + reason + '"(' + (pointsChange > 0 ? '+' : '') + pointsChange + '分)的记录吗?')) return;
showToast('正在批量撤销...', 'info');
try {
// 先查询匹配的记录
const params = {
var params = {
page: 1, page_size: 1000,
start_date: document.getElementById('historyStartDate').value,
end_date: document.getElementById('historyEndDate').value,
@@ -208,15 +240,14 @@ async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt)
grouped: false
};
const res = await apiGet('/api/admin/conduct/history', params);
var res = await apiGet('/api/admin/conduct/history', params);
if (!res || !res.success || !res.data.records) {
showToast('查询记录失败', 'error');
return;
}
// 精确匹配
const matchedIds = [];
res.data.records.forEach(r => {
var matchedIds = [];
res.data.records.forEach(function(r) {
if (r.reason === reason && r.points_change === pointsChange && r.is_revoked == 0) {
matchedIds.push(r.record_id);
}
@@ -227,32 +258,33 @@ async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt)
return;
}
const revokeRes = await apiPost('/api/admin/conduct/batch-revoke', { record_ids: matchedIds });
var revokeRes = await apiPost('/api/admin/conduct/batch-revoke', { record_ids: matchedIds });
if (revokeRes && revokeRes.success) {
showToast(`批量撤销完成: ${revokeRes.data.success_count}条成功`);
showToast('批量撤销完成: ' + (revokeRes.data ? revokeRes.data.success_count : 0) + '条成功');
loadHistory(currentHistoryPage);
} else {
showToast(revokeRes?.message || '批量撤销失败', 'error');
showToast(revokeRes && revokeRes.message || '批量撤销失败', 'error');
}
} catch (err) {
showToast('批量撤销失败: ' + err.message, 'error');
}
}
loadStudentsForSelect().then(() => {
const urlParams = new URLSearchParams(window.location.search);
const preStudentId = urlParams.get('student_id');
loadStudentsForSelect().then(function() {
var urlParams = new URLSearchParams(window.location.search);
var preStudentId = urlParams.get('student_id');
if (preStudentId) {
document.getElementById('historyStudentId').value = preStudentId;
loadHistory();
} else {
loadHistory();
onStudentFilterChange();
}
loadHistory();
});
window.loadHistory = loadHistory;
window.loadStudentsForSelect = loadStudentsForSelect;
window.exportHistoryRecords = exportHistoryRecords;
window.batchRevokeGrouped = batchRevokeGrouped;
window.onStudentFilterChange = onStudentFilterChange;
window.toggleFilterPanel = toggleFilterPanel;
})();

View File

@@ -127,25 +127,29 @@ function toggleSubjectPanel() {
async function loadSubjectList() {
const res = await apiGet('/api/subject/list');
if (res && res.success) {
if (res && res.success && res.data) {
let html = '';
res.data.subjects.forEach(sub => {
const subjects = res.data.subjects || [];
subjects.forEach(sub => {
const safeName = escapeHtml(sub.subject_name || '');
const safeCode = escapeHtml(sub.subject_code || '');
const sortOrder = sub.sort_order || 0;
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-name">${safeName}</span>
<span class="subject-code">${safeCode}</span>
<span class="subject-status ${sub.is_active ? 'subject-status-active' : 'subject-status-inactive'}">
${sub.is_active ? '启用' : '禁用'}
</span>
<button class="btn btn-sm btn-outline" onclick="showEditSubjectModal(${sub.subject_id}, '${escapeHtml(sub.subject_name)}', '${escapeHtml(sub.subject_code || '')}', ${sub.sort_order || 0})">编辑</button>
<button class="btn btn-sm btn-ghost" onclick="toggleSubjectStatus(${sub.subject_id}, ${!sub.is_active})">
<button class="btn btn-sm btn-outline" onclick="showEditSubjectModal(${sub.subject_id}, '${safeName.replace(/'/g, "\\'")}', '${safeCode.replace(/'/g, "\\'")}', ${sortOrder})">编辑</button>
<button class="btn btn-sm btn-ghost" onclick="toggleSubjectStatus(${sub.subject_id}, ${sub.is_active ? 'false' : 'true'})">
${sub.is_active ? '禁用' : '启用'}
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteSubject(${sub.subject_id})">删除</button>
</div>
`;
});
if (res.data.subjects.length === 0) {
if (subjects.length === 0) {
html = '<p style="text-align:center;padding:40px;">暂无科目,请点击"添加科目"</p>';
}
document.getElementById('subjectList').innerHTML = html;

View File

@@ -15,12 +15,9 @@
// HTML转义
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
var el = document.createElement('span');
el.appendChild(document.createTextNode(str));
return el.innerHTML;
}
// 全选功能

View File

@@ -81,10 +81,12 @@ async function loadSemesters() {
recordText = `${conductCount}条操行分 / ${attendanceCount}条考勤`;
}
const weekText = sem.current_week ? `${sem.current_week}` : '-';
html += `<tr>
<td>${escapeHtml(sem.semester_name)}</td>
<td>${formatDate(sem.start_date)}</td>
<td>${formatDate(sem.end_date)}</td>
<td>${weekText}</td>
<td><span class="${statusClass}">${statusText}</span></td>
<td>${recordText}</td>
<td>${formatDateTime(sem.created_at)}</td>
@@ -92,7 +94,7 @@ async function loadSemesters() {
</tr>`;
});
if (semesters.length === 0) {
html = '<tr><td colspan="7" style="text-align:center;">暂无学期,请点击上方按钮创建新学期</td></tr>';
html = '<tr><td colspan="8" style="text-align:center;">暂无学期,请点击上方按钮创建新学期</td></tr>';
}
document.getElementById('semesterList').innerHTML = html;
}