diff --git a/README.md b/README.md index 7863e13..4ec343c 100644 --- a/README.md +++ b/README.md @@ -267,8 +267,8 @@ classmanager/ | v1.3 | 2026.4.27 | 考勤时段系统(早上/中午/晚修三时段)、历史记录扣分类型筛选、管理员/科目信息编辑、全链路输入安全校验 | | v1.4 | 2026.4.28 | 全量代码审查修复:双重密码哈希bug、学生端XSS漏洞、性能优化、Pydantic schema统一、权限检查补全、考勤委员撤销权限 | | v1.5 | 2026.4.29 | 登录错误封禁5分钟+手动解锁、加减分回显修复、权限限制修复、按钮样式补全 | -| v1.5.1 | 2026.4.29 | 权限修复:考勤委员提示遗漏、历史记录权限泄露、时间筛选失效、作业页分数限制与后端同步 | -| v1.6 | 2026.5.12 | 全量一致性审计:前后端配置统一(.env.example/config.py/config.php)、清理废弃全局变量、角色权限表精确化 | +| v1.6 | 2026.4.29 | 权限修复:考勤委员提示遗漏、历史记录权限泄露、时间筛选失效、作业页分数限制与后端同步 | +| v1.7 | 2026.5.21 | 全量一致性审计:前后端配置统一(.env.example/config.py/config.php)、清理废弃全局变量、角色权限表精确化 | ## 许可证 diff --git a/backend/.env.example b/backend/.env.example index 66cf913..28b49af 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -54,6 +54,8 @@ REDIS_MAX_CONNECTIONS=50 JWT_SECRET_KEY=your-jwt-secret-key-min-32-chars JWT_ALGORITHM=HS256 JWT_EXPIRE_MINUTES=30 +# JWT空闲超时时间(分钟)- 无操作超过此时间需重新登录 +JWT_IDLE_TIMEOUT_MINUTES=10 # =========================================== # 密码加密配置 diff --git a/backend/main.py b/backend/main.py index eea76e8..d23f649 100644 --- a/backend/main.py +++ b/backend/main.py @@ -22,6 +22,7 @@ from utils.database import init_db_pool, close_db_pool from utils.redis_client import init_redis_pool, close_redis_pool from middleware.auth_middleware import AuthMiddleware from routes import auth, student, parent, admin, subject, semester, debug +from routes.config import router as config_router # 设置日志 @@ -117,6 +118,7 @@ app.include_router(parent.router, prefix="/api/parent", tags=["家长端"]) app.include_router(admin.router, prefix="/api/admin", tags=["管理端"]) app.include_router(subject.router, prefix="/api/subject", tags=["科目管理"]) app.include_router(semester.router, prefix="/api/semester", tags=["学期管理"]) +app.include_router(config_router, prefix="/api/config", tags=["配置"]) app.include_router(debug.router, tags=["调试"]) diff --git a/backend/middleware/auth_middleware.py b/backend/middleware/auth_middleware.py index accd804..5e3cce7 100644 --- a/backend/middleware/auth_middleware.py +++ b/backend/middleware/auth_middleware.py @@ -30,6 +30,7 @@ PUBLIC_PATHS = [ r'^/health$', r'^/api/auth/login$', r'^/api/auth/logout$', + r'^/api/config/deduction-rules$', ] def is_public_path(path: str) -> bool: """检查是否为公开路径""" diff --git a/backend/middleware/sanitize.py b/backend/middleware/sanitize.py index 50d06a8..108372b 100644 --- a/backend/middleware/sanitize.py +++ b/backend/middleware/sanitize.py @@ -56,6 +56,21 @@ class SanitizeMiddleware(BaseHTTPMiddleware): # 去除首尾空格 value = value.strip() + # SQL注入模式检测 + sql_patterns = [ + r'(?i)(\bunion\b\s+\bselect\b)', + r'(?i)(\bor\b\s+\d+\s*=\s*\d+)', + r'(?i)(\bdrop\b\s+\btable\b)', + r'(?i)(\bdelete\b\s+\bfrom\b)', + r'(?i)(\binsert\b\s+\binto\b)', + r'(?i)(\bupdate\b\s+\w+\s+\bset\b)', + ] + for pattern in sql_patterns: + value = re.sub(pattern, '', value) + + # 路径遍历检测 + value = value.replace('../', '').replace('..\\', '') + # 限制长度 if len(value) > 1000: value = value[:1000] @@ -106,7 +121,9 @@ def validate_reason(reason: str) -> tuple: """ if not reason or not reason.strip(): return False, "原因不能为空" - if len(reason) > 255: + # 计算可见字符长度(不含换行符),支持多行输入 + visible_length = len(reason.replace('\n', '')) + if visible_length > 255: return False, "原因长度不能超过255个字符" return True, "" diff --git a/backend/models/homework.py b/backend/models/homework.py index cd11c0d..ccddc47 100644 --- a/backend/models/homework.py +++ b/backend/models/homework.py @@ -42,18 +42,22 @@ class HomeworkModel: """ return await execute_query(sql, tuple(subject_ids)) + @staticmethod @staticmethod async def get_student_homework(student_id: int) -> List[Dict[str, Any]]: sql = """ - SELECT a.assignment_id, a.title, a.description, a.deadline, - s.subject_name, hs.status, hs.submit_time, hs.comments, hs.deduction_applied + SELECT a.assignment_id, a.title, a.description, a.deadline, a.created_at, + s.subject_name, hs.status, hs.submit_time, hs.comments, hs.deduction_applied, + cr.points_change AS points FROM assignments a JOIN subjects s ON a.subject_id = s.subject_id LEFT JOIN homework_submissions hs ON a.assignment_id = hs.assignment_id AND hs.student_id = %s + LEFT JOIN conduct_records cr ON cr.related_type = 'homework' + AND cr.related_id = a.assignment_id AND cr.student_id = %s AND cr.is_revoked = 0 + GROUP BY a.assignment_id ORDER BY a.deadline ASC, a.created_at DESC """ - return await execute_query(sql, (student_id,)) - + return await execute_query(sql, (student_id, student_id)) @staticmethod async def get_submission(submission_id: int) -> Optional[Dict[str, Any]]: sql = """ diff --git a/backend/models/student.py b/backend/models/student.py index d31a253..befbbac 100644 --- a/backend/models/student.py +++ b/backend/models/student.py @@ -44,8 +44,8 @@ class StudentModel: async def get_all(include_disabled: bool = False) -> List[Dict[str, Any]]: """获取所有学生列表(单班级)""" sql = """ - SELECT student_id, student_no, name, total_points, parent_phone, status - FROM students + SELECT student_id, student_no, name, total_points, parent_phone, dormitory_number, status + FROM students WHERE 1=1 """ if not include_disabled: @@ -58,17 +58,18 @@ class StudentModel: student_no: str, name: str, parent_phone: str = None, + dormitory_number: str = None, initial_points: int = 60 ) -> int: """创建学生(初始操行分默认60分)""" sql = """ - INSERT INTO students (student_no, name, parent_phone, total_points) - VALUES (%s, %s, %s, %s) + INSERT INTO students (student_no, name, parent_phone, dormitory_number, total_points) + VALUES (%s, %s, %s, %s, %s) """ - return await execute_insert(sql, (student_no, name, parent_phone, initial_points)) + return await execute_insert(sql, (student_no, name, parent_phone, dormitory_number, initial_points)) @staticmethod - async def update(student_id: int, name: str = None, parent_phone: str = None, status: int = None) -> bool: + async def update(student_id: int, name: str = None, parent_phone: str = None, dormitory_number: str = None, status: int = None) -> bool: """更新学生信息""" updates = [] params = [] @@ -79,6 +80,9 @@ class StudentModel: if parent_phone is not None: updates.append("parent_phone = %s") params.append(parent_phone) + if dormitory_number is not None: + updates.append("dormitory_number = %s") + params.append(dormitory_number) if status is not None: updates.append("status = %s") params.append(status) @@ -101,7 +105,7 @@ class StudentModel: @staticmethod async def update_total_points(student_id: int, points_change: int) -> bool: """更新学生总分""" - sql = "UPDATE students SET total_points = total_points + %s WHERE student_id = %s" + sql = "UPDATE students SET total_points = total_points + %s, points_updated_at = CURRENT_TIMESTAMP WHERE student_id = %s" result = await execute_update(sql, (points_change, student_id)) return result > 0 @@ -109,10 +113,10 @@ class StudentModel: async def get_ranking(limit: int = 50) -> List[Dict[str, Any]]: """获取学生排行(单班级)""" sql = """ - SELECT student_id, student_no, name, total_points + SELECT student_id, student_no, name, total_points, points_updated_at FROM students WHERE status = 1 - ORDER BY total_points DESC + ORDER BY total_points DESC, points_updated_at ASC LIMIT %s """ results = await execute_query(sql, (limit,)) @@ -137,6 +141,7 @@ class StudentModel: student_no=student.get('student_no'), name=student.get('name'), parent_phone=student.get('parent_phone'), + dormitory_number=student.get('dormitory_number'), initial_points=initial_points ) results.append({ diff --git a/backend/models/user.py b/backend/models/user.py index f993d87..a20b718 100644 --- a/backend/models/user.py +++ b/backend/models/user.py @@ -44,7 +44,7 @@ class UserModel: @staticmethod async def create_student(username: str, password: str, real_name: str, student_id: int) -> int: """创建学生账号""" - password_hash = security.sha1_md5_password(password) + password_hash = security.bcrypt_password(password) sql = """ INSERT INTO users (username, password_hash, real_name, user_type, student_id, need_change_password) VALUES (%s, %s, %s, 'student', %s, 1) @@ -54,7 +54,7 @@ class UserModel: @staticmethod async def create_parent(username: str, password: str, real_name: str, student_id: int) -> int: """创建家长账号""" - password_hash = security.sha1_md5_password(password) + password_hash = security.bcrypt_password(password) sql = """ INSERT INTO users (username, password_hash, real_name, user_type, student_id, need_change_password) VALUES (%s, %s, %s, 'parent', %s, 0) @@ -64,7 +64,7 @@ class UserModel: @staticmethod async def create_admin(username: str, password: str, real_name: str) -> int: """创建管理员账号""" - password_hash = security.sha1_md5_password(password) + password_hash = security.bcrypt_password(password) sql = """ INSERT INTO users (username, password_hash, real_name, user_type, need_change_password) VALUES (%s, %s, %s, 'admin', 1) @@ -74,9 +74,9 @@ class UserModel: @staticmethod async def update_password(user_id: int, new_password: str) -> bool: """更新密码""" - password_hash = security.sha1_md5_password(new_password) + password_hash = security.bcrypt_password(new_password) sql = """ - UPDATE users + UPDATE users SET password_hash = %s, need_change_password = 0 WHERE user_id = %s """ diff --git a/backend/routes/config.py b/backend/routes/config.py new file mode 100644 index 0000000..4dc73ec --- /dev/null +++ b/backend/routes/config.py @@ -0,0 +1,29 @@ +# =========================================== +# 班级操行分管理系统 - 后端服务 +# +# 开发者: Canglan +# 联系方式: admin@sea-studio.top +# 版权归属: Sea Network Technology Studio +# 许可证: MIT License +# +# 版权所有 © Sea Network Technology Studio +# =========================================== + +from fastapi import APIRouter +from config import settings +from utils.response import success_response + +router = APIRouter() + +@router.get("/deduction-rules") +async def get_deduction_rules(): + """获取扣分规则配置(公开接口)""" + data = { + "DEDUCTION_HOMEWORK_NOT_SUBMIT": settings.DEDUCTION_HOMEWORK_NOT_SUBMIT, + "DEDUCTION_HOMEWORK_LATE": settings.DEDUCTION_HOMEWORK_LATE, + "DEDUCTION_ATTENDANCE_ABSENT": settings.DEDUCTION_ATTENDANCE_ABSENT, + "DEDUCTION_ATTENDANCE_LATE": settings.DEDUCTION_ATTENDANCE_LATE, + "DEDUCTION_ATTENDANCE_LEAVE": settings.DEDUCTION_ATTENDANCE_LEAVE, + "STUDENT_INITIAL_POINTS": settings.STUDENT_INITIAL_POINTS, + } + return success_response(data=data) diff --git a/backend/schemas/admin.py b/backend/schemas/admin.py index fa6d1ec..cfb58ff 100644 --- a/backend/schemas/admin.py +++ b/backend/schemas/admin.py @@ -76,6 +76,7 @@ class AddStudentRequest(BaseModel): student_no: str = Field(..., min_length=1, max_length=20, pattern=r'^[a-zA-Z0-9]+$', description="学号") name: str = Field(..., min_length=1, max_length=50, description="姓名") parent_phone: Optional[str] = Field(None, max_length=11, pattern=r'^\d{0,11}$', description="家长手机号") + dormitory_number: Optional[str] = Field(None, max_length=20, description="宿舍号") class AddAttendanceRequest(BaseModel): @@ -109,6 +110,7 @@ class UpdateStudentRequest(BaseModel): """更新学生请求""" 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="家长手机号") + dormitory_number: Optional[str] = Field(None, max_length=20, description="宿舍号") class CreateAssignmentRequest(BaseModel): diff --git a/backend/schemas/config.py b/backend/schemas/config.py new file mode 100644 index 0000000..fddc68e --- /dev/null +++ b/backend/schemas/config.py @@ -0,0 +1,22 @@ +# =========================================== +# 班级操行分管理系统 - 后端服务 +# +# 开发者: Canglan +# 联系方式: admin@sea-studio.top +# 版权归属: Sea Network Technology Studio +# 许可证: MIT License +# +# 版权所有 © Sea Network Technology Studio +# =========================================== + +from pydantic import BaseModel +from typing import Optional + +class DeductionConfigResponse(BaseModel): + """扣分规则配置响应""" + DEDUCTION_HOMEWORK_NOT_SUBMIT: int + DEDUCTION_HOMEWORK_LATE: int + DEDUCTION_ATTENDANCE_ABSENT: int + DEDUCTION_ATTENDANCE_LATE: int + DEDUCTION_ATTENDANCE_LEAVE: int + STUDENT_INITIAL_POINTS: int diff --git a/backend/schemas/student.py b/backend/schemas/student.py index 7a8e799..1bfe1b4 100644 --- a/backend/schemas/student.py +++ b/backend/schemas/student.py @@ -21,6 +21,7 @@ class StudentInfo(BaseModel): name: str total_points: int parent_phone: Optional[str] = None + dormitory_number: Optional[str] = None status: int diff --git a/backend/services/admin_service.py b/backend/services/admin_service.py index 8480950..2fbf183 100644 --- a/backend/services/admin_service.py +++ b/backend/services/admin_service.py @@ -88,6 +88,7 @@ class AdminService: student_no = student.get("student_no", "").strip() name = student.get("name", "").strip() parent_phone = student.get("parent_phone", "").strip() + dormitory_number = student.get("dormitory_number", "").strip() if student.get("dormitory_number") else None password = student.get("password", "").strip() if not student_no or not name: @@ -113,6 +114,7 @@ class AdminService: student_no=student_no, name=name, parent_phone=parent_phone if parent_phone else None, + dormitory_number=dormitory_number, initial_points=initial_points ) existing_student_nos.add(student_no) @@ -162,7 +164,8 @@ class AdminService: name: str, parent_phone: Optional[str], operator_id: int, - initial_points: int = 60 + initial_points: int = 60, + dormitory_number: Optional[str] = None ) -> Dict[str, Any]: """新增学生""" if not security.validate_student_no(student_no): @@ -179,6 +182,7 @@ class AdminService: student_no=student_no, name=name, parent_phone=parent_phone if parent_phone else None, + dormitory_number=dormitory_number, initial_points=initial_points ) @@ -248,14 +252,14 @@ class AdminService: return {"admins": admins} @staticmethod - async def update_student(student_id: int, name: str = None, parent_phone: str = None) -> Dict[str, Any]: + async def update_student(student_id: int, name: str = None, parent_phone: str = None, dormitory_number: str = None) -> Dict[str, Any]: """编辑学生信息""" try: student = await StudentModel.get_by_id(student_id) if not student: return {"success": False, "message": "学生不存在"} - result = await StudentModel.update(student_id, name=name, parent_phone=parent_phone) + result = await StudentModel.update(student_id, name=name, parent_phone=parent_phone, dormitory_number=dormitory_number) if result: return {"success": True, "message": "学生信息更新成功"} return {"success": False, "message": "更新失败"} diff --git a/backend/services/auth_service.py b/backend/services/auth_service.py index 45687b7..109bfc4 100644 --- a/backend/services/auth_service.py +++ b/backend/services/auth_service.py @@ -48,11 +48,19 @@ class AuthService: return {"success": False, "message": "用户名或密码错误"} # 验证密码 - if not security.verify_password(password, user["password_hash"]): + is_valid, needs_upgrade = security.verify_password_v2(password, user["password_hash"]) + if not is_valid: await RedisClient.set_login_attempts(username) await LogService.write_login_log(username, 0, ip, user_agent, "用户名或密码错误") return {"success": False, "message": "用户名或密码错误"} + # 自动升级旧哈希密码 + if needs_upgrade: + try: + await UserModel.update_password(user["user_id"], password) + except Exception: + pass + # 检查账号状态 if user["status"] != 1: await LogService.write_login_log(username, 0, ip, user_agent, "账号已被禁用") @@ -116,8 +124,10 @@ class AuthService: return {"success": False, "message": "用户不存在"} # 验证原密码(强制改密时跳过) - if not force and not security.verify_password(old_password, user["password_hash"]): - return {"success": False, "message": "原密码错误"} + if not force: + is_valid, _ = security.verify_password_v2(old_password, user["password_hash"]) + if not is_valid: + return {"success": False, "message": "原密码错误"} # 验证新密码强度 is_valid, msg = security.validate_password_strength(new_password) diff --git a/backend/services/parent_service.py b/backend/services/parent_service.py index 29ca19c..2f8eec7 100644 --- a/backend/services/parent_service.py +++ b/backend/services/parent_service.py @@ -41,7 +41,8 @@ class ParentService: "student_id": student["student_id"], "student_name": student["name"], "student_no": student["student_no"], - "total_points": student["total_points"] + "total_points": student["total_points"], + "dormitory_number": student.get("dormitory_number") } @staticmethod diff --git a/backend/utils/security.py b/backend/utils/security.py index edba94b..00b5ee0 100644 --- a/backend/utils/security.py +++ b/backend/utils/security.py @@ -12,6 +12,7 @@ import hashlib import secrets import re +from passlib.hash import bcrypt as bcrypt_hash from config import settings @@ -32,6 +33,27 @@ class SecurityUtils: md5_hash = hashlib.md5(salted.encode('utf-8')).hexdigest() return md5_hash + @staticmethod + def bcrypt_password(password: str) -> str: + """使用bcrypt加密密码""" + return bcrypt_hash.using(rounds=12).hash(password) + + @staticmethod + def verify_password_v2(plain_password: str, hashed_password: str) -> tuple: + """ + 验证密码(支持bcrypt和旧哈希) + 返回: (是否验证成功, 是否需要升级哈希) + """ + try: + if bcrypt_hash.verify(plain_password, hashed_password): + return True, False + except Exception: + pass + # 回退到旧的sha1_md5验证 + if SecurityUtils.sha1_md5_password(plain_password) == hashed_password: + return True, True + return False, False + @staticmethod def verify_password(plain_password: str, hashed_password: str) -> bool: """验证密码""" @@ -83,6 +105,21 @@ class SecurityUtils: # 去除首尾空格 value = value.strip() + # SQL注入模式检测 + sql_patterns = [ + r'(?i)(\bunion\b\s+\bselect\b)', + r'(?i)(\bor\b\s+\d+\s*=\s*\d+)', + r'(?i)(\bdrop\b\s+\btable\b)', + r'(?i)(\bdelete\b\s+\bfrom\b)', + r'(?i)(\binsert\b\s+\binto\b)', + r'(?i)(\bupdate\b\s+\w+\s+\bset\b)', + ] + for pattern in sql_patterns: + value = re.sub(pattern, '', value) + + # 路径遍历检测 + value = value.replace('../', '').replace('..\\', '') + # 限制长度 if len(value) > max_length: value = value[:max_length] diff --git a/frontend/.env.example b/frontend/.env.example index a87bd19..e17b84c 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -32,24 +32,4 @@ SESSION_TIMEOUT=30 ICP_ENABLED=false # ICP备案号 ICP_NUMBER=京ICP备1234567890号-x - -# =========================================== -# 扣分规则配置 -# =========================================== - -# 作业-未交扣分 -DEDUCTION_HOMEWORK_NOT_SUBMIT=2 -# 作业-迟交扣分 -DEDUCTION_HOMEWORK_LATE=1 -# 作业-每次加减分上限(绝对值,仅影响作业管理页面的前端输入限制) -HOMEWORK_MAX_POINTS=5 - -# 考勤-缺勤扣分 -DEDUCTION_ATTENDANCE_ABSENT=3 -# 考勤-迟到扣分 -DEDUCTION_ATTENDANCE_LATE=1 -# 考勤-请假扣分(设为0表示不扣分) -DEDUCTION_ATTENDANCE_LEAVE=0 - -# 学生初始操行分 STUDENT_INITIAL_POINTS=60 \ No newline at end of file diff --git a/frontend/admin/admins.php b/frontend/admin/admins.php index 9f42f04..6588f68 100644 --- a/frontend/admin/admins.php +++ b/frontend/admin/admins.php @@ -147,165 +147,8 @@ include __DIR__ . '/../includes/header.php'; - - + + + diff --git a/frontend/admin/attendance.php b/frontend/admin/attendance.php index 90ddac5..d32499a 100644 --- a/frontend/admin/attendance.php +++ b/frontend/admin/attendance.php @@ -87,196 +87,7 @@ include __DIR__ . '/../includes/header.php'; .student-cell-name { font-size: 14px; font-weight: 500; } .student-cell-no { font-size: 11px; color: #999; } - - + + diff --git a/frontend/admin/conduct.php b/frontend/admin/conduct.php index aea4b40..8f3d780 100644 --- a/frontend/admin/conduct.php +++ b/frontend/admin/conduct.php @@ -58,139 +58,10 @@ include __DIR__ . '/../includes/header.php'; - - + + + + - - + + \ No newline at end of file diff --git a/frontend/admin/history.php b/frontend/admin/history.php index 5f485e2..33a60ec 100644 --- a/frontend/admin/history.php +++ b/frontend/admin/history.php @@ -44,13 +44,19 @@ include __DIR__ . '/../includes/header.php';
+
+ + +
@@ -59,7 +65,7 @@ include __DIR__ . '/../includes/header.php';
- + @@ -77,156 +83,10 @@ include __DIR__ . '/../includes/header.php'; - - + + + + + \ No newline at end of file diff --git a/frontend/admin/homework.php b/frontend/admin/homework.php index 69d370b..f7847e3 100644 --- a/frontend/admin/homework.php +++ b/frontend/admin/homework.php @@ -81,6 +81,16 @@ include __DIR__ . '/../includes/header.php'; + +
+ + +
+
+ + +
+
@@ -101,118 +111,10 @@ include __DIR__ . '/../includes/header.php';
- - + + + + + \ No newline at end of file diff --git a/frontend/admin/password.php b/frontend/admin/password.php index 3a43202..3757e07 100644 --- a/frontend/admin/password.php +++ b/frontend/admin/password.php @@ -77,6 +77,5 @@ document.getElementById('passwordForm').addEventListener('submit', async (e) => } }); - \ No newline at end of file diff --git a/frontend/admin/semesters.php b/frontend/admin/semesters.php index cfddf51..0416b2d 100644 --- a/frontend/admin/semesters.php +++ b/frontend/admin/semesters.php @@ -196,279 +196,7 @@ include __DIR__ . '/../includes/header.php'; - - + + diff --git a/frontend/admin/students.php b/frontend/admin/students.php index 9969d14..ce30d0e 100644 --- a/frontend/admin/students.php +++ b/frontend/admin/students.php @@ -46,6 +46,7 @@ include __DIR__ . '/../includes/header.php'; + @@ -102,6 +103,10 @@ include __DIR__ . '/../includes/header.php'; 填写后将自动创建家长账号(密码同学生初始密码123456) +
+ + +
+
+ + +
- - + + + + + +
时间 学生 分数变动 学号 姓名宿舍号 操行分 家长手机号 操作
'; - html += ''; - html += ''; - - students.forEach(s => { - html += ` - - - - - `; - }); - - html += `
学号姓名家长手机号初始密码
${escapeHtml(s.student_no || '')}${escapeHtml(s.name || '')}${escapeHtml(s.parent_phone || '')}${escapeHtml(s.password || '123456')}

共 ${students.length} 条记录,初始操行分默认为60分

`; - document.getElementById('importPreview').innerHTML = html; - document.getElementById('importPreview').style.display = 'block'; - document.getElementById('importBtn').style.display = 'inline-block'; - } catch (error) { - showToast('JSON格式错误', 'error'); - } - }; - reader.readAsText(file); -} - -// 执行导入 -async function doImport() { - const file = document.getElementById('importFile').files[0]; - if (!file) { - showToast('请选择文件', 'warning'); - return; - } - - const formData = new FormData(); - formData.append('file', file); - - const token = getToken(); - const response = await fetch(`${API_BASE_URL}/api/admin/students/import`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}` - }, - body: formData - }); - - const result = await response.json(); - - if (result.success) { - showToast(result.message); - closeModal('importModal'); - loadStudents(); - - // 显示详细导入结果 - if (result.data && result.data.results) { - const failedList = result.data.results.filter(r => !r.success); - if (failedList.length > 0) { - let detail = '失败详情:\n'; - failedList.forEach(r => { - detail += `${r.student_no || '未知'}: ${r.error}\n`; - }); - alert(detail); - } - } - } else { - showToast(result.message, 'error'); - } -} - -// 显示新增学生模态框 -function showAddStudentModal() { - document.getElementById('addStudentModal').style.display = 'flex'; - document.getElementById('addStudentForm').reset(); -} - -// 提交新增学生 -async function submitAddStudent() { - const studentNo = document.getElementById('studentNo').value.trim(); - const name = document.getElementById('studentName').value.trim(); - const parentPhone = document.getElementById('parentPhone').value.trim(); - - if (!studentNo || !name) { - showToast('请填写学号和姓名', 'warning'); - return; - } - - const res = await apiPost('/api/admin/students', { - student_no: studentNo, - name: name, - parent_phone: parentPhone - }); - - if (res && res.success) { - showToast('学生添加成功'); - closeModal('addStudentModal'); - loadStudents(); - } else { - showToast(res?.message || '添加失败', 'error'); - } -} - -// 显示添加管理员模态框 -function showAddAdminModal() { - document.getElementById('addAdminModal').style.display = 'flex'; - document.getElementById('addAdminForm')?.reset(); -} - -// 提交添加管理员 -async function submitAddAdmin() { - const username = document.getElementById('adminUsername').value.trim(); - const realName = document.getElementById('adminRealName').value.trim(); - const password = document.getElementById('adminPassword').value; - const roleType = document.getElementById('adminRole').value; - - if (!username || !realName || !roleType) { - showToast('请填写完整信息', 'warning'); - return; - } - - const res = await apiPost('/api/admin/add', { - username: username, - real_name: realName, - password: password || undefined, - role_type: roleType - }); - - if (res && res.success) { - let msg = `管理员 ${res.data.username} 添加成功`; - if (res.data.password) msg += `,密码: ${res.data.password}`; - showToast(msg); - closeModal('addAdminModal'); - loadAdmins(); - } else { - showToast(res?.message || '添加失败', 'error'); - } -} - -// 显示添加科目模态框 -function showAddSubjectModal() { - document.getElementById('addSubjectModal').style.display = 'flex'; - document.getElementById('addSubjectForm').reset(); -} - -// 提交添加科目 -async function submitAddSubject() { - const subjectName = document.getElementById('subjectName').value.trim(); - const subjectCode = document.getElementById('subjectCode').value.trim(); - - if (!subjectName) { - showToast('请填写科目名称', 'warning'); - return; - } - - const res = await apiPost('/api/subject/create', { - subject_name: subjectName, - subject_code: subjectCode - }); - - if (res && res.success) { - showToast('科目添加成功'); - closeModal('addSubjectModal'); - loadSubjects(); - } else { - showToast(res?.message || '添加失败', 'error'); - } -} - -// 撤销扣分记录 -async function revokeRecord(recordId) { - if (!confirm('确定要撤销这条记录吗?撤销后学生分数将恢复。')) return; - - const res = await apiPost('/api/admin/conduct/revoke', { record_id: recordId }); - if (res && res.success) { - showToast('撤销成功'); - loadHistory(currentHistoryPage); - } else { - showToast(res?.message || '撤销失败', 'error'); - } -} - -// 反撤销(恢复)记录 -async function restoreRecord(recordId) { - if (!confirm('确定要反撤销这条记录吗?分数变动将重新生效。')) return; - - const res = await apiPost('/api/admin/conduct/restore', { record_id: recordId }); - if (res && res.success) { - showToast('反撤销成功'); - loadHistory(currentHistoryPage); - } else { - showToast(res?.message || '反撤销失败', 'error'); - } -} - -// 关闭模态框 -function closeModal(modalId) { - const modal = document.getElementById(modalId); - if (modal) { - modal.style.display = 'none'; - } -} - -// HTML转义 -function escapeHtml(str) { - if (!str) return ''; - return str.replace(/[&<>]/g, function(m) { - if (m === '&') return '&'; - if (m === '<') return '<'; - if (m === '>') return '>'; - return m; - }); -} - -// 全选功能 -function toggleSelectAll() { - const selectAll = document.getElementById('selectAll'); - if (selectAll) { - document.querySelectorAll('.student-checkbox').forEach(cb => { - cb.checked = selectAll.checked; - }); - } -} - -// 绑定文件选择事件 -document.addEventListener('DOMContentLoaded', () => { - const fileInput = document.getElementById('importFile'); - if (fileInput) { - fileInput.addEventListener('change', previewImportFile); - } -}); - -// ===== 学生编辑/删除/重置密码 ===== - -function showEditStudentModal(studentId, studentNo, name, phone) { - document.getElementById('editStudentId').value = studentId; - document.getElementById('editStudentNo').value = studentNo; - document.getElementById('editStudentName').value = name; - document.getElementById('editStudentPhone').value = phone || ''; - document.getElementById('editStudentModal').style.display = 'flex'; -} - -async function submitEditStudent() { - const studentId = document.getElementById('editStudentId').value; - const name = document.getElementById('editStudentName').value.trim(); - const phone = document.getElementById('editStudentPhone').value.trim(); - - if (!name) { - showToast('请输入姓名', 'warning'); - return; - } - - const res = await apiPut(`/api/admin/students/${studentId}`, { - name: name, - parent_phone: phone || null - }); - - if (res && res.success) { - showToast('学生信息更新成功'); - closeModal('editStudentModal'); - location.reload(); - } else { - showToast(res?.message || '更新失败', 'error'); - } -} - -function showResetStudentPasswordModal(studentId, name) { - document.getElementById('resetStudentId').value = studentId; - document.getElementById('resetStudentInfo').textContent = `正在重置学生 "${name}" 的密码`; - document.getElementById('newStudentPassword').value = ''; - document.getElementById('resetStudentPasswordModal').style.display = 'flex'; -} - -async function submitResetStudentPassword() { - const studentId = document.getElementById('resetStudentId').value; - const newPassword = document.getElementById('newStudentPassword').value; - - if (!newPassword || newPassword.length < 6) { - showToast('密码至少6位', 'warning'); - return; - } - - const res = await apiPost(`/api/admin/students/reset-password/${studentId}`, { - new_password: newPassword - }); - - if (res && res.success) { - showToast('密码重置成功'); - closeModal('resetStudentPasswordModal'); - } else { - showToast(res?.message || '重置失败', 'error'); - } -} - -async function deleteStudent(studentId, name) { - if (!confirm(`确定要删除学生 "${name}" 吗?删除后学生账号将被禁用。`)) return; - - const res = await apiDelete(`/api/admin/students/${studentId}`); - - if (res && res.success) { - showToast('学生删除成功'); - location.reload(); - } else { - showToast(res?.message || '删除失败', 'error'); - } -} \ No newline at end of file diff --git a/frontend/assets/js/admins.js b/frontend/assets/js/admins.js new file mode 100644 index 0000000..e18a5b4 --- /dev/null +++ b/frontend/assets/js/admins.js @@ -0,0 +1,141 @@ +/** + * 班级操行分管理系统 - 管理员管理页JS + * + * 开发者: Canglan + * 版权归属: Sea Network Technology Studio + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { +'use strict'; + +let currentEditUserId = null; +let currentResetUserId = null; + +async function loadAdmins() { + const res = await apiGet('/api/admin/list'); + if (res && res.success) { + let html = ''; + res.data.admins.forEach(admin => { + html += ` + ${escapeHtml(admin.username)} + ${escapeHtml(admin.real_name)} + ${escapeHtml(admin.role_type)} + + + + + + + `; + }); + if (res.data.admins.length === 0) { + html = '暂无管理员'; + } + document.getElementById('adminList').innerHTML = html; + } +} + +function showEditAdminModal(userId, username, realName, roleType) { + currentEditUserId = userId; + document.getElementById('editAdminUserId').value = userId; + document.getElementById('editAdminUsername').value = username; + document.getElementById('editAdminRealName').value = realName; + document.getElementById('editAdminRole').value = roleType; + document.getElementById('editAdminModal').style.display = 'flex'; +} + +async function submitEditAdmin() { + if (!currentEditUserId) return; + + const roleType = document.getElementById('editAdminRole').value; + if (!roleType) { + showToast('请选择角色', 'warning'); + return; + } + + const res = await apiPut(`/api/admin/update/${currentEditUserId}`, { + real_name: document.getElementById('editAdminRealName').value, + role_type: roleType + }); + + if (res && res.success) { + showToast('管理员更新成功'); + closeModal('editAdminModal'); + loadAdmins(); + } else { + showToast(res?.message || '更新失败', 'error'); + } +} + +async function deleteAdmin(userId, realName) { + if (!confirm(`确定要删除管理员 "${realName}" 吗?此操作不可恢复。`)) { + return; + } + + const res = await apiDelete(`/api/admin/delete/${userId}`); + if (res && res.success) { + showToast('管理员删除成功'); + loadAdmins(); + } else { + showToast(res?.message || '删除失败', 'error'); + } +} + +function resetAdminPassword(userId, realName) { + currentResetUserId = userId; + document.getElementById('resetPasswordUserId').value = userId; + document.getElementById('resetPasswordAdminName').value = realName; + document.getElementById('newPassword').value = ''; + document.getElementById('resetPasswordModal').style.display = 'flex'; +} + +async function unlockUser(username, realName) { + if (!confirm(`确定要解除用户 "${realName}" 的登录锁定吗?\n(适用于多次登录失败被禁止登录的情况)`)) { + return; + } + + const res = await apiPost('/api/admin/unlock-user', { + username: username + }); + + if (res && res.success) { + showToast(res.message || '解锁成功'); + } else { + showToast(res?.message || '解锁失败', 'error'); + } +} + +async function submitResetPassword() { + if (!currentResetUserId) return; + + const newPassword = document.getElementById('newPassword').value; + if (!newPassword || newPassword.length < 6) { + showToast('密码长度至少6位', 'warning'); + return; + } + + const res = await apiPost(`/api/admin/reset-password/${currentResetUserId}`, { + new_password: newPassword + }); + + if (res && res.success) { + showToast('密码重置成功'); + closeModal('resetPasswordModal'); + } else { + showToast(res?.message || '密码重置失败', 'error'); + } +} + +loadAdmins(); + +window.loadAdmins = loadAdmins; +window.showEditAdminModal = showEditAdminModal; +window.submitEditAdmin = submitEditAdmin; +window.deleteAdmin = deleteAdmin; +window.resetAdminPassword = resetAdminPassword; +window.unlockUser = unlockUser; +window.submitResetPassword = submitResetPassword; + +})(); diff --git a/frontend/assets/js/attendance-manage.js b/frontend/assets/js/attendance-manage.js new file mode 100644 index 0000000..ec79f9a --- /dev/null +++ b/frontend/assets/js/attendance-manage.js @@ -0,0 +1,195 @@ +/** + * 班级操行分管理系统 - 考勤管理页JS + * + * 开发者: Canglan + * 版权归属: Sea Network Technology Studio + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { +'use strict'; + +let currentStatus = 'absent'; +let studentsData = []; +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) { + document.querySelectorAll('.status-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + currentStatus = btn.dataset.status; + const defaultDeduction = attendanceDeductionMap[currentStatus] || 0; + if (defaultDeduction > 0) { + document.getElementById('customDeduction').value = defaultDeduction; + } else { + document.getElementById('customDeduction').value = ''; + } +} + +async function loadStudents() { + const res = await apiGet('/api/admin/students', {page_size: 1000}); + if (res && res.success) { + studentsData = res.data.students; + renderStudentGrid(); + await loadExistingRecords(); + } else { + document.getElementById('studentGrid').innerHTML = '
加载学生列表失败
'; + } +} + +function renderStudentGrid() { + const currentSlot = document.getElementById('attendanceSlot').value; + let html = ''; + studentsData.forEach(student => { + const hasRecord = existingRecords.some(r => r.student_id === student.student_id && r.slot === currentSlot); + html += `
+ ${escapeHtml(student.name)} + ${escapeHtml(student.student_no)} +
`; + }); + if (studentsData.length === 0) { + html = '
暂无学生数据
'; + } + document.getElementById('studentGrid').innerHTML = html; +} + +function toggleStudent(cell) { + cell.classList.toggle('selected'); +} + +function selectAllStudents() { + document.querySelectorAll('.student-cell:not(.has-record)').forEach(cell => { + cell.classList.add('selected'); + }); +} + +function deselectAllStudents() { + document.querySelectorAll('.student-cell').forEach(cell => { + cell.classList.remove('selected'); + }); +} + +async function loadExistingRecords() { + const date = document.getElementById('attendanceDate').value; + const slot = document.getElementById('attendanceSlot').value; + const res = await apiGet('/api/admin/attendance/records', { date, slot }); + if (res && res.success) { + existingRecords = res.data.records || []; + renderStudentGrid(); + } +} + +async function submitAttendance() { + const selectedCells = document.querySelectorAll('.student-cell.selected'); + if (selectedCells.length === 0) { + showToast('请先选择有考勤异常的学生', 'warning'); + return; + } + + const date = document.getElementById('attendanceDate').value; + const slot = document.getElementById('attendanceSlot').value; + const reason = document.getElementById('attendanceReason').value; + const customDeduction = document.getElementById('customDeduction').value; + const customDeductionValue = customDeduction ? parseInt(customDeduction) : null; + + const promises = []; + selectedCells.forEach(cell => { + const studentId = parseInt(cell.dataset.id); + const payload = { + student_id: studentId, + date: date, + slot: slot, + status: currentStatus, + reason: reason, + apply_deduction: true + }; + if (customDeductionValue !== null && customDeductionValue > 0) { + payload.custom_deduction = customDeductionValue; + } + promises.push(apiPost('/api/admin/attendance', payload)); + }); + + const results = await Promise.allSettled(promises); + const succeeded = results.filter(r => r.status === 'fulfilled' && r.value?.success).length; + const failed = results.length - succeeded; + + if (failed === 0) { + showToast(`考勤提交成功(${succeeded}条)`); + } else { + showToast(`提交完成:成功${succeeded}条,失败${failed}条`, 'error'); + } + + deselectAllStudents(); + await loadExistingRecords(); + loadAttendanceRecords(); +} + +async function loadAttendanceRecords() { + const date = document.getElementById('attendanceDate').value; + const res = await apiGet('/api/admin/attendance/records', { date }); + if (res && res.success) { + let html = ''; + const records = res.data.records || []; + records.forEach(record => { + html += ` + ${escapeHtml(record.student_no)} + ${escapeHtml(record.student_name)} + ${getStatusBadge(record.status, 'attendance')} + ${escapeHtml(record.reason || '-')} + ${escapeHtml(record.recorder_name || '-')} + ${record.deduction_applied ? '已扣分' : '-'} + `; + }); + if (records.length === 0) { + html = '暂无考勤记录'; + } + document.getElementById('attendanceList').innerHTML = html; + } +} + +// 日期或时段变化时重新加载 +document.getElementById('attendanceDate').addEventListener('change', function() { + loadExistingRecords(); + loadAttendanceRecords(); +}); +document.getElementById('attendanceSlot').addEventListener('change', function() { + loadExistingRecords(); +}); + +// 页面初始化 +initAttendanceButtons(); +loadStudents(); +loadAttendanceRecords(); + +window.selectStatus = selectStatus; +window.loadStudents = loadStudents; +window.toggleStudent = toggleStudent; +window.selectAllStudents = selectAllStudents; +window.deselectAllStudents = deselectAllStudents; +window.submitAttendance = submitAttendance; +window.loadAttendanceRecords = loadAttendanceRecords; + +})(); diff --git a/frontend/assets/js/common.js b/frontend/assets/js/common.js index 5ab0ae8..a20a5ed 100644 --- a/frontend/assets/js/common.js +++ b/frontend/assets/js/common.js @@ -334,4 +334,21 @@ document.addEventListener('DOMContentLoaded', () => { if (logoutBtn) { logoutBtn.addEventListener('click', logout); } +}); + +// 全局textarea键盘事件:Enter提交表单,Ctrl+Enter换行 +document.addEventListener('keydown', function(e) { + if (e.target.tagName !== 'TEXTAREA') return; + + if (e.key === 'Enter' && !e.ctrlKey && !e.shiftKey && !e.metaKey) { + // Enter键提交表单 + e.preventDefault(); + var form = e.target.closest('form'); + if (form) { + // 触发form的submit事件 + var submitEvent = new Event('submit', { cancelable: true, bubbles: true }); + form.dispatchEvent(submitEvent); + } + } + // Ctrl+Enter和Shift+Enter保持默认换行行为(不拦截) }); \ No newline at end of file diff --git a/frontend/assets/js/conduct.js b/frontend/assets/js/conduct.js new file mode 100644 index 0000000..e6bfce5 --- /dev/null +++ b/frontend/assets/js/conduct.js @@ -0,0 +1,128 @@ +/** + * 班级操行分管理系统 - 操行分管理页JS + * + * 开发者: Canglan + * 版权归属: Sea Network Technology Studio + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { +'use strict'; + +async function loadStudents() { + const res = await apiGet('/api/admin/students', {page_size: 1000}); + if (res && res.success) { + let html = ''; + res.data.students.forEach(student => { + html += ` + + ${escapeHtml(student.student_no)} + ${escapeHtml(student.name)} + ${student.total_points} + + `; + }); + if (res.data.students.length === 0) { + html = '暂无学生数据'; + } + document.getElementById('studentList').innerHTML = html; + } +} + +function showSinglePointsModal(studentId, studentName) { + window.selectedStudentIds = [studentId]; + document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`; + document.getElementById('pointsChange').value = ''; + document.getElementById('pointsReason').value = ''; + document.getElementById('batchPointsModal').style.display = 'flex'; +} + +async function exportMoralityRecords() { + showToast('正在导出德育分记录...', 'info'); + + try { + const studentsRes = await apiGet('/api/admin/students', { page_size: 1000 }); + if (!studentsRes || !studentsRes.success) { + showToast('获取学生列表失败', 'error'); + return; + } + + const students = studentsRes.data.students; + if (students.length === 0) { + showToast('没有找到学生', 'warning'); + return; + } + + const historyRes = await apiGet('/api/admin/conduct/history', { page: 1, page_size: 1000 }); + if (!historyRes || !historyRes.success) { + showToast('获取历史记录失败', 'error'); + return; + } + + const allRecords = historyRes.data.records || []; + + const recordsByStudent = {}; + allRecords.forEach(record => { + const sid = record.student_id; + if (!recordsByStudent[sid]) { + recordsByStudent[sid] = []; + } + recordsByStudent[sid].push(record); + }); + + const studentRecords = []; + for (const student of students) { + const studentRecords_list = recordsByStudent[student.student_id] || []; + const positiveRecords = studentRecords_list.filter(r => r.points_change > 0).map(r => `${r.reason}(+${r.points_change})`); + const negativeRecords = studentRecords_list.filter(r => r.points_change < 0).map(r => `${r.reason}(${r.points_change})`); + + studentRecords.push({ + student_no: student.student_no, + name: student.name, + total_points: student.total_points || 0, + positive_history: positiveRecords.join('; '), + negative_history: negativeRecords.join('; ') + }); + } + + function escapeCsvField(field) { + if (field === null || field === undefined) return ''; + let str = String(field).replace(/[\r\n]+/g, ' '); + str = str.replace(/"/g, '""'); + if (/[\,\;\"\s]/.test(str)) { + str = '"' + str + '"'; + } + return str; + } + + let csv = '\uFEFF'; + csv += '学号,姓名,分数,加分历史,减分记录\n'; + studentRecords.forEach(s => { + csv += `${escapeCsvField(s.student_no)},${escapeCsvField(s.name)},${escapeCsvField(s.total_points)},${escapeCsvField(s.positive_history)},${escapeCsvField(s.negative_history)}\n`; + }); + + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `德育分记录_${new Date().toISOString().slice(0,10)}.csv`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + + showToast(`导出成功,共${studentRecords.length}名学生`); + } catch (err) { + showToast('导出失败:' + err.message, 'error'); + console.error('导出失败:', err); + } +} + +loadStudents(); + +window.loadStudents = loadStudents; +window.showSinglePointsModal = showSinglePointsModal; +window.exportMoralityRecords = exportMoralityRecords; + +})(); diff --git a/frontend/assets/js/dashboard.js b/frontend/assets/js/dashboard.js new file mode 100644 index 0000000..cec02a4 --- /dev/null +++ b/frontend/assets/js/dashboard.js @@ -0,0 +1,90 @@ +/** + * 班级操行分管理系统 - 管理端首页JS + * + * 开发者: Canglan + * 版权归属: Sea Network Technology Studio + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { +'use strict'; + +const role = window.PAGE_CONFIG.role; +let totalStudents = 0; + +async function loadDashboard() { + const studentsRes = await apiGet('/api/admin/students'); + if (studentsRes && studentsRes.success) { + document.getElementById('dashboardStats').innerHTML = ` +
+
学生总数
+
${studentsRes.data.total || 0}
+
+ `; + } + + let quickActions = ''; + if (role === '班主任' || role === '班长' || role === '劳动委员' || role === '志愿委员') { + quickActions += ''; + } + if (role === '班主任') { + quickActions += ''; + quickActions += ''; + } + document.getElementById('quickActions').innerHTML = quickActions || '

暂无快捷操作

'; + + const rankingRes = await apiGet('/api/student/ranking', { limit: 100 }); + if (rankingRes && rankingRes.success) { + totalStudents = rankingRes.data.total_students || 0; + let html = ''; + rankingRes.data.ranking.forEach((student, index) => { + const rank = index + 1; + html += ` + ${rank} + ${escapeHtml(student.student_no)} + ${escapeHtml(student.name)} + ${student.total_points} + `; + }); + if (rankingRes.data.ranking.length === 0) { + html = '暂无数据'; + } + document.getElementById('rankingList').innerHTML = html; + } +} + +function applyPercentileFilter() { + const input = document.getElementById('percentileFilter'); + const percentile = parseInt(input.value); + if (isNaN(percentile) || percentile < 1 || percentile > 100) { + showToast('请输入 1-100 之间的整数', 'error'); + return; + } + const rows = document.getElementById('rankingList').querySelectorAll('tr'); + if (rows.length === 0) return; + const showCount = Math.max(1, Math.floor(totalStudents * (percentile / 100))); + rows.forEach(function(row, index) { + row.style.display = index < showCount ? '' : 'none'; + }); +} + +function resetPercentileFilter() { + document.getElementById('percentileFilter').value = 100; + const rows = document.getElementById('rankingList').querySelectorAll('tr'); + rows.forEach(function(row) { + row.style.display = ''; + }); +} + +document.getElementById('percentileFilter').addEventListener('keypress', function(e) { + if (e.key === 'Enter') applyPercentileFilter(); +}); + +loadDashboard(); + +window.loadDashboard = loadDashboard; +window.applyPercentileFilter = applyPercentileFilter; +window.resetPercentileFilter = resetPercentileFilter; + +})(); diff --git a/frontend/assets/js/history.js b/frontend/assets/js/history.js new file mode 100644 index 0000000..939c043 --- /dev/null +++ b/frontend/assets/js/history.js @@ -0,0 +1,199 @@ +/** + * 班级操行分管理系统 - 历史记录页JS + * + * 开发者: Canglan + * 版权归属: Sea Network Technology Studio + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { +'use strict'; + +const role = window.PAGE_CONFIG.role; +const currentUserId = window.PAGE_CONFIG.userId; +let currentHistoryPage = 1; +let totalHistoryPages = 1; + +async function loadStudentsForSelect() { + const res = await apiGet('/api/admin/students', {page_size: 1000}); + if (res && res.success) { + let html = ''; + res.data.students.forEach(s => { + html += ``; + }); + document.getElementById('historyStudentId').innerHTML = html; + } +} + +async function loadHistory(page = 1) { + currentHistoryPage = page; + const startDate = document.getElementById('historyStartDate').value; + const endDate = document.getElementById('historyEndDate').value; + const studentId = document.getElementById('historyStudentId').value; + const relatedType = document.getElementById('historyRelatedType').value; + const isGrouped = document.getElementById('historyGrouped').checked; + + const params = { + page, page_size: 20, + start_date: startDate, + end_date: endDate + }; + if (studentId) params.student_id = studentId; + if (relatedType && !isGrouped) params.related_type = relatedType; + if (isGrouped) params.grouped = true; + + const res = await apiGet('/api/admin/conduct/history', params); + + if (res && res.success) { + let headHtml = ''; + if (isGrouped) { + headHtml = '时间原因分值操作人涉及学生'; + } else { + headHtml = '时间学生分数变动原因操作人'; + if (role === '班主任' || role === '班长' || role === '考勤委员') { + headHtml += '操作'; + } + } + document.getElementById('historyTableHead').innerHTML = headHtml; + + let html = ''; + if (isGrouped) { + res.data.records.forEach(record => { + const pointsClass = record.points_change > 0 ? 'plus' : 'minus'; + const names = record.student_names ? record.student_names.split(', ') : []; + let tagsHtml = '
'; + names.forEach(name => { + tagsHtml += `${escapeHtml(name)}`; + }); + tagsHtml += '
'; + html += ` + ${formatDateTime(record.created_at)} + ${escapeHtml(record.reason)} + ${record.points_change > 0 ? '+' : ''}${record.points_change}×${record.student_count} + ${escapeHtml(record.recorder_name || '')} + ${tagsHtml} + `; + }); + if (res.data.records.length === 0) { + html = '暂无记录'; + } + } else { + res.data.records.forEach(record => { + const pointsClass = record.points_change > 0 ? 'plus' : 'minus'; + const revokedStyle = record.is_revoked == 1 ? ' style="opacity:0.5; text-decoration:line-through;"' : ''; + html += ` + ${formatDateTime(record.created_at)} + ${escapeHtml(record.student_name)} + ${record.points_change > 0 ? '+' : ''}${record.points_change} + ${escapeHtml(record.reason)} + ${escapeHtml(record.recorder_name)}`; + if (role === '班主任') { + if (record.is_revoked == 1) { + const revokerInfo = record.revoker_name ? `由 ${escapeHtml(record.revoker_name)} 撤销` : '已撤销'; + html += `${revokerInfo}`; + } else { + html += ``; + } + } else if (role === '班长') { + if (record.is_revoked == 1) { + const revokerInfo = record.revoker_name ? `由 ${escapeHtml(record.revoker_name)} 撤销` : '已撤销'; + html += `${revokerInfo}`; + } else { + html += ``; + } + } else if (role === '考勤委员') { + if (record.is_revoked == 1) { + html += `已撤销`; + } else if (record.recorder_id == currentUserId) { + html += ``; + } else { + html += `-`; + } + } + html += ``; + }); + + if (res.data.records.length === 0) { + const colSpan = (role === '班主任' || role === '班长' || role === '考勤委员') ? 6 : 5; + html = `暂无记录`; + } + } + + document.getElementById('historyList').innerHTML = html; + + totalHistoryPages = res.data.total_pages || 1; + renderHistoryPagination(); + } +} + +function renderHistoryPagination() { + renderSmartPagination('historyPagination', currentHistoryPage, totalHistoryPages, function(page) { + loadHistory(page); + }); +} + +async function exportHistoryRecords() { + const startDate = document.getElementById('historyStartDate').value; + const endDate = document.getElementById('historyEndDate').value; + const studentId = document.getElementById('historyStudentId').value; + + showToast('正在导出历史记录...', 'info'); + + try { + const relatedType = document.getElementById('historyRelatedType').value; + const params = { page: 1, page_size: 1000 }; + if (startDate) params.start_date = startDate; + if (endDate) params.end_date = endDate; + if (studentId) params.student_id = studentId; + if (relatedType) params.related_type = relatedType; + + const res = await apiGet('/api/admin/conduct/history', params); + if (res && res.success && res.data.records) { + const records = res.data.records; + if (records.length === 0) { + showToast('没有找到记录', 'warning'); + return; + } + + let csv = '\uFEFF'; + csv += '时间,学号,姓名,分数变动,原因,操作人\n'; + records.forEach(r => { + csv += `${r.created_at || ''},${r.student_no || ''},${r.student_name || ''},${r.points_change > 0 ? '+' : ''}${r.points_change},${(r.reason || '').replace(/,/g, ';')},${r.recorder_name || ''}\n`; + }); + + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `历史记录_${new Date().toISOString().slice(0,10)}.csv`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + + showToast(`导出成功,共${records.length}条记录`); + } else { + showToast('导出失败:' + (res?.message || '未知错误'), 'error'); + } + } catch (err) { + showToast('导出失败:' + err.message, 'error'); + } +} + +loadStudentsForSelect().then(() => { + const urlParams = new URLSearchParams(window.location.search); + const preStudentId = urlParams.get('student_id'); + if (preStudentId) { + document.getElementById('historyStudentId').value = preStudentId; + loadHistory(); + } else { + loadHistory(); + } +}); + +window.loadHistory = loadHistory; +window.loadStudentsForSelect = loadStudentsForSelect; +window.exportHistoryRecords = exportHistoryRecords; + +})(); diff --git a/frontend/assets/js/homework-manage.js b/frontend/assets/js/homework-manage.js new file mode 100644 index 0000000..247684d --- /dev/null +++ b/frontend/assets/js/homework-manage.js @@ -0,0 +1,127 @@ +/** + * 班级操行分管理系统 - 作业管理页JS + * + * 开发者: Canglan + * 版权归属: Sea Network Technology Studio + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { +'use strict'; + +const hwRole = window.PAGE_CONFIG.role; + +// 初始化扣分配置 +const hwMaxPoints = hwRole === '班主任' ? 100 : 5; +const hwNotSubmit = window.DEDUCTION_HOMEWORK_NOT_SUBMIT || 2; +const hwLate = window.DEDUCTION_HOMEWORK_LATE || 1; + +// 更新页面中的配置值显示 +document.querySelectorAll('.hw-not-submit').forEach(el => el.textContent = hwNotSubmit); +document.querySelectorAll('.hw-late').forEach(el => el.textContent = hwLate); +document.querySelectorAll('.hw-max').forEach(el => el.textContent = hwMaxPoints); + +// 更新输入框的 min/max +document.getElementById('pointsChange').setAttribute('min', -hwMaxPoints); +document.getElementById('pointsChange').setAttribute('max', hwMaxPoints); + +// 加载科目列表(学习委员) +async function loadSubjectsForHomework() { + if (hwRole !== '学习委员') return; + const subjectSelect = document.getElementById('hwSubjectSelect'); + if (!subjectSelect) return; + const res = await apiGet('/api/subject/list'); + if (res && res.success && res.data && res.data.subjects) { + let html = ''; + res.data.subjects.forEach(s => { + html += ``; + }); + subjectSelect.innerHTML = html; + } +} + +async function loadStudents() { + const res = await apiGet('/api/admin/students', {page_size: 1000}); + if (res && res.success) { + let html = ''; + res.data.students.forEach(student => { + html += ` + + ${escapeHtml(student.student_no)} + ${escapeHtml(student.name)} + ${student.total_points} + + `; + }); + if (res.data.students.length === 0) { + html = '暂无学生数据'; + } + document.getElementById('studentList').innerHTML = html; + } +} + +function showSinglePointsModal(studentId, studentName) { + window.selectedStudentIds = [studentId]; + document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`; + document.getElementById('pointsChange').value = ''; + document.getElementById('pointsReason').value = ''; + document.getElementById('batchPointsModal').style.display = 'flex'; +} + +function selectDeductionType(points, reason) { + document.getElementById('pointsChange').value = points; + if (points !== 0) { + document.getElementById('pointsReason').value = reason; + } else { + document.getElementById('pointsReason').value = ''; + document.getElementById('pointsReason').focus(); + } +} + +function handleSubmitPoints() { + const pointsChange = parseInt(document.getElementById('pointsChange').value); + if (isNaN(pointsChange) || pointsChange === 0) { + showToast('请输入有效的加减分值', 'warning'); + return; + } + if (Math.abs(pointsChange) > hwMaxPoints) { + showToast(`每次加减分不超过${hwMaxPoints}分`, 'warning'); + return; + } + + // 学习委员附加科目前缀、具体作业和缴交时间 + if (hwRole === '学习委员') { + const subjectSelect = document.getElementById('hwSubjectSelect'); + const subjectName = subjectSelect ? subjectSelect.value : ''; + const hwTitle = document.getElementById('hwTitle').value.trim(); + const hwDeadline = document.getElementById('hwDeadline').value; + const reasonEl = document.getElementById('pointsReason'); + + let prefix = ''; + if (subjectName) { + prefix = `[${subjectName}]`; + } + if (hwTitle) { + prefix += `[${hwTitle}]`; + } + if (hwDeadline) { + prefix += ` 缴交:${hwDeadline}`; + } + if (prefix) { + reasonEl.value = prefix + ' ' + reasonEl.value; + } + } + + submitBatchPoints(); +} + +loadStudents(); +loadSubjectsForHomework(); + +window.loadStudents = loadStudents; +window.showSinglePointsModal = showSinglePointsModal; +window.selectDeductionType = selectDeductionType; +window.handleSubmitPoints = handleSubmitPoints; + +})(); diff --git a/frontend/assets/js/modules/admin-mgmt.js b/frontend/assets/js/modules/admin-mgmt.js new file mode 100644 index 0000000..3f709a1 --- /dev/null +++ b/frontend/assets/js/modules/admin-mgmt.js @@ -0,0 +1,53 @@ +/** + * 班级操行分管理系统 - 管理员管理函数 + * + * 开发者: Canglan + * 联系方式: admin@sea-studio.top + * 版权归属: Sea Network Technology Studio + * 许可证: MIT License + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { + 'use strict'; + + // 显示添加管理员模态框 + function showAddAdminModal() { + document.getElementById('addAdminModal').style.display = 'flex'; + document.getElementById('addAdminForm')?.reset(); + } + + // 提交添加管理员 + async function submitAddAdmin() { + const username = document.getElementById('adminUsername').value.trim(); + const realName = document.getElementById('adminRealName').value.trim(); + const password = document.getElementById('adminPassword').value; + const roleType = document.getElementById('adminRole').value; + + if (!username || !realName || !roleType) { + showToast('请填写完整信息', 'warning'); + return; + } + + const res = await apiPost('/api/admin/add', { + username: username, + real_name: realName, + password: password || undefined, + role_type: roleType + }); + + if (res && res.success) { + let msg = `管理员 ${res.data.username} 添加成功`; + if (res.data.password) msg += `,密码: ${res.data.password}`; + showToast(msg); + closeModal('addAdminModal'); + loadAdmins(); + } else { + showToast(res?.message || '添加失败', 'error'); + } + } + + window.showAddAdminModal = showAddAdminModal; + window.submitAddAdmin = submitAddAdmin; +})(); diff --git a/frontend/assets/js/modules/modal-utils.js b/frontend/assets/js/modules/modal-utils.js new file mode 100644 index 0000000..3d0cf1f --- /dev/null +++ b/frontend/assets/js/modules/modal-utils.js @@ -0,0 +1,24 @@ +/** + * 班级操行分管理系统 - 模态框工具函数 + * + * 开发者: Canglan + * 联系方式: admin@sea-studio.top + * 版权归属: Sea Network Technology Studio + * 许可证: MIT License + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { + 'use strict'; + + // 关闭模态框 + function closeModal(modalId) { + const modal = document.getElementById(modalId); + if (modal) { + modal.style.display = 'none'; + } + } + + window.closeModal = closeModal; +})(); diff --git a/frontend/assets/js/modules/points-mgmt.js b/frontend/assets/js/modules/points-mgmt.js new file mode 100644 index 0000000..2edff8b --- /dev/null +++ b/frontend/assets/js/modules/points-mgmt.js @@ -0,0 +1,98 @@ +/** + * 班级操行分管理系统 - 加减分管理函数 + * + * 开发者: Canglan + * 联系方式: admin@sea-studio.top + * 版权归属: Sea Network Technology Studio + * 许可证: MIT License + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { + 'use strict'; + + // 全局变量 + var selectedStudentIds = []; + var currentHistoryPage = 1; + + // 显示批量加减分模态框 + function showBatchPointsModal() { + selectedStudentIds = []; + document.querySelectorAll('.student-checkbox:checked').forEach(cb => { + selectedStudentIds.push(parseInt(cb.dataset.id)); + }); + + if (selectedStudentIds.length === 0) { + showToast('请先选择学生', 'warning'); + return; + } + + document.getElementById('selectedStudentsCount').innerHTML = `${selectedStudentIds.length} 人`; + document.getElementById('pointsChange').value = ''; + document.getElementById('pointsReason').value = ''; + document.getElementById('batchPointsModal').style.display = 'flex'; + } + + // 提交批量加减分 + async function submitBatchPoints() { + const pointsChange = parseInt(document.getElementById('pointsChange').value); + const reason = document.getElementById('pointsReason').value; + + if (isNaN(pointsChange) || pointsChange === 0) { + showToast('分值不能为0', 'error'); + return; + } + + if (!reason.trim()) { + showToast('请填写原因', 'error'); + return; + } + + const res = await apiPost('/api/admin/conduct/add', { + student_ids: selectedStudentIds, + points_change: pointsChange, + reason: reason + }); + + if (res && res.success) { + showToast(`操作成功: ${res.data.success_count} 人成功`); + closeModal('batchPointsModal'); + loadStudents(); + if (typeof loadConductStudents === 'function') loadConductStudents(); + } else { + showToast(res?.message || '操作失败', 'error'); + } + } + + // 撤销扣分记录 + async function revokeRecord(recordId) { + if (!confirm('确定要撤销这条记录吗?撤销后学生分数将恢复。')) return; + + const res = await apiPost('/api/admin/conduct/revoke', { record_id: recordId }); + if (res && res.success) { + showToast('撤销成功'); + loadHistory(currentHistoryPage); + } else { + showToast(res?.message || '撤销失败', 'error'); + } + } + + // 反撤销(恢复)记录 + async function restoreRecord(recordId) { + if (!confirm('确定要反撤销这条记录吗?分数变动将重新生效。')) return; + + const res = await apiPost('/api/admin/conduct/restore', { record_id: recordId }); + if (res && res.success) { + showToast('反撤销成功'); + loadHistory(currentHistoryPage); + } else { + showToast(res?.message || '反撤销失败', 'error'); + } + } + + window.showBatchPointsModal = showBatchPointsModal; + window.submitBatchPoints = submitBatchPoints; + window.revokeRecord = revokeRecord; + window.restoreRecord = restoreRecord; +})(); diff --git a/frontend/assets/js/modules/student-mgmt.js b/frontend/assets/js/modules/student-mgmt.js new file mode 100644 index 0000000..a884e5e --- /dev/null +++ b/frontend/assets/js/modules/student-mgmt.js @@ -0,0 +1,233 @@ +/** + * 班级操行分管理系统 - 学生管理函数 + * + * 开发者: Canglan + * 联系方式: admin@sea-studio.top + * 版权归属: Sea Network Technology Studio + * 许可证: MIT License + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { + 'use strict'; + + // 显示新增学生模态框 + function showAddStudentModal() { + document.getElementById('addStudentModal').style.display = 'flex'; + document.getElementById('addStudentForm').reset(); + } + + // 提交新增学生 + async function submitAddStudent() { + const studentNo = document.getElementById('studentNo').value.trim(); + const name = document.getElementById('studentName').value.trim(); + const parentPhone = document.getElementById('parentPhone').value.trim(); + + if (!studentNo || !name) { + showToast('请填写学号和姓名', 'warning'); + return; + } + + const res = await apiPost('/api/admin/students', { + student_no: studentNo, + name: name, + parent_phone: parentPhone, + dormitory_number: document.getElementById('addDormitoryNumber').value.trim() + }); + + if (res && res.success) { + showToast('学生添加成功'); + closeModal('addStudentModal'); + loadStudents(); + } else { + showToast(res?.message || '添加失败', 'error'); + } + } + + // 显示编辑学生模态框 + function showEditStudentModal(studentId, studentNo, name, phone, dormitoryNumber) { + document.getElementById('editStudentId').value = studentId; + document.getElementById('editStudentNo').value = studentNo; + document.getElementById('editStudentName').value = name; + document.getElementById('editStudentPhone').value = phone || ''; + document.getElementById('editDormitoryNumber').value = dormitoryNumber || ''; + document.getElementById('editStudentModal').style.display = 'flex'; + } + + // 提交编辑学生 + async function submitEditStudent() { + const studentId = document.getElementById('editStudentId').value; + const name = document.getElementById('editStudentName').value.trim(); + const phone = document.getElementById('editStudentPhone').value.trim(); + + if (!name) { + showToast('请输入姓名', 'warning'); + return; + } + + const res = await apiPut(`/api/admin/students/${studentId}`, { + name: name, + parent_phone: phone || null, + dormitory_number: document.getElementById('editDormitoryNumber').value.trim() + }); + + if (res && res.success) { + showToast('学生信息更新成功'); + closeModal('editStudentModal'); + location.reload(); + } else { + showToast(res?.message || '更新失败', 'error'); + } + } + + // 显示重置学生密码模态框 + function showResetStudentPasswordModal(studentId, name) { + document.getElementById('resetStudentId').value = studentId; + document.getElementById('resetStudentInfo').textContent = `正在重置学生 "${name}" 的密码`; + document.getElementById('newStudentPassword').value = ''; + document.getElementById('resetStudentPasswordModal').style.display = 'flex'; + } + + // 提交重置学生密码 + async function submitResetStudentPassword() { + const studentId = document.getElementById('resetStudentId').value; + const newPassword = document.getElementById('newStudentPassword').value; + + if (!newPassword || newPassword.length < 6) { + showToast('密码至少6位', 'warning'); + return; + } + + const res = await apiPost(`/api/admin/students/reset-password/${studentId}`, { + new_password: newPassword + }); + + if (res && res.success) { + showToast('密码重置成功'); + closeModal('resetStudentPasswordModal'); + } else { + showToast(res?.message || '重置失败', 'error'); + } + } + + // 删除学生 + async function deleteStudent(studentId, name) { + if (!confirm(`确定要删除学生 "${name}" 吗?删除后学生账号将被禁用。`)) return; + + const res = await apiDelete(`/api/admin/students/${studentId}`); + + if (res && res.success) { + showToast('学生删除成功'); + location.reload(); + } else { + showToast(res?.message || '删除失败', 'error'); + } + } + + // 显示导入模态框 + function showImportModal() { + document.getElementById('importModal').style.display = 'flex'; + document.getElementById('importPreview').style.display = 'none'; + document.getElementById('importPreview').innerHTML = ''; + document.getElementById('importBtn').style.display = 'none'; + document.getElementById('importFile').value = ''; + } + + // 预览导入文件 + function previewImportFile() { + const file = document.getElementById('importFile').files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = function(e) { + try { + const data = JSON.parse(e.target.result); + const students = data.students || []; + + let html = '

预览数据

'; + html += ''; + html += ''; + + students.forEach(s => { + html += ` + + + + + `; + }); + + html += `
学号姓名家长手机号初始密码
${escapeHtml(s.student_no || '')}${escapeHtml(s.name || '')}${escapeHtml(s.parent_phone || '')}${escapeHtml(s.password || '123456')}

共 ${students.length} 条记录,初始操行分默认为60分

`; + document.getElementById('importPreview').innerHTML = html; + document.getElementById('importPreview').style.display = 'block'; + document.getElementById('importBtn').style.display = 'inline-block'; + } catch (error) { + showToast('JSON格式错误', 'error'); + } + }; + reader.readAsText(file); + } + + // 执行导入 + async function doImport() { + const file = document.getElementById('importFile').files[0]; + if (!file) { + showToast('请选择文件', 'warning'); + return; + } + + const formData = new FormData(); + formData.append('file', file); + + const token = getToken(); + const response = await fetch(`${API_BASE_URL}/api/admin/students/import`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}` + }, + body: formData + }); + + const result = await response.json(); + + if (result.success) { + showToast(result.message); + closeModal('importModal'); + loadStudents(); + + // 显示详细导入结果 + if (result.data && result.data.results) { + const failedList = result.data.results.filter(r => !r.success); + if (failedList.length > 0) { + let detail = '失败详情:\n'; + failedList.forEach(r => { + detail += `${r.student_no || '未知'}: ${r.error}\n`; + }); + alert(detail); + } + } + } else { + showToast(result.message, 'error'); + } + } + + // 绑定文件选择事件 + document.addEventListener('DOMContentLoaded', () => { + const fileInput = document.getElementById('importFile'); + if (fileInput) { + fileInput.addEventListener('change', previewImportFile); + } + }); + + window.showAddStudentModal = showAddStudentModal; + window.submitAddStudent = submitAddStudent; + window.showEditStudentModal = showEditStudentModal; + window.submitEditStudent = submitEditStudent; + window.showResetStudentPasswordModal = showResetStudentPasswordModal; + window.submitResetStudentPassword = submitResetStudentPassword; + window.deleteStudent = deleteStudent; + window.showImportModal = showImportModal; + window.previewImportFile = previewImportFile; + window.doImport = doImport; +})(); diff --git a/frontend/assets/js/modules/subject-mgmt.js b/frontend/assets/js/modules/subject-mgmt.js new file mode 100644 index 0000000..9c55142 --- /dev/null +++ b/frontend/assets/js/modules/subject-mgmt.js @@ -0,0 +1,47 @@ +/** + * 班级操行分管理系统 - 科目管理函数 + * + * 开发者: Canglan + * 联系方式: admin@sea-studio.top + * 版权归属: Sea Network Technology Studio + * 许可证: MIT License + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { + 'use strict'; + + // 显示添加科目模态框 + function showAddSubjectModal() { + document.getElementById('addSubjectModal').style.display = 'flex'; + document.getElementById('addSubjectForm').reset(); + } + + // 提交添加科目 + async function submitAddSubject() { + const subjectName = document.getElementById('subjectName').value.trim(); + const subjectCode = document.getElementById('subjectCode').value.trim(); + + if (!subjectName) { + showToast('请填写科目名称', 'warning'); + return; + } + + const res = await apiPost('/api/subject/create', { + subject_name: subjectName, + subject_code: subjectCode + }); + + if (res && res.success) { + showToast('科目添加成功'); + closeModal('addSubjectModal'); + loadSubjects(); + } else { + showToast(res?.message || '添加失败', 'error'); + } + } + + window.showAddSubjectModal = showAddSubjectModal; + window.submitAddSubject = submitAddSubject; +})(); diff --git a/frontend/assets/js/modules/utils.js b/frontend/assets/js/modules/utils.js new file mode 100644 index 0000000..92e807a --- /dev/null +++ b/frontend/assets/js/modules/utils.js @@ -0,0 +1,38 @@ +/** + * 班级操行分管理系统 - 通用工具函数 + * + * 开发者: Canglan + * 联系方式: admin@sea-studio.top + * 版权归属: Sea Network Technology Studio + * 许可证: MIT License + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { + 'use strict'; + + // HTML转义 + function escapeHtml(str) { + if (!str) return ''; + return str.replace(/[&<>]/g, function(m) { + if (m === '&') return '&'; + if (m === '<') return '<'; + if (m === '>') return '>'; + return m; + }); + } + + // 全选功能 + function toggleSelectAll() { + const selectAll = document.getElementById('selectAll'); + if (selectAll) { + document.querySelectorAll('.student-checkbox').forEach(cb => { + cb.checked = selectAll.checked; + }); + } + } + + window.escapeHtml = escapeHtml; + window.toggleSelectAll = toggleSelectAll; +})(); diff --git a/frontend/assets/js/semesters.js b/frontend/assets/js/semesters.js new file mode 100644 index 0000000..832d427 --- /dev/null +++ b/frontend/assets/js/semesters.js @@ -0,0 +1,293 @@ +/** + * 班级操行分管理系统 - 学期管理页JS + * + * 开发者: Canglan + * 版权归属: Sea Network Technology Studio + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { +'use strict'; + +let archiveSemesterId = null; +let archivePage = 1; +let archiveTotalPages = 1; +let associateSemesterId = null; + +function fillSemesterDates(type) { + const now = new Date(); + const currentYear = now.getFullYear(); + const currentMonth = now.getMonth() + 1; + const startDateInput = document.getElementById('semesterStartDate'); + const endDateInput = document.getElementById('semesterEndDate'); + + if (type === 'upper') { + const year = currentMonth >= 6 ? currentYear : currentYear - 1; + const endYear = year + 1; + let febDay = 28; + if ((endYear % 4 === 0 && endYear % 100 !== 0) || endYear % 400 === 0) { + febDay = 29; + } + startDateInput.value = year + '-09-01'; + endDateInput.value = endYear + '-02-' + febDay; + } else if (type === 'lower') { + startDateInput.value = currentYear + '-03-01'; + endDateInput.value = currentYear + '-07-15'; + } +} + +async function loadSemesters() { + const res = await apiGet('/api/semester/list'); + if (res && res.success) { + let html = ''; + const semesters = res.data || []; + semesters.forEach(sem => { + let statusText = ''; + let statusClass = ''; + if (sem.is_archived) { + statusText = '已归档'; + statusClass = 'status-badge status-not_submitted'; + } else if (sem.is_active) { + statusText = '当前学期'; + statusClass = 'status-badge status-submitted'; + } else { + statusText = '未激活'; + statusClass = 'status-badge status-late'; + } + + let actions = ''; + const startDate = sem.start_date || ''; + const endDate = sem.end_date || ''; + if (!sem.is_archived) { + actions += ` `; + if (!sem.is_active) { + actions += ` `; + } + actions += ` `; + actions += ` `; + } + if (sem.is_archived) { + actions += ``; + } + + html += ` + ${escapeHtml(sem.semester_name)} + ${formatDate(sem.start_date)} + ${formatDate(sem.end_date)} + ${statusText} + ${formatDateTime(sem.created_at)} + ${actions} + `; + }); + if (semesters.length === 0) { + html = '暂无学期,请点击上方按钮创建新学期'; + } + document.getElementById('semesterList').innerHTML = html; + } +} + +function showCreateSemesterModal() { + document.getElementById('semesterName').value = ''; + document.getElementById('semesterStartDate').value = ''; + document.getElementById('semesterEndDate').value = ''; + document.getElementById('createSemesterModal').style.display = 'flex'; +} + +async function submitCreateSemester() { + const name = document.getElementById('semesterName').value.trim(); + const startDate = document.getElementById('semesterStartDate').value; + const endDate = document.getElementById('semesterEndDate').value; + + if (!name) { + showToast('请输入学期名称', 'warning'); + return; + } + + const res = await apiPost('/api/semester/create', { + semester_name: name, + start_date: startDate || null, + end_date: endDate || null + }); + + if (res && res.success) { + showToast(res.message || '学期创建成功'); + closeModal('createSemesterModal'); + loadSemesters(); + } else { + showToast(res?.message || '创建失败', 'error'); + } +} + +async function activateSemester(semesterId) { + if (!confirm('确认将此学期设为当前活跃学期?其他学期将被设为非活跃。')) { + return; + } + + const res = await apiPut(`/api/semester/activate/${semesterId}`); + if (res && res.success) { + showToast(res.message || '已设为当前学期'); + loadSemesters(); + } else { + showToast(res?.message || '操作失败', 'error'); + } +} + +function showEditSemesterModal(id, name, startDate, endDate) { + document.getElementById('editSemesterId').value = id; + document.getElementById('editSemesterName').value = name; + document.getElementById('editSemesterStartDate').value = startDate || ''; + document.getElementById('editSemesterEndDate').value = endDate || ''; + document.getElementById('editSemesterModal').style.display = 'flex'; +} + +async function submitEditSemester() { + const id = document.getElementById('editSemesterId').value; + const name = document.getElementById('editSemesterName').value.trim(); + const startDate = document.getElementById('editSemesterStartDate').value; + const endDate = document.getElementById('editSemesterEndDate').value; + + if (!name) { + showToast('请输入学期名称', 'warning'); + return; + } + + const data = { semester_name: name }; + if (startDate) data.start_date = startDate; + if (endDate) data.end_date = endDate; + + const res = await apiPut(`/api/semester/update/${id}`, data); + if (res && res.success) { + showToast(res.message || '更新成功'); + closeModal('editSemesterModal'); + loadSemesters(); + } else { + showToast(res?.message || '更新失败', 'error'); + } +} + +async function deleteSemester() { + const id = document.getElementById('editSemesterId').value; + if (!confirm('确定要删除该学期吗?如果学期已有归档数据则无法删除。')) { + return; + } + + const res = await apiDelete(`/api/semester/delete/${id}`); + if (res && res.success) { + showToast(res.message || '删除成功'); + closeModal('editSemesterModal'); + loadSemesters(); + } else { + showToast(res?.message || '删除失败', 'error'); + } +} + +function showAssociateConfirm(id, name, startDate, endDate) { + associateSemesterId = id; + const dateRange = startDate ? `${startDate} ~ ${endDate || '至今'}` : '未设置日期范围'; + document.getElementById('associateConfirmText').innerHTML = + `即将关联 ${dateRange} 内的所有未分配学期的操行分记录和考勤记录到学期 "${name}"。`; + document.getElementById('associateConfirmModal').style.display = 'flex'; +} + +async function confirmAssociate() { + if (!associateSemesterId) return; + + const res = await apiPost(`/api/semester/${associateSemesterId}/associate`); + if (res && res.success) { + showToast(res.message || '关联成功'); + closeModal('associateConfirmModal'); + associateSemesterId = null; + } else { + showToast(res?.message || '关联失败', 'error'); + } +} + +function showArchiveConfirm(semesterId, semesterName) { + archiveSemesterId = semesterId; + document.getElementById('archiveResetScores').checked = false; + document.getElementById('archiveConfirmText').innerHTML = + `确定要归档学期 "${semesterName}" 吗?
归档后将保存所有学生的当前操行分快照,该学期数据将变为只读。`; + document.getElementById('archiveConfirmModal').style.display = 'flex'; +} + +async function confirmArchive() { + if (!archiveSemesterId) return; + + const resetScores = document.getElementById('archiveResetScores').checked; + const url = `/api/semester/archive/${archiveSemesterId}?reset_scores=${resetScores}`; + + const res = await apiPost(url); + if (res && res.success) { + showToast(res.message || '归档成功'); + closeModal('archiveConfirmModal'); + archiveSemesterId = null; + loadSemesters(); + } else { + showToast(res?.message || '归档失败', 'error'); + } +} + +async function viewArchiveData(semesterId, semesterName, page) { + page = page || 1; + archivePage = page; + document.getElementById('archiveDataTitle').textContent = `归档数据 - ${semesterName}`; + + const res = await apiGet(`/api/semester/archive/${semesterId}/records`, { + page: page, page_size: 50 + }); + + if (res && res.success) { + const data = res.data || {}; + const archives = data.archives || []; + let html = ''; + archives.forEach(a => { + html += ` + ${a.rank_position || '-'} + ${escapeHtml(a.student_no)} + ${escapeHtml(a.student_name)} + ${a.final_points} + ${a.attendance_present || 0} + ${a.attendance_absent || 0} + ${a.attendance_late || 0} + ${a.attendance_leave || 0} + ${a.homework_submitted || 0} + ${a.homework_not_submitted || 0} + ${a.homework_late || 0} + `; + }); + if (archives.length === 0) { + html = '暂无归档数据'; + } + document.getElementById('archiveDataList').innerHTML = html; + + archiveTotalPages = data.total_pages || 1; + renderArchivePagination(semesterId, semesterName); + document.getElementById('archiveDataModal').style.display = 'flex'; + } else { + showToast(res?.message || '获取归档数据失败', 'error'); + } +} + +function renderArchivePagination(semesterId, semesterName) { + renderSmartPagination('archivePagination', archivePage, archiveTotalPages, function(page) { + viewArchiveData(semesterId, semesterName, page); + }); +} + +loadSemesters(); + +window.fillSemesterDates = fillSemesterDates; +window.showCreateSemesterModal = showCreateSemesterModal; +window.submitCreateSemester = submitCreateSemester; +window.activateSemester = activateSemester; +window.showEditSemesterModal = showEditSemesterModal; +window.submitEditSemester = submitEditSemester; +window.deleteSemester = deleteSemester; +window.showAssociateConfirm = showAssociateConfirm; +window.confirmAssociate = confirmAssociate; +window.showArchiveConfirm = showArchiveConfirm; +window.confirmArchive = confirmArchive; +window.viewArchiveData = viewArchiveData; + +})(); diff --git a/frontend/assets/js/student-homework.js b/frontend/assets/js/student-homework.js new file mode 100644 index 0000000..0886a66 --- /dev/null +++ b/frontend/assets/js/student-homework.js @@ -0,0 +1,38 @@ +/** + * 班级操行分管理系统 - 学生端作业情况JS + * + * 开发者: Canglan + * 版权归属: Sea Network Technology Studio + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { +'use strict'; + +const STUDENT_ID = window.PAGE_CONFIG.studentId; + +async function loadHomework() { + const res = await apiGet(`/api/student/homework/${STUDENT_ID}`); + if (res && res.success) { + let html = ''; + res.data.homework.forEach(hw => { + const pointsDisplay = hw.points ? hw.points + '分' : '-'; + html += ` + ${escapeHtml(hw.subject_name)} + ${hw.deadline || hw.created_at} + ${pointsDisplay} + ${escapeHtml(hw.comments || '-')} + ${escapeHtml(hw.title)} + `; + }); + if (res.data.homework.length === 0) { + html = '暂无作业'; + } + document.getElementById('homeworkList').innerHTML = html; + } +} + +loadHomework(); + +})(); diff --git a/frontend/assets/js/students-manage.js b/frontend/assets/js/students-manage.js new file mode 100644 index 0000000..759a26b --- /dev/null +++ b/frontend/assets/js/students-manage.js @@ -0,0 +1,95 @@ +/** + * 班级操行分管理系统 - 学生管理页JS + * + * 开发者: Canglan + * 版权归属: Sea Network Technology Studio + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { +'use strict'; + +const userRole = window.PAGE_CONFIG.role; +let currentPage = 1; +let totalPages = 1; + +async function loadStudents(page = 1) { + currentPage = page; + const search = document.getElementById('searchInput').value; + const res = await apiGet('/api/admin/students', { page, page_size: 20, search }); + + if (res && res.success) { + let html = ''; + res.data.students.forEach(student => { + html += ` + + ${escapeHtml(student.student_no)} + ${escapeHtml(student.name)} + ${escapeHtml(student.dormitory_number || '-')} + ${student.total_points} + ${userRole === '班主任' ? `${student.parent_phone ? student.parent_phone.slice(0,3) + '******' + student.parent_phone.slice(-2) : '-'}` : ''} + + + ${userRole === '班主任' ? ` + + + ` : ''} + + `; + }); + + if (res.data.students.length === 0) { + html = `暂无学生数据`; + } + + document.getElementById('studentList').innerHTML = html; + + totalPages = res.data.total_pages || 1; + renderPagination(); + } +} + +function renderPagination() { + renderSmartPagination('pagination', currentPage, totalPages, function(page) { + loadStudents(page); + }); +} + +function showSinglePointsModal(studentId, studentName) { + window.selectedStudentIds = [studentId]; + document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`; + document.getElementById('pointsChange').value = ''; + document.getElementById('pointsReason').value = ''; + document.getElementById('batchPointsModal').style.display = 'flex'; +} + +async function unlockStudent(studentNo, studentName) { + if (!confirm(`确定要解除学生 "${studentName}" 的登录锁定吗?\n(适用于多次登录失败被禁止登录的情况)`)) { + return; + } + + const res = await apiPost('/api/admin/unlock-user', { + username: studentNo + }); + + if (res && res.success) { + showToast(res.message || '解锁成功'); + } else { + showToast(res?.message || '解锁失败', 'error'); + } +} + +loadStudents(); + +let searchTimeout; +document.getElementById('searchInput').addEventListener('input', () => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => loadStudents(1), 500); +}); + +window.loadStudents = loadStudents; +window.showSinglePointsModal = showSinglePointsModal; +window.unlockStudent = unlockStudent; + +})(); diff --git a/frontend/assets/js/subjects.js b/frontend/assets/js/subjects.js new file mode 100644 index 0000000..4e83ed3 --- /dev/null +++ b/frontend/assets/js/subjects.js @@ -0,0 +1,102 @@ +/** + * 班级操行分管理系统 - 科目管理页JS + * + * 开发者: Canglan + * 版权归属: Sea Network Technology Studio + * + * 版权所有 © Sea Network Technology Studio + */ + +(function() { +'use strict'; + +async function loadSubjects() { + const res = await apiGet('/api/subject/list'); + if (res && res.success) { + let html = ''; + res.data.subjects.forEach(sub => { + html += ` +
+ ${escapeHtml(sub.subject_name)} + ${escapeHtml(sub.subject_code || '')} + + ${sub.is_active ? '启用' : '禁用'} + + + + +
+ `; + }); + if (res.data.subjects.length === 0) { + html = '

暂无科目,请点击"添加科目"

'; + } + document.getElementById('subjectList').innerHTML = html; + } +} + +async function toggleSubject(subjectId, enable) { + const res = await apiPut(`/api/subject/update/${subjectId}`, { is_active: enable }); + if (res && res.success) { + showToast(enable ? '科目已启用' : '科目已禁用'); + loadSubjects(); + } else { + showToast(res?.message || '操作失败', 'error'); + } +} + +async function deleteSubject(subjectId) { + if (!confirm('确定要删除该科目吗?')) return; + const res = await apiDelete('/api/subject/delete/' + subjectId); + if (res && res.success) { + showToast('科目删除成功'); + loadSubjects(); + } else { + showToast(res?.message || '删除失败', 'error'); + } +} + +function showEditSubjectModal(subjectId, name, code, sortOrder) { + document.getElementById('editSubjectId').value = subjectId; + document.getElementById('editSubjectName').value = name; + document.getElementById('editSubjectCode').value = code; + document.getElementById('editSubjectSortOrder').value = sortOrder; + document.getElementById('editSubjectModal').style.display = 'flex'; +} + +async function submitEditSubject() { + const subjectId = document.getElementById('editSubjectId').value; + const subjectName = document.getElementById('editSubjectName').value.trim(); + const subjectCode = document.getElementById('editSubjectCode').value.trim(); + const sortOrder = document.getElementById('editSubjectSortOrder').value; + + if (!subjectName) { + showToast('请填写科目名称', 'warning'); + return; + } + + const data = { subject_name: subjectName }; + if (subjectCode) data.subject_code = subjectCode; + if (sortOrder !== '') data.sort_order = parseInt(sortOrder); + + const res = await apiPut(`/api/subject/update/${subjectId}`, data); + if (res && res.success) { + showToast('科目更新成功'); + closeModal('editSubjectModal'); + loadSubjects(); + } else { + showToast(res?.message || '更新失败', 'error'); + } +} + +loadSubjects(); + +window.loadSubjects = loadSubjects; +window.toggleSubject = toggleSubject; +window.deleteSubject = deleteSubject; +window.showEditSubjectModal = showEditSubjectModal; +window.submitEditSubject = submitEditSubject; + +})(); diff --git a/frontend/config.php b/frontend/config.php index 271ee7c..c77a01a 100644 --- a/frontend/config.php +++ b/frontend/config.php @@ -60,18 +60,7 @@ define('SITE_NAME', $config['SITE_NAME']); define('SESSION_TIMEOUT', (int)$config['SESSION_TIMEOUT']); define('ICP_ENABLED', $config['ICP_ENABLED'] !== 'false'); define('ICP_NUMBER', $config['ICP_NUMBER'] ?? ''); - -// 扣分规则配置(有默认值,不强制要求在.env中配置) -define('DEDUCTION_HOMEWORK_NOT_SUBMIT', (int)($config['DEDUCTION_HOMEWORK_NOT_SUBMIT'] ?? 2)); -define('DEDUCTION_HOMEWORK_LATE', (int)($config['DEDUCTION_HOMEWORK_LATE'] ?? 1)); -define('HOMEWORK_MAX_POINTS', (int)($config['HOMEWORK_MAX_POINTS'] ?? 5)); -define('DEDUCTION_ATTENDANCE_ABSENT', (int)($config['DEDUCTION_ATTENDANCE_ABSENT'] ?? 3)); -define('DEDUCTION_ATTENDANCE_LATE', (int)($config['DEDUCTION_ATTENDANCE_LATE'] ?? 1)); -define('DEDUCTION_ATTENDANCE_LEAVE', (int)($config['DEDUCTION_ATTENDANCE_LEAVE'] ?? 0)); - -// 学生初始操行分 -define('STUDENT_INITIAL_POINTS', (int)($config['STUDENT_INITIAL_POINTS'] ?? 60)); - +// 会话配置 // 会话配置 ini_set('session.cookie_httponly', 1); ini_set('session.use_only_cookies', 1); diff --git a/frontend/includes/header.php b/frontend/includes/header.php index e2586ab..fe3b66e 100644 --- a/frontend/includes/header.php +++ b/frontend/includes/header.php @@ -45,11 +45,34 @@ $page_title = $page_title ?? '首页'; window.API_BASE_URL = ''; window.JWT_STORAGE_KEY = ''; window.USER_STORAGE_KEY = ''; - window.DEDUCTION_HOMEWORK_NOT_SUBMIT = ; - window.DEDUCTION_HOMEWORK_LATE = ; - window.DEDUCTION_ATTENDANCE_ABSENT = ; - window.DEDUCTION_ATTENDANCE_LATE = ; - window.DEDUCTION_ATTENDANCE_LEAVE = ; - window.STUDENT_INITIAL_POINTS = ; + + \ No newline at end of file diff --git a/frontend/parent/dashboard.php b/frontend/parent/dashboard.php index b0b16ff..1b0ffbd 100644 --- a/frontend/parent/dashboard.php +++ b/frontend/parent/dashboard.php @@ -31,6 +31,7 @@ include __DIR__ . '/../includes/header.php';
--
--
+
@@ -41,6 +42,10 @@ include __DIR__ . '/../includes/header.php';
班级排名
--
+
+
缺交次数
+
--
+
@@ -71,6 +76,10 @@ async function loadDashboard() { document.getElementById('childName').textContent = res.data.student_name; document.getElementById('childNo').textContent = res.data.student_no; document.getElementById('totalPoints').textContent = res.data.total_points; + if (res.data.dormitory_number) { + document.getElementById('childDormitory').textContent = '宿舍号: ' + res.data.dormitory_number; + document.getElementById('childDormitory').style.display = ''; + } } // 加载排名信息 @@ -83,6 +92,16 @@ async function loadDashboard() { document.getElementById('studentRank').textContent = '--'; } } + + // 加载作业缺交次数 + const hwRes = await apiGet('/api/parent/child/homework'); + if (hwRes && hwRes.success && hwRes.data.homework) { + const homework = hwRes.data.homework; + const total = homework.length; + const notSubmitted = homework.filter(h => h.status === 'not_submitted' || h.status === 'late').length; + document.getElementById('homeworkMissing').textContent = `缺交 ${notSubmitted}/${total} 次`; + } + // 显示初始分提示 const initialPoints = window.STUDENT_INITIAL_POINTS || 60; document.getElementById('initialPointsHint').textContent = `初始操行分为 ${initialPoints} 分`; diff --git a/frontend/parent/history.php b/frontend/parent/history.php index 94778b8..b6c5538 100644 --- a/frontend/parent/history.php +++ b/frontend/parent/history.php @@ -84,7 +84,7 @@ async function loadHistory(page) { html += ` ${formatDateTime(record.created_at)} ${escapeHtml(record.related_type || '手动')} - ${escapeHtml(record.reason || '-')} + ${escapeHtml(record.reason || '-')} ${pointsText} ${escapeHtml(record.recorder_name || '-')} `; diff --git a/frontend/student/dashboard.php b/frontend/student/dashboard.php index a139cf3..40fc616 100644 --- a/frontend/student/dashboard.php +++ b/frontend/student/dashboard.php @@ -94,8 +94,8 @@ include __DIR__ . '/../includes/header.php';
--
-
作业完成率
-
--%
+
缺交次数
+
--
本月出勤率
@@ -103,6 +103,11 @@ include __DIR__ . '/../includes/header.php';
+ +
最新操行分记录
@@ -136,10 +141,10 @@ include __DIR__ . '/../includes/header.php'; 科目 - 作业标题 - 截止日期 - 状态 + 时间 + 分值 备注 + 作业 @@ -296,6 +301,13 @@ include __DIR__ . '/../includes/header.php'; document.getElementById('recentRecords').innerHTML = html; } + // 获取个人信息(宿舍号) + const infoRes = await apiGet('/api/student/my-info'); + if (infoRes && infoRes.success && infoRes.data.dormitory_number) { + document.getElementById('dormitoryNumber').textContent = infoRes.data.dormitory_number; + document.getElementById('dormitoryInfo').style.display = ''; + } + // 获取班级排名 const rankingRes = await apiGet('/api/student/ranking', { limit: 100 }); if (rankingRes && rankingRes.success) { @@ -307,13 +319,14 @@ include __DIR__ . '/../includes/header.php'; document.getElementById('studentRank').textContent = '--'; } } - - // 获取作业统计 + // 获取作业统计 - 缺交次数 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 notSubmitted = (stats.not_submitted || 0) + (stats.late || 0); + const total = stats.total || 0; + document.getElementById('homeworkRate').textContent = `缺交 ${notSubmitted}/${total} 次`; + } } // 获取考勤统计 @@ -381,13 +394,14 @@ include __DIR__ . '/../includes/header.php'; if (res && res.success) { let html = ''; res.data.homework.forEach(hw => { + const pointsDisplay = hw.points ? hw.points + '分' : '-'; html += ` ${escapeHtml(hw.subject_name)} - ${escapeHtml(hw.title)} - ${escapeHtml(hw.deadline)} - ${getStatusBadge(hw.status, 'homework')} + ${hw.deadline || hw.created_at} + ${pointsDisplay} ${escapeHtml(hw.comments || '-')} + ${escapeHtml(hw.title)} `; }); diff --git a/frontend/student/homework.php b/frontend/student/homework.php index fbf8ff2..0c8b5a0 100644 --- a/frontend/student/homework.php +++ b/frontend/student/homework.php @@ -36,7 +36,7 @@ include __DIR__ . '/../includes/header.php';
- +
科目作业标题截止日期状态备注
科目时间分值备注作业
@@ -44,31 +44,8 @@ include __DIR__ . '/../includes/header.php';
- + + \ No newline at end of file diff --git a/sql/init.sql b/sql/init.sql index 7685301..aed8b66 100644 --- a/sql/init.sql +++ b/sql/init.sql @@ -48,8 +48,10 @@ CREATE TABLE IF NOT EXISTS `students` ( `name` VARCHAR(50) NOT NULL, `total_points` INT DEFAULT 60, `parent_phone` VARCHAR(20) DEFAULT NULL, + `dormitory_number` VARCHAR(20) DEFAULT NULL COMMENT '宿舍号', `status` TINYINT DEFAULT 1, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `points_updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '分数最后更新时间', `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; @@ -57,7 +59,7 @@ CREATE TABLE IF NOT EXISTS `students` ( CREATE TABLE IF NOT EXISTS `users` ( `user_id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, - `password_hash` VARCHAR(64) NOT NULL, + `password_hash` VARCHAR(255) NOT NULL, `real_name` VARCHAR(50) NOT NULL, `user_type` ENUM('student', 'parent', 'admin') NOT NULL, `student_id` INT DEFAULT NULL, @@ -440,3 +442,34 @@ INSERT IGNORE INTO `subjects` (`subject_name`, `subject_code`, `sort_order`) VAL ('英语', 'ENG', 3); SELECT '数据库初始化/迁移完成!' AS message; + +-- =========================================== +-- v2.0 迁移脚本 +-- =========================================== + +-- 扩展密码哈希字段长度以支持bcrypt +ALTER TABLE users MODIFY COLUMN password_hash VARCHAR(255) NOT NULL; + +-- 添加宿舍号字段 +SET @dbname = DATABASE(); +SET @tablename = 'students'; +SET @columnname = 'dormitory_number'; +SET @preparedStatement = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = @columnname) > 0, + 'SELECT 1', + 'ALTER TABLE students ADD COLUMN dormitory_number VARCHAR(20) DEFAULT NULL COMMENT ''宿舍号'' AFTER parent_phone' +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +-- 添加分数最后更新时间字段 +SET @columnname = 'points_updated_at'; +SET @preparedStatement = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = @columnname) > 0, + 'SELECT 1', + 'ALTER TABLE students ADD COLUMN points_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT ''分数最后更新时间''' +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists;