diff --git a/README.md b/README.md index 470281b..d82b9a5 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,7 @@ classmanager/ | 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转义修复 | +| v2.6 | 2026.5.28 | 历史记录筛选面板重构(学生筛选移入面板、合并按钮始终可见)、新增科目下拉筛选、新增原因关键词搜索、科目删除后默认仅显示启用科目、筛选面板展开修复、版本号同步至2.6 | ## 许可证 diff --git a/VERSION b/VERSION index 73462a5..5154b3f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.5.1 +2.6 diff --git a/backend/models/conduct.py b/backend/models/conduct.py index 1e0c685..2cefb0c 100644 --- a/backend/models/conduct.py +++ b/backend/models/conduct.py @@ -152,6 +152,7 @@ class ConductModel: params.extend([limit, offset]) return await execute_query(sql, tuple(params)) + @staticmethod @staticmethod async def get_all_records( limit: int = 100, @@ -162,7 +163,8 @@ class ConductModel: include_revoked: bool = True, related_type: str = None, reason_prefix: str = None, - is_revoked: int = None + is_revoked: int = None, + reason_search: str = None ) -> List[Dict[str, Any]]: """获取所有记录(班主任/班长专用)""" # 空字符串转为None @@ -174,6 +176,8 @@ class ConductModel: related_type = None if reason_prefix == "": reason_prefix = None + if reason_search == "": + reason_search = None sql = """ SELECT cr.*, s.name as student_name, s.student_no, u.real_name as recorder_name, ru.real_name as revoker_name @@ -207,6 +211,10 @@ class ConductModel: sql += " AND cr.reason LIKE %s" params.append(f"{reason_prefix}%") + if reason_search: + sql += " AND cr.reason LIKE %s" + params.append(f"%{reason_search}%") + if is_revoked is not None: sql += " AND cr.is_revoked = %s" params.append(1 if is_revoked else 0) @@ -215,7 +223,7 @@ class ConductModel: params.extend([limit, offset]) return await execute_query(sql, tuple(params)) - + @staticmethod async def get_grouped_records( student_id: int = None, @@ -225,7 +233,8 @@ class ConductModel: reason_prefix: str = None, page: int = 1, page_size: int = 20, - is_revoked: int = None + is_revoked: int = None, + reason_search: str = None ) -> Dict[str, Any]: """获取分组后的操行分记录(同批次合并)""" if start_date == "": @@ -236,6 +245,8 @@ class ConductModel: related_type = None if reason_prefix == "": reason_prefix = None + if reason_search == "": + reason_search = None conditions = ["1=1"] params = [] @@ -261,6 +272,9 @@ class ConductModel: if reason_prefix: conditions.append("cr.reason LIKE %s") params.append(f"{reason_prefix}%") + if reason_search: + conditions.append("cr.reason LIKE %s") + params.append(f"%{reason_search}%") where_clause = " AND ".join(conditions) diff --git a/backend/routes/admin.py b/backend/routes/admin.py index bda3b11..8e4e7b0 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -320,7 +320,8 @@ async def get_conduct_history( grouped: bool = Query(False), related_type: Optional[str] = None, reason_prefix: Optional[str] = None, - is_revoked: Optional[int] = None + is_revoked: Optional[int] = None, + reason_search: Optional[str] = None ): """获取操行分历史记录""" try: @@ -337,7 +338,8 @@ async def get_conduct_history( grouped=grouped, related_type=related_type, reason_prefix=reason_prefix, - is_revoked=is_revoked + is_revoked=is_revoked, + reason_search=reason_search ) return success_response(data=result) except Exception as e: diff --git a/backend/routes/upgrade.py b/backend/routes/upgrade.py index d12c8b1..bb76e34 100644 --- a/backend/routes/upgrade.py +++ b/backend/routes/upgrade.py @@ -18,6 +18,7 @@ import re logger = setup_logger() router = APIRouter() +# 版本列表(按顺序) # 版本列表(按顺序) ALL_VERSIONS = { '1.0': 'v1.0.sql', @@ -34,12 +35,11 @@ ALL_VERSIONS = { '2.1': 'v2.1.sql', '2.2': 'v2.2.sql', '2.3': 'v2.3.sql', - '2.3': 'v2.3.sql', '2.4': 'v2.4.sql', '2.5': 'v2.5.sql', '2.5.1': 'v2.5.1.sql', + '2.6': 'v2.6.sql', } - # 版本特征标记(按优先级从高到低) VERSION_MARKERS = [ ('2.0', 'students', 'dormitory_number'), diff --git a/backend/services/conduct_service.py b/backend/services/conduct_service.py index 25a2ee3..0ecb8cd 100644 --- a/backend/services/conduct_service.py +++ b/backend/services/conduct_service.py @@ -230,7 +230,8 @@ class ConductService: grouped: bool = False, related_type: Optional[str] = None, reason_prefix: Optional[str] = None, - is_revoked: Optional[int] = None + is_revoked: Optional[int] = None, + reason_search: Optional[str] = None ) -> Dict[str, Any]: """获取历史记录""" # 空字符串转为None @@ -242,6 +243,8 @@ class ConductService: related_type = None if reason_prefix == "": reason_prefix = None + if reason_search == "": + reason_search = None if related_type and related_type not in ('manual', 'homework', 'attendance'): return {"records": [], "page": page, "page_size": page_size, "total": 0, "total_pages": 0} @@ -259,7 +262,8 @@ class ConductService: reason_prefix=reason_prefix, page=page, page_size=page_size, - is_revoked=is_revoked + is_revoked=is_revoked, + reason_search=reason_search ) records = await ConductModel.get_all_records( @@ -270,7 +274,8 @@ class ConductService: student_id=student_id, related_type=related_type, reason_prefix=reason_prefix, - is_revoked=is_revoked + is_revoked=is_revoked, + reason_search=reason_search ) # 获取总数 @@ -292,6 +297,9 @@ class ConductService: if reason_prefix: count_conditions.append("cr.reason LIKE %s") count_params.append(f"{reason_prefix}%") + if reason_search: + count_conditions.append("cr.reason LIKE %s") + count_params.append(f"%{reason_search}%") if is_revoked is not None: count_conditions.append("cr.is_revoked = %s") count_params.append(1 if is_revoked else 0) diff --git a/frontend/admin/history.php b/frontend/admin/history.php index fe52d53..a1c0a58 100644 --- a/frontend/admin/history.php +++ b/frontend/admin/history.php @@ -27,14 +27,11 @@ include __DIR__ . '/../includes/header.php';
-
- - -
+ @@ -42,6 +39,22 @@ include __DIR__ . '/../includes/header.php';
diff --git a/frontend/assets/js/history.js b/frontend/assets/js/history.js index 905546c..7e90542 100644 --- a/frontend/assets/js/history.js +++ b/frontend/assets/js/history.js @@ -33,6 +33,20 @@ async function loadStudentsForSelect() { } } +// 加载科目下拉列表 +async function loadSubjectsForFilter() { + var subjectSelect = document.getElementById('historySubjectFilter'); + if (!subjectSelect) return; + var res = await apiGet('/api/subject/list', { is_active: true }); + if (res && res.success && res.data && res.data.subjects) { + let html = ''; + res.data.subjects.forEach(s => { + html += ''; + }); + subjectSelect.innerHTML = html; + } +} + // 筛选学生时自动取消合并记录 function onStudentFilterChange() { var studentId = document.getElementById('historyStudentId').value; @@ -42,10 +56,19 @@ function onStudentFilterChange() { } } +// 科目筛选变化时,取消扣分类型筛选(互斥) +function onSubjectFilterChange() { + var subjectVal = document.getElementById('historySubjectFilter').value; + if (subjectVal) { + document.getElementById('historyReasonFilter').value = ''; + } +} + // 折叠/展开筛选面板 function toggleFilterPanel() { var panel = document.getElementById('advancedFilters'); var btn = document.getElementById('filterToggleBtn'); + if (!panel || !btn) return; if (panel.style.display === 'none') { panel.style.display = 'block'; btn.textContent = '收起筛选 ▲'; @@ -62,6 +85,8 @@ async function loadHistory(page) { 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 : ''; @@ -74,7 +99,15 @@ async function loadHistory(page) { end_date: endDate }; if (studentId) params.student_id = studentId; - if (reasonFilter) params.reason_prefix = reasonFilter; + + // 科目筛选优先于扣分类型筛选 + if (subjectFilter) { + params.reason_prefix = '[' + subjectFilter + ']'; + } else if (reasonFilter) { + params.reason_prefix = reasonFilter; + } + + if (reasonSearch) params.reason_search = reasonSearch; if (isGrouped) params.grouped = true; if (statusFilter !== '') params.is_revoked = parseInt(statusFilter); @@ -186,11 +219,18 @@ async function exportHistoryRecords() { 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 }; if (startDate) params.start_date = startDate; if (endDate) params.end_date = endDate; if (studentId) params.student_id = studentId; - if (reasonFilter) params.reason_prefix = reasonFilter; + if (subjectFilter) { + params.reason_prefix = '[' + subjectFilter + ']'; + } else if (reasonFilter) { + params.reason_prefix = reasonFilter; + } + if (reasonSearch) params.reason_search = reasonSearch; var res = await apiGet('/api/admin/conduct/history', params); if (res && res.success && res.data.records) { @@ -270,7 +310,8 @@ async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt) } } -loadStudentsForSelect().then(function() { +// 初始化:并行加载学生和科目列表,然后加载历史记录 +Promise.all([loadStudentsForSelect(), loadSubjectsForFilter()]).then(function() { var urlParams = new URLSearchParams(window.location.search); var preStudentId = urlParams.get('student_id'); if (preStudentId) { @@ -285,6 +326,7 @@ window.loadStudentsForSelect = loadStudentsForSelect; window.exportHistoryRecords = exportHistoryRecords; window.batchRevokeGrouped = batchRevokeGrouped; window.onStudentFilterChange = onStudentFilterChange; +window.onSubjectFilterChange = onSubjectFilterChange; window.toggleFilterPanel = toggleFilterPanel; })(); diff --git a/frontend/assets/js/homework-manage.js b/frontend/assets/js/homework-manage.js index 4b27eab..e4256ad 100644 --- a/frontend/assets/js/homework-manage.js +++ b/frontend/assets/js/homework-manage.js @@ -126,7 +126,7 @@ function toggleSubjectPanel() { } async function loadSubjectList() { - const res = await apiGet('/api/subject/list'); + const res = await apiGet('/api/subject/list', { is_active: true }); if (res && res.success && res.data) { let html = ''; const subjects = res.data.subjects || []; diff --git a/sql/init.sql b/sql/init.sql index 2a7db7d..542ef77 100644 --- a/sql/init.sql +++ b/sql/init.sql @@ -230,10 +230,10 @@ INSERT IGNORE INTO `subjects` (`subject_name`, `subject_code`, `sort_order`) VAL ('数学', 'MATH', 2), ('英语', 'ENG', 3); +-- 初始化系统版本号 -- 初始化系统版本号 INSERT INTO `system_settings` (`setting_key`, `setting_value`) -VALUES ('db_version', '2.5.1') -ON DUPLICATE KEY UPDATE `setting_value` = '2.5.1'; - +VALUES ('db_version', '2.6') +ON DUPLICATE KEY UPDATE `setting_value` = '2.6'; -- 控制台输出初始化结果(含版本号) SELECT CONCAT('数据库初始化完成!版本: v', (SELECT setting_value FROM system_settings WHERE setting_key = 'db_version')) AS message; diff --git a/sql/upgrades/v2.6.sql b/sql/upgrades/v2.6.sql new file mode 100644 index 0000000..94617dd --- /dev/null +++ b/sql/upgrades/v2.6.sql @@ -0,0 +1,12 @@ +-- =========================================== +-- 班级操行分管理系统 - v2.5.1 → v2.6 升级脚本 +-- 字符集: utf8mb4 +-- +-- 说明: v2.6 为功能增强版本,无数据库 schema 变更。 +-- 主要变更: +-- 1. 历史记录页筛选面板重构(学生筛选移入面板、合并按钮移出面板) +-- 2. 新增科目下拉筛选(通过 reason 前缀匹配) +-- 3. 新增原因关键词搜索(reason_search 模糊匹配) +-- 4. 科目管理删除后仅显示启用科目 +-- 5. 筛选面板展开/收起功能修复 +-- =========================================== diff --git a/upgrade.php b/upgrade.php index 7d73908..ccb55b8 100644 --- a/upgrade.php +++ b/upgrade.php @@ -29,6 +29,7 @@ $UPGRADE_VERSIONS = [ '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', + '2.6' => __DIR__ . '/sql/upgrades/v2.6.sql', ]; /**