优化操作记录详情展开
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<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>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
@@ -406,7 +406,6 @@
|
||||
<th>结果</th>
|
||||
<th>学生</th>
|
||||
<th>批次/任务</th>
|
||||
<th>详情</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -454,6 +453,6 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/static/admin.js?v=20260616-rollback-log"></script>
|
||||
<script src="/static/admin.js?v=20260618-log-expand"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+127
-39
@@ -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 `<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) {
|
||||
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 `<div class="log-detail">${details.map(escapeHtml).join("<br>") || "暂无详情"}</div>`;
|
||||
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 `<div class="log-detail-panel">${sections.join("") || '<span class="muted">暂无详情</span>'}</div>`;
|
||||
}
|
||||
|
||||
function renderLogActions(item) {
|
||||
@@ -888,30 +969,20 @@ function renderLogActions(item) {
|
||||
}
|
||||
|
||||
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" });
|
||||
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) => `<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>`;
|
||||
}
|
||||
renderCurrentOperationLogs();
|
||||
} 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) => {
|
||||
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);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<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>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user