feat: add summary registration and deletion review

This commit is contained in:
Codex
2026-06-15 16:23:06 +08:00
parent cc46f651dc
commit 854e4942ba
10 changed files with 597 additions and 22 deletions
+86 -10
View File
@@ -64,11 +64,16 @@ 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 currentSummaryReviews = [];
let activeSummaryReview = null;
let summaryRegisterItemSeq = 0;
function fmtHours(value) {
return Number(value || 0).toLocaleString("zh-CN", { maximumFractionDigits: 2 });
@@ -282,21 +287,37 @@ 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="6" class="empty">正在读取</td></tr>`;
const params = new URLSearchParams({ type: "class_record_correction" });
if (reviewStatus.value) params.set("status", reviewStatus.value);
reviewRows.innerHTML = `<tr><td colspan="7" class="empty">正在读取</td></tr>`;
try {
const data = await fetchJson(`/api/admin/tasks?${params.toString()}`);
reviewMeta.innerHTML = [metric("当前结果", `${data.count}`)].join("");
reviewRows.innerHTML = data.items
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>${renderReviewLine(item.corrected_line)}</td>
<td>${renderReviewTarget(item)}</td>
<td>${escapeHtml(item.created_at || "")}</td>
<td class="record-action-cell">
<div class="record-actions">
@@ -307,11 +328,11 @@ async function loadReviews() {
</tr>`;
})
.join("");
if (!data.items.length) {
reviewRows.innerHTML = `<tr><td colspan="6" class="empty">没有符合条件的审核项</td></tr>`;
if (!items.length) {
reviewRows.innerHTML = `<tr><td colspan="7" class="empty">没有符合条件的审核项</td></tr>`;
}
} catch (error) {
reviewRows.innerHTML = `<tr><td colspan="6" class="empty">读取失败:${escapeHtml(error.message)}</td></tr>`;
reviewRows.innerHTML = `<tr><td colspan="7" class="empty">读取失败:${escapeHtml(error.message)}</td></tr>`;
}
}
@@ -569,6 +590,47 @@ async function submitRegister(event, textarea, statusNode, url) {
}
}
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}" rows="7" placeholder="每个输入框填写一条课程小结">${escapeHtml(value)}</textarea>
<button class="small-button summary-register-remove" type="button">删除本框</button>`;
summaryRegisterItems.appendChild(item);
}
function summaryRegisterLines() {
return Array.from(summaryRegisterItems.querySelectorAll("textarea"))
.map((textarea) => textarea.value.trim())
.filter(Boolean);
}
async function submitSummaryRegister(event) {
event.preventDefault();
const lines = summaryRegisterLines();
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.review_pending} 条;重复 ${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));
});
@@ -647,6 +709,19 @@ classRegisterForm.addEventListener("submit", (event) => {
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();
@@ -657,4 +732,5 @@ refreshBtn.addEventListener("click", () => {
});
loadAdminHealth();
addSummaryRegisterItem();
loadAccounts();