v2.9update
This commit is contained in:
@@ -26,7 +26,7 @@
|
|||||||
- 作业管理:发布作业、查看提交情况
|
- 作业管理:发布作业、查看提交情况
|
||||||
- 考勤管理:按时段(早上 7:15/中午 14:00/晚修 19:30)记录考勤、自定义考勤扣分值
|
- 考勤管理:按时段(早上 7:15/中午 14:00/晚修 19:30)记录考勤、自定义考勤扣分值
|
||||||
- 科目管理:动态增删学科
|
- 科目管理:动态增删学科
|
||||||
- 管理员管理:添加/编辑/删除/重置密码班长/科代表/考勤委员/劳动委员/志愿委员
|
- 管理员管理:添加/编辑/禁用-启用/重置密码班长/科代表/考勤委员/劳动委员/志愿委员
|
||||||
- 学期管理:创建/编辑/删除学期、激活学期、归档学期(含考勤/作业统计快照)、关联历史记录、归档后可选重置分数
|
- 学期管理:创建/编辑/删除学期、激活学期、归档学期(含考勤/作业统计快照)、关联历史记录、归档后可选重置分数
|
||||||
- 排行榜百分比筛选:在排行榜上方输入百分比,筛选显示前N%的学生(抹零法)
|
- 排行榜百分比筛选:在排行榜上方输入百分比,筛选显示前N%的学生(抹零法)
|
||||||
- 数据导出:导出历史记录、导出德育分记录(含加分/减分历史)
|
- 数据导出:导出历史记录、导出德育分记录(含加分/减分历史)
|
||||||
@@ -279,6 +279,8 @@ classmanager/
|
|||||||
| v2.5.1 | 2026.5.29 | 筛选学生时自动取消合并记录、合并记录选项样式修复(竖排显示)、历史记录筛选改为折叠式、科目管理调用修复、全局escapeHtml XSS转义修复 |
|
| v2.5.1 | 2026.5.29 | 筛选学生时自动取消合并记录、合并记录选项样式修复(竖排显示)、历史记录筛选改为折叠式、科目管理调用修复、全局escapeHtml XSS转义修复 |
|
||||||
| v2.6 | 2026.5.29 | 历史记录筛选面板重构(学生筛选移入面板、合并按钮始终可见)、新增科目下拉筛选、新增原因关键词搜索、科目删除后默认仅显示启用科目、筛选面板展开修复 |
|
| v2.6 | 2026.5.29 | 历史记录筛选面板重构(学生筛选移入面板、合并按钮始终可见)、新增科目下拉筛选、新增原因关键词搜索、科目删除后默认仅显示启用科目、筛选面板展开修复 |
|
||||||
| v2.7 | 2026.5.29 | 操作菜单定位修复、历史记录显示修复、筛选面板修复、科目删除修复、学期周数列修复、escapeHtml 转义修复 |
|
| v2.7 | 2026.5.29 | 操作菜单定位修复、历史记录显示修复、筛选面板修复、科目删除修复、学期周数列修复、escapeHtml 转义修复 |
|
||||||
|
| v2.8 | 2026.6.2 | 管理员软删除改为禁用/启用切换(保留admin_roles记录)、科目管理面板显示全部科目(禁用/启用均可见)、添加管理员支持重新激活已禁用账号、升级系统补全v2.8版本号 |
|
||||||
|
| v2.9 | 2026.6.8 | 历史记录表头优化:合并/非合并视图统一新增"类型"列(手动/作业/考勤),列顺序统一为"类型、分值、原因、学生、操作人、时间" |
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ class AdminRoleModel:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_all() -> List[Dict[str, Any]]:
|
async def get_all() -> List[Dict[str, Any]]:
|
||||||
sql = """
|
sql = """
|
||||||
SELECT ar.*, u.real_name, u.username, s.subject_name
|
SELECT ar.*, u.real_name, u.username, u.status, s.subject_name
|
||||||
FROM admin_roles ar
|
FROM admin_roles ar
|
||||||
JOIN users u ON ar.user_id = u.user_id AND u.status = 1
|
JOIN users u ON ar.user_id = u.user_id
|
||||||
LEFT JOIN subjects s ON ar.subject_id = s.subject_id
|
LEFT JOIN subjects s ON ar.subject_id = s.subject_id
|
||||||
ORDER BY ar.role_type
|
ORDER BY ar.role_type
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ class ConductModel:
|
|||||||
cr.points_change,
|
cr.points_change,
|
||||||
cr.reason,
|
cr.reason,
|
||||||
cr.recorder_name,
|
cr.recorder_name,
|
||||||
|
MAX(cr.related_type) as related_type,
|
||||||
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,
|
||||||
|
|||||||
@@ -93,6 +93,17 @@ class UserModel:
|
|||||||
"""
|
"""
|
||||||
await execute_update(sql, (ip, user_id))
|
await execute_update(sql, (ip, user_id))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_by_username_any(username: str) -> dict:
|
||||||
|
"""根据用户名获取用户(含已禁用)"""
|
||||||
|
sql = """
|
||||||
|
SELECT user_id, username, password_hash, real_name, user_type,
|
||||||
|
student_id, status, need_change_password, last_login_time, last_login_ip
|
||||||
|
FROM users
|
||||||
|
WHERE username = %s
|
||||||
|
"""
|
||||||
|
return await execute_one(sql, (username,))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def check_username_exists(username: str) -> bool:
|
async def check_username_exists(username: str) -> bool:
|
||||||
"""检查用户名是否存在"""
|
"""检查用户名是否存在"""
|
||||||
|
|||||||
@@ -500,11 +500,13 @@ async def add_admin(request: Request, req: AddAdminRequest):
|
|||||||
operator_id=user["user_id"]
|
operator_id=user["user_id"]
|
||||||
)
|
)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
|
is_reactivation = result.get("password") is None
|
||||||
await LogService.write_operation_log(
|
await LogService.write_operation_log(
|
||||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||||
operator_role="班主任", operation_type="add_admin",
|
operator_role="班主任",
|
||||||
|
operation_type="reactivate_admin" if is_reactivation else "add_admin",
|
||||||
target_type="admin",
|
target_type="admin",
|
||||||
details=f"新增管理员: {req.real_name}({req.username}), 角色: {req.role_type}",
|
details=f"{'重新激活' if is_reactivation else '新增'}管理员: {req.real_name}({req.username}), 角色: {req.role_type}",
|
||||||
ip=request.client.host
|
ip=request.client.host
|
||||||
)
|
)
|
||||||
return success_response(data=result, message="管理员添加成功")
|
return success_response(data=result, message="管理员添加成功")
|
||||||
@@ -563,36 +565,43 @@ async def update_admin(request: Request, user_id: int, req: UpdateAdminRequest):
|
|||||||
return error_response(message="更新失败或管理员不存在")
|
return error_response(message="更新失败或管理员不存在")
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/delete/{user_id}")
|
@router.put("/toggle-status/{user_id}")
|
||||||
async def delete_admin(request: Request, user_id: int):
|
async def toggle_admin_status(request: Request, user_id: int):
|
||||||
"""删除管理员(班主任)"""
|
"""启用/禁用管理员(班主任),保留角色记录"""
|
||||||
user = await get_current_user(request)
|
user = await get_current_user(request)
|
||||||
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
||||||
if not is_teacher:
|
if not is_teacher:
|
||||||
return error_response(message="仅班主任可删除管理员", code=403)
|
return error_response(message="仅班主任可操作", code=403)
|
||||||
|
|
||||||
# 防止删除自己
|
# 防止禁用自己
|
||||||
if user_id == user["user_id"]:
|
if user_id == user["user_id"]:
|
||||||
return error_response(message="不能删除当前登录的管理员", code=400)
|
return error_response(message="不能禁用当前登录的管理员", code=400)
|
||||||
|
|
||||||
from models.admin_role import AdminRoleModel
|
from models.admin_role import AdminRoleModel
|
||||||
from models.user import UserModel
|
from models.user import UserModel
|
||||||
|
|
||||||
# 先删除角色记录
|
target_user = await UserModel.get_by_user_id(user_id)
|
||||||
role_deleted = await AdminRoleModel.delete(user_id)
|
if not target_user or target_user.get("user_type") != "admin":
|
||||||
if role_deleted:
|
return error_response(message="管理员不存在", code=404)
|
||||||
# 再删除用户账号(软删除,将状态设为禁用)
|
|
||||||
await UserModel.update_status(user_id, 0)
|
# 检查是否有管理员角色
|
||||||
await LogService.write_operation_log(
|
admin_role = await AdminRoleModel.get_by_user_id(user_id)
|
||||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
if not admin_role:
|
||||||
operator_role="班主任", operation_type="delete_admin",
|
return error_response(message="该用户无管理员角色", code=400)
|
||||||
target_type="admin", target_id=user_id,
|
|
||||||
details=f"删除管理员: ID={user_id}",
|
new_status = 0 if target_user["status"] == 1 else 1
|
||||||
ip=request.client.host
|
action_text = "禁用" if new_status == 0 else "启用"
|
||||||
)
|
|
||||||
return success_response(message="管理员删除成功")
|
await UserModel.update_status(user_id, new_status)
|
||||||
else:
|
|
||||||
return error_response(message="删除失败或管理员不存在")
|
await LogService.write_operation_log(
|
||||||
|
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||||
|
operator_role="班主任", operation_type="toggle_admin_status",
|
||||||
|
target_type="admin", target_id=user_id,
|
||||||
|
details=f"{action_text}管理员: {target_user['real_name']}({target_user['username']})",
|
||||||
|
ip=request.client.host
|
||||||
|
)
|
||||||
|
return success_response(message=f"管理员已{action_text}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/reset-password/{user_id}")
|
@router.post("/reset-password/{user_id}")
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ ALL_VERSIONS = {
|
|||||||
'2.6': 'v2.6.sql',
|
'2.6': 'v2.6.sql',
|
||||||
'2.7': 'v2.7.sql',
|
'2.7': 'v2.7.sql',
|
||||||
'2.8': 'v2.8.sql',
|
'2.8': 'v2.8.sql',
|
||||||
|
'2.9': 'v2.9.sql',
|
||||||
}
|
}
|
||||||
# 版本特征标记(按优先级从高到低)
|
# 版本特征标记(按优先级从高到低)
|
||||||
VERSION_MARKERS = [
|
VERSION_MARKERS = [
|
||||||
|
|||||||
@@ -245,10 +245,30 @@ class AdminService:
|
|||||||
role_type: str,
|
role_type: str,
|
||||||
operator_id: int
|
operator_id: int
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""添加管理员"""
|
"""添加管理员(支持重新激活已删除的管理员)"""
|
||||||
existing = await UserModel.get_by_username(username)
|
# 检查用户名是否存在(含已禁用用户,因 username 有 UNIQUE 约束)
|
||||||
|
existing = await UserModel.get_by_username_any(username)
|
||||||
if existing:
|
if existing:
|
||||||
return {"success": False, "message": "用户名已存在"}
|
if existing.get('status') == 1:
|
||||||
|
return {"success": False, "message": "用户名已存在"}
|
||||||
|
# 用户已被软删除(status=0),重新激活
|
||||||
|
await UserModel.update_status(existing['user_id'], 1)
|
||||||
|
await UserModel.update_real_name(existing['user_id'], real_name)
|
||||||
|
user_id = existing['user_id']
|
||||||
|
# 检查是否已有管理员角色
|
||||||
|
existing_role = await AdminRoleModel.get_by_user_id(user_id)
|
||||||
|
if existing_role:
|
||||||
|
await AdminRoleModel.update_role(user_id, role_type)
|
||||||
|
else:
|
||||||
|
await AdminRoleModel.create(user_id=user_id, role_type=role_type, subject_id=None)
|
||||||
|
logger.info(f"用户[{operator_id}] 重新激活管理员: {username} ({role_type})")
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"user_id": user_id,
|
||||||
|
"username": username,
|
||||||
|
"password": None, # 重新激活不返回密码
|
||||||
|
"role_type": role_type
|
||||||
|
}
|
||||||
|
|
||||||
if not password:
|
if not password:
|
||||||
password = security.generate_random_password()
|
password = security.generate_random_password()
|
||||||
@@ -274,7 +294,6 @@ class AdminService:
|
|||||||
"password": password,
|
"password": password,
|
||||||
"role_type": role_type
|
"role_type": role_type
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_admins() -> Dict[str, Any]:
|
async def get_admins() -> Dict[str, Any]:
|
||||||
"""获取管理员列表"""
|
"""获取管理员列表"""
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>用户名</th><th>姓名</th><th>角色</th><th>操作</th></tr>
|
<tr><th>用户名</th><th>姓名</th><th>角色</th><th>状态</th><th>操作</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="adminList"></tbody>
|
<tbody id="adminList"></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -93,11 +93,12 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr id="historyTableHead">
|
<tr id="historyTableHead">
|
||||||
<th>时间</th>
|
<th>类型</th>
|
||||||
<th>学生</th>
|
<th>分值</th>
|
||||||
<th>分数变动</th>
|
|
||||||
<th>原因</th>
|
<th>原因</th>
|
||||||
|
<th>学生</th>
|
||||||
<th style="white-space: nowrap; min-width: 80px;">操作人</th>
|
<th style="white-space: nowrap; min-width: 80px;">操作人</th>
|
||||||
|
<th>时间</th>
|
||||||
<?php if ($role === '班主任' || $role === '班长' || $role === '考勤委员'): ?>
|
<?php if ($role === '班主任' || $role === '班长' || $role === '考勤委员'): ?>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -18,10 +18,14 @@ async function loadAdmins() {
|
|||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
let html = '';
|
let html = '';
|
||||||
res.data.admins.forEach(admin => {
|
res.data.admins.forEach(admin => {
|
||||||
|
const isActive = admin.status === 1;
|
||||||
|
const statusClass = isActive ? 'subject-status-active' : 'subject-status-inactive';
|
||||||
|
const statusText = isActive ? '启用' : '禁用';
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${escapeHtml(admin.username)}</td>
|
<td>${escapeHtml(admin.username)}</td>
|
||||||
<td>${escapeHtml(admin.real_name)}</td>
|
<td>${escapeHtml(admin.real_name)}</td>
|
||||||
<td>${escapeHtml(admin.role_type)}</td>
|
<td>${escapeHtml(admin.role_type)}</td>
|
||||||
|
<td><span class="subject-status ${statusClass}">${statusText}</span></td>
|
||||||
<td>
|
<td>
|
||||||
<div class="action-dropdown">
|
<div class="action-dropdown">
|
||||||
<button class="btn btn-sm action-dropdown-toggle" onclick="toggleActionDropdown(this)">操作 ▼</button>
|
<button class="btn btn-sm action-dropdown-toggle" onclick="toggleActionDropdown(this)">操作 ▼</button>
|
||||||
@@ -29,14 +33,14 @@ async function loadAdmins() {
|
|||||||
<a onclick="showEditAdminModal(${admin.user_id}, '${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}', '${escapeHtml(admin.role_type)}')">编辑</a>
|
<a onclick="showEditAdminModal(${admin.user_id}, '${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}', '${escapeHtml(admin.role_type)}')">编辑</a>
|
||||||
<a onclick="resetAdminPassword(${admin.user_id}, '${escapeHtml(admin.real_name)}')">重置密码</a>
|
<a onclick="resetAdminPassword(${admin.user_id}, '${escapeHtml(admin.real_name)}')">重置密码</a>
|
||||||
<a onclick="unlockUser('${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}')">解锁</a>
|
<a onclick="unlockUser('${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}')">解锁</a>
|
||||||
<a class="danger" onclick="deleteAdmin(${admin.user_id}, '${escapeHtml(admin.real_name)}')">删除</a>
|
<a class="${isActive ? 'danger' : ''}" onclick="toggleAdminStatus(${admin.user_id}, '${escapeHtml(admin.real_name)}', ${isActive ? 1 : 0})">${isActive ? '禁用' : '启用'}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
if (res.data.admins.length === 0) {
|
if (res.data.admins.length === 0) {
|
||||||
html = '<tr><td colspan="4" style="text-align:center;">暂无管理员</td></tr>';
|
html = '<tr><td colspan="5" style="text-align:center;">暂无管理员</td></tr>';
|
||||||
}
|
}
|
||||||
document.getElementById('adminList').innerHTML = html;
|
document.getElementById('adminList').innerHTML = html;
|
||||||
}
|
}
|
||||||
@@ -74,17 +78,21 @@ async function submitEditAdmin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAdmin(userId, realName) {
|
async function toggleAdminStatus(userId, realName, currentStatus) {
|
||||||
if (!confirm(`确定要删除管理员 "${realName}" 吗?此操作不可恢复。`)) {
|
const action = currentStatus === 1 ? '禁用' : '启用';
|
||||||
|
const warnMsg = currentStatus === 1
|
||||||
|
? `禁用后该管理员将无法登录,确定要禁用 "${realName}" 吗?`
|
||||||
|
: `确定要重新启用管理员 "${realName}" 吗?`;
|
||||||
|
if (!confirm(warnMsg)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await apiDelete(`/api/admin/delete/${userId}`);
|
const res = await apiPut(`/api/admin/toggle-status/${userId}`);
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
showToast('管理员删除成功');
|
showToast(res.message || `管理员已${action}`);
|
||||||
loadAdmins();
|
loadAdmins();
|
||||||
} else {
|
} else {
|
||||||
showToast(res?.message || '删除失败', 'error');
|
showToast(res?.message || '操作失败', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +146,7 @@ loadAdmins();
|
|||||||
window.loadAdmins = loadAdmins;
|
window.loadAdmins = loadAdmins;
|
||||||
window.showEditAdminModal = showEditAdminModal;
|
window.showEditAdminModal = showEditAdminModal;
|
||||||
window.submitEditAdmin = submitEditAdmin;
|
window.submitEditAdmin = submitEditAdmin;
|
||||||
window.deleteAdmin = deleteAdmin;
|
window.toggleAdminStatus = toggleAdminStatus;
|
||||||
window.resetAdminPassword = resetAdminPassword;
|
window.resetAdminPassword = resetAdminPassword;
|
||||||
window.unlockUser = unlockUser;
|
window.unlockUser = unlockUser;
|
||||||
window.submitResetPassword = submitResetPassword;
|
window.submitResetPassword = submitResetPassword;
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ function escapeHtml(str) {
|
|||||||
return el.innerHTML;
|
return el.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function typeMap(relatedType) {
|
||||||
|
var map = { manual: '手动', homework: '作业', attendance: '考勤' };
|
||||||
|
return map[relatedType] || '手动';
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -117,12 +122,12 @@ async function loadHistory(page) {
|
|||||||
var nowrapStyle = ' style="white-space:nowrap;min-width:80px;"';
|
var nowrapStyle = ' style="white-space:nowrap;min-width:80px;"';
|
||||||
var 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>学生名单</th><th' + nowrapStyle + '>操作人</th><th>时间</th>';
|
||||||
if (role === '班主任' || role === '班长') {
|
if (role === '班主任' || role === '班长') {
|
||||||
headHtml += '<th>操作</th>';
|
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><th>时间</th>';
|
||||||
if (role === '班主任' || role === '班长' || role === '考勤委员') {
|
if (role === '班主任' || role === '班长' || role === '考勤委员') {
|
||||||
headHtml += '<th>操作</th>';
|
headHtml += '<th>操作</th>';
|
||||||
}
|
}
|
||||||
@@ -133,15 +138,17 @@ async function loadHistory(page) {
|
|||||||
if (isGrouped) {
|
if (isGrouped) {
|
||||||
res.data.records.forEach(function(record) {
|
res.data.records.forEach(function(record) {
|
||||||
var pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
var pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||||
|
var typeLabel = typeMap(record.related_type);
|
||||||
var names = record.student_names || '';
|
var names = record.student_names || '';
|
||||||
var allRevoked = record.all_revoked;
|
var allRevoked = record.all_revoked;
|
||||||
var 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>' + typeLabel + '</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 class="history-reason">' + escapeHtml(record.reason) + '</td>' +
|
||||||
|
'<td class="history-students">' + escapeHtml(names) + '</td>' +
|
||||||
'<td>' + escapeHtml(record.recorder_name || '') + '</td>' +
|
'<td>' + escapeHtml(record.recorder_name || '') + '</td>' +
|
||||||
'<td class="history-students">' + escapeHtml(names) + '</td>';
|
'<td class="history-time">' + formatDateTime(record.created_at) + '</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>';
|
||||||
@@ -152,19 +159,21 @@ async function loadHistory(page) {
|
|||||||
html += '</tr>';
|
html += '</tr>';
|
||||||
});
|
});
|
||||||
if (res.data.records.length === 0) {
|
if (res.data.records.length === 0) {
|
||||||
var colSpan = (role === '班主任' || role === '班长') ? 6 : 5;
|
var colSpan = (role === '班主任' || role === '班长') ? 7 : 6;
|
||||||
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(function(record) {
|
res.data.records.forEach(function(record) {
|
||||||
var pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
var pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||||
|
var typeLabel = typeMap(record.related_type);
|
||||||
var 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>' + typeLabel + '</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="history-reason">' + escapeHtml(record.reason) + '</td>' +
|
'<td class="history-reason">' + escapeHtml(record.reason) + '</td>' +
|
||||||
'<td>' + escapeHtml(record.recorder_name) + '</td>';
|
'<td>' + escapeHtml(record.student_name) + '</td>' +
|
||||||
|
'<td>' + escapeHtml(record.recorder_name) + '</td>' +
|
||||||
|
'<td class="history-time">' + formatDateTime(record.created_at) + '</td>';
|
||||||
if (role === '班主任') {
|
if (role === '班主任') {
|
||||||
if (record.is_revoked == 1) {
|
if (record.is_revoked == 1) {
|
||||||
var revokerInfo = record.revoker_name ? '由 ' + escapeHtml(record.revoker_name) + ' 撤销' : '已撤销';
|
var revokerInfo = record.revoker_name ? '由 ' + escapeHtml(record.revoker_name) + ' 撤销' : '已撤销';
|
||||||
@@ -192,7 +201,7 @@ async function loadHistory(page) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.data.records.length === 0) {
|
if (res.data.records.length === 0) {
|
||||||
var colSpan = (role === '班主任' || role === '班长' || role === '考勤委员') ? 6 : 5;
|
var colSpan = (role === '班主任' || role === '班长' || role === '考勤委员') ? 7 : 6;
|
||||||
html = '<tr><td colspan="' + colSpan + '" style="text-align:center;">暂无记录</td></tr>';
|
html = '<tr><td colspan="' + colSpan + '" style="text-align:center;">暂无记录</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ document.getElementById('pointsChange').setAttribute('max', hwMaxPoints);
|
|||||||
async function loadSubjectsForHomework() {
|
async function loadSubjectsForHomework() {
|
||||||
const subjectSelect = document.getElementById('hwSubjectSelect');
|
const subjectSelect = document.getElementById('hwSubjectSelect');
|
||||||
if (!subjectSelect) return;
|
if (!subjectSelect) return;
|
||||||
const res = await apiGet('/api/subject/list');
|
// 作业下拉只显示已启用的科目
|
||||||
|
const res = await apiGet('/api/subject/list', { is_active: true });
|
||||||
if (res && res.success && res.data && res.data.subjects) {
|
if (res && res.success && res.data && res.data.subjects) {
|
||||||
let html = '<option value="">不选择科目</option>';
|
let html = '<option value="">不选择科目</option>';
|
||||||
res.data.subjects.forEach(s => {
|
res.data.subjects.forEach(s => {
|
||||||
@@ -126,7 +127,7 @@ function toggleSubjectPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadSubjectList() {
|
async function loadSubjectList() {
|
||||||
const res = await apiGet('/api/subject/list', { is_active: true });
|
const res = await apiGet('/api/subject/list');
|
||||||
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 || [];
|
||||||
|
|||||||
@@ -38,8 +38,12 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
let msg = `管理员 ${res.data.username} 添加成功`;
|
let msg;
|
||||||
if (res.data.password) msg += `,密码: ${res.data.password}`;
|
if (res.data.password) {
|
||||||
|
msg = `管理员 ${res.data.username} 添加成功,密码: ${res.data.password}`;
|
||||||
|
} else {
|
||||||
|
msg = `管理员 ${res.data.username} 已重新激活(原密码不变)`;
|
||||||
|
}
|
||||||
showToast(msg);
|
showToast(msg);
|
||||||
closeModal('addAdminModal');
|
closeModal('addAdminModal');
|
||||||
loadAdmins();
|
loadAdmins();
|
||||||
|
|||||||
@@ -19,15 +19,17 @@ async function loadHomework() {
|
|||||||
res.data.homework.forEach(record => {
|
res.data.homework.forEach(record => {
|
||||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||||
const pointsColor = record.points_change > 0 ? '#38a169' : '#e53e3e';
|
const pointsColor = record.points_change > 0 ? '#38a169' : '#e53e3e';
|
||||||
|
const typeLabel = { manual: '手动', homework: '作业', attendance: '考勤' }[record.related_type] || '作业';
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${formatDateTime(record.created_at)}</td>
|
<td>${typeLabel}</td>
|
||||||
<td style="color: ${pointsColor}; font-weight: bold;">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
<td style="color: ${pointsColor}; font-weight: bold;">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
||||||
<td>${escapeHtml(record.reason)}</td>
|
<td>${escapeHtml(record.reason)}</td>
|
||||||
<td>${escapeHtml(record.recorder_name || '-')}</td>
|
<td>${escapeHtml(record.recorder_name || '-')}</td>
|
||||||
|
<td>${formatDateTime(record.created_at)}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
if (res.data.homework.length === 0) {
|
if (res.data.homework.length === 0) {
|
||||||
html = '<tr><td colspan="4" style="text-align:center; padding: 40px; color: #999;">📝 暂无作业扣分记录</td></tr>';
|
html = '<tr><td colspan="5" style="text-align:center; padding: 40px; color: #999;">📝 暂无作业扣分记录</td></tr>';
|
||||||
}
|
}
|
||||||
document.getElementById('homeworkList').innerHTML = html;
|
document.getElementById('homeworkList').innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,14 +34,15 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>日期</th>
|
<th>类型</th>
|
||||||
<th>原因</th>
|
|
||||||
<th>分值</th>
|
<th>分值</th>
|
||||||
|
<th>原因</th>
|
||||||
<th>记录人</th>
|
<th>记录人</th>
|
||||||
|
<th>日期</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="historyList">
|
<tbody id="historyList">
|
||||||
<tr><td colspan="4" style="text-align:center;">加载中...</td></tr>
|
<tr><td colspan="5" style="text-align:center;">加载中...</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,18 +74,20 @@ const pageSize = 20;
|
|||||||
async function loadHistory(page) {
|
async function loadHistory(page) {
|
||||||
const res = await apiGet('/api/parent/child/history', { page: page, page_size: pageSize });
|
const res = await apiGet('/api/parent/child/history', { page: page, page_size: pageSize });
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
let html = '';
|
|
||||||
if (res.data.records.length === 0) {
|
if (res.data.records.length === 0) {
|
||||||
html = '<tr><td colspan="4" style="text-align:center;">暂无记录</td></tr>';
|
html = '<tr><td colspan="5" 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 pointsText = record.points_change > 0 ? `+${record.points_change}` : record.points_change;
|
const pointsText = record.points_change > 0 ? `+${record.points_change}` : record.points_change;
|
||||||
|
const typeLabel = { manual: '手动', homework: '作业', attendance: '考勤' }[record.related_type] || '手动';
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${formatDateTime(record.created_at)}</td>
|
<td>${typeLabel}</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 class="history-reason">${escapeHtml(record.reason || '-')}</td>
|
||||||
<td>班主任</td>
|
<td>班主任</td>
|
||||||
|
<td>${formatDateTime(record.created_at)}</td>
|
||||||
|
</tr>`;
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,10 +140,11 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>时间</th>
|
<th>类型</th>
|
||||||
<th>分值</th>
|
<th>分值</th>
|
||||||
<th>原因</th>
|
<th>原因</th>
|
||||||
<th>操作人</th>
|
<th>操作人</th>
|
||||||
|
<th>时间</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="homeworkList"></tbody>
|
<tbody id="homeworkList"></tbody>
|
||||||
@@ -351,21 +352,23 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
document.getElementById('conductTotalPoints').textContent = res.data.total_points;
|
document.getElementById('conductTotalPoints').textContent = res.data.total_points;
|
||||||
|
|
||||||
let html = '<div class="table-wrapper"><table><thead><tr><th>时间</th><th>分数变动</th><th>原因</th><th>操作人</th></tr></thead><tbody>';
|
let html = '<div class="table-wrapper"><table><thead><tr><th>类型</th><th>分值</th><th>原因</th><th>操作人</th><th>时间</th></tr></thead><tbody>';
|
||||||
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 recorderDisplay = record.points_change < 0 ? '班主任' : escapeHtml(record.recorder_name || '班主任');
|
const recorderDisplay = record.points_change < 0 ? '班主任' : escapeHtml(record.recorder_name || '班主任');
|
||||||
|
const typeLabel = { manual: '手动', homework: '作业', attendance: '考勤' }[record.related_type] || '手动';
|
||||||
html += `
|
html += `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${formatDateTime(record.created_at)}</td>
|
<td>${typeLabel}</td>
|
||||||
<td class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
<td class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
||||||
<td>${escapeHtml(record.reason)}</td>
|
<td>${escapeHtml(record.reason)}</td>
|
||||||
<td>${recorderDisplay}</td>
|
<td>${recorderDisplay}</td>
|
||||||
|
<td>${formatDateTime(record.created_at)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
if (res.data.records.length === 0) {
|
if (res.data.records.length === 0) {
|
||||||
html += '<tr><td colspan="4" style="text-align:center;">暂无记录</td></tr>';
|
html += '<tr><td colspan="5" style="text-align:center;">暂无记录</td></tr>';
|
||||||
}
|
}
|
||||||
html += '</tbody></table></div>';
|
html += '</tbody></table></div>';
|
||||||
document.getElementById('conductRecords').innerHTML = html;
|
document.getElementById('conductRecords').innerHTML = html;
|
||||||
@@ -391,20 +394,22 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
try {
|
try {
|
||||||
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
let html = '';
|
|
||||||
res.data.homework.forEach(record => {
|
res.data.homework.forEach(record => {
|
||||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||||
|
const typeLabel = { manual: '手动', homework: '作业', attendance: '考勤' }[record.related_type] || '作业';
|
||||||
html += `
|
html += `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${formatDateTime(record.created_at)}</td>
|
<td>${typeLabel}</td>
|
||||||
<td class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
<td class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
||||||
<td>${escapeHtml(record.reason)}</td>
|
<td>${escapeHtml(record.reason)}</td>
|
||||||
<td>${escapeHtml(record.recorder_name || '-')}</td>
|
<td>${escapeHtml(record.recorder_name || '-')}</td>
|
||||||
|
<td>${formatDateTime(record.created_at)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
if (res.data.homework.length === 0) {
|
if (res.data.homework.length === 0) {
|
||||||
html = '<tr><td colspan="4" style="text-align:center;">暂无作业扣分记录</td></tr>';
|
html = '<tr><td colspan="5" style="text-align:center;">暂无作业扣分记录</td></tr>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
document.getElementById('homeworkList').innerHTML = html;
|
document.getElementById('homeworkList').innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>时间</th><th>分值</th><th>原因</th><th>操作人</th></tr>
|
<tr><th>类型</th><th>分值</th><th>原因</th><th>操作人</th><th>时间</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="homeworkList"></tbody>
|
<tbody id="homeworkList"></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
11
sql/upgrades/v2.9.sql
Normal file
11
sql/upgrades/v2.9.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- ===========================================
|
||||||
|
-- 班级操行分管理系统 - v2.8 → v2.9 升级脚本
|
||||||
|
-- 字符集: utf8mb4
|
||||||
|
--
|
||||||
|
-- 说明: v2.9 为UI优化版本,无数据库 schema 变更。
|
||||||
|
-- 主要变更:
|
||||||
|
-- 1. 优化历史记录表头顺序(合并视图 + 非合并视图)
|
||||||
|
-- 2. 新增"类型"列显示 manual/homework/attendance
|
||||||
|
-- 3. 管理员"删除"改为"禁用/启用"切换
|
||||||
|
-- 4. 科目管理面板显示全部科目(禁用/启用均可见)
|
||||||
|
-- ===========================================
|
||||||
@@ -32,6 +32,7 @@ $UPGRADE_VERSIONS = [
|
|||||||
'2.6' => __DIR__ . '/sql/upgrades/v2.6.sql',
|
'2.6' => __DIR__ . '/sql/upgrades/v2.6.sql',
|
||||||
'2.7' => __DIR__ . '/sql/upgrades/v2.7.sql',
|
'2.7' => __DIR__ . '/sql/upgrades/v2.7.sql',
|
||||||
'2.8' => __DIR__ . '/sql/upgrades/v2.8.sql',
|
'2.8' => __DIR__ . '/sql/upgrades/v2.8.sql',
|
||||||
|
'2.9' => __DIR__ . '/sql/upgrades/v2.9.sql',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user