v1.7版本更新
This commit is contained in:
@@ -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)、清理废弃全局变量、角色权限表精确化 |
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
# ===========================================
|
||||
# 密码加密配置
|
||||
|
||||
@@ -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=["调试"])
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
"""检查是否为公开路径"""
|
||||
|
||||
@@ -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, ""
|
||||
|
||||
|
||||
@@ -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 = """
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
29
backend/routes/config.py
Normal file
29
backend/routes/config.py
Normal file
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
22
backend/schemas/config.py
Normal file
22
backend/schemas/config.py
Normal file
@@ -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
|
||||
@@ -21,6 +21,7 @@ class StudentInfo(BaseModel):
|
||||
name: str
|
||||
total_points: int
|
||||
parent_phone: Optional[str] = None
|
||||
dormitory_number: Optional[str] = None
|
||||
status: int
|
||||
|
||||
|
||||
|
||||
@@ -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": "更新失败"}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -147,165 +147,8 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var currentEditUserId = null;
|
||||
var currentResetUserId = null;
|
||||
|
||||
async function loadAdmins() {
|
||||
const res = await apiGet('/api/admin/list');
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.admins.forEach(admin => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(admin.username)}</td>
|
||||
<td>${escapeHtml(admin.real_name)}</td>
|
||||
<td>${escapeHtml(admin.role_type)}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="showEditAdminModal(${admin.user_id}, '${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}', '${escapeHtml(admin.role_type)}')">编辑</button>
|
||||
<button class="btn btn-sm btn-warning" onclick="resetAdminPassword(${admin.user_id}, '${escapeHtml(admin.real_name)}')">重置密码</button>
|
||||
<button class="btn btn-sm btn-info" onclick="unlockUser('${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}')">解锁</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteAdmin(${admin.user_id}, '${escapeHtml(admin.real_name)}')">删除</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.admins.length === 0) {
|
||||
html = '<tr><td colspan="4" style="text-align:center;">暂无管理员</td></tr>';
|
||||
}
|
||||
document.getElementById('adminList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
function showAddAdminModal() {
|
||||
document.getElementById('addAdminModal').style.display = 'flex';
|
||||
document.getElementById('adminUsername').value = '';
|
||||
document.getElementById('adminRealName').value = '';
|
||||
document.getElementById('adminPassword').value = '';
|
||||
document.getElementById('adminRole').value = '';
|
||||
}
|
||||
|
||||
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 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');
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) modal.style.display = 'none';
|
||||
}
|
||||
|
||||
loadAdmins();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/modules/admin-mgmt.js"></script>
|
||||
<script src="/assets/js/admins.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.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; }
|
||||
</style>
|
||||
<script>
|
||||
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 = '<div style="text-align:center;padding:20px;color:#999;">加载学生列表失败</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染学生方格
|
||||
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 += `<div class="student-cell${hasRecord ? ' has-record' : ''}"
|
||||
data-id="${student.student_id}"
|
||||
data-name="${escapeHtml(student.name)}"
|
||||
onclick="toggleStudent(this)">
|
||||
<span class="student-cell-name">${escapeHtml(student.name)}</span>
|
||||
<span class="student-cell-no">${escapeHtml(student.student_no)}</span>
|
||||
</div>`;
|
||||
});
|
||||
if (studentsData.length === 0) {
|
||||
html = '<div style="text-align:center;padding:20px;color:#999;width:100%;">暂无学生数据</div>';
|
||||
}
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
// 加载当天已有考勤记录(用于标记 .has-record)
|
||||
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(); // 重新渲染以标记 has-record
|
||||
}
|
||||
}
|
||||
|
||||
// 提交考勤
|
||||
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 += `<tr>
|
||||
<td>${escapeHtml(record.student_no)}</td>
|
||||
<td>${escapeHtml(record.student_name)}</td>
|
||||
<td>${getStatusBadge(record.status, 'attendance')}</td>
|
||||
<td>${escapeHtml(record.reason || '-')}</td>
|
||||
<td>${escapeHtml(record.recorder_name || '-')}</td>
|
||||
<td>${record.deduction_applied ? '已扣分' : '-'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (records.length === 0) {
|
||||
html = '<tr><td colspan="6" style="text-align:center;">暂无考勤记录</td></tr>';
|
||||
}
|
||||
document.getElementById('attendanceList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
// 日期或时段变化时重新加载
|
||||
document.getElementById('attendanceDate').addEventListener('change', function() {
|
||||
loadExistingRecords();
|
||||
loadAttendanceRecords();
|
||||
});
|
||||
document.getElementById('attendanceSlot').addEventListener('change', function() {
|
||||
loadExistingRecords();
|
||||
});
|
||||
|
||||
// 页面初始化
|
||||
initAttendanceButtons();
|
||||
loadStudents();
|
||||
loadAttendanceRecords();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/attendance-manage.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
|
||||
@@ -58,139 +58,10 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var selectedStudentIds = [];
|
||||
|
||||
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 += `<tr>
|
||||
<td><input type="checkbox" class="student-checkbox" data-id="${student.student_id}"></td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td><a href="/admin/history.php?student_id=${student.student_id}" style="color: #3498db; text-decoration: none;">${escapeHtml(student.name)}</a></td>
|
||||
<td>${student.total_points}</td>
|
||||
<td><button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button></td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.students.length === 0) {
|
||||
html = '<tr><td colspan="5" style="text-align:center;">暂无学生数据</td></tr>';
|
||||
}
|
||||
document.getElementById('studentList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
const selectAll = document.getElementById('selectAll');
|
||||
document.querySelectorAll('.student-checkbox').forEach(cb => {
|
||||
cb.checked = selectAll.checked;
|
||||
});
|
||||
}
|
||||
|
||||
function showSinglePointsModal(studentId, studentName) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 获取所有历史记录(获取全部,API限制最大1000条)
|
||||
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 || [];
|
||||
|
||||
// 按学生ID分组历史记录
|
||||
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('; ')
|
||||
});
|
||||
}
|
||||
|
||||
// CSV字段转义函数
|
||||
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;
|
||||
}
|
||||
|
||||
// 构建CSV内容
|
||||
let csv = '\uFEFF'; // BOM for UTF-8
|
||||
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();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/modules/utils.js"></script>
|
||||
<script src="/assets/js/modules/points-mgmt.js"></script>
|
||||
<script src="/assets/js/conduct.js"></script>
|
||||
|
||||
<!-- 批量加减分模态框 -->
|
||||
<div id="batchPointsModal" class="modal">
|
||||
|
||||
@@ -52,79 +52,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var totalStudents = 0;
|
||||
|
||||
async function loadDashboard() {
|
||||
const studentsRes = await apiGet('/api/admin/students');
|
||||
if (studentsRes && studentsRes.success) {
|
||||
document.getElementById('dashboardStats').innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">学生总数</div>
|
||||
<div class="stat-value">${studentsRes.data.total || 0}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
let quickActions = '';
|
||||
if ('<?php echo $role; ?>' === '班主任' || '<?php echo $role; ?>' === '班长' || '<?php echo $role; ?>' === '劳动委员' || '<?php echo $role; ?>' === '志愿委员') {
|
||||
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>';
|
||||
}
|
||||
if ('<?php echo $role; ?>' === '班主任') {
|
||||
quickActions += '<button class="btn btn-success" onclick="location.href=\'/admin/students.php\'">导入学生</button>';
|
||||
quickActions += '<button class="btn btn-secondary" onclick="location.href=\'/admin/conduct.php\'">导出德育分记录</button>';
|
||||
}
|
||||
document.getElementById('quickActions').innerHTML = quickActions || '<p>暂无快捷操作</p>';
|
||||
|
||||
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 += `<tr>
|
||||
<td>${rank}</td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td>${escapeHtml(student.name)}</td>
|
||||
<td>${student.total_points}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (rankingRes.data.ranking.length === 0) {
|
||||
html = '<tr><td colspan="4" style="text-align:center;">暂无数据</td></tr>';
|
||||
}
|
||||
document.getElementById('rankingList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('percentileFilter').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') applyPercentileFilter();
|
||||
});
|
||||
|
||||
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 = '';
|
||||
});
|
||||
}
|
||||
|
||||
loadDashboard();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script>window.PAGE_CONFIG = { role: '<?php echo $role; ?>' };</script>
|
||||
<script src="/assets/js/dashboard.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
@@ -44,13 +44,19 @@ include __DIR__ . '/../includes/header.php';
|
||||
<div class="filter-group">
|
||||
<label>扣分类型</label>
|
||||
<select id="historyRelatedType">
|
||||
<option value="">全部</option>
|
||||
<option value="">全部类型</option>
|
||||
<option value="manual">手动加减分</option>
|
||||
<option value="homework">作业</option>
|
||||
<option value="attendance">考勤</option>
|
||||
<option value="homework">作业扣分</option>
|
||||
<option value="attendance">考勤扣分</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="loadHistory(1)">查询</button>
|
||||
<div class="filter-group" style="min-width:auto;">
|
||||
<label> </label>
|
||||
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;font-size:13px;">
|
||||
<input type="checkbox" id="historyGrouped" onchange="loadHistory(1)"> 批次合并
|
||||
</label>
|
||||
</div>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<button class="btn btn-secondary" onclick="exportHistoryRecords()">导出历史记录</button>
|
||||
<?php endif; ?>
|
||||
@@ -59,7 +65,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<tr id="historyTableHead">
|
||||
<th>时间</th>
|
||||
<th>学生</th>
|
||||
<th>分数变动</th>
|
||||
@@ -77,156 +83,10 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var currentHistoryPage = 1;
|
||||
var totalHistoryPages = 1;
|
||||
var currentUserId = <?php echo intval($_SESSION['user_id']); ?>;
|
||||
|
||||
async function loadStudentsForSelect() {
|
||||
const res = await apiGet('/api/admin/students', {page_size: 1000});
|
||||
if (res && res.success) {
|
||||
let html = '<option value="">全部</option>';
|
||||
res.data.students.forEach(s => {
|
||||
html += `<option value="${s.student_id}">${escapeHtml(s.student_no)} - ${escapeHtml(s.name)}</option>`;
|
||||
});
|
||||
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 params = {
|
||||
page, page_size: 20,
|
||||
start_date: startDate,
|
||||
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) {
|
||||
let html = '';
|
||||
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 += `<tr${revokedStyle}>
|
||||
<td>${formatDateTime(record.created_at)}</td>
|
||||
<td>${escapeHtml(record.student_name)}</td>
|
||||
<td class="${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
||||
<td>${escapeHtml(record.reason)}</td>
|
||||
<td>${escapeHtml(record.recorder_name)}</td>`;
|
||||
<?php if ($role === '班主任'): ?>
|
||||
if (record.is_revoked == 1) {
|
||||
const revokerInfo = record.revoker_name ? `由 ${escapeHtml(record.revoker_name)} 撤销` : '已撤销';
|
||||
html += `<td><span class="text-muted" style="margin-right:4px;">${revokerInfo}</span><button class="btn btn-sm btn-secondary" onclick="restoreRecord(${record.record_id})">反撤销</button></td>`;
|
||||
} else {
|
||||
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||
}
|
||||
<?php elseif ($role === '班长'): ?>
|
||||
if (record.is_revoked == 1) {
|
||||
const revokerInfo = record.revoker_name ? `由 ${escapeHtml(record.revoker_name)} 撤销` : '已撤销';
|
||||
html += `<td><span class="text-muted">${revokerInfo}</span></td>`;
|
||||
} else {
|
||||
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||
}
|
||||
<?php elseif ($role === '考勤委员'): ?>
|
||||
if (record.is_revoked == 1) {
|
||||
html += `<td><span class="text-muted">已撤销</span></td>`;
|
||||
} else if (record.recorder_id == currentUserId) {
|
||||
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||
} else {
|
||||
html += `<td><span class="text-muted">-</span></td>`;
|
||||
}
|
||||
<?php endif; ?>
|
||||
html += `</tr>`;
|
||||
});
|
||||
|
||||
if (res.data.records.length === 0) {
|
||||
const colSpan = <?php echo ($role === '班主任' || $role === '班长' || $role === '考勤委员') ? '6' : '5'; ?>;
|
||||
html = `<tr><td colspan="${colSpan}" style="text-align:center;">暂无记录</td></tr>`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 构建CSV内容
|
||||
let csv = '\uFEFF'; // BOM for UTF-8
|
||||
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();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script>window.PAGE_CONFIG = { role: '<?php echo $role; ?>', userId: <?php echo intval($_SESSION['user_id']); ?> };</script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/modules/utils.js"></script>
|
||||
<script src="/assets/js/modules/points-mgmt.js"></script>
|
||||
<script src="/assets/js/history.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
@@ -81,6 +81,16 @@ include __DIR__ . '/../includes/header.php';
|
||||
<button type="button" class="btn btn-sm" onclick="selectDeductionType(0, '')">自定义</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($role === '学习委员'): ?>
|
||||
<div class="form-group">
|
||||
<label>具体作业</label>
|
||||
<input type="text" id="hwTitle" placeholder="选填,如:第三章练习">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>缴交时间</label>
|
||||
<input type="date" id="hwDeadline">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="form-group">
|
||||
<label>分数变动</label>
|
||||
<input type="number" id="pointsChange" required min="-5" max="5" step="1" placeholder="正数加分,负数扣分">
|
||||
@@ -101,118 +111,10 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var selectedStudentIds = [];
|
||||
const hwRole = '<?php echo $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 = '<option value="">不选择科目</option>';
|
||||
res.data.subjects.forEach(s => {
|
||||
html += `<option value="${escapeHtml(s.subject_name)}">${escapeHtml(s.subject_name)}</option>`;
|
||||
});
|
||||
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 += `<tr>
|
||||
<td><input type="checkbox" class="student-checkbox" data-id="${student.student_id}"></td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td>${escapeHtml(student.name)}</td>
|
||||
<td>${student.total_points}</td>
|
||||
<td><button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button></td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.students.length === 0) {
|
||||
html = '<tr><td colspan="5" style="text-align:center;">暂无学生数据</td></tr>';
|
||||
}
|
||||
document.getElementById('studentList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
function showSinglePointsModal(studentId, studentName) {
|
||||
selectedStudentIds = [studentId];
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`;
|
||||
document.getElementById('pointsChange').value = '';
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
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 reasonEl = document.getElementById('pointsReason');
|
||||
if (subjectName && !reasonEl.value.startsWith('[')) {
|
||||
reasonEl.value = `[${subjectName}] ${reasonEl.value}`;
|
||||
}
|
||||
}
|
||||
|
||||
submitBatchPoints();
|
||||
}
|
||||
|
||||
loadStudents();
|
||||
loadSubjectsForHomework();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script>window.PAGE_CONFIG = { role: '<?php echo $role; ?>' };</script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/modules/utils.js"></script>
|
||||
<script src="/assets/js/modules/points-mgmt.js"></script>
|
||||
<script src="/assets/js/homework-manage.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
@@ -77,6 +77,5 @@ document.getElementById('passwordForm').addEventListener('submit', async (e) =>
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
@@ -196,279 +196,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var archiveSemesterId = null;
|
||||
var archivePage = 1;
|
||||
var archiveTotalPages = 1;
|
||||
var associateSemesterId = null;
|
||||
|
||||
function fillSemesterDates(type) {
|
||||
var now = new Date();
|
||||
var currentYear = now.getFullYear();
|
||||
var currentMonth = now.getMonth() + 1;
|
||||
var startDateInput = document.getElementById('semesterStartDate');
|
||||
var endDateInput = document.getElementById('semesterEndDate');
|
||||
|
||||
if (type === 'upper') {
|
||||
var year = currentMonth >= 6 ? currentYear : currentYear - 1;
|
||||
var endYear = year + 1;
|
||||
var 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 += `<button class="btn btn-sm" style="border:1px solid #667eea;color:#667eea;" onclick="showEditSemesterModal(${sem.semester_id}, '${escapeHtml(sem.semester_name)}', '${startDate}', '${endDate}')">编辑</button> `;
|
||||
if (!sem.is_active) {
|
||||
actions += `<button class="btn btn-sm btn-primary" onclick="activateSemester(${sem.semester_id})">激活</button> `;
|
||||
}
|
||||
actions += `<button class="btn btn-sm" style="border:1px solid #2ecc71;color:#2ecc71;" onclick="showAssociateConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}', '${startDate}', '${endDate}')">关联数据</button> `;
|
||||
actions += `<button class="btn btn-sm btn-warning" onclick="showArchiveConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">归档</button> `;
|
||||
}
|
||||
if (sem.is_archived) {
|
||||
actions += `<button class="btn btn-sm btn-secondary" onclick="viewArchiveData(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">查看归档</button>`;
|
||||
}
|
||||
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(sem.semester_name)}</td>
|
||||
<td>${formatDate(sem.start_date)}</td>
|
||||
<td>${formatDate(sem.end_date)}</td>
|
||||
<td><span class="${statusClass}">${statusText}</span></td>
|
||||
<td>${formatDateTime(sem.created_at)}</td>
|
||||
<td>${actions}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (semesters.length === 0) {
|
||||
html = '<tr><td colspan="6" style="text-align:center;">暂无学期,请点击上方按钮创建新学期</td></tr>';
|
||||
}
|
||||
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 =
|
||||
`即将关联 <strong>${dateRange}</strong> 内的所有未分配学期的操行分记录和考勤记录到学期 "<strong>${name}</strong>"。`;
|
||||
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 =
|
||||
`确定要归档学期 "<strong>${semesterName}</strong>" 吗?<br>归档后将保存所有学生的当前操行分快照,该学期数据将变为只读。`;
|
||||
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 += `<tr>
|
||||
<td>${a.rank_position || '-'}</td>
|
||||
<td>${escapeHtml(a.student_no)}</td>
|
||||
<td>${escapeHtml(a.student_name)}</td>
|
||||
<td>${a.final_points}</td>
|
||||
<td>${a.attendance_present || 0}</td>
|
||||
<td>${a.attendance_absent || 0}</td>
|
||||
<td>${a.attendance_late || 0}</td>
|
||||
<td>${a.attendance_leave || 0}</td>
|
||||
<td>${a.homework_submitted || 0}</td>
|
||||
<td>${a.homework_not_submitted || 0}</td>
|
||||
<td>${a.homework_late || 0}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (archives.length === 0) {
|
||||
html = '<tr><td colspan="11" style="text-align:center;">暂无归档数据</td></tr>';
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) modal.style.display = 'none';
|
||||
}
|
||||
|
||||
loadSemesters();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/semesters.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
|
||||
@@ -46,6 +46,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
<th><input type="checkbox" id="selectAll" onclick="toggleSelectAll()"></th>
|
||||
<th>学号</th>
|
||||
<th>姓名</th>
|
||||
<th>宿舍号</th>
|
||||
<th>操行分</th>
|
||||
<?php if ($role === '班主任'): ?><th>家长手机号</th><?php endif; ?>
|
||||
<th>操作</th>
|
||||
@@ -102,6 +103,10 @@ include __DIR__ . '/../includes/header.php';
|
||||
<input type="tel" id="parentPhone" placeholder="11位手机号">
|
||||
<small>填写后将自动创建家长账号(密码同学生初始密码123456)</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>宿舍号</label>
|
||||
<input type="text" id="addDormitoryNumber" placeholder="选填">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">确认添加</button>
|
||||
<button type="button" class="btn" onclick="closeModal('addStudentModal')">取消</button>
|
||||
@@ -131,6 +136,10 @@ include __DIR__ . '/../includes/header.php';
|
||||
<label>家长手机号</label>
|
||||
<input type="text" id="editStudentPhone" maxlength="20">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>宿舍号</label>
|
||||
<input type="text" id="editDormitoryNumber" placeholder="选填">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">保存修改</button>
|
||||
<button type="button" class="btn" onclick="closeModal('editStudentModal')">取消</button>
|
||||
@@ -161,97 +170,12 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const userRole = '<?php echo $role; ?>';
|
||||
var currentPage = 1;
|
||||
var 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 += `<tr>
|
||||
<td><input type="checkbox" class="student-checkbox" data-id="${student.student_id}"></td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td><a href="/admin/history.php?student_id=${student.student_id}" style="color: #3498db; text-decoration: none;">${escapeHtml(student.name)}</a></td>
|
||||
<td>${student.total_points}</td>
|
||||
${userRole === '班主任' ? `<td>${student.parent_phone ? student.parent_phone.slice(0,3) + '******' + student.parent_phone.slice(-2) : '-'}</td>` : ''}
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button>
|
||||
${userRole === '班主任' ? `<button class="btn btn-sm btn-secondary" onclick="showEditStudentModal(${student.student_id}, '${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}', '${escapeHtml(student.parent_phone || '')}')">编辑</button>
|
||||
<button class="btn btn-sm btn-warning" onclick="showResetStudentPasswordModal(${student.student_id}, '${escapeHtml(student.name)}')">重置密码</button>
|
||||
<button class="btn btn-sm btn-info" onclick="unlockStudent('${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}')">解锁</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteStudent(${student.student_id}, '${escapeHtml(student.name)}')">删除</button>` : ''}
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
if (res.data.students.length === 0) {
|
||||
html = `<tr><td colspan="${userRole === '班主任' ? '6' : '5'}" style="text-align:center;">暂无学生数据</td></tr>`;
|
||||
}
|
||||
|
||||
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) {
|
||||
selectedStudentIds = [studentId];
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`;
|
||||
document.getElementById('pointsChange').value = '';
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
const selectAll = document.getElementById('selectAll');
|
||||
if (selectAll) {
|
||||
document.querySelectorAll('.student-checkbox').forEach(cb => {
|
||||
cb.checked = selectAll.checked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 解锁学生登录
|
||||
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);
|
||||
});
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script>window.PAGE_CONFIG = { role: '<?php echo $role; ?>' };</script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/modules/utils.js"></script>
|
||||
<script src="/assets/js/modules/student-mgmt.js"></script>
|
||||
<script src="/assets/js/modules/points-mgmt.js"></script>
|
||||
<script src="/assets/js/students-manage.js"></script>
|
||||
|
||||
<!-- 批量加减分模态框(共用) -->
|
||||
<div id="batchPointsModal" class="modal">
|
||||
|
||||
@@ -129,108 +129,8 @@ include __DIR__ . '/../includes/header.php';
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
async function loadSubjects() {
|
||||
const res = await apiGet('/api/subject/list');
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.subjects.forEach(sub => {
|
||||
html += `
|
||||
<div class="subject-item">
|
||||
<span class="subject-name">${escapeHtml(sub.subject_name)}</span>
|
||||
<span class="subject-code">${escapeHtml(sub.subject_code || '')}</span>
|
||||
<span class="subject-status ${sub.is_active ? 'subject-status-active' : 'subject-status-inactive'}">
|
||||
${sub.is_active ? '启用' : '禁用'}
|
||||
</span>
|
||||
<button class="btn btn-sm btn-primary" onclick="showEditSubjectModal(${sub.subject_id}, '${escapeHtml(sub.subject_name)}', '${escapeHtml(sub.subject_code || '')}', ${sub.sort_order || 0})">编辑</button>
|
||||
<button class="btn btn-sm" onclick="toggleSubject(${sub.subject_id}, ${!sub.is_active})">
|
||||
${sub.is_active ? '禁用' : '启用'}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
if (res.data.subjects.length === 0) {
|
||||
html = '<p style="text-align:center;padding:40px;">暂无科目,请点击"添加科目"</p>';
|
||||
}
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) modal.style.display = 'none';
|
||||
}
|
||||
|
||||
loadSubjects();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||
<script src="/assets/js/modules/subject-mgmt.js"></script>
|
||||
<script src="/assets/js/subjects.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
@@ -206,6 +206,21 @@
|
||||
color: #4338ca;
|
||||
}
|
||||
|
||||
.student-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: #e8f4f8;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
margin: 2px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
.student-tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.student-cell {
|
||||
width: calc(100% / 4 - 10px);
|
||||
@@ -217,3 +232,8 @@
|
||||
width: calc(100% / 3 - 10px);
|
||||
}
|
||||
}
|
||||
|
||||
.preserve-newlines {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@@ -473,6 +473,11 @@ tr:hover {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
min-height: 60px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* ========== 复选框组 ========== */
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
|
||||
@@ -1,388 +1,14 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理端JS
|
||||
* admin.js - 管理端公共函数库
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
* 此文件已拆分为独立模块,各模块文件位于 /assets/js/modules/ 目录
|
||||
* 各页面通过引用对应模块获取所需功能
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
* 模块列表:
|
||||
* - modules/modal-utils.js - 模态框工具函数
|
||||
* - modules/utils.js - 通用工具函数(escapeHtml, toggleSelectAll等)
|
||||
* - modules/student-mgmt.js - 学生管理函数
|
||||
* - modules/admin-mgmt.js - 管理员管理函数
|
||||
* - modules/subject-mgmt.js - 科目管理函数
|
||||
* - modules/points-mgmt.js - 加减分管理函数
|
||||
*/
|
||||
|
||||
// 全局变量(使用 var 避免与页面级 let 重复声明冲突)
|
||||
var selectedStudentIds = [];
|
||||
var currentPage = 1;
|
||||
var totalPages = 1;
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示导入模态框
|
||||
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 = '<h4>预览数据</h4><div class="table-wrapper"><table><thead><tr>';
|
||||
html += '<th>学号</th><th>姓名</th><th>家长手机号</th><th>初始密码</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
students.forEach(s => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(s.student_no || '')}</td>
|
||||
<td>${escapeHtml(s.name || '')}</td>
|
||||
<td>${escapeHtml(s.parent_phone || '')}</td>
|
||||
<td>${escapeHtml(s.password || '123456')}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += `</tbody></table></div><p>共 ${students.length} 条记录,初始操行分默认为60分</p>`;
|
||||
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');
|
||||
}
|
||||
}
|
||||
141
frontend/assets/js/admins.js
Normal file
141
frontend/assets/js/admins.js
Normal file
@@ -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 += `<tr>
|
||||
<td>${escapeHtml(admin.username)}</td>
|
||||
<td>${escapeHtml(admin.real_name)}</td>
|
||||
<td>${escapeHtml(admin.role_type)}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="showEditAdminModal(${admin.user_id}, '${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}', '${escapeHtml(admin.role_type)}')">编辑</button>
|
||||
<button class="btn btn-sm btn-warning" onclick="resetAdminPassword(${admin.user_id}, '${escapeHtml(admin.real_name)}')">重置密码</button>
|
||||
<button class="btn btn-sm btn-info" onclick="unlockUser('${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}')">解锁</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteAdmin(${admin.user_id}, '${escapeHtml(admin.real_name)}')">删除</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.admins.length === 0) {
|
||||
html = '<tr><td colspan="4" style="text-align:center;">暂无管理员</td></tr>';
|
||||
}
|
||||
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;
|
||||
|
||||
})();
|
||||
195
frontend/assets/js/attendance-manage.js
Normal file
195
frontend/assets/js/attendance-manage.js
Normal file
@@ -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 = '<div style="text-align:center;padding:20px;color:#999;">加载学生列表失败</div>';
|
||||
}
|
||||
}
|
||||
|
||||
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 += `<div class="student-cell${hasRecord ? ' has-record' : ''}"
|
||||
data-id="${student.student_id}"
|
||||
data-name="${escapeHtml(student.name)}"
|
||||
onclick="toggleStudent(this)">
|
||||
<span class="student-cell-name">${escapeHtml(student.name)}</span>
|
||||
<span class="student-cell-no">${escapeHtml(student.student_no)}</span>
|
||||
</div>`;
|
||||
});
|
||||
if (studentsData.length === 0) {
|
||||
html = '<div style="text-align:center;padding:20px;color:#999;width:100%;">暂无学生数据</div>';
|
||||
}
|
||||
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 += `<tr>
|
||||
<td>${escapeHtml(record.student_no)}</td>
|
||||
<td>${escapeHtml(record.student_name)}</td>
|
||||
<td>${getStatusBadge(record.status, 'attendance')}</td>
|
||||
<td>${escapeHtml(record.reason || '-')}</td>
|
||||
<td>${escapeHtml(record.recorder_name || '-')}</td>
|
||||
<td>${record.deduction_applied ? '已扣分' : '-'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (records.length === 0) {
|
||||
html = '<tr><td colspan="6" style="text-align:center;">暂无考勤记录</td></tr>';
|
||||
}
|
||||
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;
|
||||
|
||||
})();
|
||||
@@ -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保持默认换行行为(不拦截)
|
||||
});
|
||||
128
frontend/assets/js/conduct.js
Normal file
128
frontend/assets/js/conduct.js
Normal file
@@ -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 += `<tr>
|
||||
<td><input type="checkbox" class="student-checkbox" data-id="${student.student_id}"></td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td><a href="/admin/history.php?student_id=${student.student_id}" style="color: #3498db; text-decoration: none;">${escapeHtml(student.name)}</a></td>
|
||||
<td>${student.total_points}</td>
|
||||
<td><button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button></td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.students.length === 0) {
|
||||
html = '<tr><td colspan="5" style="text-align:center;">暂无学生数据</td></tr>';
|
||||
}
|
||||
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;
|
||||
|
||||
})();
|
||||
90
frontend/assets/js/dashboard.js
Normal file
90
frontend/assets/js/dashboard.js
Normal file
@@ -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 = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">学生总数</div>
|
||||
<div class="stat-value">${studentsRes.data.total || 0}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
let quickActions = '';
|
||||
if (role === '班主任' || role === '班长' || role === '劳动委员' || role === '志愿委员') {
|
||||
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>';
|
||||
}
|
||||
if (role === '班主任') {
|
||||
quickActions += '<button class="btn btn-success" onclick="location.href=\'/admin/students.php\'">导入学生</button>';
|
||||
quickActions += '<button class="btn btn-secondary" onclick="location.href=\'/admin/conduct.php\'">导出德育分记录</button>';
|
||||
}
|
||||
document.getElementById('quickActions').innerHTML = quickActions || '<p>暂无快捷操作</p>';
|
||||
|
||||
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 += `<tr>
|
||||
<td>${rank}</td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td>${escapeHtml(student.name)}</td>
|
||||
<td>${student.total_points}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (rankingRes.data.ranking.length === 0) {
|
||||
html = '<tr><td colspan="4" style="text-align:center;">暂无数据</td></tr>';
|
||||
}
|
||||
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;
|
||||
|
||||
})();
|
||||
199
frontend/assets/js/history.js
Normal file
199
frontend/assets/js/history.js
Normal file
@@ -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 = '<option value="">全部</option>';
|
||||
res.data.students.forEach(s => {
|
||||
html += `<option value="${s.student_id}">${escapeHtml(s.student_no)} - ${escapeHtml(s.name)}</option>`;
|
||||
});
|
||||
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 = '<th>时间</th><th>原因</th><th>分值</th><th>操作人</th><th>涉及学生</th>';
|
||||
} else {
|
||||
headHtml = '<th>时间</th><th>学生</th><th>分数变动</th><th>原因</th><th>操作人</th>';
|
||||
if (role === '班主任' || role === '班长' || role === '考勤委员') {
|
||||
headHtml += '<th>操作</th>';
|
||||
}
|
||||
}
|
||||
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 = '<div class="student-tags-container">';
|
||||
names.forEach(name => {
|
||||
tagsHtml += `<span class="student-tag">${escapeHtml(name)}</span>`;
|
||||
});
|
||||
tagsHtml += '</div>';
|
||||
html += `<tr>
|
||||
<td>${formatDateTime(record.created_at)}</td>
|
||||
<td class="preserve-newlines">${escapeHtml(record.reason)}</td>
|
||||
<td class="${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}×${record.student_count}</td>
|
||||
<td>${escapeHtml(record.recorder_name || '')}</td>
|
||||
<td>${tagsHtml}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.records.length === 0) {
|
||||
html = '<tr><td colspan="5" style="text-align:center;">暂无记录</td></tr>';
|
||||
}
|
||||
} 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 += `<tr${revokedStyle}>
|
||||
<td>${formatDateTime(record.created_at)}</td>
|
||||
<td>${escapeHtml(record.student_name)}</td>
|
||||
<td class="${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
||||
<td class="preserve-newlines">${escapeHtml(record.reason)}</td>
|
||||
<td>${escapeHtml(record.recorder_name)}</td>`;
|
||||
if (role === '班主任') {
|
||||
if (record.is_revoked == 1) {
|
||||
const revokerInfo = record.revoker_name ? `由 ${escapeHtml(record.revoker_name)} 撤销` : '已撤销';
|
||||
html += `<td><span class="text-muted" style="margin-right:4px;">${revokerInfo}</span><button class="btn btn-sm btn-secondary" onclick="restoreRecord(${record.record_id})">反撤销</button></td>`;
|
||||
} else {
|
||||
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||
}
|
||||
} else if (role === '班长') {
|
||||
if (record.is_revoked == 1) {
|
||||
const revokerInfo = record.revoker_name ? `由 ${escapeHtml(record.revoker_name)} 撤销` : '已撤销';
|
||||
html += `<td><span class="text-muted">${revokerInfo}</span></td>`;
|
||||
} else {
|
||||
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||
}
|
||||
} else if (role === '考勤委员') {
|
||||
if (record.is_revoked == 1) {
|
||||
html += `<td><span class="text-muted">已撤销</span></td>`;
|
||||
} else if (record.recorder_id == currentUserId) {
|
||||
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||
} else {
|
||||
html += `<td><span class="text-muted">-</span></td>`;
|
||||
}
|
||||
}
|
||||
html += `</tr>`;
|
||||
});
|
||||
|
||||
if (res.data.records.length === 0) {
|
||||
const colSpan = (role === '班主任' || role === '班长' || role === '考勤委员') ? 6 : 5;
|
||||
html = `<tr><td colspan="${colSpan}" style="text-align:center;">暂无记录</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
})();
|
||||
127
frontend/assets/js/homework-manage.js
Normal file
127
frontend/assets/js/homework-manage.js
Normal file
@@ -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 = '<option value="">不选择科目</option>';
|
||||
res.data.subjects.forEach(s => {
|
||||
html += `<option value="${escapeHtml(s.subject_name)}">${escapeHtml(s.subject_name)}</option>`;
|
||||
});
|
||||
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 += `<tr>
|
||||
<td><input type="checkbox" class="student-checkbox" data-id="${student.student_id}"></td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td>${escapeHtml(student.name)}</td>
|
||||
<td>${student.total_points}</td>
|
||||
<td><button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button></td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.students.length === 0) {
|
||||
html = '<tr><td colspan="5" style="text-align:center;">暂无学生数据</td></tr>';
|
||||
}
|
||||
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;
|
||||
|
||||
})();
|
||||
53
frontend/assets/js/modules/admin-mgmt.js
Normal file
53
frontend/assets/js/modules/admin-mgmt.js
Normal file
@@ -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;
|
||||
})();
|
||||
24
frontend/assets/js/modules/modal-utils.js
Normal file
24
frontend/assets/js/modules/modal-utils.js
Normal file
@@ -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;
|
||||
})();
|
||||
98
frontend/assets/js/modules/points-mgmt.js
Normal file
98
frontend/assets/js/modules/points-mgmt.js
Normal file
@@ -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;
|
||||
})();
|
||||
233
frontend/assets/js/modules/student-mgmt.js
Normal file
233
frontend/assets/js/modules/student-mgmt.js
Normal file
@@ -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 = '<h4>预览数据</h4><div class="table-wrapper"><table><thead><tr>';
|
||||
html += '<th>学号</th><th>姓名</th><th>家长手机号</th><th>初始密码</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
students.forEach(s => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(s.student_no || '')}</td>
|
||||
<td>${escapeHtml(s.name || '')}</td>
|
||||
<td>${escapeHtml(s.parent_phone || '')}</td>
|
||||
<td>${escapeHtml(s.password || '123456')}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += `</tbody></table></div><p>共 ${students.length} 条记录,初始操行分默认为60分</p>`;
|
||||
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;
|
||||
})();
|
||||
47
frontend/assets/js/modules/subject-mgmt.js
Normal file
47
frontend/assets/js/modules/subject-mgmt.js
Normal file
@@ -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;
|
||||
})();
|
||||
38
frontend/assets/js/modules/utils.js
Normal file
38
frontend/assets/js/modules/utils.js
Normal file
@@ -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;
|
||||
})();
|
||||
293
frontend/assets/js/semesters.js
Normal file
293
frontend/assets/js/semesters.js
Normal file
@@ -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 += `<button class="btn btn-sm" style="border:1px solid #667eea;color:#667eea;" onclick="showEditSemesterModal(${sem.semester_id}, '${escapeHtml(sem.semester_name)}', '${startDate}', '${endDate}')">编辑</button> `;
|
||||
if (!sem.is_active) {
|
||||
actions += `<button class="btn btn-sm btn-primary" onclick="activateSemester(${sem.semester_id})">激活</button> `;
|
||||
}
|
||||
actions += `<button class="btn btn-sm" style="border:1px solid #2ecc71;color:#2ecc71;" onclick="showAssociateConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}', '${startDate}', '${endDate}')">关联数据</button> `;
|
||||
actions += `<button class="btn btn-sm btn-warning" onclick="showArchiveConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">归档</button> `;
|
||||
}
|
||||
if (sem.is_archived) {
|
||||
actions += `<button class="btn btn-sm btn-secondary" onclick="viewArchiveData(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">查看归档</button>`;
|
||||
}
|
||||
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(sem.semester_name)}</td>
|
||||
<td>${formatDate(sem.start_date)}</td>
|
||||
<td>${formatDate(sem.end_date)}</td>
|
||||
<td><span class="${statusClass}">${statusText}</span></td>
|
||||
<td>${formatDateTime(sem.created_at)}</td>
|
||||
<td>${actions}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (semesters.length === 0) {
|
||||
html = '<tr><td colspan="6" style="text-align:center;">暂无学期,请点击上方按钮创建新学期</td></tr>';
|
||||
}
|
||||
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 =
|
||||
`即将关联 <strong>${dateRange}</strong> 内的所有未分配学期的操行分记录和考勤记录到学期 "<strong>${name}</strong>"。`;
|
||||
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 =
|
||||
`确定要归档学期 "<strong>${semesterName}</strong>" 吗?<br>归档后将保存所有学生的当前操行分快照,该学期数据将变为只读。`;
|
||||
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 += `<tr>
|
||||
<td>${a.rank_position || '-'}</td>
|
||||
<td>${escapeHtml(a.student_no)}</td>
|
||||
<td>${escapeHtml(a.student_name)}</td>
|
||||
<td>${a.final_points}</td>
|
||||
<td>${a.attendance_present || 0}</td>
|
||||
<td>${a.attendance_absent || 0}</td>
|
||||
<td>${a.attendance_late || 0}</td>
|
||||
<td>${a.attendance_leave || 0}</td>
|
||||
<td>${a.homework_submitted || 0}</td>
|
||||
<td>${a.homework_not_submitted || 0}</td>
|
||||
<td>${a.homework_late || 0}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (archives.length === 0) {
|
||||
html = '<tr><td colspan="11" style="text-align:center;">暂无归档数据</td></tr>';
|
||||
}
|
||||
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;
|
||||
|
||||
})();
|
||||
38
frontend/assets/js/student-homework.js
Normal file
38
frontend/assets/js/student-homework.js
Normal file
@@ -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 += `<tr>
|
||||
<td>${escapeHtml(hw.subject_name)}</td>
|
||||
<td>${hw.deadline || hw.created_at}</td>
|
||||
<td>${pointsDisplay}</td>
|
||||
<td>${escapeHtml(hw.comments || '-')}</td>
|
||||
<td>${escapeHtml(hw.title)}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.homework.length === 0) {
|
||||
html = '<tr><td colspan="5" style="text-align:center;">暂无作业</td></tr>';
|
||||
}
|
||||
document.getElementById('homeworkList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
loadHomework();
|
||||
|
||||
})();
|
||||
95
frontend/assets/js/students-manage.js
Normal file
95
frontend/assets/js/students-manage.js
Normal file
@@ -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 += `<tr>
|
||||
<td><input type="checkbox" class="student-checkbox" data-id="${student.student_id}"></td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td><a href="/admin/history.php?student_id=${student.student_id}" style="color: #3498db; text-decoration: none;">${escapeHtml(student.name)}</a></td>
|
||||
<td>${escapeHtml(student.dormitory_number || '-')}</td>
|
||||
<td>${student.total_points}</td>
|
||||
${userRole === '班主任' ? `<td>${student.parent_phone ? student.parent_phone.slice(0,3) + '******' + student.parent_phone.slice(-2) : '-'}</td>` : ''}
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button>
|
||||
${userRole === '班主任' ? `<button class="btn btn-sm btn-secondary" onclick="showEditStudentModal(${student.student_id}, '${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}', '${escapeHtml(student.parent_phone || '')}', '${escapeHtml(student.dormitory_number || '')}')">编辑</button>
|
||||
<button class="btn btn-sm btn-warning" onclick="showResetStudentPasswordModal(${student.student_id}, '${escapeHtml(student.name)}')">重置密码</button>
|
||||
<button class="btn btn-sm btn-info" onclick="unlockStudent('${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}')">解锁</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteStudent(${student.student_id}, '${escapeHtml(student.name)}')">删除</button>` : ''}
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
if (res.data.students.length === 0) {
|
||||
html = `<tr><td colspan="${userRole === '班主任' ? '7' : '6'}" style="text-align:center;">暂无学生数据</td></tr>`;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
})();
|
||||
102
frontend/assets/js/subjects.js
Normal file
102
frontend/assets/js/subjects.js
Normal file
@@ -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 += `
|
||||
<div class="subject-item">
|
||||
<span class="subject-name">${escapeHtml(sub.subject_name)}</span>
|
||||
<span class="subject-code">${escapeHtml(sub.subject_code || '')}</span>
|
||||
<span class="subject-status ${sub.is_active ? 'subject-status-active' : 'subject-status-inactive'}">
|
||||
${sub.is_active ? '启用' : '禁用'}
|
||||
</span>
|
||||
<button class="btn btn-sm btn-primary" onclick="showEditSubjectModal(${sub.subject_id}, '${escapeHtml(sub.subject_name)}', '${escapeHtml(sub.subject_code || '')}', ${sub.sort_order || 0})">编辑</button>
|
||||
<button class="btn btn-sm" onclick="toggleSubject(${sub.subject_id}, ${!sub.is_active})">
|
||||
${sub.is_active ? '禁用' : '启用'}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteSubject(${sub.subject_id})">删除</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
if (res.data.subjects.length === 0) {
|
||||
html = '<p style="text-align:center;padding:40px;">暂无科目,请点击"添加科目"</p>';
|
||||
}
|
||||
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;
|
||||
|
||||
})();
|
||||
@@ -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);
|
||||
|
||||
@@ -45,11 +45,34 @@ $page_title = $page_title ?? '首页';
|
||||
window.API_BASE_URL = '<?php echo API_BASE_URL; ?>';
|
||||
window.JWT_STORAGE_KEY = '<?php echo JWT_STORAGE_KEY; ?>';
|
||||
window.USER_STORAGE_KEY = '<?php echo USER_STORAGE_KEY; ?>';
|
||||
window.DEDUCTION_HOMEWORK_NOT_SUBMIT = <?php echo DEDUCTION_HOMEWORK_NOT_SUBMIT; ?>;
|
||||
window.DEDUCTION_HOMEWORK_LATE = <?php echo DEDUCTION_HOMEWORK_LATE; ?>;
|
||||
window.DEDUCTION_ATTENDANCE_ABSENT = <?php echo DEDUCTION_ATTENDANCE_ABSENT; ?>;
|
||||
window.DEDUCTION_ATTENDANCE_LATE = <?php echo DEDUCTION_ATTENDANCE_LATE; ?>;
|
||||
window.DEDUCTION_ATTENDANCE_LEAVE = <?php echo DEDUCTION_ATTENDANCE_LEAVE; ?>;
|
||||
window.STUDENT_INITIAL_POINTS = <?php echo STUDENT_INITIAL_POINTS; ?>;
|
||||
</script>
|
||||
<script>
|
||||
// 从后端API同步加载扣分规则配置
|
||||
(function() {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', window.API_BASE_URL + '/api/config/deduction-rules', false); // 同步请求
|
||||
try {
|
||||
xhr.send();
|
||||
if (xhr.status === 200) {
|
||||
var resp = JSON.parse(xhr.responseText);
|
||||
if (resp.success && resp.data) {
|
||||
window.DEDUCTION_HOMEWORK_NOT_SUBMIT = resp.data.DEDUCTION_HOMEWORK_NOT_SUBMIT;
|
||||
window.DEDUCTION_HOMEWORK_LATE = resp.data.DEDUCTION_HOMEWORK_LATE;
|
||||
window.DEDUCTION_ATTENDANCE_ABSENT = resp.data.DEDUCTION_ATTENDANCE_ABSENT;
|
||||
window.DEDUCTION_ATTENDANCE_LATE = resp.data.DEDUCTION_ATTENDANCE_LATE;
|
||||
window.DEDUCTION_ATTENDANCE_LEAVE = resp.data.DEDUCTION_ATTENDANCE_LEAVE;
|
||||
window.STUDENT_INITIAL_POINTS = resp.data.STUDENT_INITIAL_POINTS;
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
// API加载失败时使用默认值
|
||||
window.DEDUCTION_HOMEWORK_NOT_SUBMIT = 2;
|
||||
window.DEDUCTION_HOMEWORK_LATE = 1;
|
||||
window.DEDUCTION_ATTENDANCE_ABSENT = 3;
|
||||
window.DEDUCTION_ATTENDANCE_LATE = 1;
|
||||
window.DEDUCTION_ATTENDANCE_LEAVE = 0;
|
||||
window.STUDENT_INITIAL_POINTS = 60;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script src="/assets/js/common.js"></script>
|
||||
@@ -31,6 +31,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
<div class="child-info">
|
||||
<div class="child-name" id="childName">--</div>
|
||||
<div class="child-no" id="childNo">--</div>
|
||||
<div class="child-no" id="childDormitory" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
@@ -41,6 +42,10 @@ include __DIR__ . '/../includes/header.php';
|
||||
<div class="stat-label">班级排名</div>
|
||||
<div class="stat-value" id="studentRank">--</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">缺交次数</div>
|
||||
<div class="stat-value" id="homeworkMissing">--</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="initial-points-hint" id="initialPointsHint"></div>
|
||||
</div>
|
||||
@@ -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} 分`;
|
||||
|
||||
@@ -84,7 +84,7 @@ async function loadHistory(page) {
|
||||
html += `<tr>
|
||||
<td>${formatDateTime(record.created_at)}</td>
|
||||
<td>${escapeHtml(record.related_type || '手动')}</td>
|
||||
<td>${escapeHtml(record.reason || '-')}</td>
|
||||
<td class="preserve-newlines">${escapeHtml(record.reason || '-')}</td>
|
||||
<td><span class="record-points ${pointsClass}">${pointsText}</span></td>
|
||||
<td>${escapeHtml(record.recorder_name || '-')}</td>
|
||||
</tr>`;
|
||||
|
||||
@@ -94,8 +94,8 @@ include __DIR__ . '/../includes/header.php';
|
||||
<div class="stat-value" id="studentRank">--</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">作业完成率</div>
|
||||
<div class="stat-value" id="homeworkRate">--%</div>
|
||||
<div class="stat-label">缺交次数</div>
|
||||
<div class="stat-value" id="homeworkRate">--</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">本月出勤率</div>
|
||||
@@ -103,6 +103,11 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item" id="dormitoryInfo" style="display:none; text-align: center; padding: 8px 0; color: #555; font-size: 14px;">
|
||||
<span class="info-label">宿舍号: </span>
|
||||
<span class="info-value" id="dormitoryNumber"></span>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">最新操行分记录</div>
|
||||
<div id="recentRecords"></div>
|
||||
@@ -136,10 +141,10 @@ include __DIR__ . '/../includes/header.php';
|
||||
<thead>
|
||||
<tr>
|
||||
<th>科目</th>
|
||||
<th>作业标题</th>
|
||||
<th>截止日期</th>
|
||||
<th>状态</th>
|
||||
<th>时间</th>
|
||||
<th>分值</th>
|
||||
<th>备注</th>
|
||||
<th>作业</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="homeworkList"></tbody>
|
||||
@@ -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 += `
|
||||
<tr>
|
||||
<td>${escapeHtml(hw.subject_name)}</td>
|
||||
<td>${escapeHtml(hw.title)}</td>
|
||||
<td>${escapeHtml(hw.deadline)}</td>
|
||||
<td>${getStatusBadge(hw.status, 'homework')}</td>
|
||||
<td>${hw.deadline || hw.created_at}</td>
|
||||
<td>${pointsDisplay}</td>
|
||||
<td>${escapeHtml(hw.comments || '-')}</td>
|
||||
<td>${escapeHtml(hw.title)}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
@@ -36,7 +36,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>科目</th><th>作业标题</th><th>截止日期</th><th>状态</th><th>备注</th></tr>
|
||||
<tr><th>科目</th><th>时间</th><th>分值</th><th>备注</th><th>作业</th></tr>
|
||||
</thead>
|
||||
<tbody id="homeworkList"></tbody>
|
||||
</table>
|
||||
@@ -44,31 +44,8 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const STUDENT_ID = <?php echo $student_id; ?>;
|
||||
|
||||
async function loadHomework() {
|
||||
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.homework.forEach(hw => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(hw.subject_name)}</td>
|
||||
<td>${escapeHtml(hw.title)}</td>
|
||||
<td>${hw.deadline}</td>
|
||||
<td>${getStatusBadge(hw.status, 'homework')}</td>
|
||||
<td>${escapeHtml(hw.comments || '-')}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.homework.length === 0) {
|
||||
html = '<tr><td colspan="5" style="text-align:center;">暂无作业</td></tr>';
|
||||
}
|
||||
document.getElementById('homeworkList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
loadHomework();
|
||||
</script>
|
||||
<script>window.PAGE_CONFIG = { studentId: <?php echo $student_id; ?> };</script>
|
||||
<script src="/assets/js/student.js"></script>
|
||||
<script src="/assets/js/student-homework.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
35
sql/init.sql
35
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;
|
||||
|
||||
Reference in New Issue
Block a user