feat: 支持单套试卷同一提交人多次刷卷并展示

- DataService::build 新增 aggregateByStudent 按学号聚合:
  dedupe=true 仅保留最新一份;false 则每学号一行(代表成绩取最高分)
  附 attempts(次数) 与 attempts_list(每次提交明细),修正提交人数被多次提交inflate的问题
- api.php dashboard 透传 attempts/attempts_list;statOf 增加 attempts 求和;export 增加刷题次数列
- dashboard.php 成员榜加"刷N次"徽标,点击下钻弹窗看每次提交(时间/成绩/时长/暂存);hero 显示总提交次数
- 主统计页成绩明细同样加"刷N次"徽标与下钻弹窗
- stats 新增 attempts_total
This commit is contained in:
2026-09-09 15:55:06 +08:00
parent 3952c77afe
commit d2f0908385
6 changed files with 188 additions and 27 deletions
+28 -3
View File
@@ -447,10 +447,13 @@
const sig = r.sid + '|' + r.score + '|' + r.submit_at;
const changed = state.prevMap[r.sid] && state.prevMap[r.sid] !== sig;
const isNew = r.sid && state.newSids.has(r.sid);
const clsName = (changed ? 'diff ' : '') + (isNew ? 'new' : '');
return '<tr class="' + clsName.trim() + '">' +
const canDrill = (r.attempts || 1) > 1;
const clsName = (changed ? 'diff ' : '') + (isNew ? 'new ' : '') + (canDrill ? 'clickable' : '');
const att = canDrill ? ' <span class="tag tag-att" title="查看 ' + r.attempts + ' 次提交明细">刷' + r.attempts + '次</span>' : '';
const onclk = canDrill ? ' onclick="openAttemptsModal(' + JSON.stringify(r.sid) + ')"' : '';
return '<tr class="' + clsName.trim() + '"' + onclk + '>' +
'<td class="sid">' + esc(r.sid) + '</td>' +
'<td class="name">' + esc(r.name) + '</td>' +
'<td class="name">' + esc(r.name) + att + '</td>' +
'<td>' + esc(r.cls || '') + '</td>' +
'<td>' + esc(r.grp || '') + '</td>' +
'<td class="score num ' + scoreClass(r.score) + '">' + (r.score == null ? '-' : r.score) + '</td>' +
@@ -499,6 +502,28 @@
'</td><td class="num">' + esc(u.submit_at) + '</td></tr>').join('');
}
/* 多次刷题下钻弹窗(主统计页成绩明细) */
function openAttemptsModal(sid) {
const r = (state.rows || []).find(x => String(x.sid) === String(sid));
if (!r || (r.attempts || 1) <= 1) return;
const list = r.attempts_list || [];
const rows = list.map(a => {
const sc = a.score == null ? '<span class="muted">未判分</span>' : esc(a.score);
const tmp = a.temp_save ? ' <span class="tag tag-gray">暂存</span>' : '';
return '<tr><td class="num">' + esc(a.submit_at) + '</td><td class="num">' + sc + tmp +
'</td><td class="num">' + esc(a.duration_txt || '-') + '</td></tr>';
}).join('');
$('#attTitle').textContent = (r.name || '') + ' · 共 ' + r.attempts + ' 次提交';
$('#attBody').innerHTML = list.length
? '<table class="grid compact"><thead><tr><th>提交时间</th><th>成绩</th><th>考试时长</th></tr></thead><tbody>' + rows + '</tbody></table>'
: '<div class="empty">无提交明细</div>';
$('#attemptsModal').classList.add('open');
}
function closeAttemptsModal() {
const m = $('#attemptsModal');
if (m) m.classList.remove('open');
}
/* ---------------- 自动刷新 ---------------- */
function schedule() {
if (state.timer) clearInterval(state.timer);