feat: 多班级版班级管理系统 v2.0
技术栈:Go (Gin + GORM) + PHP + MySQL 5.7 + Redis 主要功能: - 多班级完全隔离(class_id 贯穿全系统) - 后端 Go Gin(端口 56789),Nginx 反代 - 超级管理员独立登录(env 配置,默认账密 admin/Admin123) - bcrypt 密码加密(无 PASSWORD_SALT) - 科任老师/课代表新角色 - 课代表作业管理页面 - 排行榜分项排行(操行分/考勤/作业) - 角色加减分上下限由班主任配置 - 家长改密功能(可开关) - 班级角色按需开关 - 宿舍号格式:南0-000 - 周度/月度重置功能 - MySQL 5.7 兼容 - 43 轮代码审查 + 全部修复 开发者: Canglan 版权归属: Sea Network Technology Studio 许可证: Apache License 2.0
This commit is contained in:
214
frontend/admin/classes.php
Normal file
214
frontend/admin/classes.php
Normal 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()">×</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'; ?>
|
||||
Reference in New Issue
Block a user