feat: add summary registration and deletion review
This commit is contained in:
+86
-10
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user