const state = { classes: [], snapshot: null, activeClass: '', username: '' };
const el = id => document.getElementById(id);
function renderUnauthenticated() {
el('loginArea').classList.remove('hidden');
el('mainArea').classList.add('hidden');
el('teacherToolbar').classList.add('hidden');
}
function renderState() {
el('loginArea').classList.add('hidden');
el('mainArea').classList.remove('hidden');
el('teacherToolbar').classList.remove('hidden');
el('teacherChip').textContent = `教师:${state.username}`;
renderClassTabs();
renderStatistics();
renderGroups();
renderUngrouped();
}
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} 人
${group.members.length ? group.members.map(member => `- ${text(member.student_no)} · ${text(member.name)} ${member.is_leader ? '组长' : ''}${memberActions(member, group)}
`).join('') : '- 暂无成员
'}
`).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 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('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();