feat: improve summary review layout

This commit is contained in:
Codex
2026-06-15 15:11:12 +08:00
parent 6cd1288b18
commit cc46f651dc
3 changed files with 327 additions and 14 deletions
+117 -12
View File
@@ -31,6 +31,18 @@ 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");
@@ -55,6 +67,8 @@ const paymentRegisterStatus = document.querySelector("#paymentRegisterStatus");
let currentAccounts = [];
let editingAccountId = "";
let currentSummaryReviews = [];
let activeSummaryReview = null;
function fmtHours(value) {
return Number(value || 0).toLocaleString("zh-CN", { maximumFractionDigits: 2 });
@@ -301,7 +315,21 @@ async function loadReviews() {
}
}
function renderSummaryInfo(summary) {
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,
@@ -309,13 +337,65 @@ function renderSummaryInfo(summary) {
summary.teacher,
summary.subject,
].filter(Boolean);
return `${parts.map(escapeHtml).join("<br>")}<br><small>${escapeHtml(summary.group || "")}</small>`;
return `${renderReviewTag(summaryReviewKind(item))}${parts.map(escapeHtml).join("<br>")}<br><small>${escapeHtml(summary.group || "")}</small>`;
}
function renderSummaryBody(summary) {
function renderSummaryPreview(summary) {
const body = String(summary.body || "");
const preview = body.length > 260 ? `${body.slice(0, 260)}...` : body;
return `<div class="summary-body">${escapeHtml(preview)}</div>`;
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() {
@@ -324,21 +404,23 @@ async function loadSummaryReviews() {
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 = data.items
summaryReviewRows.innerHTML = currentSummaryReviews
.map((item) => {
const canReview = item.status === "pending" || item.status === "conflict";
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)}</td>
<td>${renderSummaryInfo(summary, item)}</td>
<td>${item.proposed_line ? renderReviewLine(item.proposed_line) : "<span class=\"muted\">暂无</span>"}</td>
<td>${renderSummaryBody(summary)}</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>
@@ -346,10 +428,11 @@ async function loadSummaryReviews() {
</tr>`;
})
.join("");
if (!data.items.length) {
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>`;
}
}
@@ -457,7 +540,8 @@ async function reviewTask(taskId, action) {
}
async function summaryReviewTask(taskId, action) {
runTaskAction(taskId, action, loadSummaryReviews);
await runTaskAction(taskId, action, loadSummaryReviews);
closeSummaryReviewDrawer();
}
function linesFromTextarea(textarea) {
@@ -513,11 +597,32 @@ reviewRows.addEventListener("click", (event) => {
});
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 (approve) summaryReviewTask(approve.dataset.taskId, "approve");
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();