const state = { user: null, membership: null, snapshot: null }; const el = id => document.getElementById(id); function renderUnauthenticated() { el('loginArea').classList.remove('hidden'); el('mainArea').classList.add('hidden'); el('userToolbar').classList.add('hidden'); el('passwordModal').classList.add('hidden'); } function renderStatistics() { const stats = state.snapshot.statistics; const remaining = state.snapshot.statistics.total_students - state.snapshot.statistics.grouped_students; el('statistics').innerHTML = [ { label: '班级总人数', value: stats.total_students }, { label: '已分组人数', value: stats.grouped_students }, { label: '未分组人数', value: remaining } ].map(item => `
${text(item.value)}${item.label}
`).join(''); } function renderIdentity() { const user = state.user; el('userChip').textContent = `${user.class_name}班 · ${user.name} · ${user.student_no}`; el('identity').innerHTML = [ ['姓名', user.name], ['学号', user.student_no], ['班级', `${user.class_name}班`], ['登录状态', '已登录'] ].map(row => `${text(row[0])}${text(row[1])}`).join(''); el('classStatus').innerHTML = [ `${text(user.class_name)}班`, `${state.snapshot.locked ? '老师已停止操作' : '分组进行中'}` ].join(''); } function renderMyGroup() { const target = el('myGroupAction'); const membership = state.membership; if (!membership) { target.innerHTML = `

创建后你是组长。每组成员(包括组长)最多 ${state.snapshot.group_limit} 人。已加入别人的组时不能创建。

`; el('createForm').addEventListener('submit', submitCreate); return; } const isLeader = membership.leader_student_id === state.user.id; target.innerHTML = `
当前组:${text(membership.name)} ${isLeader ? '我是组长' : '我是组员'}

${isLeader ? '组长退出会解散整个组,所有成员都会变为未分组。' : '退出后可以加入其他组。已加入其他组时,不能创建新组。'}

`; el('leaveButton').addEventListener('click', submitLeave); } function memberLine(member) { return `${text(member.student_no)} · ${text(member.name)}${member.is_leader ? ' 组长' : ''}`; } function renderGroups() { const grid = el('groupsGrid'); if (!state.snapshot.groups.length) { grid.innerHTML = '
当前班级还没有小组。你可以创建第一个组。
'; return; } grid.innerHTML = state.snapshot.groups.map(group => { const own = state.membership && state.membership.group_id === group.id; const ownLeader = own && state.membership.leader_student_id === state.user.id; const locked = state.snapshot.locked; const full = group.full; let disabledReason = ''; if (locked) disabledReason = '老师已停止分组操作,当前只能查看分组结果'; else if (full) disabledReason = `该组已有 ${state.snapshot.group_limit} 人,不能再加入新成员`; return `
${text(group.name)}
${group.member_count}/${state.snapshot.group_limit} 人
${own ? '我的当前组' : ``}
`; }).join(''); grid.querySelectorAll('button[data-group]').forEach(button => button.addEventListener('click', () => { if (button.dataset.reason) { toast(button.dataset.reason, true); return; } submitJoin(Number(button.dataset.group)); })); grid.querySelectorAll('button[data-kick]').forEach(button => button.addEventListener('click', () => { if (button.dataset.reason) { toast(button.dataset.reason, true); return; } if (requireConfirm(`确定将 ${button.dataset.name} 移出小组吗?`)) { submitKick(Number(button.dataset.kick)); } })); } function renderState() { if (!state.user) return renderUnauthenticated(); el('loginArea').classList.add('hidden'); el('mainArea').classList.remove('hidden'); el('userToolbar').classList.remove('hidden'); renderStatistics(); renderIdentity(); renderMyGroup(); renderGroups(); if (state.user.must_change_password) { el('passwordModal').classList.remove('hidden'); } else { el('passwordModal').classList.add('hidden'); } } async function loadState() { const result = await api('state'); if (!result.ok || !result.authenticated) return renderUnauthenticated(); state.user = result.user; state.membership = result.membership; state.snapshot = result.snapshot; renderState(); } async function submitLogin(event) { event.preventDefault(); setButtonsDisabled(el('loginForm'), true); const result = await api('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 submitCreate(event) { event.preventDefault(); const result = await api('groups', { method: 'POST', json: { name: el('groupName').value } }); toast(result.message, !result.ok); if (result.ok) await loadState(); } async function submitJoin(groupId, confirmed = false) { const result = await api('join', { method: 'POST', json: { group_id: groupId, confirmed } }); if (!result.ok && result.require_confirm) { if (requireConfirm(result.message)) return submitJoin(groupId, true); toast('已取消加入'); return; } toast(result.message, !result.ok); if (result.ok) await loadState(); } async function submitKick(studentId) { const result = await api('groups/remove-member', { method: 'POST', json: { student_id: studentId } }); toast(result.message, !result.ok); if (result.ok) await loadState(); } async function submitLeave(confirmed = false) { const result = await api('leave', { method: 'POST', json: { confirmed } }); if (!result.ok && result.require_confirm) { if (requireConfirm(result.message)) return submitLeave(true); toast('已取消操作'); return; } toast(result.message, !result.ok); if (result.ok) await loadState(); } async function submitPassword(prefix) { const oldInput = el(`${prefix}Old`); const newInput = el(`${prefix}New`); const confirmInput = el(`${prefix}Confirm`); const form = prefix === 'first' ? el('firstPasswordForm') : el('passwordForm'); const notice = prefix === 'first' ? el('firstPasswordNotice') : null; if (newInput.value !== confirmInput.value) { if (notice) notice.textContent = '两次输入的新密码不一致'; else toast('两次输入的新密码不一致', true); return; } setButtonsDisabled(form, true); const result = await api('change-password', { method: 'POST', json: { old_password: oldInput.value, new_password: newInput.value } }); setButtonsDisabled(form, false); if (notice) notice.textContent = result.ok ? '' : result.message; toast(result.message, !result.ok); if (result.ok) { form.reset(); await loadState(); } } async function logout() { await api('logout', { method: 'POST' }); state.user = null; renderUnauthenticated(); } el('loginForm').addEventListener('submit', submitLogin); el('logoutButton').addEventListener('click', logout); el('logoutFromModal').addEventListener('click', logout); el('passwordForm').addEventListener('submit', event => { event.preventDefault(); submitPassword(''); }); el('firstPasswordForm').addEventListener('submit', event => { event.preventDefault(); submitPassword('first'); }); loadState();