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 = `
`;
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 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} 人
${group.members.length ? group.members.map(member => `- ${memberLine(member)}
`).join('') : '- 暂无成员
'}
${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));
}));
}
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 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();