优化操作记录详情展开

This commit is contained in:
Codex
2026-06-18 04:06:06 +08:00
parent 9d9500ed36
commit e5e01b9dbb
4 changed files with 204 additions and 45 deletions
+2 -3
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>管理后台</title> <title>管理后台</title>
<link rel="stylesheet" href="/static/styles.css?v=20260616-duration-text" /> <link rel="stylesheet" href="/static/styles.css?v=20260618-log-expand" />
</head> </head>
<body> <body>
<header class="topbar"> <header class="topbar">
@@ -406,7 +406,6 @@
<th>结果</th> <th>结果</th>
<th>学生</th> <th>学生</th>
<th>批次/任务</th> <th>批次/任务</th>
<th>详情</th>
<th>操作</th> <th>操作</th>
</tr> </tr>
</thead> </thead>
@@ -454,6 +453,6 @@
</section> </section>
</main> </main>
<script src="/static/admin.js?v=20260616-rollback-log"></script> <script src="/static/admin.js?v=20260618-log-expand"></script>
</body> </body>
</html> </html>
+127 -39
View File
@@ -108,6 +108,8 @@ let currentTeachers = [];
let editingTeacherId = ""; let editingTeacherId = "";
let currentSummaryReviews = []; let currentSummaryReviews = [];
let activeSummaryReview = null; let activeSummaryReview = null;
let currentOperationLogs = [];
let expandedOperationLogId = "";
const registerPreviewState = { const registerPreviewState = {
class_record: null, class_record: null,
payment: null, payment: null,
@@ -852,29 +854,108 @@ async function scanDuplicateSummaries() {
} }
} }
function logField(label, value) {
if (value === undefined || value === null || value === "") return "";
return `<div class="log-detail-field"><span>${escapeHtml(label)}</span><code>${escapeHtml(value)}</code></div>`;
}
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 `<section class="log-detail-group">
<h3>${escapeHtml(title)}</h3>
<div class="log-detail-grid">${body}</div>
</section>`;
}
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("<br>") || '<span class="muted">-</span>';
}
function renderOperationLogRows(items) {
return items
.map((item) => {
const expanded = String(item.id) === expandedOperationLogId;
const mainRow = `<tr class="log-row" data-log-id="${escapeHtml(item.id)}" tabindex="0" role="button" aria-expanded="${expanded ? "true" : "false"}">
<td>${escapeHtml(item.created_at || "")}</td>
<td>${escapeHtml(item.operation || "")}</td>
<td><span class="status ${taskStatusClass(item.status)}">${escapeHtml(item.status || "")}</span></td>
<td>${escapeHtml(item.student || "")}</td>
<td>${renderLogAssociation(item)}</td>
<td class="record-action-cell">${renderLogActions(item)}</td>
</tr>`;
const detailRow = expanded
? `<tr class="log-detail-row" data-log-detail="${escapeHtml(item.id)}">
<td colspan="6">${renderLogDetail(item)}</td>
</tr>`
: "";
return `${mainRow}${detailRow}`;
})
.join("");
}
function renderCurrentOperationLogs() {
logRows.innerHTML = renderOperationLogRows(currentOperationLogs);
if (!currentOperationLogs.length) {
logRows.innerHTML = `<tr><td colspan="6" class="empty">没有符合条件的操作记录</td></tr>`;
}
}
function renderLogDetail(item) { function renderLogDetail(item) {
const details = []; const sections = [
if (item.task_type) details.push(`类型:${item.task_type}`); renderLogDetailGroup("基本信息", [
if (item.source_id) details.push(`来源:${item.source_id}`); logField("记录ID", item.id),
if (item.student_id) details.push(`学生ID${item.student_id}`); logField("操作时间", item.created_at),
if (item.teacher_id) details.push(`教师ID${item.teacher_id}`); logField("操作", item.operation),
if (item.teacher || item.subject) details.push(`老师/科目:${item.teacher || ""} ${item.subject || ""}`.trim()); logField("状态", item.status),
if (item.time_range) details.push(`时间:${item.time_range}`); logField("学生", item.student),
if (item.proposed_line) details.push(`记录:${item.proposed_line}`); logField("学生ID", item.student_id),
if (item.summary_id) details.push(`小结ID${item.summary_id}`); ]),
if (item.scanned_groups !== undefined) details.push(`扫描重复组:${item.scanned_groups}`); renderLogDetailGroup("关联对象", [
if (item.created_tasks !== undefined) details.push(`新增任务:${item.created_tasks}`); logField("任务类型", item.task_type),
if (Array.isArray(item.reasons) && item.reasons.length) details.push(`原因:${item.reasons.join("")}`); logField("任务号", item.task_id ? `#${item.task_id}` : ""),
if (item.error) details.push(`错误:${item.error}`); logField("来源ID", item.source_id),
if (item.backup_id) details.push(`备份:${item.backup_id}`); logField("记录ID", item.record_id),
if (item.target_log_id) details.push(`撤回记录:${item.target_log_id}`); logField("课程小结ID", item.summary_id),
if (item.target_backup_id) details.push(`撤回备份:${item.target_backup_id}`); logField("批次", item.batch_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("")}`); renderLogDetailGroup("课程信息", [
if (item.restored_hours !== undefined && item.restored_hours !== "") details.push(`返还课时:${item.restored_hours}`); logField("老师", item.teacher),
if (Array.isArray(item.restored_files) && item.restored_files.length) details.push(`恢复文件:${item.restored_files.join("")}`); logField("教师ID", item.teacher_id),
if (item.saved_path) details.push(`文件:${item.saved_path}`); logField("科目", item.subject),
return `<div class="log-detail">${details.map(escapeHtml).join("<br>") || "暂无详情"}</div>`; 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 `<div class="log-detail-panel">${sections.join("") || '<span class="muted">暂无详情</span>'}</div>`;
} }
function renderLogActions(item) { function renderLogActions(item) {
@@ -888,30 +969,20 @@ function renderLogActions(item) {
} }
async function loadOperationLogs() { async function loadOperationLogs() {
logRows.innerHTML = `<tr><td colspan="7" class="empty">正在读取</td></tr>`; logRows.innerHTML = `<tr><td colspan="6" class="empty">正在读取</td></tr>`;
const params = new URLSearchParams({ limit: "200" }); const params = new URLSearchParams({ limit: "200" });
if (logOperation.value) params.set("operation", logOperation.value); if (logOperation.value) params.set("operation", logOperation.value);
if (logStatus.value) params.set("status", logStatus.value); if (logStatus.value) params.set("status", logStatus.value);
if (logStudent.value.trim()) params.set("student", logStudent.value.trim()); if (logStudent.value.trim()) params.set("student", logStudent.value.trim());
try { try {
const data = await fetchJson(`/api/admin/operation-logs?${params.toString()}`); 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(""); logMeta.innerHTML = [metric("当前结果", `${data.count}`)].join("");
logRows.innerHTML = data.items renderCurrentOperationLogs();
.map((item) => `<tr>
<td>${escapeHtml(item.created_at || "")}</td>
<td>${escapeHtml(item.operation || "")}</td>
<td><span class="status ${taskStatusClass(item.status)}">${escapeHtml(item.status || "")}</span></td>
<td>${escapeHtml(item.student || "")}</td>
<td>${escapeHtml(item.batch_id || "")}${item.task_id ? `<br><small>任务 #${escapeHtml(item.task_id)}</small>` : ""}</td>
<td>${renderLogDetail(item)}</td>
<td class="record-action-cell">${renderLogActions(item)}</td>
</tr>`)
.join("");
if (!data.items.length) {
logRows.innerHTML = `<tr><td colspan="7" class="empty">没有符合条件的操作记录</td></tr>`;
}
} catch (error) { } catch (error) {
logRows.innerHTML = `<tr><td colspan="7" class="empty">读取失败:${escapeHtml(error.message)}</td></tr>`; currentOperationLogs = [];
logRows.innerHTML = `<tr><td colspan="6" class="empty">读取失败:${escapeHtml(error.message)}</td></tr>`;
} }
} }
@@ -1268,7 +1339,24 @@ logFilterForm.addEventListener("submit", (event) => {
}); });
logRows.addEventListener("click", (event) => { logRows.addEventListener("click", (event) => {
const rollback = event.target.closest(".log-rollback"); 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) => { classRegisterForm.addEventListener("submit", (event) => {
previewRegister(event, "class_record", linesFromTextarea(classRegisterLines), classRegisterStatus); previewRegister(event, "class_record", linesFromTextarea(classRegisterLines), classRegisterStatus);
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>新时空教务管理系统</title> <title>新时空教务管理系统</title>
<link rel="stylesheet" href="/static/styles.css?v=20260618-summary-missing-width" /> <link rel="stylesheet" href="/static/styles.css?v=20260618-summary-missing-rail" />
</head> </head>
<body> <body>
<header class="topbar"> <header class="topbar">
+74 -2
View File
@@ -573,7 +573,69 @@ textarea:focus {
} }
.log-detail { .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 { .summary-preview {
@@ -849,7 +911,8 @@ td {
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
gap: 10px; gap: 10px;
padding: 12px 16px; padding: 12px 16px 12px 18px;
border-left: 4px solid #f59e0b;
color: var(--muted); color: var(--muted);
font-size: 13px; font-size: 13px;
} }
@@ -1498,4 +1561,13 @@ td {
padding: 12px 14px; padding: 12px 14px;
grid-template-columns: repeat(2, 8em); grid-template-columns: repeat(2, 8em);
} }
.log-detail-panel {
padding: 12px 14px 14px;
}
.log-detail-grid {
grid-template-columns: 1fr;
gap: 10px;
}
} }