const adminHealthText = document.querySelector("#adminHealthText");
const refreshBtn = document.querySelector("#refreshBtn");
const panels = {
accounts: document.querySelector("#accountsPanel"),
reviews: document.querySelector("#reviewsPanel"),
summaries: document.querySelector("#summariesPanel"),
summarySearch: document.querySelector("#summarySearchPanel"),
logs: document.querySelector("#logsPanel"),
register: document.querySelector("#registerPanel"),
};
const accountForm = document.querySelector("#accountForm");
const accountQuery = document.querySelector("#accountQuery");
const accountStatus = document.querySelector("#accountStatus");
const accountMeta = document.querySelector("#accountMeta");
const accountRows = document.querySelector("#accountRows");
const accountEditor = document.querySelector("#accountEditor");
const accountEditorTitle = document.querySelector("#accountEditorTitle");
const accountEditForm = document.querySelector("#accountEditForm");
const accountEditError = document.querySelector("#accountEditError");
const cancelAccountEditBtn = document.querySelector("#cancelAccountEditBtn");
const newAccountBtn = document.querySelector("#newAccountBtn");
const editStudentId = document.querySelector("#editStudentId");
const editStudent = document.querySelector("#editStudent");
const editRemaining = document.querySelector("#editRemaining");
const editStatus = document.querySelector("#editStatus");
const editPayments = document.querySelector("#editPayments");
const editNote = document.querySelector("#editNote");
const reviewStatus = document.querySelector("#reviewStatus");
const reviewMeta = document.querySelector("#reviewMeta");
const reviewRows = document.querySelector("#reviewRows");
const summaryReviewStatus = document.querySelector("#summaryReviewStatus");
const summaryReviewMeta = document.querySelector("#summaryReviewMeta");
const summaryReviewRows = document.querySelector("#summaryReviewRows");
const summaryReviewDrawerBackdrop = document.querySelector("#summaryReviewDrawerBackdrop");
const summaryReviewDrawerClose = document.querySelector("#summaryReviewDrawerClose");
const summaryReviewDrawerTitle = document.querySelector("#summaryReviewDrawerTitle");
const summaryReviewDrawerMeta = document.querySelector("#summaryReviewDrawerMeta");
const summaryReviewDrawerStatus = document.querySelector("#summaryReviewDrawerStatus");
const summaryReviewDrawerCourse = document.querySelector("#summaryReviewDrawerCourse");
const summaryReviewDrawerLine = document.querySelector("#summaryReviewDrawerLine");
const summaryReviewDrawerReasons = document.querySelector("#summaryReviewDrawerReasons");
const summaryReviewDrawerBody = document.querySelector("#summaryReviewDrawerBody");
const summaryReviewDrawerSource = document.querySelector("#summaryReviewDrawerSource");
const summaryReviewDrawerApprove = document.querySelector("#summaryReviewDrawerApprove");
const summaryReviewDrawerReject = document.querySelector("#summaryReviewDrawerReject");
const summarySearchForm = document.querySelector("#summarySearchForm");
const summarySearchQuery = document.querySelector("#summarySearchQuery");
const summarySearchStudent = document.querySelector("#summarySearchStudent");
const summarySearchTeacher = document.querySelector("#summarySearchTeacher");
const summarySearchSubject = document.querySelector("#summarySearchSubject");
const summarySearchDateFrom = document.querySelector("#summarySearchDateFrom");
const summarySearchDateTo = document.querySelector("#summarySearchDateTo");
const summarySearchMeta = document.querySelector("#summarySearchMeta");
const summarySearchRows = document.querySelector("#summarySearchRows");
const logOperation = document.querySelector("#logOperation");
const logStatus = document.querySelector("#logStatus");
const logFilterForm = document.querySelector("#logFilterForm");
const logStudent = document.querySelector("#logStudent");
const logMeta = document.querySelector("#logMeta");
const logRows = document.querySelector("#logRows");
const classRegisterForm = document.querySelector("#classRegisterForm");
const classRegisterLines = document.querySelector("#classRegisterLines");
const classRegisterStatus = document.querySelector("#classRegisterStatus");
const paymentRegisterForm = document.querySelector("#paymentRegisterForm");
const paymentRegisterLines = document.querySelector("#paymentRegisterLines");
const paymentRegisterStatus = document.querySelector("#paymentRegisterStatus");
const summaryRegisterForm = document.querySelector("#summaryRegisterForm");
const summaryRegisterItems = document.querySelector("#summaryRegisterItems");
const addSummaryRegisterItemBtn = document.querySelector("#addSummaryRegisterItemBtn");
const summaryRegisterStatus = document.querySelector("#summaryRegisterStatus");
let currentAccounts = [];
let editingAccountId = "";
let currentSummaryReviews = [];
let activeSummaryReview = null;
let summaryRegisterItemSeq = 0;
const SUMMARY_REQUIRED_FIELDS = [
["student", "学生"],
["date", "日期"],
["time", "时间"],
["teacher", "老师"],
["subject", "科目"],
];
function fmtHours(value) {
return Number(value || 0).toLocaleString("zh-CN", { maximumFractionDigits: 2 });
}
function fmtTime(seconds) {
if (!seconds) return "未知";
return new Date(seconds * 1000).toLocaleString("zh-CN", { hour12: false });
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function metric(label, value) {
return `
${escapeHtml(label)}${escapeHtml(value)}
`;
}
function statusClass(status) {
if (status === "欠费") return "debt";
if (status === "预警") return "warning";
if (status === "正常") return "normal";
return "closed";
}
function taskStatusClass(status) {
if (status === "approved" || status === "auto_registered") return "normal";
if (status === "rejected" || status === "duplicate") return "closed";
if (status === "conflict") return "debt";
return "warning";
}
function renderPayments(payments) {
if (!payments.length) return "暂无";
return payments.map((item) => `${escapeHtml(item.date)}:${fmtHours(item.hours)} 小时`).join("
");
}
async function fetchJson(url, options = {}) {
const response = await fetch(url, { cache: "no-store", ...options });
if (!response.ok) {
let detail = `${response.status} ${response.statusText}`;
try {
const payload = await response.json();
detail = payload.detail || detail;
} catch (_error) {
detail = response.statusText || detail;
}
throw new Error(detail);
}
return response.json();
}
function setActiveTab(tabName) {
document.querySelectorAll("[data-admin-tab]").forEach((button) => {
button.classList.toggle("is-active", button.dataset.adminTab === tabName);
});
Object.entries(panels).forEach(([name, panel]) => {
panel.hidden = name !== tabName;
});
if (tabName === "accounts") loadAccounts();
if (tabName === "reviews") loadReviews();
if (tabName === "summaries") loadSummaryReviews();
if (tabName === "summarySearch") loadSummarySearch();
if (tabName === "logs") loadOperationLogs();
}
async function loadAdminHealth() {
try {
const data = await fetchJson("/api/account-health");
adminHealthText.textContent = `账户 ${data.accounts_count} 人;数据更新时间 ${fmtTime(data.accounts.mtime)}`;
} catch (error) {
adminHealthText.textContent = `读取失败:${error.message}`;
}
}
function renderAccountSummary(summary, count) {
accountMeta.innerHTML = [
metric("当前结果", `${count} 人`),
metric("欠费", summary.debt),
metric("预警", summary.warning),
metric("正常", summary.normal),
].join("");
}
async function loadAccounts() {
accountRows.innerHTML = `| 正在读取 |
`;
const params = new URLSearchParams();
if (accountQuery.value.trim()) params.set("q", accountQuery.value.trim());
if (accountStatus.value) params.set("status", accountStatus.value);
try {
const data = await fetchJson(`/api/accounts?${params.toString()}`);
currentAccounts = data.accounts;
renderAccountSummary(data.summary, data.count);
accountRows.innerHTML = data.accounts
.map(
(row) => `
${escapeHtml(row.student)} ${escapeHtml(row.student_id)} |
${escapeHtml(row.account_status)} |
${fmtHours(row.remaining)} |
${renderPayments(row.payments)} |
${escapeHtml(row.note || "")} |
|
`,
)
.join("");
if (!data.accounts.length) {
accountRows.innerHTML = `| 没有符合条件的账户 |
`;
}
} catch (error) {
accountRows.innerHTML = `| 读取失败:${escapeHtml(error.message)} |
`;
}
}
function paymentsToText(payments) {
return payments.map((item) => `${item.date}:${item.hours}`).join("\n");
}
function parsePaymentsInput(value) {
return value
.split(/[\n,]+/)
.map((item) => item.trim())
.filter(Boolean)
.map((item) => {
const [date, hoursText] = item.split(":");
if (!date || hoursText === undefined) {
throw new Error(`缴费记录格式错误:${item}`);
}
const hours = Number(hoursText);
if (!Number.isFinite(hours)) {
throw new Error(`缴费课时格式错误:${item}`);
}
return { date: date.trim(), hours };
});
}
function openNewAccountEditor() {
editingAccountId = "";
accountEditorTitle.textContent = "新增账户";
editStudentId.value = "";
editStudentId.placeholder = "保存时自动生成";
editStudent.value = "";
editRemaining.value = "0";
editStatus.value = "正常";
editPayments.value = "";
editNote.value = "";
accountEditError.hidden = true;
accountEditor.hidden = false;
editStudent.focus();
}
function openEditAccountEditor(studentId) {
const account = currentAccounts.find((item) => item.student_id === studentId);
if (!account) return;
editingAccountId = account.student_id;
accountEditorTitle.textContent = `编辑账户:${account.student}`;
editStudentId.value = account.student_id;
editStudentId.placeholder = "";
editStudent.value = account.student;
editRemaining.value = account.remaining;
editStatus.value = account.account_status;
editPayments.value = paymentsToText(account.payments);
editNote.value = account.note || "";
accountEditError.hidden = true;
accountEditor.hidden = false;
editStudent.focus();
}
function buildAccountPayload() {
const remaining = Number(editRemaining.value);
if (!Number.isFinite(remaining)) {
throw new Error("剩余课时必须是数字");
}
return {
student_id: editStudentId.value.trim(),
student: editStudent.value.trim(),
payments: parsePaymentsInput(editPayments.value),
remaining,
account_status: editStatus.value,
note: editNote.value.trim(),
};
}
async function saveAccount(event) {
event.preventDefault();
try {
const payload = buildAccountPayload();
const url = editingAccountId
? `/api/admin/accounts/${encodeURIComponent(editingAccountId)}`
: "/api/admin/accounts";
const method = editingAccountId ? "PUT" : "POST";
await fetchJson(url, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
accountEditor.hidden = true;
await loadAdminHealth();
await loadAccounts();
} catch (error) {
accountEditError.textContent = error.message;
accountEditError.hidden = false;
}
}
function renderReviewLine(line) {
return `${escapeHtml(line)}`;
}
function reviewTaskTypeLabel(item) {
if (item.type === "class_record_deletion") return "删除";
return "纠错";
}
function renderReviewTarget(item) {
if (item.type === "class_record_deletion") {
const restored = item.original && item.original.duration_hours ? `通过后恢复 ${fmtHours(item.original.duration_hours)} 小时` : "通过后恢复课时";
return `删除
${escapeHtml(restored)}`;
}
return renderReviewLine(item.corrected_line);
}
async function loadReviews() {
reviewRows.innerHTML = `| 正在读取 |
`;
try {
const [correctionData, deletionData] = await Promise.all([
fetchJson(`/api/admin/tasks?${new URLSearchParams({ type: "class_record_correction", ...(reviewStatus.value ? { status: reviewStatus.value } : {}) }).toString()}`),
fetchJson(`/api/admin/tasks?${new URLSearchParams({ type: "class_record_deletion", ...(reviewStatus.value ? { status: reviewStatus.value } : {}) }).toString()}`),
]);
const items = [...(correctionData.items || []), ...(deletionData.items || [])].sort((a, b) => Number(b.id || 0) - Number(a.id || 0));
reviewMeta.innerHTML = [metric("当前结果", `${items.length} 条`)].join("");
reviewRows.innerHTML = items
.map((item) => {
const canReview = item.status === "pending" || item.status === "conflict";
return `
| #${escapeHtml(item.id)} |
${escapeHtml(item.status)}${item.message ? ` ${escapeHtml(item.message)}` : ""} |
${escapeHtml(reviewTaskTypeLabel(item))} |
${renderReviewLine(item.original_line)} |
${renderReviewTarget(item)} |
${escapeHtml(item.created_at || "")} |
|
`;
})
.join("");
if (!items.length) {
reviewRows.innerHTML = `| 没有符合条件的审核项 |
`;
}
} catch (error) {
reviewRows.innerHTML = `| 读取失败:${escapeHtml(error.message)} |
`;
}
}
function summaryReviewKind(item) {
const sourceId = String(item.source_id || "");
const reasons = Array.isArray(item.reasons) ? item.reasons.join(" ") : "";
if (sourceId.startsWith("history-missing:") || reasons.includes("历史 classnotes缺失")) {
return "历史缺失";
}
return "";
}
function renderReviewTag(label) {
if (!label) return "";
return `${escapeHtml(label)}`;
}
function renderSummaryInfo(summary, item = {}) {
const parts = [
summary.student,
summary.date_iso,
summary.time_range,
summary.teacher,
summary.subject,
].filter(Boolean);
return `${renderReviewTag(summaryReviewKind(item))}${parts.map(escapeHtml).join("
")}
${escapeHtml(summary.group || "")}`;
}
function renderSummaryPreview(summary) {
const body = String(summary.body || "");
const preview = body.length > 140 ? `${body.slice(0, 140)}...` : body;
return `${escapeHtml(preview || "暂无原文")}
`;
}
function findSummaryReview(taskId) {
return currentSummaryReviews.find((item) => String(item.id) === String(taskId));
}
function canReviewSummary(item) {
return item && (item.status === "pending" || item.status === "conflict");
}
function courseInfoLines(summary) {
return [
summary.student ? `学生:${summary.student}` : "",
summary.date_iso ? `日期:${summary.date_iso}` : "",
summary.time_range ? `时间:${summary.time_range}` : "",
summary.teacher || summary.subject ? `老师/科目:${summary.teacher || ""} ${summary.subject || ""}`.trim() : "",
summary.group ? `班级/分组:${summary.group}` : "",
].filter(Boolean);
}
function openSummaryReviewDrawer(taskId) {
const item = findSummaryReview(taskId);
if (!item) return;
const summary = item.summary || {};
const reasons = Array.isArray(item.reasons) ? item.reasons : [];
const kind = summaryReviewKind(item);
const canReview = canReviewSummary(item);
activeSummaryReview = item;
summaryReviewDrawerTitle.textContent = `课程小结详情 #${item.id}`;
summaryReviewDrawerMeta.innerHTML = [
item.created_at ? `提交时间:${escapeHtml(item.created_at)}` : "",
kind ? renderReviewTag(kind) : "",
].filter(Boolean).join(" ");
summaryReviewDrawerStatus.innerHTML = `${escapeHtml(item.status || "")}${item.message ? ` ${escapeHtml(item.message)}` : ""}`;
summaryReviewDrawerCourse.innerHTML = courseInfoLines(summary).map(escapeHtml).join("
") || "暂无课程信息";
summaryReviewDrawerLine.innerHTML = item.proposed_line ? renderReviewLine(item.proposed_line) : "暂无";
summaryReviewDrawerReasons.innerHTML = reasons.length ? reasons.map(escapeHtml).join("
") : "待人工复核";
summaryReviewDrawerBody.textContent = summary.body || "暂无原文";
summaryReviewDrawerSource.innerHTML = [
item.source_id ? `来源ID:${escapeHtml(item.source_id)}` : "",
item.saved_path ? `保存文件:${escapeHtml(item.saved_path)}` : "",
item.batch_id ? `批次:${escapeHtml(item.batch_id)}` : "",
].filter(Boolean).join("
") || "暂无来源信息";
summaryReviewDrawerApprove.disabled = !canReview;
summaryReviewDrawerReject.disabled = !canReview;
summaryReviewDrawerBackdrop.hidden = false;
}
function closeSummaryReviewDrawer() {
activeSummaryReview = null;
summaryReviewDrawerBackdrop.hidden = true;
}
async function loadSummaryReviews() {
summaryReviewRows.innerHTML = `| 正在读取 |
`;
const params = new URLSearchParams({ type: "course_summary_review" });
if (summaryReviewStatus.value) params.set("status", summaryReviewStatus.value);
try {
const data = await fetchJson(`/api/admin/tasks?${params.toString()}`);
currentSummaryReviews = data.items || [];
summaryReviewMeta.innerHTML = [metric("当前结果", `${data.count} 条`)].join("");
summaryReviewRows.innerHTML = currentSummaryReviews
.map((item) => {
const canReview = canReviewSummary(item);
const summary = item.summary || {};
const reasons = Array.isArray(item.reasons) ? item.reasons : [];
return `
| #${escapeHtml(item.id)} |
${escapeHtml(item.status)}${item.message ? ` ${escapeHtml(item.message)}` : ""} |
${renderSummaryInfo(summary, item)} |
${item.proposed_line ? renderReviewLine(item.proposed_line) : "暂无"} |
${renderSummaryPreview(summary)} |
${reasons.map(escapeHtml).join(" ") || "待人工复核"} |
|
`;
})
.join("");
if (!currentSummaryReviews.length) {
summaryReviewRows.innerHTML = `| 没有符合条件的课程小结审核项 |
`;
}
} catch (error) {
currentSummaryReviews = [];
summaryReviewRows.innerHTML = `| 读取失败:${escapeHtml(error.message)} |
`;
}
}
function summarySearchParams() {
const params = new URLSearchParams({ limit: "200" });
if (summarySearchQuery.value.trim()) params.set("q", summarySearchQuery.value.trim());
if (summarySearchStudent.value.trim()) params.set("student", summarySearchStudent.value.trim());
if (summarySearchTeacher.value.trim()) params.set("teacher", summarySearchTeacher.value.trim());
if (summarySearchSubject.value.trim()) params.set("subject", summarySearchSubject.value.trim());
if (summarySearchDateFrom.value) params.set("date_from", summarySearchDateFrom.value);
if (summarySearchDateTo.value) params.set("date_to", summarySearchDateTo.value);
return params;
}
function renderSummarySearchBody(item) {
const fullId = `summary-full-${item.id}`;
const hasFull = String(item.body || "") !== String(item.body_preview || "");
return `${escapeHtml(item.body_preview || "")}
${escapeHtml(item.body || "")}
`;
}
function renderMatchedFields(item) {
const fields = Array.isArray(item.matched_fields) ? item.matched_fields : [];
return fields.length ? `
命中:${fields.map(escapeHtml).join("、")}` : "";
}
async function loadSummarySearch() {
summarySearchRows.innerHTML = `| 正在读取 |
`;
try {
const data = await fetchJson(`/api/admin/course-summaries?${summarySearchParams().toString()}`);
summarySearchMeta.innerHTML = [
metric("命中小结", `${data.count} 条`),
metric("当前显示", `${data.returned} 条`),
].join("");
summarySearchRows.innerHTML = data.items
.map((item) => `
| ${escapeHtml(item.date_iso || "未识别")} |
${escapeHtml(item.student || "")} ${escapeHtml(item.group || "")} |
${escapeHtml(item.teacher || "待核对老师")} ${escapeHtml(item.subject || "待核对科目")} |
${escapeHtml(item.title || "")}${renderMatchedFields(item)} |
${renderSummarySearchBody(item)} |
${escapeHtml(item.relative_path || "")} |
`)
.join("");
if (!data.items.length) {
summarySearchRows.innerHTML = `| 没有符合条件的课程小结 |
`;
}
} catch (error) {
summarySearchRows.innerHTML = `| 读取失败:${escapeHtml(error.message)} |
`;
}
}
function renderLogDetail(item) {
const details = [];
if (item.source_id) details.push(`来源:${item.source_id}`);
if (item.teacher || item.subject) details.push(`老师/科目:${item.teacher || ""} ${item.subject || ""}`.trim());
if (item.proposed_line) details.push(`记录:${item.proposed_line}`);
if (Array.isArray(item.reasons) && item.reasons.length) details.push(`原因:${item.reasons.join(";")}`);
if (item.error) details.push(`错误:${item.error}`);
if (item.backup_id) details.push(`备份:${item.backup_id}`);
if (item.saved_path) details.push(`文件:${item.saved_path}`);
return `${details.map(escapeHtml).join("
") || "暂无详情"}
`;
}
async function loadOperationLogs() {
logRows.innerHTML = `| 正在读取 |
`;
const params = new URLSearchParams({ limit: "200" });
if (logOperation.value) params.set("operation", logOperation.value);
if (logStatus.value) params.set("status", logStatus.value);
if (logStudent.value.trim()) params.set("student", logStudent.value.trim());
try {
const data = await fetchJson(`/api/admin/operation-logs?${params.toString()}`);
logMeta.innerHTML = [metric("当前结果", `${data.count} 条`)].join("");
logRows.innerHTML = data.items
.map((item) => `
| ${escapeHtml(item.created_at || "")} |
${escapeHtml(item.operation || "")} |
${escapeHtml(item.status || "")} |
${escapeHtml(item.student || "")} |
${escapeHtml(item.batch_id || "")}${item.task_id ? ` 任务 #${escapeHtml(item.task_id)}` : ""} |
${renderLogDetail(item)} |
`)
.join("");
if (!data.items.length) {
logRows.innerHTML = `| 没有符合条件的操作记录 |
`;
}
} catch (error) {
logRows.innerHTML = `| 读取失败:${escapeHtml(error.message)} |
`;
}
}
async function runTaskAction(taskId, action, reload) {
try {
await fetchJson(`/api/admin/tasks/${encodeURIComponent(taskId)}/${action}`, { method: "POST" });
} catch (error) {
alert(error.message);
}
reload();
}
async function reviewTask(taskId, action) {
runTaskAction(taskId, action, loadReviews);
}
async function summaryReviewTask(taskId, action) {
await runTaskAction(taskId, action, loadSummaryReviews);
closeSummaryReviewDrawer();
}
function linesFromTextarea(textarea) {
return textarea.value
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
}
async function submitRegister(event, textarea, statusNode, url) {
event.preventDefault();
statusNode.textContent = "正在提交";
try {
const data = await fetchJson(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ lines: linesFromTextarea(textarea) }),
});
statusNode.textContent = `已登记 ${data.registered} 条;备份 ${data.backup_id}`;
textarea.value = "";
await loadAdminHealth();
await loadAccounts();
} catch (error) {
statusNode.textContent = `提交失败:${error.message}`;
}
}
function addSummaryRegisterItem(value = "") {
summaryRegisterItemSeq += 1;
const itemId = `summary-register-${summaryRegisterItemSeq}`;
const item = document.createElement("div");
item.className = "summary-register-item";
item.innerHTML = `
`;
summaryRegisterItems.appendChild(item);
}
function inferSummaryField(body, field) {
const patterns = {
student: /(?:学生|学员)[::]\s*([^\n;;,,]+)/,
date: /(?:日期|上课日期)[::]\s*(\d{4}[./-]\d{1,2}[./-]\d{1,2})|(\d{4}[./-]\d{1,2}[./-]\d{1,2})/,
time: /(?:时间|上课时间)[::]\s*(\d{1,2}:\d{2}-\d{1,2}:\d{2})|(\d{1,2}:\d{2}-\d{1,2}:\d{2})/,
teacher: /(?:老师|教师)[::]\s*([^\n;;,,]+)|([\u4e00-\u9fa5A-Za-z0-9]{1,8}老师)/,
subject: /(?:科目|课程)[::]\s*([^\n;;,,]+)|(数学|语文|英语|物理|化学|生物|历史|地理|政治|道法)/,
};
const match = String(body || "").match(patterns[field]);
if (!match) return "";
return (match[1] || match[2] || "").trim();
}
function summaryRegisterItemPayload(item) {
const body = item.querySelector("[data-summary-field='body']").value.trim();
const values = { body };
SUMMARY_REQUIRED_FIELDS.forEach(([field]) => {
const input = item.querySelector(`[data-summary-field='${field}']`);
values[field] = input.value.trim() || inferSummaryField(body, field);
if (!input.value.trim() && values[field]) input.value = values[field];
});
const missing = SUMMARY_REQUIRED_FIELDS.filter(([field]) => !values[field]).map(([, label]) => label);
const missingNode = item.querySelector(".summary-register-missing");
missingNode.hidden = missing.length === 0;
missingNode.textContent = missing.length ? `请补齐:${missing.join("、")};无法补齐请删除本框,本条不会提交。` : "";
item.classList.toggle("has-missing", missing.length > 0);
if (!body || missing.length) return null;
return [
`学生:${values.student}`,
`日期:${values.date}`,
`时间:${values.time}`,
`老师:${values.teacher}`,
`科目:${values.subject}`,
"小结:",
body,
].join("\n");
}
function summaryRegisterLines() {
const items = Array.from(summaryRegisterItems.querySelectorAll(".summary-register-item"));
const lines = items.map(summaryRegisterItemPayload).filter(Boolean);
return {
lines,
blocked: items.some((item) => item.classList.contains("has-missing")),
};
}
async function submitSummaryRegister(event) {
event.preventDefault();
const { lines, blocked } = summaryRegisterLines();
if (blocked) {
summaryRegisterStatus.textContent = "还有课程小结缺字段,请补齐后再提交;无法补齐的请删除本框。";
return;
}
if (!lines.length) {
summaryRegisterStatus.textContent = "请至少填写一条课程小结";
return;
}
summaryRegisterStatus.textContent = "正在提交";
try {
const data = await fetchJson("/api/register/course-summaries", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ lines }),
});
summaryRegisterStatus.textContent = `已接收 ${data.received} 条;自动登记 ${data.auto_registered} 条;重复 ${data.duplicates} 条;舍弃 ${data.rejected} 条`;
summaryRegisterItems.innerHTML = "";
addSummaryRegisterItem();
await loadAdminHealth();
await loadAccounts();
} catch (error) {
summaryRegisterStatus.textContent = `提交失败:${error.message}`;
}
}
document.querySelectorAll("[data-admin-tab]").forEach((button) => {
button.addEventListener("click", () => setActiveTab(button.dataset.adminTab));
});
accountForm.addEventListener("submit", (event) => {
event.preventDefault();
loadAccounts();
});
accountStatus.addEventListener("change", loadAccounts);
newAccountBtn.addEventListener("click", openNewAccountEditor);
cancelAccountEditBtn.addEventListener("click", () => {
accountEditor.hidden = true;
});
accountEditForm.addEventListener("submit", saveAccount);
accountRows.addEventListener("click", (event) => {
const button = event.target.closest(".account-edit");
if (!button) return;
openEditAccountEditor(button.dataset.studentId);
});
reviewStatus.addEventListener("change", loadReviews);
reviewRows.addEventListener("click", (event) => {
const approve = event.target.closest(".review-approve");
const reject = event.target.closest(".review-reject");
if (approve) reviewTask(approve.dataset.taskId, "approve");
if (reject) reviewTask(reject.dataset.taskId, "reject");
});
summaryReviewStatus.addEventListener("change", loadSummaryReviews);
summaryReviewRows.addEventListener("click", (event) => {
const detail = event.target.closest(".summary-detail");
const approve = event.target.closest(".summary-approve");
const reject = event.target.closest(".summary-reject");
if (detail) {
openSummaryReviewDrawer(detail.dataset.taskId);
return;
}
if (approve) {
summaryReviewTask(approve.dataset.taskId, "approve");
return;
}
if (reject) summaryReviewTask(reject.dataset.taskId, "reject");
});
summaryReviewDrawerClose.addEventListener("click", closeSummaryReviewDrawer);
summaryReviewDrawerBackdrop.addEventListener("click", (event) => {
if (event.target === summaryReviewDrawerBackdrop) closeSummaryReviewDrawer();
});
summaryReviewDrawerApprove.addEventListener("click", () => {
if (activeSummaryReview) summaryReviewTask(activeSummaryReview.id, "approve");
});
summaryReviewDrawerReject.addEventListener("click", () => {
if (activeSummaryReview) summaryReviewTask(activeSummaryReview.id, "reject");
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !summaryReviewDrawerBackdrop.hidden) closeSummaryReviewDrawer();
});
summarySearchForm.addEventListener("submit", (event) => {
event.preventDefault();
loadSummarySearch();
});
summarySearchRows.addEventListener("click", (event) => {
const button = event.target.closest(".summary-toggle");
if (!button) return;
const target = document.getElementById(button.dataset.target);
if (!target) return;
target.hidden = !target.hidden;
button.textContent = target.hidden ? "展开" : "收起";
});
logOperation.addEventListener("change", loadOperationLogs);
logStatus.addEventListener("change", loadOperationLogs);
logFilterForm.addEventListener("submit", (event) => {
event.preventDefault();
loadOperationLogs();
});
classRegisterForm.addEventListener("submit", (event) => {
submitRegister(event, classRegisterLines, classRegisterStatus, "/api/register/class-records");
});
paymentRegisterForm.addEventListener("submit", (event) => {
submitRegister(event, paymentRegisterLines, paymentRegisterStatus, "/api/register/payments");
});
addSummaryRegisterItemBtn.addEventListener("click", () => addSummaryRegisterItem());
summaryRegisterItems.addEventListener("click", (event) => {
const button = event.target.closest(".summary-register-remove");
if (!button) return;
const items = summaryRegisterItems.querySelectorAll(".summary-register-item");
if (items.length <= 1) {
const textarea = button.closest(".summary-register-item").querySelector("textarea");
if (textarea) textarea.value = "";
return;
}
button.closest(".summary-register-item").remove();
});
summaryRegisterForm.addEventListener("submit", submitSummaryRegister);
refreshBtn.addEventListener("click", () => {
loadAdminHealth();
if (!panels.accounts.hidden) loadAccounts();
if (!panels.reviews.hidden) loadReviews();
if (!panels.summaries.hidden) loadSummaryReviews();
if (!panels.summarySearch.hidden) loadSummarySearch();
if (!panels.logs.hidden) loadOperationLogs();
});
loadAdminHealth();
addSummaryRegisterItem();
loadAccounts();