Files
selfgroup/static/student.js
2026-09-08 11:34:39 +08:00

234 lines
9.2 KiB
JavaScript

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 => `<div class="statistic"><strong>${text(item.value)}</strong><span>${item.label}</span></div>`).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 => `<span>${text(row[0])}</span><strong>${text(row[1])}</strong>`).join('');
el('classStatus').innerHTML = [
`<span class="chip">${text(user.class_name)}班</span>`,
`<span class="chip ${state.snapshot.locked ? 'locked' : 'active'}">${state.snapshot.locked ? '老师已停止操作' : '分组进行中'}</span>`
].join('');
}
function renderMyGroup() {
const target = el('myGroupAction');
const membership = state.membership;
if (!membership) {
target.innerHTML = `
<form id="createForm">
<label for="groupName">创建新组</label>
<div class="form-line">
<input id="groupName" maxlength="30" value="${text(state.user.name)}" placeholder="默认使用你的姓名" required>
<button data-working type="submit">创建</button>
</div>
<p class="hint">创建后你是组长。每组成员(包括组长)最多 ${state.snapshot.group_limit} 人。已加入别人的组时不能创建。</p>
</form>`;
el('createForm').addEventListener('submit', submitCreate);
return;
}
const isLeader = membership.leader_student_id === state.user.id;
target.innerHTML = `
<div class="status-row">
<span class="chip">当前组:${text(membership.name)}</span>
<span class="chip ${isLeader ? 'leader' : 'member'}">${isLeader ? '我是组长' : '我是组员'}</span>
</div>
<div class="actions" style="margin-top:14px">
<button class="${isLeader ? 'danger' : 'ghost'}" data-working id="leaveButton" type="button">${isLeader ? '解散我的组' : '退出当前组'}</button>
</div>
<p class="hint">${isLeader
? '组长退出会解散整个组,所有成员都会变为未分组。'
: '退出后可以加入其他组。已加入其他组时,不能创建新组。'}</p>`;
el('leaveButton').addEventListener('click', submitLeave);
}
function memberLine(member) {
return `<span class="member-name">${text(member.student_no)} · ${text(member.name)}${member.is_leader ? ' <span class="chip leader">组长</span>' : ''}</span>`;
}
function renderGroups() {
const grid = el('groupsGrid');
if (!state.snapshot.groups.length) {
grid.innerHTML = '<div class="empty">当前班级还没有小组。你可以创建第一个组。</div>';
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 `
<article class="group-card ${own ? 'own' : ''}">
<div class="group-top">
<div class="group-title">${text(group.name)}</div>
<span class="chip ${full ? 'full' : ''}">${group.member_count}/${state.snapshot.group_limit} 人</span>
</div>
<ul class="member-list">
${group.members.length ? group.members.map(member => `<li>${memberLine(member)}${ownLeader && !member.is_leader
? `<button class="small danger" data-working data-kick="${member.id}" data-name="${text(member.name)}"
${locked ? 'disabled data-reason="老师已停止分组操作"' : ''}>移出</button>` : ''}</li>`).join('')
: '<li><span class="hint">暂无成员</span></li>'}
</ul>
${own ? '<span class="chip active">我的当前组</span>'
: `<button class="small" data-working data-group="${group.id}" data-reason="${text(disabledReason)}">加入</button>`}
</article>`;
}).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();