From 1b7683baab0069714dfe6c6ef900d146a12edb89 Mon Sep 17 00:00:00 2001 From: nxxy335 Date: Tue, 8 Sep 2026 19:44:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9C=8B=E6=9D=BF=E6=94=AF=E6=8C=81A/B?= =?UTF-8?q?=E7=BB=84=E5=AF=B9=E6=AF=94=E4=B8=8B=E9=92=BB=20+=20=E5=85=A8?= =?UTF-8?q?=E5=B1=8F=20+=20=E5=88=A0=E9=99=A4=E8=BF=94=E5=9B=9E=E6=8C=89?= =?UTF-8?q?=E9=92=AE=20+=20=E5=85=A8=E7=B3=BB=E7=BB=9F=E7=BB=84=E5=88=AB(A?= =?UTF-8?q?/B)=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api.php | 77 ++++++++++++------ assets/app.js | 8 +- assets/style.css | 29 +++++++ dashboard.php | 190 ++++++++++++++++++++++++++++++++++---------- index.php | 11 +-- lib/DataService.php | 4 + lib/Roster.php | 9 ++- 7 files changed, 251 insertions(+), 77 deletions(-) diff --git a/api.php b/api.php index 351b230..92c922e 100644 --- a/api.php +++ b/api.php @@ -148,29 +148,56 @@ try { $rowsByCls[$k][] = $r; } + // 通用统计器:给定学生集合 + 答卷集合,算一组指标 + $statOf = static function (array $stu, array $rs, int $fs): array { + $total = count($stu); + $submitted = count($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))); + $passRate = $submitted > 0 ? round($pass / $submitted * 100, 1) : 0; + $durs = array_values(array_filter(array_map(static fn($r) => $r['duration'] ?? null, $rs), static fn($v) => $v !== null)); + $avgDur = $durs !== [] ? (int)round(array_sum($durs) / count($durs)) : null; + return [ + 'total' => $total, + 'submitted' => $submitted, + 'absent' => max(0, $total - $submitted), + 'rate' => $total > 0 ? round($submitted / $total * 100, 1) : 0, + 'avg' => $avg, + 'pass_rate' => $passRate, + 'max' => $scores !== [] ? max($scores) : null, + 'min' => $scores !== [] ? min($scores) : null, + 'avg_duration' => $avgDur, + ]; + }; + $classes = []; foreach ($byCls as $k => $stu) { - $rs = $rowsByCls[$k] ?? []; - $total = count($stu); - $submitted = count($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))); - $passRate = $submitted > 0 ? round($pass / $submitted * 100, 1) : 0; - $durs = array_values(array_filter(array_map(static fn($r) => $r['duration'] ?? null, $rs), static fn($v) => $v !== null)); - $avgDur = $durs !== [] ? (int)round(array_sum($durs) / count($durs)) : null; - $classes[] = [ - 'class' => $k, - 'total' => $total, - 'submitted' => $submitted, - 'absent' => max(0, $total - $submitted), - 'rate' => $total > 0 ? round($submitted / $total * 100, 1) : 0, - 'avg' => $avg, - 'pass_rate' => $passRate, - 'max' => $scores !== [] ? max($scores) : null, - 'min' => $scores !== [] ? min($scores) : null, - 'avg_duration' => $avgDur, - ]; + $rs = $rowsByCls[$k] ?? []; + $row = $statOf($stu, $rs, $fs); + $row['class'] = $k; + + // 班级内按组别(A/B 等)再聚合,用于看板下钻对比 + $stuByGrp = []; + foreach ($stu as $s) { + $g = trim((string)($s['grp'] ?? '')) ?: '(未分组)'; + $stuByGrp[$g][] = $s; + } + $rowsByGrp = []; + foreach ($rs as $r) { + $g = trim((string)($r['grp'] ?? '')) ?: '(未分组)'; + $rowsByGrp[$g][] = $r; + } + $groups = []; + foreach ($stuByGrp as $g => $gstu) { + $gs = $statOf($gstu, $rowsByGrp[$g] ?? [], $fs); + $gs['group'] = $g; + $groups[] = $gs; + } + usort($groups, static fn(array $a, array $b): int => strcmp((string)$a['group'], (string)$b['group'])); + $row['groups'] = $groups; + + $classes[] = $row; } usort($classes, static function (array $a, array $b): int { $an = is_numeric($a['class']) ? (int)$a['class'] : $a['class']; @@ -233,6 +260,7 @@ try { 'sid' => (string)($s['sid'] ?? ''), 'name' => (string)($s['name'] ?? ''), 'cls' => trim((string)($s['cls'] ?? '')), + 'grp' => trim((string)($s['grp'] ?? '')), ]; } Roster::save($students); @@ -263,17 +291,18 @@ try { $out = fopen('php://output', 'w'); if ($action === 'export_absent') { - fputcsv($out, ['学号', '姓名', '班级']); + fputcsv($out, ['学号', '姓名', '班级', '组别']); foreach ($result['absent'] as $r) { - fputcsv($out, [$r['sid'], $r['name'], $r['cls'] ?? '']); + fputcsv($out, [$r['sid'], $r['name'], $r['cls'] ?? '', $r['grp'] ?? '']); } } else { - fputcsv($out, ['学号', '姓名', '班级', '成绩', '提交时间', '考试时长', '考试时长(秒)']); + fputcsv($out, ['学号', '姓名', '班级', '组别', '成绩', '提交时间', '考试时长', '考试时长(秒)']); foreach ($result['rows'] as $r) { fputcsv($out, [ $r['sid'], $r['name'], $r['cls'] ?? '', + $r['grp'] ?? '', $r['score'] === null ? '' : $r['score'], $r['submit_at'], $r['duration_txt'], diff --git a/assets/app.js b/assets/app.js index f5af05d..d194a15 100644 --- a/assets/app.js +++ b/assets/app.js @@ -287,7 +287,7 @@ const tb = $('#tbody'); if (!rows.length) { - tb.innerHTML = '' + + tb.innerHTML = '' + (state.rows.length ? '没有匹配的记录' : '暂无提交记录') + ''; return; } @@ -300,6 +300,7 @@ '' + esc(r.sid) + '' + '' + esc(r.name) + '' + '' + esc(r.cls || '') + '' + + '' + esc(r.grp || '') + '' + '' + (r.score == null ? '-' : r.score) + '' + '' + esc(r.submit_at) + '' + '' + esc(r.duration_txt) + '' + @@ -325,11 +326,11 @@ const list = rosterForCls.filter(s => !submittedSids.has(s.sid)); const tb = $('#absentBody'); if (!list.length) { - tb.innerHTML = '' + (clsFilter ? '本班全部已提交 🎉' : '全部已提交 🎉') + ''; + tb.innerHTML = '' + (clsFilter ? '本班全部已提交 🎉' : '全部已提交 🎉') + ''; return; } tb.innerHTML = list.map(a => - '' + esc(a.sid) + '' + esc(a.name) + '' + esc(a.cls || '') + '').join(''); + '' + esc(a.sid) + '' + esc(a.name) + '' + esc(a.cls || '') + '' + esc(a.grp || '') + '').join(''); } function renderUnmatched() { @@ -425,6 +426,7 @@ '' + '' + '' + + '' + '删除'; }).join(''); } diff --git a/assets/style.css b/assets/style.css index 75565ca..31c151c 100644 --- a/assets/style.css +++ b/assets/style.css @@ -476,3 +476,32 @@ body.fs-on .topbar { padding-top: 8px; padding-bottom: 8px; } .empty { text-align: center; color: var(--text-3); padding: 40px 0; } +/* 班级卡片:可点击进入该班详情 */ +.class-card.clickable { cursor: pointer; } +.class-card.clickable:hover { border-color: var(--primary); } +.grp-tag { + display: inline-block; margin-top: 12px; padding: 2px 10px; border-radius: 999px; + font-size: 11px; font-weight: 600; color: var(--primary); background: var(--primary-weak); +} + +/* 班级详情头部 */ +.detail-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 18px; } +.detail-head h2 { margin: 0; font-size: 18px; font-weight: 700; } + +/* 分组对比:A 在左、B 在右 */ +.group-compare { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 14px; } +@media (max-width: 860px) { .group-compare { grid-template-columns: 1fr; } } +.gpanel { + background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); + padding: 16px 18px; box-shadow: var(--shadow); +} +.gpanel .ghead { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; } +.gpanel .ghead strong { font-size: 16px; } +.gpanel .ghead .rate-num { font-size: 15px; font-weight: 800; } + +/* 看板全屏适配 */ +body.fs-on .wrap { max-width: none; padding-top: 14px; } +body.fs-on .class-grid { grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); } +body.fs-on .group-compare { grid-template-columns: 1fr 1fr; } +body.fs-on .dash-hero { border-radius: var(--radius); } + diff --git a/dashboard.php b/dashboard.php index 7c71c58..33e2c60 100644 --- a/dashboard.php +++ b/dashboard.php @@ -33,63 +33,73 @@ require __DIR__ . '/bootstrap.php'; - ← 返回主系统 + -
整体概况
-
-
名单总人数-
-
已提交-
-
缺考-
-
提交率-
-
平均分-
-
合格率(≥60%满分)-
-
- -
各班整体情况
-
-
请选择问卷 / 考试以查看各班数据
-
- -
+
最后更新:—
+ + +
+
整体概况
+
+
名单总人数-
+
已提交-
+
缺考-
+
提交率-
+
平均分-
+
合格率(≥60%满分)-
+
+ +
各班整体情况 + 点击班级卡片查看该班详情 +
+
+
请选择问卷 / 考试以查看各班数据
+
+
+ + + +