+ | #${escapeHtml(item.id)} |
+ ${escapeHtml(item.status)}${item.message ? ` ${escapeHtml(item.message)}` : ""} |
+ ${escapeHtml(reviewTaskTypeLabel(item))} |
+ ${renderReviewLine(item.original_line)} |
+ ${renderReviewTarget(item)} |
+ ${escapeHtml(item.created_at || "")} |
+
@@ -765,6 +841,7 @@ async function loadReviews() {
if (!items.length) {
reviewRows.innerHTML = ` | 没有符合条件的审核项 | `;
}
+ renderPager(reviewPager, currentReviewPage, "reviews");
} catch (error) {
reviewRows.innerHTML = ` | 读取失败:${escapeHtml(error.message)} | `;
}
@@ -1044,26 +1121,33 @@ function closeSummaryReviewDrawer() {
summaryReviewDrawerBackdrop.hidden = true;
}
-async function loadSummaryReviews() {
+async function loadSummaryReviews(offset = currentSummaryReviewPage.offset || 0) {
summaryReviewRows.innerHTML = ` | 正在读取 | `;
- const reviewParams = new URLSearchParams({ type: "course_summary_review" });
- const duplicateParams = new URLSearchParams({ type: "course_summary_duplicate_review" });
- if (summaryReviewStatus.value) {
- reviewParams.set("status", summaryReviewStatus.value);
- duplicateParams.set("status", summaryReviewStatus.value);
- }
+ setPagerLoading(summaryReviewPager);
+ const normalizedOffset = Math.max(0, Number(offset || 0));
+ const params = new URLSearchParams({
+ type: "course_summary_review,course_summary_duplicate_review",
+ limit: String(ADMIN_PAGE_SIZE),
+ offset: String(normalizedOffset),
+ });
+ if (summaryReviewStatus.value) params.set("status", summaryReviewStatus.value);
try {
- const [reviewData, duplicateData] = await Promise.all([
- fetchJson(`/api/admin/tasks?${reviewParams.toString()}`),
- fetchJson(`/api/admin/tasks?${duplicateParams.toString()}`),
- ]);
- const reviewItems = reviewData.items || [];
- const duplicateItems = duplicateData.items || [];
- currentSummaryReviews = [...reviewItems, ...duplicateItems].sort((a, b) => Number(b.id || 0) - Number(a.id || 0));
+ const data = await fetchJson(`/api/admin/tasks?${params.toString()}`);
+ currentSummaryReviews = data.items || [];
+ const totalCount = Number(data.count || 0);
+ const typeCounts = data.type_counts || {};
+ currentSummaryReviewPage = {
+ offset: normalizedOffset,
+ limit: Number(data.limit || ADMIN_PAGE_SIZE),
+ total: totalCount,
+ shown: Number(data.returned || currentSummaryReviews.length),
+ hasMore: Boolean(data.has_more),
+ };
summaryReviewMeta.innerHTML = [
- metric("待处理任务", `${currentSummaryReviews.length} 条`),
- metric("课程小结审核", `${reviewItems.length} 条`),
- metric("重复小结", `${duplicateItems.length} 条`),
+ metric("待处理任务", `${totalCount} 条`),
+ metric("当前显示", pageRangeText(currentSummaryReviewPage)),
+ metric("课程小结审核", `${typeCounts.course_summary_review || 0} 条`),
+ metric("重复小结", `${typeCounts.course_summary_duplicate_review || 0} 条`),
].join("");
summaryReviewRows.innerHTML = currentSummaryReviews
.map((item) => {
@@ -1074,14 +1158,14 @@ async function loadSummaryReviews() {
const approveButton = canApproveDirectly
? ` `
: "";
- return `
- | #${escapeHtml(item.id)} |
- ${escapeHtml(item.status)}${item.message ? ` ${escapeHtml(item.message)}` : ""} |
- ${renderSummaryInfo(summary, item)} |
- ${item.proposed_line ? renderReviewLine(item.proposed_line) : "暂无"} |
- ${renderSummaryPreview(summary)} |
- ${renderSummaryReviewReasons(item)} |
-
+ return ` |
+ | #${escapeHtml(item.id)} |
+ ${escapeHtml(item.status)}${item.message ? ` ${escapeHtml(item.message)}` : ""} |
+ ${renderSummaryInfo(summary, item)} |
+ ${item.proposed_line ? renderReviewLine(item.proposed_line) : "暂无"} |
+ ${renderSummaryPreview(summary)} |
+ ${renderSummaryReviewReasons(item)} |
+
${approveButton}
@@ -1094,6 +1178,7 @@ async function loadSummaryReviews() {
if (!currentSummaryReviews.length) {
summaryReviewRows.innerHTML = ` | 没有符合条件的课程小结任务 | `;
}
+ renderPager(summaryReviewPager, currentSummaryReviewPage, "summaryReviews");
} catch (error) {
currentSummaryReviews = [];
summaryReviewRows.innerHTML = ` | 读取失败:${escapeHtml(error.message)} | `;
@@ -1101,7 +1186,7 @@ async function loadSummaryReviews() {
}
function summarySearchParams() {
- const params = new URLSearchParams({ limit: "200" });
+ const params = new URLSearchParams({ limit: String(ADMIN_PAGE_SIZE), offset: String(currentSummarySearchPage.offset || 0) });
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());
@@ -1314,11 +1399,11 @@ function renderSummarySearchRows(items) {
const binding = summaryBinding(item);
const unmatchedClass = binding.status === "matched" ? "" : " unmatched-summary";
const mainRow = `
- | ${escapeHtml(item.date_iso || "未识别")} |
- ${escapeHtml(item.time_range || "缺少时间")} |
- ${escapeHtml(item.student || "")} ${escapeHtml(item.group || "")} |
- ${escapeHtml(item.teacher || "待核对老师")} ${escapeHtml(item.subject || "待核对科目")} |
- ${renderSummaryBindingStatus(item)} |
+ ${escapeHtml(item.date_iso || "未识别")} |
+ ${escapeHtml(item.time_range || "缺少时间")} |
+ ${escapeHtml(item.student || "")} ${escapeHtml(item.group || "")} |
+ ${escapeHtml(item.teacher || "待核对老师")} ${escapeHtml(item.subject || "待核对科目")} |
+ ${renderSummaryBindingStatus(item)} |
`;
const detailRow = expanded
? `
@@ -1350,11 +1435,20 @@ function renderCurrentSummarySearch() {
}
}
-async function loadSummarySearch() {
+async function loadSummarySearch(offset = currentSummarySearchPage.offset || 0) {
+ currentSummarySearchPage.offset = Math.max(0, Number(offset || 0));
summarySearchRows.innerHTML = ` | 正在读取 | `;
+ setPagerLoading(summarySearchPager);
try {
const data = await fetchJson(`/api/admin/course-summaries?${summarySearchParams().toString()}`);
currentSummarySearchItems = data.items || [];
+ currentSummarySearchPage = {
+ offset: data.offset || currentSummarySearchPage.offset || 0,
+ limit: data.limit || ADMIN_PAGE_SIZE,
+ total: data.count || 0,
+ shown: data.returned || currentSummarySearchItems.length,
+ hasMore: Boolean(data.has_more),
+ };
if (!currentSummarySearchItems.some((item) => String(item.id) === expandedSummarySearchId)) expandedSummarySearchId = "";
if (!currentSummarySearchItems.some((item) => String(item.id) === editingSummarySearchId)) editingSummarySearchId = "";
if (!currentSummarySearchItems.some((item) => String(item.id) === editingSummaryIdentityId)) editingSummaryIdentityId = "";
@@ -1367,12 +1461,13 @@ async function loadSummarySearch() {
const bindingSummary = summarizeSummaryBindings(currentSummarySearchItems);
summarySearchMeta.innerHTML = [
metric("命中小结", `${data.count} 条`),
- metric("当前显示", `${data.returned} 条`),
+ metric("当前显示", pageRangeText(currentSummarySearchPage)),
metric("已绑定", `${bindingSummary.matched} 条`),
metric("待处理", `${bindingSummary.pending} 条`),
metric("有候选", `${bindingSummary.withCandidate} 条`),
].join("");
renderCurrentSummarySearch();
+ renderPager(summarySearchPager, currentSummarySearchPage, "summarySearch");
updateQuickMismatchButton();
} catch (error) {
currentSummarySearchItems = [];
@@ -1602,12 +1697,12 @@ function renderOperationLogRows(items) {
.map((item) => {
const expanded = String(item.id) === expandedOperationLogId;
const mainRow = `
- | ${escapeHtml(item.created_at || "")} |
- ${escapeHtml(item.operation || "")} |
- ${escapeHtml(item.status || "")} |
- ${escapeHtml(item.student || "")} |
- ${renderLogAssociation(item)} |
- ${renderLogActions(item)} |
+ ${escapeHtml(item.created_at || "")} |
+ ${escapeHtml(item.operation || "")} |
+ ${escapeHtml(item.status || "")} |
+ ${escapeHtml(item.student || "")} |
+ ${renderLogAssociation(item)} |
+ ${renderLogActions(item)} |
`;
const detailRow = expanded
? `
@@ -1688,18 +1783,31 @@ function renderLogActions(item) {
return `-`;
}
-async function loadOperationLogs() {
+async function loadOperationLogs(offset = currentLogPage.offset || 0) {
+ currentLogPage.offset = Math.max(0, Number(offset || 0));
logRows.innerHTML = ` | 正在读取 | `;
- const params = new URLSearchParams({ limit: "200" });
+ setPagerLoading(logPager);
+ const params = new URLSearchParams({ limit: String(ADMIN_PAGE_SIZE), offset: String(currentLogPage.offset || 0) });
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()}`);
currentOperationLogs = data.items || [];
+ currentLogPage = {
+ offset: data.offset || currentLogPage.offset || 0,
+ limit: data.limit || ADMIN_PAGE_SIZE,
+ total: data.count || 0,
+ shown: data.returned || currentOperationLogs.length,
+ hasMore: Boolean(data.has_more),
+ };
if (!currentOperationLogs.some((item) => String(item.id) === expandedOperationLogId)) expandedOperationLogId = "";
- logMeta.innerHTML = [metric("当前结果", `${data.count} 条`)].join("");
+ logMeta.innerHTML = [
+ metric("当前结果", `${data.count} 条`),
+ metric("当前显示", pageRangeText(currentLogPage)),
+ ].join("");
renderCurrentOperationLogs();
+ renderPager(logPager, currentLogPage, "logs");
} catch (error) {
currentOperationLogs = [];
logRows.innerHTML = ` | 读取失败:${escapeHtml(error.message)} | `;
@@ -2113,14 +2221,14 @@ teacherRows.addEventListener("click", (event) => {
if (!button) return;
openEditTeacherEditor(button.dataset.teacherId);
});
-reviewStatus.addEventListener("change", loadReviews);
+reviewStatus.addEventListener("change", () => loadReviews(0));
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);
+summaryReviewStatus.addEventListener("change", () => loadSummaryReviews(0));
duplicateSummaryScanBtns.forEach((button) => {
button.addEventListener("click", scanDuplicateSummaries);
});
@@ -2167,15 +2275,15 @@ document.addEventListener("keydown", (event) => {
summarySearchForm.addEventListener("submit", (event) => {
event.preventDefault();
quickSummaryMismatchMode = false;
- loadSummarySearch();
+ loadSummarySearch(0);
});
summarySearchBindingStatus.addEventListener("change", () => {
quickSummaryMismatchMode = false;
- loadSummarySearch();
+ loadSummarySearch(0);
});
summarySearchHasCandidate.addEventListener("change", () => {
quickSummaryMismatchMode = false;
- loadSummarySearch();
+ loadSummarySearch(0);
});
quickMismatchBtn.addEventListener("click", () => {
processQuickMismatchItem().catch((error) => {
@@ -2278,11 +2386,11 @@ summarySearchRows.addEventListener("keydown", (event) => {
expandedSummarySearchId = expandedSummarySearchId === row.dataset.summaryId ? "" : row.dataset.summaryId;
renderCurrentSummarySearch();
});
-logOperation.addEventListener("change", loadOperationLogs);
-logStatus.addEventListener("change", loadOperationLogs);
+logOperation.addEventListener("change", () => loadOperationLogs(0));
+logStatus.addEventListener("change", () => loadOperationLogs(0));
logFilterForm.addEventListener("submit", (event) => {
event.preventDefault();
- loadOperationLogs();
+ loadOperationLogs(0);
});
logRows.addEventListener("click", (event) => {
const rollback = event.target.closest(".log-rollback");
@@ -2305,6 +2413,25 @@ logRows.addEventListener("keydown", (event) => {
expandedOperationLogId = expandedOperationLogId === row.dataset.logId ? "" : row.dataset.logId;
renderCurrentOperationLogs();
});
+
+function bindPager(container, getState, loadPage) {
+ if (!container) return;
+ container.addEventListener("click", (event) => {
+ const previous = event.target.closest(".pager-prev");
+ const next = event.target.closest(".pager-next");
+ if (!previous && !next) return;
+ const state = getState();
+ const limit = state.limit || ADMIN_PAGE_SIZE;
+ const nextOffset = Math.max(0, (state.offset || 0) + (previous ? -limit : limit));
+ loadPage(nextOffset);
+ });
+}
+
+bindPager(summarySearchPager, () => currentSummarySearchPage, loadSummarySearch);
+bindPager(summaryReviewPager, () => currentSummaryReviewPage, loadSummaryReviews);
+bindPager(reviewPager, () => currentReviewPage, loadReviews);
+bindPager(logPager, () => currentLogPage, loadOperationLogs);
+
classRegisterForm.addEventListener("submit", (event) => {
previewRegister(event, "class_record", linesFromTextarea(classRegisterLines), classRegisterStatus);
});
@@ -2335,7 +2462,7 @@ refreshBtn.addEventListener("click", () => {
if (!panels.accounts.hidden) loadAccounts();
if (!panels.reviews.hidden) loadReviews();
if (!panels.summarySearch.hidden) refreshSummaryWorkspace({ logs: false });
- if (!panels.logs.hidden) loadOperationLogs();
+ if (!panels.logs.hidden) loadOperationLogs(currentLogPage.offset || 0);
});
loadAdminHealth();
diff --git a/app/app/static/app.js b/app/app/static/app.js
index 077576c..2df6b19 100644
--- a/app/app/static/app.js
+++ b/app/app/static/app.js
@@ -4,6 +4,7 @@ const recordForm = document.querySelector("#recordForm");
const recordQuery = document.querySelector("#recordQuery");
const recordMeta = document.querySelector("#recordMeta");
const recordRows = document.querySelector("#recordRows");
+const recordPager = document.querySelector("#recordPager");
const dateSortBtn = document.querySelector("#dateSortBtn");
const timeSortBtn = document.querySelector("#timeSortBtn");
const inlineAccount = document.querySelector("#inlineAccount");
@@ -42,11 +43,16 @@ const submitSummarySupplementBtn = document.querySelector("#submitSummarySupplem
const WEEKDAYS = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
const COPY_LINE_BREAK = "\r\n";
const SORT_DIRECTIONS = { asc: 1, desc: -1 };
+const RECORD_PAGE_SIZE = 30;
let currentRecords = [];
let recordSort = { date: "asc", time: "asc" };
let currentRecordOrder = [];
let correctedRecords = new Map();
+let correctedRecordOrder = [];
let expandedSummaryRecords = new Set();
+let expandedSummaryBodies = new Set();
+let collapsedTeacherGroups = new Set();
+let currentRecordPage = { query: "", offset: 0, limit: RECORD_PAGE_SIZE, total: 0, shown: 0, hasMore: false };
let activeCorrectionKey = "";
let activeDeleteKey = "";
let activeSummarySupplementKey = "";
@@ -84,6 +90,44 @@ function metricHtml(label, valueHtml) {
return ` ${escapeHtml(label)}${valueHtml} `;
}
+function currentPageNumber(state) {
+ return Math.floor((state.offset || 0) / (state.limit || RECORD_PAGE_SIZE)) + 1;
+}
+
+function totalPageNumber(state) {
+ const limit = state.limit || RECORD_PAGE_SIZE;
+ return Math.max(1, Math.ceil((state.total || 0) / limit));
+}
+
+function renderPager(container, state) {
+ if (!container) return;
+ const total = Number(state.total || 0);
+ const shown = Number(state.shown || 0);
+ const limit = Number(state.limit || RECORD_PAGE_SIZE);
+ const offset = Number(state.offset || 0);
+ if (total <= limit && offset === 0) {
+ container.hidden = true;
+ container.innerHTML = "";
+ return;
+ }
+ const start = shown ? offset + 1 : 0;
+ const end = offset + shown;
+ container.hidden = false;
+ container.innerHTML = `
+ `;
+}
+
+function pageRangeText(state) {
+ const total = Number(state.total || 0);
+ const shown = Number(state.shown || 0);
+ const offset = Number(state.offset || 0);
+ if (!total || !shown) return "0 条";
+ return `${offset + 1}-${offset + shown} 条`;
+}
+
function makeRecordKey(row, index) {
return row.record_id || JSON.stringify([index, row.date, row.time, row.student, row.duration, row.teacher, row.subject]);
}
@@ -207,13 +251,13 @@ function renderRecordRow(row) {
const badge = corrected ? ' 已修改' : "";
const summaryExpanded = expandedSummaryRecords.has(key);
return `
- | ${escapeHtml(displayRow.date)} ${escapeHtml(displayRow.weekday)} |
- ${escapeHtml(displayRow.time)} |
- ${escapeHtml(displayRow.student)} |
- ${escapeHtml(displayRow.teacher)} |
- ${escapeHtml(displayRow.subject)} |
- ${escapeHtml(displayRow.duration)} |
-
+ | ${escapeHtml(displayRow.date)} ${escapeHtml(displayRow.weekday)} |
+ ${escapeHtml(displayRow.time)} |
+ ${escapeHtml(displayRow.student)} |
+ ${escapeHtml(displayRow.teacher)} |
+ ${escapeHtml(displayRow.subject)} |
+ ${escapeHtml(displayRow.duration)} |
+
@@ -223,7 +267,16 @@ function renderRecordRow(row) {
| `;
}
-function renderSummaryEntry(summary) {
+function summaryBodyKey(recordKey, index) {
+ return `${recordKey}:${index}`;
+}
+
+function summaryPreviewText(value) {
+ const text = String(value || "暂无正文");
+ return text.length > 120 ? `${text.slice(0, 120)}...` : text;
+}
+
+function renderSummaryEntry(summary, recordKey, index) {
const title = summary.title || "课程小结";
const time = summary.time_range ? ` · ${summary.time_range}` : "";
const meta = [
@@ -231,10 +284,15 @@ function renderSummaryEntry(summary) {
summary.teacher || "",
summary.subject || "",
].filter(Boolean).join(" · ");
+ const bodyKey = summaryBodyKey(recordKey, index);
+ const body = summary.body || "暂无正文";
+ const expanded = expandedSummaryBodies.has(bodyKey);
+ const canToggle = body.length > 120;
return `
${escapeHtml(title)}${escapeHtml(time)}
${meta ? ` ${escapeHtml(meta)} ` : ""}
- ${escapeHtml(summary.body || "暂无正文")}
+ ${escapeHtml(expanded ? body : summaryPreviewText(body))}
+ ${canToggle ? ` ` : ""}
`;
}
@@ -253,25 +311,31 @@ function renderGroupedRecords(records) {
currentRecordOrder = [];
return groupRecordsByTeacher(records)
.map(
- (group) => `
+ (group) => {
+ const collapsed = collapsedTeacherGroups.has(group.teacher);
+ const rows = collapsed
+ ? ""
+ : sortRecordsForDisplay(group.records)
+ .map((row) => {
+ currentRecordOrder.push(row._recordKey);
+ const summaryCount = Number(row.summary_count || 0);
+ const summaryRow = summaryCount
+ ? `
+ | ${(row.summaries || []).map((summary, index) => renderSummaryEntry(summary, row._recordKey, index)).join("")} |
+ `
+ : renderSummaryMissingRow(row._recordKey, expandedSummaryRecords.has(row._recordKey));
+ return `${renderRecordRow(row)}${summaryRow}`;
+ })
+ .join("");
+ return `
|
-
+
+ ${group.count} 条记录 · ${displayHours(group.totalHours)} · ${collapsed ? "展开" : "收起"}
+
|
- ${sortRecordsForDisplay(group.records)
- .map((row) => {
- currentRecordOrder.push(row._recordKey);
- const summaryCount = Number(row.summary_count || 0);
- const summaryRow = summaryCount
- ? `
- | ${(row.summaries || []).map(renderSummaryEntry).join("")} |
- `
- : renderSummaryMissingRow(row._recordKey, expandedSummaryRecords.has(row._recordKey));
- return `${renderRecordRow(row)}${summaryRow}`;
- })
- .join("")}`,
+ | ${rows}`;
+ },
)
.join("");
}
@@ -289,6 +353,24 @@ function toggleRecordSummary(key) {
toggleSummaryRow(key);
}
+function toggleTeacherGroup(teacher) {
+ if (collapsedTeacherGroups.has(teacher)) {
+ collapsedTeacherGroups.delete(teacher);
+ } else {
+ collapsedTeacherGroups.add(teacher);
+ }
+ renderCurrentRecords();
+}
+
+function toggleSummaryBody(key) {
+ if (expandedSummaryBodies.has(key)) {
+ expandedSummaryBodies.delete(key);
+ } else {
+ expandedSummaryBodies.add(key);
+ }
+ renderCurrentRecords();
+}
+
function statusClass(status) {
if (status === "欠费") return "debt";
if (status === "预警") return "warning";
@@ -349,8 +431,11 @@ function updateCorrectionToolbar(message = "", isError = false) {
function resetCorrections() {
correctedRecords = new Map();
+ correctedRecordOrder = [];
currentRecordOrder = [];
expandedSummaryRecords = new Set();
+ expandedSummaryBodies = new Set();
+ collapsedTeacherGroups = new Set();
activeCorrectionKey = "";
updateCorrectionToolbar();
}
@@ -596,8 +681,10 @@ function saveCorrection() {
const corrected = buildCorrectedRecord(original);
if (buildRecordLine(corrected) === buildRecordLine(original)) {
correctedRecords.delete(activeCorrectionKey);
+ correctedRecordOrder = correctedRecordOrder.filter((key) => key !== activeCorrectionKey);
} else {
correctedRecords.set(activeCorrectionKey, corrected);
+ if (!correctedRecordOrder.includes(activeCorrectionKey)) correctedRecordOrder.push(activeCorrectionKey);
}
recordRows.innerHTML = renderGroupedRecords(currentRecords);
closeCorrectionDialog();
@@ -630,7 +717,7 @@ async function copyTextToClipboard(text) {
}
async function copyCorrectedRecords() {
- const correctedInPageOrder = currentRecordOrder
+ const correctedInPageOrder = correctedRecordOrder
.map((key) => correctedRecords.get(key))
.filter(Boolean);
if (!correctedInPageOrder.length) return;
@@ -644,7 +731,7 @@ async function copyCorrectedRecords() {
}
async function submitCorrectedRecords() {
- const items = currentRecordOrder
+ const items = correctedRecordOrder
.map((key) => {
const corrected = correctedRecords.get(key);
if (!corrected) return null;
@@ -698,40 +785,64 @@ async function loadHealth() {
}
}
-async function queryRecords(query) {
+async function queryRecords(query, options = {}) {
const q = query.trim();
if (!q) return;
+ const offset = Number(options.offset || 0);
recordRows.innerHTML = ` | 正在查询 | `;
recordMeta.innerHTML = "";
+ if (recordPager) {
+ recordPager.hidden = true;
+ recordPager.innerHTML = "";
+ }
currentRecords = [];
- resetCorrections();
- resetRecordSort();
- clearInlineAccount();
+ if (options.reset !== false) {
+ resetCorrections();
+ resetRecordSort();
+ clearInlineAccount();
+ } else {
+ currentRecordOrder = [];
+ expandedSummaryRecords = new Set();
+ expandedSummaryBodies = new Set();
+ collapsedTeacherGroups = new Set();
+ }
try {
- const data = await fetchJson(`/api/records?q=${encodeURIComponent(q)}&limit=500`);
+ const params = new URLSearchParams({ q, limit: String(RECORD_PAGE_SIZE), offset: String(offset) });
+ const data = await fetchJson(`/api/records?${params.toString()}`);
const summary = data.summary;
+ currentRecordPage = {
+ query: q,
+ offset: data.offset || offset,
+ limit: data.limit || RECORD_PAGE_SIZE,
+ total: data.total_records || 0,
+ shown: data.shown_records || (data.records || []).length,
+ hasMore: Boolean(data.has_more),
+ };
recordMeta.innerHTML = [
metric("识别日期", data.query.date_range),
metric("命中记录", `${summary.count} 条`),
+ metric("当前显示", pageRangeText(currentRecordPage)),
metric("总课时", displayHours(summary.total_hours, summary.total_duration)),
metric("授课老师", `${Object.keys(summary.teachers || {}).length} 位`),
].join("");
- if (data.query.students.length === 1) {
+ if (options.reset !== false && data.query.students.length === 1) {
await loadInlineAccount(data.query.students[0]);
}
if (!data.records.length) {
recordRows.innerHTML = ` | 未找到符合条件的上课记录 | `;
+ renderPager(recordPager, currentRecordPage);
return;
}
currentRecords = data.records.map((row, index) => ({
...row,
- _recordIndex: index,
- _recordKey: makeRecordKey(row, index),
+ _recordIndex: currentRecordPage.offset + index,
+ _recordKey: makeRecordKey(row, currentRecordPage.offset + index),
}));
recordRows.innerHTML = renderGroupedRecords(currentRecords);
+ renderPager(recordPager, currentRecordPage);
} catch (error) {
recordRows.innerHTML = ` | 查询失败:${escapeHtml(error.message)} | `;
}
@@ -761,6 +872,8 @@ recordRows.addEventListener("click", (event) => {
const summarySupplementButton = event.target.closest(".summary-supplement");
const editButton = event.target.closest(".correction-edit");
const deleteButton = event.target.closest(".record-delete");
+ const teacherToggle = event.target.closest(".teacher-group-toggle");
+ const summaryToggle = event.target.closest(".summary-toggle");
if (summarySupplementButton) {
openSummarySupplementDialog(summarySupplementButton.dataset.recordKey);
return;
@@ -773,6 +886,14 @@ recordRows.addEventListener("click", (event) => {
openDeleteDialog(deleteButton.dataset.recordKey);
return;
}
+ if (teacherToggle) {
+ toggleTeacherGroup(teacherToggle.dataset.teacher);
+ return;
+ }
+ if (summaryToggle) {
+ toggleSummaryBody(summaryToggle.dataset.summaryBodyKey);
+ return;
+ }
const row = event.target.closest(".record-row");
if (row && !event.target.closest(".record-action-cell")) toggleRecordSummary(row.dataset.recordKey);
});
@@ -787,6 +908,17 @@ recordRows.addEventListener("keydown", (event) => {
toggleRecordSummary(row.dataset.recordKey);
});
+if (recordPager) {
+ recordPager.addEventListener("click", (event) => {
+ const previous = event.target.closest(".pager-prev");
+ const next = event.target.closest(".pager-next");
+ if (!previous && !next) return;
+ const delta = previous ? -currentRecordPage.limit : currentRecordPage.limit;
+ const nextOffset = Math.max(0, currentRecordPage.offset + delta);
+ queryRecords(currentRecordPage.query || recordQuery.value, { offset: nextOffset, reset: false });
+ });
+}
+
[correctionDate, correctionTime, correctionStudent, correctionTeacher, correctionSubject].forEach((input) => {
input.addEventListener("input", updateCorrectionPreview);
});
diff --git a/app/app/static/index.html b/app/app/static/index.html
index 37ad5a6..f6a2b46 100644
--- a/app/app/static/index.html
+++ b/app/app/static/index.html
@@ -73,6 +73,7 @@
+
diff --git a/app/app/static/styles.css b/app/app/static/styles.css
index b56d588..6a19de6 100644
--- a/app/app/static/styles.css
+++ b/app/app/static/styles.css
@@ -938,6 +938,22 @@ textarea:focus {
margin-top: 8px;
}
+.summary-toggle {
+ min-height: 30px;
+ padding: 0 10px;
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ background: #fff;
+ color: var(--accent-strong);
+ cursor: pointer;
+ font-size: 13px;
+ font-weight: 700;
+}
+
+.summary-toggle:hover {
+ border-color: var(--accent);
+}
+
.summary-conflict-list {
display: grid;
gap: 10px;
@@ -1234,6 +1250,35 @@ textarea:focus {
max-height: calc(100vh - 262px);
}
+.pager {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 12px 16px;
+ border-top: 1px solid var(--line);
+ background: #fbfcfd;
+}
+
+.pager-info {
+ min-width: 0;
+ color: #344054;
+ font-size: 13px;
+ font-weight: 700;
+ overflow-wrap: anywhere;
+}
+
+.pager-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 0 0 auto;
+}
+
+.pager-actions .secondary-button {
+ min-width: 76px;
+}
+
table {
width: 100%;
min-width: 720px;
@@ -1310,18 +1355,39 @@ td {
color: var(--accent-strong);
}
+.teacher-group-toggle {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ width: 100%;
+ min-height: 34px;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: var(--accent-strong);
+ cursor: pointer;
+ text-align: left;
+}
+
.teacher-group-title strong {
font-size: 15px;
line-height: 1.25;
}
-.teacher-group-title span {
+.teacher-group-title span,
+.teacher-group-toggle span {
color: #344054;
font-size: 13px;
font-weight: 700;
white-space: nowrap;
}
+.teacher-group-toggle strong {
+ font-size: 15px;
+ line-height: 1.25;
+}
+
.record-row td:nth-child(4) {
color: var(--muted);
}
@@ -1833,6 +1899,130 @@ td {
overscroll-behavior-y: auto;
}
+ .records-panel .table-wrap,
+ #reviewsPanel .table-wrap,
+ #summarySearchPanel .table-wrap,
+ #logsPanel .table-wrap {
+ overflow: visible;
+ }
+
+ .records-panel table,
+ #reviewsPanel table,
+ #summarySearchPanel table,
+ #logsPanel table {
+ min-width: 0;
+ }
+
+ .records-panel thead,
+ #reviewsPanel thead,
+ #summarySearchPanel thead,
+ #logsPanel thead {
+ display: none;
+ }
+
+ .records-panel tbody,
+ .records-panel tr,
+ .records-panel td,
+ #reviewsPanel tbody,
+ #reviewsPanel tr,
+ #reviewsPanel td,
+ #summarySearchPanel tbody,
+ #summarySearchPanel tr,
+ #summarySearchPanel td,
+ #logsPanel tbody,
+ #logsPanel tr,
+ #logsPanel td {
+ display: block;
+ width: 100%;
+ }
+
+ .record-row,
+ .admin-data-row,
+ .summary-search-result-row,
+ .log-row {
+ margin: 10px 12px;
+ overflow: hidden;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: #fff;
+ }
+
+ .record-row td,
+ .admin-data-row td,
+ .summary-search-result-row td,
+ .log-row td {
+ display: grid;
+ grid-template-columns: 82px minmax(0, 1fr);
+ gap: 10px;
+ padding: 9px 12px;
+ border-bottom: 1px solid #eef2f6;
+ white-space: normal;
+ overflow-wrap: anywhere;
+ }
+
+ .record-row td::before,
+ .admin-data-row td::before,
+ .summary-search-result-row td::before,
+ .log-row td::before {
+ content: attr(data-label);
+ color: var(--muted);
+ font-size: 12px;
+ font-weight: 700;
+ }
+
+ .record-row td:last-child,
+ .admin-data-row td:last-child,
+ .summary-search-result-row td:last-child,
+ .log-row td:last-child {
+ border-bottom: 0;
+ }
+
+ .record-row .num {
+ text-align: left;
+ }
+
+ .record-row .record-action-cell,
+ .admin-data-row .record-action-cell,
+ .log-row .record-action-cell {
+ grid-template-columns: 82px minmax(0, 1fr);
+ }
+
+ .record-actions {
+ justify-content: flex-start;
+ }
+
+ .teacher-group,
+ .summary-collapse-row,
+ .summary-search-detail-row,
+ .log-detail-row {
+ display: block;
+ margin: 10px 12px;
+ }
+
+ .teacher-group td,
+ .summary-collapse-row td,
+ .summary-search-detail-row td,
+ .log-detail-row td {
+ display: block;
+ width: 100%;
+ }
+
+ .teacher-group {
+ margin-top: 14px;
+ }
+
+ .teacher-group td {
+ border: 1px solid #b7d8d4;
+ border-radius: 8px;
+ }
+
+ .summary-collapse-row td,
+ .summary-search-detail-row td,
+ .log-detail-row td {
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ }
+
.inline-account-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -1966,6 +2156,15 @@ td {
.admin-tabs {
overflow-x: auto;
gap: 6px;
+ margin: 0 -12px;
+ padding: 0 12px 8px;
+ scroll-padding: 12px;
+ }
+
+ .admin-tabs::after {
+ content: "";
+ flex: 0 0 18px;
+ align-self: stretch;
}
.admin-tab {
@@ -2070,9 +2269,32 @@ td {
gap: 4px;
}
+ .teacher-group-toggle {
+ align-items: flex-start;
+ flex-direction: column;
+ gap: 4px;
+ }
+
.teacher-group-title span {
white-space: normal;
}
+
+ .teacher-group-toggle span {
+ white-space: normal;
+ }
+
+ .pager {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .pager-actions {
+ width: 100%;
+ }
+
+ .pager-actions .secondary-button {
+ flex: 1 1 0;
+ }
}
@media (max-width: 420px) {
|