const state = { classes: [], snapshot: null, activeClass: '', username: '' }; const el = id => document.getElementById(id); const adminTabs = ['groups', 'ungrouped', 'settings']; function renderUnauthenticated() { el('loginArea').classList.remove('hidden'); el('mainArea').classList.add('hidden'); el('teacherToolbar').classList.add('hidden'); } function switchAdminTab(tabName) { if (!adminTabs.includes(tabName)) tabName = 'groups'; document.querySelectorAll('.admin-tab').forEach(button => { const active = button.dataset.tab === tabName; button.classList.toggle('active', active); button.setAttribute('aria-selected', String(active)); el(`${button.dataset.tab}Panel`).classList.toggle('hidden', !active); }); } function renderState() { el('loginArea').classList.add('hidden'); el('mainArea').classList.remove('hidden'); el('teacherToolbar').classList.remove('hidden'); el('teacherChip').textContent = `教师:${state.username}`; el('groupLimit').value = state.snapshot.group_limit; renderClassTabs(); renderStatistics(); renderGroups(); renderUngrouped(); } document.querySelectorAll('.admin-tab').forEach(button => { button.addEventListener('click', () => switchAdminTab(button.dataset.tab)); }); function renderClassTabs() { el('classTabs').innerHTML = state.classes.map(item => ` `).join(''); const current = state.classes.find(item => item.class_name === state.activeClass); const locked = current ? current.locked : false; el('managementStatus').innerHTML = [ `开放登录:${text(state.activeClass)}班`, `${locked ? '已停止学生操作' : '学生可正常操作'}`, `${state.snapshot.statistics.group_count} 个小组` ].join(''); el('lockButton').textContent = locked ? '恢复学生操作' : '停止学生操作'; el('lockButton').className = locked ? 'success' : 'warning'; el('exportButton').disabled = false; el('classTabs').querySelectorAll('button[data-class]').forEach(button => { button.addEventListener('click', () => switchClass(button.dataset.class)); }); } function renderStatistics() { const stats = state.snapshot.statistics; el('statistics').innerHTML = [ { label: '班级总人数', value: stats.total_students }, { label: '已分组人数', value: stats.grouped_students }, { label: '未分组人数', value: stats.total_students - stats.grouped_students } ].map(item => `
${text(item.value)}${item.label}
`).join(''); } function memberActions(member, group) { const isLeader = member.id === group.leader_student_id; return ` `; } function renderGroups() { const grid = el('groupsGrid'); if (!state.snapshot.groups.length) { grid.innerHTML = '
该班还没有小组,可先手工创建,或等学生自行创建。
'; return; } grid.innerHTML = state.snapshot.groups.map(group => `
${group.member_count}/${state.snapshot.group_limit} 人
`).join(''); grid.querySelectorAll('form[data-rename]').forEach(form => form.addEventListener('submit', async event => { event.preventDefault(); const result = await api('teacher/rename-group', { method: 'POST', json: { group_id: Number(form.dataset.rename), name: form.querySelector('input').value } }); toast(result.message, !result.ok); if (result.ok) await loadState(); })); bindGridActions(grid); } function bindGridActions(root) { root.querySelectorAll('button[data-action]').forEach(button => button.addEventListener('click', async () => { const action = button.dataset.action; if (action === 'disband' && !requireConfirm('解散后所有成员都会变为未分组,确定继续吗?')) return; if (action === 'remove' && !requireConfirm('确定将该学生移出当前小组吗?')) return; let result; if (action === 'leader') { const groupId = Number(button.dataset.group); const studentId = button.textContent.trim() === '取消组长' ? null : Number(button.dataset.student); result = await api('teacher/set-leader', { method: 'POST', json: { group_id: groupId, student_id: studentId } }); } else if (action === 'remove') { result = await api('teacher/remove-member', { method: 'POST', json: { student_id: Number(button.dataset.student) } }); } else if (action === 'disband') { result = await api('teacher/disband', { method: 'POST', json: { group_id: Number(button.dataset.group) } }); } toast(result.message, !result.ok); if (result.ok) await loadState(); })); } function renderUngrouped() { const container = el('ungroupedList'); const students = state.snapshot.ungrouped; if (!students.length) { container.innerHTML = '
没有未分组学生。
'; return; } const options = state.snapshot.groups.map(group => ``).join(''); container.innerHTML = `
${students.map(student => `
${text(student.student_no)} · ${text(student.name)}${text(student.class_name)}班
`).join('')}
`; container.querySelectorAll('form[data-add]').forEach(form => form.addEventListener('submit', async event => { event.preventDefault(); const group = Number(form.querySelector('select').value); if (!group) return; const result = await api('teacher/add-member', { method: 'POST', json: { group_id: group, student_id: Number(form.dataset.add) } }); toast(result.message, !result.ok); if (result.ok) await loadState(); })); } async function loadState() { const result = await api('state'); if (!result.ok || !result.authenticated || result.role !== 'teacher') return renderUnauthenticated(); state.classes = result.classes; state.snapshot = result.snapshot; state.activeClass = result.active_class; state.username = result.teacher.username; renderState(); } async function submitLogin(event) { event.preventDefault(); setButtonsDisabled(el('loginForm'), true); const result = await api('teacher-login', { method: 'POST', json: { username: el('username').value.trim(), password: el('password').value } }); setButtonsDisabled(el('loginForm'), false); el('loginNotice').textContent = result.ok ? '' : result.message; if (result.ok) { toast(result.message); await loadState(); } } async function submitTeacherPassword(event) { event.preventDefault(); const oldPassword = el('teacherOldPassword').value; const newPassword = el('teacherNewPassword').value; const confirmPassword = el('teacherConfirmPassword').value; const notice = el('teacherPasswordNotice'); notice.textContent = ''; if (newPassword !== confirmPassword) { notice.textContent = '两次输入的新密码不一致'; return; } setButtonsDisabled(el('teacherPasswordForm'), true); const result = await api('teacher/change-password', { method: 'POST', json: { old_password: oldPassword, new_password: newPassword } }); setButtonsDisabled(el('teacherPasswordForm'), false); notice.textContent = result.ok ? '' : result.message; toast(result.message, !result.ok); if (result.ok) { el('teacherPasswordForm').reset(); } } async function submitGroupLimit(event) { event.preventDefault(); setButtonsDisabled(el('groupLimitForm'), true); const result = await api('teacher/group-limit', { method: 'POST', json: { limit: Number(el('groupLimit').value) } }); setButtonsDisabled(el('groupLimitForm'), false); el('groupLimitNotice').textContent = result.ok ? '' : result.message; toast(result.message, !result.ok); if (result.ok) await loadState(); } async function switchClass(className) { const result = await api('teacher/class', { method: 'POST', json: { class_name: className } }); toast(result.message, !result.ok); if (result.ok) await loadState(); } async function toggleLock() { const current = state.classes.find(item => item.class_name === state.activeClass); const locked = !current || !current.locked; const message = locked ? '停止后学生只能查看分组结果,不能再创建/加入/退出。确定继续吗?' : '确定恢复学生分组操作吗?'; if (!requireConfirm(message)) return; const result = await api('teacher/lock', { method: 'POST', json: { class_name: state.activeClass, locked } }); toast(result.message, !result.ok); if (result.ok) await loadState(); } async function submitCreateGroup(event) { event.preventDefault(); const result = await api('teacher/create-group', { method: 'POST', json: { class_name: state.activeClass, name: el('newGroupName').value } }); toast(result.message, !result.ok); if (result.ok) { el('newGroupName').value = ''; await loadState(); } } async function importRoster(file) { if (!file) return; if (!requireConfirm('导入名单会重建全部学生账号和登录密码,并解散所有班级的已有分组。确定继续吗?')) return; toast('正在导入 Excel,请稍候...'); const result = await api(`teacher/import?filename=${encodeURIComponent(file.name)}`, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body: file }); toast(result.message, !result.ok); if (result.ok) await loadState(); } async function logout() { await api('logout', { method: 'POST' }); renderUnauthenticated(); } el('loginForm').addEventListener('submit', submitLogin); el('teacherPasswordForm').addEventListener('submit', submitTeacherPassword); el('groupLimitForm').addEventListener('submit', submitGroupLimit); el('logoutButton').addEventListener('click', logout); el('lockButton').addEventListener('click', toggleLock); el('createGroupForm').addEventListener('submit', submitCreateGroup); el('exportButton').addEventListener('click', () => { window.location.href = `/api/teacher/export?class=${encodeURIComponent(state.activeClass)}`; }); el('importFile').addEventListener('change', event => { importRoster(event.target.files[0]); event.target.value = ''; }); loadState();