feat: add date and time sorting controls

This commit is contained in:
Codex
2026-06-13 23:26:17 +08:00
parent 839149c258
commit ad77a19fbd
3 changed files with 133 additions and 6 deletions
+99 -2
View File
@@ -4,6 +4,8 @@ const recordForm = document.querySelector("#recordForm");
const recordQuery = document.querySelector("#recordQuery");
const recordMeta = document.querySelector("#recordMeta");
const recordRows = document.querySelector("#recordRows");
const dateSortBtn = document.querySelector("#dateSortBtn");
const timeSortBtn = document.querySelector("#timeSortBtn");
const inlineAccount = document.querySelector("#inlineAccount");
const correctionToolbar = document.querySelector("#correctionToolbar");
const correctionStatus = document.querySelector("#correctionStatus");
@@ -23,7 +25,9 @@ const cancelCorrectionBtn = document.querySelector("#cancelCorrectionBtn");
const WEEKDAYS = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
const COPY_LINE_BREAK = "\r\n";
const SORT_DIRECTIONS = { asc: 1, desc: -1 };
let currentRecords = [];
let recordSort = { date: "asc", time: "asc" };
let currentRecordOrder = [];
let correctedRecords = new Map();
let activeCorrectionKey = "";
@@ -76,10 +80,97 @@ function groupRecordsByTeacher(records) {
});
}
function getDisplayRecord(row) {
return correctedRecords.get(row._recordKey) || row;
}
function normalizeDateForSort(value) {
const text = String(value || "");
const match = text.match(/^(\d{4})\.(\d{1,2})\.(\d{1,2})$/);
if (!match) return text;
return `${match[1]}.${match[2].padStart(2, "0")}.${match[3].padStart(2, "0")}`;
}
function compareDatesForSort(a, b) {
return normalizeDateForSort(a).localeCompare(normalizeDateForSort(b), "zh-CN", { numeric: true });
}
function parseStartMinutes(value) {
const match = String(value || "").match(/^(\d{1,2}):(\d{2})/);
if (!match) return null;
return Number(match[1]) * 60 + Number(match[2]);
}
function compareStartTimesForSort(a, b) {
const minutesA = parseStartMinutes(a);
const minutesB = parseStartMinutes(b);
if (minutesA !== null && minutesB !== null) return minutesA - minutesB;
if (minutesA !== null) return -1;
if (minutesB !== null) return 1;
return String(a || "").localeCompare(String(b || ""), "zh-CN", { numeric: true });
}
function compareRecordsForDisplay(a, b) {
const displayA = getDisplayRecord(a);
const displayB = getDisplayRecord(b);
const dateCompare = compareDatesForSort(displayA.date, displayB.date);
if (dateCompare !== 0) return dateCompare * SORT_DIRECTIONS[recordSort.date];
const timeCompare = compareStartTimesForSort(displayA.time, displayB.time);
if (timeCompare !== 0) return timeCompare * SORT_DIRECTIONS[recordSort.time];
return (a._recordIndex || 0) - (b._recordIndex || 0);
}
function sortRecordsForDisplay(records) {
return [...records].sort(compareRecordsForDisplay);
}
function sortDirectionLabel(direction) {
return direction === "asc" ? "升序排列" : "降序排列";
}
function sortDirectionName(direction) {
return sortDirectionLabel(direction);
}
function nextSortDirectionName(direction) {
return direction === "asc" ? "降序排列" : "升序排列";
}
function updateRecordSortButtons() {
dateSortBtn.textContent = `日期 ${sortDirectionLabel(recordSort.date)}`;
dateSortBtn.setAttribute(
"aria-label",
`日期排序:${sortDirectionName(recordSort.date)},点击切换为${nextSortDirectionName(recordSort.date)}`,
);
timeSortBtn.textContent = `时间 ${sortDirectionLabel(recordSort.time)}`;
timeSortBtn.setAttribute(
"aria-label",
`时间排序:${sortDirectionName(recordSort.time)},点击切换为${nextSortDirectionName(recordSort.time)}`,
);
}
function resetRecordSort() {
recordSort = { date: "asc", time: "asc" };
updateRecordSortButtons();
}
function renderCurrentRecords() {
if (!currentRecords.length) return;
recordRows.innerHTML = renderGroupedRecords(currentRecords);
}
function toggleRecordSort(field) {
recordSort = { ...recordSort, [field]: recordSort[field] === "asc" ? "desc" : "asc" };
updateRecordSortButtons();
renderCurrentRecords();
}
function renderRecordRow(row) {
const key = row._recordKey;
const corrected = correctedRecords.get(key);
const displayRow = corrected || row;
const displayRow = getDisplayRecord(row);
const correctedClass = corrected ? " corrected-row" : "";
const actionLabel = corrected ? "编辑" : "纠错";
const badge = corrected ? '<span class="correction-badge">已修改</span>' : "";
@@ -110,7 +201,7 @@ function renderGroupedRecords(records) {
<span>${group.count} 条记录 · ${fmtHours(group.totalHours)} 小时</span>
</div>
</td>
</tr>${group.records
</tr>${sortRecordsForDisplay(group.records)
.map((row) => {
currentRecordOrder.push(row._recordKey);
return renderRecordRow(row);
@@ -375,6 +466,7 @@ async function queryRecords(query) {
recordMeta.innerHTML = "";
currentRecords = [];
resetCorrections();
resetRecordSort();
clearInlineAccount();
try {
const data = await fetchJson(`/api/records?q=${encodeURIComponent(q)}&limit=500`);
@@ -397,6 +489,7 @@ async function queryRecords(query) {
currentRecords = data.records.map((row, index) => ({
...row,
_recordIndex: index,
_recordKey: makeRecordKey(row, index),
}));
recordRows.innerHTML = renderGroupedRecords(currentRecords);
@@ -415,6 +508,9 @@ refreshBtn.addEventListener("click", () => {
if (recordQuery.value.trim()) queryRecords(recordQuery.value);
});
dateSortBtn.addEventListener("click", () => toggleRecordSort("date"));
timeSortBtn.addEventListener("click", () => toggleRecordSort("time"));
document.querySelectorAll("[data-query]").forEach((button) => {
button.addEventListener("click", () => {
recordQuery.value = button.dataset.query;
@@ -455,4 +551,5 @@ document.addEventListener("keydown", (event) => {
copyCorrectedBtn.addEventListener("click", copyCorrectedRecords);
updateRecordSortButtons();
loadHealth();
+8 -4
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=20260611-weekend-shortcuts" />
<link rel="stylesheet" href="/static/styles.css?v=20260613-date-time-sort" />
</head>
<body>
<header class="topbar">
@@ -50,8 +50,12 @@
<table>
<thead>
<tr>
<th>日期</th>
<th>时间</th>
<th>
<button id="dateSortBtn" class="sort-header-button" type="button" aria-label="日期排序:升序排列,点击切换为降序排列">日期 升序排列</button>
</th>
<th>
<button id="timeSortBtn" class="sort-header-button" type="button" aria-label="时间排序:升序排列,点击切换为降序排列">时间 升序排列</button>
</th>
<th>学生</th>
<th>老师</th>
<th>科目</th>
@@ -112,6 +116,6 @@
</section>
</div>
<script src="/static/app.js?v=20260612-relative-shortcuts"></script>
<script src="/static/app.js?v=20260613-sort-labels"></script>
</body>
</html>
+26
View File
@@ -358,6 +358,32 @@ th {
font-weight: 700;
}
.sort-header-button {
display: inline-flex;
align-items: center;
justify-content: flex-start;
min-width: 64px;
padding: 0;
border: 0;
background: transparent;
color: inherit;
cursor: pointer;
font: inherit;
font-weight: 700;
white-space: nowrap;
}
.sort-header-button:hover,
.sort-header-button:focus-visible {
color: var(--accent-strong);
}
.sort-header-button:focus-visible {
border-radius: 4px;
outline: 2px solid rgba(15, 118, 110, 0.35);
outline-offset: 3px;
}
td {
font-size: 14px;
}