优化课程小结查询展开详情
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
+64
-36
@@ -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">
|
||||||
.join("");
|
<div class="summary-search-detail-panel">
|
||||||
if (!data.items.length) {
|
<div class="summary-search-detail-head">
|
||||||
summarySearchRows.innerHTML = `<tr><td colspan="7" class="empty">没有符合条件的课程小结</td></tr>`;
|
<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) {
|
} 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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user