feat: 成绩明细打印 + SQLite + 每卷独立配置(满分/合格率/免考) + 导航精简

- 成绩明细打印:默认成绩降序,可选按班级/组别分组对比(段首小计)
- 修复看板退出全屏无反应(exitFullscreen 挂在 document 而非 documentElement)
- 导航栏精简:三页统一为单一主标题,移除 Logo 与副标题
- 优化三端布局:重排断点、竖排隐藏 spacer、触摸设备热区
- 全面启用 SQLite(data/ny.db):配置/名单/每卷配置入库,无 pdo_sqlite 自动降级文件
- 新增独立合格率配置(全局 + 每卷),合格率不再写死 60%
- 新增免考配置,仅对当前试卷生效,试卷间隔离
- 试卷独立满分从设置移出,改到主页面「试卷设置」
- 测试:新增 4 条每卷配置隔离断言,全套 113 项通过
This commit is contained in:
2026-09-10 20:22:17 +08:00
parent 0fd880624f
commit 157f257728
11 changed files with 1049 additions and 210 deletions
+79 -17
View File
@@ -22,11 +22,7 @@ $rosterCount = Roster::count();
<header class="topbar">
<div class="brand">
<span class="logo">NY</span>
<div class="brand-text">
<strong>NY问卷考试系统数据平台</strong>
<small>卷王问卷系统 · 实时数据统计</small>
</div>
<h1 class="brand-title">NY问卷考试系统数据平台</h1>
</div>
<div class="top-right">
<div id="conn" class="conn conn-off"><i></i><span>未连接</span></div>
@@ -99,6 +95,7 @@ $rosterCount = Roster::count();
<button class="btn" id="btnSnapshot" title="保存当前成绩为一份带标签的时间点快照">存快照</button>
<button class="btn" id="btnTrend" title="查看本卷各快照间每名学生的成绩趋势">趋势</button>
<button class="btn" id="btnAnomaly" title="异常检测线索:短时长高分 / 高频重考 / 雷同成绩">异常</button>
<button class="btn btn-primary" id="btnProjCfg" title="为当前选中的试卷单独设置满分 / 合格率 / 免考名单">试卷设置</button>
</div>
</section>
@@ -109,7 +106,7 @@ $rosterCount = Roster::count();
<div class="card accent-teal"><span>平均分</span><b id="sAvg">-</b></div>
<div class="card accent-green"><span>最高分</span><b id="sMax">-</b></div>
<div class="card accent-red"><span>最低分</span><b id="sMin">-</b></div>
<div class="card accent-indigo"><span>合格率(≥60%满分)</span><b id="sPass">-</b></div>
<div class="card accent-indigo"><span>合格率<span id="sPassLabel">(≥60%满分)</span></span><b id="sPass">-</b></div>
<div class="card accent-gray"><span>平均用时</span><b id="sDur">-</b></div>
</section>
@@ -119,6 +116,7 @@ $rosterCount = Roster::count();
<h2>成绩明细 <small id="tableHint"></small></h2>
<div class="updated">
<button class="btn sm" id="btnClearHl" hidden title="清除新数据的绿色高亮标记(每 30 秒自动清除一次)">清除高亮 <b id="hlBadge">0</b></button>
<button class="btn sm" id="btnPrintTable" title="打印成绩明细(默认按成绩降序,可选分组对比)">打印</button>
最后更新:<span id="lastUpdate">—</span><i id="liveDot" class="dot"></i>
</div>
</div>
@@ -197,9 +195,14 @@ $rosterCount = Roster::count();
<p class="hint">实际生效上限 200,超出会自动夹到 200(防止单页内存耗尽)</p>
</div>
<div class="fi">
<label>默认满分(用于合格率)</label>
<label>全局默认满分</label>
<input type="number" id="cfgFull" min="1" max="1000">
<p class="hint" id="cfgFullHint">未选中试卷时的默认值</p>
<p class="hint" id="cfgFullHint">全局默认;每份试卷可在主页面「试卷设置」中单独覆盖</p>
</div>
<div class="fi">
<label>全局默认合格率(%)</label>
<input type="number" id="cfgPassRate" min="1" max="100" step="1">
<p class="hint">达到满分的该百分比即算合格;每份试卷可单独覆盖</p>
</div>
<div class="fi">
<label>历史成绩展示方式</label>
@@ -224,15 +227,6 @@ $rosterCount = Roster::count();
<input type="text" id="cfgKeywords">
<p class="hint">英文逗号分隔,用于自动识别"姓名"题目</p>
</div>
<div class="fi full">
<label>各试卷独立满分</label>
<div id="fullScoresBox" class="fullscores">
<span class="hint">未选中试卷时,此处为空;选中某套试卷后保存即在设置中为该卷单独设定满分</span>
</div>
<!-- 注意:#fullScoresHint 必须放在 #fullScoresBox 之外,
否则 renderFullScores() 的 box.innerHTML 覆盖会把它销毁,导致 textContent 赋值报 null -->
<p class="hint" id="fullScoresHint">给某卷填入数字即可单独覆盖默认满分;留空=用默认</p>
</div>
<div class="fi full checks">
<label><input type="checkbox" id="cfgDedupe"> 同一学号只保留最新一份答卷</label>
<label><input type="checkbox" id="cfgTemp"> 包含"暂存"未提交的答卷</label>
@@ -435,6 +429,74 @@ $rosterCount = Roster::count();
</div>
</div>
<!-- 成绩明细打印设置 -->
<div class="modal" id="printTableModal">
<div class="modal-box">
<div class="modal-head"><h3>打印成绩明细</h3><button class="x" data-close>&times;</button></div>
<div class="modal-body">
<div class="fi">
<label>排序方式</label>
<select id="ptSort">
<option value="score_desc" selected>按成绩降序(默认)</option>
<option value="score_asc">按成绩升序</option>
<option value="sid">按学号</option>
<option value="time">按提交时间</option>
</select>
</div>
<div class="fi">
<label>分组对比</label>
<select id="ptGroup">
<option value="none" selected>不分组(连续打印)</option>
<option value="cls">按班级分组</option>
<option value="grp">按组别分组</option>
</select>
<p class="hint">分组后每个班/组单独一段,段首显示小计(人数/平均分/合格率)</p>
</div>
<div class="fi checks">
<label><input type="checkbox" id="ptRank" checked> 显示名次</label>
<label><input type="checkbox" id="ptAttempts" checked> 显示考试次数</label>
</div>
<p class="hint" id="ptHint"></p>
</div>
<div class="modal-foot">
<span class="spacer"></span>
<button class="btn" data-close>取消</button>
<button class="btn btn-primary" id="btnPtPrint">打印</button>
</div>
</div>
</div>
<!-- 试卷独立设置(满分 / 合格率 / 免考名单)——仅对当前选中试卷生效 -->
<div class="modal" id="projCfgModal">
<div class="modal-box">
<div class="modal-head"><h3>试卷设置 <small id="projCfgSub"></small></h3><button class="x" data-close>&times;</button></div>
<div class="modal-body">
<p class="hint" style="margin:0 0 12px">以下设置<b>仅对当前选中的这份试卷生效</b>,各试卷之间互不影响。留空表示跟随全局默认。</p>
<div class="fi">
<label>本卷满分</label>
<input type="number" id="pcFull" min="1" max="1000" placeholder="留空=用全局默认">
<p class="hint" id="pcFullHint"></p>
</div>
<div class="fi">
<label>本卷合格率(%)</label>
<input type="number" id="pcPassRate" min="1" max="100" step="1" placeholder="留空=用全局默认">
<p class="hint" id="pcPassHint"></p>
</div>
<div class="fi">
<label>免考学生(学号)</label>
<textarea id="pcExempt" rows="4" placeholder="每行一个学号,也可用逗号分隔"></textarea>
<p class="hint">免考学生不计入「名单总人数」与「缺考名单」</p>
</div>
</div>
<div class="modal-foot">
<button class="btn danger" id="btnPcReset">清除本卷设置</button>
<span class="spacer"></span>
<button class="btn" data-close>取消</button>
<button class="btn btn-primary" id="btnPcSave">保存</button>
</div>
</div>
</div>
<!-- 异常检测线索 -->
<div class="modal" id="anomalyModal">
<div class="modal-box wide">