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
+12 -2
View File
@@ -128,8 +128,9 @@
<tr>
<th>编号</th>
<th>状态</th>
<th>类型</th>
<th>原记录</th>
<th>修改后</th>
<th>处理内容</th>
<th>提交时间</th>
<th>操作</th>
</tr>
@@ -305,10 +306,19 @@
<button type="submit">提交缴费记录</button>
</div>
</form>
<form id="summaryRegisterForm" class="admin-form">
<h3>课程小结登记</h3>
<div id="summaryRegisterItems" class="summary-register-items"></div>
<button id="addSummaryRegisterItemBtn" class="secondary-button" type="button">再填一条</button>
<p id="summaryRegisterStatus" class="inline-account-loading"></p>
<div class="modal-actions">
<button type="submit">提交课程小结</button>
</div>
</form>
</div>
</section>
</main>
<script src="/static/admin.js?v=20260615-summary-review-drawer"></script>
<script src="/static/admin.js?v=20260615-summary-register-delete"></script>
</body>
</html>
+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();
+63 -3
View File
@@ -23,6 +23,12 @@ const correctionTeacher = document.querySelector("#correctionTeacher");
const correctionSubject = document.querySelector("#correctionSubject");
const closeCorrectionBtn = document.querySelector("#closeCorrectionBtn");
const cancelCorrectionBtn = document.querySelector("#cancelCorrectionBtn");
const deleteDialog = document.querySelector("#deleteDialog");
const deleteOriginal = document.querySelector("#deleteOriginal");
const deleteError = document.querySelector("#deleteError");
const closeDeleteBtn = document.querySelector("#closeDeleteBtn");
const cancelDeleteBtn = document.querySelector("#cancelDeleteBtn");
const confirmDeleteBtn = document.querySelector("#confirmDeleteBtn");
const WEEKDAYS = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
const COPY_LINE_BREAK = "\r\n";
@@ -32,6 +38,7 @@ let recordSort = { date: "asc", time: "asc" };
let currentRecordOrder = [];
let correctedRecords = new Map();
let activeCorrectionKey = "";
let activeDeleteKey = "";
function fmtHours(value) {
return Number(value || 0).toLocaleString("zh-CN", { maximumFractionDigits: 2 });
@@ -185,6 +192,7 @@ function renderRecordRow(row) {
<td class="record-action-cell">
<div class="record-actions">
<button class="small-button correction-edit" type="button" data-record-key="${escapeHtml(key)}">${actionLabel}</button>
<button class="small-button record-delete" type="button" data-record-key="${escapeHtml(key)}">删除</button>
${badge}
</div>
</td>
@@ -385,6 +393,45 @@ function closeCorrectionDialog() {
setCorrectionError("");
}
function setDeleteError(message) {
deleteError.textContent = message;
deleteError.hidden = !message;
}
function openDeleteDialog(key) {
const original = findCurrentRecord(key);
if (!original) return;
activeDeleteKey = key;
deleteOriginal.textContent = `将提交删除审核:${buildRecordLine(original)}`;
setDeleteError("");
deleteDialog.hidden = false;
}
function closeDeleteDialog() {
deleteDialog.hidden = true;
activeDeleteKey = "";
setDeleteError("");
}
async function submitDeleteRecord() {
const original = findCurrentRecord(activeDeleteKey);
if (!original) return;
confirmDeleteBtn.disabled = true;
try {
const data = await fetchJson("/api/deletions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items: [{ original_line: buildRecordLine(original) }] }),
});
closeDeleteDialog();
updateCorrectionToolbar(`已提交 ${data.submitted} 条删除审核`);
} catch (error) {
setDeleteError(`提交失败:${error.message}`);
} finally {
confirmDeleteBtn.disabled = false;
}
}
function saveCorrection() {
const original = findCurrentRecord(activeCorrectionKey);
if (!original) return;
@@ -550,9 +597,13 @@ document.querySelectorAll("[data-query]").forEach((button) => {
});
recordRows.addEventListener("click", (event) => {
const button = event.target.closest(".correction-edit");
if (!button) return;
openCorrectionDialog(button.dataset.recordKey);
const editButton = event.target.closest(".correction-edit");
const deleteButton = event.target.closest(".record-delete");
if (editButton) {
openCorrectionDialog(editButton.dataset.recordKey);
return;
}
if (deleteButton) openDeleteDialog(deleteButton.dataset.recordKey);
});
[correctionDate, correctionTime, correctionStudent, correctionTeacher, correctionSubject].forEach((input) => {
@@ -574,10 +625,19 @@ cancelCorrectionBtn.addEventListener("click", closeCorrectionDialog);
correctionDialog.addEventListener("click", (event) => {
if (event.target === correctionDialog) closeCorrectionDialog();
});
closeDeleteBtn.addEventListener("click", closeDeleteDialog);
cancelDeleteBtn.addEventListener("click", closeDeleteDialog);
confirmDeleteBtn.addEventListener("click", submitDeleteRecord);
deleteDialog.addEventListener("click", (event) => {
if (event.target === deleteDialog) closeDeleteDialog();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !correctionDialog.hidden) {
closeCorrectionDialog();
}
if (event.key === "Escape" && !deleteDialog.hidden) {
closeDeleteDialog();
}
});
copyCorrectedBtn.addEventListener("click", copyCorrectedRecords);
+22 -1
View File
@@ -119,6 +119,27 @@
</section>
</div>
<script src="/static/app.js?v=20260613-admin-review"></script>
<div id="deleteDialog" class="modal-backdrop" hidden>
<section class="correction-modal" role="dialog" aria-modal="true" aria-labelledby="deleteTitle">
<div class="modal-head">
<h2 id="deleteTitle">删除课程记录</h2>
<button id="closeDeleteBtn" class="modal-close" type="button" aria-label="关闭">关闭</button>
</div>
<div class="correction-form">
<p id="deleteOriginal" class="correction-original"></p>
<p id="deleteError" class="correction-error" hidden></p>
<div class="correction-preview">
<span>审核说明</span>
<code>确认后会提交后台审核;审核通过才会删除 classnotes 中的记录,并恢复对应学生课时。</code>
</div>
<div class="modal-actions">
<button id="cancelDeleteBtn" class="secondary-button" type="button">取消</button>
<button id="confirmDeleteBtn" type="button">确认提交删除审核</button>
</div>
</div>
</section>
</div>
<script src="/static/app.js?v=20260615-delete-review"></script>
</body>
</html>
+26 -2
View File
@@ -369,10 +369,30 @@ textarea:focus {
.register-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.summary-register-items {
display: grid;
gap: 10px;
}
.summary-register-item {
display: grid;
gap: 7px;
}
.summary-register-item label {
color: #344054;
font-size: 13px;
font-weight: 700;
}
.summary-register-remove {
justify-self: start;
}
.line-code {
display: block;
max-width: 420px;
@@ -922,7 +942,7 @@ td {
}
.register-grid {
grid-template-columns: 1fr;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@@ -979,6 +999,10 @@ td {
grid-template-columns: 1fr;
}
.register-grid {
grid-template-columns: 1fr;
}
.admin-tabs {
overflow-x: auto;
}