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
+43 -1
View File
@@ -19,10 +19,49 @@ class Roster
return class_exists('Db') && Db::pdo() !== null;
}
/**
* 首次启用 SQLite 时,把已有的 data/roster.json 迁进数据库(老数据不丢)。
* 只在「DB 名单为空 且 文件有数据」时执行一次,之后由 DB 全权负责。
*/
private static function migrateFileToDbOnce(): void
{
if (!self::useDb()) {
return;
}
try {
if (Db::rosterCount() > 0) {
return; // DB 已有数据,不覆盖
}
$d = ny_read_json(self::file(), null);
if (!is_array($d) || empty($d['students']) || !is_array($d['students'])) {
return;
}
$clean = [];
foreach ($d['students'] as $s) {
if (!is_array($s)) {
continue;
}
$clean[] = [
'sid' => trim((string)($s['sid'] ?? '')),
'name' => trim((string)($s['name'] ?? '')),
'cls' => trim((string)($s['cls'] ?? '')),
'grp' => trim((string)($s['grp'] ?? '')),
'login' => trim((string)($s['login'] ?? '')),
];
}
if ($clean !== []) {
Db::rosterReplace($clean);
}
} catch (Throwable $e) {
// 迁移失败不影响主流程:仍可退回文件读取
}
}
/** @return array<int,array{sid:string,name:string,cls:string}> */
public static function all(): array
{
if (self::useDb()) {
self::migrateFileToDbOnce();
return Db::rosterAll();
}
$d = ny_read_json(self::file(), null);
@@ -105,7 +144,10 @@ class Roster
public static function fingerprint(): string
{
if (self::useDb()) {
return 'db:' . self::count();
// 人数 + 内容哈希:名单内容变了指纹才变(只靠人数会在"换人"时误命中缓存)
$n = self::count();
$hash = md5(json_encode(self::all(), JSON_UNESCAPED_UNICODE));
return 'db:' . $n . ':' . $hash;
}
$f = self::file();
$mt = is_file($f) ? (string)filemtime($f) : '0';