成绩明细历史记录:按提交年月日分类,每个分类标记最高分

新增 DataService::dayKeyOf(),日期键一律用服务端本地时区计算(避免 UTC 导致东八区零点前后错一天);aggregateByStudent 为 attempts_list 每项附加 day/day_best/day_count,expandAttempts 透传。

弹窗按天分组渲染分组行(日期 + 共N次 + 最高X分),组内新到旧;当日最高标签仅在当天有多次提交时显示,避免单次日每行都挂标签。

修复:submit_ts=0(时间未知)的记录原本被误标为第1次(升序排序 idx=1 但展示在末尾),现显示 -;未知时间统一归入「未知时间」组。

aggregate 单测 17 -> 33(跨天归属/日内次数/每日最高/未知时间组/时区边界 00:30 与 23:30);全量 148 项全绿。
This commit is contained in:
2026-09-14 07:28:02 +08:00
parent eb5d06d392
commit 555f500dc7
4 changed files with 186 additions and 31 deletions
+36 -11
View File
@@ -926,6 +926,7 @@
idx: 1, is_best: true, score: r.score, submit_at: r.submit_at,
submit_ts: r.submit_ts || r.last_submit_ts || 0,
duration_txt: r.duration_txt, temp_save: !!r.temp_save,
day: '', day_best: false, day_count: 1,
}];
}
// 新 -> 旧 展示(缺 submit_ts 时按 idx 倒序兜底,保证"次序"看起来合理)
@@ -934,17 +935,41 @@
if (ta !== tb) return tb - ta;
return (b.idx || 0) - (a.idx || 0);
});
const rows = desc.map((a, i) => {
const sc = (a.score == null) ? '<span class="muted">未判分</span>' : esc(a.score);
const best = (a.is_best && attempts > 1) ? ' <span class="tag tag-best">最佳</span>' : '';
const tmp = a.temp_save ? ' <span class="tag tag-gray">暂存</span>' : '';
const no = attempts > 1 ? (a.idx || (desc.length - i)) : 1;
return '<tr>' +
'<td class="num">' + (attempts > 1 ? '第 ' + no + ' 次' : '-') + '</td>' +
'<td class="num">' + esc(a.submit_at || '-') + '</td>' +
'<td class="num">' + sc + best + tmp + '</td>' +
'<td class="num">' + esc(a.duration_txt || '-') + '</td>' +
'</tr>';
// 按「提交时间年月日」分组:新的一天在前。
// 日期键由后端按服务端本地时区算好(a.day),这里仅兜底;时间未知的(ts=0)key 为 '',
// 且排序里排最后,天然连续落在末尾 → 归入「未知时间」组。
const groups = [];
desc.forEach(a => {
const key = a.day || (a.submit_at && a.submit_at !== '-' ? String(a.submit_at).slice(0, 10) : '') || '';
let g = groups.length ? groups[groups.length - 1] : null;
if (!g || g.key !== key) { g = { key: key, items: [] }; groups.push(g); }
g.items.push(a);
});
// 每组的最高分(未判分不参与),用于分组行显示「最高 X 分」
groups.forEach(g => {
g.max = g.items.reduce(
(m, a) => (a.score == null ? m : (m == null || Number(a.score) > m ? Number(a.score) : m)), null);
});
const rows = groups.map(g => {
const maxTxt = (g.max == null) ? '' : ' · 最高 ' + g.max + ' 分';
let html = '<tr class="att-day"><td colspan="4">' + esc(g.key || '未知时间') +
'<span class="att-day-sub">共 ' + g.items.length + ' 次' + maxTxt + '</span></td></tr>';
html += g.items.map(a => {
const sc = (a.score == null) ? '<span class="muted">未判分</span>' : esc(a.score);
const best = (a.is_best && attempts > 1) ? ' <span class="tag tag-best">最佳</span>' : '';
// 当日最高:仅当天有多次提交时才标,避免"一天只考一次"时每行都挂标签、反而看不出重点
const dayBest = (a.day_best && (a.day_count || 0) >= 2) ? ' <span class="tag tag-day">当日最高</span>' : '';
const tmp = a.temp_save ? ' <span class="tag tag-gray">暂存</span>' : '';
// 时间未知(submit_ts=0)时无从判断先后,次序显示 '-',而不是误报"第 1 次"
const noTxt = (attempts > 1 && a.submit_ts) ? ('第 ' + (a.idx || 1) + ' 次') : '-';
return '<tr>' +
'<td class="num">' + noTxt + '</td>' +
'<td class="num">' + esc(a.submit_at || '-') + '</td>' +
'<td class="num">' + sc + best + dayBest + tmp + '</td>' +
'<td class="num">' + esc(a.duration_txt || '-') + '</td>' +
'</tr>';
}).join('');
return html;
}).join('');
const bestScore = (r.best_score == null) ? r.score : r.best_score;
$('#attTitle').textContent = (r.name || '') + '(' + r.sid + ') · 共 ' + attempts + ' 次考试' +