588 lines
20 KiB
JavaScript
588 lines
20 KiB
JavaScript
const healthText = document.querySelector("#healthText");
|
||
const refreshBtn = document.querySelector("#refreshBtn");
|
||
const recordForm = document.querySelector("#recordForm");
|
||
const recordQuery = document.querySelector("#recordQuery");
|
||
const recordMeta = document.querySelector("#recordMeta");
|
||
const recordRows = document.querySelector("#recordRows");
|
||
const dateSortBtn = document.querySelector("#dateSortBtn");
|
||
const timeSortBtn = document.querySelector("#timeSortBtn");
|
||
const inlineAccount = document.querySelector("#inlineAccount");
|
||
const correctionToolbar = document.querySelector("#correctionToolbar");
|
||
const correctionStatus = document.querySelector("#correctionStatus");
|
||
const copyCorrectedBtn = document.querySelector("#copyCorrectedBtn");
|
||
const submitCorrectionsBtn = document.querySelector("#submitCorrectionsBtn");
|
||
const correctionDialog = document.querySelector("#correctionDialog");
|
||
const correctionForm = document.querySelector("#correctionForm");
|
||
const correctionOriginal = document.querySelector("#correctionOriginal");
|
||
const correctionPreview = document.querySelector("#correctionPreview");
|
||
const correctionError = document.querySelector("#correctionError");
|
||
const correctionDate = document.querySelector("#correctionDate");
|
||
const correctionTime = document.querySelector("#correctionTime");
|
||
const correctionStudent = document.querySelector("#correctionStudent");
|
||
const correctionTeacher = document.querySelector("#correctionTeacher");
|
||
const correctionSubject = document.querySelector("#correctionSubject");
|
||
const closeCorrectionBtn = document.querySelector("#closeCorrectionBtn");
|
||
const cancelCorrectionBtn = document.querySelector("#cancelCorrectionBtn");
|
||
|
||
const WEEKDAYS = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
|
||
const COPY_LINE_BREAK = "\r\n";
|
||
const SORT_DIRECTIONS = { asc: 1, desc: -1 };
|
||
let currentRecords = [];
|
||
let recordSort = { date: "asc", time: "asc" };
|
||
let currentRecordOrder = [];
|
||
let correctedRecords = new Map();
|
||
let activeCorrectionKey = "";
|
||
|
||
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 makeRecordKey(row, index) {
|
||
return JSON.stringify([index, row.date, row.time, row.student, row.duration, row.teacher, row.subject]);
|
||
}
|
||
|
||
function buildRecordLine(row) {
|
||
return `${row.date}-${row.weekday}-${row.time}-${row.student}-${row.duration}-${row.teacher}-${row.subject}`;
|
||
}
|
||
|
||
function groupRecordsByTeacher(records) {
|
||
const groups = new Map();
|
||
records.forEach((row) => {
|
||
const teacher = row.teacher || "未标注老师";
|
||
if (!groups.has(teacher)) {
|
||
groups.set(teacher, { teacher, count: 0, totalHours: 0, records: [] });
|
||
}
|
||
const group = groups.get(teacher);
|
||
group.count += 1;
|
||
group.totalHours += Number(row.duration_hours || 0);
|
||
group.records.push(row);
|
||
});
|
||
return Array.from(groups.values()).sort((a, b) => {
|
||
if (b.totalHours !== a.totalHours) return b.totalHours - a.totalHours;
|
||
return a.teacher.localeCompare(b.teacher, "zh-CN");
|
||
});
|
||
}
|
||
|
||
function getDisplayRecord(row) {
|
||
return correctedRecords.get(row._recordKey) || row;
|
||
}
|
||
|
||
function normalizeDateForSort(value) {
|
||
const text = String(value || "");
|
||
const match = text.match(/^(\d{4})\.(\d{1,2})\.(\d{1,2})$/);
|
||
if (!match) return text;
|
||
return `${match[1]}.${match[2].padStart(2, "0")}.${match[3].padStart(2, "0")}`;
|
||
}
|
||
|
||
function compareDatesForSort(a, b) {
|
||
return normalizeDateForSort(a).localeCompare(normalizeDateForSort(b), "zh-CN", { numeric: true });
|
||
}
|
||
|
||
function parseStartMinutes(value) {
|
||
const match = String(value || "").match(/^(\d{1,2}):(\d{2})/);
|
||
if (!match) return null;
|
||
return Number(match[1]) * 60 + Number(match[2]);
|
||
}
|
||
|
||
function compareStartTimesForSort(a, b) {
|
||
const minutesA = parseStartMinutes(a);
|
||
const minutesB = parseStartMinutes(b);
|
||
if (minutesA !== null && minutesB !== null) return minutesA - minutesB;
|
||
if (minutesA !== null) return -1;
|
||
if (minutesB !== null) return 1;
|
||
return String(a || "").localeCompare(String(b || ""), "zh-CN", { numeric: true });
|
||
}
|
||
|
||
function compareRecordsForDisplay(a, b) {
|
||
const displayA = getDisplayRecord(a);
|
||
const displayB = getDisplayRecord(b);
|
||
const dateCompare = compareDatesForSort(displayA.date, displayB.date);
|
||
if (dateCompare !== 0) return dateCompare * SORT_DIRECTIONS[recordSort.date];
|
||
|
||
const timeCompare = compareStartTimesForSort(displayA.time, displayB.time);
|
||
if (timeCompare !== 0) return timeCompare * SORT_DIRECTIONS[recordSort.time];
|
||
|
||
return (a._recordIndex || 0) - (b._recordIndex || 0);
|
||
}
|
||
|
||
function sortRecordsForDisplay(records) {
|
||
return [...records].sort(compareRecordsForDisplay);
|
||
}
|
||
|
||
function sortDirectionLabel(direction) {
|
||
return direction === "asc" ? "升序排列" : "降序排列";
|
||
}
|
||
|
||
function sortDirectionName(direction) {
|
||
return sortDirectionLabel(direction);
|
||
}
|
||
|
||
function nextSortDirectionName(direction) {
|
||
return direction === "asc" ? "降序排列" : "升序排列";
|
||
}
|
||
|
||
function updateRecordSortButtons() {
|
||
dateSortBtn.textContent = `日期 ${sortDirectionLabel(recordSort.date)}`;
|
||
dateSortBtn.setAttribute(
|
||
"aria-label",
|
||
`日期排序:${sortDirectionName(recordSort.date)},点击切换为${nextSortDirectionName(recordSort.date)}`,
|
||
);
|
||
timeSortBtn.textContent = `时间 ${sortDirectionLabel(recordSort.time)}`;
|
||
timeSortBtn.setAttribute(
|
||
"aria-label",
|
||
`时间排序:${sortDirectionName(recordSort.time)},点击切换为${nextSortDirectionName(recordSort.time)}`,
|
||
);
|
||
}
|
||
|
||
function resetRecordSort() {
|
||
recordSort = { date: "asc", time: "asc" };
|
||
updateRecordSortButtons();
|
||
}
|
||
|
||
function renderCurrentRecords() {
|
||
if (!currentRecords.length) return;
|
||
recordRows.innerHTML = renderGroupedRecords(currentRecords);
|
||
}
|
||
|
||
function toggleRecordSort(field) {
|
||
recordSort = { ...recordSort, [field]: recordSort[field] === "asc" ? "desc" : "asc" };
|
||
updateRecordSortButtons();
|
||
renderCurrentRecords();
|
||
}
|
||
|
||
function renderRecordRow(row) {
|
||
const key = row._recordKey;
|
||
const corrected = correctedRecords.get(key);
|
||
const displayRow = getDisplayRecord(row);
|
||
const correctedClass = corrected ? " corrected-row" : "";
|
||
const actionLabel = corrected ? "编辑" : "纠错";
|
||
const badge = corrected ? '<span class="correction-badge">已修改</span>' : "";
|
||
return `<tr class="record-row${correctedClass}">
|
||
<td>${escapeHtml(displayRow.date)} ${escapeHtml(displayRow.weekday)}</td>
|
||
<td>${escapeHtml(displayRow.time)}</td>
|
||
<td>${escapeHtml(displayRow.student)}</td>
|
||
<td>${escapeHtml(displayRow.teacher)}</td>
|
||
<td>${escapeHtml(displayRow.subject)}</td>
|
||
<td class="num">${escapeHtml(displayRow.duration)}</td>
|
||
<td class="record-action-cell">
|
||
<div class="record-actions">
|
||
<button class="small-button correction-edit" type="button" data-record-key="${escapeHtml(key)}">${actionLabel}</button>
|
||
${badge}
|
||
</div>
|
||
</td>
|
||
</tr>`;
|
||
}
|
||
|
||
function renderGroupedRecords(records) {
|
||
currentRecordOrder = [];
|
||
return groupRecordsByTeacher(records)
|
||
.map(
|
||
(group) => `<tr class="teacher-group">
|
||
<td colspan="7">
|
||
<div class="teacher-group-title">
|
||
<strong>${escapeHtml(group.teacher)}</strong>
|
||
<span>${group.count} 条记录 · ${fmtHours(group.totalHours)} 小时</span>
|
||
</div>
|
||
</td>
|
||
</tr>${sortRecordsForDisplay(group.records)
|
||
.map((row) => {
|
||
currentRecordOrder.push(row._recordKey);
|
||
return renderRecordRow(row);
|
||
})
|
||
.join("")}`,
|
||
)
|
||
.join("");
|
||
}
|
||
|
||
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) => `${item.date}:${fmtHours(item.hours)} 小时`).join(",");
|
||
}
|
||
|
||
function renderInlineAccount(account) {
|
||
inlineAccount.hidden = false;
|
||
inlineAccount.innerHTML = `<div class="inline-account-head">
|
||
<div>
|
||
<h3>${escapeHtml(account.student)} 课时账户</h3>
|
||
<p>${escapeHtml(account.student_id)}</p>
|
||
</div>
|
||
<span class="status ${statusClass(account.account_status)}">${escapeHtml(account.account_status)}</span>
|
||
</div>
|
||
<div class="inline-account-grid">
|
||
${metric("剩余课时", fmtHours(account.remaining))}
|
||
${metric("缴费次数", account.payments_count)}
|
||
${metric("缴费记录", renderPayments(account.payments))}
|
||
${metric("备注", account.note || "无")}
|
||
</div>`;
|
||
}
|
||
|
||
async function loadInlineAccount(student) {
|
||
inlineAccount.hidden = false;
|
||
inlineAccount.innerHTML = `<div class="inline-account-loading">正在读取 ${escapeHtml(student)} 的课时账户</div>`;
|
||
try {
|
||
const account = await fetchJson(`/api/student-account/${encodeURIComponent(student)}`);
|
||
renderInlineAccount(account);
|
||
} catch (error) {
|
||
inlineAccount.innerHTML = `<div class="inline-account-loading">课时账户读取失败:${escapeHtml(error.message)}</div>`;
|
||
}
|
||
}
|
||
|
||
function clearInlineAccount() {
|
||
inlineAccount.hidden = true;
|
||
inlineAccount.innerHTML = "";
|
||
}
|
||
|
||
function updateCorrectionToolbar(message = "", isError = false) {
|
||
const count = correctedRecords.size;
|
||
correctionToolbar.hidden = count === 0;
|
||
copyCorrectedBtn.disabled = count === 0;
|
||
submitCorrectionsBtn.disabled = count === 0;
|
||
copyCorrectedBtn.textContent = `复制已修改记录 ${count} 条`;
|
||
submitCorrectionsBtn.textContent = `提交审核 ${count} 条`;
|
||
correctionStatus.textContent = message || `已修改 ${count} 条记录`;
|
||
correctionStatus.classList.toggle("is-error", isError);
|
||
}
|
||
|
||
function resetCorrections() {
|
||
correctedRecords = new Map();
|
||
currentRecordOrder = [];
|
||
activeCorrectionKey = "";
|
||
updateCorrectionToolbar();
|
||
}
|
||
|
||
function parseCorrectionDate(value) {
|
||
const match = value.trim().match(/^(\d{4})\.(\d{2})\.(\d{2})$/);
|
||
if (!match) {
|
||
throw new Error("日期格式应为 YYYY.MM.DD,例如 2026.06.10");
|
||
}
|
||
const year = Number(match[1]);
|
||
const month = Number(match[2]);
|
||
const day = Number(match[3]);
|
||
const parsed = new Date(year, month - 1, day);
|
||
if (parsed.getFullYear() !== year || parsed.getMonth() !== month - 1 || parsed.getDate() !== day) {
|
||
throw new Error("日期不存在,请检查年月日");
|
||
}
|
||
return {
|
||
date: `${year}.${String(month).padStart(2, "0")}.${String(day).padStart(2, "0")}`,
|
||
weekday: WEEKDAYS[parsed.getDay()],
|
||
};
|
||
}
|
||
|
||
function parseCorrectionTime(value) {
|
||
const match = value.trim().match(/^([01]?\d|2[0-3]):([0-5]\d)-([01]?\d|2[0-3]):([0-5]\d)$/);
|
||
if (!match) {
|
||
throw new Error("时间格式应为 HH:MM-HH:MM,例如 08:00-10:00");
|
||
}
|
||
const startHour = Number(match[1]);
|
||
const startMinute = Number(match[2]);
|
||
const endHour = Number(match[3]);
|
||
const endMinute = Number(match[4]);
|
||
const startTotal = startHour * 60 + startMinute;
|
||
const endTotal = endHour * 60 + endMinute;
|
||
if (endTotal <= startTotal) {
|
||
throw new Error("结束时间必须晚于开始时间");
|
||
}
|
||
const totalMinutes = endTotal - startTotal;
|
||
const hours = Math.floor(totalMinutes / 60);
|
||
const minutes = totalMinutes % 60;
|
||
return {
|
||
time: `${String(startHour).padStart(2, "0")}:${String(startMinute).padStart(2, "0")}-${String(endHour).padStart(2, "0")}:${String(endMinute).padStart(2, "0")}`,
|
||
duration: `${hours}小时${minutes}分`,
|
||
duration_hours: Math.round((totalMinutes / 60) * 100) / 100,
|
||
};
|
||
}
|
||
|
||
function requireText(value, label) {
|
||
const text = value.trim();
|
||
if (!text) {
|
||
throw new Error(`${label}不能为空`);
|
||
}
|
||
if (text.includes("-")) {
|
||
throw new Error(`${label}不能包含 -`);
|
||
}
|
||
return text;
|
||
}
|
||
|
||
function findCurrentRecord(key) {
|
||
return currentRecords.find((row) => row._recordKey === key);
|
||
}
|
||
|
||
function buildCorrectedRecord(original) {
|
||
const datePart = parseCorrectionDate(correctionDate.value);
|
||
const timePart = parseCorrectionTime(correctionTime.value);
|
||
return {
|
||
...original,
|
||
...datePart,
|
||
...timePart,
|
||
student: requireText(correctionStudent.value, "学生"),
|
||
teacher: requireText(correctionTeacher.value, "老师"),
|
||
subject: requireText(correctionSubject.value, "科目"),
|
||
};
|
||
}
|
||
|
||
function setCorrectionError(message) {
|
||
correctionError.textContent = message;
|
||
correctionError.hidden = !message;
|
||
}
|
||
|
||
function updateCorrectionPreview() {
|
||
const original = findCurrentRecord(activeCorrectionKey);
|
||
if (!original) return;
|
||
try {
|
||
const corrected = buildCorrectedRecord(original);
|
||
correctionPreview.textContent = buildRecordLine(corrected);
|
||
setCorrectionError("");
|
||
} catch (error) {
|
||
correctionPreview.textContent = "请修正上方字段后保存";
|
||
setCorrectionError(error.message);
|
||
}
|
||
}
|
||
|
||
function openCorrectionDialog(key) {
|
||
const original = findCurrentRecord(key);
|
||
if (!original) return;
|
||
const corrected = correctedRecords.get(key) || original;
|
||
activeCorrectionKey = key;
|
||
correctionOriginal.textContent = `原记录:${buildRecordLine(original)}`;
|
||
correctionDate.value = corrected.date;
|
||
correctionTime.value = corrected.time;
|
||
correctionStudent.value = corrected.student;
|
||
correctionTeacher.value = corrected.teacher;
|
||
correctionSubject.value = corrected.subject;
|
||
correctionDialog.hidden = false;
|
||
updateCorrectionPreview();
|
||
correctionDate.focus();
|
||
}
|
||
|
||
function closeCorrectionDialog() {
|
||
correctionDialog.hidden = true;
|
||
activeCorrectionKey = "";
|
||
setCorrectionError("");
|
||
}
|
||
|
||
function saveCorrection() {
|
||
const original = findCurrentRecord(activeCorrectionKey);
|
||
if (!original) return;
|
||
const corrected = buildCorrectedRecord(original);
|
||
if (buildRecordLine(corrected) === buildRecordLine(original)) {
|
||
correctedRecords.delete(activeCorrectionKey);
|
||
} else {
|
||
correctedRecords.set(activeCorrectionKey, corrected);
|
||
}
|
||
recordRows.innerHTML = renderGroupedRecords(currentRecords);
|
||
closeCorrectionDialog();
|
||
updateCorrectionToolbar(`已保存 ${correctedRecords.size} 条修改`);
|
||
}
|
||
|
||
async function copyTextToClipboard(text) {
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
return;
|
||
} catch (_error) {
|
||
// HTTP access on the VPS usually needs the fallback path below.
|
||
}
|
||
}
|
||
const textarea = document.createElement("textarea");
|
||
textarea.value = text;
|
||
textarea.setAttribute("readonly", "");
|
||
textarea.style.position = "fixed";
|
||
textarea.style.left = "-9999px";
|
||
textarea.style.top = "0";
|
||
document.body.appendChild(textarea);
|
||
textarea.focus();
|
||
textarea.select();
|
||
const copied = document.execCommand("copy");
|
||
textarea.remove();
|
||
if (!copied) {
|
||
throw new Error("浏览器拒绝写入剪切板");
|
||
}
|
||
}
|
||
|
||
async function copyCorrectedRecords() {
|
||
const correctedInPageOrder = currentRecordOrder
|
||
.map((key) => correctedRecords.get(key))
|
||
.filter(Boolean);
|
||
if (!correctedInPageOrder.length) return;
|
||
const text = correctedInPageOrder.map(buildRecordLine).join(COPY_LINE_BREAK);
|
||
try {
|
||
await copyTextToClipboard(text);
|
||
updateCorrectionToolbar(`已复制 ${correctedInPageOrder.length} 条修改`);
|
||
} catch (error) {
|
||
updateCorrectionToolbar(`复制失败:${error.message}`, true);
|
||
}
|
||
}
|
||
|
||
async function submitCorrectedRecords() {
|
||
const items = currentRecordOrder
|
||
.map((key) => {
|
||
const original = findCurrentRecord(key);
|
||
const corrected = correctedRecords.get(key);
|
||
if (!original || !corrected) return null;
|
||
return {
|
||
original_line: buildRecordLine(original),
|
||
corrected_line: buildRecordLine(corrected),
|
||
};
|
||
})
|
||
.filter(Boolean);
|
||
if (!items.length) return;
|
||
submitCorrectionsBtn.disabled = true;
|
||
try {
|
||
const data = await fetchJson("/api/corrections", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ items }),
|
||
});
|
||
updateCorrectionToolbar(`已提交 ${data.submitted} 条纠错审核`);
|
||
} catch (error) {
|
||
updateCorrectionToolbar(`提交失败:${error.message}`, true);
|
||
} finally {
|
||
submitCorrectionsBtn.disabled = correctedRecords.size === 0;
|
||
}
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
async function loadHealth() {
|
||
try {
|
||
const data = await fetchJson("/api/health");
|
||
healthText.textContent = `数据更新时间 ${fmtTime(data.classnotes.mtime)}`;
|
||
} catch (error) {
|
||
healthText.textContent = `读取失败:${error.message}`;
|
||
}
|
||
}
|
||
|
||
async function queryRecords(query) {
|
||
const q = query.trim();
|
||
if (!q) return;
|
||
recordRows.innerHTML = `<tr><td colspan="7" class="empty">正在查询</td></tr>`;
|
||
recordMeta.innerHTML = "";
|
||
currentRecords = [];
|
||
resetCorrections();
|
||
resetRecordSort();
|
||
clearInlineAccount();
|
||
try {
|
||
const data = await fetchJson(`/api/records?q=${encodeURIComponent(q)}&limit=500`);
|
||
const summary = data.summary;
|
||
recordMeta.innerHTML = [
|
||
metric("识别日期", data.query.date_range),
|
||
metric("命中记录", `${summary.count} 条`),
|
||
metric("总课时", `${fmtHours(summary.total_hours)} 小时`),
|
||
metric("授课老师", `${Object.keys(summary.teachers || {}).length} 位`),
|
||
].join("");
|
||
|
||
if (data.query.students.length === 1) {
|
||
await loadInlineAccount(data.query.students[0]);
|
||
}
|
||
|
||
if (!data.records.length) {
|
||
recordRows.innerHTML = `<tr><td colspan="7" class="empty">未找到符合条件的上课记录</td></tr>`;
|
||
return;
|
||
}
|
||
|
||
currentRecords = data.records.map((row, index) => ({
|
||
...row,
|
||
_recordIndex: index,
|
||
_recordKey: makeRecordKey(row, index),
|
||
}));
|
||
recordRows.innerHTML = renderGroupedRecords(currentRecords);
|
||
} catch (error) {
|
||
recordRows.innerHTML = `<tr><td colspan="7" class="empty">查询失败:${escapeHtml(error.message)}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
recordForm.addEventListener("submit", (event) => {
|
||
event.preventDefault();
|
||
queryRecords(recordQuery.value);
|
||
});
|
||
|
||
refreshBtn.addEventListener("click", () => {
|
||
loadHealth();
|
||
if (recordQuery.value.trim()) queryRecords(recordQuery.value);
|
||
});
|
||
|
||
dateSortBtn.addEventListener("click", () => toggleRecordSort("date"));
|
||
timeSortBtn.addEventListener("click", () => toggleRecordSort("time"));
|
||
|
||
document.querySelectorAll("[data-query]").forEach((button) => {
|
||
button.addEventListener("click", () => {
|
||
recordQuery.value = button.dataset.query;
|
||
queryRecords(recordQuery.value);
|
||
});
|
||
});
|
||
|
||
recordRows.addEventListener("click", (event) => {
|
||
const button = event.target.closest(".correction-edit");
|
||
if (!button) return;
|
||
openCorrectionDialog(button.dataset.recordKey);
|
||
});
|
||
|
||
[correctionDate, correctionTime, correctionStudent, correctionTeacher, correctionSubject].forEach((input) => {
|
||
input.addEventListener("input", updateCorrectionPreview);
|
||
});
|
||
|
||
correctionForm.addEventListener("submit", (event) => {
|
||
event.preventDefault();
|
||
try {
|
||
saveCorrection();
|
||
} catch (error) {
|
||
setCorrectionError(error.message);
|
||
correctionPreview.textContent = "请修正上方字段后保存";
|
||
}
|
||
});
|
||
|
||
closeCorrectionBtn.addEventListener("click", closeCorrectionDialog);
|
||
cancelCorrectionBtn.addEventListener("click", closeCorrectionDialog);
|
||
correctionDialog.addEventListener("click", (event) => {
|
||
if (event.target === correctionDialog) closeCorrectionDialog();
|
||
});
|
||
document.addEventListener("keydown", (event) => {
|
||
if (event.key === "Escape" && !correctionDialog.hidden) {
|
||
closeCorrectionDialog();
|
||
}
|
||
});
|
||
|
||
copyCorrectedBtn.addEventListener("click", copyCorrectedRecords);
|
||
submitCorrectionsBtn.addEventListener("click", submitCorrectedRecords);
|
||
|
||
updateRecordSortButtons();
|
||
loadHealth();
|