From b2c36cab2ce6453b847ca6a20c9a4b16708a9d69 Mon Sep 17 00:00:00 2001 From: canglan Date: Fri, 29 May 2026 20:16:25 +0800 Subject: [PATCH] =?UTF-8?q?v2.5.1=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 + VERSION | 2 +- backend/models/conduct.py | 18 ++- backend/routes/semester.py | 21 ++- backend/routes/subject.py | 2 +- backend/routes/upgrade.py | 1 + backend/services/conduct_service.py | 19 ++- backend/services/semester_service.py | 13 ++ frontend/admin/history.php | 85 +++++------ frontend/admin/semesters.php | 1 + frontend/assets/css/style.css | 35 ++++- frontend/assets/js/common.js | 35 +++-- frontend/assets/js/dashboard.js | 28 +++- frontend/assets/js/history.js | 198 +++++++++++++++----------- frontend/assets/js/homework-manage.js | 18 ++- frontend/assets/js/modules/utils.js | 9 +- frontend/assets/js/semesters.js | 4 +- frontend/parent/history.php | 2 +- sql/init.sql | 4 +- sql/upgrades/v2.5.1.sql | 11 ++ sql/upgrades/v2.5.sql | 2 +- upgrade.php | 1 + 22 files changed, 347 insertions(+), 166 deletions(-) create mode 100644 sql/upgrades/v2.5.1.sql diff --git a/README.md b/README.md index de0e323..470281b 100644 --- a/README.md +++ b/README.md @@ -273,6 +273,10 @@ classmanager/ | v1.8 | 2026.5.22 | 科目管理融入作业管理页、科目删除数据依赖检查、加减分记录类型区分(manual/homework/attendance)、学生端作业详情优化 | | v2.0.1 | 2026.5.23 | 操作列折叠优化、扣分类型大类区分、科目选择修复、改名作业扣分、记录人优化、家长端优化、学期管理优化 | | v2.1 | 2026.5.26 | CSS变量化统一配色方案、简化按钮系统、操作列按钮风格统一、清理内联颜色、修复科目管理面板无法展开、数据库索引优化、清理init.sql冗余迁移代码、安全审计通过 | +| v2.2 | 2026.5.27 | 安全修复:管理员操作越权漏洞修复、新增宿舍集体加分功能、学生导入支持宿舍号、导入预览显示宿舍号列 | +| v2.3 | 2026.5.28 | 升级系统全面重构:修复前后端通信错误处理、SQL DELIMITER解析修复、XSS防护、升级验证+自动重试+失败回滚机制、补全v1.0-v1.6增量升级脚本 | +| v2.5 | 2026.5.28 | 历史记录页优化(文字宽度/换行/合并按钮样式)、新增状态筛选项、合并记录批量撤销/反撤销、操作菜单底部遮挡修复、删除科目报错修复、学期自动关联+当前周数计算 | +| v2.5.1 | 2026.5.28 | 筛选学生时自动取消合并记录、合并记录选项样式修复(竖排显示)、历史记录筛选改为折叠式、科目管理调用修复、全局escapeHtml XSS转义修复 | ## 许可证 diff --git a/VERSION b/VERSION index 95e3ba8..73462a5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.5 +2.5.1 diff --git a/backend/models/conduct.py b/backend/models/conduct.py index eb543a5..1e0c685 100644 --- a/backend/models/conduct.py +++ b/backend/models/conduct.py @@ -224,7 +224,8 @@ class ConductModel: related_type: str = None, reason_prefix: str = None, page: int = 1, - page_size: int = 20 + page_size: int = 20, + is_revoked: int = None ) -> Dict[str, Any]: """获取分组后的操行分记录(同批次合并)""" if start_date == "": @@ -236,9 +237,15 @@ class ConductModel: if reason_prefix == "": reason_prefix = None - conditions = ["cr.is_revoked = 0"] + conditions = ["1=1"] params = [] + if is_revoked is not None: + conditions.append("cr.is_revoked = %s") + params.append(1 if is_revoked else 0) + else: + conditions.append("cr.is_revoked = 0") + if student_id: conditions.append("cr.student_id = %s") params.append(student_id) @@ -258,7 +265,7 @@ class ConductModel: where_clause = " AND ".join(conditions) count_sql = f""" - SELECT COUNT(DISTINCT CONCAT(cr.points_change, '|', cr.reason, '|', cr.recorder_id, '|', DATE_FORMAT(cr.created_at, '%%Y-%%m-%%d %%H:%%i'))) as total + SELECT COUNT(DISTINCT CONCAT(cr.points_change, '|', cr.reason, '|', cr.recorder_id, '|', DATE_FORMAT(cr.created_at, '%%Y-%%m-%%d %%H:%%i:%%s'))) as total FROM conduct_records cr WHERE {where_clause} """ @@ -270,11 +277,12 @@ class ConductModel: cr.recorder_name, DATE_FORMAT(MIN(cr.created_at), '%%Y-%%m-%%d %%H:%%i:%%s') as created_at, GROUP_CONCAT(s.name ORDER BY s.student_id SEPARATOR ', ') as student_names, - COUNT(*) as student_count + COUNT(*) as student_count, + MAX(cr.is_revoked) as all_revoked FROM conduct_records cr JOIN students s ON cr.student_id = s.student_id WHERE {where_clause} - GROUP BY cr.points_change, cr.reason, cr.recorder_id, DATE_FORMAT(cr.created_at, '%%Y-%%m-%%d %%H:%%i') + GROUP BY cr.points_change, cr.reason, cr.recorder_id, DATE_FORMAT(cr.created_at, '%%Y-%%m-%%d %%H:%%i:%%s') ORDER BY MIN(cr.created_at) DESC LIMIT %s OFFSET %s """ diff --git a/backend/routes/semester.py b/backend/routes/semester.py index a3e0115..cb34e6d 100644 --- a/backend/routes/semester.py +++ b/backend/routes/semester.py @@ -42,11 +42,28 @@ async def list_semesters(request: Request): @router.get("/active") async def get_active_semester(request: Request): - """获取当前活跃学期""" + """获取当前活跃学期(含当前周数)""" user = await get_current_user(request) result = await SemesterService.get_active_semester() if result["success"]: - return success_response(data=result.get("semester")) + semester = result.get("semester") + if semester and semester.get('start_date'): + from datetime import date, datetime + try: + start = semester['start_date'] + if isinstance(start, str): + start_date = datetime.strptime(start, '%Y-%m-%d').date() + else: + start_date = start + today = date.today() + delta = (today - start_date).days + if delta >= 0: + semester['current_week'] = delta // 7 + 1 + else: + semester['current_week'] = 0 + except Exception: + semester['current_week'] = None + return success_response(data=semester) else: return error_response(message=result["message"]) diff --git a/backend/routes/subject.py b/backend/routes/subject.py index 913a5d4..efa0ea3 100644 --- a/backend/routes/subject.py +++ b/backend/routes/subject.py @@ -52,4 +52,4 @@ async def delete_subject(request: Request, subject_id: int): if not await PermissionChecker.check_can_manage_subjects(user["user_id"]): return error_response(message="无权限", code=403) result = await SubjectService.delete_subject(subject_id) - return success_response(message="科目已禁用") if result["success"] else error_response(message=result["message"]) \ No newline at end of file + return success_response(message="科目已删除") if result["success"] else error_response(message=result["message"]) \ No newline at end of file diff --git a/backend/routes/upgrade.py b/backend/routes/upgrade.py index d6dee2c..d12c8b1 100644 --- a/backend/routes/upgrade.py +++ b/backend/routes/upgrade.py @@ -37,6 +37,7 @@ ALL_VERSIONS = { '2.3': 'v2.3.sql', '2.4': 'v2.4.sql', '2.5': 'v2.5.sql', + '2.5.1': 'v2.5.1.sql', } # 版本特征标记(按优先级从高到低) diff --git a/backend/services/conduct_service.py b/backend/services/conduct_service.py index cd32e0b..25a2ee3 100644 --- a/backend/services/conduct_service.py +++ b/backend/services/conduct_service.py @@ -15,6 +15,7 @@ from datetime import datetime from models.student import StudentModel from models.conduct import ConductModel from models.user import UserModel +from models.semester import SemesterModel from middleware.permission import PermissionChecker from config import settings from utils.logger import get_logger @@ -86,6 +87,10 @@ class ConductService: fail_count = 0 details = [] + # 自动获取当前活跃学期 + active_semester = await SemesterModel.get_active() + semester_id = active_semester['semester_id'] if active_semester else None + for student_id in student_ids: try: # 检查学生是否存在 @@ -104,6 +109,17 @@ class ConductService: related_type=related_type ) + # 自动关联到当前学期 + if semester_id and record_id: + try: + from utils.database import execute_update as _exec_update + await _exec_update( + "UPDATE conduct_records SET semester_id = %s WHERE record_id = %s AND semester_id IS NULL", + (semester_id, record_id) + ) + except Exception: + pass # 关联失败不影响主流程 + # 更新学生总分 await StudentModel.update_total_points(student_id, points_change) @@ -242,7 +258,8 @@ class ConductService: related_type=related_type, reason_prefix=reason_prefix, page=page, - page_size=page_size + page_size=page_size, + is_revoked=is_revoked ) records = await ConductModel.get_all_records( diff --git a/backend/services/semester_service.py b/backend/services/semester_service.py index 82ea56d..63e6448 100644 --- a/backend/services/semester_service.py +++ b/backend/services/semester_service.py @@ -30,10 +30,23 @@ class SemesterService: """获取学期列表""" try: semesters = await SemesterModel.get_all() + today = datetime.date.today() for sem in semesters: counts = await SemesterModel.count_records_by_semester(sem['semester_id']) sem['conduct_count'] = counts['conduct_count'] sem['attendance_count'] = counts['attendance_count'] + # 计算当前周数(仅活跃学期且有开始日期时) + sem['current_week'] = None + if sem.get('is_active') and sem.get('start_date'): + try: + s_date = sem['start_date'] + if isinstance(s_date, str): + s_date = datetime.datetime.strptime(s_date, '%Y-%m-%d').date() + delta = (today - s_date).days + if delta >= 0: + sem['current_week'] = delta // 7 + 1 + except (ValueError, TypeError): + pass return { "success": True, "semesters": semesters diff --git a/frontend/admin/history.php b/frontend/admin/history.php index fac1f32..fe52d53 100644 --- a/frontend/admin/history.php +++ b/frontend/admin/history.php @@ -26,54 +26,59 @@ include __DIR__ . '/../includes/header.php';
-
-
- - -
-
- - -
+
-
-
- - -
- -
- - -
- -
- -
+ +
@@ -103,4 +108,4 @@ include __DIR__ . '/../includes/header.php'; - \ No newline at end of file + diff --git a/frontend/admin/semesters.php b/frontend/admin/semesters.php index 7de76d4..9a24842 100644 --- a/frontend/admin/semesters.php +++ b/frontend/admin/semesters.php @@ -41,6 +41,7 @@ include __DIR__ . '/../includes/header.php'; 学期名称 开始日期 结束日期 + 当前周数 状态 记录数 创建时间 diff --git a/frontend/assets/css/style.css b/frontend/assets/css/style.css index 6d46902..9f336da 100644 --- a/frontend/assets/css/style.css +++ b/frontend/assets/css/style.css @@ -931,19 +931,20 @@ tr:hover { /* ========== 历史记录页优化 ========== */ /* 时间列:确保分两行显示(日期+时间) */ .history-time { - white-space: pre-line; + white-space: nowrap; min-width: 80px; line-height: 1.5; - word-break: break-all; + vertical-align: top; } /* 原因列:每行最少7个字,自动换行 */ .history-reason { min-width: 7em; max-width: 200px; - white-space: pre-wrap; + white-space: normal; word-break: break-word; line-height: 1.5; + vertical-align: top; } /* 学生名列:允许换行 */ @@ -953,6 +954,34 @@ tr:hover { min-width: 60px; max-width: 120px; line-height: 1.5; + vertical-align: top; +} + +/* 合并记录复选框样式 */ +.history-grouped-label { + display: inline-flex; + align-items: center; + gap: 6px; + cursor: pointer; + font-size: 13px; + padding: 6px 12px; + border: 1px solid var(--color-border); + border-radius: 6px; + background: var(--color-hover); + transition: all 0.2s; + white-space: nowrap; + user-select: none; +} + +.history-grouped-label:hover { + border-color: var(--color-primary); + background: var(--color-primary-light); +} + +.history-grouped-label input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; } /* 合并记录按钮样式 */ diff --git a/frontend/assets/js/common.js b/frontend/assets/js/common.js index a3b2160..d6c3e22 100644 --- a/frontend/assets/js/common.js +++ b/frontend/assets/js/common.js @@ -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(); } }); diff --git a/frontend/assets/js/dashboard.js b/frontend/assets/js/dashboard.js index fea9407..3a948f9 100644 --- a/frontend/assets/js/dashboard.js +++ b/frontend/assets/js/dashboard.js @@ -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 += `
学生总数
${studentsRes.data.total || 0}
@@ -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 += ` +
+
当前学期
+
${semesterInfo}
+
+ `; + } + + document.getElementById('dashboardStats').innerHTML = statsHtml; + let quickActions = ''; if (role === '班主任' || role === '班长' || role === '学习委员' || role === '劳动委员' || role === '志愿委员') { quickActions += ''; diff --git a/frontend/assets/js/history.js b/frontend/assets/js/history.js index b073764..905546c 100644 --- a/frontend/assets/js/history.js +++ b/frontend/assets/js/history.js @@ -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 = ''; res.data.students.forEach(s => { - html += ``; + html += ''; }); 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 = '时间原因分值操作人涉及学生'; 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 += ` - ${formatDateTime(record.created_at)} - ${escapeHtml(record.reason)} - ${record.points_change > 0 ? '+' : ''}${record.points_change}×${record.student_count} - ${escapeHtml(record.recorder_name || '')} - ${escapeHtml(names)}`; + 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 += '' + + '' + formatDateTime(record.created_at) + '' + + '' + escapeHtml(record.reason) + '' + + '' + (record.points_change > 0 ? '+' : '') + record.points_change + '×' + record.student_count + '' + + '' + escapeHtml(record.recorder_name || '') + '' + + '' + escapeHtml(names) + ''; if (role === '班主任' || role === '班长') { if (allRevoked) { - html += `已撤销`; + html += '已撤销'; } else { - html += ``; + html += ''; } } - html += ``; + html += ''; }); if (res.data.records.length === 0) { - const colSpan = (role === '班主任' || role === '班长') ? 6 : 5; + var colSpan = (role === '班主任' || role === '班长') ? 6 : 5; html = '暂无记录'; } } 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 += ` - ${formatDateTime(record.created_at)} - ${escapeHtml(record.student_name)} - ${record.points_change > 0 ? '+' : ''}${record.points_change} - ${escapeHtml(record.reason)} - ${escapeHtml(record.recorder_name)}`; + 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 += '' + + '' + formatDateTime(record.created_at) + '' + + '' + escapeHtml(record.student_name) + '' + + '' + (record.points_change > 0 ? '+' : '') + record.points_change + '' + + '' + escapeHtml(record.reason) + '' + + '' + escapeHtml(record.recorder_name) + ''; if (role === '班主任') { if (record.is_revoked == 1) { - const revokerInfo = record.revoker_name ? `由 ${escapeHtml(record.revoker_name)} 撤销` : '已撤销'; - html += `${revokerInfo}`; + var revokerInfo = record.revoker_name ? '由 ' + escapeHtml(record.revoker_name) + ' 撤销' : '已撤销'; + html += '' + revokerInfo + ''; } else { - html += ``; + html += ''; } } else if (role === '班长') { if (record.is_revoked == 1) { - const revokerInfo = record.revoker_name ? `由 ${escapeHtml(record.revoker_name)} 撤销` : '已撤销'; - html += `${revokerInfo}`; + var revokerInfo = record.revoker_name ? '由 ' + escapeHtml(record.revoker_name) + ' 撤销' : '已撤销'; + html += '' + revokerInfo + ''; } else { - html += ``; + html += ''; } } else if (role === '考勤委员') { if (record.is_revoked == 1) { - html += `已撤销`; + html += '已撤销'; } else if (record.recorder_id == currentUserId) { - html += ``; + html += ''; } else { - html += `-`; + html += '-'; } } - html += ``; + html += ''; }); if (res.data.records.length === 0) { - const colSpan = (role === '班主任' || role === '班长' || role === '考勤委员') ? 6 : 5; - html = `暂无记录`; + var colSpan = (role === '班主任' || role === '班长' || role === '考勤委员') ? 6 : 5; + html = '暂无记录'; } } @@ -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; })(); diff --git a/frontend/assets/js/homework-manage.js b/frontend/assets/js/homework-manage.js index 723ebb5..4b27eab 100644 --- a/frontend/assets/js/homework-manage.js +++ b/frontend/assets/js/homework-manage.js @@ -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 += `
- ${escapeHtml(sub.subject_name)} - ${escapeHtml(sub.subject_code || '')} + ${safeName} + ${safeCode} ${sub.is_active ? '启用' : '禁用'} - - +
`; }); - if (res.data.subjects.length === 0) { + if (subjects.length === 0) { html = '

暂无科目,请点击"添加科目"

'; } document.getElementById('subjectList').innerHTML = html; diff --git a/frontend/assets/js/modules/utils.js b/frontend/assets/js/modules/utils.js index 92e807a..97aa8e2 100644 --- a/frontend/assets/js/modules/utils.js +++ b/frontend/assets/js/modules/utils.js @@ -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; } // 全选功能 diff --git a/frontend/assets/js/semesters.js b/frontend/assets/js/semesters.js index bd58d18..960fa5a 100644 --- a/frontend/assets/js/semesters.js +++ b/frontend/assets/js/semesters.js @@ -81,10 +81,12 @@ async function loadSemesters() { recordText = `${conductCount}条操行分 / ${attendanceCount}条考勤`; } + const weekText = sem.current_week ? `第${sem.current_week}周` : '-'; html += ` ${escapeHtml(sem.semester_name)} ${formatDate(sem.start_date)} ${formatDate(sem.end_date)} + ${weekText} ${statusText} ${recordText} ${formatDateTime(sem.created_at)} @@ -92,7 +94,7 @@ async function loadSemesters() { `; }); if (semesters.length === 0) { - html = '暂无学期,请点击上方按钮创建新学期'; + html = '暂无学期,请点击上方按钮创建新学期'; } document.getElementById('semesterList').innerHTML = html; } diff --git a/frontend/parent/history.php b/frontend/parent/history.php index 58b6823..f6061b3 100644 --- a/frontend/parent/history.php +++ b/frontend/parent/history.php @@ -82,7 +82,7 @@ async function loadHistory(page) { const pointsText = record.points_change > 0 ? `+${record.points_change}` : record.points_change; html += ` ${formatDateTime(record.created_at)} - ${escapeHtml(record.reason || '-')} + ${escapeHtml(record.reason || '-')} ${pointsText} 班主任 `; diff --git a/sql/init.sql b/sql/init.sql index fe9d3ed..2a7db7d 100644 --- a/sql/init.sql +++ b/sql/init.sql @@ -232,8 +232,8 @@ INSERT IGNORE INTO `subjects` (`subject_name`, `subject_code`, `sort_order`) VAL -- 初始化系统版本号 INSERT INTO `system_settings` (`setting_key`, `setting_value`) -VALUES ('db_version', '2.5') -ON DUPLICATE KEY UPDATE `setting_value` = '2.5'; +VALUES ('db_version', '2.5.1') +ON DUPLICATE KEY UPDATE `setting_value` = '2.5.1'; -- 控制台输出初始化结果(含版本号) SELECT CONCAT('数据库初始化完成!版本: v', (SELECT setting_value FROM system_settings WHERE setting_key = 'db_version')) AS message; diff --git a/sql/upgrades/v2.5.1.sql b/sql/upgrades/v2.5.1.sql new file mode 100644 index 0000000..c5f0884 --- /dev/null +++ b/sql/upgrades/v2.5.1.sql @@ -0,0 +1,11 @@ +-- =========================================== +-- 班级操行分管理系统 - v2.5 → v2.5.1 升级脚本 +-- 字符集: utf8mb4 +-- +-- 说明: v2.5.1 为 UI 优化版本,无数据库 schema 变更。 +-- 主要变更: +-- 1. 筛选学生时自动取消合并记录 +-- 2. 合并记录选项样式优化(修复竖排显示) +-- 3. 历史记录筛选功能改为折叠式 +-- 4. 科目管理调用修复 +-- =========================================== diff --git a/sql/upgrades/v2.5.sql b/sql/upgrades/v2.5.sql index ef4b3c0..ca5a83a 100644 --- a/sql/upgrades/v2.5.sql +++ b/sql/upgrades/v2.5.sql @@ -4,7 +4,7 @@ -- -- 说明: v2.5 为功能增强版本,无数据库 schema 变更。 -- 主要变更: --- 1. 历史记录页优化(文字宽度/换行/合并按钮样式) +-- 1. 历史记录页UI优化(文字宽度/换行/合并按钮样式) -- 2. 新增"状态"筛选项(正常/已撤销) -- 3. 合并记录支持批量撤销/反撤销 -- 4. 操作菜单底部遮挡修复 diff --git a/upgrade.php b/upgrade.php index f5b0b20..7d73908 100644 --- a/upgrade.php +++ b/upgrade.php @@ -28,6 +28,7 @@ $UPGRADE_VERSIONS = [ '2.3' => __DIR__ . '/sql/upgrades/v2.3.sql', '2.4' => __DIR__ . '/sql/upgrades/v2.4.sql', '2.5' => __DIR__ . '/sql/upgrades/v2.5.sql', + '2.5.1' => __DIR__ . '/sql/upgrades/v2.5.1.sql', ]; /**