1008 lines
42 KiB
JavaScript
1008 lines
42 KiB
JavaScript
const adminHealthText = document.querySelector("#adminHealthText");
|
||
const refreshBtn = document.querySelector("#refreshBtn");
|
||
const panels = {
|
||
accounts: document.querySelector("#accountsPanel"),
|
||
teachers: document.querySelector("#teachersPanel"),
|
||
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 teacherForm = document.querySelector("#teacherForm");
|
||
const teacherQuery = document.querySelector("#teacherQuery");
|
||
const teacherStatus = document.querySelector("#teacherStatus");
|
||
const teacherMeta = document.querySelector("#teacherMeta");
|
||
const teacherRows = document.querySelector("#teacherRows");
|
||
const teacherEditor = document.querySelector("#teacherEditor");
|
||
const teacherEditorTitle = document.querySelector("#teacherEditorTitle");
|
||
const teacherEditForm = document.querySelector("#teacherEditForm");
|
||
const teacherEditError = document.querySelector("#teacherEditError");
|
||
const cancelTeacherEditBtn = document.querySelector("#cancelTeacherEditBtn");
|
||
const newTeacherBtn = document.querySelector("#newTeacherBtn");
|
||
const editTeacherId = document.querySelector("#editTeacherId");
|
||
const editTeacherName = document.querySelector("#editTeacherName");
|
||
const editTeacherAlias = document.querySelector("#editTeacherAlias");
|
||
const editTeacherStatus = document.querySelector("#editTeacherStatus");
|
||
const editTeacherSubjects = document.querySelector("#editTeacherSubjects");
|
||
const editTeacherNote = document.querySelector("#editTeacherNote");
|
||
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 summarySearchMissingTime = document.querySelector("#summarySearchMissingTime");
|
||
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 currentTeachers = [];
|
||
let editingTeacherId = "";
|
||
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 `<div class="metric"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
|
||
}
|
||
|
||
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("<br>");
|
||
}
|
||
|
||
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 === "teachers") loadTeachers();
|
||
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");
|
||
const teacherText = data.teachers_count === undefined ? "" : `;老师 ${data.teachers_count} 位`;
|
||
adminHealthText.textContent = `账户 ${data.accounts_count} 人${teacherText};数据更新时间 ${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 = `<tr><td colspan="6" class="empty">正在读取</td></tr>`;
|
||
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) => `<tr>
|
||
<td>${escapeHtml(row.student)}<br><small>${escapeHtml(row.student_id)}</small></td>
|
||
<td><span class="status ${statusClass(row.account_status)}">${escapeHtml(row.account_status)}</span></td>
|
||
<td class="num">${fmtHours(row.remaining)}</td>
|
||
<td>${renderPayments(row.payments)}</td>
|
||
<td class="note-cell">${escapeHtml(row.note || "")}</td>
|
||
<td><button class="small-button account-edit" type="button" data-student-id="${escapeHtml(row.student_id)}">编辑</button></td>
|
||
</tr>`,
|
||
)
|
||
.join("");
|
||
if (!data.accounts.length) {
|
||
accountRows.innerHTML = `<tr><td colspan="6" class="empty">没有符合条件的账户</td></tr>`;
|
||
}
|
||
} catch (error) {
|
||
accountRows.innerHTML = `<tr><td colspan="6" class="empty">读取失败:${escapeHtml(error.message)}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
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 renderTeacherSummary(rows) {
|
||
const activeCount = rows.filter((item) => item.status === "在岗").length;
|
||
const aliasCount = rows.filter((item) => String(item.alias || "").trim()).length;
|
||
teacherMeta.innerHTML = [
|
||
metric("当前结果", `${rows.length} 位`),
|
||
metric("在岗", activeCount),
|
||
metric("已配置别名", aliasCount),
|
||
].join("");
|
||
}
|
||
|
||
function teacherMatches(row) {
|
||
const keyword = teacherQuery.value.trim();
|
||
if (teacherStatus.value && row.status !== teacherStatus.value) return false;
|
||
if (!keyword) return true;
|
||
const haystack = [
|
||
row.teacher_id,
|
||
row.name,
|
||
row.alias,
|
||
row.display_name,
|
||
...(row.subjects || []),
|
||
row.status,
|
||
row.note,
|
||
].join("\n");
|
||
return haystack.includes(keyword);
|
||
}
|
||
|
||
async function loadTeachers() {
|
||
teacherRows.innerHTML = `<tr><td colspan="6" class="empty">正在读取</td></tr>`;
|
||
try {
|
||
const data = await fetchJson("/api/admin/teachers");
|
||
currentTeachers = data.teachers || [];
|
||
const rows = currentTeachers.filter(teacherMatches);
|
||
renderTeacherSummary(rows);
|
||
teacherRows.innerHTML = rows
|
||
.map(
|
||
(row) => `<tr>
|
||
<td>${escapeHtml(row.name)}<br><small>${escapeHtml(row.teacher_id)}</small></td>
|
||
<td>${escapeHtml(row.alias || "")}</td>
|
||
<td>${escapeHtml((row.subjects || []).join("、"))}</td>
|
||
<td><span class="status ${row.status === "在岗" ? "normal" : "closed"}">${escapeHtml(row.status)}</span></td>
|
||
<td class="note-cell">${escapeHtml(row.note || "")}</td>
|
||
<td><button class="small-button teacher-edit" type="button" data-teacher-id="${escapeHtml(row.teacher_id)}">编辑</button></td>
|
||
</tr>`,
|
||
)
|
||
.join("");
|
||
if (!rows.length) {
|
||
teacherRows.innerHTML = `<tr><td colspan="6" class="empty">没有符合条件的老师档案</td></tr>`;
|
||
}
|
||
} catch (error) {
|
||
teacherRows.innerHTML = `<tr><td colspan="6" class="empty">读取失败:${escapeHtml(error.message)}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
function openNewTeacherEditor() {
|
||
editingTeacherId = "";
|
||
teacherEditorTitle.textContent = "新增老师";
|
||
editTeacherId.value = "";
|
||
editTeacherId.placeholder = "保存时自动生成";
|
||
editTeacherName.value = "";
|
||
editTeacherAlias.value = "";
|
||
editTeacherSubjects.value = "";
|
||
editTeacherStatus.value = "在岗";
|
||
editTeacherNote.value = "";
|
||
teacherEditError.hidden = true;
|
||
teacherEditor.hidden = false;
|
||
editTeacherName.focus();
|
||
}
|
||
|
||
function openEditTeacherEditor(teacherId) {
|
||
const teacher = currentTeachers.find((item) => item.teacher_id === teacherId);
|
||
if (!teacher) return;
|
||
editingTeacherId = teacher.teacher_id;
|
||
teacherEditorTitle.textContent = `编辑老师:${teacher.name}`;
|
||
editTeacherId.value = teacher.teacher_id;
|
||
editTeacherId.placeholder = "";
|
||
editTeacherName.value = teacher.name;
|
||
editTeacherAlias.value = teacher.alias || "";
|
||
editTeacherSubjects.value = (teacher.subjects || []).join("、");
|
||
editTeacherStatus.value = teacher.status;
|
||
editTeacherNote.value = teacher.note || "";
|
||
teacherEditError.hidden = true;
|
||
teacherEditor.hidden = false;
|
||
editTeacherName.focus();
|
||
}
|
||
|
||
function parseTeacherSubjects(value) {
|
||
return value
|
||
.split(/[、,,\s]+/)
|
||
.map((item) => item.trim())
|
||
.filter(Boolean);
|
||
}
|
||
|
||
function buildTeacherPayload() {
|
||
return {
|
||
teacher_id: editTeacherId.value.trim(),
|
||
name: editTeacherName.value.trim(),
|
||
alias: editTeacherAlias.value.trim(),
|
||
subjects: parseTeacherSubjects(editTeacherSubjects.value),
|
||
status: editTeacherStatus.value,
|
||
note: editTeacherNote.value.trim(),
|
||
};
|
||
}
|
||
|
||
async function saveTeacher(event) {
|
||
event.preventDefault();
|
||
try {
|
||
const payload = buildTeacherPayload();
|
||
const url = editingTeacherId
|
||
? `/api/admin/teachers/${encodeURIComponent(editingTeacherId)}`
|
||
: "/api/admin/teachers";
|
||
const method = editingTeacherId ? "PUT" : "POST";
|
||
await fetchJson(url, {
|
||
method,
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
teacherEditor.hidden = true;
|
||
await loadTeachers();
|
||
} catch (error) {
|
||
teacherEditError.textContent = error.message;
|
||
teacherEditError.hidden = false;
|
||
}
|
||
}
|
||
|
||
function renderReviewLine(line) {
|
||
return `<code class="line-code">${escapeHtml(line)}</code>`;
|
||
}
|
||
|
||
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 `<span class="status closed">删除</span><br><small>${escapeHtml(restored)}</small>`;
|
||
}
|
||
return renderReviewLine(item.corrected_line);
|
||
}
|
||
|
||
async function loadReviews() {
|
||
reviewRows.innerHTML = `<tr><td colspan="7" class="empty">正在读取</td></tr>`;
|
||
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 `<tr>
|
||
<td>#${escapeHtml(item.id)}</td>
|
||
<td><span class="status ${item.status === "approved" ? "normal" : item.status === "rejected" ? "closed" : item.status === "conflict" ? "debt" : "warning"}">${escapeHtml(item.status)}</span>${item.message ? `<br><small>${escapeHtml(item.message)}</small>` : ""}</td>
|
||
<td>${escapeHtml(reviewTaskTypeLabel(item))}</td>
|
||
<td>${renderReviewLine(item.original_line)}</td>
|
||
<td>${renderReviewTarget(item)}</td>
|
||
<td>${escapeHtml(item.created_at || "")}</td>
|
||
<td class="record-action-cell">
|
||
<div class="record-actions">
|
||
<button class="small-button review-approve" type="button" data-task-id="${escapeHtml(item.id)}" ${canReview ? "" : "disabled"}>批准</button>
|
||
<button class="small-button review-reject" type="button" data-task-id="${escapeHtml(item.id)}" ${canReview ? "" : "disabled"}>驳回</button>
|
||
</div>
|
||
</td>
|
||
</tr>`;
|
||
})
|
||
.join("");
|
||
if (!items.length) {
|
||
reviewRows.innerHTML = `<tr><td colspan="7" class="empty">没有符合条件的审核项</td></tr>`;
|
||
}
|
||
} catch (error) {
|
||
reviewRows.innerHTML = `<tr><td colspan="7" class="empty">读取失败:${escapeHtml(error.message)}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
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 `<span class="review-tag history">${escapeHtml(label)}</span>`;
|
||
}
|
||
|
||
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("<br>")}<br><small>${escapeHtml(summary.group || "")}</small>`;
|
||
}
|
||
|
||
function renderSummaryPreview(summary) {
|
||
const body = String(summary.body || "");
|
||
const preview = body.length > 140 ? `${body.slice(0, 140)}...` : body;
|
||
return `<div class="summary-body summary-preview">${escapeHtml(preview || "暂无原文")}</div>`;
|
||
}
|
||
|
||
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 = `<span class="status ${taskStatusClass(item.status)}">${escapeHtml(item.status || "")}</span>${item.message ? ` <small>${escapeHtml(item.message)}</small>` : ""}`;
|
||
summaryReviewDrawerCourse.innerHTML = courseInfoLines(summary).map(escapeHtml).join("<br>") || "暂无课程信息";
|
||
summaryReviewDrawerLine.innerHTML = item.proposed_line ? renderReviewLine(item.proposed_line) : "<span class=\"muted\">暂无</span>";
|
||
summaryReviewDrawerReasons.innerHTML = reasons.length ? reasons.map(escapeHtml).join("<br>") : "待人工复核";
|
||
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("<br>") || "暂无来源信息";
|
||
summaryReviewDrawerApprove.disabled = !canReview;
|
||
summaryReviewDrawerReject.disabled = !canReview;
|
||
summaryReviewDrawerBackdrop.hidden = false;
|
||
}
|
||
|
||
function closeSummaryReviewDrawer() {
|
||
activeSummaryReview = null;
|
||
summaryReviewDrawerBackdrop.hidden = true;
|
||
}
|
||
|
||
async function loadSummaryReviews() {
|
||
summaryReviewRows.innerHTML = `<tr><td colspan="7" class="empty">正在读取</td></tr>`;
|
||
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 `<tr>
|
||
<td>#${escapeHtml(item.id)}</td>
|
||
<td><span class="status ${taskStatusClass(item.status)}">${escapeHtml(item.status)}</span>${item.message ? `<br><small>${escapeHtml(item.message)}</small>` : ""}</td>
|
||
<td>${renderSummaryInfo(summary, item)}</td>
|
||
<td>${item.proposed_line ? renderReviewLine(item.proposed_line) : "<span class=\"muted\">暂无</span>"}</td>
|
||
<td>${renderSummaryPreview(summary)}</td>
|
||
<td>${reasons.map(escapeHtml).join("<br>") || "待人工复核"}</td>
|
||
<td class="record-action-cell">
|
||
<div class="record-actions">
|
||
<button class="small-button summary-detail" type="button" data-task-id="${escapeHtml(item.id)}">查看详情</button>
|
||
<button class="small-button summary-approve" type="button" data-task-id="${escapeHtml(item.id)}" ${canReview ? "" : "disabled"}>批准</button>
|
||
<button class="small-button summary-reject" type="button" data-task-id="${escapeHtml(item.id)}" ${canReview ? "" : "disabled"}>驳回</button>
|
||
</div>
|
||
</td>
|
||
</tr>`;
|
||
})
|
||
.join("");
|
||
if (!currentSummaryReviews.length) {
|
||
summaryReviewRows.innerHTML = `<tr><td colspan="7" class="empty">没有符合条件的课程小结审核项</td></tr>`;
|
||
}
|
||
} catch (error) {
|
||
currentSummaryReviews = [];
|
||
summaryReviewRows.innerHTML = `<tr><td colspan="7" class="empty">读取失败:${escapeHtml(error.message)}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
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);
|
||
if (summarySearchMissingTime.value) params.set("missing_time", "true");
|
||
return params;
|
||
}
|
||
|
||
function renderSummarySearchBody(item) {
|
||
const fullId = `summary-full-${item.id}`;
|
||
const hasFull = String(item.body || "") !== String(item.body_preview || "");
|
||
return `<div class="summary-body">${escapeHtml(item.body_preview || "")}</div>
|
||
<div id="${escapeHtml(fullId)}" class="summary-body summary-full" hidden>${escapeHtml(item.body || "")}</div>
|
||
<button class="small-button summary-toggle" type="button" data-target="${escapeHtml(fullId)}" ${hasFull ? "" : "hidden"}>展开</button>`;
|
||
}
|
||
|
||
function renderSummaryActions(item) {
|
||
if (item.time_range) {
|
||
return `<span class="muted">已完整</span>`;
|
||
}
|
||
const inputId = `summary-time-${item.id}`;
|
||
return `<div class="summary-time-actions">
|
||
<input id="${escapeHtml(inputId)}" class="summary-time-input" data-summary-time-input="${escapeHtml(item.id)}" autocomplete="off" placeholder="08:00-10:00" />
|
||
<button class="small-button summary-time-save" type="button" data-summary-id="${escapeHtml(item.id)}">补齐时间</button>
|
||
<button class="small-button summary-delete" type="button" data-summary-id="${escapeHtml(item.id)}">删除</button>
|
||
</div>`;
|
||
}
|
||
|
||
function renderMatchedFields(item) {
|
||
const fields = Array.isArray(item.matched_fields) ? item.matched_fields : [];
|
||
return fields.length ? `<br><small>命中:${fields.map(escapeHtml).join("、")}</small>` : "";
|
||
}
|
||
|
||
async function loadSummarySearch() {
|
||
summarySearchRows.innerHTML = `<tr><td colspan="7" class="empty">正在读取</td></tr>`;
|
||
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) => `<tr>
|
||
<td>${escapeHtml(item.date_iso || "未识别")}</td>
|
||
<td>${escapeHtml(item.time_range || "缺少时间")}</td>
|
||
<td>${escapeHtml(item.student || "")}<br><small>${escapeHtml(item.group || "")}</small></td>
|
||
<td>${escapeHtml(item.teacher || "待核对老师")}<br><small>${escapeHtml(item.subject || "待核对科目")}</small></td>
|
||
<td>${escapeHtml(item.title || "")}${renderMatchedFields(item)}</td>
|
||
<td>${renderSummarySearchBody(item)}</td>
|
||
<td>${renderSummaryActions(item)}</td>
|
||
</tr>`)
|
||
.join("");
|
||
if (!data.items.length) {
|
||
summarySearchRows.innerHTML = `<tr><td colspan="7" class="empty">没有符合条件的课程小结</td></tr>`;
|
||
}
|
||
} catch (error) {
|
||
summarySearchRows.innerHTML = `<tr><td colspan="7" class="empty">读取失败:${escapeHtml(error.message)}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
async function updateCourseSummaryTime(summaryId, timeRange) {
|
||
await fetchJson(`/api/admin/course-summaries/${encodeURIComponent(summaryId)}/time`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ time_range: timeRange }),
|
||
});
|
||
await loadSummarySearch();
|
||
}
|
||
|
||
async function deleteCourseSummary(summaryId) {
|
||
await fetchJson(`/api/admin/course-summaries/${encodeURIComponent(summaryId)}`, {
|
||
method: "DELETE",
|
||
});
|
||
await loadSummarySearch();
|
||
}
|
||
|
||
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 `<div class="log-detail">${details.map(escapeHtml).join("<br>") || "暂无详情"}</div>`;
|
||
}
|
||
|
||
async function loadOperationLogs() {
|
||
logRows.innerHTML = `<tr><td colspan="6" class="empty">正在读取</td></tr>`;
|
||
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) => `<tr>
|
||
<td>${escapeHtml(item.created_at || "")}</td>
|
||
<td>${escapeHtml(item.operation || "")}</td>
|
||
<td><span class="status ${taskStatusClass(item.status)}">${escapeHtml(item.status || "")}</span></td>
|
||
<td>${escapeHtml(item.student || "")}</td>
|
||
<td>${escapeHtml(item.batch_id || "")}${item.task_id ? `<br><small>任务 #${escapeHtml(item.task_id)}</small>` : ""}</td>
|
||
<td>${renderLogDetail(item)}</td>
|
||
</tr>`)
|
||
.join("");
|
||
if (!data.items.length) {
|
||
logRows.innerHTML = `<tr><td colspan="6" class="empty">没有符合条件的操作记录</td></tr>`;
|
||
}
|
||
} catch (error) {
|
||
logRows.innerHTML = `<tr><td colspan="6" class="empty">读取失败:${escapeHtml(error.message)}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
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 = `<label for="${itemId}">课程小结</label>
|
||
<textarea id="${itemId}" data-summary-field="body" rows="7" placeholder="每个输入框填写一条课程小结">${escapeHtml(value)}</textarea>
|
||
<div class="summary-register-fields">
|
||
<input data-summary-field="student" autocomplete="off" placeholder="学生" />
|
||
<input data-summary-field="date" autocomplete="off" placeholder="日期 2026.06.15" />
|
||
<input data-summary-field="time" autocomplete="off" placeholder="时间 08:00-09:00" />
|
||
<input data-summary-field="teacher" autocomplete="off" placeholder="老师" />
|
||
<input data-summary-field="subject" autocomplete="off" placeholder="科目" />
|
||
</div>
|
||
<p class="summary-register-missing" hidden></p>
|
||
<button class="small-button summary-register-remove" type="button">删除本框</button>`;
|
||
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);
|
||
});
|
||
teacherForm.addEventListener("submit", (event) => {
|
||
event.preventDefault();
|
||
loadTeachers();
|
||
});
|
||
teacherStatus.addEventListener("change", loadTeachers);
|
||
newTeacherBtn.addEventListener("click", openNewTeacherEditor);
|
||
cancelTeacherEditBtn.addEventListener("click", () => {
|
||
teacherEditor.hidden = true;
|
||
});
|
||
teacherEditForm.addEventListener("submit", saveTeacher);
|
||
teacherRows.addEventListener("click", (event) => {
|
||
const button = event.target.closest(".teacher-edit");
|
||
if (!button) return;
|
||
openEditTeacherEditor(button.dataset.teacherId);
|
||
});
|
||
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();
|
||
});
|
||
summarySearchMissingTime.addEventListener("change", loadSummarySearch);
|
||
summarySearchRows.addEventListener("click", (event) => {
|
||
const button = event.target.closest(".summary-toggle");
|
||
const saveTime = event.target.closest(".summary-time-save");
|
||
const deleteSummary = event.target.closest(".summary-delete");
|
||
if (saveTime) {
|
||
const input = summarySearchRows.querySelector(`[data-summary-time-input='${saveTime.dataset.summaryId}']`);
|
||
const timeRange = input ? input.value.trim() : "";
|
||
if (!timeRange) {
|
||
alert("请输入时间段");
|
||
return;
|
||
}
|
||
updateCourseSummaryTime(saveTime.dataset.summaryId, timeRange).catch((error) => alert(error.message));
|
||
return;
|
||
}
|
||
if (deleteSummary) {
|
||
if (!confirm("确认删除这条课程小结?")) return;
|
||
deleteCourseSummary(deleteSummary.dataset.summaryId).catch((error) => alert(error.message));
|
||
return;
|
||
}
|
||
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();
|