feat: 成绩明细打印 + SQLite + 每卷独立配置(满分/合格率/免考) + 导航精简
- 成绩明细打印:默认成绩降序,可选按班级/组别分组对比(段首小计) - 修复看板退出全屏无反应(exitFullscreen 挂在 document 而非 documentElement) - 导航栏精简:三页统一为单一主标题,移除 Logo 与副标题 - 优化三端布局:重排断点、竖排隐藏 spacer、触摸设备热区 - 全面启用 SQLite(data/ny.db):配置/名单/每卷配置入库,无 pdo_sqlite 自动降级文件 - 新增独立合格率配置(全局 + 每卷),合格率不再写死 60% - 新增免考配置,仅对当前试卷生效,试卷间隔离 - 试卷独立满分从设置移出,改到主页面「试卷设置」 - 测试:新增 4 条每卷配置隔离断言,全套 113 项通过
This commit is contained in:
+82
-7
@@ -227,7 +227,9 @@ function ny_default_config(): array
|
||||
'dedupe' => true,
|
||||
'name_keywords' => '姓名,名字,学生姓名,考生姓名,您的姓名,姓名:,姓名:',
|
||||
'full_score' => 100,
|
||||
'full_scores' => [],
|
||||
// 全局默认合格率(百分比,如 60 表示"≥满分的 60%"算合格)。
|
||||
// 每份试卷可在主页面单独覆盖(project_config),试卷之间互相隔离。
|
||||
'pass_rate' => 60,
|
||||
'layout' => 'default',
|
||||
// 历史成绩展示方式:click=点击成员弹窗查看(默认);inline=成绩明细内直接展开每次考试
|
||||
'history_display' => 'click',
|
||||
@@ -258,16 +260,45 @@ function ny_load_config(): array
|
||||
{
|
||||
$GLOBALS['__ny_cfg_source'] = 'default';
|
||||
|
||||
// 默认配置 + 文件覆盖(DB 后端本批次已下线,统一走文件存储)
|
||||
$cfg = ny_default_config();
|
||||
$saved = ny_read_json(ny_config_file(), null);
|
||||
$cfg = ny_default_config();
|
||||
$file = ny_config_file();
|
||||
$saved = null;
|
||||
|
||||
// 1) 优先读 SQLite(data/ny.db 的 ny_settings.config)
|
||||
if (class_exists('Db') && Db::pdo() !== null) {
|
||||
$raw = Db::settingsGet('config', '');
|
||||
if ($raw !== '') {
|
||||
$j = json_decode($raw, true);
|
||||
if (is_array($j)) {
|
||||
$saved = $j;
|
||||
$GLOBALS['__ny_cfg_source'] = 'db';
|
||||
}
|
||||
}
|
||||
// DB 可用但尚无记录 → 把已有 config.json 迁进来(老数据不丢,只迁一次)
|
||||
if ($saved === null) {
|
||||
$fromFile = ny_read_json($file, null);
|
||||
if (is_array($fromFile)) {
|
||||
Db::settingsSet('config', json_encode($fromFile, JSON_UNESCAPED_UNICODE));
|
||||
$saved = $fromFile;
|
||||
$GLOBALS['__ny_cfg_source'] = 'db';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) DB 不可用时退回文件
|
||||
if ($saved === null) {
|
||||
$saved = ny_read_json($file, null);
|
||||
if (is_array($saved)) {
|
||||
$GLOBALS['__ny_cfg_source'] = 'file';
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($saved)) {
|
||||
foreach ($cfg as $k => $v) {
|
||||
if (array_key_exists($k, $saved)) {
|
||||
$cfg[$k] = is_bool($v) ? (bool)$saved[$k] : $saved[$k];
|
||||
}
|
||||
}
|
||||
$GLOBALS['__ny_cfg_source'] = 'file';
|
||||
}
|
||||
$cfg['base_url'] = rtrim(trim((string)$cfg['base_url']), '/');
|
||||
$cfg['api_prefix'] = '/' . trim((string)$cfg['api_prefix'], '/');
|
||||
@@ -276,8 +307,52 @@ function ny_load_config(): array
|
||||
|
||||
function ny_save_config(array $cfg): bool
|
||||
{
|
||||
$GLOBALS['__ny_cfg_source'] = 'file';
|
||||
return ny_write_json(ny_config_file(), $cfg);
|
||||
// 双写:SQLite 为主(成功即算成功),文件为备份(DB 挂掉时还能读回来)
|
||||
$ok = false;
|
||||
if (class_exists('Db') && Db::pdo() !== null) {
|
||||
try {
|
||||
Db::settingsSet('config', json_encode($cfg, JSON_UNESCAPED_UNICODE));
|
||||
$GLOBALS['__ny_cfg_source'] = 'db';
|
||||
$ok = true;
|
||||
} catch (Throwable $e) {
|
||||
$ok = false;
|
||||
}
|
||||
}
|
||||
$fileOk = ny_write_json(ny_config_file(), $cfg);
|
||||
if (!$ok) {
|
||||
$GLOBALS['__ny_cfg_source'] = 'file';
|
||||
}
|
||||
return $ok || $fileOk;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析「本卷满分 + 合格率」:
|
||||
* 优先取该试卷的独立配置(试卷之间互相隔离),没有则用全局配置。
|
||||
*
|
||||
* @return array{0:int,1:float} [满分, 合格率百分比(如 60 表示 60%)]
|
||||
*/
|
||||
function ny_resolve_score_cfg(string $projectId, array $c): array
|
||||
{
|
||||
$pCfg = class_exists('DataService') ? DataService::projectConfig($projectId) : [];
|
||||
|
||||
$fs = (int)($pCfg['full_score'] ?? 0);
|
||||
if ($fs < 1) {
|
||||
$fs = (int)($c['full_score'] ?? 100);
|
||||
}
|
||||
if ($fs < 1) {
|
||||
$fs = 100;
|
||||
}
|
||||
|
||||
$pr = $pCfg['pass_rate'] ?? null;
|
||||
if ($pr === null || $pr === '') {
|
||||
$pr = $c['pass_rate'] ?? 60;
|
||||
}
|
||||
$pr = (float)$pr;
|
||||
if ($pr <= 0 || $pr > 100) {
|
||||
$pr = 60;
|
||||
}
|
||||
|
||||
return [$fs, $pr];
|
||||
}
|
||||
|
||||
/** 安全的字符串化(避免数组/对象转字符串报错) */
|
||||
|
||||
Reference in New Issue
Block a user