Files
canglan c6db68a9f4 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
2026-06-23 16:02:28 +08:00

384 lines
14 KiB
JavaScript

/**
* 多班级版班级管理系统 - 学期管理页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
*
* 版权所有 © Sea Network Technology Studio
*/
(function() {
'use strict';
let archiveSemesterId = null;
let archivePage = 1;
let archiveTotalPages = 1;
let associateSemesterId = null;
function fillSemesterDates(type) {
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1;
const startDateInput = document.getElementById('semesterStartDate');
const endDateInput = document.getElementById('semesterEndDate');
if (type === 'upper') {
const year = currentMonth >= 6 ? currentYear : currentYear - 1;
const endYear = year + 1;
let febDay = 28;
if ((endYear % 4 === 0 && endYear % 100 !== 0) || endYear % 400 === 0) {
febDay = 29;
}
startDateInput.value = year + '-09-01';
endDateInput.value = endYear + '-02-' + febDay;
} else if (type === 'lower') {
startDateInput.value = currentYear + '-03-01';
endDateInput.value = currentYear + '-07-15';
}
}
async function loadSemesters() {
const res = await apiGet('/api/semester/list');
if (res && res.success) {
let html = '';
const semesters = res.data.semesters || [];
semesters.forEach(sem => {
let statusText = '';
let statusClass = '';
if (sem.is_archived) {
statusText = '已归档';
statusClass = 'status-badge status-not_submitted';
} else if (sem.is_active) {
statusText = '当前学期';
statusClass = 'status-badge status-submitted';
} else {
statusText = '未激活';
statusClass = 'status-badge status-late';
}
let actions = '';
const startDate = sem.start_date || '';
const endDate = sem.end_date || '';
if (!sem.is_archived) {
actions += `<div class="action-dropdown">
<button class="btn btn-sm action-dropdown-toggle" onclick="toggleActionDropdown(this)">操作 ▼</button>
<div class="action-dropdown-menu">
<a onclick="showEditSemesterModal(${sem.semester_id}, '${escapeHtml(sem.semester_name)}', '${startDate}', '${endDate}')">编辑</a>
${!sem.is_active ? `<a onclick="activateSemester(${sem.semester_id})">激活</a>` : ''}
<a onclick="showAssociateConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}', '${startDate}', '${endDate}')">关联数据</a>
<a class="danger" onclick="showArchiveConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">归档</a>
</div>
</div>`;
}
if (sem.is_archived) {
actions += `<button class="btn btn-sm btn-secondary" onclick="viewArchiveData(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">查看归档</button>`;
}
const conductCount = sem.conduct_count || 0;
const attendanceCount = sem.attendance_count || 0;
let recordText = '-';
if (conductCount > 0 || attendanceCount > 0) {
recordText = `${conductCount}条操行分 / ${attendanceCount}条考勤`;
}
const weekText = sem.current_week ? `${sem.current_week}` : '-';
html += `<tr>
<td>${escapeHtml(sem.semester_name)}</td>
<td>${formatDate(sem.start_date)}</td>
<td>${formatDate(sem.end_date)}</td>
<td>${weekText}</td>
<td><span class="${statusClass}">${statusText}</span></td>
<td>${recordText}</td>
<td>${formatDateTime(sem.created_at)}</td>
<td>${actions}</td>
</tr>`;
});
if (semesters.length === 0) {
html = '<tr><td colspan="8" style="text-align:center;">暂无学期,请点击上方按钮创建新学期</td></tr>';
}
document.getElementById('semesterList').innerHTML = html;
}
}
function showCreateSemesterModal() {
document.getElementById('semesterName').value = '';
document.getElementById('semesterStartDate').value = '';
document.getElementById('semesterEndDate').value = '';
document.getElementById('createSemesterModal').style.display = 'flex';
}
async function submitCreateSemester() {
const name = document.getElementById('semesterName').value.trim();
const startDate = document.getElementById('semesterStartDate').value;
const endDate = document.getElementById('semesterEndDate').value;
if (!name) {
showToast('请输入学期名称', 'warning');
return;
}
const res = await apiPost('/api/semester/create', {
semester_name: name,
start_date: startDate || null,
end_date: endDate || null
});
if (res && res.success) {
showToast(res.message || '学期创建成功');
closeModal('createSemesterModal');
loadSemesters();
} else {
showToast(res?.message || '创建失败', 'error');
}
}
async function activateSemester(semesterId) {
if (!confirm('确认将此学期设为当前活跃学期?其他学期将被设为非活跃。')) {
return;
}
const res = await apiPut(`/api/semester/activate/${semesterId}`);
if (res && res.success) {
showToast(res.message || '已设为当前学期');
loadSemesters();
} else {
showToast(res?.message || '操作失败', 'error');
}
}
function showEditSemesterModal(id, name, startDate, endDate) {
document.getElementById('editSemesterId').value = id;
document.getElementById('editSemesterName').value = name;
document.getElementById('editSemesterStartDate').value = startDate || '';
document.getElementById('editSemesterEndDate').value = endDate || '';
document.getElementById('editSemesterModal').style.display = 'flex';
}
async function submitEditSemester() {
const id = document.getElementById('editSemesterId').value;
const name = document.getElementById('editSemesterName').value.trim();
const startDate = document.getElementById('editSemesterStartDate').value;
const endDate = document.getElementById('editSemesterEndDate').value;
if (!name) {
showToast('请输入学期名称', 'warning');
return;
}
const data = { semester_name: name };
if (startDate) data.start_date = startDate;
if (endDate) data.end_date = endDate;
const res = await apiPut(`/api/semester/update/${id}`, data);
if (res && res.success) {
showToast(res.message || '更新成功');
closeModal('editSemesterModal');
loadSemesters();
} else {
showToast(res?.message || '更新失败', 'error');
}
}
async function deleteSemester() {
const id = document.getElementById('editSemesterId').value;
if (!confirm('确定要删除该学期吗?如果学期已有归档数据则无法删除。')) {
return;
}
const res = await apiDelete(`/api/semester/delete/${id}`);
if (res && res.success) {
showToast(res.message || '删除成功');
closeModal('editSemesterModal');
loadSemesters();
} else {
showToast(res?.message || '删除失败', 'error');
}
}
function showAssociateConfirm(id, name, startDate, endDate) {
associateSemesterId = id;
const dateRange = startDate ? `${startDate} ~ ${endDate || '至今'}` : '未设置日期范围';
document.getElementById('associateConfirmText').innerHTML =
`即将关联 <strong>${dateRange}</strong> 内的所有未分配学期的操行分记录和考勤记录到学期 "<strong>${name}</strong>"。`;
document.getElementById('associateConfirmModal').style.display = 'flex';
}
async function confirmAssociate() {
if (!associateSemesterId) return;
const res = await apiPost(`/api/semester/${associateSemesterId}/associate`);
if (res && res.success) {
showToast(res.message || '关联成功');
closeModal('associateConfirmModal');
associateSemesterId = null;
} else {
showToast(res?.message || '关联失败', 'error');
}
}
function showArchiveConfirm(semesterId, semesterName) {
archiveSemesterId = semesterId;
document.getElementById('archiveResetScores').checked = false;
document.getElementById('archiveConfirmText').innerHTML =
`确定要归档学期 "<strong>${semesterName}</strong>" 吗?<br>归档后将保存所有学生的当前操行分快照,该学期数据将变为只读。`;
document.getElementById('archiveConfirmModal').style.display = 'flex';
}
async function confirmArchive() {
if (!archiveSemesterId) return;
const resetScores = document.getElementById('archiveResetScores').checked;
const url = `/api/semester/archive/${archiveSemesterId}?reset_scores=${resetScores}`;
const res = await apiPost(url);
if (res && res.success) {
showToast(res.message || '归档成功');
closeModal('archiveConfirmModal');
archiveSemesterId = null;
loadSemesters();
} else {
showToast(res?.message || '归档失败', 'error');
}
}
async function viewArchiveData(semesterId, semesterName, page) {
page = page || 1;
archivePage = page;
document.getElementById('archiveDataTitle').textContent = `归档数据 - ${semesterName}`;
const res = await apiGet(`/api/semester/archive/${semesterId}/records`, {
page: page, page_size: 50
});
if (res && res.success) {
const data = res.data || {};
const archives = data.items || [];
let html = '';
archives.forEach(a => {
html += `<tr>
<td>${a.rank_position || '-'}</td>
<td>${escapeHtml(a.student_no)}</td>
<td>${escapeHtml(a.student_name)}</td>
<td>${a.final_points}</td>
<td>${a.attendance_present || 0}</td>
<td>${a.attendance_absent || 0}</td>
<td>${a.attendance_late || 0}</td>
<td>${a.attendance_leave || 0}</td>
<td>${a.homework_submitted || 0}</td>
<td>${a.homework_not_submitted || 0}</td>
<td>${a.homework_late || 0}</td>
</tr>`;
});
if (archives.length === 0) {
html = '<tr><td colspan="11" style="text-align:center;">暂无归档数据</td></tr>';
}
document.getElementById('archiveDataList').innerHTML = html;
archiveTotalPages = data.total_pages || 1;
renderArchivePagination(semesterId, semesterName);
document.getElementById('archiveDataModal').style.display = 'flex';
} else {
showToast(res?.message || '获取归档数据失败', 'error');
}
}
function renderArchivePagination(semesterId, semesterName) {
renderSmartPagination('archivePagination', archivePage, archiveTotalPages, function(page) {
viewArchiveData(semesterId, semesterName, page);
});
}
// ========== 周期重置功能 ==========
let pendingPeriodType = null;
let periodArchivesType = null;
let periodArchivesPage = 1;
let periodArchivesTotalPages = 1;
function confirmPeriodReset(periodType) {
pendingPeriodType = periodType;
const label = periodType === 'weekly' ? '本周' : '本月';
document.getElementById('periodResetText').innerHTML =
`确定要执行 <strong>${label}重置</strong> 吗?<br>将保存当前所有学生的操行分快照,然后将所有学生操行分重置为初始值。`;
document.getElementById('periodResetModal').style.display = 'flex';
}
async function executePeriodReset() {
if (!pendingPeriodType) return;
const res = await apiPost('/api/semester/period-reset', { period: pendingPeriodType });
if (res && res.success) {
showToast(res.message || '重置成功');
closeModal('periodResetModal');
pendingPeriodType = null;
} else {
showToast(res?.message || '重置失败', 'error');
}
}
async function showPeriodArchives(type, page) {
var periodType = type || periodArchivesType;
page = page || 1;
periodArchivesType = periodType;
periodArchivesPage = page;
const label = periodType === 'weekly' ? '周' : '月';
document.getElementById('periodArchivesTitle').textContent = label + '归档数据';
const res = await apiGet('/api/semester/period-archives', {
period: periodType,
page: page,
page_size: 50
});
if (res && res.success) {
const data = res.data || {};
const archives = data.items || [];
let html = '';
archives.forEach(function(a) {
const resetByLabel = a.reset_by === 'auto' ? '自动' : '手动';
html += '<tr>' +
'<td>' + escapeHtml(a.period_label) + '</td>' +
'<td>' + (a.rank_position || '-') + '</td>' +
'<td>' + escapeHtml(a.student_no) + '</td>' +
'<td>' + escapeHtml(a.student_name) + '</td>' +
'<td>' + a.final_points + '</td>' +
'<td>' + resetByLabel + '</td>' +
'<td>' + formatDateTime(a.archived_at) + '</td>' +
'</tr>';
});
if (archives.length === 0) {
html = '<tr><td colspan="7" style="text-align:center;">暂无归档数据</td></tr>';
}
document.getElementById('periodArchivesList').innerHTML = html;
periodArchivesTotalPages = data.total_pages || 1;
renderSmartPagination('periodArchivePagination', periodArchivesPage, periodArchivesTotalPages, function(p) {
showPeriodArchives(periodArchivesType, p);
});
document.getElementById('periodArchivesModal').style.display = 'flex';
} else {
showToast(res?.message || '获取归档数据失败', 'error');
}
}
loadSemesters();
window.fillSemesterDates = fillSemesterDates;
window.showCreateSemesterModal = showCreateSemesterModal;
window.submitCreateSemester = submitCreateSemester;
window.activateSemester = activateSemester;
window.showEditSemesterModal = showEditSemesterModal;
window.submitEditSemester = submitEditSemester;
window.deleteSemester = deleteSemester;
window.showAssociateConfirm = showAssociateConfirm;
window.confirmAssociate = confirmAssociate;
window.showArchiveConfirm = showArchiveConfirm;
window.confirmArchive = confirmArchive;
window.viewArchiveData = viewArchiveData;
window.confirmPeriodReset = confirmPeriodReset;
window.executePeriodReset = executePeriodReset;
window.showPeriodArchives = showPeriodArchives;
})();