v2.6更新

This commit is contained in:
2026-05-29 21:07:27 +08:00
parent b2c36cab2c
commit 69adb30fa0
12 changed files with 117 additions and 29 deletions

View File

@@ -277,6 +277,7 @@ classmanager/
| v2.3 | 2026.5.28 | 升级系统全面重构修复前后端通信错误处理、SQL DELIMITER解析修复、XSS防护、升级验证+自动重试+失败回滚机制、补全v1.0-v1.6增量升级脚本 | | v2.3 | 2026.5.28 | 升级系统全面重构修复前后端通信错误处理、SQL DELIMITER解析修复、XSS防护、升级验证+自动重试+失败回滚机制、补全v1.0-v1.6增量升级脚本 |
| v2.5 | 2026.5.28 | 历史记录页优化(文字宽度/换行/合并按钮样式)、新增状态筛选项、合并记录批量撤销/反撤销、操作菜单底部遮挡修复、删除科目报错修复、学期自动关联+当前周数计算 | | v2.5 | 2026.5.28 | 历史记录页优化(文字宽度/换行/合并按钮样式)、新增状态筛选项、合并记录批量撤销/反撤销、操作菜单底部遮挡修复、删除科目报错修复、学期自动关联+当前周数计算 |
| v2.5.1 | 2026.5.28 | 筛选学生时自动取消合并记录、合并记录选项样式修复竖排显示、历史记录筛选改为折叠式、科目管理调用修复、全局escapeHtml XSS转义修复 | | v2.5.1 | 2026.5.28 | 筛选学生时自动取消合并记录、合并记录选项样式修复竖排显示、历史记录筛选改为折叠式、科目管理调用修复、全局escapeHtml XSS转义修复 |
| v2.6 | 2026.5.28 | 历史记录筛选面板重构学生筛选移入面板、合并按钮始终可见、新增科目下拉筛选、新增原因关键词搜索、科目删除后默认仅显示启用科目、筛选面板展开修复、版本号同步至2.6 |
## 许可证 ## 许可证

View File

@@ -1 +1 @@
2.5.1 2.6

View File

@@ -152,6 +152,7 @@ class ConductModel:
params.extend([limit, offset]) params.extend([limit, offset])
return await execute_query(sql, tuple(params)) return await execute_query(sql, tuple(params))
@staticmethod
@staticmethod @staticmethod
async def get_all_records( async def get_all_records(
limit: int = 100, limit: int = 100,
@@ -162,7 +163,8 @@ class ConductModel:
include_revoked: bool = True, include_revoked: bool = True,
related_type: str = None, related_type: str = None,
reason_prefix: str = None, reason_prefix: str = None,
is_revoked: int = None is_revoked: int = None,
reason_search: str = None
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
"""获取所有记录(班主任/班长专用)""" """获取所有记录(班主任/班长专用)"""
# 空字符串转为None # 空字符串转为None
@@ -174,6 +176,8 @@ class ConductModel:
related_type = None related_type = None
if reason_prefix == "": if reason_prefix == "":
reason_prefix = None reason_prefix = None
if reason_search == "":
reason_search = None
sql = """ sql = """
SELECT cr.*, s.name as student_name, s.student_no, u.real_name as recorder_name, SELECT cr.*, s.name as student_name, s.student_no, u.real_name as recorder_name,
ru.real_name as revoker_name ru.real_name as revoker_name
@@ -207,6 +211,10 @@ class ConductModel:
sql += " AND cr.reason LIKE %s" sql += " AND cr.reason LIKE %s"
params.append(f"{reason_prefix}%") 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: if is_revoked is not None:
sql += " AND cr.is_revoked = %s" sql += " AND cr.is_revoked = %s"
params.append(1 if is_revoked else 0) params.append(1 if is_revoked else 0)
@@ -225,7 +233,8 @@ class ConductModel:
reason_prefix: str = None, reason_prefix: str = None,
page: int = 1, page: int = 1,
page_size: int = 20, page_size: int = 20,
is_revoked: int = None is_revoked: int = None,
reason_search: str = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""获取分组后的操行分记录(同批次合并)""" """获取分组后的操行分记录(同批次合并)"""
if start_date == "": if start_date == "":
@@ -236,6 +245,8 @@ class ConductModel:
related_type = None related_type = None
if reason_prefix == "": if reason_prefix == "":
reason_prefix = None reason_prefix = None
if reason_search == "":
reason_search = None
conditions = ["1=1"] conditions = ["1=1"]
params = [] params = []
@@ -261,6 +272,9 @@ class ConductModel:
if reason_prefix: if reason_prefix:
conditions.append("cr.reason LIKE %s") conditions.append("cr.reason LIKE %s")
params.append(f"{reason_prefix}%") params.append(f"{reason_prefix}%")
if reason_search:
conditions.append("cr.reason LIKE %s")
params.append(f"%{reason_search}%")
where_clause = " AND ".join(conditions) where_clause = " AND ".join(conditions)

View File

@@ -320,7 +320,8 @@ async def get_conduct_history(
grouped: bool = Query(False), grouped: bool = Query(False),
related_type: Optional[str] = None, related_type: Optional[str] = None,
reason_prefix: Optional[str] = None, reason_prefix: Optional[str] = None,
is_revoked: Optional[int] = None is_revoked: Optional[int] = None,
reason_search: Optional[str] = None
): ):
"""获取操行分历史记录""" """获取操行分历史记录"""
try: try:
@@ -337,7 +338,8 @@ async def get_conduct_history(
grouped=grouped, grouped=grouped,
related_type=related_type, related_type=related_type,
reason_prefix=reason_prefix, reason_prefix=reason_prefix,
is_revoked=is_revoked is_revoked=is_revoked,
reason_search=reason_search
) )
return success_response(data=result) return success_response(data=result)
except Exception as e: except Exception as e:

View File

@@ -18,6 +18,7 @@ import re
logger = setup_logger() logger = setup_logger()
router = APIRouter() router = APIRouter()
# 版本列表(按顺序)
# 版本列表(按顺序) # 版本列表(按顺序)
ALL_VERSIONS = { ALL_VERSIONS = {
'1.0': 'v1.0.sql', '1.0': 'v1.0.sql',
@@ -34,12 +35,11 @@ ALL_VERSIONS = {
'2.1': 'v2.1.sql', '2.1': 'v2.1.sql',
'2.2': 'v2.2.sql', '2.2': 'v2.2.sql',
'2.3': 'v2.3.sql', '2.3': 'v2.3.sql',
'2.3': 'v2.3.sql',
'2.4': 'v2.4.sql', '2.4': 'v2.4.sql',
'2.5': 'v2.5.sql', '2.5': 'v2.5.sql',
'2.5.1': 'v2.5.1.sql', '2.5.1': 'v2.5.1.sql',
'2.6': 'v2.6.sql',
} }
# 版本特征标记(按优先级从高到低) # 版本特征标记(按优先级从高到低)
VERSION_MARKERS = [ VERSION_MARKERS = [
('2.0', 'students', 'dormitory_number'), ('2.0', 'students', 'dormitory_number'),

View File

@@ -230,7 +230,8 @@ class ConductService:
grouped: bool = False, grouped: bool = False,
related_type: Optional[str] = None, related_type: Optional[str] = None,
reason_prefix: 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]: ) -> Dict[str, Any]:
"""获取历史记录""" """获取历史记录"""
# 空字符串转为None # 空字符串转为None
@@ -242,6 +243,8 @@ class ConductService:
related_type = None related_type = None
if reason_prefix == "": if reason_prefix == "":
reason_prefix = None reason_prefix = None
if reason_search == "":
reason_search = None
if related_type and related_type not in ('manual', 'homework', 'attendance'): if related_type and related_type not in ('manual', 'homework', 'attendance'):
return {"records": [], "page": page, "page_size": page_size, "total": 0, "total_pages": 0} return {"records": [], "page": page, "page_size": page_size, "total": 0, "total_pages": 0}
@@ -259,7 +262,8 @@ class ConductService:
reason_prefix=reason_prefix, reason_prefix=reason_prefix,
page=page, page=page,
page_size=page_size, page_size=page_size,
is_revoked=is_revoked is_revoked=is_revoked,
reason_search=reason_search
) )
records = await ConductModel.get_all_records( records = await ConductModel.get_all_records(
@@ -270,7 +274,8 @@ class ConductService:
student_id=student_id, student_id=student_id,
related_type=related_type, related_type=related_type,
reason_prefix=reason_prefix, 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: if reason_prefix:
count_conditions.append("cr.reason LIKE %s") count_conditions.append("cr.reason LIKE %s")
count_params.append(f"{reason_prefix}%") 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: if is_revoked is not None:
count_conditions.append("cr.is_revoked = %s") count_conditions.append("cr.is_revoked = %s")
count_params.append(1 if is_revoked else 0) count_params.append(1 if is_revoked else 0)

View File

@@ -27,14 +27,11 @@ include __DIR__ . '/../includes/header.php';
<div class="container"> <div class="container">
<div class="card"> <div class="card">
<div class="filter-bar" id="historyFilterBar"> <div class="filter-bar" id="historyFilterBar">
<div class="filter-group">
<label>学生</label>
<select id="historyStudentId" onchange="onStudentFilterChange()">
<option value="">全部</option>
</select>
</div>
<button class="btn btn-primary" onclick="loadHistory(1)">查询</button> <button class="btn btn-primary" onclick="loadHistory(1)">查询</button>
<button class="btn btn-ghost" id="filterToggleBtn" onclick="toggleFilterPanel()">展开筛选 ▼</button> <button class="btn btn-ghost" id="filterToggleBtn" onclick="toggleFilterPanel()">展开筛选 ▼</button>
<label class="history-grouped-label">
<input type="checkbox" id="historyGrouped" checked onchange="loadHistory(1)"> 批次合并
</label>
<?php if ($role === '班主任'): ?> <?php if ($role === '班主任'): ?>
<button class="btn btn-secondary" onclick="exportHistoryRecords()">导出</button> <button class="btn btn-secondary" onclick="exportHistoryRecords()">导出</button>
<?php endif; ?> <?php endif; ?>
@@ -42,6 +39,22 @@ include __DIR__ . '/../includes/header.php';
<!-- 高级筛选面板(默认折叠) --> <!-- 高级筛选面板(默认折叠) -->
<div id="advancedFilters" style="display:none; padding: 0 16px 16px; border-top: 1px solid var(--color-border-light);"> <div id="advancedFilters" style="display:none; padding: 0 16px 16px; border-top: 1px solid var(--color-border-light);">
<div style="display:flex; flex-wrap:wrap; gap:16px; padding-top:16px;"> <div style="display:flex; flex-wrap:wrap; gap:16px; padding-top:16px;">
<div class="filter-group">
<label>学生</label>
<select id="historyStudentId" onchange="onStudentFilterChange()">
<option value="">全部</option>
</select>
</div>
<div class="filter-group">
<label>科目</label>
<select id="historySubjectFilter" onchange="onSubjectFilterChange()">
<option value="">全部科目</option>
</select>
</div>
<div class="filter-group">
<label>搜索原因</label>
<input type="text" id="historyReasonSearch" placeholder="输入关键词..." style="min-width:150px;">
</div>
<div class="filter-group"> <div class="filter-group">
<label>开始日期</label> <label>开始日期</label>
<input type="date" id="historyStartDate"> <input type="date" id="historyStartDate">
@@ -74,11 +87,6 @@ include __DIR__ . '/../includes/header.php';
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<div style="margin-top: 12px;">
<label class="history-grouped-label">
<input type="checkbox" id="historyGrouped" checked onchange="loadHistory(1)"> 批次合并
</label>
</div>
</div> </div>
<div class="table-wrapper"> <div class="table-wrapper">

View File

@@ -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 = '<option value="">全部科目</option>';
res.data.subjects.forEach(s => {
html += '<option value="' + escapeHtml(s.subject_name) + '">' + escapeHtml(s.subject_name) + '</option>';
});
subjectSelect.innerHTML = html;
}
}
// 筛选学生时自动取消合并记录 // 筛选学生时自动取消合并记录
function onStudentFilterChange() { function onStudentFilterChange() {
var studentId = document.getElementById('historyStudentId').value; 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() { function toggleFilterPanel() {
var panel = document.getElementById('advancedFilters'); var panel = document.getElementById('advancedFilters');
var btn = document.getElementById('filterToggleBtn'); var btn = document.getElementById('filterToggleBtn');
if (!panel || !btn) return;
if (panel.style.display === 'none') { if (panel.style.display === 'none') {
panel.style.display = 'block'; panel.style.display = 'block';
btn.textContent = '收起筛选 ▲'; btn.textContent = '收起筛选 ▲';
@@ -62,6 +85,8 @@ async function loadHistory(page) {
var endDate = document.getElementById('historyEndDate').value; var endDate = document.getElementById('historyEndDate').value;
var studentId = document.getElementById('historyStudentId').value; var studentId = document.getElementById('historyStudentId').value;
var reasonFilter = document.getElementById('historyReasonFilter').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 isGrouped = document.getElementById('historyGrouped').checked;
var statusFilter = document.getElementById('historyStatusFilter') ? document.getElementById('historyStatusFilter').value : ''; var statusFilter = document.getElementById('historyStatusFilter') ? document.getElementById('historyStatusFilter').value : '';
@@ -74,7 +99,15 @@ async function loadHistory(page) {
end_date: endDate end_date: endDate
}; };
if (studentId) params.student_id = studentId; 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 (isGrouped) params.grouped = true;
if (statusFilter !== '') params.is_revoked = parseInt(statusFilter); if (statusFilter !== '') params.is_revoked = parseInt(statusFilter);
@@ -186,11 +219,18 @@ async function exportHistoryRecords() {
try { try {
var reasonFilter = document.getElementById('historyReasonFilter').value; 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 }; var params = { page: 1, page_size: 1000 };
if (startDate) params.start_date = startDate; if (startDate) params.start_date = startDate;
if (endDate) params.end_date = endDate; if (endDate) params.end_date = endDate;
if (studentId) params.student_id = studentId; 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); var res = await apiGet('/api/admin/conduct/history', params);
if (res && res.success && res.data.records) { 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 urlParams = new URLSearchParams(window.location.search);
var preStudentId = urlParams.get('student_id'); var preStudentId = urlParams.get('student_id');
if (preStudentId) { if (preStudentId) {
@@ -285,6 +326,7 @@ window.loadStudentsForSelect = loadStudentsForSelect;
window.exportHistoryRecords = exportHistoryRecords; window.exportHistoryRecords = exportHistoryRecords;
window.batchRevokeGrouped = batchRevokeGrouped; window.batchRevokeGrouped = batchRevokeGrouped;
window.onStudentFilterChange = onStudentFilterChange; window.onStudentFilterChange = onStudentFilterChange;
window.onSubjectFilterChange = onSubjectFilterChange;
window.toggleFilterPanel = toggleFilterPanel; window.toggleFilterPanel = toggleFilterPanel;
})(); })();

View File

@@ -126,7 +126,7 @@ function toggleSubjectPanel() {
} }
async function loadSubjectList() { 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) { if (res && res.success && res.data) {
let html = ''; let html = '';
const subjects = res.data.subjects || []; const subjects = res.data.subjects || [];

View File

@@ -230,10 +230,10 @@ INSERT IGNORE INTO `subjects` (`subject_name`, `subject_code`, `sort_order`) VAL
('数学', 'MATH', 2), ('数学', 'MATH', 2),
('英语', 'ENG', 3); ('英语', 'ENG', 3);
-- 初始化系统版本号
-- 初始化系统版本号 -- 初始化系统版本号
INSERT INTO `system_settings` (`setting_key`, `setting_value`) INSERT INTO `system_settings` (`setting_key`, `setting_value`)
VALUES ('db_version', '2.5.1') VALUES ('db_version', '2.6')
ON DUPLICATE KEY UPDATE `setting_value` = '2.5.1'; ON DUPLICATE KEY UPDATE `setting_value` = '2.6';
-- 控制台输出初始化结果(含版本号) -- 控制台输出初始化结果(含版本号)
SELECT CONCAT('数据库初始化完成!版本: v', (SELECT setting_value FROM system_settings WHERE setting_key = 'db_version')) AS message; SELECT CONCAT('数据库初始化完成!版本: v', (SELECT setting_value FROM system_settings WHERE setting_key = 'db_version')) AS message;

12
sql/upgrades/v2.6.sql Normal file
View File

@@ -0,0 +1,12 @@
-- ===========================================
-- 班级操行分管理系统 - v2.5.1 → v2.6 升级脚本
-- 字符集: utf8mb4
--
-- 说明: v2.6 为功能增强版本,无数据库 schema 变更。
-- 主要变更:
-- 1. 历史记录页筛选面板重构(学生筛选移入面板、合并按钮移出面板)
-- 2. 新增科目下拉筛选(通过 reason 前缀匹配)
-- 3. 新增原因关键词搜索reason_search 模糊匹配)
-- 4. 科目管理删除后仅显示启用科目
-- 5. 筛选面板展开/收起功能修复
-- ===========================================

View File

@@ -29,6 +29,7 @@ $UPGRADE_VERSIONS = [
'2.4' => __DIR__ . '/sql/upgrades/v2.4.sql', '2.4' => __DIR__ . '/sql/upgrades/v2.4.sql',
'2.5' => __DIR__ . '/sql/upgrades/v2.5.sql', '2.5' => __DIR__ . '/sql/upgrades/v2.5.sql',
'2.5.1' => __DIR__ . '/sql/upgrades/v2.5.1.sql', '2.5.1' => __DIR__ . '/sql/upgrades/v2.5.1.sql',
'2.6' => __DIR__ . '/sql/upgrades/v2.6.sql',
]; ];
/** /**