v2.0.1更新
This commit is contained in:
@@ -65,6 +65,7 @@
|
|||||||
|
|
||||||
| 层级 | 技术 | 版本 |
|
| 层级 | 技术 | 版本 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
|
| 后端 | Python | 3.13.x |
|
||||||
| 后端框架 | FastAPI | 0.104+ |
|
| 后端框架 | FastAPI | 0.104+ |
|
||||||
| 数据库 | MySQL | 5.7 |
|
| 数据库 | MySQL | 5.7 |
|
||||||
| 缓存 | Redis | 7.x |
|
| 缓存 | Redis | 7.x |
|
||||||
@@ -269,6 +270,8 @@ classmanager/
|
|||||||
| v1.5 | 2026.4.29 | 登录错误封禁5分钟+手动解锁、加减分回显修复、权限限制修复、按钮样式补全 |
|
| v1.5 | 2026.4.29 | 登录错误封禁5分钟+手动解锁、加减分回显修复、权限限制修复、按钮样式补全 |
|
||||||
| v1.6 | 2026.4.29 | 权限修复:考勤委员提示遗漏、历史记录权限泄露、时间筛选失效、作业页分数限制与后端同步 |
|
| v1.6 | 2026.4.29 | 权限修复:考勤委员提示遗漏、历史记录权限泄露、时间筛选失效、作业页分数限制与后端同步 |
|
||||||
| v1.7 | 2026.5.21 | 全量一致性审计:前后端配置统一(.env.example/config.py/config.php)、清理废弃全局变量、角色权限表精确化 |
|
| v1.7 | 2026.5.21 | 全量一致性审计:前后端配置统一(.env.example/config.py/config.php)、清理废弃全局变量、角色权限表精确化 |
|
||||||
|
| v1.8 | 2026.5.22 | 科目管理融入作业管理页、科目删除数据依赖检查、加减分记录类型区分(manual/homework/attendance)、学生端作业详情优化 |
|
||||||
|
| v2.0.1 | 2026.5.23 | 操作列折叠优化、扣分类型大类区分、科目选择修复、改名作业扣分、记录人优化、家长端优化、学期管理优化 |
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ class HomeworkModel:
|
|||||||
"""
|
"""
|
||||||
return await execute_query(sql, tuple(subject_ids))
|
return await execute_query(sql, tuple(subject_ids))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_student_homework(student_id: int) -> List[Dict[str, Any]]:
|
async def get_student_homework(student_id: int) -> List[Dict[str, Any]]:
|
||||||
sql = """
|
sql = """
|
||||||
|
|||||||
@@ -126,6 +126,18 @@ class SemesterModel:
|
|||||||
"""
|
"""
|
||||||
return await execute_query(sql, (semester_id, start_date, end_date))
|
return await execute_query(sql, (semester_id, start_date, end_date))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def count_records_by_semester(semester_id: int) -> Dict[str, int]:
|
||||||
|
"""统计学期关联的记录数"""
|
||||||
|
conduct_sql = "SELECT COUNT(*) as cnt FROM conduct_records WHERE semester_id = %s"
|
||||||
|
attendance_sql = "SELECT COUNT(*) as cnt FROM attendance_records WHERE semester_id = %s"
|
||||||
|
conduct_result = await execute_one(conduct_sql, (semester_id,))
|
||||||
|
attendance_result = await execute_one(attendance_sql, (semester_id,))
|
||||||
|
return {
|
||||||
|
"conduct_count": conduct_result['cnt'] if conduct_result else 0,
|
||||||
|
"attendance_count": attendance_result['cnt'] if attendance_result else 0
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_homework_stats_by_date_range(start_date: str, end_date: str) -> List[Dict]:
|
async def get_homework_stats_by_date_range(start_date: str, end_date: str) -> List[Dict]:
|
||||||
"""通过作业截止日期范围查询所有学生的作业提交统计"""
|
"""通过作业截止日期范围查询所有学生的作业提交统计"""
|
||||||
|
|||||||
@@ -69,6 +69,13 @@ class SubjectModel:
|
|||||||
result = await execute_update(sql, tuple(params))
|
result = await execute_update(sql, tuple(params))
|
||||||
return result > 0
|
return result > 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def has_related_data(subject_id: int) -> bool:
|
||||||
|
"""检查科目是否有关联数据(assignments表)"""
|
||||||
|
sql = "SELECT COUNT(*) as cnt FROM assignments WHERE subject_id = %s"
|
||||||
|
result = await execute_one(sql, (subject_id,))
|
||||||
|
return result['cnt'] > 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete(subject_id: int) -> bool:
|
async def delete(subject_id: int) -> bool:
|
||||||
sql = "UPDATE subjects SET is_active = 0 WHERE subject_id = %s"
|
sql = "UPDATE subjects SET is_active = 0 WHERE subject_id = %s"
|
||||||
|
|||||||
@@ -212,7 +212,8 @@ async def add_conduct_points(request: Request, req: AddPointsRequest):
|
|||||||
points_change=req.points_change,
|
points_change=req.points_change,
|
||||||
reason=req.reason,
|
reason=req.reason,
|
||||||
recorder_id=user["user_id"],
|
recorder_id=user["user_id"],
|
||||||
recorder_name=user["username"]
|
recorder_name=user["real_name"],
|
||||||
|
related_type=req.related_type
|
||||||
)
|
)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ async def debug_add_admin(request: Request, req: AddAdminDebugRequest):
|
|||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
return JSONResponse(status_code=404, content={"detail": "Not Found"})
|
return JSONResponse(status_code=404, content={"detail": "Not Found"})
|
||||||
|
|
||||||
|
# 生产环境警告
|
||||||
|
if settings.APP_ENV == "production":
|
||||||
|
logger.warning(f"调试入口在生产环境中被调用!路径: {settings.DEBUG_PATH}, 来源IP: {request.client.host}")
|
||||||
|
|
||||||
from models.user import UserModel
|
from models.user import UserModel
|
||||||
|
|
||||||
valid_roles = ["班主任", "班长", "学习委员", "考勤委员", "劳动委员", "志愿委员"]
|
valid_roles = ["班主任", "班长", "学习委员", "考勤委员", "劳动委员", "志愿委员"]
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class AddPointsRequest(BaseModel):
|
|||||||
student_ids: List[int] = Field(..., min_length=1, max_length=200, description="学生ID列表")
|
student_ids: List[int] = Field(..., min_length=1, max_length=200, description="学生ID列表")
|
||||||
points_change: int = Field(..., gt=-100, lt=100, description="分数变动")
|
points_change: int = Field(..., gt=-100, lt=100, description="分数变动")
|
||||||
reason: str = Field(..., min_length=1, max_length=255, description="原因")
|
reason: str = Field(..., min_length=1, max_length=255, description="原因")
|
||||||
|
related_type: Optional[str] = Field(default='manual', pattern=r'^(manual|homework|attendance)$', description="关联类型: manual/homework/attendance")
|
||||||
|
|
||||||
|
|
||||||
class AddPointsResponse(BaseModel):
|
class AddPointsResponse(BaseModel):
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ class ConductService:
|
|||||||
points_change: int,
|
points_change: int,
|
||||||
reason: str,
|
reason: str,
|
||||||
recorder_id: int,
|
recorder_id: int,
|
||||||
recorder_name: str
|
recorder_name: str,
|
||||||
|
related_type: str = 'manual'
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""批量加减分"""
|
"""批量加减分"""
|
||||||
# 输入校验
|
# 输入校验
|
||||||
@@ -94,13 +95,13 @@ class ConductService:
|
|||||||
fail_count += 1
|
fail_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 创建记录
|
|
||||||
record_id = await ConductModel.create_record(
|
record_id = await ConductModel.create_record(
|
||||||
student_id=student_id,
|
student_id=student_id,
|
||||||
points_change=points_change,
|
points_change=points_change,
|
||||||
reason=reason,
|
reason=reason,
|
||||||
recorder_id=recorder_id,
|
recorder_id=recorder_id,
|
||||||
recorder_name=recorder_name
|
recorder_name=recorder_name,
|
||||||
|
related_type=related_type
|
||||||
)
|
)
|
||||||
|
|
||||||
# 更新学生总分
|
# 更新学生总分
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ class SemesterService:
|
|||||||
"""获取学期列表"""
|
"""获取学期列表"""
|
||||||
try:
|
try:
|
||||||
semesters = await SemesterModel.get_all()
|
semesters = await SemesterModel.get_all()
|
||||||
|
for sem in semesters:
|
||||||
|
counts = await SemesterModel.count_records_by_semester(sem['semester_id'])
|
||||||
|
sem['conduct_count'] = counts['conduct_count']
|
||||||
|
sem['attendance_count'] = counts['attendance_count']
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"semesters": semesters
|
"semesters": semesters
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ class SubjectService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
async def delete_subject(subject_id: int) -> Dict[str, Any]:
|
async def delete_subject(subject_id: int) -> Dict[str, Any]:
|
||||||
"""删除科目(软删除)"""
|
"""删除科目(软删除)"""
|
||||||
|
# 检查科目是否有关联数据
|
||||||
|
has_data = await SubjectModel.has_related_data(subject_id)
|
||||||
|
if has_data:
|
||||||
|
return {"success": False, "message": "该科目下已有作业数据,无法删除"}
|
||||||
|
|
||||||
result = await SubjectModel.delete(subject_id)
|
result = await SubjectModel.delete(subject_id)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
## 角色权限一览
|
## 角色权限一览
|
||||||
|
|
||||||
| 角色 | 操行分管理 | 历史记录 | 作业管理 | 考勤管理 | 科目管理 |
|
| 角色 | 操行分管理 | 历史记录 | 作业扣分 | 考勤管理 | 科目管理 |
|
||||||
|------|-----------|---------|---------|---------|---------|
|
|------|-----------|---------|---------|---------|---------|
|
||||||
| 班长 | ±5分以内 | 全部(可撤销) | - | - | - |
|
| 班长 | ±5分以内 | 全部(可撤销) | - | - | - |
|
||||||
| 学习委员 | ±5分以内 | 自己的 | ✓ | - | ✓ |
|
| 学习委员 | ±5分以内 | 自己的 | ✓ | - | ✓ |
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
### 学习委员
|
### 学习委员
|
||||||
|
|
||||||
#### 作业管理 (homework.php)
|
#### 作业扣分 (homework.php)
|
||||||
|
|
||||||
**批量扣分**:
|
**批量扣分**:
|
||||||
1. 在学生列表中勾选目标学生
|
1. 在学生列表中勾选目标学生
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
## 角色权限
|
## 角色权限
|
||||||
|
|
||||||
| 角色 | 操行分管理 | 历史记录 | 作业管理 | 考勤管理 | 科目管理 |
|
| 角色 | 操行分管理 | 历史记录 | 作业扣分 | 考勤管理 | 科目管理 |
|
||||||
|------|-----------|---------|---------|---------|---------|
|
|------|-----------|---------|---------|---------|---------|
|
||||||
| 班长 | ±5分以内 | 全部(可撤销) | - | - | - |
|
| 班长 | ±5分以内 | 全部(可撤销) | - | - | - |
|
||||||
| 学习委员 | ±5分以内 | 自己的 | ✓ | - | ✓ |
|
| 学习委员 | ±5分以内 | 自己的 | ✓ | - | ✓ |
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
| 首页 | 查看学生总数、排行榜、快捷入口 |
|
| 首页 | 查看学生总数、排行榜、快捷入口 |
|
||||||
| 操行分管理 | 对学生加减分(无限制)、导出德育分记录 |
|
| 操行分管理 | 对学生加减分(无限制)、导出德育分记录 |
|
||||||
| 历史记录 | 查看/导出/撤销全班记录,支持按扣分类型筛选 |
|
| 历史记录 | 查看/导出/撤销全班记录,支持按扣分类型筛选 |
|
||||||
| 作业管理 | 发布缺交作业记录、关联扣分 |
|
| 作业扣分 | 发布缺交作业记录、关联扣分 |
|
||||||
| 考勤管理 | 按时段(早上/中午/晚修)记录考勤、自定义扣分值 |
|
| 考勤管理 | 按时段(早上/中午/晚修)记录考勤、自定义扣分值 |
|
||||||
| 学生管理 | 新增/编辑/删除/批量导入学生 |
|
| 学生管理 | 新增/编辑/删除/批量导入学生 |
|
||||||
| 科目管理 | 增删改科目信息 |
|
| 科目管理 | 增删改科目信息 |
|
||||||
|
|||||||
@@ -76,9 +76,9 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4. 作业管理 (homework.php)
|
### 4. 作业扣分 (homework.php)
|
||||||
|
|
||||||
本模块用于管理学生作业缺交情况。
|
本模块用于管理学生扣分记录。
|
||||||
|
|
||||||
#### 查看学生列表
|
#### 查看学生列表
|
||||||
- 展示所有学生的学号、姓名、当前操行分
|
- 展示所有学生的学号、姓名、当前操行分
|
||||||
|
|||||||
@@ -75,6 +75,15 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<label>选中学生</label>
|
<label>选中学生</label>
|
||||||
<div id="selectedStudentsCount">0 人</div>
|
<div id="selectedStudentsCount">0 人</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>扣分类型</label>
|
||||||
|
<div class="deduction-types">
|
||||||
|
<button type="button" class="btn btn-sm" onclick="selectDeductionType(null, '卫生')">卫生</button>
|
||||||
|
<button type="button" class="btn btn-sm" onclick="selectDeductionType(null, '课堂')">课堂</button>
|
||||||
|
<button type="button" class="btn btn-sm" onclick="selectDeductionType(null, '纪律')">纪律</button>
|
||||||
|
<button type="button" class="btn btn-sm" onclick="selectDeductionType(0, '')">自定义</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>分数变动</label>
|
<label>分数变动</label>
|
||||||
<input type="number" id="pointsChange" required placeholder="正数为加分,负数为扣分">
|
<input type="number" id="pointsChange" required placeholder="正数为加分,负数为扣分">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* 班级操行分管理系统 - 管理端作业管理
|
* 班级操行分管理系统 - 管理端作业扣分
|
||||||
*
|
*
|
||||||
* 开发者: Canglan
|
* 开发者: Canglan
|
||||||
* 联系方式: admin@sea-studio.top
|
* 联系方式: admin@sea-studio.top
|
||||||
@@ -17,7 +17,7 @@ if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
$page_title = '作业管理';
|
$page_title = '作业扣分';
|
||||||
$role = $_SESSION['role'] ?? '';
|
$role = $_SESSION['role'] ?? '';
|
||||||
|
|
||||||
if (!in_array($role, ['班主任', '学习委员'])) {
|
if (!in_array($role, ['班主任', '学习委员'])) {
|
||||||
@@ -31,6 +31,20 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<?php include __DIR__ . '/../includes/nav.php'; ?>
|
<?php include __DIR__ . '/../includes/nav.php'; ?>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<!-- 科目管理折叠面板 -->
|
||||||
|
<div class="card" style="margin-bottom: 20px;">
|
||||||
|
<div class="collapsible-header" onclick="toggleSubjectPanel()" style="cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 15px 20px;">
|
||||||
|
<h3 style="margin: 0; font-size: 16px;">📚 科目管理</h3>
|
||||||
|
<span id="subjectPanelToggle" class="toggle-icon">▶ 展开</span>
|
||||||
|
</div>
|
||||||
|
<div id="subjectPanelContent" style="display: none; padding: 0 20px 20px;">
|
||||||
|
<div class="action-bar">
|
||||||
|
<button class="btn btn-primary" onclick="showAddSubjectModal()">添加科目</button>
|
||||||
|
</div>
|
||||||
|
<div id="subjectList" class="subject-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
@@ -65,7 +79,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<label>已选学生</label>
|
<label>已选学生</label>
|
||||||
<div id="selectedStudentsCount" class="selected-info">未选择学生</div>
|
<div id="selectedStudentsCount" class="selected-info">未选择学生</div>
|
||||||
</div>
|
</div>
|
||||||
<?php if ($role === '学习委员'): ?>
|
<?php if (in_array($role, ['班主任', '学习委员'])): ?>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>科目</label>
|
<label>科目</label>
|
||||||
<select id="hwSubjectSelect">
|
<select id="hwSubjectSelect">
|
||||||
@@ -78,10 +92,13 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<div class="deduction-types">
|
<div class="deduction-types">
|
||||||
<button type="button" class="btn btn-sm" onclick="selectDeductionType(-window.DEDUCTION_HOMEWORK_NOT_SUBMIT, '未交作业')">未交作业(-<span class="hw-not-submit"></span>分)</button>
|
<button type="button" class="btn btn-sm" onclick="selectDeductionType(-window.DEDUCTION_HOMEWORK_NOT_SUBMIT, '未交作业')">未交作业(-<span class="hw-not-submit"></span>分)</button>
|
||||||
<button type="button" class="btn btn-sm" onclick="selectDeductionType(-window.DEDUCTION_HOMEWORK_LATE, '迟交作业')">迟交作业(-<span class="hw-late"></span>分)</button>
|
<button type="button" class="btn btn-sm" onclick="selectDeductionType(-window.DEDUCTION_HOMEWORK_LATE, '迟交作业')">迟交作业(-<span class="hw-late"></span>分)</button>
|
||||||
|
<button type="button" class="btn btn-sm" onclick="selectDeductionType(null, '卫生')">卫生</button>
|
||||||
|
<button type="button" class="btn btn-sm" onclick="selectDeductionType(null, '课堂')">课堂</button>
|
||||||
|
<button type="button" class="btn btn-sm" onclick="selectDeductionType(null, '纪律')">纪律</button>
|
||||||
<button type="button" class="btn btn-sm" onclick="selectDeductionType(0, '')">自定义</button>
|
<button type="button" class="btn btn-sm" onclick="selectDeductionType(0, '')">自定义</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php if ($role === '学习委员'): ?>
|
<?php if (in_array($role, ['班主任', '学习委员'])): ?>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>具体作业</label>
|
<label>具体作业</label>
|
||||||
<input type="text" id="hwTitle" placeholder="选填,如:第三章练习">
|
<input type="text" id="hwTitle" placeholder="选填,如:第三章练习">
|
||||||
@@ -111,6 +128,114 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加科目模态框 -->
|
||||||
|
<div id="addSubjectModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>添加科目</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal('addSubjectModal')">×</button>
|
||||||
|
</div>
|
||||||
|
<form id="addSubjectFormInHw" onsubmit="event.preventDefault(); submitAddSubject()">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>科目名称</label>
|
||||||
|
<input type="text" id="subjectName" required placeholder="例如:语文、数学">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>科目代码</label>
|
||||||
|
<input type="text" id="subjectCode" placeholder="例如:CHI、MATH">
|
||||||
|
<small>可选,用于排序和标识</small>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" class="btn btn-primary">添加</button>
|
||||||
|
<button type="button" class="btn" onclick="closeModal('addSubjectModal')">取消</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑科目模态框 -->
|
||||||
|
<div id="editSubjectModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>编辑科目</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal('editSubjectModal')">×</button>
|
||||||
|
</div>
|
||||||
|
<form onsubmit="event.preventDefault(); submitEditSubject()">
|
||||||
|
<input type="hidden" id="editSubjectId">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>科目名称</label>
|
||||||
|
<input type="text" id="editSubjectName" required placeholder="例如:语文、数学">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>科目代码</label>
|
||||||
|
<input type="text" id="editSubjectCode" placeholder="例如:CHI、MATH">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>排序序号</label>
|
||||||
|
<input type="number" id="editSubjectSortOrder" placeholder="数字越小越靠前" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" class="btn btn-primary">保存</button>
|
||||||
|
<button type="button" class="btn" onclick="closeModal('editSubjectModal')">取消</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.subject-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.subject-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
.subject-name {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.subject-code {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.subject-status {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.subject-status-active {
|
||||||
|
background: #c6f6d5;
|
||||||
|
color: #22543d;
|
||||||
|
}
|
||||||
|
.subject-status-inactive {
|
||||||
|
background: #fed7d7;
|
||||||
|
color: #742a2a;
|
||||||
|
}
|
||||||
|
.collapsible-header {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.collapsible-header:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
.toggle-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
background: #e53e3e;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #c53030;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script>window.PAGE_CONFIG = { role: '<?php echo $role; ?>' };</script>
|
<script>window.PAGE_CONFIG = { role: '<?php echo $role; ?>' };</script>
|
||||||
<script src="/assets/js/modules/modal-utils.js"></script>
|
<script src="/assets/js/modules/modal-utils.js"></script>
|
||||||
<script src="/assets/js/modules/utils.js"></script>
|
<script src="/assets/js/modules/utils.js"></script>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<th>开始日期</th>
|
<th>开始日期</th>
|
||||||
<th>结束日期</th>
|
<th>结束日期</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
|
<th>记录数</th>
|
||||||
<th>创建时间</th>
|
<th>创建时间</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* 班级操行分管理系统 - 管理端科目管理
|
* 班级操行分管理系统 - 科目管理(已合并至作业管理页)
|
||||||
*
|
*
|
||||||
* 开发者: Canglan
|
* 开发者: Canglan
|
||||||
* 联系方式: admin@sea-studio.top
|
* 联系方式: admin@sea-studio.top
|
||||||
@@ -10,127 +10,5 @@
|
|||||||
* 版权所有 © Sea Network Technology Studio
|
* 版权所有 © Sea Network Technology Studio
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/../config.php';
|
header('Location: /admin/homework.php');
|
||||||
|
exit();
|
||||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
|
||||||
header('Location: /index.php');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$role = $_SESSION['role'] ?? '';
|
|
||||||
if (!in_array($role, ['班主任', '学习委员'])) {
|
|
||||||
header('Location: /admin/dashboard.php');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$page_title = '科目管理';
|
|
||||||
include __DIR__ . '/../includes/header.php';
|
|
||||||
?>
|
|
||||||
|
|
||||||
<?php include __DIR__ . '/../includes/nav.php'; ?>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<div class="card">
|
|
||||||
<div class="action-bar">
|
|
||||||
<button class="btn btn-primary" onclick="showAddSubjectModal()">添加科目</button>
|
|
||||||
</div>
|
|
||||||
<div id="subjectList" class="subject-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 添加科目模态框 -->
|
|
||||||
<div id="addSubjectModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>添加科目</h3>
|
|
||||||
<button class="modal-close" onclick="closeModal('addSubjectModal')">×</button>
|
|
||||||
</div>
|
|
||||||
<form onsubmit="event.preventDefault(); submitAddSubject()">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>科目名称</label>
|
|
||||||
<input type="text" id="subjectName" required placeholder="例如:语文、数学">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>科目代码</label>
|
|
||||||
<input type="text" id="subjectCode" placeholder="例如:CHI、MATH">
|
|
||||||
<small>可选,用于排序和标识</small>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="submit" class="btn btn-primary">添加</button>
|
|
||||||
<button type="button" class="btn" onclick="closeModal('addSubjectModal')">取消</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 编辑科目模态框 -->
|
|
||||||
<div id="editSubjectModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>编辑科目</h3>
|
|
||||||
<button class="modal-close" onclick="closeModal('editSubjectModal')">×</button>
|
|
||||||
</div>
|
|
||||||
<form onsubmit="event.preventDefault(); submitEditSubject()">
|
|
||||||
<input type="hidden" id="editSubjectId">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>科目名称</label>
|
|
||||||
<input type="text" id="editSubjectName" required placeholder="例如:语文、数学">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>科目代码</label>
|
|
||||||
<input type="text" id="editSubjectCode" placeholder="例如:CHI、MATH">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>排序序号</label>
|
|
||||||
<input type="number" id="editSubjectSortOrder" placeholder="数字越小越靠前" min="0">
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="submit" class="btn btn-primary">保存</button>
|
|
||||||
<button type="button" class="btn" onclick="closeModal('editSubjectModal')">取消</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.subject-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.subject-item {
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 12px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
.subject-name {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.subject-code {
|
|
||||||
color: #999;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.subject-status {
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
.subject-status-active {
|
|
||||||
background: #c6f6d5;
|
|
||||||
color: #22543d;
|
|
||||||
}
|
|
||||||
.subject-status-inactive {
|
|
||||||
background: #fed7d7;
|
|
||||||
color: #742a2a;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<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'; ?>
|
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ require_once __DIR__ . '/../config.php';
|
|||||||
// 设置响应头
|
// 设置响应头
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
// 允许跨域
|
// 仅允许同源请求
|
||||||
header('Access-Control-Allow-Origin: *');
|
|
||||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||||
header('Access-Control-Allow-Headers: Content-Type');
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,9 @@ require_once __DIR__ . '/../config.php';
|
|||||||
// 设置响应头
|
// 设置响应头
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
// 允许跨域(如果需要)
|
// 仅允许同源请求
|
||||||
header('Access-Control-Allow-Origin: *');
|
|
||||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||||
header('Access-Control-Allow-Headers: Content-Type');
|
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
||||||
|
|
||||||
// 处理预检请求
|
// 处理预检请求
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||||
@@ -93,6 +92,67 @@ if (!in_array($data['user_type'], $validUserTypes)) {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证 JWT Token
|
||||||
|
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||||
|
if (empty($authHeader) || !preg_match('/^Bearer\s+(.+)$/i', $authHeader, $matches)) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '缺少认证令牌'
|
||||||
|
]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $matches[1];
|
||||||
|
$apiUrl = API_BASE_URL . '/api/auth/me';
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_URL => $apiUrl,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 10,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Authorization: Bearer ' . $token,
|
||||||
|
'Content-Type: application/json'
|
||||||
|
],
|
||||||
|
CURLOPT_SSL_VERIFYPEER => false,
|
||||||
|
CURLOPT_SSL_VERIFYHOST => 0
|
||||||
|
]);
|
||||||
|
|
||||||
|
$apiResponse = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($httpCode !== 200 || empty($apiResponse)) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '认证令牌无效或已过期'
|
||||||
|
]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$tokenData = json_decode($apiResponse, true);
|
||||||
|
if (!$tokenData || !isset($tokenData['success']) || !$tokenData['success']) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '认证验证失败'
|
||||||
|
]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 token 中的 user_id 与请求数据中的 user_id 一致
|
||||||
|
$tokenUserId = $tokenData['data']['user_id'] ?? null;
|
||||||
|
if ($tokenUserId === null || intval($tokenUserId) !== intval($data['user_id'])) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '身份验证不匹配'
|
||||||
|
]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
// 设置 Session 变量
|
// 设置 Session 变量
|
||||||
$_SESSION['user_id'] = $data['user_id'];
|
$_SESSION['user_id'] = $data['user_id'];
|
||||||
$_SESSION['user_type'] = $data['user_type'];
|
$_SESSION['user_type'] = $data['user_type'];
|
||||||
|
|||||||
@@ -745,3 +745,88 @@ tr:hover {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========== 操作列下拉菜单 ========== */
|
||||||
|
.action-dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-dropdown-toggle {
|
||||||
|
background: #f7fafc;
|
||||||
|
color: #4a5568;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-dropdown-toggle:hover {
|
||||||
|
background: #edf2f7;
|
||||||
|
border-color: #cbd5e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-dropdown-toggle.open {
|
||||||
|
background: #edf2f7;
|
||||||
|
border-color: #a0aec0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-dropdown-menu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
min-width: 120px;
|
||||||
|
z-index: 200;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-dropdown-menu.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-dropdown-menu a {
|
||||||
|
display: block;
|
||||||
|
padding: 8px 14px;
|
||||||
|
color: #4a5568;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-dropdown-menu a:hover {
|
||||||
|
background: #f7fafc;
|
||||||
|
color: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-dropdown-menu a.danger {
|
||||||
|
color: #e53e3e;
|
||||||
|
border-top: 1px solid #edf2f7;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-dropdown-menu a.danger:hover {
|
||||||
|
background: #fff5f5;
|
||||||
|
color: #c53030;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.action-dropdown-menu {
|
||||||
|
right: auto;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,10 +23,15 @@ async function loadAdmins() {
|
|||||||
<td>${escapeHtml(admin.real_name)}</td>
|
<td>${escapeHtml(admin.real_name)}</td>
|
||||||
<td>${escapeHtml(admin.role_type)}</td>
|
<td>${escapeHtml(admin.role_type)}</td>
|
||||||
<td>
|
<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>
|
<div class="action-dropdown">
|
||||||
<button class="btn btn-sm btn-warning" onclick="resetAdminPassword(${admin.user_id}, '${escapeHtml(admin.real_name)}')">重置密码</button>
|
<button class="btn btn-sm action-dropdown-toggle" onclick="toggleActionDropdown(this)">操作 ▼</button>
|
||||||
<button class="btn btn-sm btn-info" onclick="unlockUser('${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}')">解锁</button>
|
<div class="action-dropdown-menu">
|
||||||
<button class="btn btn-sm btn-danger" onclick="deleteAdmin(${admin.user_id}, '${escapeHtml(admin.real_name)}')">删除</button>
|
<a onclick="showEditAdminModal(${admin.user_id}, '${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}', '${escapeHtml(admin.role_type)}')">编辑</a>
|
||||||
|
<a onclick="resetAdminPassword(${admin.user_id}, '${escapeHtml(admin.real_name)}')">重置密码</a>
|
||||||
|
<a onclick="unlockUser('${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}')">解锁</a>
|
||||||
|
<a class="danger" onclick="deleteAdmin(${admin.user_id}, '${escapeHtml(admin.real_name)}')">删除</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -197,12 +197,12 @@ async function logout() {
|
|||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
if (!str) return '';
|
if (!str) return '';
|
||||||
return str.replace(/[&<>]/g, function(m) {
|
return String(str)
|
||||||
if (m === '&') return '&';
|
.replace(/&/g, '\x26amp;')
|
||||||
if (m === '<') return '<';
|
.replace(/</g, '\x26lt;')
|
||||||
if (m === '>') return '>';
|
.replace(/>/g, '\x26gt;')
|
||||||
return m;
|
.replace(/"/g, '\x26quot;')
|
||||||
});
|
.replace(/'/g, '\x26#x27;');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -336,6 +336,36 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function toggleActionDropdown(el) {
|
||||||
|
var dropdown = el.closest('.action-dropdown');
|
||||||
|
if (!dropdown) return;
|
||||||
|
var menu = dropdown.querySelector('.action-dropdown-menu');
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
var isOpen = menu.classList.contains('show');
|
||||||
|
// 先关闭所有
|
||||||
|
document.querySelectorAll('.action-dropdown-menu.show').forEach(function(m) {
|
||||||
|
m.classList.remove('show');
|
||||||
|
var toggle = m.closest('.action-dropdown').querySelector('.action-dropdown-toggle');
|
||||||
|
if (toggle) toggle.classList.remove('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
menu.classList.add('show');
|
||||||
|
el.classList.add('open');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (!e.target.closest('.action-dropdown')) {
|
||||||
|
document.querySelectorAll('.action-dropdown-menu.show').forEach(function(m) {
|
||||||
|
m.classList.remove('show');
|
||||||
|
var toggle = m.closest('.action-dropdown').querySelector('.action-dropdown-toggle');
|
||||||
|
if (toggle) toggle.classList.remove('open');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 全局textarea键盘事件:Enter提交表单,Ctrl+Enter换行
|
// 全局textarea键盘事件:Enter提交表单,Ctrl+Enter换行
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener('keydown', function(e) {
|
||||||
if (e.target.tagName !== 'TEXTAREA') return;
|
if (e.target.tagName !== 'TEXTAREA') return;
|
||||||
@@ -352,3 +382,27 @@ document.addEventListener('keydown', function(e) {
|
|||||||
}
|
}
|
||||||
// Ctrl+Enter和Shift+Enter保持默认换行行为(不拦截)
|
// Ctrl+Enter和Shift+Enter保持默认换行行为(不拦截)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.selectDeductionType = function(points, reason) {
|
||||||
|
var pointsEl = document.getElementById('pointsChange');
|
||||||
|
var reasonEl = document.getElementById('pointsReason');
|
||||||
|
if (points === 0 && reason === '') {
|
||||||
|
// 自定义模式 - 清空分值和原因,聚焦原因输入框
|
||||||
|
if (pointsEl) pointsEl.value = '';
|
||||||
|
if (reasonEl) {
|
||||||
|
reasonEl.value = '';
|
||||||
|
reasonEl.focus();
|
||||||
|
}
|
||||||
|
} else if (points === null || points === undefined) {
|
||||||
|
// 类别模式 - 仅填充原因,聚焦分值输入框
|
||||||
|
if (reasonEl) reasonEl.value = reason;
|
||||||
|
if (pointsEl) {
|
||||||
|
pointsEl.value = '';
|
||||||
|
pointsEl.focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 预设模式 - 同时填充分值和原因
|
||||||
|
if (pointsEl) pointsEl.value = points;
|
||||||
|
if (reasonEl) reasonEl.value = reason;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* 班级操行分管理系统 - 作业管理页JS
|
* 班级操行分管理系统 - 作业扣分页JS
|
||||||
*
|
*
|
||||||
* 开发者: Canglan
|
* 开发者: Canglan
|
||||||
* 版权归属: Sea Network Technology Studio
|
* 版权归属: Sea Network Technology Studio
|
||||||
@@ -28,7 +28,6 @@ document.getElementById('pointsChange').setAttribute('max', hwMaxPoints);
|
|||||||
|
|
||||||
// 加载科目列表(学习委员)
|
// 加载科目列表(学习委员)
|
||||||
async function loadSubjectsForHomework() {
|
async function loadSubjectsForHomework() {
|
||||||
if (hwRole !== '学习委员') return;
|
|
||||||
const subjectSelect = document.getElementById('hwSubjectSelect');
|
const subjectSelect = document.getElementById('hwSubjectSelect');
|
||||||
if (!subjectSelect) return;
|
if (!subjectSelect) return;
|
||||||
const res = await apiGet('/api/subject/list');
|
const res = await apiGet('/api/subject/list');
|
||||||
@@ -69,16 +68,6 @@ function showSinglePointsModal(studentId, studentName) {
|
|||||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
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() {
|
function handleSubmitPoints() {
|
||||||
const pointsChange = parseInt(document.getElementById('pointsChange').value);
|
const pointsChange = parseInt(document.getElementById('pointsChange').value);
|
||||||
if (isNaN(pointsChange) || pointsChange === 0) {
|
if (isNaN(pointsChange) || pointsChange === 0) {
|
||||||
@@ -91,7 +80,7 @@ function handleSubmitPoints() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 学习委员附加科目前缀、具体作业和缴交时间
|
// 学习委员附加科目前缀、具体作业和缴交时间
|
||||||
if (hwRole === '学习委员') {
|
if (hwRole === '学习委员' || hwRole === '班主任') {
|
||||||
const subjectSelect = document.getElementById('hwSubjectSelect');
|
const subjectSelect = document.getElementById('hwSubjectSelect');
|
||||||
const subjectName = subjectSelect ? subjectSelect.value : '';
|
const subjectName = subjectSelect ? subjectSelect.value : '';
|
||||||
const hwTitle = document.getElementById('hwTitle').value.trim();
|
const hwTitle = document.getElementById('hwTitle').value.trim();
|
||||||
@@ -113,7 +102,136 @@ function handleSubmitPoints() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
submitBatchPoints();
|
submitBatchPoints({ related_type: 'homework' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 科目管理功能 ==========
|
||||||
|
|
||||||
|
function toggleSubjectPanel() {
|
||||||
|
const content = document.getElementById('subjectPanelContent');
|
||||||
|
const toggle = document.getElementById('subjectPanelToggle');
|
||||||
|
if (content.style.display === 'none') {
|
||||||
|
content.style.display = 'block';
|
||||||
|
toggle.textContent = '▼ 收起';
|
||||||
|
loadSubjectList();
|
||||||
|
} else {
|
||||||
|
content.style.display = 'none';
|
||||||
|
toggle.textContent = '▶ 展开';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSubjectList() {
|
||||||
|
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="toggleSubjectStatus(${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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAddSubjectModal() {
|
||||||
|
const form = document.getElementById('addSubjectFormInHw');
|
||||||
|
if (form) form.reset();
|
||||||
|
document.getElementById('addSubjectModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
loadSubjectList();
|
||||||
|
loadSubjectsForHomework();
|
||||||
|
} else {
|
||||||
|
showToast(res?.message || '添加失败', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleSubjectStatus(subjectId, enable) {
|
||||||
|
const res = await apiPut(`/api/subject/update/${subjectId}`, { is_active: enable });
|
||||||
|
if (res && res.success) {
|
||||||
|
showToast(enable ? '科目已启用' : '科目已禁用');
|
||||||
|
loadSubjectList();
|
||||||
|
loadSubjectsForHomework();
|
||||||
|
} 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('科目删除成功');
|
||||||
|
loadSubjectList();
|
||||||
|
loadSubjectsForHomework();
|
||||||
|
} 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');
|
||||||
|
loadSubjectList();
|
||||||
|
loadSubjectsForHomework();
|
||||||
|
} else {
|
||||||
|
showToast(res?.message || '更新失败', 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadStudents();
|
loadStudents();
|
||||||
@@ -121,7 +239,13 @@ loadSubjectsForHomework();
|
|||||||
|
|
||||||
window.loadStudents = loadStudents;
|
window.loadStudents = loadStudents;
|
||||||
window.showSinglePointsModal = showSinglePointsModal;
|
window.showSinglePointsModal = showSinglePointsModal;
|
||||||
window.selectDeductionType = selectDeductionType;
|
|
||||||
window.handleSubmitPoints = handleSubmitPoints;
|
window.handleSubmitPoints = handleSubmitPoints;
|
||||||
|
window.toggleSubjectPanel = toggleSubjectPanel;
|
||||||
|
window.showAddSubjectModal = showAddSubjectModal;
|
||||||
|
window.submitAddSubject = submitAddSubject;
|
||||||
|
window.toggleSubjectStatus = toggleSubjectStatus;
|
||||||
|
window.deleteSubject = deleteSubject;
|
||||||
|
window.showEditSubjectModal = showEditSubjectModal;
|
||||||
|
window.submitEditSubject = submitEditSubject;
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 提交批量加减分
|
// 提交批量加减分
|
||||||
async function submitBatchPoints() {
|
async function submitBatchPoints(options = {}) {
|
||||||
const pointsChange = parseInt(document.getElementById('pointsChange').value);
|
const pointsChange = parseInt(document.getElementById('pointsChange').value);
|
||||||
const reason = document.getElementById('pointsReason').value;
|
const reason = document.getElementById('pointsReason').value;
|
||||||
|
|
||||||
@@ -49,11 +49,15 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await apiPost('/api/admin/conduct/add', {
|
const data = {
|
||||||
student_ids: selectedStudentIds,
|
student_ids: selectedStudentIds,
|
||||||
points_change: pointsChange,
|
points_change: pointsChange,
|
||||||
reason: reason
|
reason: reason
|
||||||
});
|
};
|
||||||
|
if (options.related_type) {
|
||||||
|
data.related_type = options.related_type;
|
||||||
|
}
|
||||||
|
const res = await apiPost('/api/admin/conduct/add', data);
|
||||||
|
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
showToast(`操作成功: ${res.data.success_count} 人成功`);
|
showToast(`操作成功: ${res.data.success_count} 人成功`);
|
||||||
|
|||||||
@@ -60,28 +60,39 @@ async function loadSemesters() {
|
|||||||
const startDate = sem.start_date || '';
|
const startDate = sem.start_date || '';
|
||||||
const endDate = sem.end_date || '';
|
const endDate = sem.end_date || '';
|
||||||
if (!sem.is_archived) {
|
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> `;
|
actions += `<div class="action-dropdown">
|
||||||
if (!sem.is_active) {
|
<button class="btn btn-sm action-dropdown-toggle" onclick="toggleActionDropdown(this)">操作 ▼</button>
|
||||||
actions += `<button class="btn btn-sm btn-primary" onclick="activateSemester(${sem.semester_id})">激活</button> `;
|
<div class="action-dropdown-menu">
|
||||||
}
|
<a onclick="showEditSemesterModal(${sem.semester_id}, '${escapeHtml(sem.semester_name)}', '${startDate}', '${endDate}')">编辑</a>
|
||||||
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> `;
|
${!sem.is_active ? `<a onclick="activateSemester(${sem.semester_id})">激活</a>` : ''}
|
||||||
actions += `<button class="btn btn-sm btn-warning" onclick="showArchiveConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">归档</button> `;
|
<a onclick="showAssociateConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}', '${startDate}', '${endDate}')">关联数据</a>
|
||||||
|
<a class="danger" onclick="showArchiveConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">归档</a>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
if (sem.is_archived) {
|
if (sem.is_archived) {
|
||||||
actions += `<button class="btn btn-sm btn-secondary" onclick="viewArchiveData(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">查看归档</button>`;
|
actions += `<button class="btn btn-sm btn-secondary" onclick="viewArchiveData(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">查看归档</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const conductCount = sem.conduct_count || 0;
|
||||||
|
const attendanceCount = sem.attendance_count || 0;
|
||||||
|
let recordText = '-';
|
||||||
|
if (conductCount > 0 || attendanceCount > 0) {
|
||||||
|
recordText = `${conductCount}条操行分 / ${attendanceCount}条考勤`;
|
||||||
|
}
|
||||||
|
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${escapeHtml(sem.semester_name)}</td>
|
<td>${escapeHtml(sem.semester_name)}</td>
|
||||||
<td>${formatDate(sem.start_date)}</td>
|
<td>${formatDate(sem.start_date)}</td>
|
||||||
<td>${formatDate(sem.end_date)}</td>
|
<td>${formatDate(sem.end_date)}</td>
|
||||||
<td><span class="${statusClass}">${statusText}</span></td>
|
<td><span class="${statusClass}">${statusText}</span></td>
|
||||||
|
<td>${recordText}</td>
|
||||||
<td>${formatDateTime(sem.created_at)}</td>
|
<td>${formatDateTime(sem.created_at)}</td>
|
||||||
<td>${actions}</td>
|
<td>${actions}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
if (semesters.length === 0) {
|
if (semesters.length === 0) {
|
||||||
html = '<tr><td colspan="6" style="text-align:center;">暂无学期,请点击上方按钮创建新学期</td></tr>';
|
html = '<tr><td colspan="7" style="text-align:center;">暂无学期,请点击上方按钮创建新学期</td></tr>';
|
||||||
}
|
}
|
||||||
document.getElementById('semesterList').innerHTML = html;
|
document.getElementById('semesterList').innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,17 +17,24 @@ async function loadHomework() {
|
|||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
let html = '';
|
let html = '';
|
||||||
res.data.homework.forEach(hw => {
|
res.data.homework.forEach(hw => {
|
||||||
const pointsDisplay = hw.points ? hw.points + '分' : '-';
|
// 提交状态
|
||||||
|
let statusDisplay = '-';
|
||||||
|
if (hw.status) {
|
||||||
|
statusDisplay = getStatusBadge(hw.status, 'homework');
|
||||||
|
}
|
||||||
|
// 扣分显示
|
||||||
|
const pointsDisplay = hw.points ? `<span style="color: #e53e3e;">${hw.points}分</span>` : '-';
|
||||||
|
|
||||||
html += `<tr>
|
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>
|
<td>${escapeHtml(hw.title)}</td>
|
||||||
|
<td>${escapeHtml(hw.subject_name)}</td>
|
||||||
|
<td>${hw.deadline || '-'}</td>
|
||||||
|
<td>${statusDisplay}</td>
|
||||||
|
<td>${pointsDisplay}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
if (res.data.homework.length === 0) {
|
if (res.data.homework.length === 0) {
|
||||||
html = '<tr><td colspan="5" style="text-align:center;">暂无作业</td></tr>';
|
html = '<tr><td colspan="5" style="text-align:center; padding: 40px; color: #999;">📝 暂无作业记录</td></tr>';
|
||||||
}
|
}
|
||||||
document.getElementById('homeworkList').innerHTML = html;
|
document.getElementById('homeworkList').innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,11 +30,16 @@ async function loadStudents(page = 1) {
|
|||||||
<td>${student.total_points}</td>
|
<td>${student.total_points}</td>
|
||||||
${userRole === '班主任' ? `<td>${student.parent_phone ? student.parent_phone.slice(0,3) + '******' + student.parent_phone.slice(-2) : '-'}</td>` : ''}
|
${userRole === '班主任' ? `<td>${student.parent_phone ? student.parent_phone.slice(0,3) + '******' + student.parent_phone.slice(-2) : '-'}</td>` : ''}
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button>
|
<div class="action-dropdown">
|
||||||
${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-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button>
|
||||||
<button class="btn btn-sm btn-warning" onclick="showResetStudentPasswordModal(${student.student_id}, '${escapeHtml(student.name)}')">重置密码</button>
|
${userRole === '班主任' ? `<button class="btn btn-sm action-dropdown-toggle" onclick="toggleActionDropdown(this)">更多 ▼</button>
|
||||||
<button class="btn btn-sm btn-info" onclick="unlockStudent('${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}')">解锁</button>
|
<div class="action-dropdown-menu">
|
||||||
<button class="btn btn-sm btn-danger" onclick="deleteStudent(${student.student_id}, '${escapeHtml(student.name)}')">删除</button>` : ''}
|
<a onclick="showEditStudentModal(${student.student_id}, '${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}', '${escapeHtml(student.parent_phone || '')}', '${escapeHtml(student.dormitory_number || '')}')">编辑</a>
|
||||||
|
<a onclick="showResetStudentPasswordModal(${student.student_id}, '${escapeHtml(student.name)}')">重置密码</a>
|
||||||
|
<a onclick="unlockStudent('${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}')">解锁</a>
|
||||||
|
<a class="danger" onclick="deleteStudent(${student.student_id}, '${escapeHtml(student.name)}')">删除</a>
|
||||||
|
</div>` : ''}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,14 +5,11 @@
|
|||||||
<a href="/admin/conduct.php" class="nav-item<?php echo $current_page === 'conduct' ? ' active' : ''; ?>">操行分管理</a>
|
<a href="/admin/conduct.php" class="nav-item<?php echo $current_page === 'conduct' ? ' active' : ''; ?>">操行分管理</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($role === '班主任' || $role === '学习委员'): ?>
|
<?php if ($role === '班主任' || $role === '学习委员'): ?>
|
||||||
<a href="/admin/homework.php" class="nav-item<?php echo $current_page === 'homework' ? ' active' : ''; ?>">作业管理</a>
|
<a href="/admin/homework.php" class="nav-item<?php echo $current_page === 'homework' ? ' active' : ''; ?>">作业扣分</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||||
<a href="/admin/attendance.php" class="nav-item<?php echo $current_page === 'attendance' ? ' active' : ''; ?>">考勤管理</a>
|
<a href="/admin/attendance.php" class="nav-item<?php echo $current_page === 'attendance' ? ' active' : ''; ?>">考勤管理</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($role === '班主任' || $role === '学习委员'): ?>
|
|
||||||
<a href="/admin/subjects.php" class="nav-item<?php echo $current_page === 'subjects' ? ' active' : ''; ?>">科目管理</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($role === '班主任'): ?>
|
<?php if ($role === '班主任'): ?>
|
||||||
<a href="/admin/admins.php" class="nav-item<?php echo $current_page === 'admins' ? ' active' : ''; ?>">管理员管理</a>
|
<a href="/admin/admins.php" class="nav-item<?php echo $current_page === 'admins' ? ' active' : ''; ?>">管理员管理</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -95,7 +95,10 @@ if (isset($_SESSION['user_id']) && isset($_SESSION['user_type'])) {
|
|||||||
try {
|
try {
|
||||||
const sessionResponse = await fetch('/api/save_session.php', {
|
const sessionResponse = await fetch('/api/save_session.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + userData.token
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
user_id: userData.user_id,
|
user_id: userData.user_id,
|
||||||
user_type: userData.user_type,
|
user_type: userData.user_type,
|
||||||
|
|||||||
@@ -35,14 +35,13 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>日期</th>
|
<th>日期</th>
|
||||||
<th>类型</th>
|
|
||||||
<th>原因</th>
|
<th>原因</th>
|
||||||
<th>分值</th>
|
<th>分值</th>
|
||||||
<th>记录人</th>
|
<th>记录人</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="historyList">
|
<tbody id="historyList">
|
||||||
<tr><td colspan="5" style="text-align:center;">加载中...</td></tr>
|
<tr><td colspan="4" style="text-align:center;">加载中...</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,17 +75,16 @@ async function loadHistory(page) {
|
|||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
let html = '';
|
let html = '';
|
||||||
if (res.data.records.length === 0) {
|
if (res.data.records.length === 0) {
|
||||||
html = '<tr><td colspan="5" style="text-align:center;">暂无记录</td></tr>';
|
html = '<tr><td colspan="4" style="text-align:center;">暂无记录</td></tr>';
|
||||||
} else {
|
} else {
|
||||||
res.data.records.forEach(record => {
|
res.data.records.forEach(record => {
|
||||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||||
const pointsText = record.points_change > 0 ? `+${record.points_change}` : record.points_change;
|
const pointsText = record.points_change > 0 ? `+${record.points_change}` : record.points_change;
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${formatDateTime(record.created_at)}</td>
|
<td>${formatDateTime(record.created_at)}</td>
|
||||||
<td>${escapeHtml(record.related_type || '手动')}</td>
|
|
||||||
<td class="preserve-newlines">${escapeHtml(record.reason || '-')}</td>
|
<td class="preserve-newlines">${escapeHtml(record.reason || '-')}</td>
|
||||||
<td><span class="record-points ${pointsClass}">${pointsText}</span></td>
|
<td><span class="record-points ${pointsClass}">${pointsText}</span></td>
|
||||||
<td>${escapeHtml(record.recorder_name || '-')}</td>
|
<td>班主任</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -356,12 +356,13 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
let html = '<div class="table-wrapper"><table><thead><tr><th>时间</th><th>分数变动</th><th>原因</th><th>操作人</th></tr></thead><tbody>';
|
let html = '<div class="table-wrapper"><table><thead><tr><th>时间</th><th>分数变动</th><th>原因</th><th>操作人</th></tr></thead><tbody>';
|
||||||
res.data.records.forEach(record => {
|
res.data.records.forEach(record => {
|
||||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||||
|
const recorderDisplay = record.points_change < 0 ? '班主任' : escapeHtml(record.recorder_name || '班主任');
|
||||||
html += `
|
html += `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${formatDateTime(record.created_at)}</td>
|
<td>${formatDateTime(record.created_at)}</td>
|
||||||
<td class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
<td class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
||||||
<td>${escapeHtml(record.reason)}</td>
|
<td>${escapeHtml(record.reason)}</td>
|
||||||
<td>${escapeHtml(record.recorder_name)}</td>
|
<td>${recorderDisplay}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>科目</th><th>时间</th><th>分值</th><th>备注</th><th>作业</th></tr>
|
<tr><th>作业名称</th><th>科目</th><th>截止时间</th><th>提交状态</th><th>扣分</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="homeworkList"></tbody>
|
<tbody id="homeworkList"></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
143
sql/upgrade_v2.0.sql
Normal file
143
sql/upgrade_v2.0.sql
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
-- ===========================================
|
||||||
|
-- 班级操行分管理系统 - v2.0 数据库迁移脚本
|
||||||
|
-- 适用版本: v1.8 → v2.0
|
||||||
|
-- 字符集: utf8mb4
|
||||||
|
--
|
||||||
|
-- 说明:
|
||||||
|
-- v2.0 主要为应用层代码变更(UI折叠菜单、扣分类型扩展、
|
||||||
|
-- 科目选择修复、页面改名、记录人优化、学期管理优化等),
|
||||||
|
-- 数据库 schema 变更较少。本脚本主要处理历史数据迁移
|
||||||
|
-- 和索引优化。
|
||||||
|
--
|
||||||
|
-- 迁移内容:
|
||||||
|
-- 1. 将 conduct_records.recorder_name 从用户名更新为真实姓名
|
||||||
|
-- 2. 将 attendance_records 相关记录中的 recorder_name 同步更新
|
||||||
|
-- 3. 确保 semester_id 索引存在(学期记录数统计优化)
|
||||||
|
-- 4. 数据验证
|
||||||
|
--
|
||||||
|
-- 重要: 执行前请备份数据库!
|
||||||
|
-- ===========================================
|
||||||
|
|
||||||
|
USE `classmanagerdb`;
|
||||||
|
|
||||||
|
-- ===========================================
|
||||||
|
-- 1. 更新 conduct_records 中的 recorder_name
|
||||||
|
-- v2.0 将 recorder_name 从用户名(username)改为真实姓名(real_name)
|
||||||
|
-- 需要将历史记录中的用户名更新为对应的真实姓名
|
||||||
|
-- ===========================================
|
||||||
|
|
||||||
|
-- 通过 JOIN users 表将 recorder_name 从 username 更新为 real_name
|
||||||
|
UPDATE conduct_records cr
|
||||||
|
INNER JOIN users u ON cr.recorder_id = u.user_id
|
||||||
|
SET cr.recorder_name = u.real_name
|
||||||
|
WHERE cr.recorder_name != u.real_name
|
||||||
|
OR cr.recorder_name IS NULL;
|
||||||
|
|
||||||
|
-- ===========================================
|
||||||
|
-- 2. 确保学期相关索引存在
|
||||||
|
-- v2.0 新增学期记录数统计功能,需要 semester_id 索引优化查询
|
||||||
|
-- ===========================================
|
||||||
|
|
||||||
|
SET @dbname = DATABASE();
|
||||||
|
|
||||||
|
-- conduct_records 表 semester_id 索引
|
||||||
|
SET @indexname = 'idx_conduct_semester_id';
|
||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'conduct_records' AND INDEX_NAME = @indexname) > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE conduct_records ADD INDEX idx_conduct_semester_id (semester_id)'
|
||||||
|
));
|
||||||
|
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||||
|
EXECUTE alterIfNotExists;
|
||||||
|
DEALLOCATE PREPARE alterIfNotExists;
|
||||||
|
|
||||||
|
-- attendance_records 表 semester_id 索引
|
||||||
|
SET @indexname = 'idx_attendance_semester_id';
|
||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'attendance_records' AND INDEX_NAME = @indexname) > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE attendance_records ADD INDEX idx_attendance_semester_id (semester_id)'
|
||||||
|
));
|
||||||
|
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||||
|
EXECUTE alterIfNotExists;
|
||||||
|
DEALLOCATE PREPARE alterIfNotExists;
|
||||||
|
|
||||||
|
-- ===========================================
|
||||||
|
-- 3. 确保之前版本的索引也存在(幂等兼容)
|
||||||
|
-- ===========================================
|
||||||
|
|
||||||
|
-- assignments 表 subject_id 索引(v1.8 科目删除数据检查)
|
||||||
|
SET @indexname = 'idx_assignments_subject_id';
|
||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'assignments' AND INDEX_NAME = @indexname) > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE assignments ADD INDEX idx_assignments_subject_id (subject_id)'
|
||||||
|
));
|
||||||
|
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||||
|
EXECUTE alterIfNotExists;
|
||||||
|
DEALLOCATE PREPARE alterIfNotExists;
|
||||||
|
|
||||||
|
-- conduct_records 表 student_id 索引
|
||||||
|
SET @indexname = 'idx_conduct_student_id';
|
||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'conduct_records' AND INDEX_NAME = @indexname) > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE conduct_records ADD INDEX idx_conduct_student_id (student_id)'
|
||||||
|
));
|
||||||
|
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||||
|
EXECUTE alterIfNotExists;
|
||||||
|
DEALLOCATE PREPARE alterIfNotExists;
|
||||||
|
|
||||||
|
-- conduct_records 表 created_at 索引
|
||||||
|
SET @indexname = 'idx_conduct_created_at';
|
||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'conduct_records' AND INDEX_NAME = @indexname) > 0,
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE conduct_records ADD INDEX idx_conduct_created_at (created_at)'
|
||||||
|
));
|
||||||
|
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||||
|
EXECUTE alterIfNotExists;
|
||||||
|
DEALLOCATE PREPARE alterIfNotExists;
|
||||||
|
|
||||||
|
-- ===========================================
|
||||||
|
-- 4. 验证迁移结果
|
||||||
|
-- ===========================================
|
||||||
|
|
||||||
|
SELECT '===== v2.0 数据库迁移验证 =====' AS '';
|
||||||
|
|
||||||
|
-- 检查 recorder_name 是否已更新为真实姓名
|
||||||
|
SELECT
|
||||||
|
'recorder_name 迁移验证' AS `检查项`,
|
||||||
|
COUNT(*) AS `总记录数`,
|
||||||
|
SUM(CASE WHEN cr.recorder_name = u.real_name THEN 1 ELSE 0 END) AS `已匹配真实姓名`,
|
||||||
|
SUM(CASE WHEN cr.recorder_name != u.real_name AND u.real_name IS NOT NULL THEN 1 ELSE 0 END) AS `仍为用户名`
|
||||||
|
FROM conduct_records cr
|
||||||
|
INNER JOIN users u ON cr.recorder_id = u.user_id;
|
||||||
|
|
||||||
|
-- 检查学期相关索引
|
||||||
|
SELECT
|
||||||
|
'学期索引验证' AS `检查项`,
|
||||||
|
TABLE_NAME AS `表名`,
|
||||||
|
INDEX_NAME AS `索引名`
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @dbname
|
||||||
|
AND INDEX_NAME IN ('idx_conduct_semester_id', 'idx_attendance_semester_id')
|
||||||
|
GROUP BY TABLE_NAME, INDEX_NAME;
|
||||||
|
|
||||||
|
-- 检查学期记录数统计示例
|
||||||
|
SELECT
|
||||||
|
'学期记录统计示例' AS `检查项`,
|
||||||
|
s.semester_name,
|
||||||
|
s.is_active,
|
||||||
|
(SELECT COUNT(*) FROM conduct_records WHERE semester_id = s.semester_id) AS `操行分记录数`,
|
||||||
|
(SELECT COUNT(*) FROM attendance_records WHERE semester_id = s.semester_id) AS `考勤记录数`
|
||||||
|
FROM semesters s
|
||||||
|
ORDER BY s.is_active DESC, s.created_at DESC
|
||||||
|
LIMIT 5;
|
||||||
|
|
||||||
|
SELECT 'v2.0 数据库迁移完成!' AS message;
|
||||||
Reference in New Issue
Block a user