Files
SharedClassManager/frontend/student/dashboard.php
canglan 124d7f645e feat: 多班级版班级管理系统 v2.0
技术栈:Go (Gin + GORM) + PHP + MySQL 5.7 + Redis

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

开发者: Canglan
版权归属: Sea Network Technology Studio
许可证: Apache License 2.0
2026-06-22 10:21:52 +08:00

516 lines
19 KiB
PHP
Raw Blame History

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