const adminHealthText = document.querySelector("#adminHealthText");
const refreshBtn = document.querySelector("#refreshBtn");
const panels = {
accounts: document.querySelector("#accountsPanel"),
reviews: document.querySelector("#reviewsPanel"),
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 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");
let currentAccounts = [];
let editingAccountId = "";
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 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();
}
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)}`;
}
async function loadReviews() {
reviewRows.innerHTML = `| 正在读取 |
`;
const params = new URLSearchParams({ type: "class_record_correction" });
if (reviewStatus.value) params.set("status", reviewStatus.value);
try {
const data = await fetchJson(`/api/admin/tasks?${params.toString()}`);
reviewMeta.innerHTML = [metric("当前结果", `${data.count} 条`)].join("");
reviewRows.innerHTML = data.items
.map((item) => {
const canReview = item.status === "pending" || item.status === "conflict";
return `
| #${escapeHtml(item.id)} |
${escapeHtml(item.status)}${item.message ? ` ${escapeHtml(item.message)}` : ""} |
${renderReviewLine(item.original_line)} |
${renderReviewLine(item.corrected_line)} |
${escapeHtml(item.created_at || "")} |
|
`;
})
.join("");
if (!data.items.length) {
reviewRows.innerHTML = `| 没有符合条件的审核项 |
`;
}
} catch (error) {
reviewRows.innerHTML = `| 读取失败:${escapeHtml(error.message)} |
`;
}
}
async function reviewTask(taskId, action) {
try {
await fetchJson(`/api/admin/tasks/${encodeURIComponent(taskId)}/${action}`, { method: "POST" });
} catch (error) {
alert(error.message);
}
loadReviews();
}
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}`;
}
}
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");
});
classRegisterForm.addEventListener("submit", (event) => {
submitRegister(event, classRegisterLines, classRegisterStatus, "/api/register/class-records");
});
paymentRegisterForm.addEventListener("submit", (event) => {
submitRegister(event, paymentRegisterLines, paymentRegisterStatus, "/api/register/payments");
});
refreshBtn.addEventListener("click", () => {
loadAdminHealth();
if (!panels.accounts.hidden) loadAccounts();
if (!panels.reviews.hidden) loadReviews();
});
loadAdminHealth();
loadAccounts();