feat: 多班级版 v2.0 - Go后端重写 + 43轮代码审查

- 后端从 Python FastAPI 重写为 Go Gin(端口 56789)
- 多班级完全隔离
- 超级管理员独立登录
- 课代表作业管理、排行榜分项排行
- 角色加减分上下限可配置
- 家长改密功能(可开关)
- 周度/月度重置功能
- MySQL 5.7 兼容
- 43轮代码审查+全部修复
- Apache 2.0 许可证
This commit is contained in:
2026-06-22 10:06:10 +08:00
parent 4084afc53c
commit d6dec878bd
214 changed files with 12622 additions and 9725 deletions

View File

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

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 管理端管理员管理
* 多班级版班级管理系统 - 管理端管理员管理
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 管理端考勤管理
* 多班级版班级管理系统 - 管理端考勤管理
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

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'; ?>

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 管理端操行分管理
* 多班级版班级管理系统 - 管理端操行分管理
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -20,7 +20,7 @@ if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
$page_title = '操行分管理';
$role = $_SESSION['role'] ?? '';
if (!in_array($role, ['班主任', '班长', '学习委员', '考勤委员', '劳动委员', '志愿委员'])) {
if (!in_array($role, ['班主任', '班长', '学习委员', '考勤委员', '劳动委员', '志愿委员', '科任老师'])) {
header('Location: /admin/dashboard.php');
exit();
}

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 管理端首页
* 多班级版班级管理系统 - 管理端首页
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 管理端历史记录
* 多班级版班级管理系统 - 管理端历史记录
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -93,11 +93,12 @@ include __DIR__ . '/../includes/header.php';
<table class="table">
<thead>
<tr id="historyTableHead">
<th>时间</th>
<th>学生</th>
<th>分数变动</th>
<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; ?>

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 管理端作业扣分
* 多班级版班级管理系统 - 管理端作业扣分
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 管理端修改密码
* 多班级版班级管理系统 - 管理端修改密码
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

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

@@ -1,18 +1,18 @@
<?php
/**
* 班级操行分管理系统 - 学期管理页面
* 多班级版班级管理系统 - 学期管理页面
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
require_once __DIR__ . '/../config.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
if (!isset($_SESSION['user_id']) || !in_array($_SESSION['user_type'], ['admin', 'super_admin'])) {
header('Location: /index.php');
exit();
}
@@ -30,6 +30,18 @@ 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>
@@ -198,6 +210,54 @@ include __DIR__ . '/../includes/header.php';
</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>

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 管理端学生管理
* 多班级版班级管理系统 - 管理端学生管理
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -48,7 +48,7 @@ include __DIR__ . '/../includes/header.php';
<th>姓名</th>
<th>宿舍号</th>
<th>操行分</th>
<?php if ($role === '班主任'): ?><th>家长手机号</th><?php endif; ?>
<?php if ($role === '班主任'): ?><th>家长账号(推荐手机号</th><?php endif; ?>
<th>操作</th>
</tr>
</thead>
@@ -99,7 +99,7 @@ include __DIR__ . '/../includes/header.php';
<input type="text" id="studentName" required>
</div>
<div class="form-group">
<label>家长手机号</label>
<label>家长账号(推荐手机号</label>
<input type="tel" id="parentPhone" placeholder="11位手机号">
<small>填写后将自动创建家长账号密码同学生初始密码123456</small>
</div>
@@ -133,7 +133,7 @@ include __DIR__ . '/../includes/header.php';
<input type="text" id="editStudentName" required maxlength="50">
</div>
<div class="form-group">
<label>家长手机号</label>
<label>家长账号(推荐手机号</label>
<input type="text" id="editStudentPhone" maxlength="20">
</div>
<div class="form-group">

View File

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

View File

@@ -38,8 +38,8 @@ curl_setopt_array($ch, [
'Authorization: Bearer ' . $token,
'Content-Type: application/json'
],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => 0
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2
]);
$apiResponse = curl_exec($ch);

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - Session 退出清除接口
* 多班级版班级管理系统 - Session 退出清除接口
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*
@@ -38,6 +38,8 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit();
}
// CSRF 风险说明:此接口仅清除 Session无敏感数据操作。
// 部署于同域 Nginx 反代下,浏览器同源策略已阻止跨域调用,实际风险较低。
// 清除 Session
$_SESSION = array();

View File

@@ -2,19 +2,20 @@
/**
* 执行单个升级步骤(代理至后端 API
*/
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../config.php';
// 验证登录和权限
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
header('Content-Type: application/json; charset=utf-8');
// 验证登录和权限admin 班主任 或 super_admin
if (!isset($_SESSION['user_id']) || !in_array($_SESSION['user_type'], ['admin', 'super_admin'])) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => '未授权']);
exit();
}
$userType = $_SESSION['user_type'];
$role = $_SESSION['role'] ?? '';
if ($role !== '班主任') {
if ($userType === 'admin' && $role !== '班主任') {
http_response_code(403);
echo json_encode(['success' => false, 'error' => '权限不足']);
exit();
@@ -27,7 +28,8 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit();
}
$stepVersion = $_GET['version'] ?? '';
$input = json_decode(file_get_contents('php://input'), true);
$stepVersion = $input['version'] ?? '';
if (empty($stepVersion)) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => '缺少版本号参数']);
@@ -56,8 +58,8 @@ curl_setopt_array($ch, [
'Authorization: Bearer ' . $token,
'Content-Type: application/json'
],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => 0
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2
]);
$apiResponse = curl_exec($ch);

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - Session 保存接口
* 多班级版班级管理系统 - Session 保存接口
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*
@@ -27,7 +27,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
// 只允许 POST 请求
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
@@ -38,6 +37,34 @@ if ($_SERVER['REQUEST_METHOD'] !== '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');
@@ -82,7 +109,7 @@ if (!empty($missingFields)) {
}
// 验证 user_type 是否合法
$validUserTypes = ['student', 'parent', 'admin'];
$validUserTypes = ['student', 'parent', 'admin', 'super_admin'];
if (!in_array($data['user_type'], $validUserTypes)) {
http_response_code(400);
echo json_encode([
@@ -115,8 +142,8 @@ curl_setopt_array($ch, [
'Authorization: Bearer ' . $token,
'Content-Type: application/json'
],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => 0
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2
]);
$apiResponse = curl_exec($ch);
@@ -153,18 +180,23 @@ if ($tokenUserId === null || intval($tokenUserId) !== intval($data['user_id']))
exit();
}
// 设置 Session 变量
$_SESSION['user_id'] = $data['user_id'];
$_SESSION['user_type'] = $data['user_type'];
$_SESSION['username'] = $data['username'];
$_SESSION['real_name'] = $data['real_name'] ?? '';
$_SESSION['role'] = $data['role'] ?? '';
// 从后端 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
if ($data['user_type'] === 'student') {
if (empty($data['student_id'])) {
// 如果是学生,额外设置 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,
@@ -172,7 +204,7 @@ if ($data['user_type'] === 'student') {
]);
exit();
}
$_SESSION['student_id'] = $data['student_id'];
$_SESSION['student_id'] = $studentId;
}
// 保存 Session

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 管理端样式
* 多班级版班级管理系统 - 管理端样式
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 全局样式
* 多班级版班级管理系统 - 全局样式
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -691,6 +691,10 @@ tr:hover {
background: #ed8936;
}
.toast-info {
background: var(--color-primary);
}
@keyframes fadeInUp {
from {
opacity: 0;

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 管理员管理页JS
* 多班级版班级管理系统 - 管理员管理页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 考勤管理页JS
* 多班级版班级管理系统 - 考勤管理页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio

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

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 公共JS
* 多班级版班级管理系统 - 公共JS
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -18,7 +18,7 @@ function getUserInfo() {
if (!userStr) return null;
try {
return JSON.parse(userStr);
} catch {
} catch (e) {
return null;
}
}
@@ -191,11 +191,13 @@ async function logout() {
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '\x26amp;')
.replace(/</g, '\x26lt;')
.replace(/>/g, '\x26gt;')
.replace(/"/g, '\x26quot;')
.replace(/'/g, '\x26#x27;');
.replace(/&/g, '&amp;')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, '&#x27;')
.replace(/`/g, '&#x60;')
.replace(/\//g, '&#x2F;');
}
/**
@@ -383,21 +385,19 @@ document.addEventListener('click', function(e) {
}
});
// 全局textarea键盘事件Enter提交表单Ctrl+Enter换行
// 全局textarea键盘事件Ctrl+Enter提交表单Enter换行(默认行为)
document.addEventListener('keydown', function(e) {
if (e.target.tagName !== 'TEXTAREA') return;
if (e.key === 'Enter' && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
// Enter键提交表单
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
// Ctrl+Enter / Cmd+Enter 提交表单
e.preventDefault();
var form = e.target.closest('form');
if (form) {
// 触发form的submit事件
var submitEvent = new Event('submit', { cancelable: true, bubbles: true });
form.dispatchEvent(submitEvent);
}
}
// Ctrl+Enter和Shift+Enter保持默认换行行为不拦截
});
window.selectDeductionType = function(points, reason) {

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 操行分管理页JS
* 多班级版班级管理系统 - 操行分管理页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
@@ -20,7 +20,7 @@ async function loadStudents() {
<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" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button></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) {
@@ -39,7 +39,7 @@ function showSinglePointsModal(studentId, studentName) {
}
async function exportMoralityRecords() {
showToast('正在导出德育分记录...', 'info');
showToast('正在导出操行分记录...', 'info');
try {
const studentsRes = await apiGet('/api/admin/students', { page_size: 1000 });
@@ -54,13 +54,20 @@ async function exportMoralityRecords() {
return;
}
const historyRes = await apiGet('/api/admin/conduct/history', { page: 1, page_size: 1000 });
if (!historyRes || !historyRes.success) {
showToast('获取历史记录失败', 'error');
return;
}
const allRecords = historyRes.data.records || [];
const 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 => {
@@ -90,7 +97,7 @@ async function exportMoralityRecords() {
if (field === null || field === undefined) return '';
let str = String(field).replace(/[\r\n]+/g, ' ');
str = str.replace(/"/g, '""');
if (/[\,\;\"\s]/.test(str)) {
if (/[\,\"\s]/.test(str)) {
str = '"' + str + '"';
}
return str;
@@ -106,7 +113,7 @@ async function exportMoralityRecords() {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `德育分记录_${new Date().toISOString().slice(0,10)}.csv`;
link.download = `操行分记录_${new Date().toISOString().slice(0,10)}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
@@ -119,7 +126,7 @@ async function exportMoralityRecords() {
}
}
// 宿舍集体加分相关
var dormitoryStudentIds = [];
let dormitoryStudentIds = [];
async function showDormitoryPointsModal() {
dormitoryStudentIds = [];
@@ -196,6 +203,11 @@ async function submitDormitoryPoints() {
return;
}
if (Math.abs(pointsChange) > 100) {
showToast('分值绝对值不能超过100', 'error');
return;
}
if (!reason.trim()) {
showToast('请填写原因', 'error');
return;
@@ -204,7 +216,8 @@ async function submitDormitoryPoints() {
const data = {
student_ids: dormitoryStudentIds,
points_change: pointsChange,
reason: reason
reason: reason,
related_type: 'manual'
};
const res = await apiPost('/api/admin/conduct/add', data);
@@ -220,6 +233,16 @@ async function submitDormitoryPoints() {
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;

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 管理端首页JS
* 多班级版班级管理系统 - 管理端首页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 历史记录页JS
* 多班级版班级管理系统 - 历史记录页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
@@ -15,11 +15,14 @@ const currentUserId = window.PAGE_CONFIG.userId;
let currentHistoryPage = 1;
let totalHistoryPages = 1;
function escapeHtml(str) {
if (!str) return '';
var el = document.createElement('span');
el.appendChild(document.createTextNode(str));
return el.innerHTML;
function getTypeLabel(relatedType) {
if (!relatedType) return '操行';
switch (relatedType) {
case 'conduct': return '操行';
case 'homework': return '作业';
case 'attendance': return '考勤';
default: return relatedType;
}
}
async function loadStudentsForSelect() {
@@ -35,9 +38,9 @@ async function loadStudentsForSelect() {
// 加载科目下拉列表
async function loadSubjectsForFilter() {
var subjectSelect = document.getElementById('historySubjectFilter');
let subjectSelect = document.getElementById('historySubjectFilter');
if (!subjectSelect) return;
var res = await apiGet('/api/subject/list', { is_active: true });
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 => {
@@ -49,16 +52,16 @@ async function loadSubjectsForFilter() {
// 筛选学生时自动取消合并记录
function onStudentFilterChange() {
var studentId = document.getElementById('historyStudentId').value;
let studentId = document.getElementById('historyStudentId').value;
if (studentId) {
var grouped = document.getElementById('historyGrouped');
let grouped = document.getElementById('historyGrouped');
if (grouped) grouped.checked = false;
}
}
// 科目筛选变化时,取消扣分类型筛选(互斥)
function onSubjectFilterChange() {
var subjectVal = document.getElementById('historySubjectFilter').value;
let subjectVal = document.getElementById('historySubjectFilter').value;
if (subjectVal) {
document.getElementById('historyReasonFilter').value = '';
}
@@ -66,8 +69,8 @@ function onSubjectFilterChange() {
// 折叠/展开筛选面板
function toggleFilterPanel() {
var panel = document.getElementById('advancedFilters');
var btn = document.getElementById('filterToggleBtn');
let panel = document.getElementById('advancedFilters');
let btn = document.getElementById('filterToggleBtn');
if (!panel || !btn) return;
if (panel.style.display === 'none') {
panel.style.display = 'block';
@@ -81,19 +84,19 @@ function toggleFilterPanel() {
async function loadHistory(page) {
page = page || 1;
currentHistoryPage = page;
var startDate = document.getElementById('historyStartDate').value;
var endDate = document.getElementById('historyEndDate').value;
var studentId = document.getElementById('historyStudentId').value;
var reasonFilter = document.getElementById('historyReasonFilter').value;
var subjectFilter = document.getElementById('historySubjectFilter').value;
var reasonSearch = document.getElementById('historyReasonSearch').value.trim();
var isGrouped = document.getElementById('historyGrouped').checked;
var statusFilter = document.getElementById('historyStatusFilter') ? document.getElementById('historyStatusFilter').value : '';
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;
var params = {
let params = {
page: page, page_size: 20,
start_date: startDate,
end_date: endDate
@@ -111,37 +114,38 @@ async function loadHistory(page) {
if (isGrouped) params.grouped = true;
if (statusFilter !== '') params.is_revoked = parseInt(statusFilter);
var res = await apiGet('/api/admin/conduct/history', params);
let res = await apiGet('/api/admin/conduct/history', params);
if (res && res.success) {
var nowrapStyle = ' style="white-space:nowrap;min-width:80px;"';
var headHtml = '';
let nowrapStyle = ' style="white-space:nowrap;min-width:80px;"';
let headHtml = '';
if (isGrouped) {
headHtml = '<th>时间</th><th>原因</th><th>分值</th><th' + nowrapStyle + '>操作人</th><th>涉及学生</th>';
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>';
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;
var html = '';
let html = '';
if (isGrouped) {
res.data.records.forEach(function(record) {
var pointsClass = record.points_change > 0 ? 'plus' : 'minus';
var names = record.student_names || '';
var allRevoked = record.all_revoked;
var revokedStyle = allRevoked ? ' style="opacity:0.5;text-decoration:line-through;"' : '';
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 class="history-time">' + formatDateTime(record.created_at) + '</td>' +
'<td class="history-reason">' + escapeHtml(record.reason) + '</td>' +
'<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-students">' + escapeHtml(names) + '</td>';
'<td class="history-time">' + formatDateTime(record.created_at) + '</td>';
if (role === '班主任' || role === '班长') {
if (allRevoked) {
html += '<td><span class="text-muted">已撤销</span></td>';
@@ -152,29 +156,30 @@ async function loadHistory(page) {
html += '</tr>';
});
if (res.data.records.length === 0) {
var colSpan = (role === '班主任' || role === '班长') ? 6 : 5;
let colSpan = (role === '班主任' || role === '班长') ? 7 : 6;
html = '<tr><td colspan="' + colSpan + '" style="text-align:center;">暂无记录</td></tr>';
}
} else {
res.data.records.forEach(function(record) {
var pointsClass = record.points_change > 0 ? 'plus' : 'minus';
var revokedStyle = record.is_revoked == 1 ? ' style="opacity:0.5;text-decoration:line-through;"' : '';
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 class="history-time">' + formatDateTime(record.created_at) + '</td>' +
'<td>' + escapeHtml(record.student_name) + '</td>' +
'<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.recorder_name) + '</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) {
var revokerInfo = record.revoker_name ? '由 ' + escapeHtml(record.revoker_name) + ' 撤销' : '已撤销';
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) {
var revokerInfo = record.revoker_name ? '由 ' + escapeHtml(record.revoker_name) + ' 撤销' : '已撤销';
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>';
@@ -192,7 +197,7 @@ async function loadHistory(page) {
});
if (res.data.records.length === 0) {
var colSpan = (role === '班主任' || role === '班长' || role === '考勤委员') ? 6 : 5;
let colSpan = (role === '班主任' || role === '班长' || role === '考勤委员') ? 7 : 6;
html = '<tr><td colspan="' + colSpan + '" style="text-align:center;">暂无记录</td></tr>';
}
}
@@ -211,17 +216,17 @@ function renderHistoryPagination() {
}
async function exportHistoryRecords() {
var startDate = document.getElementById('historyStartDate').value;
var endDate = document.getElementById('historyEndDate').value;
var studentId = document.getElementById('historyStudentId').value;
let startDate = document.getElementById('historyStartDate').value;
let endDate = document.getElementById('historyEndDate').value;
let studentId = document.getElementById('historyStudentId').value;
showToast('正在导出历史记录...', 'info');
try {
var reasonFilter = document.getElementById('historyReasonFilter').value;
var subjectFilter = document.getElementById('historySubjectFilter').value;
var reasonSearch = document.getElementById('historyReasonSearch').value.trim();
var params = { page: 1, page_size: 1000 };
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;
@@ -232,24 +237,31 @@ async function exportHistoryRecords() {
}
if (reasonSearch) params.reason_search = reasonSearch;
var res = await apiGet('/api/admin/conduct/history', params);
let res = await apiGet('/api/admin/conduct/history', params);
if (res && res.success && res.data.records) {
var records = res.data.records;
let records = res.data.records;
if (records.length === 0) {
showToast('没有找到记录', 'warning');
return;
}
var csv = '\uFEFF';
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 += (r.created_at || '') + ',' + (r.student_no || '') + ',' + (r.student_name || '') + ',' + (r.points_change > 0 ? '+' : '') + r.points_change + ',' + (r.reason || '').replace(/,/g, ';') + ',' + (r.recorder_name || '') + '\n';
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';
});
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
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);
@@ -273,21 +285,21 @@ async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt)
showToast('正在批量撤销...', 'info');
try {
var params = {
let params = {
page: 1, page_size: 1000,
start_date: document.getElementById('historyStartDate').value,
end_date: document.getElementById('historyEndDate').value,
reason_prefix: reason.substring(0, 4),
reason_prefix: reason,
grouped: false
};
var res = await apiGet('/api/admin/conduct/history', params);
let res = await apiGet('/api/admin/conduct/history', params);
if (!res || !res.success || !res.data.records) {
showToast('查询记录失败', 'error');
return;
}
var matchedIds = [];
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);
@@ -299,7 +311,7 @@ async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt)
return;
}
var revokeRes = await apiPost('/api/admin/conduct/batch-revoke', { record_ids: matchedIds });
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);
@@ -313,8 +325,8 @@ async function batchRevokeGrouped(reason, pointsChange, recorderName, createdAt)
// 初始化:并行加载学生和科目列表,然后加载历史记录
Promise.all([loadStudentsForSelect(), loadSubjectsForFilter()]).then(function() {
var urlParams = new URLSearchParams(window.location.search);
var preStudentId = urlParams.get('student_id');
let urlParams = new URLSearchParams(window.location.search);
let preStudentId = urlParams.get('student_id');
if (preStudentId) {
document.getElementById('historyStudentId').value = preStudentId;
onStudentFilterChange();

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 作业扣分页JS
* 多班级版班级管理系统 - 作业扣分页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
@@ -187,7 +187,7 @@ async function submitAddSubject() {
}
async function toggleSubjectStatus(subjectId, enable) {
const res = await apiPut(`/api/subject/update/${subjectId}`, { is_active: enable });
const res = await apiPut(`/api/subject/toggle/${subjectId}`, { is_active: enable });
if (res && res.success) {
showToast(enable ? '科目已启用' : '科目已禁用');
loadSubjectList();

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 管理员管理函数
* 多班级版班级管理系统 - 管理员管理函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 模态框工具函数
* 多班级版班级管理系统 - 模态框工具函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 加减分管理函数
* 多班级版班级管理系统 - 加减分管理函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 学生管理函数
* 多班级版班级管理系统 - 学生管理函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -32,7 +32,7 @@
const res = await apiPost('/api/admin/students', {
student_no: studentNo,
name: name,
parent_phone: parentPhone,
parent_account: parentPhone,
dormitory_number: document.getElementById('addDormitoryNumber').value.trim()
});
@@ -68,7 +68,7 @@
const res = await apiPut(`/api/admin/students/${studentId}`, {
name: name,
parent_phone: phone || null,
parent_account: phone || null,
dormitory_number: document.getElementById('editDormitoryNumber').value.trim()
});
@@ -146,14 +146,14 @@
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 += '<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_phone || '')}</td>
<td>${escapeHtml(s.parent_account || '')}</td>
<td>${escapeHtml(s.dormitory_number || '-')}</td>
<td>${escapeHtml(s.password || '123456')}</td>
</tr>`;

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 科目管理函数
* 多班级版班级管理系统 - 科目管理函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 通用工具函数
* 多班级版班级管理系统 - 通用工具函数
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 家长端JS
* 多班级版班级管理系统 - 家长端JS
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

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

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 学期管理页JS
* 多班级版班级管理系统 - 学期管理页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
@@ -41,7 +41,7 @@ async function loadSemesters() {
const res = await apiGet('/api/semester/list');
if (res && res.success) {
let html = '';
const semesters = res.data || [];
const semesters = res.data.semesters || [];
semesters.forEach(sem => {
let statusText = '';
let statusClass = '';
@@ -252,7 +252,7 @@ async function viewArchiveData(semesterId, semesterName, page) {
if (res && res.success) {
const data = res.data || {};
const archives = data.archives || [];
const archives = data.items || [];
let html = '';
archives.forEach(a => {
html += `<tr>
@@ -288,6 +288,80 @@ function renderArchivePagination(semesterId, semesterName) {
});
}
// ========== 周期重置功能 ==========
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;
@@ -302,5 +376,8 @@ window.confirmAssociate = confirmAssociate;
window.showArchiveConfirm = showArchiveConfirm;
window.confirmArchive = confirmArchive;
window.viewArchiveData = viewArchiveData;
window.confirmPeriodReset = confirmPeriodReset;
window.executePeriodReset = executePeriodReset;
window.showPeriodArchives = showPeriodArchives;
})();

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 学生端作业情况JS
* 多班级版班级管理系统 - 学生端作业情况JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio

View File

@@ -1,10 +1,10 @@
/**
* 班级操行分管理系统 - 学生端JS
* 多班级版班级管理系统 - 学生端JS
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,5 +1,5 @@
/**
* 班级操行分管理系统 - 学生管理页JS
* 多班级版班级管理系统 - 学生管理页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
@@ -28,13 +28,13 @@ async function loadStudents(page = 1) {
<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_phone ? student.parent_phone.slice(0,3) + '******' + student.parent_phone.slice(-2) : '-'}</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_phone || '')}', '${escapeHtml(student.dormitory_number || '')}')">编辑</a>
<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>

View File

@@ -1,49 +1,23 @@
{
"_comment1": "================================================",
"_comment2": "班级操行分管理系统 - 学生批量导入模板",
"_comment3": "开发者: Canglan | 版权: Sea Network Technology Studio",
"_comment4": "================================================",
"_comment5": "字段说明:",
"_comment6": " student_no - 必填,学生学号,唯一标识",
"_comment7": " name - 必填,学生姓名",
"_comment8": " parent_phone - 可选家长手机号11位手机号",
"_comment9": " dormitory_number - 可选,宿舍号(支持字母数字组合,如 301-A",
"_comment10": " password - 可选,初始密码,不填则默认 123456",
"_comment11": "================================================",
"_comment12": "导入规则:",
"_comment13": " 1. 学生操行分初始值 = 60分",
"_comment14": " 2. 学生账号 = 学号,密码 = 指定的password或123456",
"_comment15": " 3. 家长账号 = 手机号若parent_phone有值密码 = 指定的password或123456",
"_comment16": " 4. 家长姓名默认显示为 '学生姓名家长'",
"_comment17": "================================================",
"students": [
{
"student_no": "20240001",
"name": "张三",
"parent_phone": "13800138001",
"dormitory_number": "301-A",
"password": "123456"
},
{
"student_no": "20240002",
"name": "李四",
"parent_phone": "13800138002",
"dormitory_number": "205",
"password": "123456"
},
{
"student_no": "20240003",
"name": "王五",
"parent_phone": "",
"dormitory_number": "",
"password": ""
},
{
"student_no": "20240004",
"name": "赵六",
"parent_phone": "13800138004",
"dormitory_number": "102-B",
"password": ""
}
]
}
"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"
}
]
}

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 前端配置
* 多班级版班级管理系统 - 前端配置
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -60,20 +60,38 @@ 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);
ini_set('session.cookie_secure', 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', 7200);
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(0);
ini_set('display_errors', 0);
// 生产环境关闭错误显示(保留错误日志记录,仅隐藏页面输出)
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
ini_set('display_errors', 0);
ini_set('log_errors', 1);

View File

@@ -1,17 +1,6 @@
<?php
/**
* 班级操行分管理系统 - 公共底部
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
?>
<div class="footer">
<p>&copy; <?php echo date('Y'); ?> Sea Network Technology Studio</p>
</div>
</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>
</html>

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 公共头部
* 多班级版班级管理系统 - 公共头部
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -17,6 +17,8 @@ if (!isset($_SESSION)) {
$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>
@@ -24,7 +26,7 @@ $page_title = $page_title ?? '首页';
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title><?php echo SITE_NAME; ?> - <?php echo $page_title; ?></title>
<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">
@@ -32,8 +34,11 @@ $page_title = $page_title ?? '首页';
</head>
<body>
<div class="header">
<h1><?php echo SITE_NAME; ?></h1>
<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>
@@ -42,38 +47,46 @@ $page_title = $page_title ?? '首页';
</div>
</div>
<script>
window.API_BASE_URL = '<?php echo API_BASE_URL; ?>';
window.JWT_STORAGE_KEY = '<?php echo JWT_STORAGE_KEY; ?>';
window.USER_STORAGE_KEY = '<?php echo USER_STORAGE_KEY; ?>';
window.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步加载扣分规则配置
// 从后端API步加载扣分规则配置(优先加载班级级配置)
(function() {
var xhr = new XMLHttpRequest();
xhr.open('GET', window.API_BASE_URL + '/api/config/deduction-rules', false); // 同步请求
try {
xhr.send();
if (xhr.status === 200) {
var resp = JSON.parse(xhr.responseText);
if (resp.success && resp.data) {
window.DEDUCTION_HOMEWORK_NOT_SUBMIT = resp.data.DEDUCTION_HOMEWORK_NOT_SUBMIT;
window.DEDUCTION_HOMEWORK_LATE = resp.data.DEDUCTION_HOMEWORK_LATE;
window.DEDUCTION_ATTENDANCE_ABSENT = resp.data.DEDUCTION_ATTENDANCE_ABSENT;
window.DEDUCTION_ATTENDANCE_LATE = resp.data.DEDUCTION_ATTENDANCE_LATE;
window.DEDUCTION_ATTENDANCE_LEAVE = resp.data.DEDUCTION_ATTENDANCE_LEAVE;
window.STUDENT_INITIAL_POINTS = resp.data.STUDENT_INITIAL_POINTS;
}
}
} catch(e) {
// API加载失败时使用默认值
window.DEDUCTION_HOMEWORK_NOT_SUBMIT = 2;
window.DEDUCTION_HOMEWORK_LATE = 1;
window.DEDUCTION_ATTENDANCE_ABSENT = 3;
window.DEDUCTION_ATTENDANCE_LATE = 1;
window.DEDUCTION_ATTENDANCE_LEAVE = 0;
window.STUDENT_INITIAL_POINTS = 60;
// 先设置默认值,避免异步加载期间其他脚本读取到 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>
<script src="/assets/js/modules/utils.js"></script>
<div class="container">

View File

@@ -1,20 +1,30 @@
<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 if ($role === '班主任' || $role === '班长' || $role === '学习委员' || $role === '考勤委员' || $role === '劳动委员' || $role === '志愿委员'): ?>
<?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 ($role === '班主任' || $role === '学习委员'): ?>
<?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 === '班主任' || $role === '考勤委员'): ?>
<?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 ($role === '班主任'): ?>
<?php if (in_array($role, ['班主任', '系统管理员'])): ?>
<a href="/admin/admins.php" class="nav-item<?php echo $current_page === 'admins' ? ' active' : ''; ?>">管理员管理</a>
<?php endif; ?>
<?php if ($role === '班主任'): ?>
<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>

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 登录入口
* 多班级版班级管理系统 - 登录入口
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -16,7 +16,8 @@ if (isset($_SESSION['user_id']) && isset($_SESSION['user_type'])) {
$redirect = [
'student' => '/student/dashboard.php',
'parent' => '/parent/dashboard.php',
'admin' => '/admin/dashboard.php'
'admin' => '/admin/dashboard.php',
'super_admin' => '/admin/dashboard.php'
];
header("Location: " . ($redirect[$_SESSION['user_type']] ?? '/index.php'));
exit();
@@ -27,14 +28,14 @@ if (isset($_SESSION['user_id']) && isset($_SESSION['user_type'])) {
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title><?php echo SITE_NAME; ?> - 登录</title>
<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 SITE_NAME; ?></h1>
<p>学生 / 家长 / 管理 统一登录</p>
<h1><?php echo htmlspecialchars(SITE_NAME, ENT_QUOTES, 'UTF-8'); ?></h1>
<p>学生 / 家长 / 管理 统一登录</p>
</div>
<form id="loginForm" class="login-form">
@@ -59,9 +60,9 @@ if (isset($_SESSION['user_id']) && isset($_SESSION['user_type'])) {
</div>
<script>
window.API_BASE_URL = '<?php echo API_BASE_URL; ?>';
window.JWT_STORAGE_KEY = '<?php echo JWT_STORAGE_KEY; ?>';
window.USER_STORAGE_KEY = '<?php echo USER_STORAGE_KEY; ?>';
window.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();
@@ -105,7 +106,9 @@ if (isset($_SESSION['user_id']) && isset($_SESSION['user_type'])) {
username: userData.username,
real_name: userData.real_name,
role: userData.role || '',
student_id: userData.student_id || null
student_id: userData.student_id || null,
class_id: userData.class_id || null,
class_name: userData.class_name || ''
})
});

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 家长端考勤记录
* 多班级版班级管理系统 - 家长端考勤记录
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -25,6 +25,7 @@ include __DIR__ . '/../includes/header.php';
<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">

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 家长端首页
* 多班级版班级管理系统 - 家长端首页
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -25,6 +25,7 @@ include __DIR__ . '/../includes/header.php';
<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">

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 家长端历史记录
* 多班级版班级管理系统 - 家长端历史记录
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -25,6 +25,7 @@ include __DIR__ . '/../includes/header.php';
<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">

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

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 学生端考勤记录
* 多班级版班级管理系统 - 学生端考勤记录
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 学生端主页
* 多班级版班级管理系统 - 学生端主页
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/
@@ -237,7 +237,7 @@ include __DIR__ . '/../includes/header.php';
</div>
<script>
const STUDENT_ID = <?php echo $student_id; ?>;
const STUDENT_ID = <?php echo intval($student_id); ?>;
let conductPage = 1;
let conductTotalPages = 1;
@@ -302,8 +302,8 @@ include __DIR__ . '/../includes/header.php';
// 获取个人信息(宿舍号)
const infoRes = await apiGet('/api/student/my-info');
if (infoRes && infoRes.success && infoRes.data.dormitory_number) {
document.getElementById('dormitoryNumber').textContent = infoRes.data.dormitory_number;
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 = '';
}
@@ -311,9 +311,9 @@ include __DIR__ . '/../includes/header.php';
const rankingRes = await apiGet('/api/student/ranking', { limit: 100 });
if (rankingRes && rankingRes.success) {
const ranking = rankingRes.data.ranking || [];
const rank = ranking.find(s => s.student_id === parseInt(STUDENT_ID));
if (rank) {
document.getElementById('studentRank').textContent = `第${rank.rank}名`;
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 = '--';
}
@@ -321,9 +321,8 @@ include __DIR__ . '/../includes/header.php';
// 获取作业扣分统计
const homeworkRes = await apiGet(`/api/student/homework/${STUDENT_ID}`);
if (homeworkRes && homeworkRes.success) {
const stats = homeworkRes.data.statistics;
const deductions = stats.deductions || 0;
const total = stats.total || 0;
const homework = homeworkRes.data.homework || [];
const deductions = homework.length;
document.getElementById('homeworkRate').textContent = `${deductions} 次扣分`;
}

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 学生端作业情况
* 多班级版班级管理系统 - 学生端作业情况
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 学生端修改密码
* 多班级版班级管理系统 - 学生端修改密码
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

View File

@@ -1,11 +1,11 @@
<?php
/**
* 班级操行分管理系统 - 学生端学期记录页面
* 多班级版班级管理系统 - 学生端学期记录页面
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
* 许可证: Apache License 2.0
*
* 版权所有 © Sea Network Technology Studio
*/

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>