优化课程小结查询展开详情

This commit is contained in:
Codex
2026-06-18 09:24:13 +08:00
parent 59fcee72bb
commit aa6f4a0329
3 changed files with 131 additions and 41 deletions
+2 -5
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-log-expand" /> <link rel="stylesheet" href="/static/styles.css?v=20260618-summary-search-expand" />
</head> </head>
<body> <body>
<header class="topbar"> <header class="topbar">
@@ -344,9 +344,6 @@
<th>时间</th> <th>时间</th>
<th>学生</th> <th>学生</th>
<th>老师/科目</th> <th>老师/科目</th>
<th>标题</th>
<th>小结正文</th>
<th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody id="summarySearchRows"></tbody> <tbody id="summarySearchRows"></tbody>
@@ -453,6 +450,6 @@
</section> </section>
</main> </main>
<script src="/static/admin.js?v=20260618-log-expand"></script> <script src="/static/admin.js?v=20260618-summary-search-expand"></script>
</body> </body>
</html> </html>
+62 -34
View File
@@ -108,6 +108,8 @@ let currentTeachers = [];
let editingTeacherId = ""; let editingTeacherId = "";
let currentSummaryReviews = []; let currentSummaryReviews = [];
let activeSummaryReview = null; let activeSummaryReview = null;
let currentSummarySearchItems = [];
let expandedSummarySearchId = "";
let currentOperationLogs = []; let currentOperationLogs = [];
let expandedOperationLogId = ""; let expandedOperationLogId = "";
const registerPreviewState = { const registerPreviewState = {
@@ -728,23 +730,15 @@ function summarySearchParams() {
return params; return params;
} }
function renderSummarySearchBody(item) {
const fullId = `summary-full-${item.id}`;
const hasFull = String(item.body || "") !== String(item.body_preview || "");
return `<div class="summary-body">${escapeHtml(item.body_preview || "")}</div>
<div id="${escapeHtml(fullId)}" class="summary-body summary-full" hidden>${escapeHtml(item.body || "")}</div>
<button class="small-button summary-toggle" type="button" data-target="${escapeHtml(fullId)}" ${hasFull ? "" : "hidden"}>展开</button>`;
}
function renderSummaryActions(item) { function renderSummaryActions(item) {
if (item.time_range) { if (item.time_range) {
return `<span class="muted">已完整</span>`; return `<span class="summary-action-note">时间已完整</span>`;
} }
const inputId = `summary-time-${item.id}`; const inputId = `summary-time-${item.id}`;
return `<div class="summary-time-actions"> return `<div class="summary-time-actions">
<input id="${escapeHtml(inputId)}" class="summary-time-input" data-summary-time-input="${escapeHtml(item.id)}" autocomplete="off" placeholder="08:00-10:00" /> <input id="${escapeHtml(inputId)}" class="summary-time-input" data-summary-time-input="${escapeHtml(item.id)}" autocomplete="off" placeholder="08:00-10:00" />
<button class="small-button summary-time-save" type="button" data-summary-id="${escapeHtml(item.id)}">补齐时间</button> <button class="secondary-button summary-time-save" type="button" data-summary-id="${escapeHtml(item.id)}">补齐时间</button>
<button class="small-button summary-delete" type="button" data-summary-id="${escapeHtml(item.id)}">删除</button> <button class="secondary-button summary-delete" type="button" data-summary-id="${escapeHtml(item.id)}">删除课程小结</button>
</div>`; </div>`;
} }
@@ -753,30 +747,58 @@ function renderMatchedFields(item) {
return fields.length ? `<br><small>命中:${fields.map(escapeHtml).join("、")}</small>` : ""; return fields.length ? `<br><small>命中:${fields.map(escapeHtml).join("、")}</small>` : "";
} }
async function loadSummarySearch() { function renderSummarySearchRows(items) {
summarySearchRows.innerHTML = `<tr><td colspan="7" class="empty">正在读取</td></tr>`; return items
try { .map((item) => {
const data = await fetchJson(`/api/admin/course-summaries?${summarySearchParams().toString()}`); const expanded = String(item.id) === expandedSummarySearchId;
summarySearchMeta.innerHTML = [ const mainRow = `<tr class="summary-search-result-row" data-summary-id="${escapeHtml(item.id)}" tabindex="0" role="button" aria-expanded="${expanded ? "true" : "false"}">
metric("命中小结", `${data.count}`),
metric("当前显示", `${data.returned}`),
].join("");
summarySearchRows.innerHTML = data.items
.map((item) => `<tr>
<td>${escapeHtml(item.date_iso || "未识别")}</td> <td>${escapeHtml(item.date_iso || "未识别")}</td>
<td>${escapeHtml(item.time_range || "缺少时间")}</td> <td>${escapeHtml(item.time_range || "缺少时间")}</td>
<td>${escapeHtml(item.student || "")}<br><small>${escapeHtml(item.group || "")}</small></td> <td>${escapeHtml(item.student || "")}<br><small>${escapeHtml(item.group || "")}</small></td>
<td>${escapeHtml(item.teacher || "待核对老师")}<br><small>${escapeHtml(item.subject || "待核对科目")}</small></td> <td>${escapeHtml(item.teacher || "待核对老师")}<br><small>${escapeHtml(item.subject || "待核对科目")}</small></td>
<td>${escapeHtml(item.title || "")}${renderMatchedFields(item)}</td> </tr>`;
<td>${renderSummarySearchBody(item)}</td> const detailRow = expanded
<td>${renderSummaryActions(item)}</td> ? `<tr class="summary-search-detail-row" data-summary-detail="${escapeHtml(item.id)}">
</tr>`) <td colspan="4">
<div class="summary-search-detail-panel">
<div class="summary-search-detail-head">
<div>
<div class="record-summary-title">${escapeHtml(item.title || "课程小结正文")}</div>
${renderMatchedFields(item)}
</div>
</div>
<div class="summary-body summary-search-full">${escapeHtml(item.body || item.body_preview || "暂无正文")}</div>
<div class="summary-search-actions">${renderSummaryActions(item)}</div>
</div>
</td>
</tr>`
: "";
return `${mainRow}${detailRow}`;
})
.join(""); .join("");
if (!data.items.length) { }
summarySearchRows.innerHTML = `<tr><td colspan="7" class="empty">没有符合条件的课程小结</td></tr>`;
function renderCurrentSummarySearch() {
summarySearchRows.innerHTML = renderSummarySearchRows(currentSummarySearchItems);
if (!currentSummarySearchItems.length) {
summarySearchRows.innerHTML = `<tr><td colspan="4" class="empty">没有符合条件的课程小结</td></tr>`;
} }
}
async function loadSummarySearch() {
summarySearchRows.innerHTML = `<tr><td colspan="4" class="empty">正在读取</td></tr>`;
try {
const data = await fetchJson(`/api/admin/course-summaries?${summarySearchParams().toString()}`);
currentSummarySearchItems = data.items || [];
if (!currentSummarySearchItems.some((item) => String(item.id) === expandedSummarySearchId)) expandedSummarySearchId = "";
summarySearchMeta.innerHTML = [
metric("命中小结", `${data.count}`),
metric("当前显示", `${data.returned}`),
].join("");
renderCurrentSummarySearch();
} catch (error) { } catch (error) {
summarySearchRows.innerHTML = `<tr><td colspan="7" class="empty">读取失败:${escapeHtml(error.message)}</td></tr>`; currentSummarySearchItems = [];
summarySearchRows.innerHTML = `<tr><td colspan="4" class="empty">读取失败:${escapeHtml(error.message)}</td></tr>`;
} }
} }
@@ -1307,7 +1329,6 @@ summarySearchForm.addEventListener("submit", (event) => {
}); });
summarySearchMissingTime.addEventListener("change", loadSummarySearch); summarySearchMissingTime.addEventListener("change", loadSummarySearch);
summarySearchRows.addEventListener("click", (event) => { summarySearchRows.addEventListener("click", (event) => {
const button = event.target.closest(".summary-toggle");
const saveTime = event.target.closest(".summary-time-save"); const saveTime = event.target.closest(".summary-time-save");
const deleteSummary = event.target.closest(".summary-delete"); const deleteSummary = event.target.closest(".summary-delete");
if (saveTime) { if (saveTime) {
@@ -1325,11 +1346,18 @@ summarySearchRows.addEventListener("click", (event) => {
deleteCourseSummary(deleteSummary.dataset.summaryId).catch((error) => alert(error.message)); deleteCourseSummary(deleteSummary.dataset.summaryId).catch((error) => alert(error.message));
return; return;
} }
if (!button) return; const row = event.target.closest(".summary-search-result-row");
const target = document.getElementById(button.dataset.target); if (!row) return;
if (!target) return; expandedSummarySearchId = expandedSummarySearchId === row.dataset.summaryId ? "" : row.dataset.summaryId;
target.hidden = !target.hidden; renderCurrentSummarySearch();
button.textContent = target.hidden ? "展开" : "收起"; });
summarySearchRows.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== " ") return;
const row = event.target.closest(".summary-search-result-row");
if (!row) return;
event.preventDefault();
expandedSummarySearchId = expandedSummarySearchId === row.dataset.summaryId ? "" : row.dataset.summaryId;
renderCurrentSummarySearch();
}); });
logOperation.addEventListener("change", loadOperationLogs); logOperation.addEventListener("change", loadOperationLogs);
logStatus.addEventListener("change", loadOperationLogs); logStatus.addEventListener("change", loadOperationLogs);
+65
View File
@@ -658,6 +658,57 @@ textarea:focus {
margin-top: 8px; margin-top: 8px;
} }
.summary-search-result-row {
cursor: pointer;
}
.summary-search-result-row:hover td {
background: #f8fafc;
}
.summary-search-detail-row td {
padding: 0;
background: #fbfcfd;
}
.summary-search-detail-panel {
padding: 14px 16px 16px;
border-top: 1px solid var(--line);
}
.summary-search-detail-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
}
.summary-search-full {
max-width: none;
}
.summary-search-actions {
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 10px;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--line);
}
.summary-search-actions .summary-time-actions {
justify-content: flex-end;
}
.summary-action-note {
color: var(--muted);
font-size: 13px;
font-weight: 700;
}
.muted { .muted {
color: var(--muted); color: var(--muted);
} }
@@ -1581,6 +1632,20 @@ td {
padding: 12px 14px 14px; padding: 12px 14px 14px;
} }
.summary-search-detail-panel {
padding: 12px 14px 14px;
}
.summary-search-actions,
.summary-search-actions .summary-time-actions {
justify-content: stretch;
}
.summary-search-actions .secondary-button,
.summary-search-actions .summary-time-input {
width: 100%;
}
.log-detail-grid { .log-detail-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 10px; gap: 10px;