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
+68
View File
@@ -79,11 +79,73 @@ class DataService
/**
* 组装结果
*/
/**
* 读取某份试卷的独立配置(满分 / 合格率 / 免考名单)。
* 优先 SQLite(ny_project_config 表);不可用时退回 config.json 里的 project_config 字段,
* 保证没有 pdo_sqlite 的环境也能用(只是隔离粒度仍在,只是落到文件)。
*
* @return array{full_score:?int,pass_rate:?float,exempt:array<int,string>}
*/
public static function projectConfig(string $projectId): array
{
$def = ['full_score' => null, 'pass_rate' => null, 'exempt' => []];
if ($projectId === '') {
return $def;
}
if (class_exists('Db') && Db::pdo() !== null) {
return Db::projectConfigGet($projectId);
}
// 文件回退
$all = [];
$saved = ny_read_json(NY_DATA . '/project_config.json', null);
if (is_array($saved)) {
$all = $saved;
}
$one = $all[$projectId] ?? null;
return is_array($one) ? array_merge($def, $one) : $def;
}
/** 写入某份试卷的独立配置 */
public static function projectConfigSave(string $projectId, array $cfg): void
{
if ($projectId === '') {
return;
}
if (class_exists('Db') && Db::pdo() !== null) {
Db::projectConfigSet($projectId, $cfg);
return;
}
$all = [];
$saved = ny_read_json(NY_DATA . '/project_config.json', null);
if (is_array($saved)) {
$all = $saved;
}
$all[$projectId] = $cfg;
ny_write_json(NY_DATA . '/project_config.json', $all);
}
public static function build(array $cfg, string $projectId, string $nameQid = ''): array
{
$client = new SurveyKingClient($cfg);
// 每卷独立配置(满分 / 合格率 / 免考名单):试卷之间互相隔离
$pCfg = self::projectConfig($projectId);
$exempt = [];
foreach ((array)($pCfg['exempt'] ?? []) as $sid) {
$sid = trim((string)$sid);
if ($sid !== '') {
$exempt[$sid] = true;
}
}
$students = Roster::all();
// 免考:从名单中剔除,使其既不计入「总人数」也不计入「缺考」
if ($exempt !== []) {
$students = array_values(array_filter(
$students,
static fn(array $s): bool => !isset($exempt[trim((string)($s['sid'] ?? ''))])
));
}
// 姓名 -> 学号 / 班级(规范化)
$studentInfo = [];
foreach ($students as $s) {
@@ -313,6 +375,12 @@ class DataService
'anomalies' => $anomalies,
'roster' => $students,
'roster_count' => count($students),
// 本卷独立配置(前端据此回显满分/合格率/免考);full_score 已按本卷解析
'project_cfg' => [
'full_score' => $pCfg['full_score'] ?? null,
'pass_rate' => $pCfg['pass_rate'] ?? null,
'exempt' => array_values(array_map('strval', array_keys($exempt))),
],
'updated_at' => date('Y-m-d H:i:s'),
];
}