feat(export): 多考试对比导出(横向交叉表)

新增 export_multi 接口与多考试选择弹窗:
- 第一行显示考试名称,前两列固定为学号/姓名,默认按学号排序
- 每场考试成绩可选「最新成绩」或「最高成绩」(latest 跳过 temp_save 草稿,highest 取跨全部尝试最高分)
- 学生取并集(任一考试有提交即纳入),未考的考试单元格留空
- 纯逻辑抽离到 lib/multi_export.php,新增 26 项回归测试(tests/test-multi-export.php)
This commit is contained in:
2026-09-25 08:32:24 +08:00
parent f661862d46
commit 0ba9c7c33c
7 changed files with 400 additions and 1 deletions
+30
View File
@@ -97,6 +97,7 @@ $rosterCount = Roster::count();
<span class="spacer"></span>
<button class="btn" id="btnExport">导出成绩</button>
<button class="btn" id="btnExportAbsent">导出缺考</button>
<button class="btn" id="btnExportMulti" title="勾选多场考试,横向对比同一批学生的成绩">多考试对比导出</button>
</section>
<section class="cards" id="statCards">
@@ -477,6 +478,35 @@ $rosterCount = Roster::count();
</div>
</div>
<!-- 多考试对比导出 -->
<div class="modal" id="multiExportModal">
<div class="modal-box wide">
<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>
</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>
</div>
<p class="hint" id="meMeta">已选 0 场</p>
</div>
<div class="modal-foot">
<span class="spacer"></span>
<button class="btn" data-close>取消</button>
<button class="btn btn-primary" id="btnMeExport">导出对比表</button>
</div>
</div>
</div>
<script>
window.NY = {
appName: <?= json_encode(NY_APP_NAME, JSON_UNESCAPED_UNICODE) ?>,