v2.5.1更新
This commit is contained in:
@@ -273,6 +273,10 @@ classmanager/
|
|||||||
| v1.8 | 2026.5.22 | 科目管理融入作业管理页、科目删除数据依赖检查、加减分记录类型区分(manual/homework/attendance)、学生端作业详情优化 |
|
| v1.8 | 2026.5.22 | 科目管理融入作业管理页、科目删除数据依赖检查、加减分记录类型区分(manual/homework/attendance)、学生端作业详情优化 |
|
||||||
| v2.0.1 | 2026.5.23 | 操作列折叠优化、扣分类型大类区分、科目选择修复、改名作业扣分、记录人优化、家长端优化、学期管理优化 |
|
| v2.0.1 | 2026.5.23 | 操作列折叠优化、扣分类型大类区分、科目选择修复、改名作业扣分、记录人优化、家长端优化、学期管理优化 |
|
||||||
| v2.1 | 2026.5.26 | CSS变量化统一配色方案、简化按钮系统、操作列按钮风格统一、清理内联颜色、修复科目管理面板无法展开、数据库索引优化、清理init.sql冗余迁移代码、安全审计通过 |
|
| 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转义修复 |
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
|
|||||||
@@ -224,7 +224,8 @@ class ConductModel:
|
|||||||
related_type: str = None,
|
related_type: str = None,
|
||||||
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
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""获取分组后的操行分记录(同批次合并)"""
|
"""获取分组后的操行分记录(同批次合并)"""
|
||||||
if start_date == "":
|
if start_date == "":
|
||||||
@@ -236,9 +237,15 @@ class ConductModel:
|
|||||||
if reason_prefix == "":
|
if reason_prefix == "":
|
||||||
reason_prefix = None
|
reason_prefix = None
|
||||||
|
|
||||||
conditions = ["cr.is_revoked = 0"]
|
conditions = ["1=1"]
|
||||||
params = []
|
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:
|
if student_id:
|
||||||
conditions.append("cr.student_id = %s")
|
conditions.append("cr.student_id = %s")
|
||||||
params.append(student_id)
|
params.append(student_id)
|
||||||
@@ -258,7 +265,7 @@ class ConductModel:
|
|||||||
where_clause = " AND ".join(conditions)
|
where_clause = " AND ".join(conditions)
|
||||||
|
|
||||||
count_sql = f"""
|
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
|
FROM conduct_records cr
|
||||||
WHERE {where_clause}
|
WHERE {where_clause}
|
||||||
"""
|
"""
|
||||||
@@ -270,11 +277,12 @@ class ConductModel:
|
|||||||
cr.recorder_name,
|
cr.recorder_name,
|
||||||
DATE_FORMAT(MIN(cr.created_at), '%%Y-%%m-%%d %%H:%%i:%%s') as created_at,
|
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,
|
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
|
FROM conduct_records cr
|
||||||
JOIN students s ON cr.student_id = s.student_id
|
JOIN students s ON cr.student_id = s.student_id
|
||||||
WHERE {where_clause}
|
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
|
ORDER BY MIN(cr.created_at) DESC
|
||||||
LIMIT %s OFFSET %s
|
LIMIT %s OFFSET %s
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -42,11 +42,28 @@ async def list_semesters(request: Request):
|
|||||||
|
|
||||||
@router.get("/active")
|
@router.get("/active")
|
||||||
async def get_active_semester(request: Request):
|
async def get_active_semester(request: Request):
|
||||||
"""获取当前活跃学期"""
|
"""获取当前活跃学期(含当前周数)"""
|
||||||
user = await get_current_user(request)
|
user = await get_current_user(request)
|
||||||
result = await SemesterService.get_active_semester()
|
result = await SemesterService.get_active_semester()
|
||||||
if result["success"]:
|
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:
|
else:
|
||||||
return error_response(message=result["message"])
|
return error_response(message=result["message"])
|
||||||
|
|
||||||
|
|||||||
@@ -52,4 +52,4 @@ async def delete_subject(request: Request, subject_id: int):
|
|||||||
if not await PermissionChecker.check_can_manage_subjects(user["user_id"]):
|
if not await PermissionChecker.check_can_manage_subjects(user["user_id"]):
|
||||||
return error_response(message="无权限", code=403)
|
return error_response(message="无权限", code=403)
|
||||||
result = await SubjectService.delete_subject(subject_id)
|
result = await SubjectService.delete_subject(subject_id)
|
||||||
return success_response(message="科目已禁用") if result["success"] else error_response(message=result["message"])
|
return success_response(message="科目已删除") if result["success"] else error_response(message=result["message"])
|
||||||
@@ -37,6 +37,7 @@ ALL_VERSIONS = {
|
|||||||
'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',
|
||||||
}
|
}
|
||||||
|
|
||||||
# 版本特征标记(按优先级从高到低)
|
# 版本特征标记(按优先级从高到低)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from datetime import datetime
|
|||||||
from models.student import StudentModel
|
from models.student import StudentModel
|
||||||
from models.conduct import ConductModel
|
from models.conduct import ConductModel
|
||||||
from models.user import UserModel
|
from models.user import UserModel
|
||||||
|
from models.semester import SemesterModel
|
||||||
from middleware.permission import PermissionChecker
|
from middleware.permission import PermissionChecker
|
||||||
from config import settings
|
from config import settings
|
||||||
from utils.logger import get_logger
|
from utils.logger import get_logger
|
||||||
@@ -86,6 +87,10 @@ class ConductService:
|
|||||||
fail_count = 0
|
fail_count = 0
|
||||||
details = []
|
details = []
|
||||||
|
|
||||||
|
# 自动获取当前活跃学期
|
||||||
|
active_semester = await SemesterModel.get_active()
|
||||||
|
semester_id = active_semester['semester_id'] if active_semester else None
|
||||||
|
|
||||||
for student_id in student_ids:
|
for student_id in student_ids:
|
||||||
try:
|
try:
|
||||||
# 检查学生是否存在
|
# 检查学生是否存在
|
||||||
@@ -104,6 +109,17 @@ class ConductService:
|
|||||||
related_type=related_type
|
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)
|
await StudentModel.update_total_points(student_id, points_change)
|
||||||
|
|
||||||
@@ -242,7 +258,8 @@ class ConductService:
|
|||||||
related_type=related_type,
|
related_type=related_type,
|
||||||
reason_prefix=reason_prefix,
|
reason_prefix=reason_prefix,
|
||||||
page=page,
|
page=page,
|
||||||
page_size=page_size
|
page_size=page_size,
|
||||||
|
is_revoked=is_revoked
|
||||||
)
|
)
|
||||||
|
|
||||||
records = await ConductModel.get_all_records(
|
records = await ConductModel.get_all_records(
|
||||||
|
|||||||
@@ -30,10 +30,23 @@ class SemesterService:
|
|||||||
"""获取学期列表"""
|
"""获取学期列表"""
|
||||||
try:
|
try:
|
||||||
semesters = await SemesterModel.get_all()
|
semesters = await SemesterModel.get_all()
|
||||||
|
today = datetime.date.today()
|
||||||
for sem in semesters:
|
for sem in semesters:
|
||||||
counts = await SemesterModel.count_records_by_semester(sem['semester_id'])
|
counts = await SemesterModel.count_records_by_semester(sem['semester_id'])
|
||||||
sem['conduct_count'] = counts['conduct_count']
|
sem['conduct_count'] = counts['conduct_count']
|
||||||
sem['attendance_count'] = counts['attendance_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 {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"semesters": semesters
|
"semesters": semesters
|
||||||
|
|||||||
@@ -26,54 +26,59 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="filter-bar">
|
<div class="filter-bar" id="historyFilterBar">
|
||||||
<div class="filter-group">
|
|
||||||
<label>开始日期</label>
|
|
||||||
<input type="date" id="historyStartDate">
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>结束日期</label>
|
|
||||||
<input type="date" id="historyEndDate">
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label>学生</label>
|
<label>学生</label>
|
||||||
<select id="historyStudentId">
|
<select id="historyStudentId" onchange="onStudentFilterChange()">
|
||||||
<option value="">全部</option>
|
<option value="">全部</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
|
||||||
<label>扣分类型</label>
|
|
||||||
<select id="historyReasonFilter">
|
|
||||||
<option value="">全部类型</option>
|
|
||||||
<option value="卫生">卫生</option>
|
|
||||||
<option value="课堂">课堂</option>
|
|
||||||
<option value="纪律">纪律</option>
|
|
||||||
<option value="作业">作业</option>
|
|
||||||
<option value="考勤">考勤</option>
|
|
||||||
<option value="劳动">劳动</option>
|
|
||||||
<option value="志愿">志愿</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>状态</label>
|
|
||||||
<select id="historyStatusFilter">
|
|
||||||
<option value="">全部</option>
|
|
||||||
<option value="0">正常</option>
|
|
||||||
<option value="1">已撤销</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<button class="btn btn-primary" onclick="loadHistory(1)">查询</button>
|
<button class="btn btn-primary" onclick="loadHistory(1)">查询</button>
|
||||||
<div class="filter-group" style="min-width:auto;">
|
<button class="btn btn-ghost" id="filterToggleBtn" onclick="toggleFilterPanel()">展开筛选 ▼</button>
|
||||||
<label> </label>
|
<?php if ($role === '班主任'): ?>
|
||||||
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;font-size:13px;">
|
<button class="btn btn-secondary" onclick="exportHistoryRecords()">导出</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<!-- 高级筛选面板(默认折叠) -->
|
||||||
|
<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 class="filter-group">
|
||||||
|
<label>开始日期</label>
|
||||||
|
<input type="date" id="historyStartDate">
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>结束日期</label>
|
||||||
|
<input type="date" id="historyEndDate">
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>扣分类型</label>
|
||||||
|
<select id="historyReasonFilter">
|
||||||
|
<option value="">全部类型</option>
|
||||||
|
<option value="卫生">卫生</option>
|
||||||
|
<option value="课堂">课堂</option>
|
||||||
|
<option value="纪律">纪律</option>
|
||||||
|
<option value="作业">作业</option>
|
||||||
|
<option value="考勤">考勤</option>
|
||||||
|
<option value="劳动">劳动</option>
|
||||||
|
<option value="志愿">志愿</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>状态</label>
|
||||||
|
<select id="historyStatusFilter">
|
||||||
|
<option value="">全部</option>
|
||||||
|
<option value="0">正常</option>
|
||||||
|
<option value="1">已撤销</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 12px;">
|
||||||
|
<label class="history-grouped-label">
|
||||||
<input type="checkbox" id="historyGrouped" checked onchange="loadHistory(1)"> 批次合并
|
<input type="checkbox" id="historyGrouped" checked onchange="loadHistory(1)"> 批次合并
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<?php if ($role === '班主任'): ?>
|
|
||||||
<button class="btn btn-secondary" onclick="exportHistoryRecords()">导出历史记录</button>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<th>学期名称</th>
|
<th>学期名称</th>
|
||||||
<th>开始日期</th>
|
<th>开始日期</th>
|
||||||
<th>结束日期</th>
|
<th>结束日期</th>
|
||||||
|
<th>当前周数</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
<th>记录数</th>
|
<th>记录数</th>
|
||||||
<th>创建时间</th>
|
<th>创建时间</th>
|
||||||
|
|||||||
@@ -931,19 +931,20 @@ tr:hover {
|
|||||||
/* ========== 历史记录页优化 ========== */
|
/* ========== 历史记录页优化 ========== */
|
||||||
/* 时间列:确保分两行显示(日期+时间) */
|
/* 时间列:确保分两行显示(日期+时间) */
|
||||||
.history-time {
|
.history-time {
|
||||||
white-space: pre-line;
|
white-space: nowrap;
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
word-break: break-all;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 原因列:每行最少7个字,自动换行 */
|
/* 原因列:每行最少7个字,自动换行 */
|
||||||
.history-reason {
|
.history-reason {
|
||||||
min-width: 7em;
|
min-width: 7em;
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
white-space: pre-wrap;
|
white-space: normal;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 学生名列:允许换行 */
|
/* 学生名列:允许换行 */
|
||||||
@@ -953,6 +954,34 @@ tr:hover {
|
|||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
max-width: 120px;
|
max-width: 120px;
|
||||||
line-height: 1.5;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 合并记录按钮样式 */
|
/* 合并记录按钮样式 */
|
||||||
|
|||||||
@@ -337,25 +337,40 @@ function toggleActionDropdown(el) {
|
|||||||
|
|
||||||
var isOpen = menu.classList.contains('show');
|
var isOpen = menu.classList.contains('show');
|
||||||
// 先关闭所有
|
// 先关闭所有
|
||||||
document.querySelectorAll('.action-dropdown-menu.show').forEach(function(m) {
|
closeAllDropdowns();
|
||||||
m.classList.remove('show');
|
|
||||||
var toggle = m.closest('.action-dropdown').querySelector('.action-dropdown-toggle');
|
|
||||||
if (toggle) toggle.classList.remove('open');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isOpen) {
|
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');
|
menu.classList.add('show');
|
||||||
el.classList.add('open');
|
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) {
|
document.addEventListener('click', function(e) {
|
||||||
if (!e.target.closest('.action-dropdown')) {
|
if (!e.target.closest('.action-dropdown')) {
|
||||||
document.querySelectorAll('.action-dropdown-menu.show').forEach(function(m) {
|
closeAllDropdowns();
|
||||||
m.classList.remove('show');
|
|
||||||
var toggle = m.closest('.action-dropdown').querySelector('.action-dropdown-toggle');
|
|
||||||
if (toggle) toggle.classList.remove('open');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,15 @@ const role = window.PAGE_CONFIG.role;
|
|||||||
let totalStudents = 0;
|
let totalStudents = 0;
|
||||||
|
|
||||||
async function loadDashboard() {
|
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) {
|
if (studentsRes && studentsRes.success) {
|
||||||
document.getElementById('dashboardStats').innerHTML = `
|
statsHtml += `
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">学生总数</div>
|
<div class="stat-label">学生总数</div>
|
||||||
<div class="stat-value">${studentsRes.data.total || 0}</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 = '';
|
let quickActions = '';
|
||||||
if (role === '班主任' || role === '班长' || role === '学习委员' || role === '劳动委员' || role === '志愿委员') {
|
if (role === '班主任' || role === '班长' || role === '学习委员' || role === '劳动委员' || role === '志愿委员') {
|
||||||
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>';
|
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>';
|
||||||
|
|||||||
@@ -15,41 +15,74 @@ const currentUserId = window.PAGE_CONFIG.userId;
|
|||||||
let currentHistoryPage = 1;
|
let currentHistoryPage = 1;
|
||||||
let totalHistoryPages = 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() {
|
async function loadStudentsForSelect() {
|
||||||
const res = await apiGet('/api/admin/students', {page_size: 1000});
|
const res = await apiGet('/api/admin/students', {page_size: 1000});
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
let html = '<option value="">全部</option>';
|
let html = '<option value="">全部</option>';
|
||||||
res.data.students.forEach(s => {
|
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;
|
document.getElementById('historyStudentId').innerHTML = html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadHistory(page = 1) {
|
// 筛选学生时自动取消合并记录
|
||||||
currentHistoryPage = page;
|
function onStudentFilterChange() {
|
||||||
const startDate = document.getElementById('historyStartDate').value;
|
var studentId = document.getElementById('historyStudentId').value;
|
||||||
const endDate = document.getElementById('historyEndDate').value;
|
if (studentId) {
|
||||||
const studentId = document.getElementById('historyStudentId').value;
|
var grouped = document.getElementById('historyGrouped');
|
||||||
const reasonFilter = document.getElementById('historyReasonFilter').value;
|
if (grouped) grouped.checked = false;
|
||||||
const isGrouped = document.getElementById('historyGrouped').checked;
|
}
|
||||||
const statusFilter = document.getElementById('historyStatusFilter')?.value;
|
}
|
||||||
|
|
||||||
const params = {
|
// 折叠/展开筛选面板
|
||||||
page, page_size: 20,
|
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;
|
||||||
|
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 : '';
|
||||||
|
|
||||||
|
// 筛选学生时强制取消合并
|
||||||
|
if (studentId) isGrouped = false;
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
page: page, page_size: 20,
|
||||||
start_date: startDate,
|
start_date: startDate,
|
||||||
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 (reasonFilter) params.reason_prefix = reasonFilter;
|
||||||
if (isGrouped) params.grouped = true;
|
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) {
|
if (res && res.success) {
|
||||||
const nowrapStyle = ' style="white-space: nowrap; min-width: 80px;"';
|
var nowrapStyle = ' style="white-space:nowrap;min-width:80px;"';
|
||||||
let headHtml = '';
|
var headHtml = '';
|
||||||
if (isGrouped) {
|
if (isGrouped) {
|
||||||
headHtml = '<th>时间</th><th>原因</th><th>分值</th><th' + nowrapStyle + '>操作人</th><th>涉及学生</th>';
|
headHtml = '<th>时间</th><th>原因</th><th>分值</th><th' + nowrapStyle + '>操作人</th><th>涉及学生</th>';
|
||||||
if (role === '班主任' || role === '班长') {
|
if (role === '班主任' || role === '班长') {
|
||||||
@@ -63,71 +96,71 @@ async function loadHistory(page = 1) {
|
|||||||
}
|
}
|
||||||
document.getElementById('historyTableHead').innerHTML = headHtml;
|
document.getElementById('historyTableHead').innerHTML = headHtml;
|
||||||
|
|
||||||
let html = '';
|
var html = '';
|
||||||
if (isGrouped) {
|
if (isGrouped) {
|
||||||
res.data.records.forEach(record => {
|
res.data.records.forEach(function(record) {
|
||||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
var pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||||
const names = record.student_names || '';
|
var names = record.student_names || '';
|
||||||
const allRevoked = record.all_revoked;
|
var allRevoked = record.all_revoked;
|
||||||
const revokedStyle = allRevoked ? ' style="opacity:0.5; text-decoration:line-through;"' : '';
|
var revokedStyle = allRevoked ? ' style="opacity:0.5;text-decoration:line-through;"' : '';
|
||||||
html += `<tr${revokedStyle}>
|
html += '<tr' + revokedStyle + '>' +
|
||||||
<td class="history-time">${formatDateTime(record.created_at)}</td>
|
'<td class="history-time">' + formatDateTime(record.created_at) + '</td>' +
|
||||||
<td class="preserve-newlines history-reason">${escapeHtml(record.reason)}</td>
|
'<td class="history-reason">' + escapeHtml(record.reason) + '</td>' +
|
||||||
<td class="${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}×${record.student_count}</td>
|
'<td class="' + pointsClass + '">' + (record.points_change > 0 ? '+' : '') + record.points_change + '×' + record.student_count + '</td>' +
|
||||||
<td>${escapeHtml(record.recorder_name || '')}</td>
|
'<td>' + escapeHtml(record.recorder_name || '') + '</td>' +
|
||||||
<td class="history-students">${escapeHtml(names)}</td>`;
|
'<td class="history-students">' + escapeHtml(names) + '</td>';
|
||||||
if (role === '班主任' || role === '班长') {
|
if (role === '班主任' || role === '班长') {
|
||||||
if (allRevoked) {
|
if (allRevoked) {
|
||||||
html += `<td><span class="text-muted">已撤销</span></td>`;
|
html += '<td><span class="text-muted">已撤销</span></td>';
|
||||||
} else {
|
} 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) {
|
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>';
|
html = '<tr><td colspan="' + colSpan + '" style="text-align:center;">暂无记录</td></tr>';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.data.records.forEach(record => {
|
res.data.records.forEach(function(record) {
|
||||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
var pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||||
const revokedStyle = record.is_revoked == 1 ? ' style="opacity:0.5; text-decoration:line-through;"' : '';
|
var revokedStyle = record.is_revoked == 1 ? ' style="opacity:0.5;text-decoration:line-through;"' : '';
|
||||||
html += `<tr${revokedStyle}>
|
html += '<tr' + revokedStyle + '>' +
|
||||||
<td class="history-time">${formatDateTime(record.created_at)}</td>
|
'<td class="history-time">' + formatDateTime(record.created_at) + '</td>' +
|
||||||
<td class="history-students">${escapeHtml(record.student_name)}</td>
|
'<td>' + escapeHtml(record.student_name) + '</td>' +
|
||||||
<td class="${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
'<td class="' + pointsClass + '">' + (record.points_change > 0 ? '+' : '') + record.points_change + '</td>' +
|
||||||
<td class="preserve-newlines history-reason">${escapeHtml(record.reason)}</td>
|
'<td class="history-reason">' + escapeHtml(record.reason) + '</td>' +
|
||||||
<td>${escapeHtml(record.recorder_name)}</td>`;
|
'<td>' + escapeHtml(record.recorder_name) + '</td>';
|
||||||
if (role === '班主任') {
|
if (role === '班主任') {
|
||||||
if (record.is_revoked == 1) {
|
if (record.is_revoked == 1) {
|
||||||
const revokerInfo = record.revoker_name ? `由 ${escapeHtml(record.revoker_name)} 撤销` : '已撤销';
|
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>`;
|
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 {
|
} 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 === '班长') {
|
} else if (role === '班长') {
|
||||||
if (record.is_revoked == 1) {
|
if (record.is_revoked == 1) {
|
||||||
const revokerInfo = record.revoker_name ? `由 ${escapeHtml(record.revoker_name)} 撤销` : '已撤销';
|
var revokerInfo = record.revoker_name ? '由 ' + escapeHtml(record.revoker_name) + ' 撤销' : '已撤销';
|
||||||
html += `<td><span class="text-muted">${revokerInfo}</span></td>`;
|
html += '<td><span class="text-muted">' + revokerInfo + '</span></td>';
|
||||||
} else {
|
} 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 === '考勤委员') {
|
} else if (role === '考勤委员') {
|
||||||
if (record.is_revoked == 1) {
|
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) {
|
} 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 {
|
} 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) {
|
if (res.data.records.length === 0) {
|
||||||
const colSpan = (role === '班主任' || role === '班长' || role === '考勤委员') ? 6 : 5;
|
var colSpan = (role === '班主任' || role === '班长' || role === '考勤委员') ? 6 : 5;
|
||||||
html = `<tr><td colspan="${colSpan}" style="text-align:center;">暂无记录</td></tr>`;
|
html = '<tr><td colspan="' + colSpan + '" style="text-align:center;">暂无记录</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,47 +178,47 @@ function renderHistoryPagination() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function exportHistoryRecords() {
|
async function exportHistoryRecords() {
|
||||||
const startDate = document.getElementById('historyStartDate').value;
|
var startDate = document.getElementById('historyStartDate').value;
|
||||||
const endDate = document.getElementById('historyEndDate').value;
|
var endDate = document.getElementById('historyEndDate').value;
|
||||||
const studentId = document.getElementById('historyStudentId').value;
|
var studentId = document.getElementById('historyStudentId').value;
|
||||||
|
|
||||||
showToast('正在导出历史记录...', 'info');
|
showToast('正在导出历史记录...', 'info');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const reasonFilter = document.getElementById('historyReasonFilter').value;
|
var reasonFilter = document.getElementById('historyReasonFilter').value;
|
||||||
const 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 (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) {
|
if (res && res.success && res.data.records) {
|
||||||
const records = res.data.records;
|
var records = res.data.records;
|
||||||
if (records.length === 0) {
|
if (records.length === 0) {
|
||||||
showToast('没有找到记录', 'warning');
|
showToast('没有找到记录', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let csv = '\uFEFF';
|
var csv = '\uFEFF';
|
||||||
csv += '时间,学号,姓名,分数变动,原因,操作人\n';
|
csv += '时间,学号,姓名,分数变动,原因,操作人\n';
|
||||||
records.forEach(r => {
|
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`;
|
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;' });
|
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
const url = URL.createObjectURL(blob);
|
var url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
var link = document.createElement('a');
|
||||||
link.href = url;
|
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);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
showToast(`导出成功,共${records.length}条记录`);
|
showToast('导出成功,共' + records.length + '条记录');
|
||||||
} else {
|
} else {
|
||||||
showToast('导出失败:' + (res?.message || '未知错误'), 'error');
|
showToast('导出失败:' + (res && res.message || '未知错误'), 'error');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast('导出失败:' + err.message, 'error');
|
showToast('导出失败:' + err.message, 'error');
|
||||||
@@ -194,13 +227,12 @@ async function exportHistoryRecords() {
|
|||||||
|
|
||||||
// 批量撤销合并记录(按条件查找并撤销)
|
// 批量撤销合并记录(按条件查找并撤销)
|
||||||
async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt) {
|
async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt) {
|
||||||
if (!confirm(`确定要撤销所有"${reason}"(${pointsChange > 0 ? '+' : ''}${pointsChange}分)的记录吗?`)) return;
|
if (!confirm('确定要撤销所有"' + reason + '"(' + (pointsChange > 0 ? '+' : '') + pointsChange + '分)的记录吗?')) return;
|
||||||
|
|
||||||
showToast('正在批量撤销...', 'info');
|
showToast('正在批量撤销...', 'info');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 先查询匹配的记录
|
var params = {
|
||||||
const params = {
|
|
||||||
page: 1, page_size: 1000,
|
page: 1, page_size: 1000,
|
||||||
start_date: document.getElementById('historyStartDate').value,
|
start_date: document.getElementById('historyStartDate').value,
|
||||||
end_date: document.getElementById('historyEndDate').value,
|
end_date: document.getElementById('historyEndDate').value,
|
||||||
@@ -208,15 +240,14 @@ async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt)
|
|||||||
grouped: false
|
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) {
|
if (!res || !res.success || !res.data.records) {
|
||||||
showToast('查询记录失败', 'error');
|
showToast('查询记录失败', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 精确匹配
|
var matchedIds = [];
|
||||||
const matchedIds = [];
|
res.data.records.forEach(function(r) {
|
||||||
res.data.records.forEach(r => {
|
|
||||||
if (r.reason === reason && r.points_change === pointsChange && r.is_revoked == 0) {
|
if (r.reason === reason && r.points_change === pointsChange && r.is_revoked == 0) {
|
||||||
matchedIds.push(r.record_id);
|
matchedIds.push(r.record_id);
|
||||||
}
|
}
|
||||||
@@ -227,32 +258,33 @@ async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt)
|
|||||||
return;
|
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) {
|
if (revokeRes && revokeRes.success) {
|
||||||
showToast(`批量撤销完成: ${revokeRes.data.success_count}条成功`);
|
showToast('批量撤销完成: ' + (revokeRes.data ? revokeRes.data.success_count : 0) + '条成功');
|
||||||
loadHistory(currentHistoryPage);
|
loadHistory(currentHistoryPage);
|
||||||
} else {
|
} else {
|
||||||
showToast(revokeRes?.message || '批量撤销失败', 'error');
|
showToast(revokeRes && revokeRes.message || '批量撤销失败', 'error');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast('批量撤销失败: ' + err.message, 'error');
|
showToast('批量撤销失败: ' + err.message, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadStudentsForSelect().then(() => {
|
loadStudentsForSelect().then(function() {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
var urlParams = new URLSearchParams(window.location.search);
|
||||||
const preStudentId = urlParams.get('student_id');
|
var preStudentId = urlParams.get('student_id');
|
||||||
if (preStudentId) {
|
if (preStudentId) {
|
||||||
document.getElementById('historyStudentId').value = preStudentId;
|
document.getElementById('historyStudentId').value = preStudentId;
|
||||||
loadHistory();
|
onStudentFilterChange();
|
||||||
} else {
|
|
||||||
loadHistory();
|
|
||||||
}
|
}
|
||||||
|
loadHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.loadHistory = loadHistory;
|
window.loadHistory = loadHistory;
|
||||||
window.loadStudentsForSelect = loadStudentsForSelect;
|
window.loadStudentsForSelect = loadStudentsForSelect;
|
||||||
window.exportHistoryRecords = exportHistoryRecords;
|
window.exportHistoryRecords = exportHistoryRecords;
|
||||||
window.batchRevokeGrouped = batchRevokeGrouped;
|
window.batchRevokeGrouped = batchRevokeGrouped;
|
||||||
|
window.onStudentFilterChange = onStudentFilterChange;
|
||||||
|
window.toggleFilterPanel = toggleFilterPanel;
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -127,25 +127,29 @@ function toggleSubjectPanel() {
|
|||||||
|
|
||||||
async function loadSubjectList() {
|
async function loadSubjectList() {
|
||||||
const res = await apiGet('/api/subject/list');
|
const res = await apiGet('/api/subject/list');
|
||||||
if (res && res.success) {
|
if (res && res.success && res.data) {
|
||||||
let html = '';
|
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 += `
|
html += `
|
||||||
<div class="subject-item">
|
<div class="subject-item">
|
||||||
<span class="subject-name">${escapeHtml(sub.subject_name)}</span>
|
<span class="subject-name">${safeName}</span>
|
||||||
<span class="subject-code">${escapeHtml(sub.subject_code || '')}</span>
|
<span class="subject-code">${safeCode}</span>
|
||||||
<span class="subject-status ${sub.is_active ? 'subject-status-active' : 'subject-status-inactive'}">
|
<span class="subject-status ${sub.is_active ? 'subject-status-active' : 'subject-status-inactive'}">
|
||||||
${sub.is_active ? '启用' : '禁用'}
|
${sub.is_active ? '启用' : '禁用'}
|
||||||
</span>
|
</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-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})">
|
<button class="btn btn-sm btn-ghost" onclick="toggleSubjectStatus(${sub.subject_id}, ${sub.is_active ? 'false' : 'true'})">
|
||||||
${sub.is_active ? '禁用' : '启用'}
|
${sub.is_active ? '禁用' : '启用'}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteSubject(${sub.subject_id})">删除</button>
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteSubject(${sub.subject_id})">删除</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
if (res.data.subjects.length === 0) {
|
if (subjects.length === 0) {
|
||||||
html = '<p style="text-align:center;padding:40px;">暂无科目,请点击"添加科目"</p>';
|
html = '<p style="text-align:center;padding:40px;">暂无科目,请点击"添加科目"</p>';
|
||||||
}
|
}
|
||||||
document.getElementById('subjectList').innerHTML = html;
|
document.getElementById('subjectList').innerHTML = html;
|
||||||
|
|||||||
@@ -15,12 +15,9 @@
|
|||||||
// HTML转义
|
// HTML转义
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
if (!str) return '';
|
if (!str) return '';
|
||||||
return str.replace(/[&<>]/g, function(m) {
|
var el = document.createElement('span');
|
||||||
if (m === '&') return '&';
|
el.appendChild(document.createTextNode(str));
|
||||||
if (m === '<') return '<';
|
return el.innerHTML;
|
||||||
if (m === '>') return '>';
|
|
||||||
return m;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全选功能
|
// 全选功能
|
||||||
|
|||||||
@@ -81,10 +81,12 @@ async function loadSemesters() {
|
|||||||
recordText = `${conductCount}条操行分 / ${attendanceCount}条考勤`;
|
recordText = `${conductCount}条操行分 / ${attendanceCount}条考勤`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const weekText = sem.current_week ? `第${sem.current_week}周` : '-';
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${escapeHtml(sem.semester_name)}</td>
|
<td>${escapeHtml(sem.semester_name)}</td>
|
||||||
<td>${formatDate(sem.start_date)}</td>
|
<td>${formatDate(sem.start_date)}</td>
|
||||||
<td>${formatDate(sem.end_date)}</td>
|
<td>${formatDate(sem.end_date)}</td>
|
||||||
|
<td>${weekText}</td>
|
||||||
<td><span class="${statusClass}">${statusText}</span></td>
|
<td><span class="${statusClass}">${statusText}</span></td>
|
||||||
<td>${recordText}</td>
|
<td>${recordText}</td>
|
||||||
<td>${formatDateTime(sem.created_at)}</td>
|
<td>${formatDateTime(sem.created_at)}</td>
|
||||||
@@ -92,7 +94,7 @@ async function loadSemesters() {
|
|||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
if (semesters.length === 0) {
|
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;
|
document.getElementById('semesterList').innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ async function loadHistory(page) {
|
|||||||
const pointsText = record.points_change > 0 ? `+${record.points_change}` : record.points_change;
|
const pointsText = record.points_change > 0 ? `+${record.points_change}` : record.points_change;
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${formatDateTime(record.created_at)}</td>
|
<td>${formatDateTime(record.created_at)}</td>
|
||||||
<td class="preserve-newlines">${escapeHtml(record.reason || '-')}</td>
|
<td class="history-reason">${escapeHtml(record.reason || '-')}</td>
|
||||||
<td><span class="record-points ${pointsClass}">${pointsText}</span></td>
|
<td><span class="record-points ${pointsClass}">${pointsText}</span></td>
|
||||||
<td>班主任</td>
|
<td>班主任</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
|
|||||||
@@ -232,8 +232,8 @@ INSERT IGNORE INTO `subjects` (`subject_name`, `subject_code`, `sort_order`) VAL
|
|||||||
|
|
||||||
-- 初始化系统版本号
|
-- 初始化系统版本号
|
||||||
INSERT INTO `system_settings` (`setting_key`, `setting_value`)
|
INSERT INTO `system_settings` (`setting_key`, `setting_value`)
|
||||||
VALUES ('db_version', '2.5')
|
VALUES ('db_version', '2.5.1')
|
||||||
ON DUPLICATE KEY UPDATE `setting_value` = '2.5';
|
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;
|
SELECT CONCAT('数据库初始化完成!版本: v', (SELECT setting_value FROM system_settings WHERE setting_key = 'db_version')) AS message;
|
||||||
|
|||||||
11
sql/upgrades/v2.5.1.sql
Normal file
11
sql/upgrades/v2.5.1.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- ===========================================
|
||||||
|
-- 班级操行分管理系统 - v2.5 → v2.5.1 升级脚本
|
||||||
|
-- 字符集: utf8mb4
|
||||||
|
--
|
||||||
|
-- 说明: v2.5.1 为 UI 优化版本,无数据库 schema 变更。
|
||||||
|
-- 主要变更:
|
||||||
|
-- 1. 筛选学生时自动取消合并记录
|
||||||
|
-- 2. 合并记录选项样式优化(修复竖排显示)
|
||||||
|
-- 3. 历史记录筛选功能改为折叠式
|
||||||
|
-- 4. 科目管理调用修复
|
||||||
|
-- ===========================================
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
--
|
--
|
||||||
-- 说明: v2.5 为功能增强版本,无数据库 schema 变更。
|
-- 说明: v2.5 为功能增强版本,无数据库 schema 变更。
|
||||||
-- 主要变更:
|
-- 主要变更:
|
||||||
-- 1. 历史记录页优化(文字宽度/换行/合并按钮样式)
|
-- 1. 历史记录页UI优化(文字宽度/换行/合并按钮样式)
|
||||||
-- 2. 新增"状态"筛选项(正常/已撤销)
|
-- 2. 新增"状态"筛选项(正常/已撤销)
|
||||||
-- 3. 合并记录支持批量撤销/反撤销
|
-- 3. 合并记录支持批量撤销/反撤销
|
||||||
-- 4. 操作菜单底部遮挡修复
|
-- 4. 操作菜单底部遮挡修复
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ $UPGRADE_VERSIONS = [
|
|||||||
'2.3' => __DIR__ . '/sql/upgrades/v2.3.sql',
|
'2.3' => __DIR__ . '/sql/upgrades/v2.3.sql',
|
||||||
'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',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user