v2.5更新
This commit is contained in:
@@ -161,7 +161,8 @@ class ConductModel:
|
|||||||
student_id: int = None,
|
student_id: int = None,
|
||||||
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
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""获取所有记录(班主任/班长专用)"""
|
"""获取所有记录(班主任/班长专用)"""
|
||||||
# 空字符串转为None
|
# 空字符串转为None
|
||||||
@@ -206,6 +207,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 is_revoked is not None:
|
||||||
|
sql += " AND cr.is_revoked = %s"
|
||||||
|
params.append(1 if is_revoked else 0)
|
||||||
|
|
||||||
sql += " ORDER BY cr.created_at DESC LIMIT %s OFFSET %s"
|
sql += " ORDER BY cr.created_at DESC LIMIT %s OFFSET %s"
|
||||||
params.extend([limit, offset])
|
params.extend([limit, offset])
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ class SubjectModel:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(subject_id: int) -> bool:
|
async def delete(subject_id: int) -> bool:
|
||||||
|
"""软删除科目(设置 is_active = 0),如果已禁用也返回成功"""
|
||||||
|
subject = await SubjectModel.get_by_id(subject_id)
|
||||||
|
if not subject:
|
||||||
|
return False
|
||||||
|
if subject.get("is_active") == 0:
|
||||||
|
return True # 已禁用,视为成功
|
||||||
sql = "UPDATE subjects SET is_active = 0 WHERE subject_id = %s"
|
sql = "UPDATE subjects SET is_active = 0 WHERE subject_id = %s"
|
||||||
result = await execute_update(sql, (subject_id,))
|
result = await execute_update(sql, (subject_id,))
|
||||||
return result > 0
|
return result > 0
|
||||||
|
|||||||
@@ -319,7 +319,8 @@ async def get_conduct_history(
|
|||||||
end_date: Optional[str] = None,
|
end_date: Optional[str] = None,
|
||||||
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
|
||||||
):
|
):
|
||||||
"""获取操行分历史记录"""
|
"""获取操行分历史记录"""
|
||||||
try:
|
try:
|
||||||
@@ -335,7 +336,8 @@ async def get_conduct_history(
|
|||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
grouped=grouped,
|
grouped=grouped,
|
||||||
related_type=related_type,
|
related_type=related_type,
|
||||||
reason_prefix=reason_prefix
|
reason_prefix=reason_prefix,
|
||||||
|
is_revoked=is_revoked
|
||||||
)
|
)
|
||||||
return success_response(data=result)
|
return success_response(data=result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -343,6 +345,86 @@ async def get_conduct_history(
|
|||||||
return error_response(message=f"获取历史记录失败: {str(e)}")
|
return error_response(message=f"获取历史记录失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/conduct/batch-revoke")
|
||||||
|
async def batch_revoke_conduct_records(request: Request):
|
||||||
|
"""批量撤销操行分记录"""
|
||||||
|
try:
|
||||||
|
user = await get_current_user(request)
|
||||||
|
if user["user_type"] != "admin":
|
||||||
|
return error_response(message="无权进行此操作", code=403)
|
||||||
|
|
||||||
|
body = await request.json()
|
||||||
|
record_ids = body.get("record_ids", [])
|
||||||
|
if not record_ids or not isinstance(record_ids, list):
|
||||||
|
return error_response(message="请提供要撤销的记录ID列表", code=400)
|
||||||
|
if len(record_ids) > 100:
|
||||||
|
return error_response(message="单次最多撤销100条记录", code=400)
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
fail_count = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for record_id in record_ids:
|
||||||
|
result = await ConductService.revoke_record(
|
||||||
|
record_id=record_id,
|
||||||
|
revoker_id=user["user_id"]
|
||||||
|
)
|
||||||
|
if result["success"]:
|
||||||
|
success_count += 1
|
||||||
|
else:
|
||||||
|
fail_count += 1
|
||||||
|
errors.append({"record_id": record_id, "error": result["message"]})
|
||||||
|
|
||||||
|
return success_response(data={
|
||||||
|
"success_count": success_count,
|
||||||
|
"fail_count": fail_count,
|
||||||
|
"errors": errors
|
||||||
|
}, message=f"批量撤销完成: {success_count}条成功, {fail_count}条失败")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"批量撤销失败: {e}", exc_info=True)
|
||||||
|
return error_response(message=f"批量撤销失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/conduct/batch-restore")
|
||||||
|
async def batch_restore_conduct_records(request: Request):
|
||||||
|
"""批量反撤销操行分记录"""
|
||||||
|
try:
|
||||||
|
user = await get_current_user(request)
|
||||||
|
if user["user_type"] != "admin":
|
||||||
|
return error_response(message="无权进行此操作", code=403)
|
||||||
|
|
||||||
|
body = await request.json()
|
||||||
|
record_ids = body.get("record_ids", [])
|
||||||
|
if not record_ids or not isinstance(record_ids, list):
|
||||||
|
return error_response(message="请提供要反撤销的记录ID列表", code=400)
|
||||||
|
if len(record_ids) > 100:
|
||||||
|
return error_response(message="单次最多反撤销100条记录", code=400)
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
fail_count = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for record_id in record_ids:
|
||||||
|
result = await ConductService.restore_record(
|
||||||
|
record_id=record_id,
|
||||||
|
restorer_id=user["user_id"]
|
||||||
|
)
|
||||||
|
if result["success"]:
|
||||||
|
success_count += 1
|
||||||
|
else:
|
||||||
|
fail_count += 1
|
||||||
|
errors.append({"record_id": record_id, "error": result["message"]})
|
||||||
|
|
||||||
|
return success_response(data={
|
||||||
|
"success_count": success_count,
|
||||||
|
"fail_count": fail_count,
|
||||||
|
"errors": errors
|
||||||
|
}, message=f"批量反撤销完成: {success_count}条成功, {fail_count}条失败")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"批量反撤销失败: {e}", exc_info=True)
|
||||||
|
return error_response(message=f"批量反撤销失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ========== 考勤管理 ==========
|
# ========== 考勤管理 ==========
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ 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',
|
||||||
@@ -35,7 +34,9 @@ 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',
|
||||||
}
|
}
|
||||||
|
|
||||||
# 版本特征标记(按优先级从高到低)
|
# 版本特征标记(按优先级从高到低)
|
||||||
|
|||||||
@@ -213,7 +213,8 @@ class ConductService:
|
|||||||
end_date: Optional[str] = None,
|
end_date: Optional[str] = None,
|
||||||
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
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""获取历史记录"""
|
"""获取历史记录"""
|
||||||
# 空字符串转为None
|
# 空字符串转为None
|
||||||
@@ -251,7 +252,8 @@ class ConductService:
|
|||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
||||||
# 获取总数
|
# 获取总数
|
||||||
@@ -273,6 +275,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 is_revoked is not None:
|
||||||
|
count_conditions.append("cr.is_revoked = %s")
|
||||||
|
count_params.append(1 if is_revoked else 0)
|
||||||
count_where = " AND ".join(count_conditions)
|
count_where = " AND ".join(count_conditions)
|
||||||
count_sql = f"""
|
count_sql = f"""
|
||||||
SELECT COUNT(*) as total FROM conduct_records cr
|
SELECT COUNT(*) as total FROM conduct_records cr
|
||||||
|
|||||||
@@ -54,6 +54,16 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<option value="志愿">志愿</option>
|
<option value="志愿">志愿</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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;">
|
<div class="filter-group" style="min-width:auto;">
|
||||||
<label> </label>
|
<label> </label>
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ body {
|
|||||||
/* ========== 表格 ========== */
|
/* ========== 表格 ========== */
|
||||||
.table-wrapper {
|
.table-wrapper {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
@@ -853,9 +854,9 @@ tr:hover {
|
|||||||
.action-dropdown-menu {
|
.action-dropdown-menu {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
bottom: 100%;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin-top: 4px;
|
margin-bottom: 4px;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||||
@@ -926,3 +927,50 @@ tr:hover {
|
|||||||
.tag-danger { background: #ffebee; color: #c62828; }
|
.tag-danger { background: #ffebee; color: #c62828; }
|
||||||
.tag-warning { background: #fff3e0; color: #e65100; }
|
.tag-warning { background: #fff3e0; color: #e65100; }
|
||||||
.tag-info { background: #e3f2fd; color: #1565c0; }
|
.tag-info { background: #e3f2fd; color: #1565c0; }
|
||||||
|
|
||||||
|
/* ========== 历史记录页优化 ========== */
|
||||||
|
/* 时间列:确保分两行显示(日期+时间) */
|
||||||
|
.history-time {
|
||||||
|
white-space: pre-line;
|
||||||
|
min-width: 80px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 原因列:每行最少7个字,自动换行 */
|
||||||
|
.history-reason {
|
||||||
|
min-width: 7em;
|
||||||
|
max-width: 200px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 学生名列:允许换行 */
|
||||||
|
.history-students {
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
min-width: 60px;
|
||||||
|
max-width: 120px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 合并记录按钮样式 */
|
||||||
|
.btn-outline-danger {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-danger);
|
||||||
|
border: 1px solid var(--color-danger);
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-danger:hover {
|
||||||
|
background: var(--color-danger-light);
|
||||||
|
color: var(--color-danger-dark);
|
||||||
|
border-color: var(--color-danger-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ async function loadHistory(page = 1) {
|
|||||||
const studentId = document.getElementById('historyStudentId').value;
|
const studentId = document.getElementById('historyStudentId').value;
|
||||||
const reasonFilter = document.getElementById('historyReasonFilter').value;
|
const reasonFilter = document.getElementById('historyReasonFilter').value;
|
||||||
const isGrouped = document.getElementById('historyGrouped').checked;
|
const isGrouped = document.getElementById('historyGrouped').checked;
|
||||||
|
const statusFilter = document.getElementById('historyStatusFilter')?.value;
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
page, page_size: 20,
|
page, page_size: 20,
|
||||||
@@ -42,6 +43,7 @@ async function loadHistory(page = 1) {
|
|||||||
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);
|
||||||
|
|
||||||
const res = await apiGet('/api/admin/conduct/history', params);
|
const res = await apiGet('/api/admin/conduct/history', params);
|
||||||
|
|
||||||
@@ -50,6 +52,9 @@ async function loadHistory(page = 1) {
|
|||||||
let headHtml = '';
|
let 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 === '班长') {
|
||||||
|
headHtml += '<th>操作</th>';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
headHtml = '<th>时间</th><th>学生</th><th>分数变动</th><th>原因</th><th' + nowrapStyle + '>操作人</th>';
|
headHtml = '<th>时间</th><th>学生</th><th>分数变动</th><th>原因</th><th' + nowrapStyle + '>操作人</th>';
|
||||||
if (role === '班主任' || role === '班长' || role === '考勤委员') {
|
if (role === '班主任' || role === '班长' || role === '考勤委员') {
|
||||||
@@ -63,46 +68,56 @@ async function loadHistory(page = 1) {
|
|||||||
res.data.records.forEach(record => {
|
res.data.records.forEach(record => {
|
||||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||||
const names = record.student_names || '';
|
const names = record.student_names || '';
|
||||||
html += `<tr>
|
const allRevoked = record.all_revoked;
|
||||||
<td>${formatDateTime(record.created_at)}</td>
|
const revokedStyle = allRevoked ? ' style="opacity:0.5; text-decoration:line-through;"' : '';
|
||||||
<td class="preserve-newlines">${escapeHtml(record.reason)}</td>
|
html += `<tr${revokedStyle}>
|
||||||
|
<td class="history-time">${formatDateTime(record.created_at)}</td>
|
||||||
|
<td class="preserve-newlines history-reason">${escapeHtml(record.reason)}</td>
|
||||||
<td class="${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}×${record.student_count}</td>
|
<td 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 style="white-space: nowrap;">${escapeHtml(names)}</td>
|
<td class="history-students">${escapeHtml(names)}</td>`;
|
||||||
</tr>`;
|
if (role === '班主任' || role === '班长') {
|
||||||
|
if (allRevoked) {
|
||||||
|
html += `<td><span class="text-muted">已撤销</span></td>`;
|
||||||
|
} else {
|
||||||
|
html += `<td><button class="btn btn-sm btn-outline-danger" onclick="batchRevokeGrouped('${escapeHtml(record.reason)}', ${record.points_change}, '${escapeHtml(record.recorder_name || '')}', '${formatDateTime(record.created_at)}')">批量撤销</button></td>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html += `</tr>`;
|
||||||
});
|
});
|
||||||
if (res.data.records.length === 0) {
|
if (res.data.records.length === 0) {
|
||||||
html = '<tr><td colspan="5" style="text-align:center;">暂无记录</td></tr>';
|
const colSpan = (role === '班主任' || role === '班长') ? 6 : 5;
|
||||||
|
html = '<tr><td colspan="' + colSpan + '" style="text-align:center;">暂无记录</td></tr>';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.data.records.forEach(record => {
|
res.data.records.forEach(record => {
|
||||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||||
const revokedStyle = record.is_revoked == 1 ? ' style="opacity:0.5; text-decoration:line-through;"' : '';
|
const revokedStyle = record.is_revoked == 1 ? ' style="opacity:0.5; text-decoration:line-through;"' : '';
|
||||||
html += `<tr${revokedStyle}>
|
html += `<tr${revokedStyle}>
|
||||||
<td>${formatDateTime(record.created_at)}</td>
|
<td class="history-time">${formatDateTime(record.created_at)}</td>
|
||||||
<td>${escapeHtml(record.student_name)}</td>
|
<td class="history-students">${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">${escapeHtml(record.reason)}</td>
|
<td class="preserve-newlines 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)} 撤销` : '已撤销';
|
const revokerInfo = record.revoker_name ? `由 ${escapeHtml(record.revoker_name)} 撤销` : '已撤销';
|
||||||
html += `<td><span class="text-muted" style="margin-right:4px;">${revokerInfo}</span><button class="btn btn-sm btn-secondary" 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-danger" 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)} 撤销` : '已撤销';
|
const 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-danger" 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-danger" 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>`;
|
||||||
}
|
}
|
||||||
@@ -177,6 +192,53 @@ async function exportHistoryRecords() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量撤销合并记录(按条件查找并撤销)
|
||||||
|
async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt) {
|
||||||
|
if (!confirm(`确定要撤销所有"${reason}"(${pointsChange > 0 ? '+' : ''}${pointsChange}分)的记录吗?`)) return;
|
||||||
|
|
||||||
|
showToast('正在批量撤销...', 'info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 先查询匹配的记录
|
||||||
|
const params = {
|
||||||
|
page: 1, page_size: 1000,
|
||||||
|
start_date: document.getElementById('historyStartDate').value,
|
||||||
|
end_date: document.getElementById('historyEndDate').value,
|
||||||
|
reason_prefix: reason.substring(0, 4),
|
||||||
|
grouped: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const 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 => {
|
||||||
|
if (r.reason === reason && r.points_change === pointsChange && r.is_revoked == 0) {
|
||||||
|
matchedIds.push(r.record_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchedIds.length === 0) {
|
||||||
|
showToast('没有找到可撤销的记录', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const revokeRes = await apiPost('/api/admin/conduct/batch-revoke', { record_ids: matchedIds });
|
||||||
|
if (revokeRes && revokeRes.success) {
|
||||||
|
showToast(`批量撤销完成: ${revokeRes.data.success_count}条成功`);
|
||||||
|
loadHistory(currentHistoryPage);
|
||||||
|
} else {
|
||||||
|
showToast(revokeRes?.message || '批量撤销失败', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showToast('批量撤销失败: ' + err.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loadStudentsForSelect().then(() => {
|
loadStudentsForSelect().then(() => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const preStudentId = urlParams.get('student_id');
|
const preStudentId = urlParams.get('student_id');
|
||||||
@@ -191,5 +253,6 @@ loadStudentsForSelect().then(() => {
|
|||||||
window.loadHistory = loadHistory;
|
window.loadHistory = loadHistory;
|
||||||
window.loadStudentsForSelect = loadStudentsForSelect;
|
window.loadStudentsForSelect = loadStudentsForSelect;
|
||||||
window.exportHistoryRecords = exportHistoryRecords;
|
window.exportHistoryRecords = exportHistoryRecords;
|
||||||
|
window.batchRevokeGrouped = batchRevokeGrouped;
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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.4')
|
VALUES ('db_version', '2.5')
|
||||||
ON DUPLICATE KEY UPDATE `setting_value` = '2.4';
|
ON DUPLICATE KEY UPDATE `setting_value` = '2.5';
|
||||||
|
|
||||||
-- 控制台输出初始化结果(含版本号)
|
-- 控制台输出初始化结果(含版本号)
|
||||||
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.5.sql
Normal file
12
sql/upgrades/v2.5.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
-- ===========================================
|
||||||
|
-- 班级操行分管理系统 - v2.3 → v2.5 升级脚本
|
||||||
|
-- 字符集: utf8mb4
|
||||||
|
--
|
||||||
|
-- 说明: v2.5 为功能增强版本,无数据库 schema 变更。
|
||||||
|
-- 主要变更:
|
||||||
|
-- 1. 历史记录页优化(文字宽度/换行/合并按钮样式)
|
||||||
|
-- 2. 新增"状态"筛选项(正常/已撤销)
|
||||||
|
-- 3. 合并记录支持批量撤销/反撤销
|
||||||
|
-- 4. 操作菜单底部遮挡修复
|
||||||
|
-- 5. 删除科目报错修复
|
||||||
|
-- ===========================================
|
||||||
@@ -26,6 +26,8 @@ $UPGRADE_VERSIONS = [
|
|||||||
'2.1' => __DIR__ . '/sql/upgrades/v2.1.sql',
|
'2.1' => __DIR__ . '/sql/upgrades/v2.1.sql',
|
||||||
'2.2' => __DIR__ . '/sql/upgrades/v2.2.sql',
|
'2.2' => __DIR__ . '/sql/upgrades/v2.2.sql',
|
||||||
'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.5' => __DIR__ . '/sql/upgrades/v2.5.sql',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user