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

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 name="viewport" content="width=device-width, initial-scale=1" />
<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>
<body>
<header class="topbar">
@@ -344,9 +344,6 @@
<th>时间</th>
<th>学生</th>
<th>老师/科目</th>
<th>标题</th>
<th>小结正文</th>
<th>操作</th>
</tr>
</thead>
<tbody id="summarySearchRows"></tbody>
@@ -453,6 +450,6 @@
</section>
</main>
<script src="/static/admin.js?v=20260618-log-expand"></script>
<script src="/static/admin.js?v=20260618-summary-search-expand"></script>
</body>
</html>
+64 -36
View File
@@ -108,6 +108,8 @@ let currentTeachers = [];
let editingTeacherId = "";
let currentSummaryReviews = [];
let activeSummaryReview = null;
let currentSummarySearchItems = [];
let expandedSummarySearchId = "";
let currentOperationLogs = [];
let expandedOperationLogId = "";
const registerPreviewState = {
@@ -728,23 +730,15 @@ function summarySearchParams() {
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) {
if (item.time_range) {
return `<span class="muted">已完整</span>`;
return `<span class="summary-action-note">时间已完整</span>`;
}
const inputId = `summary-time-${item.id}`;
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" />
<button class="small-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-time-save" 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>`;
}
@@ -753,30 +747,58 @@ function renderMatchedFields(item) {
return fields.length ? `<br><small>命中:${fields.map(escapeHtml).join("、")}</small>` : "";
}
async function loadSummarySearch() {
summarySearchRows.innerHTML = `<tr><td colspan="7" class="empty">正在读取</td></tr>`;
try {
const data = await fetchJson(`/api/admin/course-summaries?${summarySearchParams().toString()}`);
summarySearchMeta.innerHTML = [
metric("命中小结", `${data.count}`),
metric("当前显示", `${data.returned}`),
].join("");
summarySearchRows.innerHTML = data.items
.map((item) => `<tr>
function renderSummarySearchRows(items) {
return items
.map((item) => {
const expanded = String(item.id) === expandedSummarySearchId;
const mainRow = `<tr class="summary-search-result-row" data-summary-id="${escapeHtml(item.id)}" tabindex="0" role="button" aria-expanded="${expanded ? "true" : "false"}">
<td>${escapeHtml(item.date_iso || "未识别")}</td>
<td>${escapeHtml(item.time_range || "缺少时间")}</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.title || "")}${renderMatchedFields(item)}</td>
<td>${renderSummarySearchBody(item)}</td>
<td>${renderSummaryActions(item)}</td>
</tr>`)
.join("");
if (!data.items.length) {
summarySearchRows.innerHTML = `<tr><td colspan="7" class="empty">没有符合条件的课程小结</td></tr>`;
}
</tr>`;
const detailRow = expanded
? `<tr class="summary-search-detail-row" data-summary-detail="${escapeHtml(item.id)}">
<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("");
}
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) {
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);
summarySearchRows.addEventListener("click", (event) => {
const button = event.target.closest(".summary-toggle");
const saveTime = event.target.closest(".summary-time-save");
const deleteSummary = event.target.closest(".summary-delete");
if (saveTime) {
@@ -1325,11 +1346,18 @@ summarySearchRows.addEventListener("click", (event) => {
deleteCourseSummary(deleteSummary.dataset.summaryId).catch((error) => alert(error.message));
return;
}
if (!button) return;
const target = document.getElementById(button.dataset.target);
if (!target) return;
target.hidden = !target.hidden;
button.textContent = target.hidden ? "展开" : "收起";
const row = event.target.closest(".summary-search-result-row");
if (!row) return;
expandedSummarySearchId = expandedSummarySearchId === row.dataset.summaryId ? "" : row.dataset.summaryId;
renderCurrentSummarySearch();
});
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);
logStatus.addEventListener("change", loadOperationLogs);
+65
View File
@@ -658,6 +658,57 @@ textarea:focus {
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 {
color: var(--muted);
}
@@ -1581,6 +1632,20 @@ td {
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 {
grid-template-columns: 1fr;
gap: 10px;