feat: 看板支持A/B组对比下钻 + 全屏 + 删除返回按钮 + 全系统组别(A/B)支持
This commit is contained in:
@@ -148,9 +148,8 @@ try {
|
||||
$rowsByCls[$k][] = $r;
|
||||
}
|
||||
|
||||
$classes = [];
|
||||
foreach ($byCls as $k => $stu) {
|
||||
$rs = $rowsByCls[$k] ?? [];
|
||||
// 通用统计器:给定学生集合 + 答卷集合,算一组指标
|
||||
$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));
|
||||
@@ -159,8 +158,7 @@ try {
|
||||
$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,
|
||||
return [
|
||||
'total' => $total,
|
||||
'submitted' => $submitted,
|
||||
'absent' => max(0, $total - $submitted),
|
||||
@@ -171,6 +169,35 @@ try {
|
||||
'min' => $scores !== [] ? min($scores) : null,
|
||||
'avg_duration' => $avgDur,
|
||||
];
|
||||
};
|
||||
|
||||
$classes = [];
|
||||
foreach ($byCls as $k => $stu) {
|
||||
$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'],
|
||||
|
||||
+5
-3
@@ -287,7 +287,7 @@
|
||||
|
||||
const tb = $('#tbody');
|
||||
if (!rows.length) {
|
||||
tb.innerHTML = '<tr><td colspan="6" class="empty">' +
|
||||
tb.innerHTML = '<tr><td colspan="7" class="empty">' +
|
||||
(state.rows.length ? '没有匹配的记录' : '暂无提交记录') + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
@@ -300,6 +300,7 @@
|
||||
'<td class="sid">' + esc(r.sid) + '</td>' +
|
||||
'<td class="name">' + esc(r.name) + '</td>' +
|
||||
'<td>' + esc(r.cls || '') + '</td>' +
|
||||
'<td>' + esc(r.grp || '') + '</td>' +
|
||||
'<td class="score num ' + scoreClass(r.score) + '">' + (r.score == null ? '-' : r.score) + '</td>' +
|
||||
'<td class="num">' + esc(r.submit_at) + '</td>' +
|
||||
'<td class="num">' + esc(r.duration_txt) + '</td>' +
|
||||
@@ -325,11 +326,11 @@
|
||||
const list = rosterForCls.filter(s => !submittedSids.has(s.sid));
|
||||
const tb = $('#absentBody');
|
||||
if (!list.length) {
|
||||
tb.innerHTML = '<tr><td colspan="3" class="empty">' + (clsFilter ? '本班全部已提交 🎉' : '全部已提交 🎉') + '</td></tr>';
|
||||
tb.innerHTML = '<tr><td colspan="4" class="empty">' + (clsFilter ? '本班全部已提交 🎉' : '全部已提交 🎉') + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
tb.innerHTML = list.map(a =>
|
||||
'<tr><td class="sid">' + esc(a.sid) + '</td><td>' + esc(a.name) + '</td><td>' + esc(a.cls || '') + '</td></tr>').join('');
|
||||
'<tr><td class="sid">' + esc(a.sid) + '</td><td>' + esc(a.name) + '</td><td>' + esc(a.cls || '') + '</td><td>' + esc(a.grp || '') + '</td></tr>').join('');
|
||||
}
|
||||
|
||||
function renderUnmatched() {
|
||||
@@ -425,6 +426,7 @@
|
||||
'<td><input value="' + esc(r.sid) + '" data-f="sid"></td>' +
|
||||
'<td><input value="' + esc(r.name) + '" data-f="name"></td>' +
|
||||
'<td><input value="' + esc(r.cls || '') + '" data-f="cls" placeholder="可选"></td>' +
|
||||
'<td><input value="' + esc(r.grp || '') + '" data-f="grp" placeholder="A/B"></td>' +
|
||||
'<td><span class="link-del" data-del="' + gi + '">删除</span></td></tr>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
|
||||
|
||||
+131
-29
@@ -33,10 +33,16 @@ require __DIR__ . '/bootstrap.php';
|
||||
<option value="60">60 秒</option>
|
||||
</select>
|
||||
<button class="btn" id="btnRefresh">刷新</button>
|
||||
<a class="back" href="index.php">← 返回主系统</a>
|
||||
<button class="btn" id="btnFullscreen" title="切换全屏">⛶ 全屏</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:14px;font-size:12px;color:var(--text-3);display:flex;align-items:center;gap:6px;">
|
||||
最后更新:<span id="lastUpdate">—</span><i id="liveDot" class="dot"></i>
|
||||
</div>
|
||||
|
||||
<!-- ================= 总览 ================= -->
|
||||
<div id="overview">
|
||||
<div class="sec-title">整体概况</div>
|
||||
<section class="cards" id="summary">
|
||||
<div class="card accent-blue"><span>名单总人数</span><b id="oTotal">-</b></div>
|
||||
@@ -47,49 +53,53 @@ require __DIR__ . '/bootstrap.php';
|
||||
<div class="card accent-indigo"><span>合格率(≥60%满分)</span><b id="oPass">-</b></div>
|
||||
</section>
|
||||
|
||||
<div class="sec-title">各班整体情况 <span class="tag-gray" id="classCount"></span></div>
|
||||
<div class="sec-title">各班整体情况 <span class="tag-gray" id="classCount"></span>
|
||||
<span class="hint" style="margin-left:auto;font-weight:500">点击班级卡片查看该班详情</span>
|
||||
</div>
|
||||
<div class="class-grid" id="classGrid">
|
||||
<div class="empty">请选择问卷 / 考试以查看各班数据</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:18px;font-size:12px;color:var(--text-3);display:flex;align-items:center;gap:6px;">
|
||||
最后更新:<span id="lastUpdate">—</span><i id="liveDot" class="dot"></i>
|
||||
</div>
|
||||
|
||||
<!-- ================= 班级详情 ================= -->
|
||||
<div id="detail" class="hidden">
|
||||
<div class="detail-head">
|
||||
<button class="btn" id="btnBack">← 返回总览</button>
|
||||
<h2 id="detailTitle">班级详情</h2>
|
||||
<span class="tag-gray" id="detailSub"></span>
|
||||
</div>
|
||||
|
||||
<section class="cards" id="detailSummary"></section>
|
||||
|
||||
<div class="sec-title" id="groupSecTitle">分组对比</div>
|
||||
<div id="groupCompare"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const $ = s => document.querySelector(s);
|
||||
const $$ = s => Array.from(document.querySelectorAll(s));
|
||||
function esc(s){ return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
||||
function fmtDur(sec){
|
||||
if (sec == null) return '-';
|
||||
if (sec < 60) return sec + ' 秒';
|
||||
const m = Math.floor(sec / 60), s = sec % 60;
|
||||
if (m < 60) return m + ' 分 ' + String(s).padStart(2, '0') + ' 秒';
|
||||
const h = Math.floor(m / 60);
|
||||
return h + ' 小时 ' + String(m % 60).padStart(2, '0') + ' 分';
|
||||
}
|
||||
function rateColor(r){
|
||||
if (r >= 90) return 'var(--ok)';
|
||||
if (r >= 60) return 'var(--warn)';
|
||||
return 'var(--danger)';
|
||||
}
|
||||
|
||||
let DASH = null; // 最近一次看板数据
|
||||
let CUR = null; // 当前查看的班级名
|
||||
|
||||
async function api(action, opts){
|
||||
opts = opts || {};
|
||||
let url = 'api.php?action=' + encodeURIComponent(action);
|
||||
if (opts.params) {
|
||||
for (const k in opts.params) url += '&' + k + '=' + encodeURIComponent(opts.params[k]);
|
||||
}
|
||||
const init = { method: 'GET', headers: { 'Accept': 'application/json' } };
|
||||
try {
|
||||
const res = await fetch(url, init);
|
||||
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
||||
const j = await res.json();
|
||||
if (j && j.ok) return j.data;
|
||||
throw new Error((j && (j.msg || j.error)) || '请求失败');
|
||||
} catch (e) {
|
||||
throw new Error(e.message || '网络错误');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProjects(){
|
||||
@@ -110,9 +120,9 @@ function renderSummary(d){
|
||||
$('#oTotal').textContent = o.roster_total ?? '-';
|
||||
$('#oSubmit').textContent = o.submitted ?? '-';
|
||||
$('#oAbsent').textContent = o.absent ?? '-';
|
||||
$('#oRate').textContent = (o.submit_rate != null ? o.submit_rate : '-') + (o.submit_rate != null ? '%' : '');
|
||||
$('#oRate').textContent = (o.submit_rate != null ? o.submit_rate + '%' : '-');
|
||||
$('#oAvg').textContent = o.avg_score != null ? o.avg_score : '-';
|
||||
$('#oPass').textContent = (o.pass_rate != null ? o.pass_rate : '-') + (o.pass_rate != null ? '%' : '');
|
||||
$('#oPass').textContent = (o.pass_rate != null ? o.pass_rate + '%' : '-');
|
||||
}
|
||||
|
||||
function renderClasses(d){
|
||||
@@ -125,21 +135,79 @@ function renderClasses(d){
|
||||
$('#classGrid').innerHTML = classes.map(c => {
|
||||
const rate = c.rate || 0;
|
||||
const color = rateColor(rate);
|
||||
const passCls = c.pass_rate >= 60 ? 'metric-ok' : (c.pass_rate > 0 ? 'metric-warn' : '');
|
||||
return '<div class="class-card">' +
|
||||
const gs = (c.groups || []).filter(g => g.group !== '(未分组)');
|
||||
const gtag = gs.length ? gs.map(g => esc(g.group) + '组').join(' / ') : '';
|
||||
return '<div class="class-card clickable" data-cls="' + esc(c.class) + '">' +
|
||||
'<h3><span>' + esc(c.class) + '</span><span class="rate-num" style="color:' + color + '">' + rate + '%</span></h3>' +
|
||||
'<div class="sub">已提交 ' + c.submitted + ' / 总人数 ' + c.total + ' · 缺考 ' + c.absent + '</div>' +
|
||||
'<div class="bar"><i style="width:' + rate + '%;background:' + color + '"></i></div>' +
|
||||
'<div class="class-metrics">' +
|
||||
'<div class="m"><span>平均分</span><b>' + (c.avg != null ? c.avg : '-') + '</b></div>' +
|
||||
'<div class="m ' + passCls + '"><span>合格率</span><b>' + c.pass_rate + '%</b></div>' +
|
||||
'<div class="m"><span>合格率</span><b>' + c.pass_rate + '%</b></div>' +
|
||||
'<div class="m metric-ok"><span>最高分</span><b>' + (c.max != null ? c.max : '-') + '</b></div>' +
|
||||
'<div class="m metric-danger"><span>最低分</span><b>' + (c.min != null ? c.min : '-') + '</b></div>' +
|
||||
'</div>' +
|
||||
(gtag ? '<div class="grp-tag">' + gtag + '</div>' : '') +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* ---------- 班级详情:整体 + A/B 分组对比 ---------- */
|
||||
function showClassDetail(name){
|
||||
if (!DASH) return;
|
||||
const c = (DASH.classes || []).find(x => x.class === name);
|
||||
if (!c) return;
|
||||
CUR = name;
|
||||
|
||||
$('#overview').classList.add('hidden');
|
||||
$('#detail').classList.remove('hidden');
|
||||
|
||||
$('#detailTitle').textContent = c.class;
|
||||
$('#detailSub').textContent = '总人数 ' + c.total + ' · 已提交 ' + c.submitted + ' · 缺考 ' + c.absent;
|
||||
|
||||
const color = rateColor(c.rate || 0);
|
||||
$('#detailSummary').innerHTML =
|
||||
'<div class="card accent-purple"><span>提交率</span><b style="color:' + color + '">' + c.rate + '%</b></div>' +
|
||||
'<div class="card ok accent-green"><span>已提交</span><b>' + c.submitted + '</b></div>' +
|
||||
'<div class="card warn accent-orange"><span>缺考</span><b>' + c.absent + '</b></div>' +
|
||||
'<div class="card accent-blue"><span>总人数</span><b>' + c.total + '</b></div>' +
|
||||
'<div class="card accent-teal"><span>平均分</span><b>' + (c.avg != null ? c.avg : '-') + '</b></div>' +
|
||||
'<div class="card accent-indigo"><span>合格率</span><b>' + c.pass_rate + '%</b></div>' +
|
||||
'<div class="card accent-green"><span>最高分</span><b>' + (c.max != null ? c.max : '-') + '</b></div>' +
|
||||
'<div class="card accent-red"><span>最低分</span><b>' + (c.min != null ? c.min : '-') + '</b></div>';
|
||||
|
||||
// 分组对比:过滤掉“未分组”,按组名排序(A 在左,B 在右)
|
||||
const groups = (c.groups || []).filter(g => g.group !== '(未分组)');
|
||||
const box = $('#groupCompare');
|
||||
if (!groups.length) {
|
||||
$('#groupSecTitle').textContent = '分组对比';
|
||||
box.innerHTML = '<div class="empty">该班级未设置分组(A/B),仅显示整体数据</div>';
|
||||
return;
|
||||
}
|
||||
$('#groupSecTitle').textContent = '分组对比(' + groups.map(g => g.group).join(' vs ') + ')';
|
||||
box.innerHTML = '<div class="group-compare">' + groups.map(g => {
|
||||
const gc = rateColor(g.rate || 0);
|
||||
return '<div class="gpanel">' +
|
||||
'<div class="ghead"><strong>' + esc(g.group) + ' 组</strong>' +
|
||||
'<span class="rate-num" style="color:' + gc + '">' + g.rate + '%</span></div>' +
|
||||
'<div class="sub">已提交 ' + g.submitted + ' / 总人数 ' + g.total + ' · 缺考 ' + g.absent + '</div>' +
|
||||
'<div class="bar"><i style="width:' + g.rate + '%;background:' + gc + '"></i></div>' +
|
||||
'<div class="class-metrics">' +
|
||||
'<div class="m"><span>平均分</span><b>' + (g.avg != null ? g.avg : '-') + '</b></div>' +
|
||||
'<div class="m"><span>合格率</span><b>' + g.pass_rate + '%</b></div>' +
|
||||
'<div class="m metric-ok"><span>最高分</span><b>' + (g.max != null ? g.max : '-') + '</b></div>' +
|
||||
'<div class="m metric-danger"><span>最低分</span><b>' + (g.min != null ? g.min : '-') + '</b></div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
|
||||
function backToOverview(){
|
||||
CUR = null;
|
||||
$('#detail').classList.add('hidden');
|
||||
$('#overview').classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function loadDashboard(){
|
||||
const pid = $('#projectSel').value;
|
||||
if (!pid) {
|
||||
@@ -147,17 +215,40 @@ async function loadDashboard(){
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const d = await api('dashboard', { params: { projectId: pid } });
|
||||
renderSummary(d);
|
||||
renderClasses(d);
|
||||
$('#lastUpdate').textContent = d.updated_at || '-';
|
||||
DASH = await api('dashboard', { params: { projectId: pid } });
|
||||
renderSummary(DASH);
|
||||
renderClasses(DASH);
|
||||
$('#lastUpdate').textContent = DASH.updated_at || '-';
|
||||
$('#liveDot').classList.add('on');
|
||||
setTimeout(() => $('#liveDot').classList.remove('on'), 900);
|
||||
// 若正在看某班详情,刷新后同步更新
|
||||
if (CUR) {
|
||||
const still = (DASH.classes || []).some(x => x.class === CUR);
|
||||
if (still) showClassDetail(CUR); else backToOverview();
|
||||
}
|
||||
} catch (e) {
|
||||
$('#classGrid').innerHTML = '<div class="empty">数据加载失败:' + esc(e.message) + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 全屏 ---------- */
|
||||
function toggleFullscreen(){
|
||||
const el = document.documentElement;
|
||||
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
|
||||
const req = el.requestFullscreen || el.webkitRequestFullscreen;
|
||||
if (req) req.call(el);
|
||||
} else {
|
||||
const ex = document.exitFullscreen || document.webkitExitFullscreen;
|
||||
if (ex) ex.call(document);
|
||||
}
|
||||
}
|
||||
function onFsChange(){
|
||||
const on = !!(document.fullscreenElement || document.webkitFullscreenElement);
|
||||
document.body.classList.toggle('fs-on', on);
|
||||
const btn = $('#btnFullscreen');
|
||||
if (btn) btn.textContent = on ? '⛶ 退出全屏' : '⛶ 全屏';
|
||||
}
|
||||
|
||||
function schedule(){
|
||||
if (window.__timer) clearInterval(window.__timer);
|
||||
if (!$('#autoRefresh').checked) return;
|
||||
@@ -165,10 +256,21 @@ function schedule(){
|
||||
window.__timer = setInterval(loadDashboard, sec * 1000);
|
||||
}
|
||||
|
||||
$('#projectSel').addEventListener('change', loadDashboard);
|
||||
/* ---------- 事件 ---------- */
|
||||
$('#projectSel').addEventListener('change', () => { backToOverview(); loadDashboard(); });
|
||||
$('#btnRefresh').addEventListener('click', loadDashboard);
|
||||
$('#autoRefresh').addEventListener('change', schedule);
|
||||
$('#intervalSel').addEventListener('change', schedule);
|
||||
$('#btnFullscreen').addEventListener('click', toggleFullscreen);
|
||||
document.addEventListener('fullscreenchange', onFsChange);
|
||||
document.addEventListener('webkitfullscreenchange', onFsChange);
|
||||
$('#btnBack').addEventListener('click', backToOverview);
|
||||
|
||||
// 班级卡片点击进入详情
|
||||
$('#classGrid').addEventListener('click', e => {
|
||||
const card = e.target.closest('.class-card');
|
||||
if (card && card.dataset.cls) showClassDetail(card.dataset.cls);
|
||||
});
|
||||
|
||||
loadProjects().then(loadDashboard);
|
||||
schedule();
|
||||
|
||||
@@ -124,12 +124,13 @@ $rosterCount = Roster::count();
|
||||
<th class="sortable" data-k="sid">学号</th>
|
||||
<th class="sortable" data-k="name">姓名</th>
|
||||
<th class="sortable" data-k="cls">班级</th>
|
||||
<th class="sortable" data-k="grp">组别</th>
|
||||
<th class="sortable" data-k="score">成绩</th>
|
||||
<th class="sortable" data-k="submit_ts">提交时间</th>
|
||||
<th class="sortable" data-k="duration">考试时长</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody"><tr><td colspan="6" class="empty">请选择问卷 / 考试</td></tr></tbody>
|
||||
<tbody id="tbody"><tr><td colspan="7" class="empty">请选择问卷 / 考试</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
@@ -139,8 +140,8 @@ $rosterCount = Roster::count();
|
||||
<div class="panel-head"><h2>缺考名单 <small id="absentHint"></small></h2></div>
|
||||
<div class="table-scroll short">
|
||||
<table class="grid compact">
|
||||
<thead><tr><th>学号</th><th>姓名</th><th>班级</th></tr></thead>
|
||||
<tbody id="absentBody"><tr><td colspan="3" class="empty">—</td></tr></tbody>
|
||||
<thead><tr><th>学号</th><th>姓名</th><th>班级</th><th>组别</th></tr></thead>
|
||||
<tbody id="absentBody"><tr><td colspan="4" class="empty">—</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
@@ -229,7 +230,7 @@ $rosterCount = Roster::count();
|
||||
<input type="file" id="rosterFile" accept=".xlsx,.xlsm,.csv,.txt" hidden>
|
||||
<div class="uz-inner">
|
||||
<strong>点击或拖拽文件到此上传</strong>
|
||||
<span>支持 .xlsx / .csv,需包含「学号」「姓名」两列(「班级」可选)</span>
|
||||
<span>支持 .xlsx / .csv,需包含「学号」「姓名」两列(「班级」「组别」可选,组别如 A/B)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="roster-bar">
|
||||
@@ -245,7 +246,7 @@ $rosterCount = Roster::count();
|
||||
</div>
|
||||
<div class="table-scroll mid">
|
||||
<table class="grid compact" id="rosterTable">
|
||||
<thead><tr><th style="width:32%">学号</th><th style="width:26%">姓名</th><th>班级</th><th style="width:70px">操作</th></tr></thead>
|
||||
<thead><tr><th style="width:28%">学号</th><th style="width:22%">姓名</th><th>班级</th><th>组别</th><th style="width:70px">操作</th></tr></thead>
|
||||
<tbody id="rosterBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -92,6 +92,7 @@ class DataService
|
||||
$studentInfo[$k] = [
|
||||
'sid' => (string)$s['sid'],
|
||||
'cls' => (string)($s['cls'] ?? ''),
|
||||
'grp' => (string)($s['grp'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -203,11 +204,13 @@ class DataService
|
||||
$info = $studentInfo[$key] ?? null;
|
||||
$sid = $info['sid'] ?? '';
|
||||
$cls = $info['cls'] ?? '';
|
||||
$grp = $info['grp'] ?? '';
|
||||
|
||||
$row = [
|
||||
'sid' => $sid,
|
||||
'name' => $name,
|
||||
'cls' => $cls,
|
||||
'grp' => $grp,
|
||||
'score' => $score,
|
||||
'submit_ts' => $ts,
|
||||
'submit_at' => $ts > 0 ? date('Y-m-d H:i:s', $ts) : '-',
|
||||
@@ -262,6 +265,7 @@ class DataService
|
||||
'sid' => (string)$s['sid'],
|
||||
'name' => (string)$s['name'],
|
||||
'cls' => (string)($s['cls'] ?? ''),
|
||||
'grp' => (string)($s['grp'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -29,6 +29,7 @@ class Roster
|
||||
'sid' => trim((string)($s['sid'] ?? '')),
|
||||
'name' => trim((string)($s['name'] ?? '')),
|
||||
'cls' => trim((string)($s['cls'] ?? '')),
|
||||
'grp' => trim((string)($s['grp'] ?? '')),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
@@ -56,6 +57,7 @@ class Roster
|
||||
'sid' => $sid,
|
||||
'name' => $name,
|
||||
'cls' => trim((string)($s['cls'] ?? '')),
|
||||
'grp' => trim((string)($s['grp'] ?? '')),
|
||||
];
|
||||
}
|
||||
return ny_write_json(self::file(), [
|
||||
@@ -177,6 +179,7 @@ class Roster
|
||||
$sidCol = -1;
|
||||
$nameCol = -1;
|
||||
$clsCol = -1;
|
||||
$grpCol = -1;
|
||||
$headerIdx = -1;
|
||||
|
||||
// 找表头
|
||||
@@ -202,6 +205,9 @@ class Roster
|
||||
if ($clsCol < 0 && preg_match('/班\s*级|班级|class/i', $c)) {
|
||||
$clsCol = $j;
|
||||
}
|
||||
if ($grpCol < 0 && preg_match('/组\s*别|分\s*组|^\s*组\s*$|group/i', $c)) {
|
||||
$grpCol = $j;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($sidCol < 0) {
|
||||
@@ -222,7 +228,8 @@ class Roster
|
||||
// 学号去掉 .0 尾巴(Excel 数字读取)
|
||||
$sid = preg_replace('/\.0+$/', '', $sid) ?? $sid;
|
||||
$cls = $clsCol >= 0 ? trim((string)($row[$clsCol] ?? '')) : '';
|
||||
$out[] = ['sid' => $sid, 'name' => $name, 'cls' => $cls];
|
||||
$grp = $grpCol >= 0 ? trim((string)($row[$grpCol] ?? '')) : '';
|
||||
$out[] = ['sid' => $sid, 'name' => $name, 'cls' => $cls, 'grp' => $grp];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user