feat: 多班级版班级管理系统 v2.0

技术栈:Go (Gin + GORM) + PHP + MySQL 5.7 + Redis

主要功能:
- 多班级完全隔离(class_id 贯穿全系统)
- 后端 Go Gin(端口 56789),Nginx 反代
- 超级管理员独立登录(env 配置,默认账密 admin/Admin123)
- bcrypt 密码加密(无 PASSWORD_SALT)
- 科任老师/课代表新角色
- 课代表作业管理页面
- 排行榜分项排行(操行分/考勤/作业)
- 角色加减分上下限由班主任配置
- 家长改密功能(可开关)
- 班级角色按需开关
- 宿舍号格式:南0-000
- 周度/月度重置功能
- MySQL 5.7 兼容
- 43 轮代码审查 + 全部修复

开发者: Canglan
版权归属: Sea Network Technology Studio
许可证: Apache License 2.0
This commit is contained in:
2026-06-22 10:21:52 +08:00
commit 16059ad3bf
135 changed files with 19933 additions and 0 deletions

40
frontend/.env.example Normal file
View File

@@ -0,0 +1,40 @@
# ===========================================
# 多班级版班级管理系统 - 前端配置
#
# 开发者: Canglan
# 联系方式: admin@sea-studio.top
# 版权归属: Sea Network Technology Studio
# 许可证: Apache License 2.0
#
# 版权所有 © Sea Network Technology Studio
# ===========================================
# 后端API地址Go 后端默认端口 56789通过 Nginx 反代后可直接使用域名)
# 如果直接访问 Go 后端,格式为 http://your-server-ip:56789
API_BASE_URL=https://your-api-domain.com
# API超时时间
API_TIMEOUT=30
# JWT存储Key
JWT_STORAGE_KEY=class_system_token
# 用户信息存储Key
USER_STORAGE_KEY=class_system_user
# 站点名称
SITE_NAME=多班级版班级管理系统
# 会话超时时间(分钟)
SESSION_TIMEOUT=30
# ICP备案号配置
# 是否启用ICP备案号显示 - true/false
ICP_ENABLED=false
# ICP备案号
ICP_NUMBER=京ICP备1234567890号-x
# 超级管理员独立登录路径(不含 /api 前缀,代码会自动拼接)
SUPER_ADMIN_LOGIN_PATH=/super-admin
STUDENT_INITIAL_POINTS=60

154
frontend/admin/admins.php Normal file
View File

@@ -0,0 +1,154 @@
<?php
/**
* 多班级版班级管理系统 - 管理端管理员管理
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$role = $_SESSION['role'] ?? '';
if ($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="showAddAdminModal()">添加管理员</button>
</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr><th>用户名</th><th>姓名</th><th>角色</th><th>操作</th></tr>
</thead>
<tbody id="adminList"></tbody>
</table>
</div>
</div>
</div>
<!-- 添加管理员模态框 -->
<div id="addAdminModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>添加管理员</h3>
<button class="modal-close" onclick="closeModal('addAdminModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitAddAdmin()">
<div class="form-group">
<label>用户名</label>
<input type="text" id="adminUsername" required placeholder="登录账号">
</div>
<div class="form-group">
<label>姓名</label>
<input type="text" id="adminRealName" required placeholder="真实姓名">
</div>
<div class="form-group">
<label>密码</label>
<input type="text" id="adminPassword" placeholder="留空则自动生成">
<small>自动生成8位随机密码</small>
</div>
<div class="form-group">
<label>角色</label>
<select id="adminRole" required>
<option value="">请选择角色</option>
<option value='班长'>班长</option>
<option value='学习委员'>学习委员</option>
<option value='考勤委员'>考勤委员</option>
<option value='劳动委员'>劳动委员</option>
<option value='志愿委员'>志愿委员</option>
</select>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">添加</button>
<button type="button" class="btn" onclick="closeModal('addAdminModal')">取消</button>
</div>
</form>
</div>
</div>
<!-- 编辑管理员模态框 -->
<div id="editAdminModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>编辑管理员</h3>
<button class="modal-close" onclick="closeModal('editAdminModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitEditAdmin()">
<input type="hidden" id="editAdminUserId">
<div class="form-group">
<label>用户名</label>
<input type="text" id="editAdminUsername" disabled>
</div>
<div class="form-group">
<label>姓名</label>
<input type="text" id="editAdminRealName" required>
</div>
<div class="form-group">
<label>角色</label>
<select id="editAdminRole" required>
<option value="">请选择角色</option>
<option value='班长'>班长</option>
<option value='学习委员'>学习委员</option>
<option value='考勤委员'>考勤委员</option>
<option value='劳动委员'>劳动委员</option>
<option value='志愿委员'>志愿委员</option>
</select>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">保存</button>
<button type="button" class="btn" onclick="closeModal('editAdminModal')">取消</button>
</div>
</form>
</div>
</div>
<!-- 重置密码模态框 -->
<div id="resetPasswordModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>重置密码</h3>
<button class="modal-close" onclick="closeModal('resetPasswordModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitResetPassword()">
<input type="hidden" id="resetPasswordUserId">
<div class="form-group">
<label>管理员</label>
<input type="text" id="resetPasswordAdminName" disabled>
</div>
<div class="form-group">
<label>新密码</label>
<input type="text" id="newPassword" required minlength="6" placeholder="请输入新密码至少6位">
<small>密码长度至少6位</small>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">确认重置</button>
<button type="button" class="btn" onclick="closeModal('resetPasswordModal')">取消</button>
</div>
</form>
</div>
</div>
<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'; ?>

View File

@@ -0,0 +1,100 @@
<?php
/**
* 多班级版班级管理系统 - 管理端考勤管理
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '考勤管理';
$role = $_SESSION['role'] ?? '';
if (!in_array($role, ['班主任', '考勤委员'])) {
header('Location: /admin/dashboard.php');
exit();
}
include __DIR__ . '/../includes/header.php';
?>
<?php include __DIR__ . '/../includes/nav.php'; ?>
<div class="container">
<!-- 考勤操作工具栏 -->
<div class="card">
<div class="attendance-toolbar">
<div class="toolbar-field">
<label class="toolbar-label">日期</label>
<input type="date" id="attendanceDate" value="<?php echo date('Y-m-d'); ?>">
</div>
<div class="toolbar-field">
<label class="toolbar-label">时段</label>
<select id="attendanceSlot">
<option value="morning">早上 7:15</option>
<option value="afternoon">中午 14:00</option>
<option value="evening">晚修 19:30</option>
</select>
</div>
<div class="status-group" style="margin-left:auto;">
<button class="status-btn active" data-status="absent" onclick="selectStatus(this)" id="btnAbsent">缺勤</button>
<button class="status-btn" data-status="late" onclick="selectStatus(this)" id="btnLate">迟到</button>
<button class="status-btn" data-status="leave" onclick="selectStatus(this)" id="btnLeave">请假</button>
</div>
<button class="btn btn-danger" onclick="submitAttendance()">提交考勤</button>
</div>
<div class="attendance-toolbar">
<div class="toolbar-field">
<label class="toolbar-label">扣分</label>
<input type="number" id="customDeduction" placeholder="默认值" min="0" max="20" style="width:80px;" title="留空或0使用默认值">
</div>
<div class="toolbar-field" style="flex:1;min-width:180px;">
<label class="toolbar-label">原因</label>
<input type="text" id="attendanceReason" placeholder="选填">
</div>
<button class="btn btn-primary" style="margin-left:auto;" onclick="selectAllStudents()">全选</button>
<button class="btn" onclick="deselectAllStudents()">取消全选</button>
</div>
</div>
<!-- 学生方格网格 -->
<div class="card">
<div class="card-title">点击选择有考勤异常的学生</div>
<div class="student-grid" id="studentGrid">
<!-- JS 动态生成 -->
</div>
</div>
<!-- 历史考勤记录 -->
<div class="card">
<div class="card-title">考勤记录</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr><th>学号</th><th>姓名</th><th>状态</th><th>原因</th><th>记录人</th><th>扣分</th></tr>
</thead>
<tbody id="attendanceList"></tbody>
</table>
</div>
</div>
</div>
<style>
.student-cell { display: flex; flex-direction: column; align-items: center; }
.student-cell-name { font-size: 14px; font-weight: 500; }
.student-cell-no { font-size: 11px; color: #999; }
</style>
<script src="/assets/js/modules/modal-utils.js"></script>
<script src="/assets/js/attendance-manage.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,125 @@
<?php
/**
* 多班级版班级管理系统 - 课代表作业管理页
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
$page_title = '作业管理';
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$role = $_SESSION['role'] ?? '';
if ($role !== '课代表') {
header('Location: /admin/dashboard.php');
exit();
}
require_once __DIR__ . '/../includes/header.php';
require_once __DIR__ . '/../includes/nav.php';
?>
<div class="container">
<div class="page-header">
<h2>作业管理</h2>
<p class="text-muted">课代表可管理所代表科目的作业缺交情况</p>
</div>
<div class="card">
<div class="action-bar">
<div class="action-buttons">
<button class="btn btn-primary" onclick="showPublishModal()">发布作业</button>
</div>
</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<th>作业标题</th>
<th>科目</th>
<th>截止日期</th>
<th>描述</th>
<th>操作</th>
</tr>
</thead>
<tbody id="homeworkList">
<tr><td colspan="5" style="text-align:center;">加载中...</td></tr>
</tbody>
</table>
</div>
<div class="pagination" id="pagination" style="display:none;">
<button class="btn btn-sm" id="prevBtn" onclick="changePage(-1)">上一页</button>
<span id="pageInfo">1 / 1</span>
<button class="btn btn-sm" id="nextBtn" onclick="changePage(1)">下一页</button>
</div>
</div>
</div>
<!-- 发布作业模态框 -->
<div id="publishModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>发布作业</h3>
<button class="modal-close" onclick="closeModal('publishModal')">&times;</button>
</div>
<form id="publishForm" onsubmit="event.preventDefault(); submitHomework()">
<div class="form-group">
<label>作业标题 <span style="color:red;">*</span></label>
<input type="text" id="hwTitle" required placeholder="例如:第三章练习">
</div>
<div class="form-group">
<label>截止日期 <span style="color:red;">*</span></label>
<input type="date" id="hwDeadline" required value="<?php echo date('Y-m-d'); ?>">
</div>
<div class="form-group">
<label>描述</label>
<textarea id="hwDescription" rows="3" placeholder="选填,作业详细说明"></textarea>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">发布</button>
<button type="button" class="btn" onclick="closeModal('publishModal')">取消</button>
</div>
</form>
</div>
</div>
<!-- 缺交登记模态框 -->
<div id="absentModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>登记缺交学生</h3>
<button class="modal-close" onclick="closeModal('absentModal')">&times;</button>
</div>
<div id="absentStudentList"></div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="submitAbsent()">提交缺交记录</button>
<button class="btn" onclick="closeModal('absentModal')">取消</button>
</div>
</div>
</div>
<style>
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin-top: 15px;
padding: 10px 0;
}
.pagination .btn { padding: 6px 16px; font-size: 13px; }
.pagination span { color: #666; font-size: 14px; }
</style>
<script src="/assets/js/cadre-homework.js"></script>
<?php require_once __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,439 @@
<?php
/**
* 多班级版班级管理系统 - 班级设置页面
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
$page_title = '班级设置';
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id'])) {
header('Location: /index.php');
exit();
}
$role = $_SESSION['role'] ?? '';
if (!in_array($role, ['班主任', '系统管理员'])) {
header('Location: /admin/dashboard.php');
exit();
}
require_once __DIR__ . '/../includes/header.php';
require_once __DIR__ . '/../includes/nav.php';
?>
<div class="container">
<div class="page-header">
<h2>班级设置</h2>
<p class="text-muted">修改当前班级的扣分规则、加减分限制和功能开关(仅班主任可修改)</p>
</div>
<!-- 扣分规则 -->
<div class="card">
<h3>扣分规则</h3>
<div id="deductionRules">
<div class="form-group">
<label>学生初始操行分</label>
<input type="number" id="setting_student_initial_points" value="60" min="0" max="200">
</div>
<div class="form-group">
<label>作业未提交扣分</label>
<input type="number" id="setting_deduction_homework_not_submit" value="2" min="0" max="20">
</div>
<div class="form-group">
<label>作业迟交扣分</label>
<input type="number" id="setting_deduction_homework_late" value="1" min="0" max="20">
</div>
<div class="form-group">
<label>缺勤扣分</label>
<input type="number" id="setting_deduction_attendance_absent" value="3" min="0" max="20">
</div>
<div class="form-group">
<label>迟到扣分</label>
<input type="number" id="setting_deduction_attendance_late" value="1" min="0" max="20">
</div>
<div class="form-group">
<label>请假扣分0=不扣分)</label>
<input type="number" id="setting_deduction_attendance_leave" value="0" min="0" max="20">
</div>
<button class="btn btn-primary" onclick="saveSettings()">保存设置</button>
</div>
</div>
<!-- 角色加减分上下限 -->
<div class="card">
<h3>角色加减分限制</h3>
<p class="text-muted" style="margin-bottom:15px;">配置各角色单次加减分的上下限</p>
<div id="pointLimits">
<div class="settings-grid">
<div class="form-group">
<label>班长单次加分上限</label>
<input type="number" id="limit_monitor_max_add" value="5" min="0" max="100">
</div>
<div class="form-group">
<label>班长单次扣分下限</label>
<input type="number" id="limit_monitor_max_subtract" value="-5" min="-100" max="0">
</div>
<div class="form-group">
<label>学习委员加分上限</label>
<input type="number" id="limit_study_comm_max_points" value="5" min="0" max="100">
</div>
<div class="form-group">
<label>学习委员扣分下限</label>
<input type="number" id="limit_study_comm_min_points" value="-5" min="-100" max="0">
</div>
<div class="form-group">
<label>考勤委员扣分上限</label>
<input type="number" id="limit_attendance_rep_max_points" value="8" min="0" max="100">
</div>
<div class="form-group">
<label>考勤委员扣分下限</label>
<input type="number" id="limit_attendance_rep_min_points" value="-8" min="-100" max="0">
</div>
<div class="form-group">
<label>劳动委员加分上限</label>
<input type="number" id="limit_labor_rep_max_points" value="1" min="0" max="100">
</div>
<div class="form-group">
<label>劳动委员扣分下限</label>
<input type="number" id="limit_labor_rep_min_points" value="-1" min="-100" max="0">
</div>
<div class="form-group">
<label>志愿委员加分上限</label>
<input type="number" id="limit_volunteer_rep_max_points" value="5" min="0" max="100">
</div>
<div class="form-group">
<label>志愿委员扣分下限</label>
<input type="number" id="limit_volunteer_rep_min_points" value="-5" min="-100" max="0">
</div>
<div class="form-group">
<label>科任老师加分上限</label>
<input type="number" id="limit_subject_teacher_max_points" value="5" min="0" max="100">
</div>
<div class="form-group">
<label>科任老师扣分下限</label>
<input type="number" id="limit_subject_teacher_min_points" value="-5" min="-100" max="0">
</div>
<button class="btn btn-primary" onclick="savePointLimits()">保存加减分限制</button>
</div>
</div>
<!-- 周期重置 -->
<div class="card">
<h3>周期重置</h3>
<p class="text-muted" style="margin-bottom:15px;">按周或按月自动重置学生操行分(需配合定时任务或手动触发)</p>
<div id="periodResetSettings">
<div class="form-group">
<label>重置频率</label>
<select id="setting_reset_frequency" onchange="toggleResetDay()">
<option value="none">不重置(仅学期结算)</option>
<option value="weekly">每周重置</option>
<option value="monthly">每月重置</option>
</select>
</div>
<div class="form-group" id="reset_day_of_week_group" style="display:none;">
<label>每周重置日</label>
<select id="setting_reset_day_of_week">
<option value="1">周一</option>
<option value="2">周二</option>
<option value="3">周三</option>
<option value="4">周四</option>
<option value="5">周五</option>
<option value="6">周六</option>
<option value="7">周日</option>
</select>
</div>
<div class="form-group" id="reset_day_of_month_group" style="display:none;">
<label>每月重置日</label>
<input type="number" id="setting_reset_day_of_month" value="1" min="1" max="28">
<small style="color:#999;">1~28日建议避免月末最后几天</small>
</div>
<button class="btn btn-primary" onclick="savePeriodResetSettings()">保存周期重置设置</button>
</div>
</div>
<!-- 角色开关 -->
<div class="card">
<h3>功能开关</h3>
<p class="text-muted" style="margin-bottom:15px;">控制各角色的功能启用状态</p>
<div id="roleToggles">
<div class="toggle-group">
<label class="toggle-label">
<input type="checkbox" id="toggle_parent_account_enabled">
<span>家长账号启用</span>
</label>
<label class="toggle-label">
<input type="checkbox" id="toggle_parent_password_change_enabled">
<span>家长改密启用</span>
</label>
<label class="toggle-label">
<input type="checkbox" id="toggle_parent_view_attendance">
<span>家长查看考勤</span>
</label>
<label class="toggle-label">
<input type="checkbox" id="toggle_parent_view_ranking">
<span>家长查看排名</span>
</label>
<label class="toggle-label">
<input type="checkbox" id="toggle_student_view_ranking">
<span>学生查看排行榜</span>
</label>
<label class="toggle-label">
<input type="checkbox" id="toggle_homework_management">
<span>作业管理模块</span>
</label>
<label class="toggle-label">
<input type="checkbox" id="toggle_attendance_management">
<span>考勤管理模块</span>
</label>
<label class="toggle-label">
<input type="checkbox" id="toggle_cadre_homework">
<span>课代表作业管理</span>
</label>
</div>
<button class="btn btn-primary" onclick="saveRoleToggles()">保存功能开关</button>
</div>
</div>
</div>
<style>
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.toggle-group {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 15px;
}
.toggle-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
}
.toggle-label input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
background: #fff;
cursor: pointer;
}
</style>
<script>
// limitFieldMap: 前端 input ID 后缀 → 后端 class_settings 表 key
var limitFieldMap = {
'monitor_max_add': 'point_limit_班长_max',
'monitor_max_subtract': 'point_limit_班长_min',
'study_comm_max_points': 'point_limit_学习委员_max',
'study_comm_min_points': 'point_limit_学习委员_min',
'attendance_rep_max_points': 'point_limit_考勤委员_max',
'attendance_rep_min_points': 'point_limit_考勤委员_min',
'labor_rep_max_points': 'point_limit_劳动委员_max',
'labor_rep_min_points': 'point_limit_劳动委员_min',
'volunteer_rep_max_points': 'point_limit_志愿委员_max',
'volunteer_rep_min_points': 'point_limit_志愿委员_min',
'subject_teacher_max_points': 'point_limit_科任老师_max',
'subject_teacher_min_points': 'point_limit_科任老师_min'
};
var limitFields = Object.keys(limitFieldMap);
var toggleFields = [
'parent_account_enabled', 'parent_password_change_enabled', 'parent_view_attendance',
'parent_view_ranking', 'student_view_ranking', 'homework_management',
'attendance_management', 'cadre_homework'
];
async function loadSettings() {
var params = {};
if (window.CLASS_ID) {
params.class_id = window.CLASS_ID;
}
var result = await apiGet('/api/config/deduction-rules', params);
if (result && result.success && result.data) {
var d = result.data;
document.getElementById('setting_student_initial_points').value = d.STUDENT_INITIAL_POINTS || 60;
document.getElementById('setting_deduction_homework_not_submit').value = d.DEDUCTION_HOMEWORK_NOT_SUBMIT || 2;
document.getElementById('setting_deduction_homework_late').value = d.DEDUCTION_HOMEWORK_LATE || 1;
document.getElementById('setting_deduction_attendance_absent').value = d.DEDUCTION_ATTENDANCE_ABSENT || 3;
document.getElementById('setting_deduction_attendance_late').value = d.DEDUCTION_ATTENDANCE_LATE || 1;
document.getElementById('setting_deduction_attendance_leave').value = d.DEDUCTION_ATTENDANCE_LEAVE || 0;
}
}
async function loadPointLimits() {
var result = await apiGet('/api/class/point-limits');
if (result && result.success && result.data) {
var d = result.data.settings || result.data;
limitFields.forEach(function(key) {
var el = document.getElementById('limit_' + key);
var backendKey = limitFieldMap[key];
if (el) {
// 优先读取后端 point_limit_* key兼容旧 key
if (backendKey && d[backendKey] !== undefined) {
el.value = d[backendKey];
} else if (d[key] !== undefined) {
el.value = d[key];
}
}
});
}
}
async function loadRoleToggles() {
var result = await apiGet('/api/class/features');
if (result && result.success && result.data) {
var d = result.data.features || result.data;
toggleFields.forEach(function(key) {
var el = document.getElementById('toggle_' + key);
if (el) {
el.checked = d[key] === true || d[key] === 1 || d[key] === '1';
}
});
}
}
async function saveSettings() {
var settings = [
{ key: 'initial_points', value: document.getElementById('setting_student_initial_points').value },
{ key: 'deduction_homework_not_submit', value: document.getElementById('setting_deduction_homework_not_submit').value },
{ key: 'deduction_homework_late', value: document.getElementById('setting_deduction_homework_late').value },
{ key: 'deduction_attendance_absent', value: document.getElementById('setting_deduction_attendance_absent').value },
{ key: 'deduction_attendance_late', value: document.getElementById('setting_deduction_attendance_late').value },
{ key: 'deduction_attendance_leave', value: document.getElementById('setting_deduction_attendance_leave').value },
];
var success = true;
for (var i = 0; i < settings.length; i++) {
var s = settings[i];
var result = await apiPost('/api/class/settings', { setting_key: s.key, setting_value: s.value });
if (!result || !result.success) {
success = false;
}
}
if (success) {
showToast('班级设置已保存');
} else {
showToast('部分设置保存失败', 'error');
}
}
async function savePointLimits() {
var data = {};
limitFields.forEach(function(key) {
var el = document.getElementById('limit_' + key);
if (el) {
// 使用后端 point_limit_* key 保存,确保 conduct_service 能正确读取
var backendKey = limitFieldMap[key] || key;
data[backendKey] = el.value;
}
});
var result = await apiPost('/api/class/point-limits', data);
if (result && result.success) {
showToast('加减分限制已保存');
} else {
showToast(result && result.message ? result.message : '保存失败', 'error');
}
}
async function saveRoleToggles() {
var success = true;
for (var i = 0; i < toggleFields.length; i++) {
var key = toggleFields[i];
var el = document.getElementById('toggle_' + key);
if (el) {
var result = await apiPost('/api/class/features', {
feature_key: key,
enabled: el.checked ? 1 : 0
});
if (!result || !result.success) {
success = false;
}
}
}
if (success) {
showToast('功能开关已保存');
} else {
showToast('部分开关保存失败', 'error');
}
}
// ========== 周期重置设置 ==========
async function loadPeriodResetSettings() {
var result = await apiGet('/api/class/settings');
if (result && result.success && result.data && result.data.settings) {
var s = result.data.settings;
var freqSelect = document.getElementById('setting_reset_frequency');
freqSelect.value = s['reset_frequency'] || 'none';
toggleResetDay();
if (s['reset_day_of_week']) {
document.getElementById('setting_reset_day_of_week').value = s['reset_day_of_week'];
}
if (s['reset_day_of_month']) {
document.getElementById('setting_reset_day_of_month').value = s['reset_day_of_month'];
}
}
}
function toggleResetDay() {
var freq = document.getElementById('setting_reset_frequency').value;
document.getElementById('reset_day_of_week_group').style.display = (freq === 'weekly') ? 'block' : 'none';
document.getElementById('reset_day_of_month_group').style.display = (freq === 'monthly') ? 'block' : 'none';
}
async function savePeriodResetSettings() {
var freq = document.getElementById('setting_reset_frequency').value;
var settings = [
{ key: 'reset_frequency', value: freq }
];
if (freq === 'weekly') {
settings.push({ key: 'reset_day_of_week', value: document.getElementById('setting_reset_day_of_week').value });
} else if (freq === 'monthly') {
settings.push({ key: 'reset_day_of_month', value: document.getElementById('setting_reset_day_of_month').value });
}
var success = true;
for (var i = 0; i < settings.length; i++) {
var result = await apiPost('/api/class/settings', { setting_key: settings[i].key, setting_value: settings[i].value });
if (!result || !result.success) {
success = false;
}
}
if (success) {
showToast('周期重置设置已保存');
} else {
showToast('部分设置保存失败', 'error');
}
}
document.addEventListener('DOMContentLoaded', function() {
loadSettings();
loadPointLimits();
loadRoleToggles();
loadPeriodResetSettings();
});
</script>
<?php require_once __DIR__ . '/../includes/footer.php'; ?>

214
frontend/admin/classes.php Normal file
View File

@@ -0,0 +1,214 @@
<?php
/**
* 共享班级管理系统 - 班级管理页面
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
$page_title = '班级管理';
require_once __DIR__ . '/../config.php';
// 权限检查
if (!isset($_SESSION['user_type']) || $_SESSION['user_type'] !== 'super_admin') {
header('Location: /admin/dashboard.php');
exit();
}
require_once __DIR__ . '/../includes/header.php';
require_once __DIR__ . '/../includes/nav.php';
?>
<div class="container">
<div class="page-header">
<h2>班级管理</h2>
<button class="btn btn-primary" onclick="showCreateModal()">新增班级</button>
</div>
<div id="classList" class="card-grid">
<div class="loading">加载中...</div>
</div>
</div>
<!-- 创建/编辑班级弹窗 -->
<div id="classModal" class="modal" style="display:none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modalTitle">新增班级</h3>
<button class="btn-close" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<input type="hidden" id="editClassId">
<div class="form-group">
<label>班级名称 <span class="required">*</span></label>
<input type="text" id="className" placeholder="如:高一(1)班" maxlength="100">
</div>
<div class="form-group">
<label>年级</label>
<input type="text" id="classGrade" placeholder="如:高一" maxlength="50">
</div>
<div class="form-group">
<label>描述</label>
<textarea id="classDesc" placeholder="班级描述(选填)" maxlength="255"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveClass()">保存</button>
</div>
</div>
</div>
<script>
async function loadClasses() {
const result = await apiGet('/api/class/list', { include_disabled: true });
if (result && result.success) {
renderClasses(result.data.classes || []);
} else {
document.getElementById('classList').innerHTML = '<div class="empty-state">暂无班级数据</div>';
}
}
function renderClasses(classes) {
if (classes.length === 0) {
document.getElementById('classList').innerHTML = '<div class="empty-state">暂无班级数据,点击右上角"新增班级"创建</div>';
return;
}
let html = '';
classes.forEach(cls => {
const statusBadge = cls.status === 1
? '<span class="status-badge status-submitted">启用</span>'
: '<span class="status-badge status-not_submitted">禁用</span>';
html += `
<div class="card">
<div class="card-header">
<h3>${escapeHtml(cls.class_name)}</h3>
${statusBadge}
</div>
<div class="card-body">
<p>年级: ${escapeHtml(cls.grade || '-')}</p>
<p>学生人数: ${cls.student_count || 0}</p>
<p>描述: ${escapeHtml(cls.description || '-')}</p>
</div>
<div class="card-footer">
<button class="btn btn-sm btn-primary" onclick="switchClass(${cls.class_id}, '${escapeHtml(cls.class_name)}')">进入班级</button>
<button class="btn btn-sm btn-secondary" onclick="editClass(${cls.class_id}, '${escapeHtml(cls.class_name)}', '${escapeHtml(cls.grade || '')}', '${escapeHtml(cls.description || '')}')">编辑</button>
<button class="btn btn-sm btn-danger" onclick="deleteClass(${cls.class_id}, '${escapeHtml(cls.class_name)}')">删除</button>
</div>
</div>
`;
});
document.getElementById('classList').innerHTML = html;
}
function showCreateModal() {
document.getElementById('modalTitle').textContent = '新增班级';
document.getElementById('editClassId').value = '';
document.getElementById('className').value = '';
document.getElementById('classGrade').value = '';
document.getElementById('classDesc').value = '';
document.getElementById('classModal').style.display = 'flex';
}
function editClass(classId, name, grade, desc) {
document.getElementById('modalTitle').textContent = '编辑班级';
document.getElementById('editClassId').value = classId;
document.getElementById('className').value = name;
document.getElementById('classGrade').value = grade;
document.getElementById('classDesc').value = desc;
document.getElementById('classModal').style.display = 'flex';
}
function closeModal() {
document.getElementById('classModal').style.display = 'none';
}
async function saveClass() {
const classId = document.getElementById('editClassId').value;
const data = {
class_name: document.getElementById('className').value.trim(),
grade: document.getElementById('classGrade').value.trim() || null,
description: document.getElementById('classDesc').value.trim() || null
};
if (!data.class_name) {
showToast('请输入班级名称', 'error');
return;
}
let result;
if (classId) {
result = await apiPut(`/api/class/update/${classId}`, data);
} else {
result = await apiPost('/api/class/create', data);
}
if (result && result.success) {
showToast(classId ? '班级更新成功' : '班级创建成功');
closeModal();
loadClasses();
} else {
showToast(result ? result.message : '操作失败', 'error');
}
}
async function deleteClass(classId, className) {
if (!confirm(`确定要删除班级 "${className}" 吗?此操作不可撤销。`)) return;
const result = await apiDelete(`/api/class/delete/${classId}`);
if (result && result.success) {
showToast('班级已删除');
loadClasses();
} else {
showToast(result ? result.message : '删除失败', 'error');
}
}
async function switchClass(classId, className) {
const result = await apiPost('/api/class/switch', { class_id: classId });
if (result && result.success) {
// 更新本地存储的用户信息
const userInfo = getUserInfo();
if (userInfo) {
userInfo.class_id = classId;
userInfo.class_name = className;
// 更新token
if (result.data && result.data.token) {
localStorage.setItem(window.JWT_STORAGE_KEY || 'class_system_token', result.data.token);
}
setUserInfo(userInfo);
}
// 同步到 PHP Session
try {
await fetch('/api/save_session.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getToken() },
body: JSON.stringify({
user_id: userInfo.user_id,
user_type: userInfo.user_type,
username: userInfo.username,
real_name: userInfo.real_name,
role: userInfo.role,
class_id: classId,
class_name: className
})
});
} catch (e) {
console.warn('同步Session失败', e);
}
showToast(`已切换到: ${className}`);
window.location.href = '/admin/dashboard.php';
} else {
showToast(result ? result.message : '切换失败', 'error');
}
}
document.addEventListener('DOMContentLoaded', loadClasses);
</script>
<?php require_once __DIR__ . '/../includes/footer.php'; ?>

154
frontend/admin/conduct.php Normal file
View File

@@ -0,0 +1,154 @@
<?php
/**
* 多班级版班级管理系统 - 管理端操行分管理
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '操行分管理';
$role = $_SESSION['role'] ?? '';
if (!in_array($role, ['班主任', '班长', '学习委员', '考勤委员', '劳动委员', '志愿委员', '科任老师'])) {
header('Location: /admin/dashboard.php');
exit();
}
include __DIR__ . '/../includes/header.php';
?>
<?php include __DIR__ . '/../includes/nav.php'; ?>
<div class="container">
<div class="card">
<div class="action-bar">
<div class="action-buttons">
<button class="btn btn-primary" onclick="showBatchPointsModal()">批量加减分</button>
<button class="btn btn-secondary" onclick="showDormitoryPointsModal()">宿舍加分</button>
<?php if ($role === '班主任'): ?>
<button class="btn btn-secondary" onclick="exportMoralityRecords()">导出德育分记录</button>
<?php endif; ?>
</div>
</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<th><input type="checkbox" id="selectAll" onclick="toggleSelectAll()"></th>
<th>学号</th>
<th>姓名</th>
<th>当前操行分</th>
<th>操作</th>
</tr>
</thead>
<tbody id="studentList"></tbody>
</table>
</div>
</div>
</div>
<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">
<div class="modal-content">
<div class="modal-header">
<h3>批量加减分</h3>
<button class="modal-close" onclick="closeModal('batchPointsModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitBatchPoints()">
<div class="form-group">
<label>选中学生</label>
<div id="selectedStudentsCount">0 人</div>
</div>
<div class="form-group">
<label>扣分类型</label>
<div class="deduction-types" style="display: flex; flex-wrap: wrap; gap: 6px;">
<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(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(null, '志愿')">志愿</button>
<button type="button" class="btn btn-sm" onclick="selectDeductionType(0, '')">自定义</button>
</div>
</div>
<div class="form-group">
<label>分数变动</label>
<input type="number" id="pointsChange" required placeholder="正数为加分,负数为扣分">
<small><?php
$hints = [
'班长' => '班长单次±5分以内',
'学习委员' => '学习委员单次±5分以内',
'考勤委员' => '考勤委员仅限扣分单次最多扣8分',
'劳动委员' => '劳动委员单次±1分以内',
'志愿委员' => '志愿委员仅限加分,最多+5分',
];
echo $hints[$role] ?? '班主任无限制';
?></small>
</div>
<div class="form-group">
<label>原因</label>
<textarea id="pointsReason" required rows="3" placeholder="请填写加减分原因"></textarea>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">确认提交</button>
<button type="button" class="btn" onclick="closeModal('batchPointsModal')">取消</button>
</div>
</form>
</div>
</div>
<!-- 宿舍集体加分模态框 -->
<div id="dormitoryPointsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>宿舍集体加分</h3>
<button class="modal-close" onclick="closeModal('dormitoryPointsModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitDormitoryPoints()">
<div class="form-group">
<label>选择宿舍</label>
<select id="dormitorySelect" onchange="onDormitorySelected()" required>
<option value="">-- 请选择宿舍 --</option>
</select>
</div>
<div class="form-group" id="dormitoryStudentsGroup" style="display:none;">
<label>宿舍成员</label>
<div id="dormitoryStudentsList" style="max-height: 150px; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 4px; padding: 8px;">
</div>
<small id="dormitoryStudentsCount"></small>
</div>
<div class="form-group">
<label>分数变动</label>
<input type="number" id="dormitoryPointsChange" required placeholder="正数为加分,负数为扣分">
</div>
<div class="form-group">
<label>原因</label>
<textarea id="dormitoryPointsReason" required rows="3" placeholder="请填写加减分原因"></textarea>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">确认提交</button>
<button type="button" class="btn" onclick="closeModal('dormitoryPointsModal')">取消</button>
</div>
</form>
</div>
</div>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,219 @@
<?php
/**
* 多班级版班级管理系统 - 管理端首页
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '首页';
$role = $_SESSION['role'] ?? '';
include __DIR__ . '/../includes/header.php';
?>
<?php include __DIR__ . '/../includes/nav.php'; ?>
<div class="container">
<div class="stats-grid" id="dashboardStats"></div>
<div class="card">
<div class="card-title">快捷操作</div>
<div class="action-buttons" id="quickActions"></div>
</div>
<div class="card">
<div class="card-title">操行分排行榜</div>
<div class="table-wrapper">
<div style="display: flex; align-items: center; margin-bottom: 12px; gap: 8px;">
<span style="font-size: 14px; color: #666;">显示前</span>
<input type="number" id="percentileFilter" style="width: 70px; padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px;" min="1" max="100" value="100" placeholder="1-100">
<span style="font-size: 14px; color: #666;">% 的学生</span>
<button class="btn btn-sm btn-primary" onclick="applyPercentileFilter()">筛选</button>
<button class="btn btn-sm btn-ghost" onclick="resetPercentileFilter()">显示全部</button>
</div>
<table class="table">
<thead>
<tr><th>排名</th><th>学号</th><th>姓名</th><th>操行分</th></tr>
</thead>
<tbody id="rankingList"></tbody>
</table>
</div>
</div>
</div>
<?php if ($role === '班主任'): ?>
<!-- 升级提示模态框 -->
<div id="upgradeModal" class="modal" style="display:none;">
<div class="modal-content" style="max-width:520px;">
<div class="modal-header">
<h3>🔄 系统升级</h3>
<button class="modal-close" onclick="closeModal('upgradeModal')" id="upgradeCloseBtn">&times;</button>
</div>
<div style="padding: 16px 0;">
<div style="text-align: center; margin-bottom: 12px;">
<p style="font-size: 15px; color: var(--color-text);">检测到数据库有新版本可用</p>
<p style="font-size: 13px; color: var(--color-text-muted); margin-top: 8px;">
当前版本: <span id="currentDbVersion">--</span> → 目标版本: <span id="targetDbVersion">--</span>
</p>
</div>
<div id="upgradeStepsList" style="margin: 12px 0;"></div>
<div id="upgradeResult" style="display:none; margin-top: 12px;"></div>
<p id="upgradeWarning" class="text-danger" style="font-size: 12px; text-align: center; margin-top: 8px;">⚠️ 升级前请确保已备份数据库</p>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeModal('upgradeModal')" id="upgradeLaterBtn">稍后再说</button>
<button class="btn btn-primary" onclick="startUpgrade()" id="startUpgradeBtn">开始升级</button>
</div>
</div>
</div>
<?php endif; ?>
<script>window.PAGE_CONFIG = { role: '<?php echo $role; ?>' };</script>
<script src="/assets/js/dashboard.js"></script>
<?php if ($role === '班主任'): ?>
<script>
(function() {
var upgradeSteps = [];
var currentStepIndex = 0;
function escapeHtml(str) {
if (typeof str !== 'string') return '';
var div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
fetch('/api/check_upgrade.php')
.then(function(r) { return r.json(); })
.then(function(data) {
// 检查是否返回了错误
if (data.error) {
console.warn('升级检查失败:', data.error);
return;
}
if (data.needs_upgrade) {
document.getElementById('currentDbVersion').textContent = data.current;
document.getElementById('targetDbVersion').textContent = data.target;
upgradeSteps = data.steps || [];
// 渲染步骤列表
var listHtml = '';
for (var i = 0; i < upgradeSteps.length; i++) {
listHtml += '<div style="display:flex;align-items:center;padding:8px 12px;margin:4px 0;border-radius:6px;font-size:13px;background:var(--color-hover);border-left:3px solid var(--color-border);" id="ustep-' + i + '">' +
'<span style="margin-right:8px;" id="ustep-icon-' + i + '">○</span>' +
'<span>升级至 v' + escapeHtml(upgradeSteps[i].version) + '</span>' +
'</div>';
}
document.getElementById('upgradeStepsList').innerHTML = listHtml;
document.getElementById('upgradeModal').style.display = 'flex';
}
})
.catch(function(err) {
console.warn('升级检查请求失败:', err);
});
window.startUpgrade = function() {
var btn = document.getElementById('startUpgradeBtn');
var closeBtn = document.getElementById('upgradeCloseBtn');
var laterBtn = document.getElementById('upgradeLaterBtn');
btn.disabled = true;
btn.textContent = '升级中...';
btn.style.opacity = '0.7';
closeBtn.style.display = 'none';
laterBtn.style.display = 'none';
document.getElementById('upgradeWarning').style.display = 'none';
currentStepIndex = 0;
executeNextUpgradeStep();
};
function executeNextUpgradeStep() {
if (currentStepIndex >= upgradeSteps.length) {
// 所有步骤完成
var btn = document.getElementById('startUpgradeBtn');
btn.textContent = '升级完成 ✓';
btn.style.background = '#52c41a';
var laterBtn = document.getElementById('upgradeLaterBtn');
laterBtn.style.display = '';
laterBtn.textContent = '关闭';
laterBtn.onclick = function() { location.reload(); };
document.getElementById('upgradeResult').style.display = 'block';
document.getElementById('upgradeResult').innerHTML = '<div style="background:#f6ffed;border:1px solid #b7eb8f;border-radius:6px;padding:12px;text-align:center;color:var(--color-success);font-size:14px;">✓ 数据库升级成功!</div>';
return;
}
var step = upgradeSteps[currentStepIndex];
var stepEl = document.getElementById('ustep-' + currentStepIndex);
var iconEl = document.getElementById('ustep-icon-' + currentStepIndex);
// 标记为执行中
if (stepEl) {
stepEl.style.borderLeftColor = 'var(--color-primary)';
stepEl.style.background = 'var(--color-primary-light)';
}
if (iconEl) iconEl.textContent = '⟳';
fetch('/api/execute_upgrade.php?action=step&version=' + encodeURIComponent(step.version), { method: 'POST' })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
if (stepEl) {
stepEl.style.borderLeftColor = 'var(--color-success)';
stepEl.style.background = '#f6ffed';
}
if (iconEl) iconEl.textContent = '✓';
currentStepIndex++;
executeNextUpgradeStep();
} else {
if (stepEl) {
stepEl.style.borderLeftColor = 'var(--color-danger)';
stepEl.style.background = 'var(--color-danger-light)';
}
if (iconEl) iconEl.textContent = '✗';
showUpgradeError(data.error || '未知错误');
}
})
.catch(function(err) {
if (stepEl) {
stepEl.style.borderLeftColor = 'var(--color-danger)';
stepEl.style.background = 'var(--color-danger-light)';
}
if (iconEl) iconEl.textContent = '✗';
showUpgradeError(err.message);
});
}
function showUpgradeError(msg) {
var btn = document.getElementById('startUpgradeBtn');
btn.textContent = '升级失败';
btn.style.background = 'var(--color-danger)';
btn.disabled = false;
btn.style.opacity = '';
var laterBtn = document.getElementById('upgradeLaterBtn');
laterBtn.style.display = '';
laterBtn.textContent = '关闭';
document.getElementById('upgradeResult').style.display = 'block';
document.getElementById('upgradeResult').innerHTML = '<div style="background:var(--color-danger-light);border:1px solid #ffccc7;border-radius:6px;padding:12px;color:var(--color-danger-dark);font-size:13px;"><strong>升级失败:</strong>' + escapeHtml(msg) + '</div>';
}
})();
</script>
<?php endif; ?>
<?php include __DIR__ . '/../includes/footer.php'; ?>

120
frontend/admin/history.php Normal file
View File

@@ -0,0 +1,120 @@
<?php
/**
* 多班级版班级管理系统 - 管理端历史记录
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '历史记录';
$role = $_SESSION['role'] ?? '';
include __DIR__ . '/../includes/header.php';
?>
<?php include __DIR__ . '/../includes/nav.php'; ?>
<div class="container">
<div class="card">
<div class="filter-bar" id="historyFilterBar">
<button class="btn btn-primary" onclick="loadHistory(1)">查询</button>
<button class="btn btn-ghost" id="filterToggleBtn" onclick="toggleFilterPanel()">展开筛选 ▼</button>
<label class="history-grouped-label">
<input type="checkbox" id="historyGrouped" checked onchange="loadHistory(1)"> 批次合并
</label>
<?php if ($role === '班主任'): ?>
<button class="btn btn-secondary" onclick="exportHistoryRecords()">导出</button>
<?php endif; ?>
</div>
<!-- 高级筛选面板(默认折叠) -->
<div id="advancedFilters" style="display:none; padding: 0 16px 16px; border-top: 1px solid var(--color-border-light);">
<div style="display:flex; flex-wrap:wrap; gap:16px; padding-top:16px;">
<div class="filter-group">
<label>学生</label>
<select id="historyStudentId" onchange="onStudentFilterChange()">
<option value="">全部</option>
</select>
</div>
<div class="filter-group">
<label>科目</label>
<select id="historySubjectFilter" onchange="onSubjectFilterChange()">
<option value="">全部科目</option>
</select>
</div>
<div class="filter-group">
<label>搜索原因</label>
<input type="text" id="historyReasonSearch" placeholder="输入关键词..." style="min-width:150px;">
</div>
<div class="filter-group">
<label>开始日期</label>
<input type="date" id="historyStartDate">
</div>
<div class="filter-group">
<label>结束日期</label>
<input type="date" id="historyEndDate">
</div>
<div class="filter-group">
<label>扣分类型</label>
<select id="historyReasonFilter">
<option value="">全部类型</option>
<option value="卫生">卫生</option>
<option value="课堂">课堂</option>
<option value="纪律">纪律</option>
<option value="作业">作业</option>
<option value="考勤">考勤</option>
<option value="劳动">劳动</option>
<option value="志愿">志愿</option>
</select>
</div>
<?php if ($role === '班主任' || $role === '班长'): ?>
<div class="filter-group">
<label>状态</label>
<select id="historyStatusFilter">
<option value="">全部</option>
<option value="0">正常</option>
<option value="1">已撤销</option>
</select>
</div>
<?php endif; ?>
</div>
</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr id="historyTableHead">
<th>类型</th>
<th>分值</th>
<th>原因</th>
<th>学生</th>
<th style="white-space: nowrap; min-width: 80px;">操作人</th>
<th>时间</th>
<?php if ($role === '班主任' || $role === '班长' || $role === '考勤委员'): ?>
<th>操作</th>
<?php endif; ?>
</tr>
</thead>
<tbody id="historyList"></tbody>
</table>
</div>
<div class="pagination" id="historyPagination"></div>
</div>
</div>
<script>window.PAGE_CONFIG = { role: '<?php echo htmlspecialchars($role, ENT_QUOTES, 'UTF-8'); ?>', 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'; ?>

255
frontend/admin/homework.php Normal file
View File

@@ -0,0 +1,255 @@
<?php
/**
* 多班级版班级管理系统 - 管理端作业扣分
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '作业扣分';
$role = $_SESSION['role'] ?? '';
if (!in_array($role, ['班主任', '学习委员'])) {
header('Location: /admin/dashboard.php');
exit();
}
include __DIR__ . '/../includes/header.php';
?>
<?php include __DIR__ . '/../includes/nav.php'; ?>
<div class="container">
<!-- 科目管理折叠面板 -->
<div class="card collapsible-card" style="margin-bottom: 20px;">
<div class="collapsible-header" id="subjectPanelHeader">
<h3 style="margin: 0; font-size: 16px;">科目管理</h3>
<span id="subjectPanelToggle" class="toggle-icon">▶ 展开</span>
</div>
<div id="subjectPanelContent" class="collapsible-content">
<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="action-bar">
<div class="action-buttons">
<button class="btn btn-primary" onclick="showBatchPointsModal()">批量加减分</button>
</div>
</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<th><input type="checkbox" id="selectAll" onclick="toggleSelectAll()"></th>
<th>学号</th>
<th>姓名</th>
<th>当前操行分</th>
<th>操作</th>
</tr>
</thead>
<tbody id="studentList"></tbody>
</table>
</div>
</div>
</div>
<div id="batchPointsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>批量加减分</h3>
<button class="modal-close" onclick="closeModal('batchPointsModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); handleSubmitPoints()">
<div class="form-group">
<label>已选学生</label>
<div id="selectedStudentsCount" class="selected-info">未选择学生</div>
</div>
<?php if (in_array($role, ['班主任', '学习委员'])): ?>
<div class="form-group">
<label>科目</label>
<select id="hwSubjectSelect">
<option value="">不选择科目</option>
</select>
</div>
<?php endif; ?>
<div class="form-group">
<label>扣分类型</label>
<div class="deduction-types" style="display: flex; flex-wrap: wrap; gap: 6px;">
<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(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>
<?php if (in_array($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" value="<?php echo date('Y-m-d'); ?>">
</div>
<?php endif; ?>
<div class="form-group">
<label>分数变动</label>
<input type="number" id="pointsChange" required min="-5" max="5" step="1" placeholder="正数加分,负数扣分">
<small><?php
if ($role === '学习委员') echo '学习委员单次±5分以内';
else echo '班主任无限制';
?></small>
</div>
<div class="form-group">
<label>原因</label>
<textarea id="pointsReason" rows="3" required placeholder="请输入加减分原因"></textarea>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">确认提交</button>
<button type="button" class="btn" onclick="closeModal('batchPointsModal')">取消</button>
</div>
</form>
</div>
</div>
<!-- 添加科目模态框 -->
<div id="addSubjectModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>添加科目</h3>
<button class="modal-close" onclick="closeModal('addSubjectModal')">&times;</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')">&times;</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: var(--color-hover);
padding: 12px 20px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 15px;
}
.subject-name {
font-weight: 500;
font-size: 16px;
}
.subject-code {
color: var(--color-text-muted);
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-card .collapsible-header {
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
user-select: none;
transition: background 0.2s;
border-radius: 12px;
}
.collapsible-card .collapsible-header:hover {
background: var(--color-hover, #f7fafc);
}
.collapsible-card .collapsible-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out, padding 0.3s ease-out;
padding: 0 20px;
}
.collapsible-card .collapsible-content.expanded {
max-height: 1000px;
padding: 0 20px 20px;
}
.toggle-icon {
font-size: 12px;
color: var(--color-text-secondary, #666);
}
</style>
<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'; ?>

View File

@@ -0,0 +1,81 @@
<?php
/**
* 多班级版班级管理系统 - 管理端修改密码
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '修改密码';
$role = $_SESSION['role'] ?? '';
include __DIR__ . '/../includes/header.php';
?>
<?php include __DIR__ . '/../includes/nav.php'; ?>
<div class="container">
<div class="card">
<div class="card-title">修改密码</div>
<form id="passwordForm">
<div class="form-group">
<label>原密码 <span style="color:red;">*</span></label>
<input type="password" id="oldPassword" required>
</div>
<div class="form-group">
<label>新密码 <span style="color:red;">*</span></label>
<input type="password" id="newPassword" required>
<small>密码长度6-20位需包含大写字母、小写字母、数字、特殊符号中的至少3种</small>
</div>
<div class="form-group">
<label>确认新密码 <span style="color:red;">*</span></label>
<input type="password" id="confirmPassword" required>
</div>
<button type="submit" class="btn btn-primary">确认修改</button>
</form>
</div>
</div>
<script>
document.getElementById('passwordForm').addEventListener('submit', async (e) => {
e.preventDefault();
const oldPassword = document.getElementById('oldPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (newPassword !== confirmPassword) {
showToast('两次输入的新密码不一致', 'error');
return;
}
if (newPassword.length < 6 || newPassword.length > 20) {
showToast('密码长度需为6-20位', 'error');
return;
}
const res = await apiPost('/api/auth/change-password', {
old_password: oldPassword,
new_password: newPassword
});
if (res && res.success) {
showToast('密码修改成功,请重新登录');
setTimeout(() => logout(), 1500);
} else {
showToast(res?.message || '密码修改失败', 'error');
}
});
</script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,91 @@
<?php
/**
* 多班级版班级管理系统 - 排行榜页
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
$page_title = '排行榜';
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || !in_array($_SESSION['user_type'], ['admin', 'super_admin'])) {
header('Location: /index.php');
exit();
}
$role = $_SESSION['role'] ?? '';
if (!in_array($role, ['班主任', '系统管理员'])) {
header('Location: /admin/dashboard.php');
exit();
}
require_once __DIR__ . '/../includes/header.php';
require_once __DIR__ . '/../includes/nav.php';
?>
<div class="container">
<div class="page-header">
<h2>排行榜</h2>
</div>
<div class="tab-bar">
<button class="tab-btn active" onclick="switchTab('conduct', this)">操行分排行</button>
<button class="tab-btn" onclick="switchTab('attendance', this)">考勤排行</button>
<button class="tab-btn" onclick="switchTab('homework', this)">作业排行</button>
</div>
<div class="card">
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<th>排名</th>
<th>学号</th>
<th>姓名</th>
<th>分值</th>
</tr>
</thead>
<tbody id="rankingList">
<tr><td colspan="4" style="text-align:center;">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<style>
.tab-bar {
display: flex;
gap: 0;
margin-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.tab-btn {
padding: 10px 24px;
border: none;
background: none;
cursor: pointer;
font-size: 14px;
color: #666;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: all 0.2s;
}
.tab-btn:hover {
color: #333;
}
.tab-btn.active {
color: #4a6cf7;
border-bottom-color: #4a6cf7;
font-weight: 600;
}
</style>
<script src="/assets/js/rankings.js"></script>
<?php require_once __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,264 @@
<?php
/**
* 多班级版班级管理系统 - 学期管理页面
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || !in_array($_SESSION['user_type'], ['admin', 'super_admin'])) {
header('Location: /index.php');
exit();
}
$role = $_SESSION['role'] ?? '';
if ($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">
<h3>周期重置</h3>
<p class="text-muted" style="margin-bottom:15px;">手动触发当前班级的周/月操行分重置(重置前会自动创建分数快照)</p>
<div class="action-bar">
<button class="btn btn-warning" onclick="confirmPeriodReset('weekly')">执行本周重置</button>
<button class="btn btn-warning" onclick="confirmPeriodReset('monthly')">执行本月重置</button>
<button class="btn btn-secondary" onclick="showPeriodArchives('weekly')">查看周归档</button>
<button class="btn btn-secondary" onclick="showPeriodArchives('monthly')">查看月归档</button>
</div>
</div>
<div class="card">
<div class="action-bar">
<button class="btn btn-primary" onclick="showCreateSemesterModal()">创建新学期</button>
</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<th>学期名称</th>
<th>开始日期</th>
<th>结束日期</th>
<th>当前周数</th>
<th>状态</th>
<th>记录数</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="semesterList"></tbody>
</table>
</div>
</div>
</div>
<!-- 创建学期模态框 -->
<div id="createSemesterModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>创建新学期</h3>
<button class="modal-close" onclick="closeModal('createSemesterModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitCreateSemester()">
<div class="form-group">
<label>学期名称 <span style="color:red;">*</span></label>
<input type="text" id="semesterName" required placeholder="如2025春季学期" maxlength="100">
</div>
<div style="margin-bottom: 8px;">
<button type="button" class="btn btn-sm btn-outline" style="margin-right: 6px;" onclick="fillSemesterDates('upper')">上学期9月-次年2月</button>
<button type="button" class="btn btn-sm btn-outline" onclick="fillSemesterDates('lower')">下学期3月-7月</button>
</div>
<div class="form-group">
<label>开始日期</label>
<input type="date" id="semesterStartDate">
</div>
<div class="form-group">
<label>结束日期 <small style="color: #999;">(可选)</small></label>
<input type="date" id="semesterEndDate" placeholder="可选,不确定可不填">
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">创建学期</button>
<button type="button" class="btn" onclick="closeModal('createSemesterModal')">取消</button>
</div>
</form>
</div>
</div>
<!-- 编辑学期模态框 -->
<div id="editSemesterModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>编辑学期</h3>
<button class="modal-close" onclick="closeModal('editSemesterModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitEditSemester()">
<input type="hidden" id="editSemesterId">
<div class="form-group">
<label>学期名称 <span style="color:red;">*</span></label>
<input type="text" id="editSemesterName" required placeholder="如2025春季学期" maxlength="100">
</div>
<div class="form-group">
<label>开始日期</label>
<input type="date" id="editSemesterStartDate">
</div>
<div class="form-group">
<label>结束日期 <small style="color: #999;">(可选)</small></label>
<input type="date" id="editSemesterEndDate">
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">保存修改</button>
<button type="button" class="btn btn-danger" onclick="deleteSemester()">删除学期</button>
<button type="button" class="btn" onclick="closeModal('editSemesterModal')">取消</button>
</div>
</form>
</div>
</div>
<!-- 关联数据确认模态框 -->
<div id="associateConfirmModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>关联数据到学期</h3>
<button class="modal-close" onclick="closeModal('associateConfirmModal')">&times;</button>
</div>
<div class="form-group">
<p id="associateConfirmText" style="margin: 10px 0;"></p>
<p style="color: #666; font-size: 14px;">将把该日期范围内所有未分配学期的操行分记录和考勤记录关联到此学期。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="confirmAssociate()">确认关联</button>
<button type="button" class="btn" onclick="closeModal('associateConfirmModal')">取消</button>
</div>
</div>
</div>
<!-- 归档确认模态框 -->
<div id="archiveConfirmModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>确认归档学期</h3>
<button class="modal-close" onclick="closeModal('archiveConfirmModal')">&times;</button>
</div>
<div class="form-group">
<p id="archiveConfirmText" style="margin: 10px 0;"></p>
<p style="color: #666; font-size: 14px;">归档会创建所有学生当前操行分的数据快照,原始数据不受影响。</p>
<div style="margin-top: 10px;">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 14px;">
<input type="checkbox" id="archiveResetScores">
归档后重置所有学生操行分为初始值60分
</label>
</div>
<p style="color: #e74c3c; font-size: 13px; margin-top: 6px;">注意:归档前需确保学期已设置开始日期,否则无法归档。归档后该学期数据将变为只读,不可撤销。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="confirmArchive()">确认归档</button>
<button type="button" class="btn" onclick="closeModal('archiveConfirmModal')">取消</button>
</div>
</div>
</div>
<!-- 归档数据查看模态框 -->
<div id="archiveDataModal" class="modal">
<div class="modal-content" style="max-width: 700px;">
<div class="modal-header">
<h3 id="archiveDataTitle">归档数据</h3>
<button class="modal-close" onclick="closeModal('archiveDataModal')">&times;</button>
</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<th colspan="2" style="text-align:center; border-bottom: none;">基本信息</th>
<th rowspan="2" style="vertical-align: middle;">姓名</th>
<th rowspan="2" style="vertical-align: middle;">操行分</th>
<th colspan="4" style="text-align:center; border-bottom: none;">考勤统计</th>
<th colspan="3" style="text-align:center; border-bottom: none;">作业统计</th>
</tr>
<tr>
<th>排名</th>
<th>学号</th>
<th>出勤</th>
<th>缺勤</th>
<th>迟到</th>
<th>请假</th>
<th>已交</th>
<th>未交</th>
<th>迟交</th>
</tr>
</thead>
<tbody id="archiveDataList"></tbody>
</table>
</div>
<div class="pagination" id="archivePagination"></div>
<div class="modal-footer">
<button type="button" class="btn" onclick="closeModal('archiveDataModal')">关闭</button>
</div>
</div>
</div>
<!-- 周期重置确认模态框 -->
<div id="periodResetModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>确认周期重置</h3>
<button class="modal-close" onclick="closeModal('periodResetModal')">&times;</button>
</div>
<div class="form-group">
<p id="periodResetText" style="margin: 10px 0;"></p>
<p style="color: #e74c3c; font-size: 13px; margin-top: 6px;">注意:重置前会自动保存当前操行分快照,重置后所有学生操行分将恢复为初始值。此操作不可撤销。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" onclick="executePeriodReset()">确认重置</button>
<button type="button" class="btn" onclick="closeModal('periodResetModal')">取消</button>
</div>
</div>
</div>
<!-- 周期归档数据查看模态框 -->
<div id="periodArchivesModal" class="modal">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h3 id="periodArchivesTitle">周期归档数据</h3>
<button class="modal-close" onclick="closeModal('periodArchivesModal')">&times;</button>
</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<th>周期标签</th>
<th>排名</th>
<th>学号</th>
<th>姓名</th>
<th>操行分</th>
<th>触发方式</th>
<th>归档时间</th>
</tr>
</thead>
<tbody id="periodArchivesList"></tbody>
</table>
</div>
<div class="pagination" id="periodArchivePagination"></div>
<div class="modal-footer">
<button type="button" class="btn" onclick="closeModal('periodArchivesModal')">关闭</button>
</div>
</div>
</div>
<script src="/assets/js/modules/modal-utils.js"></script>
<script src="/assets/js/semesters.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

218
frontend/admin/students.php Normal file
View File

@@ -0,0 +1,218 @@
<?php
/**
* 多班级版班级管理系统 - 管理端学生管理
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Location: /index.php');
exit();
}
$page_title = '学生管理';
$role = $_SESSION['role'] ?? '';
include __DIR__ . '/../includes/header.php';
?>
<?php include __DIR__ . '/../includes/nav.php'; ?>
<div class="container">
<div class="card">
<div class="action-bar">
<div class="action-buttons">
<?php if ($role === '班主任'): ?>
<button class="btn btn-primary" onclick="showImportModal()">导入学生</button>
<button class="btn btn-primary" onclick="showAddStudentModal()">新增学生</button>
<?php endif; ?>
</div>
<div class="search-bar">
<input type="text" id="searchInput" placeholder="搜索姓名/学号">
<button class="btn btn-primary" onclick="loadStudents(1)">搜索</button>
</div>
</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<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>
</tr>
</thead>
<tbody id="studentList"></tbody>
</table>
</div>
<div class="pagination" id="pagination"></div>
</div>
</div>
<!-- 导入学生模态框 -->
<div id="importModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>导入学生</h3>
<button class="modal-close" onclick="closeModal('importModal')">&times;</button>
</div>
<div class="import-area" onclick="document.getElementById('importFile').click()">
<p>点击选择JSON文件</p>
<p class="import-label">或点击此处上传</p>
<input type="file" id="importFile" accept=".json">
<p style="margin-top: 10px; font-size: 12px; color: #999;">
<a href="/assets/uploads/sample_import.json" download style="color: #667eea;">下载示例文件</a>
</p>
</div>
<div id="importPreview" class="preview-table" style="display: none;"></div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="doImport()" id="importBtn" style="display: none;">确认导入</button>
</div>
</div>
</div>
<!-- 新增学生模态框 -->
<div id="addStudentModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>新增学生</h3>
<button class="modal-close" onclick="closeModal('addStudentModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitAddStudent()">
<div class="form-group">
<label>学号 <span style="color:red;">*</span></label>
<input type="text" id="studentNo" required placeholder="4-20位字母数字组合">
<small>学号将作为学生登录账号</small>
</div>
<div class="form-group">
<label>姓名 <span style="color:red;">*</span></label>
<input type="text" id="studentName" required>
</div>
<div class="form-group">
<label>家长账号(推荐手机号)</label>
<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>
</div>
</form>
</div>
</div>
<!-- 编辑学生模态框 -->
<div id="editStudentModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>编辑学生信息</h3>
<button class="modal-close" onclick="closeModal('editStudentModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitEditStudent()">
<input type="hidden" id="editStudentId">
<div class="form-group">
<label>学号</label>
<input type="text" id="editStudentNo" disabled>
</div>
<div class="form-group">
<label>姓名</label>
<input type="text" id="editStudentName" required maxlength="50">
</div>
<div class="form-group">
<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>
</div>
</form>
</div>
</div>
<!-- 重置学生密码模态框 -->
<div id="resetStudentPasswordModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>重置学生密码</h3>
<button class="modal-close" onclick="closeModal('resetStudentPasswordModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitResetStudentPassword()">
<input type="hidden" id="resetStudentId">
<p id="resetStudentInfo" style="margin: 10px 0;"></p>
<div class="form-group">
<label>新密码</label>
<input type="password" id="newStudentPassword" required minlength="6" maxlength="20">
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-warning">确认重置</button>
<button type="button" class="btn" onclick="closeModal('resetStudentPasswordModal')">取消</button>
</div>
</form>
</div>
</div>
<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">
<div class="modal-content">
<div class="modal-header">
<h3>批量加减分</h3>
<button class="modal-close" onclick="closeModal('batchPointsModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitBatchPoints()">
<div class="form-group">
<label>选中学生</label>
<div id="selectedStudentsCount">0 人</div>
</div>
<div class="form-group">
<label>分数变动</label>
<input type="number" id="pointsChange" required placeholder="正数为加分,负数为扣分">
<small><?php
$hints = [
'班长' => '班长单次±5分以内',
'学习委员' => '学习委员单次±5分以内',
'考勤委员' => '考勤委员仅限扣分单次最多扣8分',
'劳动委员' => '劳动委员单次±1分以内',
'志愿委员' => '志愿委员仅限加分,最多+5分',
];
echo $hints[$role] ?? '班主任无限制';
?></small>
</div>
<div class="form-group">
<label>原因</label>
<textarea id="pointsReason" required rows="3" placeholder="请填写加减分原因"></textarea>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">确认提交</button>
<button type="button" class="btn" onclick="closeModal('batchPointsModal')">取消</button>
</div>
</form>
</div>
</div>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,14 @@
<?php
/**
* 多班级版班级管理系统 - 科目管理(已合并至作业管理页)
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
header('Location: /admin/homework.php');
exit();

View File

@@ -0,0 +1,64 @@
<?php
/**
* 多班级版班级管理系统 - Session 退出清除接口
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*
* 说明:退出登录时,清除 PHP Session
*/
// 引入配置文件以初始化 Session
require_once __DIR__ . '/../config.php';
// 设置响应头
header('Content-Type: application/json; charset=utf-8');
// 仅允许同源请求
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// 处理预检请求
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
// 只允许 POST 请求
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode([
'success' => false,
'message' => '仅支持 POST 请求'
]);
exit();
}
// CSRF 风险说明:此接口仅清除 Session无敏感数据操作。
// 部署于同域 Nginx 反代下,浏览器同源策略已阻止跨域调用,实际风险较低。
// 清除 Session
$_SESSION = array();
// 如果使用了 cookie删除 cookie
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
// 销毁 Session
session_destroy();
// 返回成功响应
http_response_code(200);
echo json_encode([
'success' => true,
'message' => 'Session 已清除'
]);
exit();

View File

@@ -0,0 +1,219 @@
<?php
/**
* 多班级版班级管理系统 - Session 保存接口
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*
* 说明:登录成功后,前端调用此接口将用户信息同步到 PHP Session
*/
// 引入配置文件以初始化 Session
require_once __DIR__ . '/../config.php';
// 设置响应头
header('Content-Type: application/json; charset=utf-8');
// 仅允许同源请求
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
// 处理预检请求
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
// 只允许 POST 请求
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode([
'success' => false,
'message' => '仅支持 POST 请求'
]);
exit();
}
// CSRF 防护:验证 Origin/Referer 头确保同源请求
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$host = $_SERVER['HTTP_HOST'] ?? '';
$serverName = $_SERVER['SERVER_NAME'] ?? '';
if (!empty($origin)) {
$parsedOrigin = parse_url($origin, PHP_URL_HOST);
if ($parsedOrigin !== $host && $parsedOrigin !== $serverName) {
http_response_code(403);
echo json_encode([
'success' => false,
'message' => '跨域请求被拒绝'
]);
exit();
}
} elseif (!empty($referer)) {
$parsedReferer = parse_url($referer, PHP_URL_HOST);
if ($parsedReferer !== $host && $parsedReferer !== $serverName) {
http_response_code(403);
echo json_encode([
'success' => false,
'message' => '跨域请求被拒绝'
]);
exit();
}
}
// 获取原始输入
$input = file_get_contents('php://input');
if (empty($input)) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => '请求数据为空'
]);
exit();
}
// 解析 JSON 数据
$data = json_decode($input, true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => 'JSON 解析失败: ' . json_last_error_msg()
]);
exit();
}
// 验证必要字段
$requiredFields = ['user_id', 'user_type', 'username'];
$missingFields = [];
foreach ($requiredFields as $field) {
if (!isset($data[$field]) || empty($data[$field])) {
$missingFields[] = $field;
}
}
if (!empty($missingFields)) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => '缺少必要字段: ' . implode(', ', $missingFields)
]);
exit();
}
// 验证 user_type 是否合法
$validUserTypes = ['student', 'parent', 'admin', 'super_admin'];
if (!in_array($data['user_type'], $validUserTypes)) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => '无效的用户类型'
]);
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 => true,
CURLOPT_SSL_VERIFYHOST => 2
]);
$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();
}
// 从后端 JWT 解析权威数据(不信任客户端传入的 user_type/role
$tokenData_user = $tokenData['data'];
// 登录成功后重新生成 Session ID防止 Session 固定攻击
session_regenerate_id(true);
$_SESSION['user_id'] = intval($tokenData_user['user_id']);
$_SESSION['user_type'] = $tokenData_user['user_type'];
$_SESSION['username'] = $tokenData_user['username'];
$_SESSION['real_name'] = $tokenData_user['real_name'] ?? '';
$_SESSION['role'] = $tokenData_user['role'] ?? '';
$_SESSION['class_id'] = $tokenData_user['class_id'] ?? null;
$_SESSION['class_name'] = $tokenData_user['class_name'] ?? '';
$_SESSION['login_time'] = time();
$_SESSION['jwt_token'] = $token;
// 如果是学生,额外设置 student_id仅从 JWT 解析,不信任客户端传入值)
if ($_SESSION['user_type'] === 'student') {
$studentId = $tokenData_user['student_id'] ?? null;
if (empty($studentId)) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => '学生类型必须提供 student_id'
]);
exit();
}
$_SESSION['student_id'] = $studentId;
}
// 保存 Session
session_write_close();
// 返回成功响应
http_response_code(200);
echo json_encode([
'success' => true,
'message' => 'Session 保存成功'
]);
exit();

View File

@@ -0,0 +1,265 @@
/**
* 多班级版班级管理系统 - 管理端样式
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
/* 批量操作栏 */
.batch-bar {
background: #f0f4ff;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.batch-info {
color: var(--color-primary);
font-weight: 500;
}
/* 导入区域 */
.import-area {
border: 2px dashed var(--color-border);
border-radius: 12px;
padding: 30px;
text-align: center;
margin-bottom: 20px;
transition: border-color 0.3s;
cursor: pointer;
}
.import-area:hover {
border-color: var(--color-primary);
}
.import-area input {
display: none;
}
.import-label {
color: var(--color-primary);
text-decoration: underline;
cursor: pointer;
}
/* 预览表格 */
.preview-table {
max-height: 300px;
overflow-y: auto;
margin-top: 16px;
}
/* 筛选栏 */
.filter-bar {
background: var(--color-hover);
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: flex-end;
}
.filter-group {
flex: 1;
min-width: 150px;
}
.filter-group label {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: var(--color-text-secondary);
}
.filter-group input,
.filter-group select {
width: 100%;
padding: 8px;
border: 1px solid var(--color-border);
border-radius: 4px;
}
/* 作业卡片 */
.assignment-card {
margin-bottom: 20px;
}
.assignment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 10px;
}
.assignment-title {
font-size: 16px;
font-weight: bold;
color: var(--color-text);
}
.assignment-meta {
color: var(--color-text-muted);
font-size: 12px;
}
/* 状态选择器 */
.status-select {
padding: 4px 8px;
border: 1px solid var(--color-border);
border-radius: 4px;
font-size: 12px;
}
/* 复选框 */
.student-checkbox {
width: 18px;
height: 18px;
cursor: pointer;
}
/* 扣分类型按钮组 */
.deduction-types {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
/* 考勤学生方格网格 */
.student-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 15px 0;
}
.student-cell {
width: calc(100% / 7 - 10px);
min-height: 60px;
border: 2px solid #e5e7eb;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
padding: 8px 4px;
text-align: center;
word-break: break-all;
user-select: none;
}
.student-cell:hover {
background: #f3f4f6;
border-color: #9ca3af;
}
.student-cell.selected {
background: #fee2e2;
border-color: #ef4444;
color: #dc2626;
}
.student-cell.has-record {
border: 2px dashed #9ca3af;
opacity: 0.7;
}
.attendance-toolbar {
display: flex;
align-items: center;
gap: 10px;
margin: 15px 0;
flex-wrap: wrap;
}
.toolbar-field {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--color-hover);
border: 1px solid var(--color-border);
border-radius: 4px;
padding: 6px 10px;
}
.toolbar-field .toolbar-label {
font-size: 12px;
color: var(--color-text-secondary);
white-space: nowrap;
}
.toolbar-field input,
.toolbar-field select {
border: none;
background: transparent;
outline: none;
font-size: 13px;
padding: 0;
min-width: 0;
}
.attendance-toolbar .status-group {
display: flex;
gap: 8px;
}
.attendance-toolbar .status-btn {
padding: 6px 16px;
border: 2px solid #e5e7eb;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.attendance-toolbar .status-btn.active {
border-color: var(--color-primary);
background: var(--color-primary-light);
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);
}
}
@media (max-width: 480px) {
.student-cell {
width: calc(100% / 3 - 10px);
}
}
.preserve-newlines {
white-space: normal;
word-break: break-word;
}

View File

@@ -0,0 +1,998 @@
/**
* 多班级版班级管理系统 - 全局样式
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
:root {
/* 主色调 */
--color-primary: #4361ee;
--color-primary-light: #eef0ff;
--color-primary-dark: #3651d4;
--color-primary-hover: #3a56d4;
/* 语义色 */
--color-danger: #e53e3e;
--color-danger-light: #fff5f5;
--color-danger-dark: #c53030;
--color-success: #38a169;
--color-success-light: #f0fff4;
--color-warning: #d69e2e;
--color-warning-light: #fffff0;
/* 灰度 */
--color-text: #1a202c;
--color-text-secondary: #4a5568;
--color-text-muted: #a0aec0;
--color-bg: #f5f7fb;
--color-card: #ffffff;
--color-border: #e2e8f0;
--color-border-light: #edf2f7;
--color-hover: #f7fafc;
/* 按钮 */
--btn-primary-bg: var(--color-primary);
--btn-primary-text: #ffffff;
--btn-outline-bg: transparent;
--btn-outline-border: var(--color-primary);
--btn-outline-text: var(--color-primary);
--btn-danger-bg: var(--color-danger);
--btn-danger-text: #ffffff;
--btn-ghost-text: var(--color-text-secondary);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--color-bg);
min-height: 100vh;
font-size: 14px;
color: var(--color-text);
}
/* ========== 登录页面 ========== */
.login-container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 400px;
max-width: 90%;
margin: 100px auto;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h1 {
font-size: 24px;
color: var(--color-text);
margin-bottom: 8px;
}
.login-header p {
color: var(--color-text-secondary);
font-size: 14px;
}
.login-form .form-group {
margin-bottom: 20px;
}
.login-form label {
display: block;
margin-bottom: 6px;
color: var(--color-text-secondary);
font-weight: 500;
}
.login-form input {
width: 100%;
padding: 12px;
border: 1px solid var(--color-border);
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
.login-form input:focus {
outline: none;
border-color: var(--color-primary);
}
.btn-login {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, var(--color-primary) 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: opacity 0.3s;
}
.btn-login:hover {
opacity: 0.9;
}
.error-msg {
background: var(--color-danger-light);
color: var(--color-danger);
padding: 10px;
border-radius: 8px;
margin-top: 15px;
text-align: center;
font-size: 13px;
}
.login-footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid var(--color-border-light);
color: var(--color-text-muted);
font-size: 12px;
}
/* ========== 公共头部 ========== */
.header {
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: 12px 24px;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.header h1 {
font-size: 18px;
color: var(--color-text);
}
.header-info {
display: flex;
align-items: center;
gap: 16px;
}
.user-name {
color: var(--color-text-secondary);
font-weight: 500;
}
.user-role {
background: var(--color-primary);
color: white;
padding: 2px 8px;
border-radius: 20px;
font-size: 11px;
}
.btn-logout {
background: var(--color-danger);
color: white;
border: none;
padding: 6px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: background 0.3s;
}
.btn-logout:hover {
background: var(--color-danger-dark);
}
/* ========== 导航菜单 ========== */
.nav {
background: white;
padding: 0 24px;
border-bottom: 1px solid var(--color-border-light);
display: flex;
gap: 4px;
overflow-x: auto;
}
.nav-item {
padding: 12px 20px;
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
border-bottom: 2px solid transparent;
text-decoration: none;
display: inline-block;
}
.nav-item:hover {
color: var(--color-primary);
}
.nav-item.active {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
}
/* ========== 容器 ========== */
.container {
max-width: 1200px;
margin: 24px auto;
padding: 0 24px;
}
/* ========== 卡片 ========== */
.card {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.card-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid var(--color-primary);
color: var(--color-text);
}
/* ========== 统计卡片网格 ========== */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 20px;
text-align: center;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: var(--color-primary);
margin: 10px 0;
}
.stat-label {
color: var(--color-text-secondary);
font-size: 13px;
}
/* ========== 表格 ========== */
.table-wrapper {
overflow-x: auto;
overflow-y: visible;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--color-border-light);
}
th {
background: var(--color-hover);
font-weight: 600;
color: var(--color-text-secondary);
}
tr:hover {
background: var(--color-hover);
}
/* ========== 状态标签 ========== */
.status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.status-submitted {
background: #c6f6d5;
color: #22543d;
}
.status-not_submitted {
background: #fed7d7;
color: #742a2a;
}
.status-late {
background: #feebc8;
color: #7c2d12;
}
.status-present {
background: #c6f6d5;
color: #22543d;
}
.status-absent {
background: #fed7d7;
color: #742a2a;
}
.status-leave {
background: #e9d8fd;
color: #553c9a;
}
/* ========== 按钮 ========== */
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.3s;
}
.btn-primary {
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
border: 1px solid transparent;
}
.btn-primary:hover {
background: var(--color-primary-hover);
}
.btn-danger {
background: var(--btn-danger-bg);
color: var(--btn-danger-text);
border: 1px solid transparent;
}
.btn-danger:hover {
background: var(--color-danger-dark);
}
.btn-success {
background: var(--color-success-light);
color: var(--color-success);
border: 1px solid #c6f6d5;
}
.btn-success:hover {
background: #c6f6d5;
}
.btn-warning {
background: var(--color-warning-light);
color: var(--color-warning);
border: 1px solid #fefcbf;
}
.btn-warning:hover {
background: #fefcbf;
}
.btn-info {
background: #e3f2fd;
color: #1565c0;
border: 1px solid #bbdefb;
}
.btn-info:hover {
background: #bbdefb;
}
.btn-secondary {
background: var(--color-hover);
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
}
.btn-secondary:hover {
background: var(--color-border-light);
}
.btn-outline {
background: transparent;
color: var(--color-primary);
border: 1px solid var(--color-primary);
}
.btn-outline:hover {
background: var(--color-primary-light);
}
.btn-ghost {
background: transparent;
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
}
.btn-ghost:hover {
background: var(--color-hover);
border-color: var(--color-text-muted);
}
.btn-outline-danger {
background: transparent;
color: var(--color-danger);
border: 1px solid var(--color-danger);
}
.btn-outline-danger:hover {
background: var(--color-danger-light);
}
.btn-sm {
padding: 4px 10px;
font-size: 12px;
}
/* ========== 模态框 ========== */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 12px;
padding: 24px;
width: 500px;
max-width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-border-light);
}
.modal-header h3 {
font-size: 18px;
color: var(--color-text);
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--color-text-muted);
}
.modal-footer {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--color-border-light);
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* ========== 表单 ========== */
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: var(--color-text-secondary);
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid var(--color-border);
border-radius: 6px;
font-size: 14px;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--color-primary);
}
.form-group small {
display: block;
color: var(--color-text-muted);
font-size: 12px;
margin-top: 4px;
}
.form-group textarea {
min-height: 60px;
resize: vertical;
}
/* ========== 复选框组 ========== */
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.checkbox-group input {
width: auto;
}
/* ========== 操作栏 ========== */
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 12px;
}
.action-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.search-bar {
display: flex;
gap: 10px;
}
.search-bar input {
padding: 8px 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
width: 200px;
}
/* ========== 分页 ========== */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 6px;
margin-top: 20px;
flex-wrap: wrap;
}
.pagination a, .pagination span {
padding: 6px 12px;
border: 1px solid var(--color-border);
border-radius: 4px;
text-decoration: none;
color: var(--color-text-secondary);
cursor: pointer;
min-width: 36px;
text-align: center;
box-sizing: border-box;
transition: all 0.2s;
}
.pagination a:hover {
background: var(--color-primary-light);
border-color: var(--color-primary);
color: var(--color-primary);
}
.pagination .active {
background: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.pagination .ellipsis {
border: none;
cursor: default;
padding: 6px 4px;
color: var(--color-text-muted);
min-width: auto;
}
.pagination .page-jump {
display: flex;
align-items: center;
gap: 4px;
margin-left: 8px;
font-size: 13px;
color: var(--color-text-secondary);
}
.pagination .page-jump input {
width: 50px;
padding: 5px 8px;
border: 1px solid var(--color-border);
border-radius: 4px;
text-align: center;
font-size: 13px;
outline: none;
}
.pagination .page-jump input:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(67, 97, 238, 0.15);
}
.pagination .page-nav {
padding: 6px 10px;
font-size: 13px;
}
/* ========== 提示消息 ========== */
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 12px 24px;
border-radius: 8px;
color: white;
font-size: 14px;
z-index: 1100;
animation: fadeInUp 0.3s ease;
}
.toast-success {
background: var(--color-success);
}
.toast-error {
background: var(--color-danger);
}
.toast-warning {
background: #ed8936;
}
.toast-info {
background: var(--color-primary);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateX(-50%) translateY(20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
/* ========== 加载动画 ========== */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid var(--color-border);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ========== 底部 ========== */
.footer {
text-align: center;
padding: 20px;
color: var(--color-text-muted);
font-size: 12px;
}
/* ========== 记录项 ========== */
.record-item {
padding: 12px 0;
border-bottom: 1px solid var(--color-border-light);
display: flex;
justify-content: space-between;
align-items: center;
}
.record-points {
font-weight: bold;
}
.record-points.plus {
color: var(--color-success);
}
.record-points.minus {
color: var(--color-danger);
}
.record-reason {
flex: 1;
margin: 0 15px;
color: var(--color-text-secondary);
}
.record-time {
font-size: 12px;
color: var(--color-text-muted);
}
.view-more {
text-align: center;
margin-top: 15px;
}
.view-more a {
color: var(--color-primary);
text-decoration: none;
}
.conduct-score {
text-align: center;
padding: 20px;
}
.score-number {
font-size: 64px;
font-weight: bold;
color: var(--color-primary);
}
/* ========== 响应式 ========== */
@media (max-width: 768px) {
.container {
padding: 0 16px;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
th, td {
padding: 8px;
font-size: 12px;
}
.card {
padding: 16px;
}
.nav {
padding: 0 16px;
}
.nav-item {
padding: 10px 14px;
font-size: 13px;
}
.action-bar {
flex-direction: column;
align-items: stretch;
}
.search-bar {
width: 100%;
}
.search-bar input {
flex: 1;
}
}
/* ========== 操作列下拉菜单 ========== */
.action-dropdown {
position: relative;
display: inline-flex;
align-items: center;
gap: 4px;
}
.action-dropdown-toggle {
background: var(--color-hover);
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
padding: 4px 10px;
font-size: 12px;
cursor: pointer;
border-radius: 6px;
transition: all 0.2s;
white-space: nowrap;
}
.action-dropdown-toggle:hover {
background: var(--color-border-light);
border-color: #cbd5e0;
}
.action-dropdown-toggle.open {
background: var(--color-border-light);
border-color: var(--color-text-muted);
}
.action-dropdown-menu {
display: none;
background: white;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
border: 1px solid var(--color-border);
min-width: 120px;
z-index: 9999;
padding: 4px 0;
}
.action-dropdown-menu.show {
display: block;
}
.action-dropdown-menu a {
display: block;
padding: 8px 14px;
color: var(--color-text-secondary);
font-size: 13px;
cursor: pointer;
text-decoration: none;
transition: background 0.15s;
white-space: nowrap;
}
.action-dropdown-menu a:hover {
background: var(--color-hover);
color: #2d3748;
}
.action-dropdown-menu a.danger {
color: var(--color-danger);
border-top: 1px solid var(--color-border-light);
margin-top: 4px;
padding-top: 10px;
}
.action-dropdown-menu a.danger:hover {
background: var(--color-danger-light);
color: var(--color-danger-dark);
}
/* ========== 链接 ========== */
.link {
color: var(--color-primary);
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
/* ========== 文本工具类 ========== */
.text-danger { color: var(--color-danger); }
.text-success { color: var(--color-success); }
.text-muted { color: var(--color-text-muted); }
/* ========== 标签 ========== */
.tag { padding: 2px 8px; border-radius: 10px; font-size: 12px; }
.tag-success { background: #e8f5e9; color: #2e7d32; }
.tag-danger { background: #ffebee; color: #c62828; }
.tag-warning { background: #fff3e0; color: #e65100; }
.tag-info { background: #e3f2fd; color: #1565c0; }
/* ========== 历史记录页优化 ========== */
/* 时间列:确保分两行显示(日期+时间) */
.history-time {
white-space: nowrap;
min-width: 80px;
line-height: 1.5;
vertical-align: top;
}
/* 原因列每行最少7个字自动换行使用td前缀提升优先级防止被preserve-newlines覆盖 */
td.history-reason {
min-width: 7em;
max-width: 200px;
white-space: normal !important;
word-break: break-word;
line-height: 1.5;
vertical-align: top;
}
/* 学生名列:允许换行 */
.history-students {
white-space: normal;
word-break: break-word;
min-width: 60px;
max-width: 120px;
line-height: 1.5;
vertical-align: top;
}
/* 合并记录复选框样式 */
.history-grouped-label {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
font-size: 13px;
padding: 6px 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
background: var(--color-hover);
transition: all 0.2s;
white-space: nowrap;
user-select: none;
}
.history-grouped-label:hover {
border-color: var(--color-primary);
background: var(--color-primary-light);
}
.history-grouped-label input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
/* 合并记录按钮样式 */
.btn-outline-danger {
background: transparent;
color: var(--color-danger);
border: 1px solid var(--color-danger);
padding: 4px 10px;
font-size: 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.btn-outline-danger:hover {
background: var(--color-danger-light);
color: var(--color-danger-dark);
border-color: var(--color-danger-dark);
}

View File

@@ -0,0 +1,14 @@
/**
* admin.js - 管理端公共函数库
*
* 此文件已拆分为独立模块,各模块文件位于 /assets/js/modules/ 目录
* 各页面通过引用对应模块获取所需功能
*
* 模块列表:
* - 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 - 加减分管理函数
*/

View File

@@ -0,0 +1,146 @@
/**
* 多班级版班级管理系统 - 管理员管理页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>
<div class="action-dropdown">
<button class="btn btn-sm action-dropdown-toggle" onclick="toggleActionDropdown(this)">操作 ▼</button>
<div class="action-dropdown-menu">
<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>
</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;
})();

View 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;
})();

View File

@@ -0,0 +1,159 @@
/**
* 多班级版班级管理系统 - 课代表作业管理JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
*
* 版权所有 © Sea Network Technology Studio
*/
(function() {
'use strict';
var currentPage = 1;
var pageSize = 20;
var currentAssignmentId = null;
async function loadHomework(page) {
var res = await apiGet('/api/cadre/homework', { page: page, page_size: pageSize });
if (res && res.success && res.data) {
var items = res.data.items || res.data.records || [];
var total = res.data.total || 0;
var html = '';
if (items.length === 0) {
html = '<tr><td colspan="5" style="text-align:center;">暂无作业记录</td></tr>';
} else {
items.forEach(function(item) {
html += '<tr>' +
'<td>' + escapeHtml(item.title || '-') + '</td>' +
'<td>' + escapeHtml(item.subject_name || '-') + '</td>' +
'<td>' + formatDate(item.deadline) + '</td>' +
'<td>' + escapeHtml(item.description || '-') + '</td>' +
'<td><button class="btn btn-sm btn-outline" onclick="showAbsentModal(' + item.assignment_id + ')">登记缺交</button></td>' +
'</tr>';
});
}
document.getElementById('homeworkList').innerHTML = html;
var totalPages = Math.ceil(total / pageSize);
if (totalPages > 1) {
document.getElementById('pagination').style.display = 'flex';
document.getElementById('pageInfo').textContent = page + ' / ' + totalPages;
document.getElementById('prevBtn').disabled = page <= 1;
document.getElementById('nextBtn').disabled = page >= totalPages;
} else {
document.getElementById('pagination').style.display = 'none';
}
}
}
window.changePage = function(delta) {
currentPage += delta;
loadHomework(currentPage);
};
window.showPublishModal = function() {
document.getElementById('publishForm').reset();
document.getElementById('hwDeadline').value = new Date().toISOString().split('T')[0];
document.getElementById('publishModal').style.display = 'flex';
};
window.submitHomework = async function() {
var title = document.getElementById('hwTitle').value.trim();
var deadline = document.getElementById('hwDeadline').value;
var description = document.getElementById('hwDescription').value.trim();
if (!title) {
showToast('请填写作业标题', 'error');
return;
}
if (!deadline) {
showToast('请选择截止日期', 'error');
return;
}
var res = await apiPost('/api/cadre/homework', {
title: title,
deadline: deadline,
description: description
});
if (res && res.success) {
showToast('作业发布成功');
closeModal('publishModal');
loadHomework(currentPage);
} else {
showToast(res && res.message ? res.message : '发布失败', 'error');
}
};
window.showAbsentModal = async function(assignmentId) {
currentAssignmentId = assignmentId;
var res = await apiGet('/api/admin/students', { page_size: 1000 });
if (res && res.success && res.data) {
var students = res.data.students || res.data.items || [];
var html = '<div class="form-group"><label>选择缺交学生</label></div>';
if (students.length === 0) {
html += '<p style="text-align:center;padding:20px;">暂无学生数据</p>';
} else {
html += '<div class="table-wrapper"><table class="table"><thead><tr>' +
'<th><input type="checkbox" id="selectAllAbsent" onchange="toggleAllAbsent(this)"></th>' +
'<th>学号</th><th>姓名</th></tr></thead><tbody>';
students.forEach(function(s) {
html += '<tr>' +
'<td><input type="checkbox" class="absent-checkbox" data-id="' + s.student_id + '"></td>' +
'<td>' + escapeHtml(s.student_no) + '</td>' +
'<td>' + escapeHtml(s.name) + '</td>' +
'</tr>';
});
html += '</tbody></table></div>';
}
document.getElementById('absentStudentList').innerHTML = html;
document.getElementById('absentModal').style.display = 'flex';
} else {
showToast('获取学生列表失败', 'error');
}
};
window.toggleAllAbsent = function(el) {
var checkboxes = document.querySelectorAll('.absent-checkbox');
checkboxes.forEach(function(cb) { cb.checked = el.checked; });
};
window.submitAbsent = async function() {
var checkboxes = document.querySelectorAll('.absent-checkbox:checked');
if (checkboxes.length === 0) {
showToast('请选择至少一名缺交学生', 'error');
return;
}
var studentIds = [];
checkboxes.forEach(function(cb) {
studentIds.push(parseInt(cb.getAttribute('data-id')));
});
var hwDeduct = window.DEDUCTION_HOMEWORK_NOT_SUBMIT || 2;
var res = await apiPost('/api/cadre/conduct/add', {
student_ids: studentIds,
points_change: -hwDeduct,
reason: '作业未提交',
related_type: 'homework'
});
if (res && res.success) {
showToast('已登记 ' + studentIds.length + ' 名学生缺交');
closeModal('absentModal');
} else {
showToast(res && res.message ? res.message : '提交失败', 'error');
}
};
window.closeModal = function(id) {
document.getElementById(id).style.display = 'none';
};
document.addEventListener('DOMContentLoaded', function() {
loadHomework(currentPage);
});
})();

View File

@@ -0,0 +1,425 @@
/**
* 多班级版班级管理系统 - 公共JS
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
function getToken() {
return localStorage.getItem(window.JWT_STORAGE_KEY || 'class_system_token');
}
function getUserInfo() {
const userStr = localStorage.getItem(window.USER_STORAGE_KEY || 'class_system_user');
if (!userStr) return null;
try {
return JSON.parse(userStr);
} catch (e) {
return null;
}
}
function setUserInfo(user) {
localStorage.setItem(window.USER_STORAGE_KEY || 'class_system_user', JSON.stringify(user));
}
function clearAuth() {
localStorage.removeItem(window.JWT_STORAGE_KEY || 'class_system_token');
localStorage.removeItem(window.USER_STORAGE_KEY || 'class_system_user');
}
async function apiRequest(url, options = {}) {
const token = getToken();
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const baseUrl = window.API_BASE_URL;
const fullUrl = `${baseUrl}${url}`;
try {
const response = await fetch(fullUrl, { ...options, headers });
const data = await response.json();
if (response.status === 401) {
clearAuth();
// 同步清除 PHP Session防止 index.php 302 重定向循环
try {
await fetch('/api/clear_session.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
} catch (e) {
console.warn('[Auth] 清除PHP Session失败:', e);
}
// 防循环机制:检查是否已在登录页
if (window.location.pathname === '/index.php' || window.location.pathname === '/') {
console.warn('[Auth] 已在登录页收到401停止重定向');
return null;
}
// 防循环机制5秒内重复401则停止重定向
const now = Date.now();
const lastRedirect = parseInt(sessionStorage.getItem('_last_401_redirect') || '0');
if (now - lastRedirect < 5000) {
console.warn('[Auth] 5秒内重复401停止重定向。请检查Token是否有效。');
return null;
}
sessionStorage.setItem('_last_401_redirect', now.toString());
window.location.href = '/index.php';
return null;
}
return data;
} catch (error) {
console.error('API请求错误:', error);
showToast('网络错误,请稍后重试', 'error');
return null;
}
}
function apiGet(url, params = {}) {
const queryString = new URLSearchParams(params).toString();
const fullUrl = queryString ? `${url}?${queryString}` : url;
return apiRequest(fullUrl, { method: 'GET' });
}
function apiPost(url, data = {}) {
return apiRequest(url, {
method: 'POST',
body: JSON.stringify(data)
});
}
function apiPut(url, data = {}) {
return apiRequest(url, {
method: 'PUT',
body: JSON.stringify(data)
});
}
function apiDelete(url) {
return apiRequest(url, { method: 'DELETE' });
}
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
function formatDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
function formatDateTime(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}
function getStatusBadge(status, type = 'attendance') {
const statusMap = {
attendance: {
'present': '出勤',
'absent': '缺勤',
'late': '迟到',
'leave': '请假'
}
};
const texts = statusMap[type] || statusMap.attendance;
const text = texts[status] || status;
let className = 'status-badge ';
switch (status) {
case 'present':
className += 'status-submitted';
break;
case 'absent':
className += 'status-not_submitted';
break;
case 'late':
className += 'status-late';
break;
case 'leave':
className += 'status-leave';
break;
default:
className += 'status-not_submitted';
}
return `<span class="${className}">${text}</span>`;
}
async function logout() {
// 清除 PHP Session
try {
await fetch('/api/clear_session.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
} catch (e) {
console.warn('清除Session失败', e);
}
// 清除后端 Token
try {
await apiPost('/api/auth/logout');
} catch (e) {
console.warn('后端登出失败', e);
}
// 清除 localStorage
clearAuth();
// 跳转回登录页
window.location.href = '/index.php';
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, '&#x27;')
.replace(/`/g, '&#x60;')
.replace(/\//g, '&#x2F;');
}
/**
* 智能分页渲染最多显示7个页码 + 跳转输入框)
* @param {string|HTMLElement} container - 分页容器ID或DOM元素
* @param {number} currentPage - 当前页码
* @param {number} totalPages - 总页数
* @param {function} onPageChange - 页码变化回调函数,参数为新的页码
*/
function renderSmartPagination(container, currentPage, totalPages, onPageChange) {
if (typeof container === 'string') {
container = document.getElementById(container);
}
if (!container || totalPages <= 1) {
if (container) container.innerHTML = '';
return;
}
const MAX_VISIBLE = 7;
let html = '';
// 上一页按钮
if (currentPage > 1) {
html += `<a href="#" class="page-nav" data-page="${currentPage - 1}">&laquo; 上一页</a>`;
}
if (totalPages <= MAX_VISIBLE) {
// 总页数不超过最大显示数,全部显示
for (let i = 1; i <= totalPages; i++) {
if (i === currentPage) {
html += `<span class="active">${i}</span>`;
} else {
html += `<a href="#" data-page="${i}">${i}</a>`;
}
}
} else {
// 需要省略号
// 始终显示第1页
if (currentPage === 1) {
html += `<span class="active">1</span>`;
} else {
html += `<a href="#" data-page="1">1</a>`;
}
// 计算中间页码范围
let start = Math.max(2, currentPage - 2);
let end = Math.min(totalPages - 1, currentPage + 2);
// 调整确保中间至少有3个页码加上首尾共5-7个
if (currentPage <= 3) {
end = Math.min(5, totalPages - 1);
}
if (currentPage >= totalPages - 2) {
start = Math.max(2, totalPages - 4);
}
// 前省略号
if (start > 2) {
html += `<span class="ellipsis">...</span>`;
}
// 中间页码
for (let i = start; i <= end; i++) {
if (i === currentPage) {
html += `<span class="active">${i}</span>`;
} else {
html += `<a href="#" data-page="${i}">${i}</a>`;
}
}
// 后省略号
if (end < totalPages - 1) {
html += `<span class="ellipsis">...</span>`;
}
// 始终显示最后一页
if (currentPage === totalPages) {
html += `<span class="active">${totalPages}</span>`;
} else {
html += `<a href="#" data-page="${totalPages}">${totalPages}</a>`;
}
}
// 下一页按钮
if (currentPage < totalPages) {
html += `<a href="#" class="page-nav" data-page="${currentPage + 1}">下一页 &raquo;</a>`;
}
// 页码跳转
html += `<span class="page-jump">跳至 <input type="number" min="1" max="${totalPages}" placeholder="页码"> / ${totalPages}页</span>`;
container.innerHTML = html;
// 绑定页码点击事件
container.querySelectorAll('a[data-page]').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const page = parseInt(this.dataset.page);
if (page && page !== currentPage && page >= 1 && page <= totalPages) {
onPageChange(page);
}
});
});
// 绑定跳转输入框事件
const jumpInput = container.querySelector('.page-jump input');
if (jumpInput) {
jumpInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
const page = parseInt(this.value);
if (page && page >= 1 && page <= totalPages) {
onPageChange(page);
} else {
showToast(`请输入1-${totalPages}之间的页码`, 'warning');
}
}
});
}
}
document.addEventListener('DOMContentLoaded', () => {
const user = getUserInfo();
const userNameSpan = document.getElementById('userName');
if (userNameSpan && user) {
userNameSpan.textContent = user.real_name || user.username;
}
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', logout);
}
});
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');
// 先关闭所有
closeAllDropdowns();
if (!isOpen) {
// 使用 fixed 定位,避免被 overflow 容器裁剪
var rect = el.getBoundingClientRect();
// 先显示以便测量高度
menu.classList.add('show');
menu.style.position = 'fixed';
menu.style.bottom = 'auto';
menu.style.right = 'auto';
menu.style.transform = 'none';
var menuHeight = menu.offsetHeight;
// 智能判断:按钮在上半部分则菜单显示在下方,否则显示在上方
if (rect.top < window.innerHeight / 2) {
menu.style.top = rect.bottom + 'px';
} else {
menu.style.top = (rect.top - menuHeight) + 'px';
}
menu.style.left = Math.min(rect.left, window.innerWidth - 130) + 'px';
el.classList.add('open');
}
}
function closeAllDropdowns() {
document.querySelectorAll('.action-dropdown-menu.show').forEach(function(m) {
m.classList.remove('show');
m.style.position = '';
m.style.left = '';
m.style.top = '';
m.style.bottom = '';
m.style.right = '';
m.style.transform = '';
var toggle = m.closest('.action-dropdown');
if (toggle) {
var btn = toggle.querySelector('.action-dropdown-toggle');
if (btn) btn.classList.remove('open');
}
});
}
document.addEventListener('click', function(e) {
if (!e.target.closest('.action-dropdown')) {
closeAllDropdowns();
}
});
// 全局textarea键盘事件Ctrl+Enter提交表单Enter换行默认行为
document.addEventListener('keydown', function(e) {
if (e.target.tagName !== 'TEXTAREA') return;
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
// Ctrl+Enter / Cmd+Enter 提交表单
e.preventDefault();
var form = e.target.closest('form');
if (form) {
var submitEvent = new Event('submit', { cancelable: true, bubbles: true });
form.dispatchEvent(submitEvent);
}
}
});
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;
}
};

View File

@@ -0,0 +1,253 @@
/**
* 多班级版班级管理系统 - 操行分管理页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}" class="link">${escapeHtml(student.name)}</a></td>
<td>${student.total_points}</td>
<td><button class="btn btn-sm btn-outline js-single-points" data-student-id="${student.student_id}" data-student-name="${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 allRecords = [];
let page = 1;
let totalPages = 1;
do {
const historyRes = await apiGet('/api/admin/conduct/history', { page: page, page_size: 500 });
if (!historyRes || !historyRes.success) {
showToast('获取历史记录失败', 'error');
return;
}
const records = historyRes.data.records || [];
allRecords.push(...records);
totalPages = historyRes.data.total_pages || 1;
page++;
} while (page <= totalPages);
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);
}
}
// 宿舍集体加分相关
let dormitoryStudentIds = [];
async function showDormitoryPointsModal() {
dormitoryStudentIds = [];
document.getElementById('dormitorySelect').innerHTML = '<option value="">-- 请选择宿舍 --</option>';
document.getElementById('dormitoryStudentsGroup').style.display = 'none';
document.getElementById('dormitoryStudentsList').innerHTML = '';
document.getElementById('dormitoryPointsChange').value = '';
document.getElementById('dormitoryPointsReason').value = '';
// 加载宿舍列表
const res = await apiGet('/api/admin/students/dormitories');
if (res && res.success && res.data.dormitories) {
const select = document.getElementById('dormitorySelect');
res.data.dormitories.forEach(d => {
const option = document.createElement('option');
option.value = d;
option.textContent = d;
select.appendChild(option);
});
}
document.getElementById('dormitoryPointsModal').style.display = 'flex';
}
async function onDormitorySelected() {
const dormitory = document.getElementById('dormitorySelect').value;
const studentsGroup = document.getElementById('dormitoryStudentsGroup');
const studentsList = document.getElementById('dormitoryStudentsList');
const studentsCount = document.getElementById('dormitoryStudentsCount');
dormitoryStudentIds = [];
studentsList.innerHTML = '';
if (!dormitory) {
studentsGroup.style.display = 'none';
return;
}
// 加载该宿舍的学生
const res = await apiGet('/api/admin/students', { dormitory_number: dormitory, page_size: 1000 });
if (res && res.success && res.data.students) {
const students = res.data.students;
if (students.length === 0) {
studentsList.innerHTML = '<p style="color: var(--text-secondary);">该宿舍暂无学生</p>';
studentsCount.textContent = '';
} else {
students.forEach(s => {
dormitoryStudentIds.push(s.student_id);
const div = document.createElement('div');
div.style.cssText = 'display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid var(--border-color);';
div.innerHTML = `<span>${escapeHtml(s.name)}</span><span style="color: var(--text-secondary);">${escapeHtml(s.student_no)}</span>`;
studentsList.appendChild(div);
});
studentsCount.textContent = `${students.length}`;
}
studentsGroup.style.display = 'block';
} else {
studentsList.innerHTML = '<p style="color: var(--text-secondary);">加载失败</p>';
studentsGroup.style.display = 'block';
}
}
async function submitDormitoryPoints() {
if (dormitoryStudentIds.length === 0) {
showToast('该宿舍没有学生', 'warning');
return;
}
const pointsChange = parseInt(document.getElementById('dormitoryPointsChange').value);
const reason = document.getElementById('dormitoryPointsReason').value;
if (isNaN(pointsChange) || pointsChange === 0) {
showToast('分值不能为0', 'error');
return;
}
if (Math.abs(pointsChange) > 100) {
showToast('分值绝对值不能超过100', 'error');
return;
}
if (!reason.trim()) {
showToast('请填写原因', 'error');
return;
}
const data = {
student_ids: dormitoryStudentIds,
points_change: pointsChange,
reason: reason,
related_type: 'manual'
};
const res = await apiPost('/api/admin/conduct/add', data);
if (res && res.success) {
showToast(`操作成功: ${res.data.success_count} 人成功`);
closeModal('dormitoryPointsModal');
loadStudents();
} else {
showToast(res?.message || '操作失败', 'error');
}
}
loadStudents();
document.getElementById('studentList').addEventListener('click', function(e) {
const btn = e.target.closest('.js-single-points');
if (btn) {
showSinglePointsModal(
parseInt(btn.dataset.studentId, 10),
btn.dataset.studentName
);
}
});
window.loadStudents = loadStudents;
window.showSinglePointsModal = showSinglePointsModal;
window.exportMoralityRecords = exportMoralityRecords;
window.showDormitoryPointsModal = showDormitoryPointsModal;
window.onDormitorySelected = onDormitorySelected;
window.submitDormitoryPoints = submitDormitoryPoints;
})();

View File

@@ -0,0 +1,120 @@
/**
* 多班级版班级管理系统 - 管理端首页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, semesterRes] = await Promise.all([
apiGet('/api/admin/students'),
apiGet('/api/semester/active')
]);
let statsHtml = '';
if (studentsRes && studentsRes.success) {
statsHtml += `
<div class="stat-card">
<div class="stat-label">学生总数</div>
<div class="stat-value">${studentsRes.data.total || 0}</div>
</div>
`;
}
// 显示学期信息和当前周数
if (semesterRes && semesterRes.success && semesterRes.data) {
const sem = semesterRes.data;
const weekNum = sem.current_week;
let semesterInfo = escapeHtml(sem.semester_name);
if (weekNum && weekNum > 0) {
semesterInfo += ` · 第${weekNum}`;
}
statsHtml += `
<div class="stat-card">
<div class="stat-label">当前学期</div>
<div class="stat-value" style="font-size:20px;">${semesterInfo}</div>
</div>
`;
}
document.getElementById('dashboardStats').innerHTML = statsHtml;
let quickActions = '';
if (role === '班主任' || role === '班长' || role === '学习委员' || role === '劳动委员' || role === '志愿委员') {
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>';
}
if (role === '班主任' || role === '学习委员') {
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/homework.php\'">作业扣分</button>';
}
if (role === '班主任' || role === '考勤委员') {
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/attendance.php\'">考勤管理</button>';
}
if (role === '班主任') {
quickActions += '<button class="btn btn-outline" 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;
})();

View File

@@ -0,0 +1,345 @@
/**
* 多班级版班级管理系统 - 历史记录页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;
function getTypeLabel(relatedType) {
if (!relatedType) return '操行';
switch (relatedType) {
case 'conduct': return '操行';
case 'homework': return '作业';
case 'attendance': return '考勤';
default: return relatedType;
}
}
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 loadSubjectsForFilter() {
let subjectSelect = document.getElementById('historySubjectFilter');
if (!subjectSelect) return;
let res = await apiGet('/api/subject/list', { is_active: true });
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;
}
}
// 筛选学生时自动取消合并记录
function onStudentFilterChange() {
let studentId = document.getElementById('historyStudentId').value;
if (studentId) {
let grouped = document.getElementById('historyGrouped');
if (grouped) grouped.checked = false;
}
}
// 科目筛选变化时,取消扣分类型筛选(互斥)
function onSubjectFilterChange() {
let subjectVal = document.getElementById('historySubjectFilter').value;
if (subjectVal) {
document.getElementById('historyReasonFilter').value = '';
}
}
// 折叠/展开筛选面板
function toggleFilterPanel() {
let panel = document.getElementById('advancedFilters');
let btn = document.getElementById('filterToggleBtn');
if (!panel || !btn) return;
if (panel.style.display === 'none') {
panel.style.display = 'block';
btn.textContent = '收起筛选 ▲';
} else {
panel.style.display = 'none';
btn.textContent = '展开筛选 ▼';
}
}
async function loadHistory(page) {
page = page || 1;
currentHistoryPage = page;
let startDate = document.getElementById('historyStartDate').value;
let endDate = document.getElementById('historyEndDate').value;
let studentId = document.getElementById('historyStudentId').value;
let reasonFilter = document.getElementById('historyReasonFilter').value;
let subjectFilter = document.getElementById('historySubjectFilter').value;
let reasonSearch = document.getElementById('historyReasonSearch').value.trim();
let isGrouped = document.getElementById('historyGrouped').checked;
let statusFilter = document.getElementById('historyStatusFilter') ? document.getElementById('historyStatusFilter').value : '';
// 筛选学生时强制取消合并
if (studentId) isGrouped = false;
let params = {
page: page, page_size: 20,
start_date: startDate,
end_date: endDate
};
if (studentId) params.student_id = studentId;
// 科目筛选优先于扣分类型筛选
if (subjectFilter) {
params.reason_prefix = '[' + subjectFilter + ']';
} else if (reasonFilter) {
params.reason_prefix = reasonFilter;
}
if (reasonSearch) params.reason_search = reasonSearch;
if (isGrouped) params.grouped = true;
if (statusFilter !== '') params.is_revoked = parseInt(statusFilter);
let res = await apiGet('/api/admin/conduct/history', params);
if (res && res.success) {
let nowrapStyle = ' style="white-space:nowrap;min-width:80px;"';
let headHtml = '';
if (isGrouped) {
headHtml = '<th>类型</th><th>分值</th><th>原因</th><th>学生名单</th><th' + nowrapStyle + '>操作人</th><th>时间</th>';
if (role === '班主任' || role === '班长') {
headHtml += '<th>操作</th>';
}
} else {
headHtml = '<th>类型</th><th>分值</th><th>原因</th><th>学生</th><th' + nowrapStyle + '>操作人</th><th>时间</th>';
if (role === '班主任' || role === '班长' || role === '考勤委员') {
headHtml += '<th>操作</th>';
}
}
document.getElementById('historyTableHead').innerHTML = headHtml;
let html = '';
if (isGrouped) {
res.data.records.forEach(function(record) {
let pointsClass = record.points_change > 0 ? 'plus' : 'minus';
let names = record.student_names || '';
let allRevoked = record.all_revoked;
let revokedStyle = allRevoked ? ' style="opacity:0.5;text-decoration:line-through;"' : '';
html += '<tr' + revokedStyle + '>' +
'<td>' + escapeHtml(getTypeLabel(record.related_type)) + '</td>' +
'<td class="' + pointsClass + '">' + (record.points_change > 0 ? '+' : '') + record.points_change + '&times;' + record.student_count + '</td>' +
'<td class="history-reason">' + escapeHtml(record.reason) + '</td>' +
'<td class="history-students">' + escapeHtml(names) + '</td>' +
'<td>' + escapeHtml(record.recorder_name || '') + '</td>' +
'<td class="history-time">' + formatDateTime(record.created_at) + '</td>';
if (role === '班主任' || role === '班长') {
if (allRevoked) {
html += '<td><span class="text-muted">已撤销</span></td>';
} else {
html += '<td><button class="btn btn-sm btn-outline" onclick="batchRevokeGrouped(\'' + escapeHtml(record.reason).replace(/'/g, "\\'") + '\',' + record.points_change + ',\'' + escapeHtml(record.recorder_name || '').replace(/'/g, "\\'") + '\',\'' + formatDateTime(record.created_at) + '\')">批量撤销</button></td>';
}
}
html += '</tr>';
});
if (res.data.records.length === 0) {
let colSpan = (role === '班主任' || role === '班长') ? 7 : 6;
html = '<tr><td colspan="' + colSpan + '" style="text-align:center;">暂无记录</td></tr>';
}
} else {
res.data.records.forEach(function(record) {
let pointsClass = record.points_change > 0 ? 'plus' : 'minus';
let revokedStyle = record.is_revoked == 1 ? ' style="opacity:0.5;text-decoration:line-through;"' : '';
html += '<tr' + revokedStyle + '>' +
'<td>' + escapeHtml(getTypeLabel(record.related_type)) + '</td>' +
'<td class="' + pointsClass + '">' + (record.points_change > 0 ? '+' : '') + record.points_change + '</td>' +
'<td class="history-reason">' + escapeHtml(record.reason) + '</td>' +
'<td>' + escapeHtml(record.student_name) + '</td>' +
'<td>' + escapeHtml(record.recorder_name) + '</td>' +
'<td class="history-time">' + formatDateTime(record.created_at) + '</td>';
if (role === '班主任') {
if (record.is_revoked == 1) {
let 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-outline" onclick="restoreRecord(' + record.record_id + ')">反撤销</button></td>';
} else {
html += '<td><button class="btn btn-sm btn-outline" onclick="revokeRecord(' + record.record_id + ')">撤销</button></td>';
}
} else if (role === '班长') {
if (record.is_revoked == 1) {
let 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-outline" 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-outline" onclick="revokeRecord(' + record.record_id + ')">撤销</button></td>';
} else {
html += '<td><span class="text-muted">-</span></td>';
}
}
html += '</tr>';
});
if (res.data.records.length === 0) {
let colSpan = (role === '班主任' || role === '班长' || role === '考勤委员') ? 7 : 6;
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() {
let startDate = document.getElementById('historyStartDate').value;
let endDate = document.getElementById('historyEndDate').value;
let studentId = document.getElementById('historyStudentId').value;
showToast('正在导出历史记录...', 'info');
try {
let reasonFilter = document.getElementById('historyReasonFilter').value;
let subjectFilter = document.getElementById('historySubjectFilter').value;
let reasonSearch = document.getElementById('historyReasonSearch').value.trim();
let 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 (subjectFilter) {
params.reason_prefix = '[' + subjectFilter + ']';
} else if (reasonFilter) {
params.reason_prefix = reasonFilter;
}
if (reasonSearch) params.reason_search = reasonSearch;
let res = await apiGet('/api/admin/conduct/history', params);
if (res && res.success && res.data.records) {
let records = res.data.records;
if (records.length === 0) {
showToast('没有找到记录', 'warning');
return;
}
function csvField(val) {
let s = String(val == null ? '' : val);
if (s.indexOf(',') >= 0 || s.indexOf('"') >= 0 || s.indexOf('\n') >= 0) {
return '"' + s.replace(/"/g, '""') + '"';
}
return s;
}
let csv = '\uFEFF';
csv += '时间,学号,姓名,分数变动,原因,操作人\n';
records.forEach(function(r) {
if (r.is_revoked == 1) return;
csv += csvField(r.created_at) + ',' + csvField(r.student_no) + ',' + csvField(r.student_name) + ',' + csvField((r.points_change > 0 ? '+' : '') + r.points_change) + ',' + csvField(r.reason) + ',' + csvField(r.recorder_name) + '\n';
});
let blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
let url = URL.createObjectURL(blob);
let 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 && res.message || '未知错误'), 'error');
}
} catch (err) {
showToast('导出失败:' + err.message, 'error');
}
}
// 批量撤销合并记录(按条件查找并撤销)
async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt) {
if (!confirm('确定要撤销所有"' + reason + '"(' + (pointsChange > 0 ? '+' : '') + pointsChange + '分)的记录吗?')) return;
showToast('正在批量撤销...', 'info');
try {
let params = {
page: 1, page_size: 1000,
start_date: document.getElementById('historyStartDate').value,
end_date: document.getElementById('historyEndDate').value,
reason_prefix: reason,
grouped: false
};
let res = await apiGet('/api/admin/conduct/history', params);
if (!res || !res.success || !res.data.records) {
showToast('查询记录失败', 'error');
return;
}
let matchedIds = [];
res.data.records.forEach(function(r) {
if (r.reason === reason && r.points_change === pointsChange && r.is_revoked == 0) {
matchedIds.push(r.record_id);
}
});
if (matchedIds.length === 0) {
showToast('没有找到可撤销的记录', 'warning');
return;
}
let revokeRes = await apiPost('/api/admin/conduct/batch-revoke', { record_ids: matchedIds });
if (revokeRes && revokeRes.success) {
showToast('批量撤销完成: ' + (revokeRes.data ? revokeRes.data.success_count : 0) + '条成功');
loadHistory(currentHistoryPage);
} else {
showToast(revokeRes && revokeRes.message || '批量撤销失败', 'error');
}
} catch (err) {
showToast('批量撤销失败: ' + err.message, 'error');
}
}
// 初始化:并行加载学生和科目列表,然后加载历史记录
Promise.all([loadStudentsForSelect(), loadSubjectsForFilter()]).then(function() {
let urlParams = new URLSearchParams(window.location.search);
let preStudentId = urlParams.get('student_id');
if (preStudentId) {
document.getElementById('historyStudentId').value = preStudentId;
onStudentFilterChange();
}
loadHistory();
});
window.loadHistory = loadHistory;
window.loadStudentsForSelect = loadStudentsForSelect;
window.exportHistoryRecords = exportHistoryRecords;
window.batchRevokeGrouped = batchRevokeGrouped;
window.onStudentFilterChange = onStudentFilterChange;
window.onSubjectFilterChange = onSubjectFilterChange;
window.toggleFilterPanel = toggleFilterPanel;
})();

View File

@@ -0,0 +1,266 @@
/**
* 多班级版班级管理系统 - 作业扣分页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() {
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-outline" 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 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 === '学习委员' || 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({ related_type: 'homework' });
}
// ========== 科目管理功能 ==========
function toggleSubjectPanel() {
const content = document.getElementById('subjectPanelContent');
const toggle = document.getElementById('subjectPanelToggle');
if (!content || !toggle) return;
const isExpanded = content.classList.contains('expanded');
if (isExpanded) {
content.classList.remove('expanded');
toggle.classList.remove('expanded');
toggle.textContent = '▶ 展开';
} else {
content.classList.add('expanded');
toggle.classList.add('expanded');
toggle.textContent = '▼ 收起';
loadSubjectList();
}
}
async function loadSubjectList() {
const res = await apiGet('/api/subject/list', { is_active: true });
if (res && res.success && res.data) {
let html = '';
const subjects = res.data.subjects || [];
subjects.forEach(sub => {
const safeName = escapeHtml(sub.subject_name || '');
const safeCode = escapeHtml(sub.subject_code || '');
const sortOrder = sub.sort_order || 0;
html += `
<div class="subject-item">
<span class="subject-name">${safeName}</span>
<span class="subject-code">${safeCode}</span>
<span class="subject-status ${sub.is_active ? 'subject-status-active' : 'subject-status-inactive'}">
${sub.is_active ? '启用' : '禁用'}
</span>
<button class="btn btn-sm btn-outline" onclick="showEditSubjectModal(${sub.subject_id}, '${safeName.replace(/'/g, "\\'")}', '${safeCode.replace(/'/g, "\\'")}', ${sortOrder})">编辑</button>
<button class="btn btn-sm btn-ghost" onclick="toggleSubjectStatus(${sub.subject_id}, ${sub.is_active ? 'false' : 'true'})">
${sub.is_active ? '禁用' : '启用'}
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteSubject(${sub.subject_id})">删除</button>
</div>
`;
});
if (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/toggle/${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');
}
}
// 绑定科目管理折叠面板
var subjectHeader = document.getElementById('subjectPanelHeader');
if (subjectHeader) {
subjectHeader.addEventListener('click', toggleSubjectPanel);
}
loadStudents();
loadSubjectsForHomework();
window.loadStudents = loadStudents;
window.showSinglePointsModal = showSinglePointsModal;
window.handleSubmitPoints = handleSubmitPoints;
window.toggleSubjectPanel = toggleSubjectPanel;
window.showAddSubjectModal = showAddSubjectModal;
window.submitAddSubject = submitAddSubject;
window.toggleSubjectStatus = toggleSubjectStatus;
window.deleteSubject = deleteSubject;
window.showEditSubjectModal = showEditSubjectModal;
window.submitEditSubject = submitEditSubject;
})();

View File

@@ -0,0 +1,53 @@
/**
* 多班级版班级管理系统 - 管理员管理函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © 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;
})();

View File

@@ -0,0 +1,24 @@
/**
* 多班级版班级管理系统 - 模态框工具函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
(function() {
'use strict';
// 关闭模态框
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.style.display = 'none';
}
}
window.closeModal = closeModal;
})();

View File

@@ -0,0 +1,102 @@
/**
* 多班级版班级管理系统 - 加减分管理函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © 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(options = {}) {
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 data = {
student_ids: selectedStudentIds,
points_change: pointsChange,
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) {
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;
})();

View File

@@ -0,0 +1,234 @@
/**
* 多班级版班级管理系统 - 学生管理函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © 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_account: 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_account: 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><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_account || '')}</td>
<td>${escapeHtml(s.dormitory_number || '-')}</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;
})();

View File

@@ -0,0 +1,47 @@
/**
* 多班级版班级管理系统 - 科目管理函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © 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;
})();

View File

@@ -0,0 +1,35 @@
/**
* 多班级版班级管理系统 - 通用工具函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
(function() {
'use strict';
// HTML转义
function escapeHtml(str) {
if (!str) return '';
var el = document.createElement('span');
el.appendChild(document.createTextNode(str));
return el.innerHTML;
}
// 全选功能
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;
})();

View File

@@ -0,0 +1,13 @@
/**
* 多班级版班级管理系统 - 家长端JS
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
// 家长端专用功能
console.log('家长端已加载');

View File

@@ -0,0 +1,59 @@
/**
* 多班级版班级管理系统 - 排行榜JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
*
* 版权所有 © Sea Network Technology Studio
*/
(function() {
'use strict';
let currentType = 'conduct';
async function loadRankings(type) {
const res = await apiGet('/api/admin/rankings', { type: type, limit: 50 });
if (res && res.success && res.data) {
const rankings = res.data.ranking || [];
let html = '';
if (rankings.length === 0) {
html = '<tr><td colspan="4" style="text-align:center;">暂无排行数据</td></tr>';
} else {
rankings.forEach(function(item, index) {
let rankClass = '';
if (index === 0) rankClass = 'rank-gold';
else if (index === 1) rankClass = 'rank-silver';
else if (index === 2) rankClass = 'rank-bronze';
let pointsText = Number(item.points !== undefined ? item.points : (item.total_points || 0));
if (pointsText > 0) {
pointsText = '+' + pointsText;
}
html += '<tr>' +
'<td><span class="rank-badge ' + rankClass + '">' + (index + 1) + '</span></td>' +
'<td>' + escapeHtml(item.student_no || '-') + '</td>' +
'<td>' + escapeHtml(item.name || '-') + '</td>' +
'<td><span class="record-points ' + (pointsText > 0 ? 'plus' : (pointsText < 0 ? 'minus' : '')) + '">' + pointsText + '</span></td>' +
'</tr>';
});
}
document.getElementById('rankingList').innerHTML = html;
} else {
document.getElementById('rankingList').innerHTML = '<tr><td colspan="4" style="text-align:center;">加载失败</td></tr>';
}
}
window.switchTab = function(type, btn) {
currentType = type;
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
loadRankings(type);
};
document.addEventListener('DOMContentLoaded', function() {
loadRankings(currentType);
});
})();

View File

@@ -0,0 +1,383 @@
/**
* 多班级版班级管理系统 - 学期管理页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 || [];
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 += `<div class="action-dropdown">
<button class="btn btn-sm action-dropdown-toggle" onclick="toggleActionDropdown(this)">操作 ▼</button>
<div class="action-dropdown-menu">
<a onclick="showEditSemesterModal(${sem.semester_id}, '${escapeHtml(sem.semester_name)}', '${startDate}', '${endDate}')">编辑</a>
${!sem.is_active ? `<a onclick="activateSemester(${sem.semester_id})">激活</a>` : ''}
<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) {
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}条考勤`;
}
const weekText = sem.current_week ? `${sem.current_week}` : '-';
html += `<tr>
<td>${escapeHtml(sem.semester_name)}</td>
<td>${formatDate(sem.start_date)}</td>
<td>${formatDate(sem.end_date)}</td>
<td>${weekText}</td>
<td><span class="${statusClass}">${statusText}</span></td>
<td>${recordText}</td>
<td>${formatDateTime(sem.created_at)}</td>
<td>${actions}</td>
</tr>`;
});
if (semesters.length === 0) {
html = '<tr><td colspan="8" 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.items || [];
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);
});
}
// ========== 周期重置功能 ==========
let pendingPeriodType = null;
let periodArchivesType = null;
let periodArchivesPage = 1;
let periodArchivesTotalPages = 1;
function confirmPeriodReset(periodType) {
pendingPeriodType = periodType;
const label = periodType === 'weekly' ? '本周' : '本月';
document.getElementById('periodResetText').innerHTML =
`确定要执行 <strong>${label}重置</strong> 吗?<br>将保存当前所有学生的操行分快照,然后将所有学生操行分重置为初始值。`;
document.getElementById('periodResetModal').style.display = 'flex';
}
async function executePeriodReset() {
if (!pendingPeriodType) return;
const res = await apiPost('/api/semester/period-reset', { period: pendingPeriodType });
if (res && res.success) {
showToast(res.message || '重置成功');
closeModal('periodResetModal');
pendingPeriodType = null;
} else {
showToast(res?.message || '重置失败', 'error');
}
}
async function showPeriodArchives(type, page) {
var periodType = type || periodArchivesType;
page = page || 1;
periodArchivesType = periodType;
periodArchivesPage = page;
const label = periodType === 'weekly' ? '周' : '月';
document.getElementById('periodArchivesTitle').textContent = label + '归档数据';
const res = await apiGet('/api/semester/period-archives', {
period: periodType,
page: page,
page_size: 50
});
if (res && res.success) {
const data = res.data || {};
const archives = data.items || [];
let html = '';
archives.forEach(function(a) {
const resetByLabel = a.reset_by === 'auto' ? '自动' : '手动';
html += '<tr>' +
'<td>' + escapeHtml(a.period_label) + '</td>' +
'<td>' + (a.rank_position || '-') + '</td>' +
'<td>' + escapeHtml(a.student_no) + '</td>' +
'<td>' + escapeHtml(a.student_name) + '</td>' +
'<td>' + a.final_points + '</td>' +
'<td>' + resetByLabel + '</td>' +
'<td>' + formatDateTime(a.archived_at) + '</td>' +
'</tr>';
});
if (archives.length === 0) {
html = '<tr><td colspan="7" style="text-align:center;">暂无归档数据</td></tr>';
}
document.getElementById('periodArchivesList').innerHTML = html;
periodArchivesTotalPages = data.total_pages || 1;
renderSmartPagination('periodArchivePagination', periodArchivesPage, periodArchivesTotalPages, function(p) {
showPeriodArchives(periodArchivesType, p);
});
document.getElementById('periodArchivesModal').style.display = 'flex';
} else {
showToast(res?.message || '获取归档数据失败', 'error');
}
}
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;
window.confirmPeriodReset = confirmPeriodReset;
window.executePeriodReset = executePeriodReset;
window.showPeriodArchives = showPeriodArchives;
})();

View 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(record => {
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
const pointsColor = record.points_change > 0 ? '#38a169' : '#e53e3e';
html += `<tr>
<td>${formatDateTime(record.created_at)}</td>
<td style="color: ${pointsColor}; font-weight: bold;">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
<td>${escapeHtml(record.reason)}</td>
<td>${escapeHtml(record.recorder_name || '-')}</td>
</tr>`;
});
if (res.data.homework.length === 0) {
html = '<tr><td colspan="4" style="text-align:center; padding: 40px; color: #999;">📝 暂无作业扣分记录</td></tr>';
}
document.getElementById('homeworkList').innerHTML = html;
}
}
loadHomework();
})();

View File

@@ -0,0 +1,13 @@
/**
* 多班级版班级管理系统 - 学生端JS
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
// 学生端专用功能
console.log('学生端已加载');

View File

@@ -0,0 +1,100 @@
/**
* 多班级版班级管理系统 - 学生管理页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}" class="link">${escapeHtml(student.name)}</a></td>
<td>${escapeHtml(student.dormitory_number || '-')}</td>
<td>${student.total_points}</td>
${userRole === '班主任' ? `<td>${student.parent_account ? student.parent_account.slice(0,3) + '******' + student.parent_account.slice(-2) : '-'}</td>` : ''}
<td>
<div class="action-dropdown">
<button class="btn btn-sm btn-outline" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button>
${userRole === '班主任' ? `<button class="btn btn-sm action-dropdown-toggle" onclick="toggleActionDropdown(this)">更多 ▼</button>
<div class="action-dropdown-menu">
<a onclick="showEditStudentModal(${student.student_id}, '${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}', '${escapeHtml(student.parent_account || '')}', '${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>
</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;
})();

View File

@@ -0,0 +1,23 @@
{
"students": [
{
"student_no": "2025001",
"name": "张三",
"parent_account": "13800138001",
"dormitory_number": "A301",
"password": "123456"
},
{
"student_no": "2025002",
"name": "李四",
"parent_account": "13800138002",
"dormitory_number": "A302"
},
{
"student_no": "2025003",
"name": "王五",
"parent_account": "",
"dormitory_number": "B101"
}
]
}

97
frontend/config.php Normal file
View File

@@ -0,0 +1,97 @@
<?php
/**
* 多班级版班级管理系统 - 前端配置
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
// 读取.env文件
$envFile = __DIR__ . '/.env';
$config = [];
if (!file_exists($envFile)) {
die('错误: 配置文件 .env 不存在,请复制 .env.example 并修改配置');
}
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines === false) {
die('错误: 无法读取配置文件 .env');
}
foreach ($lines as $line) {
$line = trim($line);
if (strpos($line, '#') === 0 || empty($line)) {
continue;
}
if (strpos($line, '=') !== false) {
$parts = explode('=', $line, 2);
$key = trim($parts[0]);
$value = trim($parts[1]);
$value = trim($value, '"\'');
$config[$key] = $value;
}
}
// 检查必要配置是否存在
$requiredKeys = ['API_BASE_URL', 'API_TIMEOUT', 'JWT_STORAGE_KEY', 'USER_STORAGE_KEY', 'SITE_NAME', 'SESSION_TIMEOUT', 'ICP_ENABLED', 'ICP_NUMBER'];
$missingKeys = [];
foreach ($requiredKeys as $key) {
if (!isset($config[$key]) || $config[$key] === '') {
$missingKeys[] = $key;
}
}
if (!empty($missingKeys)) {
die('错误: 配置文件 .env 缺少必要配置项: ' . implode(', ', $missingKeys));
}
// 定义常量
define('API_BASE_URL', $config['API_BASE_URL']);
define('API_TIMEOUT', (int)$config['API_TIMEOUT']);
define('JWT_STORAGE_KEY', $config['JWT_STORAGE_KEY']);
define('USER_STORAGE_KEY', $config['USER_STORAGE_KEY']);
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'] ?? '');
// 注意:此处不含 /api 前缀,前端 JS 会自动拼接 /api + path + /login
define('SUPER_ADMIN_LOGIN_PATH', ($config['SUPER_ADMIN_LOGIN_PATH'] ?? '/super-admin'));
// 会话配置
ini_set('session.cookie_httponly', 1);
ini_set('session.use_only_cookies', 1);
// 开发环境允许 HTTP生产环境强制 HTTPS
$appEnv = $config['APP_ENV'] ?? 'production';
ini_set('session.cookie_secure', $appEnv === 'production' ? 1 : 0);
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.gc_maxlifetime', SESSION_TIMEOUT);
session_name('CLASS_SESSION');
session_start();
// 应用层会话超时检查
if (isset($_SESSION['login_time']) && (time() - $_SESSION['login_time'] > SESSION_TIMEOUT)) {
session_unset();
session_destroy();
if (strpos($_SERVER['REQUEST_URI'] ?? '', '/api/') === false) {
header('Location: /index.php');
exit();
}
http_response_code(401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'message' => '会话已过期,请重新登录']);
exit();
}
// 时区设置
date_default_timezone_set('Asia/Shanghai');
// 生产环境关闭错误显示(保留错误日志记录,仅隐藏页面输出)
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
ini_set('display_errors', 0);
ini_set('log_errors', 1);

View File

@@ -0,0 +1,6 @@
</div><!-- /.container —— 依赖页面包含 <div class="container"> -->
<footer class="footer">
<p>&copy; <?php echo date('Y'); ?> <?php echo htmlspecialchars(SITE_NAME, ENT_QUOTES, 'UTF-8'); ?> &middot; <span class="footer-dev">Powered by Canglan / Sea Network Technology Studio</span></p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,92 @@
<?php
/**
* 多班级版班级管理系统 - 公共头部
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
if (!isset($_SESSION)) {
session_start();
}
$current_page = basename($_SERVER['PHP_SELF'], '.php');
$user_type = $_SESSION['user_type'] ?? '';
$role = $_SESSION['role'] ?? '';
$class_id = $_SESSION['class_id'] ?? null;
$class_name = $_SESSION['class_name'] ?? '';
$page_title = $page_title ?? '首页';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title><?php echo htmlspecialchars(SITE_NAME, ENT_QUOTES, 'UTF-8'); ?> - <?php echo htmlspecialchars($page_title); ?></title>
<link rel="stylesheet" href="/assets/css/style.css">
<?php if ($user_type === 'admin'): ?>
<link rel="stylesheet" href="/assets/css/admin.css">
<?php endif; ?>
</head>
<body>
<div class="header">
<h1><?php echo htmlspecialchars(SITE_NAME, ENT_QUOTES, 'UTF-8'); ?></h1>
<div class="header-info">
<?php if ($class_name): ?>
<span class="class-name" id="className"><?php echo htmlspecialchars($class_name); ?></span>
<?php endif; ?>
<span class="user-name" id="userName"><?php echo htmlspecialchars($_SESSION['real_name'] ?? ''); ?></span>
<?php if ($role): ?>
<span class="user-role">(<?php echo htmlspecialchars($role); ?>)</span>
<?php endif; ?>
<button class="btn-logout" id="logoutBtn">退出登录</button>
</div>
</div>
<script>
window.API_BASE_URL = <?php echo json_encode(API_BASE_URL); ?>;
window.JWT_STORAGE_KEY = <?php echo json_encode(JWT_STORAGE_KEY); ?>;
window.USER_STORAGE_KEY = <?php echo json_encode(USER_STORAGE_KEY); ?>;
window.CLASS_ID = <?php echo $class_id ? $class_id : 'null'; ?>;
window.CLASS_NAME = <?php echo json_encode($class_name); ?>;
</script>
<script>
// 从后端API异步加载扣分规则配置优先加载班级级配置
(function() {
// 先设置默认值,避免异步加载期间其他脚本读取到 undefined
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;
var token = localStorage.getItem(window.JWT_STORAGE_KEY) || '';
var apiUrl = window.API_BASE_URL + '/api/config/deduction-rules';
if (window.CLASS_ID) {
apiUrl += '?class_id=' + window.CLASS_ID;
}
fetch(apiUrl, {
headers: token ? { 'Authorization': 'Bearer ' + token } : {}
}).then(function(resp) {
return resp.json();
}).then(function(data) {
if (data.success && data.data) {
window.DEDUCTION_HOMEWORK_NOT_SUBMIT = data.data.DEDUCTION_HOMEWORK_NOT_SUBMIT;
window.DEDUCTION_HOMEWORK_LATE = data.data.DEDUCTION_HOMEWORK_LATE;
window.DEDUCTION_ATTENDANCE_ABSENT = data.data.DEDUCTION_ATTENDANCE_ABSENT;
window.DEDUCTION_ATTENDANCE_LATE = data.data.DEDUCTION_ATTENDANCE_LATE;
window.DEDUCTION_ATTENDANCE_LEAVE = data.data.DEDUCTION_ATTENDANCE_LEAVE;
window.STUDENT_INITIAL_POINTS = data.data.STUDENT_INITIAL_POINTS;
}
}).catch(function() {
// API加载失败时保留默认值
});
})();
</script>
<script src="/assets/js/common.js"></script>
<script src="/assets/js/modules/utils.js"></script>
<div class="container">

31
frontend/includes/nav.php Normal file
View File

@@ -0,0 +1,31 @@
<div class="nav">
<a href="/admin/dashboard.php" class="nav-item<?php echo $current_page === 'dashboard' ? ' active' : ''; ?>">首页</a>
<?php if ($user_type === 'super_admin'): ?>
<a href="/admin/classes.php" class="nav-item<?php echo $current_page === 'classes' ? ' active' : ''; ?>">班级管理</a>
<?php endif; ?>
<?php if (in_array($role, ['班主任', '系统管理员'])): ?>
<a href="/admin/students.php" class="nav-item<?php echo $current_page === 'students' ? ' active' : ''; ?>">学生管理</a>
<?php endif; ?>
<?php if (in_array($role, ['班主任', '班长', '学习委员', '考勤委员', '劳动委员', '志愿委员', '科任老师', '系统管理员'])): ?>
<a href="/admin/conduct.php" class="nav-item<?php echo $current_page === 'conduct' ? ' active' : ''; ?>">操行分管理</a>
<?php endif; ?>
<?php if (in_array($role, ['班主任', '学习委员', '科任老师', '系统管理员'])): ?>
<a href="/admin/homework.php" class="nav-item<?php echo $current_page === 'homework' ? ' active' : ''; ?>">作业扣分</a>
<?php endif; ?>
<?php if ($role === '课代表'): ?>
<a href="/admin/cadre_homework.php" class="nav-item<?php echo $current_page === 'cadre_homework' ? ' active' : ''; ?>">作业管理</a>
<?php endif; ?>
<?php if (in_array($role, ['班主任', '考勤委员', '系统管理员'])): ?>
<a href="/admin/attendance.php" class="nav-item<?php echo $current_page === 'attendance' ? ' active' : ''; ?>">考勤管理</a>
<?php endif; ?>
<?php if (in_array($role, ['班主任', '系统管理员'])): ?>
<a href="/admin/admins.php" class="nav-item<?php echo $current_page === 'admins' ? ' active' : ''; ?>">管理员管理</a>
<a href="/admin/semesters.php" class="nav-item<?php echo $current_page === 'semesters' ? ' active' : ''; ?>">学期管理</a>
<a href="/admin/class_settings.php" class="nav-item<?php echo $current_page === 'class_settings' ? ' active' : ''; ?>">班级设置</a>
<?php endif; ?>
<?php if (in_array($role, ['班主任', '系统管理员'])): ?>
<a href="/admin/rankings.php" class="nav-item<?php echo $current_page === 'rankings' ? ' active' : ''; ?>">排行榜</a>
<?php endif; ?>
<a href="/admin/history.php" class="nav-item<?php echo $current_page === 'history' ? ' active' : ''; ?>">历史记录</a>
<a href="/admin/password.php" class="nav-item<?php echo $current_page === 'password' ? ' active' : ''; ?>">修改密码</a>
</div>

143
frontend/index.php Normal file
View File

@@ -0,0 +1,143 @@
<?php
/**
* 多班级版班级管理系统 - 登录入口
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/config.php';
if (isset($_SESSION['user_id']) && isset($_SESSION['user_type'])) {
$redirect = [
'student' => '/student/dashboard.php',
'parent' => '/parent/dashboard.php',
'admin' => '/admin/dashboard.php',
'super_admin' => '/admin/dashboard.php'
];
header("Location: " . ($redirect[$_SESSION['user_type']] ?? '/index.php'));
exit();
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title><?php echo htmlspecialchars(SITE_NAME, ENT_QUOTES, 'UTF-8'); ?> - 登录</title>
<link rel="stylesheet" href="/assets/css/style.css">
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1><?php echo htmlspecialchars(SITE_NAME, ENT_QUOTES, 'UTF-8'); ?></h1>
<p>学生 / 家长 / 管理员 统一登录</p>
</div>
<form id="loginForm" class="login-form">
<div class="form-group">
<label>用户名</label>
<input type="text" id="username" name="username" required autocomplete="off" placeholder="学号/手机号/管理员账号">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="password" name="password" required placeholder="请输入密码">
</div>
<button type="submit" class="btn-login">登 录</button>
<div id="errorMsg" class="error-msg" style="display: none;"></div>
</form>
<div class="login-footer">
<p>&copy; <?php echo date('Y'); ?> Sea Network Technology Studio</p>
<?php if (defined('ICP_ENABLED') && ICP_ENABLED && defined('ICP_NUMBER') && ICP_NUMBER): ?>
<p><a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer"><?php echo htmlspecialchars(ICP_NUMBER); ?></a></p>
<?php endif; ?>
</div>
</div>
<script>
window.API_BASE_URL = <?php echo json_encode(API_BASE_URL); ?>;
window.JWT_STORAGE_KEY = <?php echo json_encode(JWT_STORAGE_KEY); ?>;
window.USER_STORAGE_KEY = <?php echo json_encode(USER_STORAGE_KEY); ?>;
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
const errorMsg = document.getElementById('errorMsg');
if (!username || !password) {
showError('请填写用户名和密码');
return;
}
try {
const response = await fetch(`${API_BASE_URL}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success && data.data) {
const userData = data.data;
// 保存 Token 和用户信息到 localStorage
localStorage.setItem(JWT_STORAGE_KEY, userData.token);
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(userData));
// 同步设置 PHP Session保持 Session + Token 双轨制认证)
try {
const sessionResponse = await fetch('/api/save_session.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + userData.token
},
body: JSON.stringify({
user_id: userData.user_id,
user_type: userData.user_type,
username: userData.username,
real_name: userData.real_name,
role: userData.role || '',
student_id: userData.student_id || null,
class_id: userData.class_id || null,
class_name: userData.class_name || ''
})
});
if (!sessionResponse.ok) {
console.warn('Session 同步失败,但继续跳转');
}
} catch (sessionError) {
console.warn('Session 同步异常:', sessionError);
}
// 跳转到对应端首页
window.location.href = userData.redirect;
} else {
showError(data.message || '登录失败');
}
} catch (error) {
console.error('登录错误:', error);
showError('网络错误,请检查后端服务是否启动');
}
});
function showError(msg) {
const errorMsg = document.getElementById('errorMsg');
errorMsg.textContent = msg;
errorMsg.style.display = 'block';
setTimeout(() => {
errorMsg.style.display = 'none';
}, 3000);
}
</script>
</body>
</html>

View File

@@ -0,0 +1,84 @@
<?php
/**
* 多班级版班级管理系统 - 家长端考勤记录
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'parent') {
header('Location: /index.php');
exit();
}
$page_title = '考勤记录';
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/parent/dashboard.php" class="nav-item">首页</a>
<a href="/parent/history.php" class="nav-item">历史记录</a>
<a href="/parent/attendance.php" class="nav-item active">考勤记录</a>
<a href="/parent/password.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<div class="stats-grid">
<div class="stat-card"><div class="stat-label">出勤</div><div class="stat-value" id="attPresent">0</div></div>
<div class="stat-card"><div class="stat-label">缺勤</div><div class="stat-value" id="attAbsent">0</div></div>
<div class="stat-card"><div class="stat-label">迟到</div><div class="stat-value" id="attLate">0</div></div>
<div class="stat-card"><div class="stat-label">请假</div><div class="stat-value" id="attLeave">0</div></div>
</div>
<div class="card">
<div class="card-title">考勤记录明细</div>
<div class="table-wrapper">
<table class="table">
<thead><tr><th>日期</th><th>状态</th><th>原因</th></tr></thead>
<tbody id="attendanceList"></tbody>
</table>
</div>
</div>
</div>
<script>
async function loadAttendance() {
const res = await apiGet('/api/parent/child/attendance');
if (res && res.success) {
let present = 0, absent = 0, late = 0, leave = 0;
let html = '';
res.data.records.forEach(record => {
html += `<tr>
<td>${record.date}</td>
<td>${getStatusBadge(record.status, 'attendance')}</td>
<td>${escapeHtml(record.reason || '-')}</td>
</tr>`;
switch(record.status) {
case 'present': present++; break;
case 'absent': absent++; break;
case 'late': late++; break;
case 'leave': leave++; break;
}
});
document.getElementById('attPresent').textContent = present;
document.getElementById('attAbsent').textContent = absent;
document.getElementById('attLate').textContent = late;
document.getElementById('attLeave').textContent = leave;
if (res.data.records.length === 0) {
html = '<tr><td colspan="3" style="text-align:center;">暂无记录</td></tr>';
}
document.getElementById('attendanceList').innerHTML = html;
}
}
loadAttendance();
</script>
<script src="/assets/js/parent.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,101 @@
<?php
/**
* 多班级版班级管理系统 - 家长端首页
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'parent') {
header('Location: /index.php');
exit();
}
$page_title = '首页';
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/parent/dashboard.php" class="nav-item active">首页</a>
<a href="/parent/history.php" class="nav-item">历史记录</a>
<a href="/parent/attendance.php" class="nav-item">考勤记录</a>
<a href="/parent/password.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<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">
<div class="stat-label">当前操行分</div>
<div class="stat-value" id="totalPoints">--</div>
</div>
<div class="stat-card">
<div class="stat-label">班级排名</div>
<div class="stat-value" id="studentRank">--</div>
</div>
</div>
<div class="initial-points-hint" id="initialPointsHint"></div>
</div>
<style>
.child-info {
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
color: white;
margin-bottom: 20px;
}
.child-name { font-size: 24px; font-weight: bold; margin-bottom: 8px; }
.child-no { font-size: 14px; opacity: 0.9; }
.initial-points-hint {
text-align: center;
color: #999;
font-size: 13px;
margin-top: 8px;
}
</style>
<script>
async function loadDashboard() {
const res = await apiGet('/api/parent/child/conduct');
if (res && res.success) {
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 = '';
}
}
// 加载排名信息
const rankRes = await apiGet('/api/parent/child/ranking');
if (rankRes && rankRes.success) {
const rank = rankRes.data.rank;
if (rank) {
document.getElementById('studentRank').textContent = `第${rank}名`;
} else {
document.getElementById('studentRank').textContent = '--';
}
}
// 显示初始分提示
const initialPoints = window.STUDENT_INITIAL_POINTS || 60;
document.getElementById('initialPointsHint').textContent = `初始操行分为 ${initialPoints} 分`;
}
loadDashboard();
</script>
<script src="/assets/js/parent.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

117
frontend/parent/history.php Normal file
View File

@@ -0,0 +1,117 @@
<?php
/**
* 多班级版班级管理系统 - 家长端历史记录
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'parent') {
header('Location: /index.php');
exit();
}
$page_title = '历史记录';
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/parent/dashboard.php" class="nav-item">首页</a>
<a href="/parent/history.php" class="nav-item active">历史记录</a>
<a href="/parent/attendance.php" class="nav-item">考勤记录</a>
<a href="/parent/password.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<div class="card">
<div class="card-title">操行分历史记录</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr>
<th>日期</th>
<th>原因</th>
<th>分值</th>
<th>记录人</th>
</tr>
</thead>
<tbody id="historyList">
<tr><td colspan="4" style="text-align:center;">加载中...</td></tr>
</tbody>
</table>
</div>
<div class="pagination" id="pagination" style="display:none;">
<button class="btn btn-sm" id="prevBtn" onclick="changePage(-1)">上一页</button>
<span id="pageInfo">1 / 1</span>
<button class="btn btn-sm" id="nextBtn" onclick="changePage(1)">下一页</button>
</div>
</div>
</div>
<style>
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin-top: 15px;
padding: 10px 0;
}
.pagination .btn { padding: 6px 16px; font-size: 13px; }
.pagination span { color: #666; font-size: 14px; }
</style>
<script>
let currentPage = 1;
const pageSize = 20;
async function loadHistory(page) {
const res = await apiGet('/api/parent/child/history', { page: page, page_size: pageSize });
if (res && res.success) {
let html = '';
if (res.data.records.length === 0) {
html = '<tr><td colspan="4" style="text-align:center;">暂无记录</td></tr>';
} else {
res.data.records.forEach(record => {
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
const pointsText = record.points_change > 0 ? `+${record.points_change}` : record.points_change;
html += `<tr>
<td>${formatDateTime(record.created_at)}</td>
<td class="history-reason">${escapeHtml(record.reason || '-')}</td>
<td><span class="record-points ${pointsClass}">${pointsText}</span></td>
<td>班主任</td>
</tr>`;
});
}
document.getElementById('historyList').innerHTML = html;
// 分页
const totalPages = Math.ceil(res.data.total / pageSize);
if (totalPages > 1) {
document.getElementById('pagination').style.display = 'flex';
document.getElementById('pageInfo').textContent = `${res.data.page} / ${totalPages}`;
document.getElementById('prevBtn').disabled = res.data.page <= 1;
document.getElementById('nextBtn').disabled = res.data.page >= totalPages;
} else {
document.getElementById('pagination').style.display = 'none';
}
}
}
function changePage(delta) {
currentPage += delta;
if (currentPage < 1) currentPage = 1;
loadHistory(currentPage);
}
loadHistory(1);
</script>
<script src="/assets/js/parent.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,106 @@
<?php
/**
* 多班级版班级管理系统 - 家长修改密码页
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
$page_title = '修改密码';
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'parent') {
header('Location: /index.php');
exit();
}
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/parent/dashboard.php" class="nav-item">首页</a>
<a href="/parent/history.php" class="nav-item">历史记录</a>
<a href="/parent/attendance.php" class="nav-item">考勤记录</a>
<a href="/parent/password.php" class="nav-item active">修改密码</a>
</div>
<div class="container">
<div class="card">
<div class="card-title">修改密码</div>
<div id="featureDisabled" style="display:none;">
<p style="text-align:center;color:#999;padding:30px 0;">该功能暂未开放,请联系班主任启用"家长改密"功能开关。</p>
</div>
<form id="passwordForm" style="display:none;">
<div class="form-group">
<label>原密码 <span style="color:red;">*</span></label>
<input type="password" id="oldPassword" required>
</div>
<div class="form-group">
<label>新密码 <span style="color:red;">*</span></label>
<input type="password" id="newPassword" required>
<small>密码长度6-20位需包含大写字母、小写字母、数字、特殊符号中的至少3种</small>
</div>
<div class="form-group">
<label>确认新密码 <span style="color:red;">*</span></label>
<input type="password" id="confirmPassword" required>
</div>
<button type="submit" class="btn btn-primary">确认修改</button>
</form>
</div>
</div>
<script>
async function checkFeature() {
var res = await apiGet('/api/class/features');
if (res && res.success && res.data && res.data.features) {
var val = res.data.features.parent_password_change_enabled;
var enabled = val === 1 || val === '1' || val === true;
if (enabled) {
document.getElementById('passwordForm').style.display = '';
} else {
document.getElementById('featureDisabled').style.display = '';
}
} else {
document.getElementById('featureDisabled').style.display = '';
}
}
document.getElementById('passwordForm').addEventListener('submit', async (e) => {
e.preventDefault();
var oldPassword = document.getElementById('oldPassword').value;
var newPassword = document.getElementById('newPassword').value;
var confirmPassword = document.getElementById('confirmPassword').value;
if (newPassword !== confirmPassword) {
showToast('两次输入的新密码不一致', 'error');
return;
}
if (newPassword.length < 6 || newPassword.length > 20) {
showToast('密码长度需为6-20位', 'error');
return;
}
var res = await apiPost('/api/parent/password', {
old_password: oldPassword,
new_password: newPassword
});
if (res && res.success) {
showToast('密码修改成功,请重新登录');
setTimeout(function() { logout(); }, 1500);
} else {
showToast(res && res.message ? res.message : '密码修改失败', 'error');
}
});
document.addEventListener('DOMContentLoaded', checkFeature);
</script>
<script src="/assets/js/parent.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,83 @@
<?php
/**
* 多班级版班级管理系统 - 学生端考勤记录
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
header('Location: /index.php');
exit();
}
$page_title = '考勤记录';
$student_id = $_SESSION['student_id'];
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/student/dashboard.php" class="nav-item">首页</a>
<a href="/student/conduct_history.php" class="nav-item">操行分</a>
<a href="/student/homework.php" class="nav-item">作业</a>
<a href="/student/attendance.php" class="nav-item active">考勤</a>
<a href="/student/password.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<div class="stats-grid">
<div class="stat-card"><div class="stat-label">出勤</div><div class="stat-value" id="attPresent">0</div></div>
<div class="stat-card"><div class="stat-label">缺勤</div><div class="stat-value" id="attAbsent">0</div></div>
<div class="stat-card"><div class="stat-label">迟到</div><div class="stat-value" id="attLate">0</div></div>
<div class="stat-card"><div class="stat-label">请假</div><div class="stat-value" id="attLeave">0</div></div>
</div>
<div class="card">
<div class="card-title">考勤记录明细</div>
<div class="table-wrapper">
<table class="table">
<thead><tr><th>日期</th><th>状态</th><th>原因</th></tr></thead>
<tbody id="attendanceList"></tbody>
</table>
</div>
</div>
</div>
<script>
const STUDENT_ID = <?php echo $student_id; ?>;
async function loadAttendance() {
const res = await apiGet(`/api/student/attendance/${STUDENT_ID}`);
if (res && res.success) {
const stats = res.data.statistics;
document.getElementById('attPresent').textContent = stats.present || 0;
document.getElementById('attAbsent').textContent = stats.absent || 0;
document.getElementById('attLate').textContent = stats.late || 0;
document.getElementById('attLeave').textContent = stats.leave || 0;
let html = '';
res.data.records.forEach(record => {
html += `<tr>
<td>${record.date}</td>
<td>${getStatusBadge(record.status, 'attendance')}</td>
<td>${escapeHtml(record.reason || '-')}</td>
</tr>`;
});
if (res.data.records.length === 0) {
html = '<tr><td colspan="3" style="text-align:center;">暂无记录</td></tr>';
}
document.getElementById('attendanceList').innerHTML = html;
}
}
loadAttendance();
</script>
<script src="/assets/js/student.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,11 @@
<?php
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
header('Location: /index.php');
exit();
}
// 重定向到学生端首页的操行分标签
header('Location: /student/dashboard.php');
exit();

View File

@@ -0,0 +1,515 @@
<?php
/**
* 多班级版班级管理系统 - 学生端主页
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
// 检查登录状态
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
header('Location: /index.php');
exit();
}
$page_title = '学生端';
$student_id = $_SESSION['student_id'];
include __DIR__ . '/../includes/header.php';
?>
<style>
.conduct-score {
text-align: center;
padding: 20px;
}
.score-number {
font-size: 64px;
font-weight: bold;
color: #667eea;
}
.score-label {
color: #666;
margin-top: 8px;
}
.record-item {
padding: 12px 0;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.record-points {
font-weight: bold;
}
.record-points.plus {
color: #38a169;
}
.record-points.minus {
color: #e53e3e;
}
.record-reason {
flex: 1;
margin: 0 15px;
color: #555;
}
.record-time {
font-size: 12px;
color: #999;
}
.view-more {
text-align: center;
margin-top: 15px;
}
.view-more a {
color: #667eea;
text-decoration: none;
}
</style>
<div class="nav">
<button class="nav-item active" data-page="dashboard">首页</button>
<button class="nav-item" data-page="conduct">操行分详情</button>
<button class="nav-item" data-page="homework">作业情况</button>
<button class="nav-item" data-page="attendance">考勤记录</button>
<a href="/student/semester_history.php" class="nav-item">学期记录</a>
<button class="nav-item" data-page="password">修改密码</button>
</div>
<div class="container" id="pageContainer">
<!-- 首页内容 -->
<div id="page-dashboard" class="page-content">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">当前操行分</div>
<div class="stat-value" id="totalPoints">--</div>
</div>
<div class="stat-card">
<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="homeworkRate">--</div>
</div>
<div class="stat-card">
<div class="stat-label">本月出勤率</div>
<div class="stat-value" id="attendanceRate">--%</div>
</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>
<div class="view-more">
<a href="#" onclick="showPage('conduct'); return false;">查看更多 ></a>
</div>
</div>
</div>
<!-- 操行分详情页 -->
<div id="page-conduct" class="page-content" style="display: none;">
<div class="card">
<div class="conduct-score">
<div class="score-number" id="conductTotalPoints">--</div>
<div class="score-label">当前操行分</div>
</div>
</div>
<div class="card">
<div class="card-title">历史记录</div>
<div id="conductRecords"></div>
<div class="pagination" id="conductPagination"></div>
</div>
</div>
<!-- 作业情况页 -->
<div id="page-homework" class="page-content" style="display: none;">
<div class="card">
<div class="card-title">作业列表</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>时间</th>
<th>分值</th>
<th>原因</th>
<th>操作人</th>
</tr>
</thead>
<tbody id="homeworkList"></tbody>
</table>
</div>
</div>
</div>
<!-- 考勤记录页 -->
<div id="page-attendance" class="page-content" style="display: none;">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">出勤</div>
<div class="stat-value" id="attPresent">0</div>
</div>
<div class="stat-card">
<div class="stat-label">缺勤</div>
<div class="stat-value" id="attAbsent">0</div>
</div>
<div class="stat-card">
<div class="stat-label">迟到</div>
<div class="stat-value" id="attLate">0</div>
</div>
<div class="stat-card">
<div class="stat-label">请假</div>
<div class="stat-value" id="attLeave">0</div>
</div>
</div>
<div class="card">
<div class="card-title">考勤记录明细</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>日期</th>
<th>状态</th>
<th>原因</th>
</tr>
</thead>
<tbody id="attendanceList"></tbody>
</table>
</div>
</div>
</div>
<!-- 修改密码页 -->
<div id="page-password" class="page-content" style="display: none;">
<div class="card">
<div class="card-title">修改密码</div>
<form id="passwordForm">
<div class="form-group">
<label>原密码</label>
<input type="password" id="oldPassword" required>
</div>
<div class="form-group">
<label>新密码</label>
<input type="password" id="newPassword" required>
<small>密码长度6-20位需包含大写字母、小写字母、数字、特殊符号中的至少3种</small>
</div>
<div class="form-group">
<label>确认新密码</label>
<input type="password" id="confirmPassword" required>
</div>
<button type="submit" class="btn btn-primary">确认修改</button>
</form>
</div>
</div>
</div>
<!-- 修改密码模态框(首次登录强制) -->
<div id="forceChangePasswordModal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>首次登录,请修改密码</h3>
</div>
<form id="forcePasswordForm">
<div class="form-group">
<label>新密码</label>
<input type="password" id="forceNewPassword" required>
<small>密码长度6-20位需包含大写字母、小写字母、数字、特殊符号中的至少3种</small>
</div>
<div class="form-group">
<label>确认新密码</label>
<input type="password" id="forceConfirmPassword" required>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">确认修改</button>
</div>
</form>
</div>
</div>
<script>
const STUDENT_ID = <?php echo intval($student_id); ?>;
let conductPage = 1;
let conductTotalPages = 1;
// 页面切换
function showPage(pageName) {
document.querySelectorAll('.page-content').forEach(page => {
page.style.display = 'none';
});
document.getElementById(`page-${pageName}`).style.display = 'block';
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
if (item.dataset.page === pageName) {
item.classList.add('active');
}
});
// 加载对应页面数据
switch(pageName) {
case 'dashboard':
loadDashboard();
break;
case 'conduct':
loadConductHistory();
break;
case 'homework':
loadHomework();
break;
case 'attendance':
loadAttendance();
break;
}
}
// 加载首页
async function loadDashboard() {
try {
// 获取操行分
const conductRes = await apiGet(`/api/student/conduct/${STUDENT_ID}`);
if (conductRes && conductRes.success) {
document.getElementById('totalPoints').textContent = conductRes.data.total_points;
// 显示最近5条记录
const records = conductRes.data.records.slice(0, 5);
let html = '';
records.forEach(record => {
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
html += `
<div class="record-item">
<span class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</span>
<span class="record-reason">${escapeHtml(record.reason)}</span>
<span class="record-time">${formatDate(record.created_at)}</span>
</div>
`;
});
if (records.length === 0) {
html = '<div style="text-align:center;padding:20px;color:#999;">暂无记录</div>';
}
document.getElementById('recentRecords').innerHTML = html;
}
// 获取个人信息(宿舍号)
const infoRes = await apiGet('/api/student/my-info');
if (infoRes && infoRes.success && infoRes.data.student && infoRes.data.student.dormitory_number) {
document.getElementById('dormitoryNumber').textContent = infoRes.data.student.dormitory_number;
document.getElementById('dormitoryInfo').style.display = '';
}
// 获取班级排名
const rankingRes = await apiGet('/api/student/ranking', { limit: 100 });
if (rankingRes && rankingRes.success) {
const ranking = rankingRes.data.ranking || [];
const rankIndex = ranking.findIndex(s => s.student_id === parseInt(STUDENT_ID));
if (rankIndex >= 0) {
document.getElementById('studentRank').textContent = `第${rankIndex + 1}名`;
} else {
document.getElementById('studentRank').textContent = '--';
}
}
// 获取作业扣分统计
const homeworkRes = await apiGet(`/api/student/homework/${STUDENT_ID}`);
if (homeworkRes && homeworkRes.success) {
const homework = homeworkRes.data.homework || [];
const deductions = homework.length;
document.getElementById('homeworkRate').textContent = `${deductions} 次扣分`;
}
// 获取考勤统计
const attendanceRes = await apiGet(`/api/student/attendance/${STUDENT_ID}`);
if (attendanceRes && attendanceRes.success) {
const stats = attendanceRes.data.statistics;
const total = stats.present + stats.absent + stats.late + stats.leave;
const rate = total > 0 ? Math.round(stats.present / total * 100) : 100;
document.getElementById('attendanceRate').textContent = `${rate}%`;
}
} catch (error) {
console.error('加载首页失败:', error);
}
}
// 加载操行分历史
async function loadConductHistory(page = 1) {
conductPage = page;
try {
const res = await apiGet(`/api/student/conduct/${STUDENT_ID}`, {
limit: 20,
offset: (page - 1) * 20
});
if (res && res.success) {
document.getElementById('conductTotalPoints').textContent = res.data.total_points;
let html = '<div class="table-wrapper"><table><thead><tr><th>时间</th><th>分数变动</th><th>原因</th><th>操作人</th></tr></thead><tbody>';
res.data.records.forEach(record => {
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
const recorderDisplay = record.points_change < 0 ? '班主任' : escapeHtml(record.recorder_name || '班主任');
html += `
<tr>
<td>${formatDateTime(record.created_at)}</td>
<td class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
<td>${escapeHtml(record.reason)}</td>
<td>${recorderDisplay}</td>
</tr>
`;
});
if (res.data.records.length === 0) {
html += '<tr><td colspan="4" style="text-align:center;">暂无记录</td></tr>';
}
html += '</tbody></table></div>';
document.getElementById('conductRecords').innerHTML = html;
// 分页
const total = res.data.total || res.data.records.length;
conductTotalPages = Math.ceil(total / 20);
renderConductPagination();
}
} catch (error) {
console.error('加载操行分历史失败:', error);
}
}
function renderConductPagination() {
renderSmartPagination('conductPagination', conductPage, conductTotalPages, function(page) {
loadConductHistory(page);
});
}
// 加载作业
async function loadHomework() {
try {
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
if (res && res.success) {
let html = '';
res.data.homework.forEach(record => {
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
html += `
<tr>
<td>${formatDateTime(record.created_at)}</td>
<td class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
<td>${escapeHtml(record.reason)}</td>
<td>${escapeHtml(record.recorder_name || '-')}</td>
</tr>
`;
});
if (res.data.homework.length === 0) {
html = '<tr><td colspan="4" style="text-align:center;">暂无作业扣分记录</td></tr>';
}
document.getElementById('homeworkList').innerHTML = html;
}
} catch (error) {
console.error('加载作业记录失败:', error);
}
}
// 加载考勤
async function loadAttendance() {
try {
const res = await apiGet(`/api/student/attendance/${STUDENT_ID}`);
if (res && res.success) {
const stats = res.data.statistics;
document.getElementById('attPresent').textContent = stats.present || 0;
document.getElementById('attAbsent').textContent = stats.absent || 0;
document.getElementById('attLate').textContent = stats.late || 0;
document.getElementById('attLeave').textContent = stats.leave || 0;
let html = '';
res.data.records.forEach(record => {
html += `
<tr>
<td>${escapeHtml(record.date)}</td>
<td>${getStatusBadge(record.status, 'attendance')}</td>
<td>${escapeHtml(record.reason || '-')}</td>
</tr>
`;
});
if (res.data.records.length === 0) {
html = '<tr><td colspan="3" style="text-align:center;">暂无考勤记录</td></tr>';
}
document.getElementById('attendanceList').innerHTML = html;
}
} catch (error) {
console.error('加载考勤失败:', error);
}
}
// 修改密码
document.getElementById('passwordForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
const oldPassword = document.getElementById('oldPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (newPassword !== confirmPassword) {
showToast('两次输入的新密码不一致', 'error');
return;
}
const res = await apiPost('/api/auth/change-password', {
old_password: oldPassword,
new_password: newPassword
});
if (res && res.success) {
showToast('密码修改成功,请重新登录');
setTimeout(() => logout(), 1500);
} else {
showToast(res?.message || '密码修改失败', 'error');
}
});
// 强制修改密码
document.getElementById('forcePasswordForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
const newPassword = document.getElementById('forceNewPassword').value;
const confirmPassword = document.getElementById('forceConfirmPassword').value;
if (newPassword !== confirmPassword) {
alert('两次输入的新密码不一致');
return;
}
const res = await apiPost('/api/auth/change-password', {
old_password: '',
new_password: newPassword,
force: true
});
if (res && res.success) {
showToast('密码修改成功,请重新登录');
setTimeout(() => logout(), 1500);
} else {
alert(res?.message || '密码修改失败');
}
});
// 检查是否需要强制修改密码
function checkForceChangePassword() {
const user = getUserInfo();
if (user && user.need_change_password) {
document.getElementById('forceChangePasswordModal').style.display = 'flex';
}
}
// 初始化
document.querySelectorAll('.nav-item').forEach(btn => {
btn.addEventListener('click', () => {
showPage(btn.dataset.page);
});
});
loadDashboard();
checkForceChangePassword();
</script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,51 @@
<?php
/**
* 多班级版班级管理系统 - 学生端作业情况
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
header('Location: /index.php');
exit();
}
$page_title = '作业情况';
$student_id = $_SESSION['student_id'];
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/student/dashboard.php" class="nav-item">首页</a>
<a href="/student/conduct_history.php" class="nav-item">操行分</a>
<a href="/student/homework.php" class="nav-item active">作业</a>
<a href="/student/attendance.php" class="nav-item">考勤</a>
<a href="/student/password.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<div class="card">
<div class="card-title">作业列表</div>
<div class="table-wrapper">
<table class="table">
<thead>
<tr><th>时间</th><th>分值</th><th>原因</th><th>操作人</th></tr>
</thead>
<tbody id="homeworkList"></tbody>
</table>
</div>
</div>
</div>
<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'; ?>

View File

@@ -0,0 +1,81 @@
<?php
/**
* 多班级版班级管理系统 - 学生端修改密码
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
header('Location: /index.php');
exit();
}
$page_title = '修改密码';
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/student/dashboard.php" class="nav-item">首页</a>
<a href="/student/conduct_history.php" class="nav-item">操行分</a>
<a href="/student/homework.php" class="nav-item">作业</a>
<a href="/student/attendance.php" class="nav-item">考勤</a>
<a href="/student/password.php" class="nav-item active">修改密码</a>
</div>
<div class="container">
<div class="card">
<div class="card-title">修改密码</div>
<form id="passwordForm">
<div class="form-group">
<label>原密码</label>
<input type="password" id="oldPassword" required>
</div>
<div class="form-group">
<label>新密码</label>
<input type="password" id="newPassword" required>
<small>密码长度6-20位需包含大写字母、小写字母、数字、特殊符号中的至少3种</small>
</div>
<div class="form-group">
<label>确认新密码</label>
<input type="password" id="confirmPassword" required>
</div>
<button type="submit" class="btn btn-primary">确认修改</button>
</form>
</div>
</div>
<script>
document.getElementById('passwordForm').addEventListener('submit', async (e) => {
e.preventDefault();
const oldPassword = document.getElementById('oldPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (newPassword !== confirmPassword) {
showToast('两次输入的新密码不一致', 'error');
return;
}
const res = await apiPost('/api/auth/change-password', {
old_password: oldPassword,
new_password: newPassword
});
if (res && res.success) {
showToast('密码修改成功,请重新登录');
setTimeout(() => logout(), 1500);
} else {
showToast(res?.message || '密码修改失败', 'error');
}
});
</script>
<script src="/assets/js/student.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,206 @@
<?php
/**
* 多班级版班级管理系统 - 学生端学期记录页面
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
// 检查登录状态
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
header('Location: /index.php');
exit();
}
$page_title = '学期记录';
$student_id = $_SESSION['student_id'];
include __DIR__ . '/../includes/header.php';
?>
<style>
.nav .nav-item {
display: inline-block;
padding: 8px 16px;
margin: 0 4px;
border: none;
background: none;
color: #666;
font-size: 14px;
cursor: pointer;
text-decoration: none;
border-bottom: 2px solid transparent;
}
.nav .nav-item.active {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
font-weight: bold;
}
.semester-card {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.semester-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-border-light);
}
.semester-name {
font-size: 18px;
font-weight: bold;
color: #333;
}
.semester-date {
font-size: 12px;
color: #999;
}
.semester-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
text-align: center;
}
.semester-stat-item {
padding: 8px;
}
.semester-stat-value {
font-size: 24px;
font-weight: bold;
color: var(--color-primary);
}
.semester-stat-label {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state-icon {
font-size: 48px;
margin-bottom: 16px;
}
.archived-badge {
display: inline-block;
padding: 2px 8px;
background: #e8f5e9;
color: #388e3c;
border-radius: 10px;
font-size: 12px;
}
</style>
<div class="nav">
<a href="/student/dashboard.php" class="nav-item">首页</a>
<a href="/student/dashboard.php" class="nav-item">操行分详情</a>
<a href="/student/dashboard.php" class="nav-item">作业情况</a>
<a href="/student/dashboard.php" class="nav-item">考勤记录</a>
<a href="/student/semester_history.php" class="nav-item active">学期记录</a>
<a href="/student/dashboard.php" class="nav-item">修改密码</a>
</div>
<div class="container">
<div class="card">
<div class="card-title">历史学期记录</div>
<div id="semesterRecords"></div>
</div>
</div>
<script>
async function loadSemesterRecords() {
try {
const res = await apiGet('/api/student/semester-records');
if (res && res.success) {
const records = res.data.records || [];
const container = document.getElementById('semesterRecords');
if (records.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📋</div>
<p>暂无历史学期记录</p>
<p style="font-size: 13px; margin-top: 8px;">学期归档后,您的成绩记录将显示在这里</p>
</div>
`;
return;
}
let html = '';
records.forEach(record => {
const dateRange = record.start_date && record.end_date
? `${record.start_date} ~ ${record.end_date}`
: '';
html += `
<div class="semester-card">
<div class="semester-card-header">
<div>
<div class="semester-name">${escapeHtml(record.semester_name)}</div>
${dateRange ? `<div class="semester-date">${dateRange}</div>` : ''}
</div>
<span class="archived-badge">已归档</span>
</div>
<div class="semester-stats">
<div class="semester-stat-item">
<div class="semester-stat-value">${record.final_points}</div>
<div class="semester-stat-label">最终操行分</div>
</div>
<div class="semester-stat-item">
<div class="semester-stat-value">${record.rank_position || '--'}</div>
<div class="semester-stat-label">班级排名</div>
</div>
<div class="semester-stat-item">
<div class="semester-stat-value">${record.total_students || '--'}</div>
<div class="semester-stat-label">班级总人数</div>
</div>
</div>
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #eee;">
<div style="font-size: 12px; color: #999; margin-bottom: 8px;">考勤统计</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<span class="tag tag-success">出勤 ${record.attendance_present || 0}</span>
<span class="tag tag-danger">缺勤 ${record.attendance_absent || 0}</span>
<span class="tag tag-warning">迟到 ${record.attendance_late || 0}</span>
<span class="tag tag-info">请假 ${record.attendance_leave || 0}</span>
</div>
</div>
<div style="margin-top: 8px;">
<div style="font-size: 12px; color: #999; margin-bottom: 8px;">作业统计</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<span class="tag tag-success">已交 ${record.homework_submitted || 0}</span>
<span class="tag tag-danger">未交 ${record.homework_not_submitted || 0}</span>
<span class="tag tag-warning">迟交 ${record.homework_late || 0}</span>
</div>
</div>
</div>
`;
});
container.innerHTML = html;
}
} catch (error) {
console.error('加载学期记录失败:', error);
document.getElementById('semesterRecords').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<p>加载失败,请稍后重试</p>
</div>
`;
}
}
loadSemesterRecords();
</script>
<script src="/assets/js/student.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -0,0 +1,134 @@
<?php
/**
* 多班级版班级管理系统 - 超级管理员登录页
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (isset($_SESSION['user_id']) && $_SESSION['user_type'] === 'super_admin') {
header('Location: /admin/classes.php');
exit();
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title><?php echo htmlspecialchars(SITE_NAME, ENT_QUOTES, 'UTF-8'); ?> - 系统管理员登录</title>
<link rel="stylesheet" href="/assets/css/style.css">
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1><?php echo htmlspecialchars(SITE_NAME, ENT_QUOTES, 'UTF-8'); ?></h1>
<p>系统管理员登录</p>
</div>
<form id="superAdminLoginForm" class="login-form">
<div class="form-group">
<label>用户名</label>
<input type="text" id="username" name="username" required autocomplete="off" placeholder="系统管理员账号">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="password" name="password" required placeholder="请输入密码">
</div>
<button type="submit" class="btn-login">登 录</button>
<div id="errorMsg" class="error-msg" style="display: none;"></div>
</form>
<div class="login-footer">
<p>&copy; <?php echo date('Y'); ?> Sea Network Technology Studio</p>
</div>
</div>
<script>
window.API_BASE_URL = <?php echo json_encode(API_BASE_URL); ?>;
window.JWT_STORAGE_KEY = <?php echo json_encode(JWT_STORAGE_KEY); ?>;
window.USER_STORAGE_KEY = <?php echo json_encode(USER_STORAGE_KEY); ?>;
const superAdminLoginPath = '<?php echo htmlspecialchars(SUPER_ADMIN_LOGIN_PATH, ENT_QUOTES, 'UTF-8'); ?>';
document.getElementById('superAdminLoginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
const errorMsg = document.getElementById('errorMsg');
if (!username || !password) {
showError('请填写用户名和密码');
return;
}
try {
const loginUrl = API_BASE_URL + '/api' + superAdminLoginPath + '/login';
const response = await fetch(loginUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: username, password: password })
});
const data = await response.json();
if (data.success && data.data) {
const userData = data.data;
localStorage.setItem(JWT_STORAGE_KEY, userData.token);
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(userData));
try {
const sessionResponse = await fetch('/api/save_session.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + userData.token
},
body: JSON.stringify({
user_id: userData.user_id,
user_type: 'super_admin',
username: userData.username,
real_name: userData.real_name || '',
role: '系统管理员',
class_id: null,
class_name: '',
need_change_password: userData.need_change_password || false
})
});
if (!sessionResponse.ok) {
console.warn('Session 同步失败,但继续跳转');
}
} catch (sessionError) {
console.warn('Session 同步异常:', sessionError);
}
window.location.href = userData.redirect || '/admin/classes.php';
} else {
showError(data.message || '登录失败');
}
} catch (error) {
console.error('登录错误:', error);
showError('网络错误,请检查后端服务是否启动');
}
});
function showError(msg) {
const errorMsg = document.getElementById('errorMsg');
errorMsg.textContent = msg;
errorMsg.style.display = 'block';
setTimeout(() => {
errorMsg.style.display = 'none';
}, 3000);
}
</script>
</body>
</html>