feat(export/dashboard): 导出增强与大屏公开化

导出-每次考试/多考试对比:
- 合格次数阈值等标记项抽为常显区块,三类成绩表共用
- 附缺考名单扩展到多考试导出(取所选考试全都没交的人)
- 块表每场固定2列(成绩+合并空位),修复漏推空串导致的整行左移/串列
- 列宽按实际内容估算,合并区空位只给最小宽度
- 合格次数始终显示实际次数(含0),仅 ≥ 阈值标绿底
- 打印标题改为「成绩明细」,缺考名单分页单独成页
- 打印样式加 print-color-adjust:exact,绿底/红字可正常印出

数据大屏:
- ?class= 班级大屏免登录公开访问,接口侧放行并按班级裁剪后回传
- 单班大屏移除退出按钮,仅首页保留
- 修复红绿判定真bug:membersTable 误用 window.DASH(DASH为let声明取不到)
  改为 scoreGrade() 直接读自由变量,按后台合格率正确判红/黄/绿

测试/样例/文档同步更新,全量回归通过。
This commit is contained in:
2026-09-27 22:04:17 +08:00
parent 0ba9c7c33c
commit d05fd4a9b5
34 changed files with 4290 additions and 305 deletions
+182 -37
View File
@@ -80,24 +80,9 @@ $rosterCount = Roster::count();
<section class="subbar">
<span class="subbar-label">导出</span>
<label class="subbar-item">排序
<select id="exportSortSel">
<option value="time">按提交时间</option>
<option value="sid">按学号</option>
<option value="score_desc">按成绩降序</option>
<option value="score_asc">按成绩升序</option>
</select>
</label>
<label class="subbar-item" title="每人只导一行:最近一次成绩 + 最高分 + 考试次数">
<input type="checkbox" id="exportLatest">按最新成绩
</label>
<label class="subbar-item" title="按班级拆分多个工作表(xlsx 多表)">
<input type="checkbox" id="exportSplit">按班级分表
</label>
<span class="hint" id="exportBarHint">成绩明细 / 每次考试 / 缺考名单 / 多考试对比,都在「导出」里配置</span>
<span class="spacer"></span>
<button class="btn" id="btnExport">导出成绩</button>
<button class="btn" id="btnExportAbsent">导出缺考</button>
<button class="btn" id="btnExportMulti" title="勾选多场考试,横向对比同一批学生的成绩">多考试对比导出</button>
<button class="btn btn-primary" id="btnExportOpen" title="导出与打印:选好导出内容、范围与标记方式,再导出 Excel 或直接打印">导出</button>
</section>
<section class="cards" id="statCards">
@@ -425,10 +410,18 @@ $rosterCount = Roster::count();
<div class="fi">
<label>打印范围</label>
<select id="ptScope">
<option value="latest" selected>按最新成绩打印(每人一行)</option>
<option value="all">打印全部记录(含每位学生历史提交)</option>
<option value="latest" selected>每人一行(汇总)</option>
<option value="all">每次考试一行(含历史提交)</option>
</select>
<p class="hint">全部记录会把每位学生的每次提交各打一行,完整留存历史;按最新成绩则每人仅显示当前代表成绩。</p>
<p class="hint">「每次考试一行」会把每位学生的每次提交各打一行,完整留存历史。</p>
</div>
<div class="fi">
<label>每人成绩口径</label>
<select id="ptRowScore" title="每人一行时的代表成绩;选「每次考试一行」时本项不生效">
<option value="latest" selected>最新成绩(最近一次提交)</option>
<option value="best">最高成绩(该生最高分)</option>
</select>
<p class="hint">与「导出 → 成绩明细」的「每人成绩」一致,默认最新成绩。</p>
</div>
<div class="fi checks">
<label><input type="checkbox" id="ptRank" checked> 显示名次(全表连续编号)</label>
@@ -437,6 +430,7 @@ $rosterCount = Roster::count();
<label><input type="checkbox" id="ptAbsent" title="成绩表后另起一页附缺考名单,含备注列供手写"> 附缺考名单(另起一页)</label>
</div>
<p class="hint">打印内容<b>不受页面当前筛选外的影响</b>,始终按当前班级/组别筛选结果输出;表头会在每页自动重复,末尾附签字栏。</p>
<p class="hint">要打印<b>多场考试对比</b>或<b>「每次考试」表</b>(最高分标红、合格次数达标绿底)?用工具栏的 <b>导出</b> 弹窗,选「多考试对比」或「每次考试」后点「打印」。</p>
<p class="hint" id="ptHint"></p>
</div>
<div class="modal-foot">
@@ -478,35 +472,186 @@ $rosterCount = Roster::count();
</div>
</div>
<!-- 多考试对比导出 -->
<div class="modal" id="multiExportModal">
<!-- ================= 统一导出 / 打印 =================
所有导出(成绩明细 / 每次考试 / 缺考名单 / 多考试对比)与打印都在这一个弹窗里配置:
先选「导出什么」,再按需展开「范围」「列与标记」「打印」三个区块。 -->
<div class="modal" id="exportModal">
<div class="modal-box wide">
<div class="modal-head"><h3>多考试成绩对比导出</h3><button class="x" data-close>&times;</button></div>
<div class="modal-head"><h3>导出 / 打印</h3><button class="x" data-close>&times;</button></div>
<div class="modal-body">
<p class="hint" style="margin:0 0 12px">勾选多场考试后导出一张横向对比表:第一行是考试名称,前两列是学号 / 姓名,默认按学号排序;每场考试的成绩可按「最新」或「最高」显示。</p>
<div class="me-bar">
<label class="chk"><input type="radio" name="meScoreMode" value="latest" checked> 最新成绩</label>
<label class="chk"><input type="radio" name="meScoreMode" value="highest"> 最高成绩</label>
<span class="spacer"></span>
<button class="btn sm" id="btnMeSelAll">全选</button>
<button class="btn sm" id="btnMeClear">清空</button>
<!-- 1. 导出内容 -->
<div class="ex-sec">
<div class="ex-sec-title">导出内容</div>
<div class="ex-types">
<label class="ex-type" data-for="detail">
<input type="radio" name="exType" value="detail" checked>
<span class="ex-type-t">
<b>成绩明细</b>
<small>每人一行:学号 / 姓名 / 班级 / 组别 / 成绩 / 次数 / 时间 / 时长</small>
</span>
</label>
<label class="ex-type" data-for="attempts">
<input type="radio" name="exType" value="attempts">
<span class="ex-type-t">
<b>每次考试</b>
<small>按截图版式:一行一次考试,最高分标红、合格次数标绿</small>
</span>
</label>
<label class="ex-type" data-for="absent">
<input type="radio" name="exType" value="absent">
<span class="ex-type-t">
<b>缺考名单</b>
<small>学号 / 姓名 / 班级 / 组别(跟随下方筛选范围)</small>
</span>
</label>
<label class="ex-type" data-for="multi">
<input type="radio" name="exType" value="multi">
<span class="ex-type-t">
<b>多考试对比</b>
<small>勾选多场考试,横向对比:每场一列,最高分标红、合格次数标绿</small>
</span>
</label>
</div>
</div>
<div class="table-scroll mid">
<table class="grid compact" id="meTable">
<thead><tr><th style="width:6%"><input type="checkbox" id="meCheckAll" title="全选"></th><th style="width:8%">顺序</th><th>考试名称</th></tr></thead>
<tbody id="meBody"><tr><td colspan="3" class="empty">(暂无可导出的考试,请先在右上角刷新问卷列表)</td></tr></tbody>
</table>
<!-- 2. 统计范围(班级 / 组别) -->
<div class="ex-sec" id="exScopeSec">
<div class="ex-sec-title">统计范围 <small id="exScopeSum"></small></div>
<div class="ex-row">
<label class="ex-field">班级
<select id="exClsSel" multiple size="4" title="按住 Ctrl / ⌘ 可多选;不选 = 全部班级"></select>
</label>
<label class="ex-field">组别
<select id="exGrpSel" multiple size="4" title="按住 Ctrl / ⌘ 可多选;不选 = 全部组别"></select>
</label>
<div class="ex-field ex-field-tip">
<span class="hint" id="exScopeHint">不选 = 全部(Ctrl / ⌘ 多选)</span>
<span class="hint">选多个班级 / 组别时,导出会自动补出对应列并按相同值合并单元格。</span>
</div>
</div>
</div>
<p class="hint" id="meMeta">已选 0 场</p>
<!-- 3. 明细列与标记(成绩明细 / 每次考试 用) -->
<div class="ex-sec" id="exDetailSec">
<div class="ex-sec-title">成绩口径与列</div>
<div class="ex-row">
<label class="ex-field">每人成绩
<select id="exRowScore" title="每人一行时的代表成绩:最新 = 最近一次真实提交;最高 = 该生最高分">
<option value="latest" selected>最新成绩</option>
<option value="best">最高成绩</option>
</select>
</label>
<label class="ex-field">排序
<select id="exSort">
<option value="time">按提交时间</option>
<option value="sid">按学号</option>
<option value="score_desc">按成绩降序</option>
<option value="score_asc">按成绩升序</option>
</select>
</label>
<label class="ex-field">出表方式
<select id="exDetailMode">
<option value="latest" selected>每人一行(汇总)</option>
<option value="all">每次考试一行(明细)</option>
</select>
</label>
</div>
<div class="ex-checks" id="exDetailChecks">
<label><input type="checkbox" id="exSplit"> 按班级分工作表</label>
</div>
<p class="hint" id="exDetailHint">每人一行时可用「每人成绩」切换最新 / 最高;「每次考试一行」会保留每位学生的历史提交。</p>
</div>
<!-- 3.5 附加工作表(成绩明细 / 每次考试 / 多考试对比 共用) -->
<div class="ex-sec" id="exExtraSec">
<div class="ex-sec-title">附加内容</div>
<div class="ex-checks">
<label><input type="checkbox" id="exWithAbsent" checked> 附缺考名单(名单里有、所选考试全都没交的人)</label>
</div>
</div>
<!-- 4. 标记与合格次数(成绩明细 / 每次考试 / 多考试对比 共用) -->
<div class="ex-sec" id="exMarkSec">
<div class="ex-sec-title">标记与合格次数</div>
<div class="ex-row">
<label class="ex-field">合格次数阈值
<input type="number" id="exPassTimes" min="1" max="20" step="1" value="1"
title="同一场考试里「达到合格线」的次数 ≥ 此值才标绿(默认 1 次)">
</label>
<div class="ex-field ex-field-tip">
<span class="hint">合格线 = 该场考试满分 × 合格率(在「试卷设置」里按卷配置,缺省用全局)</span>
<span class="hint">例:阈值填 2,则该场考到合格线 ≥ 2 次才把「合格次数」标绿。</span>
</div>
</div>
<div class="ex-checks">
<label><input type="checkbox" id="exMarkBest" checked> 最高分标红</label>
<label><input type="checkbox" id="exPassRow" checked> 输出「合格次数」行(达标绿底)</label>
</div>
</div>
<!-- 5. 多考试内容 -->
<div class="ex-sec" id="exMultiSec" hidden>
<div class="ex-sec-title">多考试对比</div>
<div class="ex-row">
<label class="ex-field">表格形态
<select id="exMultiMode">
<option value="block" selected>两行表头(每次考试 + 合格次数)</option>
<option value="summary">成绩对比(每人一行)</option>
<option value="both">两者都出</option>
</select>
</label>
<label class="ex-field">对比成绩
<select id="exMultiScore">
<option value="latest" selected>最新成绩</option>
<option value="highest">最高成绩</option>
</select>
</label>
</div>
<div class="ex-pick">
<div class="ex-pick-head">
<span>勾选要对比的考试 <small id="exPickCount"></small></span>
<span class="spacer"></span>
<button class="btn sm" id="btnExPickAll" type="button">全选</button>
<button class="btn sm" id="btnExPickNone" type="button">清空</button>
</div>
<div class="table-scroll mid">
<table class="grid compact" id="exExamTable">
<thead><tr><th style="width:8%"><input type="checkbox" id="exCheckAll" title="全选"></th><th style="width:8%">顺序</th><th>考试名称</th></tr></thead>
<tbody id="exExamBody"><tr><td colspan="3" class="empty">(暂无可导出的考试,请先在右上角刷新问卷列表)</td></tr></tbody>
</table>
</div>
</div>
</div>
<!-- 6. 打印设置 -->
<div class="ex-sec" id="exPrintSec" hidden>
<div class="ex-sec-title">打印设置</div>
<div class="ex-row">
<label class="ex-field">纸张方向
<select id="exOrient" title="多考试打印默认横向(列多,横向不易折行)">
<option value="landscape" selected>横向(A4)</option>
<option value="portrait">纵向(A4)</option>
</select>
</label>
<label class="ex-field ex-field-grow">打印标题
<input type="text" id="exPrintTitle" placeholder="留空 = 用考试名自动生成">
</label>
</div>
</div>
<p class="hint" id="exSummaryLine" style="margin-top:4px"></p>
</div>
<div class="modal-foot">
<span class="spacer"></span>
<button class="btn" data-close>取消</button>
<button class="btn btn-primary" id="btnMeExport">导出对比表</button>
<button class="btn" id="btnExPrint">打印</button>
<button class="btn btn-primary" id="btnExExport">导出 Excel</button>
</div>
</div>
</div>
<script>
window.NY = {
appName: <?= json_encode(NY_APP_NAME, JSON_UNESCAPED_UNICODE) ?>,