更新v1.4版本,修复了一些已知问题
This commit is contained in:
@@ -265,6 +265,7 @@ classmanager/
|
|||||||
| v1.1 | 2026.4.20 | 更新家长端查看加减分记录功能 |
|
| v1.1 | 2026.4.20 | 更新家长端查看加减分记录功能 |
|
||||||
| v1.2 | 2026.4.22 | 学期管理、env配置加减分上限、排行榜百分比筛选、撤销操作日志、调试入口开关 |
|
| v1.2 | 2026.4.22 | 学期管理、env配置加减分上限、排行榜百分比筛选、撤销操作日志、调试入口开关 |
|
||||||
| v1.3 | 2026.4.27 | 考勤时段系统(早上/中午/晚修三时段)、历史记录扣分类型筛选、管理员/科目信息编辑、全链路输入安全校验 |
|
| v1.3 | 2026.4.27 | 考勤时段系统(早上/中午/晚修三时段)、历史记录扣分类型筛选、管理员/科目信息编辑、全链路输入安全校验 |
|
||||||
|
| v1.4 | 2026.4.28 | 全量代码审查修复:双重密码哈希bug、学生端XSS漏洞、性能优化(COUNT替代全量加载)、Pydantic schema统一、权限检查补全、考勤委员撤销权限 |
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
|
|||||||
@@ -81,13 +81,13 @@ DEDUCTION_HOMEWORK_NOT_SUBMIT=2
|
|||||||
DEDUCTION_HOMEWORK_LATE=1
|
DEDUCTION_HOMEWORK_LATE=1
|
||||||
|
|
||||||
# 缺勤扣分 - 学生无故缺勤时扣除的操行分
|
# 缺勤扣分 - 学生无故缺勤时扣除的操行分
|
||||||
DEDUCTION_ATTENDANCE_ABSENT=5
|
DEDUCTION_ATTENDANCE_ABSENT=3
|
||||||
|
|
||||||
# 迟到扣分 - 学生迟到时扣除的操行分
|
# 迟到扣分 - 学生迟到时扣除的操行分
|
||||||
DEDUCTION_ATTENDANCE_LATE=2
|
DEDUCTION_ATTENDANCE_LATE=1
|
||||||
|
|
||||||
# 请假扣分 - 学生请假时扣除的操行分(可设为0表示不扣分)
|
# 请假扣分 - 学生请假时扣除的操行分(设为0表示不扣分)
|
||||||
DEDUCTION_ATTENDANCE_LEAVE=1
|
DEDUCTION_ATTENDANCE_LEAVE=0
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 劳动委员固定分值配置
|
# 劳动委员固定分值配置
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ class Settings:
|
|||||||
|
|
||||||
DEDUCTION_HOMEWORK_NOT_SUBMIT: int = int(os.getenv("DEDUCTION_HOMEWORK_NOT_SUBMIT", "2"))
|
DEDUCTION_HOMEWORK_NOT_SUBMIT: int = int(os.getenv("DEDUCTION_HOMEWORK_NOT_SUBMIT", "2"))
|
||||||
DEDUCTION_HOMEWORK_LATE: int = int(os.getenv("DEDUCTION_HOMEWORK_LATE", "1"))
|
DEDUCTION_HOMEWORK_LATE: int = int(os.getenv("DEDUCTION_HOMEWORK_LATE", "1"))
|
||||||
DEDUCTION_ATTENDANCE_ABSENT: int = int(os.getenv("DEDUCTION_ATTENDANCE_ABSENT", "5"))
|
DEDUCTION_ATTENDANCE_ABSENT: int = int(os.getenv("DEDUCTION_ATTENDANCE_ABSENT", "3"))
|
||||||
DEDUCTION_ATTENDANCE_LATE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LATE", "2"))
|
DEDUCTION_ATTENDANCE_LATE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LATE", "1"))
|
||||||
DEDUCTION_ATTENDANCE_LEAVE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LEAVE", "1"))
|
DEDUCTION_ATTENDANCE_LEAVE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LEAVE", "0"))
|
||||||
|
|
||||||
LABOR_POINTS_ADD: int = int(os.getenv("LABOR_POINTS_ADD", "1"))
|
LABOR_POINTS_ADD: int = int(os.getenv("LABOR_POINTS_ADD", "1"))
|
||||||
LABOR_POINTS_SUBTRACT: int = int(os.getenv("LABOR_POINTS_SUBTRACT", "-1"))
|
LABOR_POINTS_SUBTRACT: int = int(os.getenv("LABOR_POINTS_SUBTRACT", "-1"))
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|||||||
logger.warning(f"[Auth] {path} - Redis Token不匹配, user_id={user_id}, stored={'有' if stored_token else '无'}")
|
logger.warning(f"[Auth] {path} - Redis Token不匹配, user_id={user_id}, stored={'有' if stored_token else '无'}")
|
||||||
return self._cors_response(request, 401, "令牌已失效,请重新登录")
|
return self._cors_response(request, 401, "令牌已失效,请重新登录")
|
||||||
|
|
||||||
# 将用户信息存储到request.state
|
|
||||||
# 将用户信息存储到request.state
|
# 将用户信息存储到request.state
|
||||||
request.state.user_id = payload.get("user_id")
|
request.state.user_id = payload.get("user_id")
|
||||||
request.state.username = payload.get("username")
|
request.state.username = payload.get("username")
|
||||||
@@ -142,20 +141,3 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|||||||
},
|
},
|
||||||
headers=headers
|
headers=headers
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user(request: Request) -> Dict[str, Any]:
|
|
||||||
"""获取当前登录用户信息"""
|
|
||||||
return {
|
|
||||||
"user_id": request.state.user_id,
|
|
||||||
"username": request.state.username,
|
|
||||||
"real_name": getattr(request.state, 'real_name', None) or request.state.username,
|
|
||||||
"user_type": request.state.user_type,
|
|
||||||
"student_id": request.state.student_id,
|
|
||||||
"role": request.state.role
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user_id(request: Request) -> int:
|
|
||||||
"""获取当前用户ID"""
|
|
||||||
return request.state.user_id
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ async def get_current_user(request: Request) -> Dict[str, Any]:
|
|||||||
return {
|
return {
|
||||||
"user_id": getattr(request.state, 'user_id', None),
|
"user_id": getattr(request.state, 'user_id', None),
|
||||||
"username": getattr(request.state, 'username', None),
|
"username": getattr(request.state, 'username', None),
|
||||||
|
"real_name": getattr(request.state, 'real_name', None),
|
||||||
"user_type": getattr(request.state, 'user_type', None),
|
"user_type": getattr(request.state, 'user_type', None),
|
||||||
"student_id": getattr(request.state, 'student_id', None),
|
"student_id": getattr(request.state, 'student_id', None),
|
||||||
"role": getattr(request.state, 'role', None)
|
"role": getattr(request.state, 'role', None)
|
||||||
@@ -124,19 +125,23 @@ class PermissionChecker:
|
|||||||
async def check_can_revoke(user_id: int, record_id: int) -> bool:
|
async def check_can_revoke(user_id: int, record_id: int) -> bool:
|
||||||
"""
|
"""
|
||||||
检查是否可以撤销扣分记录
|
检查是否可以撤销扣分记录
|
||||||
班主任:可以撤销任何记录
|
班主任:可以撤销/反撤销任何记录
|
||||||
班长:可以撤销任何记录
|
班长:可以撤销/反撤销任何记录
|
||||||
考勤委员:可以撤销自己的记录
|
考勤委员:可以撤销自己创建的记录
|
||||||
其他:只能撤销自己的记录
|
其他角色:无撤销权限
|
||||||
"""
|
"""
|
||||||
sql = "SELECT recorder_id FROM conduct_records WHERE record_id = %s"
|
record = await execute_one(
|
||||||
record = await execute_one(sql, (record_id,))
|
"SELECT record_id, recorder_id FROM conduct_records WHERE record_id = %s",
|
||||||
|
(record_id,)
|
||||||
|
)
|
||||||
if not record:
|
if not record:
|
||||||
return False
|
return False
|
||||||
role = await PermissionChecker.get_user_role(user_id)
|
role = await PermissionChecker.get_user_role(user_id)
|
||||||
if role in ["班主任", "班长", "志愿委员"]:
|
if role in ["班主任", "班长"]:
|
||||||
return True
|
return True
|
||||||
return record["recorder_id"] == user_id
|
if role == "考勤委员" and record.get("recorder_id") == user_id:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def require_auth(func: Callable):
|
def require_auth(func: Callable):
|
||||||
|
|||||||
@@ -40,6 +40,21 @@ class ConductModel:
|
|||||||
student_id, points_change, reason, recorder_id, recorder_name, related_type, related_id
|
student_id, points_change, reason, recorder_id, recorder_name, related_type, related_id
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def count_student_records(student_id: int, include_revoked: bool = False) -> int:
|
||||||
|
"""统计学生操行分记录总数"""
|
||||||
|
revoked_condition = "" if include_revoked else " AND is_revoked = 0"
|
||||||
|
sql = f"SELECT COUNT(*) as total FROM conduct_records WHERE student_id = %s{revoked_condition}"
|
||||||
|
result = await execute_one(sql, (student_id,))
|
||||||
|
return result["total"] if result else 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def count_records_by_recorder(recorder_id: int) -> int:
|
||||||
|
"""统计记录人提交的操行分记录总数"""
|
||||||
|
sql = "SELECT COUNT(*) as total FROM conduct_records WHERE recorder_id = %s"
|
||||||
|
result = await execute_one(sql, (recorder_id,))
|
||||||
|
return result["total"] if result else 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_student_records(
|
async def get_student_records(
|
||||||
student_id: int,
|
student_id: int,
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ from schemas.admin import (
|
|||||||
AddPointsRequest, RevokeRequest, AddAdminRequest,
|
AddPointsRequest, RevokeRequest, AddAdminRequest,
|
||||||
AddStudentRequest, UpdateStudentRequest,
|
AddStudentRequest, UpdateStudentRequest,
|
||||||
UpdateHomeworkStatusRequest, AddAttendanceRequest,
|
UpdateHomeworkStatusRequest, AddAttendanceRequest,
|
||||||
UpdateAdminRequest, DeleteAdminRequest, ResetPasswordRequest
|
UpdateAdminRequest, DeleteAdminRequest, ResetPasswordRequest,
|
||||||
|
CreateAssignmentRequest
|
||||||
)
|
)
|
||||||
from utils.response import success_response, error_response
|
from utils.response import success_response, error_response
|
||||||
from utils.logger import get_logger
|
from utils.logger import get_logger
|
||||||
@@ -48,6 +49,8 @@ async def get_students(
|
|||||||
):
|
):
|
||||||
"""获取所有学生列表(单班级)"""
|
"""获取所有学生列表(单班级)"""
|
||||||
user = await get_current_user(request)
|
user = await get_current_user(request)
|
||||||
|
if user["user_type"] != "admin":
|
||||||
|
return error_response(message="仅管理员可查看学生列表", code=403)
|
||||||
result = await AdminService.get_students(page=page, page_size=page_size, search=search)
|
result = await AdminService.get_students(page=page, page_size=page_size, search=search)
|
||||||
return success_response(data=result)
|
return success_response(data=result)
|
||||||
|
|
||||||
@@ -138,6 +141,13 @@ async def update_student(request: Request, student_id: int, req: UpdateStudentRe
|
|||||||
parent_phone=req.parent_phone
|
parent_phone=req.parent_phone
|
||||||
)
|
)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
|
await LogService.write_operation_log(
|
||||||
|
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||||
|
operator_role="班主任", operation_type="update_student",
|
||||||
|
target_type="student", target_id=student_id,
|
||||||
|
details=f"编辑学生ID: {student_id}",
|
||||||
|
ip=request.client.host
|
||||||
|
)
|
||||||
return success_response(message=result["message"])
|
return success_response(message=result["message"])
|
||||||
else:
|
else:
|
||||||
return error_response(message=result["message"])
|
return error_response(message=result["message"])
|
||||||
@@ -153,6 +163,13 @@ async def delete_student(request: Request, student_id: int):
|
|||||||
|
|
||||||
result = await AdminService.delete_student(student_id=student_id)
|
result = await AdminService.delete_student(student_id=student_id)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
|
await LogService.write_operation_log(
|
||||||
|
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||||
|
operator_role="班主任", operation_type="delete_student",
|
||||||
|
target_type="student", target_id=student_id,
|
||||||
|
details=f"删除学生ID: {student_id}",
|
||||||
|
ip=request.client.host
|
||||||
|
)
|
||||||
return success_response(message=result["message"])
|
return success_response(message=result["message"])
|
||||||
else:
|
else:
|
||||||
return error_response(message=result["message"])
|
return error_response(message=result["message"])
|
||||||
@@ -171,6 +188,13 @@ async def reset_student_password(request: Request, student_id: int, req: ResetPa
|
|||||||
new_password=req.new_password
|
new_password=req.new_password
|
||||||
)
|
)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
|
await LogService.write_operation_log(
|
||||||
|
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||||
|
operator_role="班主任", operation_type="reset_student_password",
|
||||||
|
target_type="student", target_id=student_id,
|
||||||
|
details=f"重置学生密码, 学生ID: {student_id}",
|
||||||
|
ip=request.client.host
|
||||||
|
)
|
||||||
return success_response(message=result["message"])
|
return success_response(message=result["message"])
|
||||||
else:
|
else:
|
||||||
return error_response(message=result["message"])
|
return error_response(message=result["message"])
|
||||||
@@ -272,6 +296,8 @@ async def get_conduct_history(
|
|||||||
"""获取操行分历史记录"""
|
"""获取操行分历史记录"""
|
||||||
try:
|
try:
|
||||||
user = await get_current_user(request)
|
user = await get_current_user(request)
|
||||||
|
if user["user_type"] != "admin":
|
||||||
|
return error_response(message="仅管理员可查看历史记录", code=403)
|
||||||
result = await ConductService.get_history(
|
result = await ConductService.get_history(
|
||||||
user_id=user["user_id"],
|
user_id=user["user_id"],
|
||||||
student_id=student_id,
|
student_id=student_id,
|
||||||
@@ -316,23 +342,17 @@ async def get_submissions(request: Request, assignment_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/homework/assignment")
|
@router.post("/homework/assignment")
|
||||||
async def create_assignment(
|
async def create_assignment(request: Request, req: CreateAssignmentRequest):
|
||||||
request: Request,
|
|
||||||
subject_id: int,
|
|
||||||
title: str,
|
|
||||||
description: Optional[str] = None,
|
|
||||||
deadline: str = None
|
|
||||||
):
|
|
||||||
"""发布作业(班主任)"""
|
"""发布作业(班主任)"""
|
||||||
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)
|
||||||
result = await HomeworkService.create_assignment(
|
result = await HomeworkService.create_assignment(
|
||||||
subject_id=subject_id,
|
subject_id=req.subject_id,
|
||||||
title=title,
|
title=req.title,
|
||||||
description=description,
|
description=req.description,
|
||||||
deadline=deadline,
|
deadline=req.deadline,
|
||||||
created_by=user["user_id"]
|
created_by=user["user_id"]
|
||||||
)
|
)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
@@ -416,6 +436,9 @@ async def get_attendance_records(
|
|||||||
):
|
):
|
||||||
"""获取考勤记录"""
|
"""获取考勤记录"""
|
||||||
user = await get_current_user(request)
|
user = await get_current_user(request)
|
||||||
|
role = await PermissionChecker.get_user_role(user["user_id"])
|
||||||
|
if role not in ["班主任", "考勤委员"]:
|
||||||
|
return error_response(message="无权查看考勤记录", code=403)
|
||||||
result = await AttendanceService.get_records(
|
result = await AttendanceService.get_records(
|
||||||
user_id=user["user_id"],
|
user_id=user["user_id"],
|
||||||
date=date,
|
date=date,
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ class AddAttendanceRequest(BaseModel):
|
|||||||
|
|
||||||
class UpdateAdminRequest(BaseModel):
|
class UpdateAdminRequest(BaseModel):
|
||||||
"""更新管理员请求"""
|
"""更新管理员请求"""
|
||||||
user_id: int = Field(..., gt=0, description="用户ID")
|
|
||||||
real_name: str = Field(..., min_length=1, max_length=50, description="真实姓名")
|
real_name: str = Field(..., min_length=1, max_length=50, description="真实姓名")
|
||||||
role_type: str = Field(..., pattern=r'^(班长|学习委员|考勤委员|劳动委员|志愿委员)$', description="角色类型")
|
role_type: str = Field(..., pattern=r'^(班长|学习委员|考勤委员|劳动委员|志愿委员)$', description="角色类型")
|
||||||
|
|
||||||
@@ -110,3 +109,11 @@ class UpdateStudentRequest(BaseModel):
|
|||||||
"""更新学生请求"""
|
"""更新学生请求"""
|
||||||
name: Optional[str] = Field(None, min_length=1, max_length=50, description="姓名")
|
name: Optional[str] = Field(None, min_length=1, max_length=50, description="姓名")
|
||||||
parent_phone: Optional[str] = Field(None, max_length=11, pattern=r'^\d{0,11}$', description="家长手机号")
|
parent_phone: Optional[str] = Field(None, max_length=11, pattern=r'^\d{0,11}$', description="家长手机号")
|
||||||
|
|
||||||
|
|
||||||
|
class CreateAssignmentRequest(BaseModel):
|
||||||
|
"""创建作业请求"""
|
||||||
|
subject_id: int = Field(..., gt=0, description="科目ID")
|
||||||
|
title: str = Field(..., min_length=1, max_length=200, description="作业标题")
|
||||||
|
description: Optional[str] = Field(None, max_length=1000, description="作业描述")
|
||||||
|
deadline: str = Field(..., min_length=1, max_length=20, description="截止日期")
|
||||||
@@ -296,9 +296,8 @@ class AdminService:
|
|||||||
if not user:
|
if not user:
|
||||||
return {"success": False, "message": "未找到对应的用户账号"}
|
return {"success": False, "message": "未找到对应的用户账号"}
|
||||||
|
|
||||||
password_hash = security.sha1_md5_password(new_password)
|
# UserModel.update_password 内部会进行哈希,无需预先哈希
|
||||||
|
result = await UserModel.update_password(user['user_id'], new_password)
|
||||||
result = await UserModel.update_password(user['user_id'], password_hash)
|
|
||||||
if result:
|
if result:
|
||||||
await execute_update(
|
await execute_update(
|
||||||
"UPDATE users SET need_change_password = 1 WHERE user_id = %s",
|
"UPDATE users SET need_change_password = 1 WHERE user_id = %s",
|
||||||
|
|||||||
@@ -89,14 +89,17 @@ class AttendanceService:
|
|||||||
points_change = -settings.DEDUCTION_ATTENDANCE_LATE
|
points_change = -settings.DEDUCTION_ATTENDANCE_LATE
|
||||||
else:
|
else:
|
||||||
points_change = -settings.DEDUCTION_ATTENDANCE_LEAVE
|
points_change = -settings.DEDUCTION_ATTENDANCE_LEAVE
|
||||||
# 创建扣分记录
|
|
||||||
|
# 扣分为0时跳过(如请假不扣分)
|
||||||
|
if points_change == 0:
|
||||||
|
logger.info(f"用户[{recorder_id}] 添加考勤记录[{attendance_id}] -> {status} (不扣分)")
|
||||||
|
return {"success": True, "message": "考勤记录添加成功(不扣分)"}
|
||||||
student = await StudentModel.get_by_id(student_id)
|
student = await StudentModel.get_by_id(student_id)
|
||||||
if student:
|
if student:
|
||||||
# 获取操作人姓名
|
# 获取操作人姓名
|
||||||
user = await UserModel.get_by_user_id(recorder_id)
|
user = await UserModel.get_by_user_id(recorder_id)
|
||||||
recorder_name = user.get("real_name", "班主任") if user else "班主任"
|
recorder_name = user.get("real_name", "班主任") if user else "班主任"
|
||||||
# 使用中文状态
|
# 使用中文状态
|
||||||
# 使用中文状态
|
|
||||||
status_text = ATTENDANCE_STATUS_MAP.get(status, status)
|
status_text = ATTENDANCE_STATUS_MAP.get(status, status)
|
||||||
await ConductModel.create_record(
|
await ConductModel.create_record(
|
||||||
student_id=student_id,
|
student_id=student_id,
|
||||||
|
|||||||
@@ -115,8 +115,10 @@ class ConductService:
|
|||||||
details.append({"student_id": student_id, "error": str(e)})
|
details.append({"student_id": student_id, "error": str(e)})
|
||||||
fail_count += 1
|
fail_count += 1
|
||||||
|
|
||||||
|
message = "操作成功" if fail_count == 0 else f"{success_count}人成功,{fail_count}人失败"
|
||||||
return {
|
return {
|
||||||
"success": fail_count == 0,
|
"success": fail_count == 0,
|
||||||
|
"message": message,
|
||||||
"success_count": success_count,
|
"success_count": success_count,
|
||||||
"fail_count": fail_count,
|
"fail_count": fail_count,
|
||||||
"details": details
|
"details": details
|
||||||
@@ -278,7 +280,7 @@ class ConductService:
|
|||||||
limit=page_size,
|
limit=page_size,
|
||||||
offset=offset
|
offset=offset
|
||||||
)
|
)
|
||||||
total = len(await ConductModel.get_student_records(student_id, limit=10000))
|
total = await ConductModel.count_student_records(student_id)
|
||||||
else:
|
else:
|
||||||
# 查看自己提交的记录
|
# 查看自己提交的记录
|
||||||
records = await ConductModel.get_records_by_recorder(
|
records = await ConductModel.get_records_by_recorder(
|
||||||
@@ -286,7 +288,7 @@ class ConductService:
|
|||||||
limit=page_size,
|
limit=page_size,
|
||||||
offset=offset
|
offset=offset
|
||||||
)
|
)
|
||||||
total = len(await ConductModel.get_records_by_recorder(user_id, limit=10000))
|
total = await ConductModel.count_records_by_recorder(user_id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"records": records,
|
"records": records,
|
||||||
|
|||||||
@@ -135,9 +135,8 @@ class ParentService:
|
|||||||
offset=offset
|
offset=offset
|
||||||
)
|
)
|
||||||
|
|
||||||
# 获取总数
|
# 使用 COUNT 查询获取总数(避免获取全部记录)
|
||||||
all_records = await ConductModel.get_student_records(user["student_id"], limit=10000)
|
total = await ConductModel.count_student_records(user["student_id"])
|
||||||
total = len(all_records)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"student_id": student["student_id"],
|
"student_id": student["student_id"],
|
||||||
|
|||||||
@@ -48,11 +48,11 @@ HOMEWORK_MAX_POINTS=3
|
|||||||
STUDY_COMMISSIONER_MAX_POINTS=5
|
STUDY_COMMISSIONER_MAX_POINTS=5
|
||||||
|
|
||||||
# 考勤-缺勤扣分
|
# 考勤-缺勤扣分
|
||||||
DEDUCTION_ATTENDANCE_ABSENT=5
|
DEDUCTION_ATTENDANCE_ABSENT=3
|
||||||
# 考勤-迟到扣分
|
# 考勤-迟到扣分
|
||||||
DEDUCTION_ATTENDANCE_LATE=2
|
DEDUCTION_ATTENDANCE_LATE=1
|
||||||
# 考勤-请假扣分
|
# 考勤-请假扣分(设为0表示不扣分)
|
||||||
DEDUCTION_ATTENDANCE_LEAVE=1
|
DEDUCTION_ATTENDANCE_LEAVE=0
|
||||||
|
|
||||||
# 学生初始操行分
|
# 学生初始操行分
|
||||||
STUDENT_INITIAL_POINTS=60
|
STUDENT_INITIAL_POINTS=60
|
||||||
@@ -227,7 +227,6 @@ async function submitEditAdmin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const res = await apiPut(`/api/admin/update/${currentEditUserId}`, {
|
const res = await apiPut(`/api/admin/update/${currentEditUserId}`, {
|
||||||
user_id: currentEditUserId,
|
|
||||||
real_name: document.getElementById('editAdminRealName').value,
|
real_name: document.getElementById('editAdminRealName').value,
|
||||||
role_type: roleType
|
role_type: roleType
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-group">
|
<div class="status-group">
|
||||||
<button class="status-btn active" data-status="absent" onclick="selectStatus(this)" data-default-deduction="3">缺勤</button>
|
<button class="status-btn active" data-status="absent" onclick="selectStatus(this)" id="btnAbsent">缺勤</button>
|
||||||
<button class="status-btn" data-status="late" onclick="selectStatus(this)" data-default-deduction="1">迟到</button>
|
<button class="status-btn" data-status="late" onclick="selectStatus(this)" id="btnLate">迟到</button>
|
||||||
<button class="status-btn" data-status="leave" onclick="selectStatus(this)" data-default-deduction="0">请假</button>
|
<button class="status-btn" data-status="leave" onclick="selectStatus(this)" id="btnLeave">请假</button>
|
||||||
<input type="number" id="customDeduction" placeholder="自定义扣分" min="0" max="10" style="width:100px;margin-left:10px;" title="留空或0使用默认值">
|
<input type="number" id="customDeduction" placeholder="自定义扣分" min="0" max="20" style="width:100px;margin-left:10px;" title="留空或0使用默认值">
|
||||||
</div>
|
</div>
|
||||||
<input type="text" id="attendanceReason" placeholder="原因(可选)" style="flex:1;min-width:150px;">
|
<input type="text" id="attendanceReason" placeholder="原因(可选)" style="flex:1;min-width:150px;">
|
||||||
<button class="btn btn-primary" onclick="selectAllStudents()">全选</button>
|
<button class="btn btn-primary" onclick="selectAllStudents()">全选</button>
|
||||||
@@ -92,14 +92,35 @@ let currentStatus = 'absent';
|
|||||||
let studentsData = [];
|
let studentsData = [];
|
||||||
let existingRecords = [];
|
let existingRecords = [];
|
||||||
|
|
||||||
|
// 考勤扣分配置映射(从后端配置注入)
|
||||||
|
const attendanceDeductionMap = {
|
||||||
|
absent: window.DEDUCTION_ATTENDANCE_ABSENT || 3,
|
||||||
|
late: window.DEDUCTION_ATTENDANCE_LATE || 1,
|
||||||
|
leave: window.DEDUCTION_ATTENDANCE_LEAVE || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化按钮文字
|
||||||
|
function initAttendanceButtons() {
|
||||||
|
const btnAbsent = document.getElementById('btnAbsent');
|
||||||
|
const btnLate = document.getElementById('btnLate');
|
||||||
|
const btnLeave = document.getElementById('btnLeave');
|
||||||
|
if (btnAbsent) btnAbsent.textContent = '缺勤(' + attendanceDeductionMap.absent + '分)';
|
||||||
|
if (btnLate) btnLate.textContent = '迟到(' + attendanceDeductionMap.late + '分)';
|
||||||
|
if (btnLeave) btnLeave.textContent = '请假(' + (attendanceDeductionMap.leave > 0 ? attendanceDeductionMap.leave + '分' : '不扣分') + ')';
|
||||||
|
// 默认选中缺勤,自动填入默认扣分
|
||||||
|
if (attendanceDeductionMap.absent > 0) {
|
||||||
|
document.getElementById('customDeduction').value = attendanceDeductionMap.absent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 选择考勤状态
|
// 选择考勤状态
|
||||||
function selectStatus(btn) {
|
function selectStatus(btn) {
|
||||||
document.querySelectorAll('.status-btn').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.status-btn').forEach(b => b.classList.remove('active'));
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
currentStatus = btn.dataset.status;
|
currentStatus = btn.dataset.status;
|
||||||
// 自动设置默认扣分值
|
// 自动设置默认扣分值(从配置读取)
|
||||||
const defaultDeduction = btn.dataset.defaultDeduction;
|
const defaultDeduction = attendanceDeductionMap[currentStatus] || 0;
|
||||||
if (defaultDeduction && defaultDeduction !== '0') {
|
if (defaultDeduction > 0) {
|
||||||
document.getElementById('customDeduction').value = defaultDeduction;
|
document.getElementById('customDeduction').value = defaultDeduction;
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('customDeduction').value = '';
|
document.getElementById('customDeduction').value = '';
|
||||||
@@ -252,6 +273,7 @@ document.getElementById('attendanceSlot').addEventListener('change', function()
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 页面初始化
|
// 页面初始化
|
||||||
|
initAttendanceButtons();
|
||||||
loadStudents();
|
loadStudents();
|
||||||
loadAttendanceRecords();
|
loadAttendanceRecords();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
|||||||
$page_title = '操行分管理';
|
$page_title = '操行分管理';
|
||||||
$role = $_SESSION['role'] ?? '';
|
$role = $_SESSION['role'] ?? '';
|
||||||
|
|
||||||
if (!in_array($role, ['班主任', '班长', '学习委员', '劳动委员', '志愿委员'])) {
|
if (!in_array($role, ['班主任', '班长', '学习委员', '考勤委员', '劳动委员', '志愿委员'])) {
|
||||||
header('Location: /admin/dashboard.php');
|
header('Location: /admin/dashboard.php');
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<th>分数变动</th>
|
<th>分数变动</th>
|
||||||
<th>原因</th>
|
<th>原因</th>
|
||||||
<th>操作人</th>
|
<th>操作人</th>
|
||||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
<?php if ($role === '班主任' || $role === '班长' || $role === '考勤委员'): ?>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -80,6 +80,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<script>
|
<script>
|
||||||
var currentHistoryPage = 1;
|
var currentHistoryPage = 1;
|
||||||
var totalHistoryPages = 1;
|
var totalHistoryPages = 1;
|
||||||
|
var currentUserId = <?php echo intval($_SESSION['user_id']); ?>;
|
||||||
|
|
||||||
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});
|
||||||
@@ -134,12 +135,20 @@ async function loadHistory(page = 1) {
|
|||||||
} 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-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||||
}
|
}
|
||||||
|
<?php elseif ($role === '考勤委员'): ?>
|
||||||
|
if (record.is_revoked == 1) {
|
||||||
|
html += `<td><span class="text-muted">已撤销</span></td>`;
|
||||||
|
} else if (record.recorder_id == currentUserId) {
|
||||||
|
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||||
|
} else {
|
||||||
|
html += `<td><span class="text-muted">-</span></td>`;
|
||||||
|
}
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
html += `</tr>`;
|
html += `</tr>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.data.records.length === 0) {
|
if (res.data.records.length === 0) {
|
||||||
const colSpan = <?php echo ($role === '班主任' || $role === '班长') ? '6' : '5'; ?>;
|
const colSpan = <?php echo ($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>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,22 +160,9 @@ async function loadHistory(page = 1) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderHistoryPagination() {
|
function renderHistoryPagination() {
|
||||||
const container = document.getElementById('historyPagination');
|
renderSmartPagination('historyPagination', currentHistoryPage, totalHistoryPages, function(page) {
|
||||||
if (!container) return;
|
loadHistory(page);
|
||||||
if (totalHistoryPages <= 1) {
|
});
|
||||||
container.innerHTML = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
for (let i = 1; i <= totalHistoryPages; i++) {
|
|
||||||
if (i === currentHistoryPage) {
|
|
||||||
html += `<span class="active">${i}</span>`;
|
|
||||||
} else {
|
|
||||||
html += `<a href="#" onclick="loadHistory(${i}); return false;">${i}</a>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
container.innerHTML = html;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出历史记录
|
// 导出历史记录
|
||||||
|
|||||||
@@ -457,21 +457,9 @@ async function viewArchiveData(semesterId, semesterName, page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderArchivePagination(semesterId, semesterName) {
|
function renderArchivePagination(semesterId, semesterName) {
|
||||||
const container = document.getElementById('archivePagination');
|
renderSmartPagination('archivePagination', archivePage, archiveTotalPages, function(page) {
|
||||||
if (!container) return;
|
viewArchiveData(semesterId, semesterName, page);
|
||||||
if (archiveTotalPages <= 1) {
|
});
|
||||||
container.innerHTML = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let html = '';
|
|
||||||
for (let i = 1; i <= archiveTotalPages; i++) {
|
|
||||||
if (i === archivePage) {
|
|
||||||
html += `<span class="active">${i}</span>`;
|
|
||||||
} else {
|
|
||||||
html += `<a href="#" onclick="viewArchiveData(${semesterId}, '${escapeHtml(semesterName)}', ${i}); return false;">${i}</a>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
container.innerHTML = html;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal(modalId) {
|
function closeModal(modalId) {
|
||||||
|
|||||||
@@ -201,22 +201,9 @@ async function loadStudents(page = 1) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderPagination() {
|
function renderPagination() {
|
||||||
const container = document.getElementById('pagination');
|
renderSmartPagination('pagination', currentPage, totalPages, function(page) {
|
||||||
if (!container) return;
|
loadStudents(page);
|
||||||
if (totalPages <= 1) {
|
});
|
||||||
container.innerHTML = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
|
||||||
if (i === currentPage) {
|
|
||||||
html += `<span class="active">${i}</span>`;
|
|
||||||
} else {
|
|
||||||
html += `<a href="#" onclick="loadStudents(${i}); return false;">${i}</a>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
container.innerHTML = html;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSinglePointsModal(studentId, studentName) {
|
function showSinglePointsModal(studentId, studentName) {
|
||||||
@@ -263,7 +250,16 @@ document.getElementById('searchInput').addEventListener('input', () => {
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>分数变动</label>
|
<label>分数变动</label>
|
||||||
<input type="number" id="pointsChange" required placeholder="正数为加分,负数为扣分">
|
<input type="number" id="pointsChange" required placeholder="正数为加分,负数为扣分">
|
||||||
<small><?php echo $role === '班长' ? '班长单次±5分以内' : '班主任无限制'; ?></small>
|
<small><?php
|
||||||
|
$hints = [
|
||||||
|
'班长' => '班长单次±5分以内',
|
||||||
|
'学习委员' => '学习委员单次±5分以内',
|
||||||
|
'考勤委员' => '考勤委员仅限扣分,单次最多扣8分',
|
||||||
|
'劳动委员' => '劳动委员单次±1分以内',
|
||||||
|
'志愿委员' => '志愿委员仅限加分,最多+5分',
|
||||||
|
];
|
||||||
|
echo $hints[$role] ?? '班主任无限制';
|
||||||
|
?></small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>原因</label>
|
<label>原因</label>
|
||||||
|
|||||||
@@ -490,8 +490,10 @@ tr:hover {
|
|||||||
.pagination {
|
.pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination a, .pagination span {
|
.pagination a, .pagination span {
|
||||||
@@ -501,6 +503,16 @@ tr:hover {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #666;
|
color: #666;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
min-width: 36px;
|
||||||
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a:hover {
|
||||||
|
background: #f0f0ff;
|
||||||
|
border-color: #667eea;
|
||||||
|
color: #667eea;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination .active {
|
.pagination .active {
|
||||||
@@ -509,6 +521,43 @@ tr:hover {
|
|||||||
border-color: #667eea;
|
border-color: #667eea;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination .ellipsis {
|
||||||
|
border: none;
|
||||||
|
cursor: default;
|
||||||
|
padding: 6px 4px;
|
||||||
|
color: #999;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-jump {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-jump input {
|
||||||
|
width: 50px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-jump input:focus {
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-nav {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ========== 提示消息 ========== */
|
/* ========== 提示消息 ========== */
|
||||||
.toast {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -205,6 +205,125 @@ function escapeHtml(str) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能分页渲染(最多显示7个页码 + 跳转输入框)
|
||||||
|
* @param {string|HTMLElement} container - 分页容器ID或DOM元素
|
||||||
|
* @param {number} currentPage - 当前页码
|
||||||
|
* @param {number} totalPages - 总页数
|
||||||
|
* @param {function} onPageChange - 页码变化回调函数,参数为新的页码
|
||||||
|
*/
|
||||||
|
function renderSmartPagination(container, currentPage, totalPages, onPageChange) {
|
||||||
|
if (typeof container === 'string') {
|
||||||
|
container = document.getElementById(container);
|
||||||
|
}
|
||||||
|
if (!container || totalPages <= 1) {
|
||||||
|
if (container) container.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_VISIBLE = 7;
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// 上一页按钮
|
||||||
|
if (currentPage > 1) {
|
||||||
|
html += `<a href="#" class="page-nav" data-page="${currentPage - 1}">« 上一页</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalPages <= MAX_VISIBLE) {
|
||||||
|
// 总页数不超过最大显示数,全部显示
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
if (i === currentPage) {
|
||||||
|
html += `<span class="active">${i}</span>`;
|
||||||
|
} else {
|
||||||
|
html += `<a href="#" data-page="${i}">${i}</a>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 需要省略号
|
||||||
|
// 始终显示第1页
|
||||||
|
if (currentPage === 1) {
|
||||||
|
html += `<span class="active">1</span>`;
|
||||||
|
} else {
|
||||||
|
html += `<a href="#" data-page="1">1</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算中间页码范围
|
||||||
|
let start = Math.max(2, currentPage - 2);
|
||||||
|
let end = Math.min(totalPages - 1, currentPage + 2);
|
||||||
|
|
||||||
|
// 调整确保中间至少有3个页码(加上首尾共5-7个)
|
||||||
|
if (currentPage <= 3) {
|
||||||
|
end = Math.min(5, totalPages - 1);
|
||||||
|
}
|
||||||
|
if (currentPage >= totalPages - 2) {
|
||||||
|
start = Math.max(2, totalPages - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前省略号
|
||||||
|
if (start > 2) {
|
||||||
|
html += `<span class="ellipsis">...</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中间页码
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
if (i === currentPage) {
|
||||||
|
html += `<span class="active">${i}</span>`;
|
||||||
|
} else {
|
||||||
|
html += `<a href="#" data-page="${i}">${i}</a>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后省略号
|
||||||
|
if (end < totalPages - 1) {
|
||||||
|
html += `<span class="ellipsis">...</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 始终显示最后一页
|
||||||
|
if (currentPage === totalPages) {
|
||||||
|
html += `<span class="active">${totalPages}</span>`;
|
||||||
|
} else {
|
||||||
|
html += `<a href="#" data-page="${totalPages}">${totalPages}</a>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下一页按钮
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
html += `<a href="#" class="page-nav" data-page="${currentPage + 1}">下一页 »</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码跳转
|
||||||
|
html += `<span class="page-jump">跳至 <input type="number" min="1" max="${totalPages}" placeholder="页码"> / ${totalPages}页</span>`;
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
// 绑定页码点击事件
|
||||||
|
container.querySelectorAll('a[data-page]').forEach(link => {
|
||||||
|
link.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const page = parseInt(this.dataset.page);
|
||||||
|
if (page && page !== currentPage && page >= 1 && page <= totalPages) {
|
||||||
|
onPageChange(page);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 绑定跳转输入框事件
|
||||||
|
const jumpInput = container.querySelector('.page-jump input');
|
||||||
|
if (jumpInput) {
|
||||||
|
jumpInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
const page = parseInt(this.value);
|
||||||
|
if (page && page >= 1 && page <= totalPages) {
|
||||||
|
onPageChange(page);
|
||||||
|
} else {
|
||||||
|
showToast(`请输入1-${totalPages}之间的页码`, 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const user = getUserInfo();
|
const user = getUserInfo();
|
||||||
const userNameSpan = document.getElementById('userName');
|
const userNameSpan = document.getElementById('userName');
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ define('ICP_NUMBER', $config['ICP_NUMBER'] ?? '');
|
|||||||
define('DEDUCTION_HOMEWORK_NOT_SUBMIT', (int)($config['DEDUCTION_HOMEWORK_NOT_SUBMIT'] ?? 2));
|
define('DEDUCTION_HOMEWORK_NOT_SUBMIT', (int)($config['DEDUCTION_HOMEWORK_NOT_SUBMIT'] ?? 2));
|
||||||
define('DEDUCTION_HOMEWORK_LATE', (int)($config['DEDUCTION_HOMEWORK_LATE'] ?? 1));
|
define('DEDUCTION_HOMEWORK_LATE', (int)($config['DEDUCTION_HOMEWORK_LATE'] ?? 1));
|
||||||
define('HOMEWORK_MAX_POINTS', (int)($config['HOMEWORK_MAX_POINTS'] ?? 3));
|
define('HOMEWORK_MAX_POINTS', (int)($config['HOMEWORK_MAX_POINTS'] ?? 3));
|
||||||
define('DEDUCTION_ATTENDANCE_ABSENT', (int)($config['DEDUCTION_ATTENDANCE_ABSENT'] ?? 5));
|
define('DEDUCTION_ATTENDANCE_ABSENT', (int)($config['DEDUCTION_ATTENDANCE_ABSENT'] ?? 3));
|
||||||
define('DEDUCTION_ATTENDANCE_LATE', (int)($config['DEDUCTION_ATTENDANCE_LATE'] ?? 2));
|
define('DEDUCTION_ATTENDANCE_LATE', (int)($config['DEDUCTION_ATTENDANCE_LATE'] ?? 1));
|
||||||
define('DEDUCTION_ATTENDANCE_LEAVE', (int)($config['DEDUCTION_ATTENDANCE_LEAVE'] ?? 1));
|
define('DEDUCTION_ATTENDANCE_LEAVE', (int)($config['DEDUCTION_ATTENDANCE_LEAVE'] ?? 0));
|
||||||
|
|
||||||
// 学生初始操行分
|
// 学生初始操行分
|
||||||
define('STUDENT_INITIAL_POINTS', (int)($config['STUDENT_INITIAL_POINTS'] ?? 60));
|
define('STUDENT_INITIAL_POINTS', (int)($config['STUDENT_INITIAL_POINTS'] ?? 60));
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="nav">
|
<div class="nav">
|
||||||
<a href="/admin/dashboard.php" class="nav-item<?php echo $current_page === 'dashboard' ? ' active' : ''; ?>">首页</a>
|
<a href="/admin/dashboard.php" class="nav-item<?php echo $current_page === 'dashboard' ? ' active' : ''; ?>">首页</a>
|
||||||
<a href="/admin/students.php" class="nav-item<?php echo $current_page === 'students' ? ' active' : ''; ?>">学生管理</a>
|
<a href="/admin/students.php" class="nav-item<?php echo $current_page === 'students' ? ' active' : ''; ?>">学生管理</a>
|
||||||
<?php if ($role === '班主任' || $role === '班长' || $role === '劳动委员' || $role === '志愿委员'): ?>
|
<?php if ($role === '班主任' || $role === '班长' || $role === '考勤委员' || $role === '劳动委员' || $role === '志愿委员'): ?>
|
||||||
<a href="/admin/conduct.php" class="nav-item<?php echo $current_page === 'conduct' ? ' active' : ''; ?>">操行分管理</a>
|
<a href="/admin/conduct.php" class="nav-item<?php echo $current_page === 'conduct' ? ' active' : ''; ?>">操行分管理</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($role === '班主任' || $role === '学习委员'): ?>
|
<?php if ($role === '班主任' || $role === '学习委员'): ?>
|
||||||
|
|||||||
@@ -133,6 +133,5 @@ if (isset($_SESSION['user_id']) && isset($_SESSION['user_type'])) {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -73,7 +73,6 @@ async function loadDashboard() {
|
|||||||
document.getElementById('totalPoints').textContent = res.data.total_points;
|
document.getElementById('totalPoints').textContent = res.data.total_points;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载排名信息
|
|
||||||
// 加载排名信息
|
// 加载排名信息
|
||||||
const rankRes = await apiGet('/api/parent/child/ranking');
|
const rankRes = await apiGet('/api/parent/child/ranking');
|
||||||
if (rankRes && rankRes.success) {
|
if (rankRes && rankRes.success) {
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
html += `
|
html += `
|
||||||
<div class="record-item">
|
<div class="record-item">
|
||||||
<span class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</span>
|
<span class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</span>
|
||||||
<span class="record-reason">${record.reason}</span>
|
<span class="record-reason">${escapeHtml(record.reason)}</span>
|
||||||
<span class="record-time">${formatDate(record.created_at)}</span>
|
<span class="record-time">${formatDate(record.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -347,8 +347,8 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<tr>
|
<tr>
|
||||||
<td>${formatDateTime(record.created_at)}</td>
|
<td>${formatDateTime(record.created_at)}</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>${record.reason}</td>
|
<td>${escapeHtml(record.reason)}</td>
|
||||||
<td>${record.recorder_name}</td>
|
<td>${escapeHtml(record.recorder_name)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
@@ -369,21 +369,9 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderConductPagination() {
|
function renderConductPagination() {
|
||||||
const container = document.getElementById('conductPagination');
|
renderSmartPagination('conductPagination', conductPage, conductTotalPages, function(page) {
|
||||||
if (!container) return;
|
loadConductHistory(page);
|
||||||
if (conductTotalPages <= 1) {
|
});
|
||||||
container.innerHTML = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let html = '';
|
|
||||||
for (let i = 1; i <= conductTotalPages; i++) {
|
|
||||||
if (i === conductPage) {
|
|
||||||
html += `<span class="active">${i}</span>`;
|
|
||||||
} else {
|
|
||||||
html += `<a href="#" onclick="loadConductHistory(${i}); return false;">${i}</a>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
container.innerHTML = html;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载作业
|
// 加载作业
|
||||||
@@ -395,11 +383,11 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
res.data.homework.forEach(hw => {
|
res.data.homework.forEach(hw => {
|
||||||
html += `
|
html += `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${hw.subject_name}</td>
|
<td>${escapeHtml(hw.subject_name)}</td>
|
||||||
<td>${hw.title}</td>
|
<td>${escapeHtml(hw.title)}</td>
|
||||||
<td>${hw.deadline}</td>
|
<td>${escapeHtml(hw.deadline)}</td>
|
||||||
<td>${getStatusBadge(hw.status, 'homework')}</td>
|
<td>${getStatusBadge(hw.status, 'homework')}</td>
|
||||||
<td>${hw.comments || '-'}</td>
|
<td>${escapeHtml(hw.comments || '-')}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
@@ -428,9 +416,9 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
res.data.records.forEach(record => {
|
res.data.records.forEach(record => {
|
||||||
html += `
|
html += `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${record.date}</td>
|
<td>${escapeHtml(record.date)}</td>
|
||||||
<td>${getStatusBadge(record.status, 'attendance')}</td>
|
<td>${getStatusBadge(record.status, 'attendance')}</td>
|
||||||
<td>${record.reason || '-'}</td>
|
<td>${escapeHtml(record.reason || '-')}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user