From 771220c9d6c9393f8aadc13d307c703268c1d0d2 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 15 Jun 2026 15:11:12 +0800 Subject: [PATCH] feat: improve summary review layout --- app/app/static/admin.html | 46 ++++++++++- app/app/static/admin.js | 129 ++++++++++++++++++++++++++--- app/app/static/styles.css | 166 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 327 insertions(+), 14 deletions(-) diff --git a/app/app/static/admin.html b/app/app/static/admin.html index ba4bf34..951820f 100644 --- a/app/app/static/admin.html +++ b/app/app/static/admin.html @@ -4,7 +4,7 @@ 管理后台 - +
@@ -169,6 +169,48 @@ + + - + diff --git a/app/app/static/admin.js b/app/app/static/admin.js index f08edbf..b5c5633 100644 --- a/app/app/static/admin.js +++ b/app/app/static/admin.js @@ -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 `${escapeHtml(label)}`; +} + +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("
")}
${escapeHtml(summary.group || "")}`; + return `${renderReviewTag(summaryReviewKind(item))}${parts.map(escapeHtml).join("
")}
${escapeHtml(summary.group || "")}`; } -function renderSummaryBody(summary) { +function renderSummaryPreview(summary) { const body = String(summary.body || ""); - const preview = body.length > 260 ? `${body.slice(0, 260)}...` : body; - return `
${escapeHtml(preview)}
`; + const preview = body.length > 140 ? `${body.slice(0, 140)}...` : body; + return `
${escapeHtml(preview || "暂无原文")}
`; +} + +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 = `${escapeHtml(item.status || "")}${item.message ? ` ${escapeHtml(item.message)}` : ""}`; + summaryReviewDrawerCourse.innerHTML = courseInfoLines(summary).map(escapeHtml).join("
") || "暂无课程信息"; + summaryReviewDrawerLine.innerHTML = item.proposed_line ? renderReviewLine(item.proposed_line) : "暂无"; + summaryReviewDrawerReasons.innerHTML = reasons.length ? reasons.map(escapeHtml).join("
") : "待人工复核"; + 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("
") || "暂无来源信息"; + 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 ` #${escapeHtml(item.id)} ${escapeHtml(item.status)}${item.message ? `
${escapeHtml(item.message)}` : ""} - ${renderSummaryInfo(summary)} + ${renderSummaryInfo(summary, item)} ${item.proposed_line ? renderReviewLine(item.proposed_line) : "暂无"} - ${renderSummaryBody(summary)} + ${renderSummaryPreview(summary)} ${reasons.map(escapeHtml).join("
") || "待人工复核"}
+
@@ -346,10 +428,11 @@ async function loadSummaryReviews() { `; }) .join(""); - if (!data.items.length) { + if (!currentSummaryReviews.length) { summaryReviewRows.innerHTML = `没有符合条件的课程小结审核项`; } } catch (error) { + currentSummaryReviews = []; summaryReviewRows.innerHTML = `读取失败:${escapeHtml(error.message)}`; } } @@ -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(); diff --git a/app/app/static/styles.css b/app/app/static/styles.css index 30417e0..e0d34e2 100644 --- a/app/app/static/styles.css +++ b/app/app/static/styles.css @@ -393,6 +393,14 @@ textarea:focus { line-height: 1.45; } +.summary-preview { + display: -webkit-box; + max-height: 4.35em; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; +} + .summary-full { max-width: min(720px, 72vw); margin-top: 8px; @@ -560,6 +568,7 @@ td { .record-actions { display: flex; align-items: center; + flex-wrap: wrap; gap: 8px; } @@ -592,6 +601,25 @@ td { font-weight: 700; } +.review-tag { + display: inline-flex; + align-items: center; + width: fit-content; + min-height: 22px; + margin: 0 0 6px; + padding: 0 8px; + border-radius: 999px; + background: #e0f2fe; + color: #075985; + font-size: 12px; + font-weight: 700; +} + +.review-tag.history { + background: #fef0c7; + color: var(--warn); +} + .note-cell { color: var(--muted); white-space: normal; @@ -638,6 +666,126 @@ td { color: #475467; } +.drawer-backdrop { + position: fixed; + inset: 0; + z-index: 30; + display: flex; + justify-content: flex-end; + background: rgba(16, 24, 40, 0.38); +} + +.drawer-backdrop[hidden] { + display: none; +} + +.summary-review-drawer { + display: flex; + flex-direction: column; + width: min(720px, 100%); + max-height: 100vh; + border-left: 1px solid var(--line); + background: var(--panel); + box-shadow: -20px 0 48px rgba(16, 24, 40, 0.2); +} + +.drawer-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + padding: 18px 20px; + border-bottom: 1px solid var(--line); +} + +.drawer-head h2 { + margin: 0 0 6px; +} + +.drawer-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + color: var(--muted); + font-size: 13px; +} + +.drawer-content { + display: grid; + gap: 14px; + padding: 18px 20px; + overflow: auto; +} + +.drawer-section { + display: grid; + gap: 7px; +} + +.drawer-label { + color: var(--muted); + font-size: 12px; + font-weight: 700; +} + +.drawer-text, +.drawer-body { + white-space: pre-wrap; + overflow-wrap: anywhere; + color: #344054; + font-size: 14px; + line-height: 1.55; +} + +.drawer-body { + padding: 12px; + border: 1px solid var(--line); + border-radius: 6px; + background: #fafbfc; +} + +.drawer-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 14px 20px; + border-top: 1px solid var(--line); + background: #fff; +} + +.drawer-actions button { + min-height: 38px; + padding: 0 16px; + border: 0; + border-radius: 6px; + background: var(--accent); + color: #fff; + cursor: pointer; + font-weight: 700; +} + +.drawer-actions .secondary-button { + border: 1px solid var(--line); + background: #fff; + color: var(--text); +} + +.drawer-actions button:hover { + background: var(--accent-strong); +} + +.drawer-actions .secondary-button:hover { + border-color: var(--accent); + background: #fff; + color: var(--accent-strong); +} + +.drawer-actions button:disabled { + cursor: not-allowed; + opacity: 0.55; +} + .modal-backdrop { position: fixed; inset: 0; @@ -851,6 +999,24 @@ td { width: 100%; } + .drawer-backdrop { + padding-left: 0; + } + + .summary-review-drawer { + width: 100%; + } + + .drawer-head, + .drawer-actions { + align-items: stretch; + flex-direction: column; + } + + .drawer-actions button { + width: 100%; + } + .inline-account-head { align-items: flex-start; flex-direction: column;