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:
+258
-49
@@ -22,7 +22,10 @@
|
||||
timer: null,
|
||||
loading: false,
|
||||
lastSig: '',
|
||||
connFail: 0
|
||||
connFail: 0,
|
||||
historyDisplay: 'click', // click=点击成员弹窗查看历史;inline=成绩明细内全部展开
|
||||
exportSort: 'time', // time=按提交时间;sid=按学号
|
||||
highlight: new Set(), // 出现过新提交/新数据的学号,绿色底纹常亮直到清除
|
||||
};
|
||||
|
||||
/* ---------------- 本地存储 ---------------- */
|
||||
@@ -197,6 +200,14 @@
|
||||
}
|
||||
|
||||
/* ---------------- 数据刷新 ---------------- */
|
||||
|
||||
/** 行指纹:任一字段变化都算"有新数据" */
|
||||
function rowSig(r) {
|
||||
return [
|
||||
r.sid, r.score, r.submit_at, r.attempts || 1, r.last_submit_ts || 0,
|
||||
].join('|');
|
||||
}
|
||||
|
||||
function applyData(d, opts) {
|
||||
opts = opts || {};
|
||||
state.questions = d.questions || [];
|
||||
@@ -206,15 +217,36 @@
|
||||
state.roster = d.roster || [];
|
||||
state.fullScore = (d.config && d.config.full_score) ? (parseInt(d.config.full_score, 10) || 100) : 100;
|
||||
|
||||
// 变更检测:学号/成绩/提交时间/考试次数/最近提交时间 任一变化即视为"有新数据"
|
||||
// (纳入 attempts 与 last_submit_ts 才能发现"重考但分数更低"这种提交)
|
||||
const fromCache = !!opts.fromCache;
|
||||
const cur = new Set(state.rows.map(r => r.sid).filter(Boolean));
|
||||
if (state.hasPrev) state.newSids = new Set(Array.from(cur).filter(s => !state.prevSids.has(s)));
|
||||
else state.newSids = new Set();
|
||||
state.hasPrev = true;
|
||||
// 缓存渲染不参与变更检测:本地缓存可能是旧数据,与上一轮真实数据比对会产生假高亮
|
||||
if (state.hasPrev && !fromCache) {
|
||||
state.newSids = new Set(Array.from(cur).filter(s => !state.prevSids.has(s)));
|
||||
state.rows.forEach(r => {
|
||||
const sid = String(r.sid || '');
|
||||
if (sid === '') return;
|
||||
const prev = state.prevMap[sid];
|
||||
if (prev === undefined || prev !== rowSig(r)) {
|
||||
state.highlight.add(sid); // 累积:一直常亮到用户清除或切换项目
|
||||
}
|
||||
});
|
||||
} else if (!state.hasPrev) {
|
||||
state.newSids = new Set();
|
||||
}
|
||||
if (!fromCache) state.hasPrev = true;
|
||||
|
||||
renderNameFields(d);
|
||||
renderStats();
|
||||
// 仅真实数据才更新指纹基准(缓存可能是旧数据,不能作为比对基准)
|
||||
if (!fromCache) {
|
||||
const map = {};
|
||||
state.rows.forEach(r => { map[String(r.sid)] = rowSig(r); });
|
||||
state.prevMap = map;
|
||||
state.prevSids = cur;
|
||||
}
|
||||
renderTable();
|
||||
state.prevSids = cur;
|
||||
populateClasses();
|
||||
populateGroups();
|
||||
renderAbsent();
|
||||
@@ -429,64 +461,168 @@
|
||||
return 'score-low';
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const TEXT_KEYS = { sid: 1, cls: 1, grp: 1, name: 1 };
|
||||
|
||||
function cmpText(x, y) {
|
||||
return String(x).localeCompare(String(y), 'zh', { numeric: true });
|
||||
}
|
||||
|
||||
/** 学号比较:纯数字按数值,否则自然排序 */
|
||||
function cmpSid(a, b) {
|
||||
const s1 = String(a.sid == null ? '' : a.sid);
|
||||
const s2 = String(b.sid == null ? '' : b.sid);
|
||||
if (s1 !== '' && s2 !== '' && !isNaN(Number(s1)) && !isNaN(Number(s2))) {
|
||||
const d = Number(s1) - Number(s2);
|
||||
if (d !== 0) return d;
|
||||
}
|
||||
return cmpText(s1, s2);
|
||||
}
|
||||
|
||||
/** 某排序键是否视为"空值"(空值恒排末尾,不受升降序影响) */
|
||||
function isEmptyVal(key, v) {
|
||||
if (v === null || v === undefined || v === '') return true;
|
||||
// 时间戳 0 表示未知时间
|
||||
if ((key === 'submit_ts' || key === 'last_submit_ts') && Number(v) === 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一比较器:
|
||||
* - 空值恒排末尾(不再因为 -1 混在中间)
|
||||
* - 主键相同时按学号做次级排序,保证顺序稳定
|
||||
*/
|
||||
function cmpBy(key, dir, a, b) {
|
||||
const x = a[key], y = b[key];
|
||||
const xe = isEmptyVal(key, x), ye = isEmptyVal(key, y);
|
||||
if (xe && ye) return cmpSid(a, b) * dir;
|
||||
if (xe) return 1; // 空值永远在后
|
||||
if (ye) return -1;
|
||||
|
||||
let d;
|
||||
if (TEXT_KEYS[key]) {
|
||||
d = cmpText(x, y);
|
||||
} else {
|
||||
const nx = Number(x), ny = Number(y);
|
||||
d = (isNaN(nx) || isNaN(ny)) ? cmpText(x, y) : (nx - ny);
|
||||
}
|
||||
if (d !== 0) return d * dir;
|
||||
return cmpSid(a, b) * dir;
|
||||
}
|
||||
|
||||
/** 展开为"每次考试一行"(历史成绩全部展示模式) */
|
||||
function expandAttempts(rows) {
|
||||
const out = [];
|
||||
rows.forEach(r => {
|
||||
const list = (r.attempts_list && r.attempts_list.length) ? r.attempts_list : null;
|
||||
if (!list) {
|
||||
out.push(Object.assign({}, r, { idx: 1, is_best: true, _att: false }));
|
||||
return;
|
||||
}
|
||||
list.forEach(a => {
|
||||
out.push({
|
||||
sid: r.sid, name: r.name, cls: r.cls, grp: r.grp,
|
||||
score: (a.score === undefined ? null : a.score),
|
||||
submit_ts: a.submit_ts || 0,
|
||||
submit_at: a.submit_at || '-',
|
||||
duration: (a.duration === undefined ? null : a.duration),
|
||||
duration_txt: a.duration_txt || '-',
|
||||
attempts: r.attempts || 1,
|
||||
best_score: (r.best_score === undefined ? null : r.best_score),
|
||||
last_submit_ts: r.last_submit_ts || 0,
|
||||
last_submit_at: r.last_submit_at || '-',
|
||||
idx: a.idx || 1,
|
||||
is_best: !!a.is_best,
|
||||
answer_id: a.answer_id || '',
|
||||
temp_save: !!a.temp_save,
|
||||
_att: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 依据筛选 + 展示模式,得到待渲染的行 */
|
||||
function buildRows() {
|
||||
const kw = $('#kw').value.trim().toLowerCase();
|
||||
const clsFilter = $('#clsSel').value;
|
||||
const grpFilter = $('#grpSel').value;
|
||||
let rows = state.rows.slice();
|
||||
let base = state.rows.slice();
|
||||
|
||||
if (clsFilter) rows = rows.filter(r => (r.cls || '') === clsFilter);
|
||||
if (grpFilter) rows = rows.filter(r => (r.grp || '') === grpFilter);
|
||||
if (kw) rows = rows.filter(r =>
|
||||
if (clsFilter) base = base.filter(r => (r.cls || '') === clsFilter);
|
||||
if (grpFilter) base = base.filter(r => (r.grp || '') === grpFilter);
|
||||
if (kw) base = base.filter(r =>
|
||||
String(r.sid).toLowerCase().includes(kw) || String(r.name).toLowerCase().includes(kw));
|
||||
|
||||
if (state.historyDisplay === 'inline') {
|
||||
return expandAttempts(base);
|
||||
}
|
||||
// 一人一行:提交时间显示"最近一次提交",使"按提交时间排序"= 谁最近交的
|
||||
return base.map(r => Object.assign({}, r, {
|
||||
submit_ts: (r.last_submit_ts || r.submit_ts || 0),
|
||||
submit_at: (r.last_submit_at && r.last_submit_at !== '-') ? r.last_submit_at : (r.submit_at || '-'),
|
||||
_att: false,
|
||||
}));
|
||||
}
|
||||
|
||||
const COL_COUNT = 8;
|
||||
|
||||
/** 更新"清除高亮"按钮的可见性与计数 */
|
||||
function renderHlBadge() {
|
||||
const btn = $('#btnClearHl');
|
||||
const badge = $('#hlBadge');
|
||||
if (!btn) return;
|
||||
const n = state.highlight.size;
|
||||
if (badge) badge.textContent = n;
|
||||
btn.hidden = (n === 0);
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const rows = buildRows();
|
||||
const k = state.sortKey, dir = state.sortAsc ? 1 : -1;
|
||||
rows.sort((a, b) => {
|
||||
let x = a[k], y = b[k];
|
||||
if (k === 'sid' || k === 'cls') {
|
||||
const na = parseFloat(x), nb = parseFloat(y);
|
||||
if (!isNaN(na) && !isNaN(nb)) return (na - nb) * dir;
|
||||
return String(x == null ? '' : x).localeCompare(String(y == null ? '' : y), 'zh', { numeric: true }) * dir;
|
||||
}
|
||||
if (x == null) x = -1;
|
||||
if (y == null) y = -1;
|
||||
if (typeof x === 'string') return x.localeCompare(String(y), 'zh', { numeric: true }) * dir;
|
||||
return (x - y) * dir;
|
||||
});
|
||||
rows.sort((a, b) => cmpBy(k, dir, a, b));
|
||||
|
||||
const tb = $('#tbody');
|
||||
if (!rows.length) {
|
||||
tb.innerHTML = '<tr><td colspan="7" class="empty">' +
|
||||
tb.innerHTML = '<tr><td colspan="' + COL_COUNT + '" class="empty">' +
|
||||
(state.rows.length ? '没有匹配的记录' : '暂无提交记录') + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
tb.innerHTML = rows.map(r => {
|
||||
const sig = r.sid + '|' + r.score + '|' + r.submit_at;
|
||||
const changed = state.prevMap[r.sid] && state.prevMap[r.sid] !== sig;
|
||||
const isNew = r.sid && state.newSids.has(r.sid);
|
||||
const canDrill = (r.attempts || 1) > 1;
|
||||
const clsName = (changed ? 'diff ' : '') + (isNew ? 'new ' : '') + (canDrill ? 'clickable' : '');
|
||||
const att = canDrill ? ' <span class="tag tag-att" title="查看 ' + r.attempts + ' 次提交明细">刷' + r.attempts + '次</span>' : '';
|
||||
const onclk = canDrill ? ' onclick="openAttemptsModal(' + JSON.stringify(r.sid) + ')"' : '';
|
||||
return '<tr class="' + clsName.trim() + '"' + onclk + '>' +
|
||||
const sid = String(r.sid || '');
|
||||
const isNew = sid !== '' && state.newSids.has(sid);
|
||||
const hl = sid !== '' && state.highlight.has(sid);
|
||||
const attempts = (r.attempts || 1);
|
||||
const clsName = [hl ? 'new' : '', 'clickable'].filter(Boolean).join(' ');
|
||||
|
||||
// 姓名后缀标记
|
||||
const tags = [];
|
||||
if (isNew) tags.push('<span class="tag tag-new">新</span>');
|
||||
if (r._att && r.is_best && attempts > 1) tags.push('<span class="tag tag-best">最佳</span>');
|
||||
if (r._att && r.temp_save) tags.push('<span class="tag tag-gray">暂存</span>');
|
||||
|
||||
// 考试次数:全部展示模式下额外标出"第几次"
|
||||
const attCell = r._att && attempts > 1
|
||||
? attempts + ' <small style="color:var(--text-3)">第' + r.idx + '次</small>'
|
||||
: String(attempts);
|
||||
|
||||
return '<tr class="' + clsName + '" data-sid="' + esc(sid) + '" title="点击查看历史成绩">' +
|
||||
'<td class="sid">' + esc(r.sid) + '</td>' +
|
||||
'<td class="name">' + esc(r.name) + att + '</td>' +
|
||||
'<td class="name">' + esc(r.name) + (tags.length ? ' ' + tags.join(' ') : '') + '</td>' +
|
||||
'<td>' + esc(r.cls || '') + '</td>' +
|
||||
'<td>' + esc(r.grp || '') + '</td>' +
|
||||
'<td class="score num ' + scoreClass(r.score) + '">' + (r.score == null ? '-' : r.score) + '</td>' +
|
||||
'<td class="num att">' + attCell + '</td>' +
|
||||
'<td class="num">' + esc(r.submit_at) + '</td>' +
|
||||
'<td class="num">' + esc(r.duration_txt) + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
|
||||
const map = {};
|
||||
state.rows.forEach(r => { map[r.sid] = r.sid + '|' + r.score + '|' + r.submit_at; });
|
||||
state.prevMap = map;
|
||||
|
||||
$$('#dataTable th.sortable').forEach(th => {
|
||||
const isCur = th.dataset.k === state.sortKey;
|
||||
th.innerHTML = th.textContent.replace(/[\s↑↓]+$/, '') + (isCur ? ' <span class="arrow">' + (state.sortAsc ? '↑' : '↓') + '</span>' : '');
|
||||
});
|
||||
|
||||
renderHlBadge();
|
||||
}
|
||||
|
||||
function renderAbsent() {
|
||||
@@ -519,21 +655,39 @@
|
||||
'</td><td class="num">' + esc(u.submit_at) + '</td></tr>').join('');
|
||||
}
|
||||
|
||||
/* 多次刷题下钻弹窗(主统计页成绩明细) */
|
||||
/* 历史成绩弹窗(主统计页成绩明细):任意成员都可点开,含只考过 1 次的 */
|
||||
function openAttemptsModal(sid) {
|
||||
const r = (state.rows || []).find(x => String(x.sid) === String(sid));
|
||||
if (!r || (r.attempts || 1) <= 1) return;
|
||||
const list = r.attempts_list || [];
|
||||
const rows = list.map(a => {
|
||||
const sc = a.score == null ? '<span class="muted">未判分</span>' : esc(a.score);
|
||||
if (!r) return;
|
||||
const attempts = (r.attempts || 1);
|
||||
let list = r.attempts_list || [];
|
||||
if (!list.length) {
|
||||
// 后端未返回明细时,用代表行兜底成一条
|
||||
list = [{
|
||||
idx: 1, is_best: true, score: r.score, submit_at: r.submit_at,
|
||||
duration_txt: r.duration_txt, temp_save: !!r.temp_save,
|
||||
}];
|
||||
}
|
||||
// 新 -> 旧 展示
|
||||
const desc = list.slice().sort((a, b) => (b.submit_ts || 0) - (a.submit_ts || 0));
|
||||
const rows = desc.map((a, i) => {
|
||||
const sc = (a.score == null) ? '<span class="muted">未判分</span>' : esc(a.score);
|
||||
const best = (a.is_best && attempts > 1) ? ' <span class="tag tag-best">最佳</span>' : '';
|
||||
const tmp = a.temp_save ? ' <span class="tag tag-gray">暂存</span>' : '';
|
||||
return '<tr><td class="num">' + esc(a.submit_at) + '</td><td class="num">' + sc + tmp +
|
||||
'</td><td class="num">' + esc(a.duration_txt || '-') + '</td></tr>';
|
||||
const no = attempts > 1 ? (a.idx || (desc.length - i)) : 1;
|
||||
return '<tr>' +
|
||||
'<td class="num">' + (attempts > 1 ? '第 ' + no + ' 次' : '-') + '</td>' +
|
||||
'<td class="num">' + esc(a.submit_at || '-') + '</td>' +
|
||||
'<td class="num">' + sc + best + tmp + '</td>' +
|
||||
'<td class="num">' + esc(a.duration_txt || '-') + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
$('#attTitle').textContent = (r.name || '') + ' · 共 ' + r.attempts + ' 次提交';
|
||||
$('#attBody').innerHTML = list.length
|
||||
? '<table class="grid compact"><thead><tr><th>提交时间</th><th>成绩</th><th>考试时长</th></tr></thead><tbody>' + rows + '</tbody></table>'
|
||||
: '<div class="empty">无提交明细</div>';
|
||||
const bestScore = (r.best_score == null) ? r.score : r.best_score;
|
||||
$('#attTitle').textContent = (r.name || '') + '(' + r.sid + ') · 共 ' + attempts + ' 次考试' +
|
||||
(bestScore == null ? '' : ' · 最高 ' + bestScore + ' 分');
|
||||
$('#attBody').innerHTML =
|
||||
'<table class="grid compact"><thead><tr><th style="width:80px">次序</th><th>提交时间</th><th>成绩</th><th>考试时长</th></tr></thead><tbody>' +
|
||||
rows + '</tbody></table>';
|
||||
$('#attemptsModal').classList.add('open');
|
||||
}
|
||||
function closeAttemptsModal() {
|
||||
@@ -592,6 +746,12 @@
|
||||
// 满分:默认 + 各试卷独立
|
||||
$('#cfgFull').value = c.full_score || 100;
|
||||
renderFullScores((c.full_scores && typeof c.full_scores === 'object') ? c.full_scores : {});
|
||||
// 历史成绩展示方式 + 导出排序
|
||||
state.historyDisplay = (c.history_display === 'inline') ? 'inline' : 'click';
|
||||
state.exportSort = (c.export_sort === 'sid') ? 'sid' : 'time';
|
||||
$('#cfgHistoryDisplay').value = state.historyDisplay;
|
||||
$('#cfgExportSort').value = state.exportSort;
|
||||
if ($('#exportSortSel')) $('#exportSortSel').value = state.exportSort;
|
||||
// 连接信息(账号回显;密码不回显)
|
||||
$('#cfgBase').value = (c.base_url || '') + (c.api_prefix || '/api');
|
||||
$('#cfgUser').value = c.username || '';
|
||||
@@ -694,12 +854,14 @@
|
||||
});
|
||||
$('#nameFieldSel').addEventListener('change', () => {
|
||||
state.prevMap = {}; state.prevSids = new Set(); state.hasPrev = false;
|
||||
state.highlight = new Set(); // 换了姓名题,旧高亮无意义
|
||||
LS.set(LS_LASTQID, $('#nameFieldSel').value || '');
|
||||
refresh();
|
||||
});
|
||||
$('#projectSel').addEventListener('change', () => {
|
||||
// 切换项目后清掉前一项目的 prevMap 与高亮缓存
|
||||
state.prevMap = {}; state.prevSids = new Set(); state.hasPrev = false;
|
||||
state.highlight = new Set(); // 换项目,绿色高亮归零
|
||||
// 用缓存数据先快速渲染,再后台拉新数据
|
||||
refresh();
|
||||
});
|
||||
@@ -720,15 +882,42 @@
|
||||
});
|
||||
});
|
||||
|
||||
/* 成绩明细行点击 → 历史成绩弹窗(事件委托,避免内联 onclick 找不到 IIFE 内函数) */
|
||||
$('#tbody').addEventListener('click', e => {
|
||||
const tr = e.target.closest('tr[data-sid]');
|
||||
if (!tr) return;
|
||||
const sid = tr.getAttribute('data-sid');
|
||||
if (sid) openAttemptsModal(sid);
|
||||
});
|
||||
|
||||
/* 清除高亮 */
|
||||
$('#btnClearHl').addEventListener('click', () => {
|
||||
state.highlight = new Set();
|
||||
renderTable();
|
||||
renderHlBadge();
|
||||
showAlert('已清除新数据高亮', 'info');
|
||||
});
|
||||
|
||||
/* 导出排序切换(即时保存到服务端配置) */
|
||||
$('#exportSortSel').addEventListener('change', async () => {
|
||||
state.exportSort = $('#exportSortSel').value;
|
||||
if ($('#cfgExportSort')) $('#cfgExportSort').value = state.exportSort;
|
||||
try {
|
||||
await api('config_save', { method: 'POST', body: { export_sort: state.exportSort } });
|
||||
} catch (e) { /* 静默:本地已生效 */ }
|
||||
});
|
||||
|
||||
// 导出跟随当前班级 / 组别筛选(统计卡片与表格显示什么,就导出什么)
|
||||
$('#btnExport').addEventListener('click', () => {
|
||||
const pid = $('#projectSel').value;
|
||||
if (!pid) return showAlert('请先选择问卷 / 考试');
|
||||
const cls = $('#clsSel').value, grp = $('#grpSel').value;
|
||||
const sort = (state.exportSort === 'sid') ? 'sid' : 'time';
|
||||
location.href = 'api.php?action=export&projectId=' + encodeURIComponent(pid) +
|
||||
'&nameQid=' + encodeURIComponent($('#nameFieldSel').value) +
|
||||
'&cls=' + encodeURIComponent(cls) + '&grp=' + encodeURIComponent(grp);
|
||||
showAlert(exportTip('成绩明细', cls, grp), 'info');
|
||||
'&cls=' + encodeURIComponent(cls) + '&grp=' + encodeURIComponent(grp) +
|
||||
'&sort=' + sort;
|
||||
showAlert(exportTip('成绩明细', cls, grp) + ',排序:' + (sort === 'sid' ? '按学号' : '按提交时间'), 'info');
|
||||
});
|
||||
$('#btnExportAbsent').addEventListener('click', () => {
|
||||
const pid = $('#projectSel').value;
|
||||
@@ -764,6 +953,8 @@
|
||||
layout: (document.querySelector('input[name=cfgLayout]:checked') || {}).value || 'default',
|
||||
full_scores: collectFullScores(),
|
||||
username: $('#cfgUser').value.trim(),
|
||||
history_display: $('#cfgHistoryDisplay').value,
|
||||
export_sort: $('#cfgExportSort').value,
|
||||
};
|
||||
if (pass !== '') payload.password = pass; // 留空 = 不修改
|
||||
let resp;
|
||||
@@ -790,6 +981,11 @@
|
||||
$('#intervalSel').value = String(best);
|
||||
schedule();
|
||||
loadProjects();
|
||||
// 展示方式可能变化 → 立即重渲染表格(不重新拉数据)
|
||||
state.historyDisplay = payload.history_display;
|
||||
state.exportSort = payload.export_sort;
|
||||
if ($('#exportSortSel')) $('#exportSortSel').value = state.exportSort;
|
||||
renderTable();
|
||||
// ping 失败时保留弹窗,让用户能立即改回
|
||||
if (!resp || !resp.ping || resp.ping.ok) {
|
||||
$('#settingsModal').classList.remove('open');
|
||||
@@ -940,5 +1136,18 @@
|
||||
// 启动:主界面在 PHP 端已做会话校验(未登录会跳到 login.php),此处直接加载数据
|
||||
schedule();
|
||||
loadProjects();
|
||||
|
||||
// 4) 读取展示偏好(历史成绩展示方式 / 导出排序),避免必须打开设置弹窗才生效
|
||||
(async () => {
|
||||
try {
|
||||
const c = await api('config_get');
|
||||
state.historyDisplay = (c.history_display === 'inline') ? 'inline' : 'click';
|
||||
state.exportSort = (c.export_sort === 'sid') ? 'sid' : 'time';
|
||||
if ($('#cfgHistoryDisplay')) $('#cfgHistoryDisplay').value = state.historyDisplay;
|
||||
if ($('#cfgExportSort')) $('#cfgExportSort').value = state.exportSort;
|
||||
if ($('#exportSortSel')) $('#exportSortSel').value = state.exportSort;
|
||||
renderTable();
|
||||
} catch (e) { /* 用默认值即可 */ }
|
||||
})();
|
||||
});
|
||||
})();
|
||||
|
||||
+12
-2
@@ -303,9 +303,15 @@ td.score { font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
.diff { animation: flash 1.6s ease-out; }
|
||||
@keyframes flash { from { background: #fff6d9; } to { background: transparent; } }
|
||||
|
||||
/* 新增数据绿色底纹(每次刷新后标记,下一轮刷新前持续显示) */
|
||||
/* 新数据绿色底纹:常亮(不再闪烁),由"清除高亮"或切换项目/姓名题清除 */
|
||||
table.grid tbody tr.new td { background: #e7f7ee; }
|
||||
table.grid tbody tr.new:nth-child(even) td { background: #e7f7ee; }
|
||||
table.grid tbody tr.new:hover td { background: #d6f2e2; }
|
||||
/* 左侧色条,进一步强调"这条是新的" */
|
||||
table.grid tbody tr.new td:first-child { box-shadow: inset 3px 0 0 var(--ok); }
|
||||
|
||||
/* 考试次数列 */
|
||||
td.att { font-weight: 700; color: var(--text-2); }
|
||||
|
||||
.tag {
|
||||
display: inline-block; padding: 1px 8px; border-radius: 999px;
|
||||
@@ -315,10 +321,14 @@ table.grid tbody tr.new:hover td { background: #d6f2e2; }
|
||||
.tag-warn { background: var(--warn-weak); color: var(--warn); }
|
||||
.tag-gray { background: var(--border-2); color: var(--text-3); }
|
||||
.tag-att { background: #e8eefc; color: #3457c9; cursor: pointer; }
|
||||
.tag-new { background: var(--ok-weak); color: var(--ok); }
|
||||
.tag-best { background: #fff1dc; color: #b26a06; }
|
||||
|
||||
/* 多次刷题:成员榜可点击行 */
|
||||
tr.clickable { cursor: pointer; }
|
||||
tr.clickable:hover td { background: var(--border-2); }
|
||||
tr.clickable:hover td { background: var(--primary-weak); }
|
||||
/* 绿色常亮行 hover 时保持绿色调,避免看起来"消失" */
|
||||
table.grid tbody tr.new.clickable:hover td { background: #d6f2e2; }
|
||||
.muted { color: var(--text-3); }
|
||||
|
||||
/* ---------- 弹窗 ---------- */
|
||||
|
||||
Reference in New Issue
Block a user