完成批次 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:
+200
-56
@@ -252,19 +252,56 @@
|
||||
].join('|');
|
||||
}
|
||||
|
||||
/**
|
||||
* 整份数据指纹:仅覆盖"会影响 DOM 渲染"的字段。
|
||||
* 用于 applyData 的无变化守卫——自动刷新拿到与屏幕完全一致的数据时,跳过昂贵整表重绘。
|
||||
* 含 project id/name 防止"不同项目数据恰好相同"导致误跳过。
|
||||
*/
|
||||
function dataSig(d) {
|
||||
const rows = d.rows || [];
|
||||
const roster = d.roster || [];
|
||||
const unmatched = d.unmatched || [];
|
||||
const questions = d.questions || [];
|
||||
const rs = rows.map(rowSig).join('|');
|
||||
const ros = roster.map(r => (r.sid || '') + ':' + (r.cls || '') + ':' + (r.grp || '')).join('|');
|
||||
const um = unmatched.map(u => (u.name || '') + ':' + (u.score == null ? '-' : u.score)).join('|');
|
||||
const q = questions.length + ':' + questions.map(q => q.id).join(',');
|
||||
const proj = (d.project && (d.project.id || d.project.name)) || '';
|
||||
return [
|
||||
proj, d.updated_at || '', rows.length, rs, ros, um, q,
|
||||
(d.absent || []).length, d.whitelist_driven ? 1 : 0, d.whitelist_total || 0,
|
||||
].join('§');
|
||||
}
|
||||
|
||||
function applyData(d, opts) {
|
||||
opts = opts || {};
|
||||
const fromCache = !!opts.fromCache;
|
||||
state.questions = d.questions || [];
|
||||
state.rows = d.rows || [];
|
||||
state.absent = d.absent || [];
|
||||
state.unmatched = d.unmatched || [];
|
||||
state.roster = d.roster || [];
|
||||
state.whitelistDriven = !!d.whitelist_driven;
|
||||
state.whitelistTotal = (d.whitelist_total || 0);
|
||||
state.fullScore = (d.config && d.config.full_score) ? (parseInt(d.config.full_score, 10) || 100) : 100;
|
||||
state.passRate = (d.config && d.config.pass_rate != null) ? (parseFloat(d.config.pass_rate) || 60) : 60;
|
||||
|
||||
// 性能守卫:数据指纹未变化则跳过昂贵整表重绘(自动刷新拿到与屏幕一致的数据时不再每轮重建 DOM)。
|
||||
// 缓存预渲染路径照常保留——刷新间隙保持视觉稳定;仅当屏幕上已是同一份数据时才跳过。
|
||||
const sig = dataSig(d);
|
||||
if (sig === state._renderedSig) {
|
||||
if (!fromCache) { // 真实刷新且数据未变:仅刷新轻量元信息,不重建 DOM
|
||||
state.hasPrev = true; // 维持指纹基准,供下次 diff
|
||||
$('#lastUpdate').textContent = d.updated_at || '—';
|
||||
state.projectName = (d.project && d.project.name) || '';
|
||||
setConn('conn-on', '实时 · ' + (d.project && d.project.name || ''));
|
||||
showAlert('');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 变更检测:学号/成绩/提交时间/考试次数/最近提交时间 任一变化即视为"有新数据"
|
||||
// (纳入 attempts 与 last_submit_ts 才能发现"重考但分数更低"这种提交)
|
||||
const fromCache = !!opts.fromCache;
|
||||
const cur = new Set(state.rows.map(r => r.sid).filter(Boolean));
|
||||
// 缓存渲染不参与变更检测:本地缓存可能是旧数据,与上一轮真实数据比对会产生假高亮
|
||||
if (state.hasPrev && !fromCache) {
|
||||
@@ -291,7 +328,10 @@
|
||||
state.prevMap = map;
|
||||
state.prevSids = cur;
|
||||
}
|
||||
// 仅在有新数据时做 FLIP 行动画(新行滑入、其它行下移);搜索/排序重排不触发
|
||||
const flipPrev = (state.newSids.size > 0) ? captureFlip($('#tbody')) : null;
|
||||
renderTable();
|
||||
if (flipPrev) playFlip($('#tbody'), flipPrev);
|
||||
populateClasses();
|
||||
populateGroups();
|
||||
renderAbsent();
|
||||
@@ -300,8 +340,9 @@
|
||||
|
||||
$('#lastUpdate').textContent = d.updated_at || '—';
|
||||
state.projectName = (d.project && d.project.name) || '';
|
||||
setConn('conn-on', (opts.fromCache ? '缓存 · ' : '') + '实时 · ' + (d.project && d.project.name || ''));
|
||||
if (!opts.fromCache) showAlert('');
|
||||
setConn('conn-on', (fromCache ? '缓存 · ' : '') + '实时 · ' + (d.project && d.project.name || ''));
|
||||
if (!fromCache) showAlert('');
|
||||
state._renderedSig = sig; // 记录当前屏幕指纹,下次相同则跳过整表重绘
|
||||
}
|
||||
|
||||
async function refresh(silent, force) {
|
||||
@@ -652,6 +693,42 @@
|
||||
btn.hidden = (n === 0);
|
||||
}
|
||||
|
||||
// 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;
|
||||
const trs = container.querySelectorAll('tr[data-sid]');
|
||||
for (const tr of trs) {
|
||||
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;
|
||||
const trs = container.querySelectorAll('tr[data-sid]');
|
||||
for (const tr of trs) {
|
||||
const sid = tr.getAttribute('data-sid');
|
||||
if (!sid) continue;
|
||||
const oldTop = prev.get(sid);
|
||||
if (oldTop === undefined) {
|
||||
tr.classList.add('row-drop-in'); // 全新行:轻微落入
|
||||
continue;
|
||||
}
|
||||
const newTop = tr.getBoundingClientRect().top;
|
||||
const dy = oldTop - newTop;
|
||||
if (Math.abs(dy) < 1) continue;
|
||||
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 = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const rows = buildRows();
|
||||
const k = state.sortKey, dir = state.sortAsc ? 1 : -1;
|
||||
@@ -662,8 +739,9 @@
|
||||
if (!rows.length) {
|
||||
tb.innerHTML = '<tr><td colspan="' + COL_COUNT + '" class="empty">' +
|
||||
(state.rows.length ? '没有匹配的记录' : '暂无提交记录') + '</td></tr>';
|
||||
_lastRange = ''; // 空表也重置窗口签名,下次有数据必重绘
|
||||
} else {
|
||||
renderVirtualRows(tb, rows);
|
||||
renderVirtualRows(tb, rows, true); // 排序/搜索/数据变更:强制重绘
|
||||
}
|
||||
|
||||
$$('#dataTable th.sortable').forEach(th => {
|
||||
@@ -690,7 +768,7 @@
|
||||
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 clsName = [hl ? 'new' : '', (_dropAnim && isNew) ? 'row-drop-in' : '', 'clickable'].filter(Boolean).join(' ');
|
||||
|
||||
const tags = [];
|
||||
if (isNew) tags.push('<span class="tag tag-new">新</span>');
|
||||
@@ -720,43 +798,60 @@
|
||||
const ROW_H_FALLBACK = 41; // 首帧未测量前的行高估值
|
||||
const VS_OVERSCAN = 12; // 视口上下各多渲染的行数,减少快速滚动时的白屏
|
||||
let measuredRowH = 0; // 实测行高(渲染后回写),CSS/字体/缩放变化都能自动适应
|
||||
function renderVirtualRows(tb, rows) {
|
||||
const scroller = tb.closest('.table-scroll');
|
||||
if (!scroller) {
|
||||
// 兜底:找不到滚动容器时退回全量渲染
|
||||
tb.innerHTML = rows.map(rowHtml).join('');
|
||||
return;
|
||||
}
|
||||
const scrollTop = scroller.scrollTop;
|
||||
const viewH = scroller.clientHeight;
|
||||
const total = rows.length;
|
||||
const rh = measuredRowH || ROW_H_FALLBACK;
|
||||
let start = Math.floor(scrollTop / rh) - VS_OVERSCAN;
|
||||
let end = Math.ceil((scrollTop + viewH) / rh) + VS_OVERSCAN;
|
||||
if (start < 0) start = 0;
|
||||
if (end > total) end = total;
|
||||
if (start >= end) start = Math.max(0, end - 1);
|
||||
let _lastRange = ''; // 上次可视窗口签名(start:end:total:rh),未变化则跳过重绘,消除滚动闪烁
|
||||
let _dropAnim = true; // 仅"数据/排序/搜索变更"的强制重绘才让新行播放落入动画;滚动复用 DOM 时不重播,避免滑动时一行行闪
|
||||
function renderVirtualRows(tb, rows, force) {
|
||||
const prevDrop = _dropAnim;
|
||||
_dropAnim = !!force; // 滚动复用(force=false)时关闭落入动画,节点重建不再触发 replay
|
||||
try {
|
||||
const scroller = tb.closest('.table-scroll');
|
||||
if (!scroller) {
|
||||
// 兜底:找不到滚动容器时退回全量渲染
|
||||
tb.innerHTML = rows.map(rowHtml).join('');
|
||||
return;
|
||||
}
|
||||
const scrollTop = scroller.scrollTop;
|
||||
const viewH = scroller.clientHeight;
|
||||
const total = rows.length;
|
||||
const rh = measuredRowH || ROW_H_FALLBACK;
|
||||
let start = Math.floor(scrollTop / rh) - VS_OVERSCAN;
|
||||
let end = Math.ceil((scrollTop + viewH) / rh) + VS_OVERSCAN;
|
||||
if (start < 0) start = 0;
|
||||
if (end > total) end = total;
|
||||
if (start >= end) start = Math.max(0, end - 1);
|
||||
|
||||
const frag = [];
|
||||
if (start > 0) {
|
||||
frag.push('<tr class="vspacer" style="height:' + (start * rh) + 'px;pointer-events:none"><td colspan="' + COL_COUNT + '"></td></tr>');
|
||||
}
|
||||
for (let i = start; i < end; i++) {
|
||||
frag.push(rowHtml(rows[i]));
|
||||
}
|
||||
if (end < total) {
|
||||
frag.push('<tr class="vspacer" style="height:' + ((total - end) * rh) + 'px;pointer-events:none"><td colspan="' + COL_COUNT + '"></td></tr>');
|
||||
}
|
||||
tb.innerHTML = frag.join('');
|
||||
// 可视窗口未变(含小幅滚动仍在 overscan 内)→ 直接复用现有 DOM:
|
||||
// 避免每帧重写 innerHTML 触发整块重绘,这是"滑一下闪一下"的根源。
|
||||
const range = start + ':' + end + ':' + total + ':' + rh;
|
||||
if (!force && range === _lastRange && tb.firstChild) return;
|
||||
_lastRange = range;
|
||||
|
||||
// 渲染后回写真实行高:换字体、改样式、页面缩放时窗口定位都不会漂移
|
||||
const tr0 = tb.querySelector('tr[data-sid]');
|
||||
if (tr0) {
|
||||
const h = tr0.offsetHeight;
|
||||
if (h > 12 && h < 300) measuredRowH = h;
|
||||
const frag = [];
|
||||
if (start > 0) {
|
||||
frag.push('<tr class="vspacer" style="height:' + (start * rh) + 'px;pointer-events:none"><td colspan="' + COL_COUNT + '"></td></tr>');
|
||||
}
|
||||
for (let i = start; i < end; i++) {
|
||||
frag.push(rowHtml(rows[i]));
|
||||
}
|
||||
if (end < total) {
|
||||
frag.push('<tr class="vspacer" style="height:' + ((total - end) * rh) + 'px;pointer-events:none"><td colspan="' + COL_COUNT + '"></td></tr>');
|
||||
}
|
||||
tb.innerHTML = frag.join('');
|
||||
|
||||
// 渲染后回写真实行高:换字体、改样式、页面缩放时窗口定位都不会漂移。
|
||||
// 仅在尚未测量时回写一次,避免滚动中反复改动行高导致 spacer 高度抖动(也会造成观感闪烁)。
|
||||
if (measuredRowH === 0) {
|
||||
const tr0 = tb.querySelector('tr[data-sid]');
|
||||
if (tr0) {
|
||||
const h = tr0.offsetHeight;
|
||||
if (h > 12 && h < 300) measuredRowH = h;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
_dropAnim = prevDrop;
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', () => { measuredRowH = 0; });
|
||||
window.addEventListener('resize', () => { measuredRowH = 0; _lastRange = ''; });
|
||||
|
||||
/** 供滚动监听调用:滚动时重算可视窗口(不重新排序,直接复用 state._sorted) */
|
||||
function onTableScroll() {
|
||||
@@ -787,13 +882,14 @@
|
||||
// 按班统计缺考人数(未按班级筛选时显示概览)
|
||||
const hint = $('#absentHint');
|
||||
if (hint) {
|
||||
const wlNote = state.whitelistDriven ? ('白名单应考 ' + (state.whitelistTotal || list.length) + ' 人 · ') : '';
|
||||
if (!clsFilter && list.length) {
|
||||
const byCls = {};
|
||||
list.forEach(a => { const c = (a.cls || '未分班'); byCls[c] = (byCls[c] || 0) + 1; });
|
||||
const parts = Object.keys(byCls).sort().map(c => c + ' ' + byCls[c] + ' 人');
|
||||
hint.textContent = '共 ' + list.length + ' 人 · ' + parts.join(' ');
|
||||
hint.textContent = wlNote + '共 ' + list.length + ' 人 · ' + parts.join(' ');
|
||||
} else {
|
||||
hint.textContent = list.length ? '共 ' + list.length + ' 人' : '';
|
||||
hint.textContent = wlNote + (list.length ? '共 ' + list.length + ' 人' : '');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -815,7 +911,7 @@
|
||||
'<td>' + esc(u.name) + '</td>' +
|
||||
'<td class="num">' + (u.score == null ? '-' : u.score) + '</td>' +
|
||||
'<td class="num">' + esc(u.submit_at) + '</td>' +
|
||||
'<td><button class="btn xs" data-map="' + esc(u.name) + '">加入名单</button></td></tr>').join('');
|
||||
'<td class="act"><button class="btn xs" data-map="' + esc(u.name) + '">加入名单</button></td></tr>').join('');
|
||||
}
|
||||
|
||||
/* 历史成绩弹窗(主统计页成绩明细):任意成员都可点开,含只考过 1 次的 */
|
||||
@@ -854,15 +950,15 @@
|
||||
$('#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>' +
|
||||
'<table class="grid compact att-table"><thead><tr>' +
|
||||
'<th data-k="attempts" style="width:18%">次序</th>' +
|
||||
'<th data-k="submit_ts" style="width:34%">提交时间</th>' +
|
||||
'<th data-k="score" style="width:24%">成绩</th>' +
|
||||
'<th data-k="duration" style="width:24%">考试时长</th>' +
|
||||
'</tr></thead><tbody>' +
|
||||
rows + '</tbody></table>';
|
||||
$('#attemptsModal').classList.add('open');
|
||||
}
|
||||
function closeAttemptsModal() {
|
||||
const m = $('#attemptsModal');
|
||||
if (m) m.classList.remove('open');
|
||||
}
|
||||
|
||||
/* ---------------- 自动刷新 ---------------- */
|
||||
// 失败退避:连续失败时延长下次刷新间隔(1x → 2x → 4x,封顶 8x),成功后复位。
|
||||
// 用 setTimeout 链而非 setInterval,便于动态调整间隔;后台标签页暂停刷新,回到前台立即补一次。
|
||||
@@ -994,7 +1090,7 @@
|
||||
String(r.sid).toLowerCase().includes(kw) || String(r.name).toLowerCase().includes(kw));
|
||||
const tb = $('#rosterBody');
|
||||
if (!rows.length) {
|
||||
tb.innerHTML = '<tr><td colspan="6" class="empty">暂无名单,请上传文件</td></tr>';
|
||||
tb.innerHTML = '<tr><td colspan="5" class="empty">暂无名单,请上传文件</td></tr>';
|
||||
$('#btnDelSel').hidden = true;
|
||||
return;
|
||||
}
|
||||
@@ -1006,8 +1102,7 @@
|
||||
'<td><input value="' + esc(r.sid) + '" data-f="sid"></td>' +
|
||||
'<td><input value="' + esc(r.name) + '" data-f="name"></td>' +
|
||||
'<td><input value="' + esc(r.cls || '') + '" data-f="cls" placeholder="可选"></td>' +
|
||||
'<td><input value="' + esc(r.grp || '') + '" data-f="grp" placeholder="A/B"></td>' +
|
||||
'<td><input value="' + esc(r.login || '') + '" data-f="login" placeholder="可选"></td></tr>';
|
||||
'<td><input value="' + esc(r.grp || '') + '" data-f="grp" placeholder="A/B"></td></tr>';
|
||||
}).join('');
|
||||
updateDelSelBtn();
|
||||
}
|
||||
@@ -1298,7 +1393,7 @@
|
||||
async function uploadRoster(file) {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
const mode = (document.querySelector('input[name=impMode]:checked') || {}).value || 'replace';
|
||||
const mode = (document.querySelector('input[name=impMode]:checked') || {}).value || 'append';
|
||||
fd.append('mode', mode);
|
||||
try {
|
||||
const d = await api('roster_import', { form: fd });
|
||||
@@ -1311,6 +1406,19 @@
|
||||
|
||||
$('#rosterSearch').addEventListener('input', renderRoster);
|
||||
|
||||
// 下载名单导入模板(CSV,带 BOM 便于 Excel 正确识别 UTF-8)
|
||||
$('#btnTpl').addEventListener('click', () => {
|
||||
const header = ['学号', '姓名', '班级', '组别', '登陆账号'];
|
||||
const sample = ['2024001', '张三', '高一(1)班', 'A', 'zhangsan'];
|
||||
const csv = '' + [header.join(','), sample.join(',')].join('\r\n');
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = '名单导入模板.csv';
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
|
||||
});
|
||||
|
||||
// 新增学生:弹出表单,填写后追加(不再直接在表格底部插入空行)
|
||||
$('#btnAddRow').addEventListener('click', () => {
|
||||
$('#addSid').value = ''; $('#addName').value = '';
|
||||
@@ -1432,7 +1540,6 @@
|
||||
} catch (err) { showAlert(err.message); }
|
||||
});
|
||||
|
||||
/* 成绩快照 / 趋势 */
|
||||
function curPid() { return $('#projectSel').value; }
|
||||
|
||||
/* 成绩明细打印:默认按成绩降序,可选按班级/组别分组对比 */
|
||||
@@ -1496,8 +1603,46 @@
|
||||
].concat(extra || []).join(' | ');
|
||||
}
|
||||
|
||||
// 展开每位学生的每次提交为独立一行(用于「打印全部记录」),与 buildRows 内的 expandAttempts 区分命名避免覆盖
|
||||
function expandAttemptsForPrint(rows, desc) {
|
||||
const out = [];
|
||||
rows.forEach(r => {
|
||||
const list = (r.attempts_list || []);
|
||||
if (!list.length) {
|
||||
const one = Object.assign({}, r);
|
||||
one.idx = 1; one.is_best = true;
|
||||
out.push(one);
|
||||
return;
|
||||
}
|
||||
list.forEach(a => {
|
||||
out.push({
|
||||
sid: r.sid, name: r.name, cls: r.cls, grp: r.grp,
|
||||
score: a.score, submit_ts: a.submit_ts, submit_at: a.submit_at,
|
||||
duration: a.duration, duration_txt: a.duration_txt,
|
||||
answer_id: a.answer_id, temp_save: a.temp_save,
|
||||
attempts: r.attempts, best_score: r.best_score,
|
||||
idx: a.idx, is_best: a.is_best,
|
||||
last_submit_ts: r.last_submit_ts, last_submit_at: r.last_submit_at,
|
||||
});
|
||||
});
|
||||
});
|
||||
out.sort((x, y) => {
|
||||
let d = (y.submit_ts || 0) - (x.submit_ts || 0);
|
||||
if (!desc) d = -d;
|
||||
if (d !== 0) return d;
|
||||
const a = String(x.sid || ''), b = String(y.sid || '');
|
||||
const na = /^\d+$/.test(a), nb = /^\d+$/.test(b);
|
||||
if (na && nb) return Number(a) - Number(b);
|
||||
return a.localeCompare(b, 'zh');
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function printTable() {
|
||||
const rows = (state._sorted || []).slice();
|
||||
const scope = $('#ptScope').value;
|
||||
let rows = (scope === 'all')
|
||||
? expandAttemptsForPrint(state._sorted || [], true)
|
||||
: (state._sorted || []).slice();
|
||||
if (!rows.length) return showAlert('当前没有可打印的成绩', 'warn');
|
||||
|
||||
const sortMode = $('#ptSort').value;
|
||||
@@ -1587,7 +1732,7 @@
|
||||
let body = '<h2>' + esc(title) + '</h2>' +
|
||||
(proj ? '<div class="sub">' + esc(proj) + '</div>' : '') +
|
||||
'<div class="meta">' + esc(printMetaLine([
|
||||
'共 ' + all.n + ' 人',
|
||||
(scope === 'all' ? '共 ' + all.n + ' 条记录' : '共 ' + all.n + ' 人'),
|
||||
'平均 ' + all.avg,
|
||||
'最高 ' + all.max + ' / 最低 ' + all.min,
|
||||
'合格 ' + all.pass + ' 人(' + all.passRate + '%)',
|
||||
@@ -1609,7 +1754,6 @@
|
||||
}
|
||||
|
||||
body += '<div class="sign">教师签字:____________ 教研组长:____________ 日期:____ 年 ____ 月 ____ 日</div>';
|
||||
body += '<div class="foot">' + esc(NY.appName || '') + ' · 本文档由系统生成,仅供教学参考</div>';
|
||||
|
||||
printDoc(title, body, { orient: orient });
|
||||
showAlert('已调起打印,如未弹出请在打印对话框中选择正确打印机', 'info');
|
||||
@@ -1619,7 +1763,7 @@
|
||||
if (!curPid()) return showAlert('请先选择问卷 / 考试');
|
||||
const n = (state._sorted || []).length;
|
||||
const a = (state._absentList || []).length;
|
||||
$('#ptHint').textContent = '当前将打印 ' + n + ' 条成绩' + (a ? '(可附带 ' + a + ' 名缺考学生)' : '');
|
||||
$('#ptHint').textContent = '当前可打印 ' + n + ' 名学生的成绩' + (a ? '(可附带 ' + a + ' 名缺考学生)' : '') + ';弹窗中可切换为「打印全部记录」';
|
||||
if (!$('#ptTitle').value.trim() && state.projectName) {
|
||||
$('#ptTitle').value = state.projectName + ' 成绩明细';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user