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 c6db68a9f4
135 changed files with 19933 additions and 0 deletions

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