From e5e01b9dbbb4832700f2e9568e14e31a94d58631 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 18 Jun 2026 04:06:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=93=8D=E4=BD=9C=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E8=AF=A6=E6=83=85=E5=B1=95=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app/static/admin.html | 5 +- app/app/static/admin.js | 166 +++++++++++++++++++++++++++++--------- app/app/static/index.html | 2 +- app/app/static/styles.css | 76 ++++++++++++++++- 4 files changed, 204 insertions(+), 45 deletions(-) diff --git a/app/app/static/admin.html b/app/app/static/admin.html index a58d509..cc0f971 100644 --- a/app/app/static/admin.html +++ b/app/app/static/admin.html @@ -4,7 +4,7 @@ 管理后台 - +
@@ -406,7 +406,6 @@ 结果 学生 批次/任务 - 详情 操作 @@ -454,6 +453,6 @@ - + diff --git a/app/app/static/admin.js b/app/app/static/admin.js index f2cd094..94f6476 100644 --- a/app/app/static/admin.js +++ b/app/app/static/admin.js @@ -108,6 +108,8 @@ let currentTeachers = []; let editingTeacherId = ""; let currentSummaryReviews = []; let activeSummaryReview = null; +let currentOperationLogs = []; +let expandedOperationLogId = ""; const registerPreviewState = { class_record: null, payment: null, @@ -852,29 +854,108 @@ async function scanDuplicateSummaries() { } } +function logField(label, value) { + if (value === undefined || value === null || value === "") return ""; + return `
${escapeHtml(label)}${escapeHtml(value)}
`; +} + +function logArrayField(label, values) { + if (!Array.isArray(values) || !values.length) return ""; + return logField(label, values.join(";")); +} + +function renderLogDetailGroup(title, fields) { + const body = fields.filter(Boolean).join(""); + if (!body) return ""; + return `
+

${escapeHtml(title)}

+
${body}
+
`; +} + +function renderLogAssociation(item) { + const parts = []; + if (item.batch_id) parts.push(`批次 ${item.batch_id}`); + if (item.task_id) parts.push(`任务 #${item.task_id}`); + if (!parts.length && item.backup_id) parts.push(`备份 ${item.backup_id}`); + if (!parts.length && item.source_id) parts.push(String(item.source_id)); + if (!parts.length && item.record_id) parts.push(`记录 ${item.record_id}`); + return parts.slice(0, 2).map(escapeHtml).join("
") || '-'; +} + +function renderOperationLogRows(items) { + return items + .map((item) => { + const expanded = String(item.id) === expandedOperationLogId; + const mainRow = ` + ${escapeHtml(item.created_at || "")} + ${escapeHtml(item.operation || "")} + ${escapeHtml(item.status || "")} + ${escapeHtml(item.student || "")} + ${renderLogAssociation(item)} + ${renderLogActions(item)} + `; + const detailRow = expanded + ? ` + ${renderLogDetail(item)} + ` + : ""; + return `${mainRow}${detailRow}`; + }) + .join(""); +} + +function renderCurrentOperationLogs() { + logRows.innerHTML = renderOperationLogRows(currentOperationLogs); + if (!currentOperationLogs.length) { + logRows.innerHTML = `没有符合条件的操作记录`; + } +} + function renderLogDetail(item) { - const details = []; - if (item.task_type) details.push(`类型:${item.task_type}`); - if (item.source_id) details.push(`来源:${item.source_id}`); - if (item.student_id) details.push(`学生ID:${item.student_id}`); - if (item.teacher_id) details.push(`教师ID:${item.teacher_id}`); - if (item.teacher || item.subject) details.push(`老师/科目:${item.teacher || ""} ${item.subject || ""}`.trim()); - if (item.time_range) details.push(`时间:${item.time_range}`); - if (item.proposed_line) details.push(`记录:${item.proposed_line}`); - if (item.summary_id) details.push(`小结ID:${item.summary_id}`); - if (item.scanned_groups !== undefined) details.push(`扫描重复组:${item.scanned_groups}`); - if (item.created_tasks !== undefined) details.push(`新增任务:${item.created_tasks}`); - if (Array.isArray(item.reasons) && item.reasons.length) details.push(`原因:${item.reasons.join(";")}`); - if (item.error) details.push(`错误:${item.error}`); - if (item.backup_id) details.push(`备份:${item.backup_id}`); - if (item.target_log_id) details.push(`撤回记录:${item.target_log_id}`); - if (item.target_backup_id) details.push(`撤回备份:${item.target_backup_id}`); - if (item.rollback_mode) details.push(`撤回方式:${item.rollback_mode}`); - if (Array.isArray(item.removed_lines) && item.removed_lines.length) details.push(`删除记录:${item.removed_lines.join(";")}`); - if (item.restored_hours !== undefined && item.restored_hours !== "") details.push(`返还课时:${item.restored_hours}`); - if (Array.isArray(item.restored_files) && item.restored_files.length) details.push(`恢复文件:${item.restored_files.join(";")}`); - if (item.saved_path) details.push(`文件:${item.saved_path}`); - return `
${details.map(escapeHtml).join("
") || "暂无详情"}
`; + const sections = [ + renderLogDetailGroup("基本信息", [ + logField("记录ID", item.id), + logField("操作时间", item.created_at), + logField("操作", item.operation), + logField("状态", item.status), + logField("学生", item.student), + logField("学生ID", item.student_id), + ]), + renderLogDetailGroup("关联对象", [ + logField("任务类型", item.task_type), + logField("任务号", item.task_id ? `#${item.task_id}` : ""), + logField("来源ID", item.source_id), + logField("记录ID", item.record_id), + logField("课程小结ID", item.summary_id), + logField("批次", item.batch_id), + ]), + renderLogDetailGroup("课程信息", [ + logField("老师", item.teacher), + logField("教师ID", item.teacher_id), + logField("科目", item.subject), + logField("时间段", item.time_range), + logField("上课记录", item.proposed_line), + ]), + renderLogDetailGroup("文件与备份", [ + logField("保存文件", item.saved_path), + logField("备份ID", item.backup_id), + logField("撤回记录", item.target_log_id), + logField("撤回备份", item.target_backup_id), + logField("撤回方式", item.rollback_mode), + logArrayField("恢复文件", item.restored_files), + ]), + renderLogDetailGroup("结果与原因", [ + logArrayField("原因", item.reasons), + logField("错误", item.error), + logField("扫描重复组", item.scanned_groups), + logField("新增任务", item.created_tasks), + logArrayField("删除记录", item.removed_lines), + logField("返还课时", item.restored_hours), + logField("标题", item.heading), + ]), + ].filter(Boolean); + return `
${sections.join("") || '暂无详情'}
`; } function renderLogActions(item) { @@ -888,30 +969,20 @@ function renderLogActions(item) { } async function loadOperationLogs() { - logRows.innerHTML = `正在读取`; + logRows.innerHTML = `正在读取`; const params = new URLSearchParams({ limit: "200" }); if (logOperation.value) params.set("operation", logOperation.value); if (logStatus.value) params.set("status", logStatus.value); if (logStudent.value.trim()) params.set("student", logStudent.value.trim()); try { const data = await fetchJson(`/api/admin/operation-logs?${params.toString()}`); + currentOperationLogs = data.items || []; + if (!currentOperationLogs.some((item) => String(item.id) === expandedOperationLogId)) expandedOperationLogId = ""; logMeta.innerHTML = [metric("当前结果", `${data.count} 条`)].join(""); - logRows.innerHTML = data.items - .map((item) => ` - ${escapeHtml(item.created_at || "")} - ${escapeHtml(item.operation || "")} - ${escapeHtml(item.status || "")} - ${escapeHtml(item.student || "")} - ${escapeHtml(item.batch_id || "")}${item.task_id ? `
任务 #${escapeHtml(item.task_id)}` : ""} - ${renderLogDetail(item)} - ${renderLogActions(item)} - `) - .join(""); - if (!data.items.length) { - logRows.innerHTML = `没有符合条件的操作记录`; - } + renderCurrentOperationLogs(); } catch (error) { - logRows.innerHTML = `读取失败:${escapeHtml(error.message)}`; + currentOperationLogs = []; + logRows.innerHTML = `读取失败:${escapeHtml(error.message)}`; } } @@ -1268,7 +1339,24 @@ logFilterForm.addEventListener("submit", (event) => { }); logRows.addEventListener("click", (event) => { const rollback = event.target.closest(".log-rollback"); - if (rollback) rollbackOperationLog(rollback.dataset.logId); + if (rollback) { + rollbackOperationLog(rollback.dataset.logId); + return; + } + const row = event.target.closest(".log-row"); + if (!row) return; + if (event.target.closest(".record-action-cell")) return; + expandedOperationLogId = expandedOperationLogId === row.dataset.logId ? "" : row.dataset.logId; + renderCurrentOperationLogs(); +}); +logRows.addEventListener("keydown", (event) => { + if (event.key !== "Enter" && event.key !== " ") return; + const row = event.target.closest(".log-row"); + if (!row) return; + if (event.target.closest(".log-rollback")) return; + event.preventDefault(); + expandedOperationLogId = expandedOperationLogId === row.dataset.logId ? "" : row.dataset.logId; + renderCurrentOperationLogs(); }); classRegisterForm.addEventListener("submit", (event) => { previewRegister(event, "class_record", linesFromTextarea(classRegisterLines), classRegisterStatus); diff --git a/app/app/static/index.html b/app/app/static/index.html index 0c826bd..1576cfb 100644 --- a/app/app/static/index.html +++ b/app/app/static/index.html @@ -4,7 +4,7 @@ 新时空教务管理系统 - +
diff --git a/app/app/static/styles.css b/app/app/static/styles.css index 1dea8ab..e77bd4d 100644 --- a/app/app/static/styles.css +++ b/app/app/static/styles.css @@ -573,7 +573,69 @@ textarea:focus { } .log-detail { - max-width: 460px; + max-width: none; +} + +.log-row { + cursor: pointer; +} + +.log-row:hover td { + background: #f8fafc; +} + +.log-row .record-action-cell { + cursor: default; +} + +.log-detail-row td { + padding: 0; + background: #fbfcfd; +} + +.log-detail-panel { + padding: 14px 16px 16px; + border-top: 1px solid var(--line); +} + +.log-detail-group + .log-detail-group { + margin-top: 12px; +} + +.log-detail-group h3 { + margin: 0 0 8px; + color: var(--accent-strong); + font-size: 13px; + line-height: 1.4; +} + +.log-detail-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px 16px; +} + +.log-detail-field { + display: grid; + gap: 4px; +} + +.log-detail-field span { + color: #52606d; + font-size: 12px; + font-weight: 700; +} + +.log-detail-field code { + white-space: pre-wrap; + overflow-wrap: anywhere; + color: #344054; + font: inherit; + line-height: 1.5; +} + +.log-detail-panel .muted { + color: #98a2b3; } .summary-preview { @@ -849,7 +911,8 @@ td { align-items: center; justify-content: flex-start; gap: 10px; - padding: 12px 16px; + padding: 12px 16px 12px 18px; + border-left: 4px solid #f59e0b; color: var(--muted); font-size: 13px; } @@ -1498,4 +1561,13 @@ td { padding: 12px 14px; grid-template-columns: repeat(2, 8em); } + + .log-detail-panel { + padding: 12px 14px 14px; + } + + .log-detail-grid { + grid-template-columns: 1fr; + gap: 10px; + } }