feat: 成绩明细考试次数/历史成绩/导出排序/高亮常亮
- 成绩明细成绩列后新增「考试次数」列(可排序) - 点击任意成员弹窗查看历史成绩(修复内联 onclick 找不到 IIFE 内函数、多次刷题下钻完全失效的 bug;仅 1 次也能点开) - 历史成绩展示方式可切换:默认点击成员弹窗;可选「成绩明细内全部展开」(每次考试一行,标注第几次/最佳) - 新数据绿色底纹常亮(不再黄闪一下):变更检测纳入 attempts/最近提交时间(重考低分也能发现);高亮跨刷新累积,新增「清除高亮」按钮,切换项目/姓名题时清零 - 导出成绩可选排序:按提交时间(默认)或按学号(同学号内按提交时间降序);导出全部考试记录并始终带「考试次数/第几次」列 - 优化排序:空值恒排末尾、主键相同时按学号次级排序;提交时间列显示「最近一次提交」 - 后端:DataService 补 last_submit_ts/last_submit_at/attempts_list 的 idx 与 is_best;新增 expandAttempts 与 sortForExport;修复 expandAttempts 降序比较器写反 - 设置新增 history_display/export_sort 两项配置(局部保存不触发重连、非法值归一化);缺考导出按学号排序 - 设置里补充「只保留最新一份」与考试次数的关系说明
This commit is contained in:
+129
-1
@@ -309,6 +309,111 @@ class DataService
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 把聚合后的学生行展开为"每次考试一行",用于导出全部考试记录 / 明细内全部展示。
|
||||
* 每行携带该次考试的成绩、提交时间、时长,以及该学生的总考试次数 attempts 与第几次 idx。
|
||||
*
|
||||
* @param array $rows 聚合后的行(每行含 attempts_list)
|
||||
* @param bool $desc 是否按提交时间降序(新 -> 旧)
|
||||
* @return array
|
||||
*/
|
||||
public static function expandAttempts(array $rows, bool $desc = true): array
|
||||
{
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$list = (array)($r['attempts_list'] ?? []);
|
||||
if ($list === []) {
|
||||
// 没有明细时退回代表行本身
|
||||
$one = $r;
|
||||
$one['idx'] = 1;
|
||||
$one['is_best'] = true;
|
||||
$out[] = $one;
|
||||
continue;
|
||||
}
|
||||
foreach ($list as $a) {
|
||||
$out[] = [
|
||||
'sid' => (string)($r['sid'] ?? ''),
|
||||
'name' => (string)($r['name'] ?? ''),
|
||||
'cls' => (string)($r['cls'] ?? ''),
|
||||
'grp' => (string)($r['grp'] ?? ''),
|
||||
'score' => $a['score'] ?? null,
|
||||
'submit_ts' => (int)($a['submit_ts'] ?? 0),
|
||||
'submit_at' => (string)($a['submit_at'] ?? '-'),
|
||||
'duration' => $a['duration'] ?? null,
|
||||
'duration_txt' => (string)($a['duration_txt'] ?? '-'),
|
||||
'answer_id' => (string)($a['answer_id'] ?? ''),
|
||||
'temp_save' => !empty($a['temp_save']),
|
||||
'attempts' => (int)($r['attempts'] ?? 1),
|
||||
'best_score' => $r['best_score'] ?? null,
|
||||
'idx' => (int)($a['idx'] ?? 1),
|
||||
'is_best' => !empty($a['is_best']),
|
||||
'last_submit_ts' => (int)($r['last_submit_ts'] ?? 0),
|
||||
'last_submit_at' => (string)($r['last_submit_at'] ?? '-'),
|
||||
];
|
||||
}
|
||||
}
|
||||
usort($out, static function (array $x, array $y) use ($desc): int {
|
||||
// 注意:x<=>y 是升序,降序要用 y<=>x
|
||||
$d = ((int)($y['submit_ts'] ?? 0)) <=> ((int)($x['submit_ts'] ?? 0));
|
||||
if (!$desc) {
|
||||
$d = -$d;
|
||||
}
|
||||
if ($d !== 0) {
|
||||
return $d;
|
||||
}
|
||||
// 同时间:学号升序
|
||||
$a = (string)$x['sid'];
|
||||
$b = (string)$y['sid'];
|
||||
if (is_numeric($a) && is_numeric($b)) {
|
||||
return $a <=> $b;
|
||||
}
|
||||
return strnatcmp($a, $b);
|
||||
});
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出/展示用的排序:
|
||||
* - time(默认):按提交时间降序,同时间按学号升序
|
||||
* - sid:按学号升序,同一学号内多次考试再按提交时间降序
|
||||
*
|
||||
* @param array $rows expandAttempts 展开后的行
|
||||
* @param string $mode 'time' | 'sid'
|
||||
*/
|
||||
public static function sortForExport(array $rows, string $mode = 'time'): array
|
||||
{
|
||||
$out = $rows;
|
||||
if ($mode === 'sid') {
|
||||
usort($out, static function ($x, $y): int {
|
||||
$a = (string)($x['sid'] ?? '');
|
||||
$b = (string)($y['sid'] ?? '');
|
||||
if (is_numeric($a) && is_numeric($b)) {
|
||||
$d = $a <=> $b;
|
||||
} else {
|
||||
$d = strnatcmp($a, $b);
|
||||
}
|
||||
if ($d !== 0) {
|
||||
return $d;
|
||||
}
|
||||
return ((int)($y['submit_ts'] ?? 0)) <=> ((int)($x['submit_ts'] ?? 0));
|
||||
});
|
||||
return $out;
|
||||
}
|
||||
usort($out, static function ($x, $y): int {
|
||||
$d = ((int)($y['submit_ts'] ?? 0)) <=> ((int)($x['submit_ts'] ?? 0));
|
||||
if ($d !== 0) {
|
||||
return $d;
|
||||
}
|
||||
$a = (string)($x['sid'] ?? '');
|
||||
$b = (string)($y['sid'] ?? '');
|
||||
if (is_numeric($a) && is_numeric($b)) {
|
||||
return $a <=> $b;
|
||||
}
|
||||
return strnatcmp($a, $b);
|
||||
});
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按学号聚合一次提交的答卷记录,解决"同人多次刷卷"在看板中被当成多个人的问题。
|
||||
* - dedupe=true:每学号仅保留提交时间最新的一份(原行为)
|
||||
@@ -332,8 +437,27 @@ class DataService
|
||||
usort($sorted, static function (array $x, array $y): int {
|
||||
return ($x['submit_ts'] ?? 0) <=> ($y['submit_ts'] ?? 0);
|
||||
});
|
||||
$attemptsList = array_map(static function (array $a): array {
|
||||
// 最佳成绩(用于标记每次考试是否为最高分)
|
||||
$maxScore = null;
|
||||
foreach ($sorted as $a) {
|
||||
if ($a['score'] !== null && ($maxScore === null || $a['score'] > $maxScore)) {
|
||||
$maxScore = $a['score'];
|
||||
}
|
||||
}
|
||||
// 最近一次提交(升序排序后的最后一份)
|
||||
$lastSubmitTs = (int)($sorted[count($sorted) - 1]['submit_ts'] ?? 0);
|
||||
// 标记是否已给"最佳"打过标(同分时只标最早达到该分数的那次)
|
||||
$bestMarked = false;
|
||||
$idx = 0;
|
||||
$attemptsList = array_map(static function (array $a) use (&$idx, $maxScore, &$bestMarked): array {
|
||||
$idx++;
|
||||
$isBest = (!$bestMarked && $a['score'] !== null && $maxScore !== null && (float)$a['score'] === (float)$maxScore);
|
||||
if ($isBest) {
|
||||
$bestMarked = true;
|
||||
}
|
||||
return [
|
||||
'idx' => $idx,
|
||||
'is_best' => $isBest,
|
||||
'submit_at' => $a['submit_at'],
|
||||
'submit_ts' => $a['submit_ts'],
|
||||
'score' => $a['score'],
|
||||
@@ -368,6 +492,10 @@ class DataService
|
||||
$row['best_score'] = $bestScore;
|
||||
$row['attempts_list'] = $attemptsList;
|
||||
}
|
||||
// 最近一次提交时间(与"代表成绩"可能是不同的一次考试):
|
||||
// 用于"按提交时间排序"和"新提交高亮",保证重考(哪怕分数更低)也能被发现
|
||||
$row['last_submit_ts'] = $lastSubmitTs;
|
||||
$row['last_submit_at'] = $lastSubmitTs > 0 ? date('Y-m-d H:i:s', $lastSubmitTs) : '-';
|
||||
$agg[] = $row;
|
||||
}
|
||||
return ['rows' => $agg, 'attempts_total' => $attemptsTotal];
|
||||
|
||||
Reference in New Issue
Block a user