From d2f090838554e8724464edceddb16505553cf4c3 Mon Sep 17 00:00:00 2001 From: nxxy335 Date: Wed, 9 Sep 2026 15:55:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=8D=95=E5=A5=97?= =?UTF-8?q?=E8=AF=95=E5=8D=B7=E5=90=8C=E4=B8=80=E6=8F=90=E4=BA=A4=E4=BA=BA?= =?UTF-8?q?=E5=A4=9A=E6=AC=A1=E5=88=B7=E5=8D=B7=E5=B9=B6=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- api.php | 14 +++++--- assets/app.js | 31 ++++++++++++++-- assets/style.css | 6 ++++ dashboard.php | 70 ++++++++++++++++++++++++++++++++---- index.php | 8 +++++ lib/DataService.php | 86 ++++++++++++++++++++++++++++++++++++++------- 6 files changed, 188 insertions(+), 27 deletions(-) diff --git a/api.php b/api.php index 697a32d..1fb2d9d 100644 --- a/api.php +++ b/api.php @@ -217,6 +217,7 @@ try { $statOf = static function (array $stu, array $rs, int $fs): array { $total = count($stu); $submitted = count($rs); + $attempts = array_sum(array_map(static fn($r) => (int)($r['attempts'] ?? 1), $rs)); $scores = array_values(array_filter(array_map(static fn($r) => $r['score'] ?? null, $rs), static fn($v) => $v !== null)); $avg = $scores !== [] ? round(array_sum($scores) / count($scores), 2) : null; $pass = count(array_filter($rs, static fn($r) => ($r['score'] ?? null) !== null && ($r['score'] >= 0.6 * $fs))); @@ -226,6 +227,7 @@ try { return [ 'total' => $total, 'submitted' => $submitted, + 'attempts' => $attempts, 'absent' => max(0, $total - $submitted), 'rate' => $total > 0 ? round($submitted / $total * 100, 1) : 0, 'avg' => $avg, @@ -237,13 +239,16 @@ try { }; // 已提交成员榜:按成绩从高到低,缺成绩者排最后,并给出排名 + // 透传 attempts / attempts_list,供前端展示"同人多刷卷" $membersOf = static function (array $rs): array { $ms = []; foreach ($rs as $r) { $ms[] = [ - 'sid' => (string)($r['sid'] ?? ''), - 'name' => (string)($r['name'] ?? ''), - 'score' => ($r['score'] ?? null), + 'sid' => (string)($r['sid'] ?? ''), + 'name' => (string)($r['name'] ?? ''), + 'score' => ($r['score'] ?? null), + 'attempts' => (int)($r['attempts'] ?? 1), + 'attempts_list' => $r['attempts_list'] ?? [], ]; } usort($ms, static function (array $a, array $b): int { @@ -440,7 +445,7 @@ try { fputcsv($out, [$r['sid'] ?? '', $r['name'] ?? '', $r['cls'] ?? '', $r['grp'] ?? '']); } } else { - fputcsv($out, ['学号', '姓名', '班级', '组别', '成绩', '提交时间', '考试时长', '考试时长(秒)']); + fputcsv($out, ['学号', '姓名', '班级', '组别', '成绩', '提交时间', '考试时长', '考试时长(秒)', '刷题次数']); foreach ($rows as $r) { fputcsv($out, [ $r['sid'] ?? '', @@ -451,6 +456,7 @@ try { $r['submit_at'] ?? '', $r['duration_txt'] ?? '', $r['duration'] === null ? '' : $r['duration'], + (int)($r['attempts'] ?? 1), ]); } } diff --git a/assets/app.js b/assets/app.js index 12ef974..95373f4 100644 --- a/assets/app.js +++ b/assets/app.js @@ -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 '' + + const canDrill = (r.attempts || 1) > 1; + const clsName = (changed ? 'diff ' : '') + (isNew ? 'new ' : '') + (canDrill ? 'clickable' : ''); + const att = canDrill ? ' 刷' + r.attempts + '次' : ''; + const onclk = canDrill ? ' onclick="openAttemptsModal(' + JSON.stringify(r.sid) + ')"' : ''; + return '' + '' + esc(r.sid) + '' + - '' + esc(r.name) + '' + + '' + esc(r.name) + att + '' + '' + esc(r.cls || '') + '' + '' + esc(r.grp || '') + '' + '' + (r.score == null ? '-' : r.score) + '' + @@ -499,6 +502,28 @@ '' + esc(u.submit_at) + '').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 ? '未判分' : esc(a.score); + const tmp = a.temp_save ? ' 暂存' : ''; + return '' + esc(a.submit_at) + '' + sc + tmp + + '' + esc(a.duration_txt || '-') + ''; + }).join(''); + $('#attTitle').textContent = (r.name || '') + ' · 共 ' + r.attempts + ' 次提交'; + $('#attBody').innerHTML = list.length + ? '' + rows + '
提交时间成绩考试时长
' + : '
无提交明细
'; + $('#attemptsModal').classList.add('open'); + } + function closeAttemptsModal() { + const m = $('#attemptsModal'); + if (m) m.classList.remove('open'); + } + /* ---------------- 自动刷新 ---------------- */ function schedule() { if (state.timer) clearInterval(state.timer); diff --git a/assets/style.css b/assets/style.css index c67e020..95a01bc 100644 --- a/assets/style.css +++ b/assets/style.css @@ -311,6 +311,12 @@ table.grid tbody tr.new:hover td { background: #d6f2e2; } .tag-ok { background: var(--ok-weak); color: var(--ok); } .tag-warn { background: var(--warn-weak); color: var(--warn); } .tag-gray { background: var(--border-2); color: var(--text-3); } +.tag-att { background: #e8eefc; color: #3457c9; cursor: pointer; } + +/* 多次刷题:成员榜可点击行 */ +tr.clickable { cursor: pointer; } +tr.clickable:hover td { background: var(--border-2); } +.muted { color: var(--text-3); } /* ---------- 弹窗 ---------- */ .modal { diff --git a/dashboard.php b/dashboard.php index a2e733d..001dc4b 100644 --- a/dashboard.php +++ b/dashboard.php @@ -71,7 +71,21 @@ if (!ny_is_auth()) { + + +