diff --git a/api.php b/api.php index 2bdd84e..e57ea35 100644 --- a/api.php +++ b/api.php @@ -5,6 +5,7 @@ declare(strict_types=1); require __DIR__ . '/bootstrap.php'; +require __DIR__ . '/lib/multi_export.php'; $action = (string)($_GET['action'] ?? $_POST['action'] ?? ''); @@ -64,6 +65,11 @@ function ny_absent_sheet_rows(array $absent): array return $out; } +/** + * 多考试对比导出(成绩交叉矩阵)的纯逻辑见 lib/multi_export.php + * (ny_exam_cell_score / ny_assemble_multi_exam_matrix),便于在 api.php 与单测中复用。 + */ + /** * 带短期缓存的数据构建(含并发锁,避免多人同时刷新时重复打卷王造成惊群) * @@ -854,6 +860,48 @@ try { echo $bin; exit; + // ---------------- 多考试对比导出(横向交叉表) ---------------- + case 'export_multi': + $idsRaw = (string)($_GET['projectIds'] ?? ''); + $ids = array_values(array_unique(array_filter( + array_map('trim', explode(',', $idsRaw)), + static fn($v): bool => $v !== '' + ))); + if ($ids === []) { + ny_fail('请至少选择一个问卷 / 考试'); + } + $nameQid = (string)($_GET['nameQid'] ?? ''); + $scoreMode = ((string)($_GET['scoreMode'] ?? '') === 'highest') ? 'highest' : 'latest'; + $c = cfg(); + + // 逐场考试取数(复用与 data 接口相同的缓存),按选择顺序组装交叉矩阵 + $exams = []; + foreach ($ids as $pid) { + $built = ny_build_data($c, $pid, $nameQid, !empty($_GET['force'])); + $result = $built['result']; + $exams[$pid] = [ + 'name' => (string)($result['project']['name'] ?? $pid), + 'rows' => (array)($result['rows'] ?? []), + ]; + } + + $matrix = ny_assemble_multi_exam_matrix($exams, $scoreMode); + $sheetRows = array_merge([$matrix['head']], $matrix['rows']); + $widths = array_merge([20, 14], array_fill(0, count($matrix['exam_order']), 12)); + $sheets = [['name' => '多考试对比', 'rows' => $sheetRows, 'widths' => $widths]]; + + $safe = static fn(string $s): string => preg_replace('/[\\\\\\/:*?"<>|]/u', '_', $s) ?? ''; + $filename = '多考试成绩对比_' . count($ids) . '场_' + . ($scoreMode === 'highest' ? '最高分' : '最新成绩') . '_' . date('Ymd_His') . '.xlsx'; + $bin = XlsxWriter::renderMulti($sheets); + + header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + header('Content-Disposition: attachment; filename="export.xlsx"; filename*=UTF-8\'\'' . rawurlencode($filename)); + header('Content-Length: ' . strlen($bin)); + header('Cache-Control: no-store'); + echo $bin; + exit; + // ---------------- 随机抽取:导出白名单(xlsx,列:姓名 / 登陆账号) ---------------- case 'draw_export': $raw = ny_request_body(); diff --git a/assets/app.js b/assets/app.js index 2aaaea0..1011f86 100644 --- a/assets/app.js +++ b/assets/app.js @@ -1373,6 +1373,79 @@ showAlert(exportTip('缺考名单', cls, grp), 'info'); }); + /* ---------------- 多考试对比导出 ---------------- */ + // 仅列出真正的"考试"(排除文件夹型项目,folder 或被其他项目作为父级引用) + function nonFolderProjects() { + const isFolder = p => (p.mode || '').toLowerCase().includes('folder'); + const parentIds = new Set(); + state.projects.forEach(p => { + const pid = (p.parent_id || '0'); + if (pid && pid !== '0' && pid !== '') parentIds.add(pid); + }); + const folderSet = new Set(); + state.projects.forEach(p => { if (isFolder(p) || parentIds.has(p.id)) folderSet.add(p.id); }); + return state.projects.filter(p => !folderSet.has(p.id)); + } + function renderMultiExportModal() { + const body = $('#meBody'); + const list = nonFolderProjects(); + body.innerHTML = ''; + if (!list.length) { + body.innerHTML = '
勾选多场考试后导出一张横向对比表:第一行是考试名称,前两列是学号 / 姓名,默认按学号排序;每场考试的成绩可按「最新」或「最高」显示。
+ +| 顺序 | 考试名称 | |
|---|---|---|
| (暂无可导出的考试,请先在右上角刷新问卷列表) | ||
已选 0 场
+