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