完成批次 L-R:限流移除/白名单驱动/打印增强/全屏/表格对齐/主题变量/滚动闪烁修复

- 移除登录失败限流;fix-config.php 修正 e2e 串味的 127.0.0.1 连接

- 白名单驱动缺/待考名单(DataService + SurveyKingClient::getWhitelist,复用已取 survey)

- 打印成绩明细增强(范围/合格列默认关/去页脚);看板全屏占满;表格 vertical-align:middle;隐藏登陆账号;导入默认追加

- 主题变量收口(:root 新增 brand/primary-2/ok-2 等,替换硬编码渐变);删死规则

- 表头/数据列对齐修复(index 未匹配表、dashboard 成员榜、draw 结果/历史/排除表);看板占位卡片配色一致

- 修复滚动闪烁:row-drop-in 动画经 _dropAnim 门控,仅强制重绘时播放

- 测试:新增 test-whitelist.js;run-all.sh 全绿 132
This commit is contained in:
2026-09-13 10:25:48 +08:00
parent 804b04de60
commit 94ac381579
17 changed files with 856 additions and 301 deletions
+47 -4
View File
@@ -94,6 +94,36 @@ function rateColor(r){
return 'var(--danger)';
}
/* FLIP 行移动画:新数据行滑到新排名、其它行顺势下移(尊重 prefers-reduced-motion) */
const REDUCED_MOTION = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
function captureFlip(container){
const m = new Map();
if (!container) return m;
container.querySelectorAll('tr[data-sid]').forEach(tr => {
const sid = tr.getAttribute('data-sid');
if (sid) m.set(sid, tr.getBoundingClientRect().top);
});
return m;
}
function playFlip(container, prev){
if (!container || !prev || REDUCED_MOTION) return;
container.querySelectorAll('tr[data-sid]').forEach(tr => {
const sid = tr.getAttribute('data-sid');
if (!sid) return;
const oldTop = prev.get(sid);
if (oldTop === undefined) { tr.classList.add('row-drop-in'); return; }
const dy = oldTop - tr.getBoundingClientRect().top;
if (Math.abs(dy) < 1) return;
tr.style.transition = 'none';
tr.style.transform = 'translateY(' + dy + 'px)';
void tr.offsetWidth;
requestAnimationFrame(() => {
tr.style.transition = 'transform .42s cubic-bezier(.22,.61,.36,1)';
tr.style.transform = '';
});
});
}
/* URL 状态:?pid=&class=&auto=1|0 */
function readUrl(){
const u = new URL(location.href);
@@ -237,7 +267,7 @@ function membersTable(members){
return '<div class="empty" style="padding:18px 0">暂无考试成员</div>';
}
return '<div class="member-list"><table class="grid compact">' +
'<thead><tr><th style="width:60px">排名</th><th>姓名</th><th style="width:80px">成绩</th></tr></thead><tbody>' +
'<thead><tr><th style="width:20%;text-align:right">排名</th><th style="width:55%">姓名</th><th style="width:25%" data-k="score">成绩</th></tr></thead><tbody>' +
members.map(m => {
const rk = (m.rank == null ? '' : m.rank);
const badge = rk === 1 ? 'rk rk-1' : (rk === 2 ? 'rk rk-2' : (rk === 3 ? 'rk rk-3' : 'rk'));
@@ -255,7 +285,7 @@ function membersTable(members){
const tag = att ? ' <span class="tag tag-att" title="查看 ' + m.attempts + ' 次提交明细">刷' + m.attempts + '次</span>' : '';
// 注意:不要把 sid 直接拼进 onclick="...",JSON.stringify 产生的双引号会提前闭合
// HTML 属性(onclick="openAttempts("123")"),点击必然报错。改用 data-sid + 事件委托。
return '<tr' + (att ? ' class="clickable" data-sid="' + esc(m.sid) + '" title="查看提交明细"' : '') + '>' +
return '<tr' + (att ? ' class="clickable"' : '') + ' data-sid="' + esc(m.sid) + '"' + (att ? ' title="查看提交明细"' : '') + '>' +
'<td class="num"><span class="' + badge + '">' + rk + '</span></td>' +
'<td class="name">' + esc(m.name) + tag + '</td>' +
'<td class="score num ' + cls + '">' + (sc == null ? '-' : sc) + '</td>' +
@@ -280,7 +310,11 @@ function openAttempts(sid){
}).join('');
$('#attTitle').textContent = info.name + ' · 共 ' + (info.attempts || list.length) + ' 次提交';
$('#attBody').innerHTML = list.length
? '<table class="grid compact"><thead><tr><th>提交时间</th><th>成绩</th><th>考试时长</th></tr></thead><tbody>' + rows + '</tbody></table>'
? '<table class="grid compact att-table"><thead><tr>' +
'<th data-k="submit_ts" style="width:40%">提交时间</th>' +
'<th data-k="score" style="width:30%">成绩</th>' +
'<th data-k="duration" style="width:30%">考试时长</th>' +
'</tr></thead><tbody>' + rows + '</tbody></table>'
: '<div class="empty">无提交明细</div>';
document.querySelector('#attemptsModal').classList.add('open');
}
@@ -306,13 +340,19 @@ document.addEventListener('keydown', e => {
});
/* ---------- 看板占位(未选问卷/班级) ---------- */
/* 占位卡片配色:与真实看板卡片(renderBoard)一一对应,保证空态左侧色条一致 */
const PLACEHOLDER_ACCENT = {
'总人数': 'accent-blue', '已提交': 'accent-green', '缺考': 'accent-orange',
'提交率': 'accent-purple', '平均分': 'accent-teal', '合格率': 'accent-indigo'
};
function renderPlaceholderBoard(){
const cards = [
['总人数','-'],['已提交','-'],['缺考','-'],
['提交率','-'],['平均分','-'],['合格率','-']
];
$('#boardSummary').innerHTML = cards.map(c =>
'<div class="card"><span>' + c[0] + '</span><b>' + c[1] + '</b></div>').join('');
// 占位卡片与真实看板卡片顺序、配色一致(accent-* 决定左侧色条),避免空态蓝条与真实卡片不一致
'<div class="card ' + PLACEHOLDER_ACCENT[c[0]] + '"><span>' + c[0] + '</span><b>' + c[1] + '</b></div>').join('');
$('#boardBody').innerHTML = '<div class="empty">请选择问卷与班级查看考试整体情况</div>';
// 注意:heroTitle 已随导航栏精简移除,只保留 heroSub(承载当前问卷/班级提示)
const _hs = document.getElementById('heroSub');
@@ -480,7 +520,10 @@ async function refreshBoard(manual){
sel.disabled = !classes.length;
if (classes.length) { sel.value = classes[0]; CUR.cls = classes[0]; writeUrl({ cls: classes[0] }); }
}
// 刷新前捕获各行位置,刷新后做 FLIP:新数据行滑入、其它行下移
const flipPrev = captureFlip($('#boardBody'));
renderBoard();
playFlip($('#boardBody'), flipPrev);
$('#lastUpdate').textContent = fresh.updated_at || '-';
markLive();
} catch (e) {