v0.6测试
This commit is contained in:
@@ -304,3 +304,38 @@
|
|||||||
- dashboard.php 第61行: 快捷操作条件添加劳动委员
|
- dashboard.php 第61行: 快捷操作条件添加劳动委员
|
||||||
- attendance.php 第27行: `/student/conduct.php` → `/student/conduct_history.php`
|
- attendance.php 第27行: `/student/conduct.php` → `/student/conduct_history.php`
|
||||||
- password.php 第26行: `/student/conduct.php` → `/student/conduct_history.php`
|
- password.php 第26行: `/student/conduct.php` → `/student/conduct_history.php`
|
||||||
|
|
||||||
|
- [x] 12.7 同步学生端dashboard.php页眉(使用header.php/footer.php)
|
||||||
|
【目标对象】`frontend/student/dashboard.php`
|
||||||
|
【修改目的】学生端dashboard.php使用自定义HTML结构,与管理端不统一,缺少共享页眉和ICP备案号
|
||||||
|
【修改方式】
|
||||||
|
- 移除自定义的 `<!DOCTYPE html>`, `<head>`, `<body>` 和自定义 header div
|
||||||
|
- 添加 `$page_title = '学生端'; include __DIR__ . '/../includes/header.php';`
|
||||||
|
- 移除重复的 `window.API_BASE_URL` 等变量设置(header.php已处理)
|
||||||
|
- 添加 `include __DIR__ . '/../includes/footer.php';` 以显示ICP备案号
|
||||||
|
|
||||||
|
- [x] 12.8 登录页面补充备案号悬挂
|
||||||
|
【目标对象】`frontend/index.php`
|
||||||
|
【修改目的】登录页面的页脚缺少ICP备案号
|
||||||
|
【修改方式】
|
||||||
|
- 在登录页 footer 的版权信息后,添加ICP备案号链接(使用 `ICP_ENABLED` 和 `ICP_NUMBER` 常量)
|
||||||
|
- 链接指向 https://beian.miit.gov.cn/,target="_blank"
|
||||||
|
|
||||||
|
- [x] 12.9 修复用户报告的4个运行时Bug
|
||||||
|
【目标对象】`backend/schemas/admin.py`、`frontend/admin/conduct.php`、`frontend/admin/homework.php`、`frontend/admin/attendance.php`
|
||||||
|
【修改目的】用户报告:1) 历史记录在加减分后无内容; 2) 管理员无法添加学生; 3) 加减分页面仅显示21人; 4) 管理端修改密码失败
|
||||||
|
【修改方式】
|
||||||
|
- 12.9a 历史记录问题:已在12.3中通过添加 `StudentModel.update_total_points()` 调用修复
|
||||||
|
- 12.9b 添加学生422错误:`add_student` 路由使用裸函数参数而非Pydantic body,FastAPI从query string读取导致422。创建 `AddStudentRequest` schema,路由改用Pydantic body
|
||||||
|
- 12.9c 仅显示21人:`loadStudents()` 未传 `page_size`,后端默认20。前端3个文件添加 `{page_size: 1000}`
|
||||||
|
- 12.9d 修改密码:已确认代码正确(`sha1_md5_password` + `need_change_password=0`)
|
||||||
|
|
||||||
|
- [x] 12.10 修复登录及管理员日志未写入数据库
|
||||||
|
【目标对象】`backend/models/log.py`(新建)、`backend/services/log_service.py`(新建)、`backend/services/auth_service.py`、`backend/routes/auth.py`、`backend/routes/admin.py`
|
||||||
|
【修改目的】数据库中 `login_logs` 和 `operation_logs` 表已存在,但没有任何后端代码写入数据。README中规划了 `models/operation_log.py` 和 `services/log_service.py`,但文件不存在
|
||||||
|
【修改方式】
|
||||||
|
- 新建 `backend/models/log.py`:包含 `LoginLogModel` 和 `OperationLogModel`,分别向 login_logs 和 operation_logs 表写入记录
|
||||||
|
- 新建 `backend/services/log_service.py`:`LogService` 提供 `write_login_log` 和 `write_operation_log` 方法,用 try-except 包裹确保日志写入失败不影响主业务
|
||||||
|
- 修改 `auth_service.py`:login 方法增加 `user_agent` 参数,在5个退出点(失败次数过多、用户不存在、密码错误、账号禁用、登录成功)均写入 login_logs
|
||||||
|
- 修改 `auth.py`:从 HTTP 请求头获取 user-agent 并传递给 AuthService.login()
|
||||||
|
- 修改 `admin.py`:8个管理操作成功后写入 operation_logs(import_students、add_student、add_points、revoke_record、create_assignment、update_submission、add_attendance、add_admin)
|
||||||
|
|||||||
64
backend/models/log.py
Normal file
64
backend/models/log.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# ===========================================
|
||||||
|
# 班级操行分管理系统 - 后端服务
|
||||||
|
#
|
||||||
|
# 开发者: Canglan
|
||||||
|
# 联系方式: admin@sea-studio.top
|
||||||
|
# 版权归属: Sea Network Technology Studio
|
||||||
|
# 许可证: MIT License
|
||||||
|
#
|
||||||
|
# 版权所有 © Sea Network Technology Studio
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
from utils.database import execute_insert
|
||||||
|
from utils.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LoginLogModel:
|
||||||
|
"""登录日志数据模型"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def create(username: str, login_result: int, ip_address: str, user_agent: str = None, fail_reason: str = None) -> int:
|
||||||
|
"""
|
||||||
|
写入登录日志
|
||||||
|
:param username: 用户名
|
||||||
|
:param login_result: 登录结果 (1=成功, 0=失败)
|
||||||
|
:param ip_address: IP地址
|
||||||
|
:param user_agent: 浏览器UA
|
||||||
|
:param fail_reason: 失败原因
|
||||||
|
:return: log_id
|
||||||
|
"""
|
||||||
|
sql = """
|
||||||
|
INSERT INTO login_logs (username, login_result, fail_reason, ip_address, user_agent)
|
||||||
|
VALUES (%s, %s, %s, %s, %s)
|
||||||
|
"""
|
||||||
|
return await execute_insert(sql, (username, login_result, fail_reason, ip_address, user_agent))
|
||||||
|
|
||||||
|
|
||||||
|
class OperationLogModel:
|
||||||
|
"""操作日志数据模型"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def create(operator_id: int, operator_name: str, operator_role: str,
|
||||||
|
operation_type: str, target_type: str = None, target_id: int = None,
|
||||||
|
details: str = None, ip_address: str = None) -> int:
|
||||||
|
"""
|
||||||
|
写入操作日志
|
||||||
|
:param operator_id: 操作者用户ID
|
||||||
|
:param operator_name: 操作者用户名
|
||||||
|
:param operator_role: 操作者角色
|
||||||
|
:param operation_type: 操作类型
|
||||||
|
:param target_type: 目标类型
|
||||||
|
:param target_id: 目标ID
|
||||||
|
:param details: 详细信息
|
||||||
|
:param ip_address: IP地址
|
||||||
|
:return: log_id
|
||||||
|
"""
|
||||||
|
sql = """
|
||||||
|
INSERT INTO operation_logs (operator_id, operator_name, operator_role,
|
||||||
|
operation_type, target_type, target_id, details, ip_address)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
"""
|
||||||
|
return await execute_insert(sql, (operator_id, operator_name, operator_role,
|
||||||
|
operation_type, target_type, target_id, details, ip_address))
|
||||||
@@ -22,8 +22,10 @@ from services.admin_service import AdminService
|
|||||||
from services.conduct_service import ConductService
|
from services.conduct_service import ConductService
|
||||||
from services.homework_service import HomeworkService
|
from services.homework_service import HomeworkService
|
||||||
from services.attendance_service import AttendanceService
|
from services.attendance_service import AttendanceService
|
||||||
|
from services.log_service import LogService
|
||||||
from schemas.admin import (
|
from schemas.admin import (
|
||||||
AddPointsRequest, RevokeRequest, AddAdminRequest,
|
AddPointsRequest, RevokeRequest, AddAdminRequest,
|
||||||
|
AddStudentRequest,
|
||||||
UpdateHomeworkStatusRequest, AddAttendanceRequest
|
UpdateHomeworkStatusRequest, AddAttendanceRequest
|
||||||
)
|
)
|
||||||
from utils.response import success_response, error_response
|
from utils.response import success_response, error_response
|
||||||
@@ -83,16 +85,18 @@ async def import_students(request: Request, file: UploadFile = File(...)):
|
|||||||
operator_id=user["user_id"],
|
operator_id=user["user_id"],
|
||||||
initial_points=60
|
initial_points=60
|
||||||
)
|
)
|
||||||
|
await LogService.write_operation_log(
|
||||||
|
operator_id=user["user_id"], operator_name=user["username"],
|
||||||
|
operator_role="班主任", operation_type="import_students",
|
||||||
|
target_type="student",
|
||||||
|
details=f"批量导入: 成功{result['success_count']}人, 失败{result['failed_count']}人",
|
||||||
|
ip=request.client.host
|
||||||
|
)
|
||||||
return success_response(data=result, message=f"导入完成: 成功{result['success_count']}人,失败{result['failed_count']}人")
|
return success_response(data=result, message=f"导入完成: 成功{result['success_count']}人,失败{result['failed_count']}人")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/students")
|
@router.post("/students")
|
||||||
async def add_student(
|
async def add_student(request: Request, req: AddStudentRequest):
|
||||||
request: Request,
|
|
||||||
student_no: str,
|
|
||||||
name: str,
|
|
||||||
parent_phone: Optional[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"])
|
||||||
@@ -100,13 +104,20 @@ async def add_student(
|
|||||||
return error_response(message="仅班主任可新增学生", code=403)
|
return error_response(message="仅班主任可新增学生", code=403)
|
||||||
|
|
||||||
result = await AdminService.add_student(
|
result = await AdminService.add_student(
|
||||||
student_no=student_no,
|
student_no=req.student_no,
|
||||||
name=name,
|
name=req.name,
|
||||||
parent_phone=parent_phone,
|
parent_phone=req.parent_phone,
|
||||||
operator_id=user["user_id"],
|
operator_id=user["user_id"],
|
||||||
initial_points=60
|
initial_points=60
|
||||||
)
|
)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
|
await LogService.write_operation_log(
|
||||||
|
operator_id=user["user_id"], operator_name=user["username"],
|
||||||
|
operator_role="班主任", operation_type="add_student",
|
||||||
|
target_type="student", target_id=result.get("student_id"),
|
||||||
|
details=f"新增学生: {req.name}({req.student_no})",
|
||||||
|
ip=request.client.host
|
||||||
|
)
|
||||||
return success_response(data=result, message="学生添加成功")
|
return success_response(data=result, message="学生添加成功")
|
||||||
else:
|
else:
|
||||||
return error_response(message=result["message"])
|
return error_response(message=result["message"])
|
||||||
@@ -126,6 +137,14 @@ async def add_conduct_points(request: Request, req: AddPointsRequest):
|
|||||||
recorder_name=user["username"]
|
recorder_name=user["username"]
|
||||||
)
|
)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
|
role = await PermissionChecker.get_user_role(user["user_id"])
|
||||||
|
await LogService.write_operation_log(
|
||||||
|
operator_id=user["user_id"], operator_name=user["username"],
|
||||||
|
operator_role=role, operation_type="add_points",
|
||||||
|
target_type="conduct",
|
||||||
|
details=f"批量加减分: {req.points_change}分, 原因: {req.reason}, 对象: {req.student_ids}",
|
||||||
|
ip=request.client.host
|
||||||
|
)
|
||||||
return success_response(data=result, message="操作成功")
|
return success_response(data=result, message="操作成功")
|
||||||
else:
|
else:
|
||||||
return error_response(message=result["message"])
|
return error_response(message=result["message"])
|
||||||
@@ -140,6 +159,13 @@ async def revoke_conduct_record(request: Request, req: RevokeRequest):
|
|||||||
revoker_id=user["user_id"]
|
revoker_id=user["user_id"]
|
||||||
)
|
)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
|
role = await PermissionChecker.get_user_role(user["user_id"])
|
||||||
|
await LogService.write_operation_log(
|
||||||
|
operator_id=user["user_id"], operator_name=user["username"],
|
||||||
|
operator_role=role, operation_type="revoke_record",
|
||||||
|
target_type="conduct", target_id=req.record_id,
|
||||||
|
ip=request.client.host
|
||||||
|
)
|
||||||
return success_response(message="撤销成功")
|
return success_response(message="撤销成功")
|
||||||
else:
|
else:
|
||||||
return error_response(message=result["message"])
|
return error_response(message=result["message"])
|
||||||
@@ -215,6 +241,13 @@ async def create_assignment(
|
|||||||
created_by=user["user_id"]
|
created_by=user["user_id"]
|
||||||
)
|
)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
|
await LogService.write_operation_log(
|
||||||
|
operator_id=user["user_id"], operator_name=user["username"],
|
||||||
|
operator_role="班主任", operation_type="create_assignment",
|
||||||
|
target_type="homework",
|
||||||
|
details=f"发布作业: {title}",
|
||||||
|
ip=request.client.host
|
||||||
|
)
|
||||||
return success_response(data=result, message="作业发布成功")
|
return success_response(data=result, message="作业发布成功")
|
||||||
else:
|
else:
|
||||||
return error_response(message=result["message"])
|
return error_response(message=result["message"])
|
||||||
@@ -235,6 +268,13 @@ async def update_submission_status(request: Request, req: UpdateHomeworkStatusRe
|
|||||||
operator_id=user["user_id"]
|
operator_id=user["user_id"]
|
||||||
)
|
)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
|
await LogService.write_operation_log(
|
||||||
|
operator_id=user["user_id"], operator_name=user["username"],
|
||||||
|
operator_role=role, operation_type="update_submission",
|
||||||
|
target_type="homework", target_id=req.submission_id,
|
||||||
|
details=f"状态: {req.status}",
|
||||||
|
ip=request.client.host
|
||||||
|
)
|
||||||
return success_response(message="状态更新成功")
|
return success_response(message="状态更新成功")
|
||||||
else:
|
else:
|
||||||
return error_response(message=result["message"])
|
return error_response(message=result["message"])
|
||||||
@@ -258,6 +298,13 @@ async def add_attendance(request: Request, req: AddAttendanceRequest):
|
|||||||
recorder_id=user["user_id"]
|
recorder_id=user["user_id"]
|
||||||
)
|
)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
|
await LogService.write_operation_log(
|
||||||
|
operator_id=user["user_id"], operator_name=user["username"],
|
||||||
|
operator_role=role, operation_type="add_attendance",
|
||||||
|
target_type="attendance",
|
||||||
|
details=f"学生ID: {req.student_id}, 日期: {req.date}, 状态: {req.status}",
|
||||||
|
ip=request.client.host
|
||||||
|
)
|
||||||
return success_response(message="考勤记录添加成功")
|
return success_response(message="考勤记录添加成功")
|
||||||
else:
|
else:
|
||||||
return error_response(message=result["message"])
|
return error_response(message=result["message"])
|
||||||
@@ -298,6 +345,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"]:
|
||||||
|
await LogService.write_operation_log(
|
||||||
|
operator_id=user["user_id"], operator_name=user["username"],
|
||||||
|
operator_role="班主任", operation_type="add_admin",
|
||||||
|
target_type="admin",
|
||||||
|
details=f"新增管理员: {req.real_name}({req.username}), 角色: {req.role_type}",
|
||||||
|
ip=request.client.host
|
||||||
|
)
|
||||||
return success_response(data=result, message="管理员添加成功")
|
return success_response(data=result, message="管理员添加成功")
|
||||||
else:
|
else:
|
||||||
return error_response(message=result["message"])
|
return error_response(message=result["message"])
|
||||||
|
|||||||
@@ -29,11 +29,13 @@ async def login(request: LoginRequest, http_request: Request):
|
|||||||
"""
|
"""
|
||||||
# 获取客户端IP
|
# 获取客户端IP
|
||||||
client_ip = http_request.client.host
|
client_ip = http_request.client.host
|
||||||
|
user_agent = http_request.headers.get("user-agent", "")
|
||||||
|
|
||||||
result = await AuthService.login(
|
result = await AuthService.login(
|
||||||
username=request.username,
|
username=request.username,
|
||||||
password=request.password,
|
password=request.password,
|
||||||
ip=client_ip
|
ip=client_ip,
|
||||||
|
user_agent=user_agent
|
||||||
)
|
)
|
||||||
|
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ class UpdateHomeworkStatusRequest(BaseModel):
|
|||||||
apply_deduction: bool = False
|
apply_deduction: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class AddStudentRequest(BaseModel):
|
||||||
|
"""新增学生请求"""
|
||||||
|
student_no: str = Field(..., min_length=1, max_length=20, description="学号")
|
||||||
|
name: str = Field(..., min_length=1, max_length=50, description="姓名")
|
||||||
|
parent_phone: Optional[str] = Field(None, max_length=11, description="家长手机号")
|
||||||
|
|
||||||
|
|
||||||
class AddAttendanceRequest(BaseModel):
|
class AddAttendanceRequest(BaseModel):
|
||||||
"""添加考勤请求"""
|
"""添加考勤请求"""
|
||||||
student_id: int
|
student_id: int
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from datetime import datetime
|
|||||||
from models.user import UserModel
|
from models.user import UserModel
|
||||||
from models.student import StudentModel
|
from models.student import StudentModel
|
||||||
from models.admin_role import AdminRoleModel
|
from models.admin_role import AdminRoleModel
|
||||||
|
from services.log_service import LogService
|
||||||
from utils.security import security
|
from utils.security import security
|
||||||
from utils.jwt_handler import jwt_handler
|
from utils.jwt_handler import jwt_handler
|
||||||
from utils.redis_client import RedisClient
|
from utils.redis_client import RedisClient
|
||||||
@@ -28,13 +29,14 @@ class AuthService:
|
|||||||
"""认证服务"""
|
"""认证服务"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def login(username: str, password: str, ip: str) -> Dict[str, Any]:
|
async def login(username: str, password: str, ip: str, user_agent: str = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
用户登录
|
用户登录
|
||||||
"""
|
"""
|
||||||
# 检查登录失败次数
|
# 检查登录失败次数
|
||||||
attempts = await RedisClient.get(f"login_attempts:{username}")
|
attempts = await RedisClient.get(f"login_attempts:{username}")
|
||||||
if attempts and int(attempts) >= 5:
|
if attempts and int(attempts) >= 5:
|
||||||
|
await LogService.write_login_log(username, 0, ip, user_agent, "登录失败次数过多")
|
||||||
return {"success": False, "message": "登录失败次数过多,请15分钟后重试"}
|
return {"success": False, "message": "登录失败次数过多,请15分钟后重试"}
|
||||||
|
|
||||||
# 获取用户信息
|
# 获取用户信息
|
||||||
@@ -42,15 +44,18 @@ class AuthService:
|
|||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
await RedisClient.set_login_attempts(username)
|
await RedisClient.set_login_attempts(username)
|
||||||
|
await LogService.write_login_log(username, 0, ip, user_agent, "用户名或密码错误")
|
||||||
return {"success": False, "message": "用户名或密码错误"}
|
return {"success": False, "message": "用户名或密码错误"}
|
||||||
|
|
||||||
# 验证密码
|
# 验证密码
|
||||||
if not security.verify_password(password, user["password_hash"]):
|
if not security.verify_password(password, user["password_hash"]):
|
||||||
await RedisClient.set_login_attempts(username)
|
await RedisClient.set_login_attempts(username)
|
||||||
|
await LogService.write_login_log(username, 0, ip, user_agent, "用户名或密码错误")
|
||||||
return {"success": False, "message": "用户名或密码错误"}
|
return {"success": False, "message": "用户名或密码错误"}
|
||||||
|
|
||||||
# 检查账号状态
|
# 检查账号状态
|
||||||
if user["status"] != 1:
|
if user["status"] != 1:
|
||||||
|
await LogService.write_login_log(username, 0, ip, user_agent, "账号已被禁用")
|
||||||
return {"success": False, "message": "账号已被禁用"}
|
return {"success": False, "message": "账号已被禁用"}
|
||||||
|
|
||||||
# 清除登录失败记录
|
# 清除登录失败记录
|
||||||
@@ -80,6 +85,8 @@ class AuthService:
|
|||||||
# 确定跳转路径
|
# 确定跳转路径
|
||||||
redirect = AuthService._get_redirect_path(user["user_type"], role)
|
redirect = AuthService._get_redirect_path(user["user_type"], role)
|
||||||
|
|
||||||
|
await LogService.write_login_log(username, 1, ip, user_agent)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"token": token,
|
"token": token,
|
||||||
|
|||||||
57
backend/services/log_service.py
Normal file
57
backend/services/log_service.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# ===========================================
|
||||||
|
# 班级操行分管理系统 - 后端服务
|
||||||
|
#
|
||||||
|
# 开发者: Canglan
|
||||||
|
# 联系方式: admin@sea-studio.top
|
||||||
|
# 版权归属: Sea Network Technology Studio
|
||||||
|
# 许可证: MIT License
|
||||||
|
#
|
||||||
|
# 版权所有 © Sea Network Technology Studio
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
from models.log import LoginLogModel, OperationLogModel
|
||||||
|
from middleware.permission import PermissionChecker
|
||||||
|
from utils.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LogService:
|
||||||
|
"""日志服务"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def write_login_log(username: str, login_result: int, ip: str, user_agent: str = None, fail_reason: str = None):
|
||||||
|
"""
|
||||||
|
写入登录日志(异步,不阻塞主流程)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await LoginLogModel.create(
|
||||||
|
username=username,
|
||||||
|
login_result=login_result,
|
||||||
|
ip_address=ip,
|
||||||
|
user_agent=user_agent,
|
||||||
|
fail_reason=fail_reason
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"写入登录日志失败: {e}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def write_operation_log(operator_id: int, operator_name: str, operator_role: str,
|
||||||
|
operation_type: str, target_type: str = None,
|
||||||
|
target_id: int = None, details: str = None, ip: str = None):
|
||||||
|
"""
|
||||||
|
写入操作日志(异步,不阻塞主流程)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await OperationLogModel.create(
|
||||||
|
operator_id=operator_id,
|
||||||
|
operator_name=operator_name,
|
||||||
|
operator_role=operator_role,
|
||||||
|
operation_type=operation_type,
|
||||||
|
target_type=target_type,
|
||||||
|
target_id=target_id,
|
||||||
|
details=details,
|
||||||
|
ip_address=ip
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"写入操作日志失败: {e}")
|
||||||
@@ -1 +1 @@
|
|||||||
# 管理端使用文档
|
# 管理端使用文档
|
||||||
@@ -97,7 +97,7 @@ function selectStatus(btn) {
|
|||||||
|
|
||||||
// 加载学生列表
|
// 加载学生列表
|
||||||
async function loadStudents() {
|
async function loadStudents() {
|
||||||
const res = await apiGet('/api/admin/students');
|
const res = await apiGet('/api/admin/students', {page_size: 1000});
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
studentsData = res.data.students;
|
studentsData = res.data.students;
|
||||||
renderStudentGrid();
|
renderStudentGrid();
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
var selectedStudentIds = [];
|
var selectedStudentIds = [];
|
||||||
|
|
||||||
async function loadStudents() {
|
async function loadStudents() {
|
||||||
const res = await apiGet('/api/admin/students');
|
const res = await apiGet('/api/admin/students', {page_size: 1000});
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
let html = '';
|
let html = '';
|
||||||
res.data.students.forEach(student => {
|
res.data.students.forEach(student => {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ document.getElementById('pointsChange').setAttribute('min', -hwMaxPoints);
|
|||||||
document.getElementById('pointsChange').setAttribute('max', hwMaxPoints);
|
document.getElementById('pointsChange').setAttribute('max', hwMaxPoints);
|
||||||
|
|
||||||
async function loadStudents() {
|
async function loadStudents() {
|
||||||
const res = await apiGet('/api/admin/students');
|
const res = await apiGet('/api/admin/students', {page_size: 1000});
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
let html = '';
|
let html = '';
|
||||||
res.data.students.forEach(student => {
|
res.data.students.forEach(student => {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ if (isset($_SESSION['user_id']) && isset($_SESSION['user_type'])) {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="login-footer">
|
<div class="login-footer">
|
||||||
<p>© Sea Network Technology Studio</p>
|
<p>© <?php echo date('Y'); ?> Sea Network Technology Studio<?php if (defined('ICP_ENABLED') && ICP_ENABLED && defined('ICP_NUMBER') && ICP_NUMBER): ?> | <a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer"><?php echo htmlspecialchars(ICP_NUMBER); ?></a><?php endif; ?></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -18,500 +18,481 @@ if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$page_title = '学生端';
|
||||||
$student_id = $_SESSION['student_id'];
|
$student_id = $_SESSION['student_id'];
|
||||||
|
include __DIR__ . '/../includes/header.php';
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title><?php echo SITE_NAME; ?> - 学生端</title>
|
|
||||||
<link rel="stylesheet" href="/assets/css/style.css">
|
|
||||||
<style>
|
|
||||||
.conduct-score {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.score-number {
|
|
||||||
font-size: 64px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
.score-label {
|
|
||||||
color: #666;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
.record-item {
|
|
||||||
padding: 12px 0;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.record-points {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.record-points.plus {
|
|
||||||
color: #38a169;
|
|
||||||
}
|
|
||||||
.record-points.minus {
|
|
||||||
color: #e53e3e;
|
|
||||||
}
|
|
||||||
.record-reason {
|
|
||||||
flex: 1;
|
|
||||||
margin: 0 15px;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
.record-time {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
.view-more {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
.view-more a {
|
|
||||||
color: #667eea;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="header">
|
|
||||||
<h1><?php echo SITE_NAME; ?> - 学生端</h1>
|
|
||||||
<div class="header-info">
|
|
||||||
<span class="user-name" id="userName"></span>
|
|
||||||
<button class="btn-logout" id="logoutBtn">退出登录</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav">
|
<style>
|
||||||
<button class="nav-item active" data-page="dashboard">首页</button>
|
.conduct-score {
|
||||||
<button class="nav-item" data-page="conduct">操行分详情</button>
|
text-align: center;
|
||||||
<button class="nav-item" data-page="homework">作业情况</button>
|
padding: 20px;
|
||||||
<button class="nav-item" data-page="attendance">考勤记录</button>
|
}
|
||||||
<button class="nav-item" data-page="password">修改密码</button>
|
.score-number {
|
||||||
</div>
|
font-size: 64px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
.score-label {
|
||||||
|
color: #666;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.record-item {
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.record-points {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.record-points.plus {
|
||||||
|
color: #38a169;
|
||||||
|
}
|
||||||
|
.record-points.minus {
|
||||||
|
color: #e53e3e;
|
||||||
|
}
|
||||||
|
.record-reason {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 15px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.record-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.view-more {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.view-more a {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div class="container" id="pageContainer">
|
<div class="nav">
|
||||||
<!-- 首页内容 -->
|
<button class="nav-item active" data-page="dashboard">首页</button>
|
||||||
<div id="page-dashboard" class="page-content">
|
<button class="nav-item" data-page="conduct">操行分详情</button>
|
||||||
<div class="stats-grid">
|
<button class="nav-item" data-page="homework">作业情况</button>
|
||||||
<div class="stat-card">
|
<button class="nav-item" data-page="attendance">考勤记录</button>
|
||||||
<div class="stat-label">当前操行分</div>
|
<button class="nav-item" data-page="password">修改密码</button>
|
||||||
<div class="stat-value" id="totalPoints">--</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
<div class="container" id="pageContainer">
|
||||||
<div class="stat-label">作业完成率</div>
|
<!-- 首页内容 -->
|
||||||
<div class="stat-value" id="homeworkRate">--%</div>
|
<div id="page-dashboard" class="page-content">
|
||||||
</div>
|
<div class="stats-grid">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">本月出勤率</div>
|
<div class="stat-label">当前操行分</div>
|
||||||
<div class="stat-value" id="attendanceRate">--%</div>
|
<div class="stat-value" id="totalPoints">--</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
<div class="card">
|
<div class="stat-label">作业完成率</div>
|
||||||
<div class="card-title">最新操行分记录</div>
|
<div class="stat-value" id="homeworkRate">--%</div>
|
||||||
<div id="recentRecords"></div>
|
</div>
|
||||||
<div class="view-more">
|
<div class="stat-card">
|
||||||
<a href="#" onclick="showPage('conduct'); return false;">查看更多 ></a>
|
<div class="stat-label">本月出勤率</div>
|
||||||
</div>
|
<div class="stat-value" id="attendanceRate">--%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 操行分详情页 -->
|
<div class="card">
|
||||||
<div id="page-conduct" class="page-content" style="display: none;">
|
<div class="card-title">最新操行分记录</div>
|
||||||
<div class="card">
|
<div id="recentRecords"></div>
|
||||||
<div class="conduct-score">
|
<div class="view-more">
|
||||||
<div class="score-number" id="conductTotalPoints">--</div>
|
<a href="#" onclick="showPage('conduct'); return false;">查看更多 ></a>
|
||||||
<div class="score-label">当前操行分</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-title">历史记录</div>
|
|
||||||
<div id="conductRecords"></div>
|
|
||||||
<div class="pagination" id="conductPagination"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 作业情况页 -->
|
|
||||||
<div id="page-homework" class="page-content" style="display: none;">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-title">作业列表</div>
|
|
||||||
<div class="table-wrapper">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>科目</th>
|
|
||||||
<th>作业标题</th>
|
|
||||||
<th>截止日期</th>
|
|
||||||
<th>状态</th>
|
|
||||||
<th>备注</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="homeworkList"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 考勤记录页 -->
|
|
||||||
<div id="page-attendance" class="page-content" style="display: none;">
|
|
||||||
<div class="stats-grid">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">出勤</div>
|
|
||||||
<div class="stat-value" id="attPresent">0</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">缺勤</div>
|
|
||||||
<div class="stat-value" id="attAbsent">0</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">迟到</div>
|
|
||||||
<div class="stat-value" id="attLate">0</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">请假</div>
|
|
||||||
<div class="stat-value" id="attLeave">0</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-title">考勤记录明细</div>
|
|
||||||
<div class="table-wrapper">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>日期</th>
|
|
||||||
<th>状态</th>
|
|
||||||
<th>原因</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="attendanceList"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 修改密码页 -->
|
|
||||||
<div id="page-password" class="page-content" style="display: none;">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-title">修改密码</div>
|
|
||||||
<form id="passwordForm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>原密码</label>
|
|
||||||
<input type="password" id="oldPassword" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>新密码</label>
|
|
||||||
<input type="password" id="newPassword" required>
|
|
||||||
<small>密码长度6-20位,需包含字母和数字</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>确认新密码</label>
|
|
||||||
<input type="password" id="confirmPassword" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">确认修改</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 修改密码模态框(首次登录强制) -->
|
<!-- 操行分详情页 -->
|
||||||
<div id="forceChangePasswordModal" class="modal" style="display: none;">
|
<div id="page-conduct" class="page-content" style="display: none;">
|
||||||
<div class="modal-content">
|
<div class="card">
|
||||||
<div class="modal-header">
|
<div class="conduct-score">
|
||||||
<h3>首次登录,请修改密码</h3>
|
<div class="score-number" id="conductTotalPoints">--</div>
|
||||||
|
<div class="score-label">当前操行分</div>
|
||||||
</div>
|
</div>
|
||||||
<form id="forcePasswordForm">
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">历史记录</div>
|
||||||
|
<div id="conductRecords"></div>
|
||||||
|
<div class="pagination" id="conductPagination"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 作业情况页 -->
|
||||||
|
<div id="page-homework" class="page-content" style="display: none;">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">作业列表</div>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>科目</th>
|
||||||
|
<th>作业标题</th>
|
||||||
|
<th>截止日期</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>备注</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="homeworkList"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 考勤记录页 -->
|
||||||
|
<div id="page-attendance" class="page-content" style="display: none;">
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">出勤</div>
|
||||||
|
<div class="stat-value" id="attPresent">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">缺勤</div>
|
||||||
|
<div class="stat-value" id="attAbsent">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">迟到</div>
|
||||||
|
<div class="stat-value" id="attLate">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">请假</div>
|
||||||
|
<div class="stat-value" id="attLeave">0</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">考勤记录明细</div>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>日期</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>原因</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="attendanceList"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改密码页 -->
|
||||||
|
<div id="page-password" class="page-content" style="display: none;">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">修改密码</div>
|
||||||
|
<form id="passwordForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>原密码</label>
|
||||||
|
<input type="password" id="oldPassword" required>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>新密码</label>
|
<label>新密码</label>
|
||||||
<input type="password" id="forceNewPassword" required>
|
<input type="password" id="newPassword" required>
|
||||||
<small>密码长度6-20位,需包含字母和数字</small>
|
<small>密码长度6-20位,需包含字母和数字</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>确认新密码</label>
|
<label>确认新密码</label>
|
||||||
<input type="password" id="forceConfirmPassword" required>
|
<input type="password" id="confirmPassword" required>
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="submit" class="btn btn-primary">确认修改</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">确认修改</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<!-- 修改密码模态框(首次登录强制) -->
|
||||||
window.API_BASE_URL = '<?php echo API_BASE_URL; ?>';
|
<div id="forceChangePasswordModal" class="modal" style="display: none;">
|
||||||
window.JWT_STORAGE_KEY = '<?php echo JWT_STORAGE_KEY; ?>';
|
<div class="modal-content">
|
||||||
window.USER_STORAGE_KEY = '<?php echo USER_STORAGE_KEY; ?>';
|
<div class="modal-header">
|
||||||
</script>
|
<h3>首次登录,请修改密码</h3>
|
||||||
<script src="/assets/js/common.js"></script>
|
</div>
|
||||||
<script>
|
<form id="forcePasswordForm">
|
||||||
const STUDENT_ID = <?php echo $student_id; ?>;
|
<div class="form-group">
|
||||||
|
<label>新密码</label>
|
||||||
|
<input type="password" id="forceNewPassword" required>
|
||||||
|
<small>密码长度6-20位,需包含字母和数字</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>确认新密码</label>
|
||||||
|
<input type="password" id="forceConfirmPassword" required>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" class="btn btn-primary">确认修改</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const STUDENT_ID = <?php echo $student_id; ?>;
|
||||||
|
|
||||||
|
let conductPage = 1;
|
||||||
|
let conductTotalPages = 1;
|
||||||
|
|
||||||
|
// 页面切换
|
||||||
|
function showPage(pageName) {
|
||||||
|
document.querySelectorAll('.page-content').forEach(page => {
|
||||||
|
page.style.display = 'none';
|
||||||
|
});
|
||||||
|
document.getElementById(`page-${pageName}`).style.display = 'block';
|
||||||
|
|
||||||
let conductPage = 1;
|
document.querySelectorAll('.nav-item').forEach(item => {
|
||||||
let conductTotalPages = 1;
|
item.classList.remove('active');
|
||||||
|
if (item.dataset.page === pageName) {
|
||||||
// 页面切换
|
item.classList.add('active');
|
||||||
function showPage(pageName) {
|
|
||||||
document.querySelectorAll('.page-content').forEach(page => {
|
|
||||||
page.style.display = 'none';
|
|
||||||
});
|
|
||||||
document.getElementById(`page-${pageName}`).style.display = 'block';
|
|
||||||
|
|
||||||
document.querySelectorAll('.nav-item').forEach(item => {
|
|
||||||
item.classList.remove('active');
|
|
||||||
if (item.dataset.page === pageName) {
|
|
||||||
item.classList.add('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 加载对应页面数据
|
|
||||||
switch(pageName) {
|
|
||||||
case 'dashboard':
|
|
||||||
loadDashboard();
|
|
||||||
break;
|
|
||||||
case 'conduct':
|
|
||||||
loadConductHistory();
|
|
||||||
break;
|
|
||||||
case 'homework':
|
|
||||||
loadHomework();
|
|
||||||
break;
|
|
||||||
case 'attendance':
|
|
||||||
loadAttendance();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// 加载首页
|
// 加载对应页面数据
|
||||||
async function loadDashboard() {
|
switch(pageName) {
|
||||||
try {
|
case 'dashboard':
|
||||||
// 获取操行分
|
loadDashboard();
|
||||||
const conductRes = await apiGet(`/api/student/conduct/${STUDENT_ID}`);
|
break;
|
||||||
if (conductRes && conductRes.success) {
|
case 'conduct':
|
||||||
document.getElementById('totalPoints').textContent = conductRes.data.total_points;
|
loadConductHistory();
|
||||||
|
break;
|
||||||
// 显示最近5条记录
|
case 'homework':
|
||||||
const records = conductRes.data.records.slice(0, 5);
|
loadHomework();
|
||||||
let html = '';
|
break;
|
||||||
records.forEach(record => {
|
case 'attendance':
|
||||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
loadAttendance();
|
||||||
html += `
|
break;
|
||||||
<div class="record-item">
|
|
||||||
<span class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</span>
|
|
||||||
<span class="record-reason">${record.reason}</span>
|
|
||||||
<span class="record-time">${formatDate(record.created_at)}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
if (records.length === 0) {
|
|
||||||
html = '<div style="text-align:center;padding:20px;color:#999;">暂无记录</div>';
|
|
||||||
}
|
|
||||||
document.getElementById('recentRecords').innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取作业统计
|
|
||||||
const homeworkRes = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
|
||||||
if (homeworkRes && homeworkRes.success) {
|
|
||||||
const stats = homeworkRes.data.statistics;
|
|
||||||
const rate = stats.total > 0 ? Math.round(stats.submitted / stats.total * 100) : 0;
|
|
||||||
document.getElementById('homeworkRate').textContent = `${rate}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取考勤统计
|
|
||||||
const attendanceRes = await apiGet(`/api/student/attendance/${STUDENT_ID}`);
|
|
||||||
if (attendanceRes && attendanceRes.success) {
|
|
||||||
const stats = attendanceRes.data.statistics;
|
|
||||||
const total = stats.present + stats.absent + stats.late + stats.leave;
|
|
||||||
const rate = total > 0 ? Math.round(stats.present / total * 100) : 100;
|
|
||||||
document.getElementById('attendanceRate').textContent = `${rate}%`;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载首页失败:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 加载操行分历史
|
|
||||||
async function loadConductHistory(page = 1) {
|
// 加载首页
|
||||||
conductPage = page;
|
async function loadDashboard() {
|
||||||
try {
|
try {
|
||||||
const res = await apiGet(`/api/student/conduct/${STUDENT_ID}`, {
|
// 获取操行分
|
||||||
limit: 20,
|
const conductRes = await apiGet(`/api/student/conduct/${STUDENT_ID}`);
|
||||||
offset: (page - 1) * 20
|
if (conductRes && conductRes.success) {
|
||||||
|
document.getElementById('totalPoints').textContent = conductRes.data.total_points;
|
||||||
|
|
||||||
|
// 显示最近5条记录
|
||||||
|
const records = conductRes.data.records.slice(0, 5);
|
||||||
|
let html = '';
|
||||||
|
records.forEach(record => {
|
||||||
|
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||||
|
html += `
|
||||||
|
<div class="record-item">
|
||||||
|
<span class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</span>
|
||||||
|
<span class="record-reason">${record.reason}</span>
|
||||||
|
<span class="record-time">${formatDate(record.created_at)}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
});
|
});
|
||||||
if (res && res.success) {
|
if (records.length === 0) {
|
||||||
document.getElementById('conductTotalPoints').textContent = res.data.total_points;
|
html = '<div style="text-align:center;padding:20px;color:#999;">暂无记录</div>';
|
||||||
|
|
||||||
let html = '<div class="table-wrapper"><table><thead><tr><th>时间</th><th>分数变动</th><th>原因</th><th>操作人</th></tr></thead><tbody>';
|
|
||||||
res.data.records.forEach(record => {
|
|
||||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
|
||||||
html += `
|
|
||||||
<tr>
|
|
||||||
<td>${formatDateTime(record.created_at)}</td>
|
|
||||||
<td class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
|
||||||
<td>${record.reason}</td>
|
|
||||||
<td>${record.recorder_name}</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
if (res.data.records.length === 0) {
|
|
||||||
html += '<tr><td colspan="4" style="text-align:center;">暂无记录</td></tr>';
|
|
||||||
}
|
|
||||||
html += '</tbody></table></div>';
|
|
||||||
document.getElementById('conductRecords').innerHTML = html;
|
|
||||||
|
|
||||||
// 分页
|
|
||||||
const total = res.data.total || res.data.records.length;
|
|
||||||
conductTotalPages = Math.ceil(total / 20);
|
|
||||||
renderConductPagination();
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
document.getElementById('recentRecords').innerHTML = html;
|
||||||
console.error('加载操行分历史失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderConductPagination() {
|
|
||||||
const container = document.getElementById('conductPagination');
|
|
||||||
if (!container) return;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载作业
|
|
||||||
async function loadHomework() {
|
|
||||||
try {
|
|
||||||
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
|
||||||
if (res && res.success) {
|
|
||||||
let html = '';
|
|
||||||
res.data.homework.forEach(hw => {
|
|
||||||
html += `
|
|
||||||
<tr>
|
|
||||||
<td>${hw.subject_name}</td>
|
|
||||||
<td>${hw.title}</td>
|
|
||||||
<td>${hw.deadline}</td>
|
|
||||||
<td>${getStatusBadge(hw.status, 'homework')}</td>
|
|
||||||
<td>${hw.comments || '-'}</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
if (res.data.homework.length === 0) {
|
|
||||||
html = '<tr><td colspan="5" style="text-align:center;">暂无作业</td></tr>';
|
|
||||||
}
|
|
||||||
document.getElementById('homeworkList').innerHTML = html;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载作业失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载考勤
|
|
||||||
async function loadAttendance() {
|
|
||||||
try {
|
|
||||||
const res = await apiGet(`/api/student/attendance/${STUDENT_ID}`);
|
|
||||||
if (res && res.success) {
|
|
||||||
const stats = res.data.statistics;
|
|
||||||
document.getElementById('attPresent').textContent = stats.present || 0;
|
|
||||||
document.getElementById('attAbsent').textContent = stats.absent || 0;
|
|
||||||
document.getElementById('attLate').textContent = stats.late || 0;
|
|
||||||
document.getElementById('attLeave').textContent = stats.leave || 0;
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
res.data.records.forEach(record => {
|
|
||||||
html += `
|
|
||||||
<tr>
|
|
||||||
<td>${record.date}</td>
|
|
||||||
<td>${getStatusBadge(record.status, 'attendance')}</td>
|
|
||||||
<td>${record.reason || '-'}</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
if (res.data.records.length === 0) {
|
|
||||||
html = '<tr><td colspan="3" style="text-align:center;">暂无考勤记录</td></tr>';
|
|
||||||
}
|
|
||||||
document.getElementById('attendanceList').innerHTML = html;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载考勤失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改密码
|
|
||||||
document.getElementById('passwordForm')?.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const oldPassword = document.getElementById('oldPassword').value;
|
|
||||||
const newPassword = document.getElementById('newPassword').value;
|
|
||||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
|
||||||
|
|
||||||
if (newPassword !== confirmPassword) {
|
|
||||||
showToast('两次输入的新密码不一致', 'error');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await apiPost('/api/auth/change-password', {
|
// 获取作业统计
|
||||||
old_password: oldPassword,
|
const homeworkRes = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
||||||
new_password: newPassword
|
if (homeworkRes && homeworkRes.success) {
|
||||||
|
const stats = homeworkRes.data.statistics;
|
||||||
|
const rate = stats.total > 0 ? Math.round(stats.submitted / stats.total * 100) : 0;
|
||||||
|
document.getElementById('homeworkRate').textContent = `${rate}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取考勤统计
|
||||||
|
const attendanceRes = await apiGet(`/api/student/attendance/${STUDENT_ID}`);
|
||||||
|
if (attendanceRes && attendanceRes.success) {
|
||||||
|
const stats = attendanceRes.data.statistics;
|
||||||
|
const total = stats.present + stats.absent + stats.late + stats.leave;
|
||||||
|
const rate = total > 0 ? Math.round(stats.present / total * 100) : 100;
|
||||||
|
document.getElementById('attendanceRate').textContent = `${rate}%`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载首页失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载操行分历史
|
||||||
|
async function loadConductHistory(page = 1) {
|
||||||
|
conductPage = page;
|
||||||
|
try {
|
||||||
|
const res = await apiGet(`/api/student/conduct/${STUDENT_ID}`, {
|
||||||
|
limit: 20,
|
||||||
|
offset: (page - 1) * 20
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
showToast('密码修改成功,请重新登录');
|
document.getElementById('conductTotalPoints').textContent = res.data.total_points;
|
||||||
setTimeout(() => logout(), 1500);
|
|
||||||
|
let html = '<div class="table-wrapper"><table><thead><tr><th>时间</th><th>分数变动</th><th>原因</th><th>操作人</th></tr></thead><tbody>';
|
||||||
|
res.data.records.forEach(record => {
|
||||||
|
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td>${formatDateTime(record.created_at)}</td>
|
||||||
|
<td class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
||||||
|
<td>${record.reason}</td>
|
||||||
|
<td>${record.recorder_name}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
if (res.data.records.length === 0) {
|
||||||
|
html += '<tr><td colspan="4" style="text-align:center;">暂无记录</td></tr>';
|
||||||
|
}
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
document.getElementById('conductRecords').innerHTML = html;
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const total = res.data.total || res.data.records.length;
|
||||||
|
conductTotalPages = Math.ceil(total / 20);
|
||||||
|
renderConductPagination();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载操行分历史失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderConductPagination() {
|
||||||
|
const container = document.getElementById('conductPagination');
|
||||||
|
if (!container) return;
|
||||||
|
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 {
|
} else {
|
||||||
showToast(res?.message || '密码修改失败', 'error');
|
html += `<a href="#" onclick="loadConductHistory(${i}); return false;">${i}</a>`;
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 强制修改密码
|
|
||||||
document.getElementById('forcePasswordForm')?.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const newPassword = document.getElementById('forceNewPassword').value;
|
|
||||||
const confirmPassword = document.getElementById('forceConfirmPassword').value;
|
|
||||||
|
|
||||||
if (newPassword !== confirmPassword) {
|
|
||||||
alert('两次输入的新密码不一致');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await apiPost('/api/auth/change-password', {
|
|
||||||
old_password: newPassword,
|
|
||||||
new_password: newPassword
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res && res.success) {
|
|
||||||
showToast('密码修改成功,请重新登录');
|
|
||||||
setTimeout(() => logout(), 1500);
|
|
||||||
} else {
|
|
||||||
alert(res?.message || '密码修改失败');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查是否需要强制修改密码
|
|
||||||
function checkForceChangePassword() {
|
|
||||||
const user = getUserInfo();
|
|
||||||
if (user && user.need_change_password) {
|
|
||||||
document.getElementById('forceChangePasswordModal').style.display = 'flex';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载作业
|
||||||
|
async function loadHomework() {
|
||||||
|
try {
|
||||||
|
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
||||||
|
if (res && res.success) {
|
||||||
|
let html = '';
|
||||||
|
res.data.homework.forEach(hw => {
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td>${hw.subject_name}</td>
|
||||||
|
<td>${hw.title}</td>
|
||||||
|
<td>${hw.deadline}</td>
|
||||||
|
<td>${getStatusBadge(hw.status, 'homework')}</td>
|
||||||
|
<td>${hw.comments || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
if (res.data.homework.length === 0) {
|
||||||
|
html = '<tr><td colspan="5" style="text-align:center;">暂无作业</td></tr>';
|
||||||
|
}
|
||||||
|
document.getElementById('homeworkList').innerHTML = html;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载作业失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载考勤
|
||||||
|
async function loadAttendance() {
|
||||||
|
try {
|
||||||
|
const res = await apiGet(`/api/student/attendance/${STUDENT_ID}`);
|
||||||
|
if (res && res.success) {
|
||||||
|
const stats = res.data.statistics;
|
||||||
|
document.getElementById('attPresent').textContent = stats.present || 0;
|
||||||
|
document.getElementById('attAbsent').textContent = stats.absent || 0;
|
||||||
|
document.getElementById('attLate').textContent = stats.late || 0;
|
||||||
|
document.getElementById('attLeave').textContent = stats.leave || 0;
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
res.data.records.forEach(record => {
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td>${record.date}</td>
|
||||||
|
<td>${getStatusBadge(record.status, 'attendance')}</td>
|
||||||
|
<td>${record.reason || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
if (res.data.records.length === 0) {
|
||||||
|
html = '<tr><td colspan="3" style="text-align:center;">暂无考勤记录</td></tr>';
|
||||||
|
}
|
||||||
|
document.getElementById('attendanceList').innerHTML = html;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载考勤失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
document.getElementById('passwordForm')?.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const oldPassword = document.getElementById('oldPassword').value;
|
||||||
|
const newPassword = document.getElementById('newPassword').value;
|
||||||
|
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||||
|
|
||||||
// 初始化
|
if (newPassword !== confirmPassword) {
|
||||||
document.querySelectorAll('.nav-item').forEach(btn => {
|
showToast('两次输入的新密码不一致', 'error');
|
||||||
btn.addEventListener('click', () => {
|
return;
|
||||||
showPage(btn.dataset.page);
|
}
|
||||||
});
|
|
||||||
|
const res = await apiPost('/api/auth/change-password', {
|
||||||
|
old_password: oldPassword,
|
||||||
|
new_password: newPassword
|
||||||
});
|
});
|
||||||
|
|
||||||
loadDashboard();
|
if (res && res.success) {
|
||||||
checkForceChangePassword();
|
showToast('密码修改成功,请重新登录');
|
||||||
</script>
|
setTimeout(() => logout(), 1500);
|
||||||
</body>
|
} else {
|
||||||
</html>
|
showToast(res?.message || '密码修改失败', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 强制修改密码
|
||||||
|
document.getElementById('forcePasswordForm')?.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const newPassword = document.getElementById('forceNewPassword').value;
|
||||||
|
const confirmPassword = document.getElementById('forceConfirmPassword').value;
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
alert('两次输入的新密码不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await apiPost('/api/auth/change-password', {
|
||||||
|
old_password: newPassword,
|
||||||
|
new_password: newPassword
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.success) {
|
||||||
|
showToast('密码修改成功,请重新登录');
|
||||||
|
setTimeout(() => logout(), 1500);
|
||||||
|
} else {
|
||||||
|
alert(res?.message || '密码修改失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查是否需要强制修改密码
|
||||||
|
function checkForceChangePassword() {
|
||||||
|
const user = getUserInfo();
|
||||||
|
if (user && user.need_change_password) {
|
||||||
|
document.getElementById('forceChangePasswordModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
document.querySelectorAll('.nav-item').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
showPage(btn.dataset.page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
loadDashboard();
|
||||||
|
checkForceChangePassword();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user