多考试块表:每场考试改为单列并修正第几次列合并
- ny_assemble_block_sheet() 中每场考试由 2 列改为 1 列,删除历史遗留的 「是否最佳」空白列,消除每场右侧多余空白窄列 - 删除「第几次」列的纵向合并,改为逐行显示 1/2/3…,不再被吞成 1 - 合格次数行同步改为每场 1 列;未考合并加 $end > $start 跨行守卫, 规避单行学生产生的非法 1x1 合并(Excel 修复提示) - 列宽按考试名估算(CJK 记 2 半角宽),clamp 8~18 - 同步更新 test-multi-export.php(95 项含 4 条防回归断言)、 make-samples.php(李四数学改为真正整场未考)、导出/接口/测试文档 及样例预览文件;全量回归 404 项 0 失败
This commit is contained in:
+51
-51
@@ -228,22 +228,24 @@ function ny_exam_attempt_list(array $r, bool $skipTemp = false): array
|
||||
* 组装「每次考试」块表(两行表头 / 合并单元格 / 未考占位)——与需求截图一致。
|
||||
*
|
||||
* 版式(对应需求截图):
|
||||
* ┌──────┬──────┬────────┬────────────────┬────────────────┐
|
||||
* │ 学号 │ 姓名 │ 第几次 │ 考试名称1 │ 考试名称2 │ ← 第 1 行:考试名横向合并 2 列
|
||||
* ├──────┼──────┼────────┼────────┬───────┼────────┬───────┤
|
||||
* │ │ │ │ 成绩 │ 成绩 │ 成绩 │ 成绩 │ ← 第 2 行(第几次纵向贯穿两行)
|
||||
* ├──────┼──────┼────────┼────────┼───────┼────────┼───────┤
|
||||
* │24030…│ 张三 │ 1 │ 127 │ 127 │ 134 │ 134 │
|
||||
* │(合并)│(合并)│ 2 │ 132 │ 132 │ 136 │ 136 │ ← 最高分用红字
|
||||
* │ │ │ … │ … │ … │ │ │
|
||||
* │ │ │合格次数│ 2 │ 1 │ 2 │ 2 │ ← 达标绿底、未达标空白
|
||||
* └──────┴──────┴────────┴────────┴───────┴────────┴───────┘
|
||||
* ┌──────┬──────┬────────┬────────┬────────┬────────┐
|
||||
* │ 学号 │ 姓名 │ 第几次 │考试名1 │考试名2 │考试名3 │ ← 第 1 行:考试名
|
||||
* ├──────┼──────┼────────┼────────┼────────┼────────┤
|
||||
* │ │ │ │ 成绩 │ 成绩 │ 成绩 │ ← 第 2 行(前导列纵向贯穿两行)
|
||||
* ├──────┼──────┼────────┼────────┼────────┼────────┤
|
||||
* │24030…│ 张三 │ 1 │ 127 │ 118 │ 134 │
|
||||
* │(合并)│(合并)│ 2 │ 132 │ 121 │ 136 │ ← 最高分用红字
|
||||
* │ │ │ … │ … │ … │ … │
|
||||
* │ │ │合格次数│ 2 │ 0 │ 1 │ ← 达标绿底,未达标不标底
|
||||
* └──────┴──────┴────────┴────────┴────────┴────────┘
|
||||
*
|
||||
* 约定:
|
||||
* - 一名学生占 max(各场有效次数) + 1 行(+1 是末尾的「合格次数」汇总行)
|
||||
* - 学号 / 姓名 / 第几次 三列按学生块纵向合并(第几次与学号同高,贯穿表头以下全部行)
|
||||
* - 每场考试占 2 列:考试名称(第 1 行横向合并)+ 成绩(仅第 2 行有「成绩」小标题)
|
||||
* - 只在该生某场考试完全没考时写「未考」并合并该场全部列;考了但次数少 → 留空
|
||||
* - 学号 / 姓名 两列按学生块纵向合并;「第几次」逐行写各自次序号(1、2、3…),**不合并**
|
||||
* (合并会把 2、3 吞掉只留 1,见截图反馈)
|
||||
* - 每场考试占 **1 列**:第 1 行考试名、第 2 行「成绩」小标题
|
||||
* (历史上每场占 2 列,第 2 列是遗留的空位,表现为"每场右边多一根空白列",已去掉)
|
||||
* - 只在该生某场考试完全没考时写「未考」并纵向合并该列的学生块;考了但次数少 → 留空
|
||||
* - 「合格次数」= 该场成绩 ≥ 合格线的次数;始终显示(含 0 次),只有 ≥ 阈值才标绿底
|
||||
* - 每场成绩等于该场最高分的那次用红字标出(同分只标最早达成的一次)
|
||||
*
|
||||
@@ -329,8 +331,8 @@ function ny_assemble_block_sheet(array $exams, array $opts = []): array
|
||||
});
|
||||
|
||||
/* ---------------- 列布局 ----------------
|
||||
前导列:班级? / 组别? / 学号 / 姓名 / 第几次(各考试共用一列,纵向合并到学生块)
|
||||
考试区:每场 2 列(考试名称跨度由第 1 行横向合并,第 2 行只有「成绩」小标题) */
|
||||
前导列:班级? / 组别? / 学号 / 姓名 / 第几次(各考试共用一列)
|
||||
考试区:每场 1 列(第 1 行考试名、第 2 行「成绩」小标题) */
|
||||
$colCls = $withCls ? 1 : 0;
|
||||
$colGrp = $withGrp ? ($withCls ? 2 : 1) : 0;
|
||||
$colSid = 1 + ($withCls ? 1 : 0) + ($withGrp ? 1 : 0);
|
||||
@@ -357,19 +359,15 @@ function ny_assemble_block_sheet(array $exams, array $opts = []): array
|
||||
}
|
||||
}
|
||||
|
||||
// 每场考试占 2 列:考试名称(第 1 行横向合并)+ 成绩(第 2 行小标题)
|
||||
// 每场考试占 1 列:第 1 行考试名、第 2 行「成绩」小标题(无需横向合并)
|
||||
$examCol = [];
|
||||
$c = $colExam0;
|
||||
foreach ($examOrder as $pid) {
|
||||
$examCol[$pid] = $c;
|
||||
$h1[] = $examNames[$pid];
|
||||
$h1[] = '';
|
||||
$h2[] = '成绩';
|
||||
$h2[] = '';
|
||||
$merges[] = [1, $c, 1, $c + 1];
|
||||
$c += 2;
|
||||
$c += 1;
|
||||
}
|
||||
$colPass = $colExam0 + 2 * count($examOrder); // 「合格次数」所在列(若启用)
|
||||
|
||||
$rows = [$h1, $h2];
|
||||
$rn = 2; // 当前行号(两行表头已占 1~2)
|
||||
@@ -428,14 +426,10 @@ function ny_assemble_block_sheet(array $exams, array $opts = []): array
|
||||
foreach ($examOrder as $pid) {
|
||||
$a = $lists[$pid][$i] ?? null;
|
||||
if ($a === null) {
|
||||
$line[] = '';
|
||||
$line[] = '';
|
||||
$line[] = ''; // 该生这场没考到第 i 次 → 留空
|
||||
continue;
|
||||
}
|
||||
$line[] = $a['score'];
|
||||
$line[] = ''; // 每场固定 2 列:第 1 列成绩、第 2 列是合并区间内的空位。
|
||||
// 漏掉这个空串会让后续考试整体左移一列——数据串列、
|
||||
// 表头合并区间与实际列对不上(截图里的错位就是它)。
|
||||
// 最高分标红:只标一处(同分只标最早达成的那一行)
|
||||
if ($markBest && $a['score'] !== '' && $bestScore[$pid] !== null
|
||||
&& (float)$a['score'] === $bestScore[$pid] && $a['is_best']) {
|
||||
@@ -450,16 +444,18 @@ function ny_assemble_block_sheet(array $exams, array $opts = []): array
|
||||
if ($end > $start) {
|
||||
$merges[] = [$start, $colSid, $end, $colSid];
|
||||
$merges[] = [$start, $colName, $end, $colName];
|
||||
$merges[] = [$start, $colIdx, $end, $colIdx];
|
||||
// 「第几次」不合并:每行是不同的次序号(合并只剩第一行的 1)
|
||||
}
|
||||
// 完全没考的考试:该场全部列 × 学生块高 → 合并成「未考」
|
||||
// 完全没考的考试:该场该列 × 学生块高 → 合并成「未考」
|
||||
foreach ($examOrder as $pid) {
|
||||
if ($lists[$pid] !== []) {
|
||||
continue;
|
||||
}
|
||||
$cc = $examCol[$pid];
|
||||
$rows[$start - 1][$cc - 1] = $absent;
|
||||
$merges[] = [$start, $cc, $end, $cc + 1];
|
||||
if ($end > $start) { // 单行块无需合并(1×1 合并是非法区间,Excel 会提示修复)
|
||||
$merges[] = [$start, $cc, $end, $cc];
|
||||
}
|
||||
}
|
||||
|
||||
$blocks[] = ['start' => $start, 'end' => $end, 'cls' => $s['cls'], 'grp' => $s['grp']];
|
||||
@@ -483,11 +479,7 @@ function ny_assemble_block_sheet(array $exams, array $opts = []): array
|
||||
$merges[] = [1, $cc, 2, $cc];
|
||||
}
|
||||
}
|
||||
$cc = $colExam0;
|
||||
foreach ($examOrder as $pid) {
|
||||
$merges[] = [1, $cc, 1, $cc + 1];
|
||||
$cc += 2;
|
||||
}
|
||||
// 考试列每场 1 列,表头两行各写「考试名 / 成绩」,无需合并
|
||||
|
||||
foreach ($sids as $sid) {
|
||||
$sid = (string)$sid;
|
||||
@@ -527,16 +519,14 @@ function ny_assemble_block_sheet(array $exams, array $opts = []): array
|
||||
if ($withGrp) { $line[] = ($i === 0) ? $s['grp'] : ''; }
|
||||
$line[] = ($i === 0) ? $sid : '';
|
||||
$line[] = ($i === 0) ? $s['name'] : '';
|
||||
$line[] = (string)($i + 1); // 第几次:所有考试共用一列
|
||||
$line[] = (string)($i + 1); // 第几次:逐行写各自次序号,不合并
|
||||
foreach ($examOrder as $pid) {
|
||||
$a = $lists[$pid][$i] ?? null;
|
||||
if ($a === null) {
|
||||
$line[] = '';
|
||||
$line[] = '';
|
||||
continue;
|
||||
}
|
||||
$line[] = $a['score'];
|
||||
$line[] = ''; // 同上:每场固定 2 列,缺了这个空串会整体左移
|
||||
if ($markBest && $a['score'] !== '' && $bestScore[$pid] !== null
|
||||
&& (float)$a['score'] === $bestScore[$pid] && $a['is_best']) {
|
||||
$marks[$rn . ',' . $examCol[$pid]] = 'red';
|
||||
@@ -546,14 +536,16 @@ function ny_assemble_block_sheet(array $exams, array $opts = []): array
|
||||
$total++;
|
||||
}
|
||||
|
||||
// 完全没考的考试:该场全部列 × 学生块高 → 合并成「未考」
|
||||
// 完全没考的考试:该场该列 × 学生块高 → 合并成「未考」
|
||||
foreach ($examOrder as $pid) {
|
||||
if ($lists[$pid] !== []) {
|
||||
continue;
|
||||
}
|
||||
$cx = $examCol[$pid];
|
||||
$rows[$start - 1][$cx - 1] = $absent;
|
||||
$merges[] = [$start, $cx, $rn, $cx + 1];
|
||||
if ($rn > $start) { // 单行块无需合并(1×1 合并是非法区间)
|
||||
$merges[] = [$start, $cx, $rn, $cx];
|
||||
}
|
||||
}
|
||||
|
||||
// 合格次数行(每个学生块一行)
|
||||
@@ -569,7 +561,6 @@ function ny_assemble_block_sheet(array $exams, array $opts = []): array
|
||||
if ($list === []) {
|
||||
// 整场未考:与「未考」合并格保持一致,不再单独判断合格次数
|
||||
$passLineRow[] = '';
|
||||
$passLineRow[] = '';
|
||||
continue;
|
||||
}
|
||||
$cnt = 0;
|
||||
@@ -581,7 +572,6 @@ function ny_assemble_block_sheet(array $exams, array $opts = []): array
|
||||
/* 合格次数始终显示(含 0 次);只有达到阈值才标绿底。
|
||||
之前写 '' 会把「考了但没达标」的人整格隐藏,用户要看全部人次。 */
|
||||
$passLineRow[] = (string)$cnt;
|
||||
$passLineRow[] = '';
|
||||
if ($cnt >= $passTimes) {
|
||||
$marks[$rn . ',' . $examCol[$pid]] = 'green'; // 达标:绿底
|
||||
}
|
||||
@@ -590,13 +580,9 @@ function ny_assemble_block_sheet(array $exams, array $opts = []): array
|
||||
$total++;
|
||||
|
||||
/* 学号 / 姓名 纵向合并到「合格次数」行(这两列该行本就是空的,合并正确);
|
||||
「第几次」只合并到最后一次考试行,末行留给「合格次数」标签——
|
||||
若连合格次数行一起合并,该格会被当作"被合并的非首格"而丢掉值。 */
|
||||
「第几次」列逐行是 1、2、3…,不合并——末行「合格次数」标签也写在这一列。 */
|
||||
$merges[] = [$start, $colSid, $rn, $colSid];
|
||||
$merges[] = [$start, $colName, $rn, $colName];
|
||||
if ($rn > $start) {
|
||||
$merges[] = [$start, $colIdx, $rn - 1, $colIdx];
|
||||
}
|
||||
$newBlocks[] = ['start' => $start, 'end' => $rn, 'cls' => $s['cls'], 'grp' => $s['grp']];
|
||||
}
|
||||
$blocks = $newBlocks;
|
||||
@@ -640,8 +626,7 @@ function ny_assemble_block_sheet(array $exams, array $opts = []): array
|
||||
/* 列宽:够用就好,宽了会把人挤到屏幕外(截图里学号列两侧大片空白就是这么来的)。
|
||||
- 学号按实际内容估宽(10 位数字约 11,超过 12 位才放宽),不再一律 20
|
||||
- 姓名同样按最长姓名估宽
|
||||
- 每场考试占 2 列,但第 2 列恒为合并区间内的空位 → 给最小宽度 2.5,
|
||||
把宽度让给真正承载成绩的第 1 列(否则一屏只看得到两场考试) */
|
||||
- 每场考试只占 1 列,列宽要装得下该列表头(考试名),中文按 2 个半角宽算 */
|
||||
$maxSidLen = 2;
|
||||
$maxNameLen = 2;
|
||||
foreach ($stu as $s) {
|
||||
@@ -659,6 +644,22 @@ function ny_assemble_block_sheet(array $exams, array $opts = []): array
|
||||
$sidW = min(18, max(10, $maxSidLen + 2));
|
||||
$nameW = min(20, max(9, $maxNameLen * 2 + 2));
|
||||
|
||||
// 考试列:取最长的考试名估一个统一宽度
|
||||
$examW = 8;
|
||||
foreach ($examOrder as $pid) {
|
||||
$nm = (string)$examNames[$pid];
|
||||
$len = mb_strlen($nm, 'UTF-8');
|
||||
$w = 0;
|
||||
for ($k = 0; $k < $len; $k++) {
|
||||
$w += (strlen((string)mb_substr($nm, $k, 1, 'UTF-8')) > 1) ? 2 : 1;
|
||||
}
|
||||
$w += 2; // 左右留白
|
||||
if ($w > $examW) {
|
||||
$examW = $w;
|
||||
}
|
||||
}
|
||||
$examW = min(18, max(8, $examW));
|
||||
|
||||
$widths = [];
|
||||
if ($withCls) {
|
||||
$widths[] = 14; // 班级名通常较长(如「计算机2401」)
|
||||
@@ -668,10 +669,9 @@ function ny_assemble_block_sheet(array $exams, array $opts = []): array
|
||||
}
|
||||
$widths[] = $sidW; // 学号
|
||||
$widths[] = $nameW; // 姓名
|
||||
$widths[] = 9; // 第几次
|
||||
$widths[] = 10; // 第几次(同时承载「合格次数」标签)
|
||||
foreach ($examOrder as $pid) {
|
||||
$widths[] = 10; // 成绩列(每场第 1 列)
|
||||
$widths[] = 2.5; // 合并区内的空列,给最小宽度即可
|
||||
$widths[] = $examW; // 每场考试 1 列(列宽装得下表头考试名)
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user