rollcall-classroom-assets 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/c-app.js +363 -8
- package/dm-app.js +36 -0
- package/package.json +14 -10
- package/zuoye-app.js +36 -0
package/c-app.js
CHANGED
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
user: null, classes: [], cls: '', date: today(), tab: 'roll',
|
|
32
32
|
roster: [], absent: {}, // 姓名 -> 'n'(缺) | 'l'(假);不在表里=到
|
|
33
33
|
subjects: [], subject: '', marks: {}, // 收缴:姓名 -> 'y'|'n'
|
|
34
|
+
notes: {}, // 收缴:姓名 -> 备注(写给 /api/submit 的 notes 字段)
|
|
35
|
+
q: '', // 收缴:搜索词(姓名/拼音/编号)
|
|
36
|
+
rollFrame: false, rollLite: false, // 点名:是否加载完整点名小程序 / 用户是否改回简易点名
|
|
34
37
|
qa: {} // 姓名 -> {ok:0, no:0}(本页累计,用于显示)
|
|
35
38
|
};
|
|
36
39
|
|
|
@@ -93,6 +96,108 @@
|
|
|
93
96
|
.catch(function () { var v = snapKey ? snap(snapKey) : null; return v ? Object.assign({ __offline: true }, v) : null; });
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
// ================= 拼音(按需加载)+ 学生编号 =================
|
|
100
|
+
// ⚠️ 编号算法必须与批改页(zuoye-app.js 的 buildCodes)**逐字一致** —— 作业本上的条码按那套印,
|
|
101
|
+
// 差一点就「扫了没反应/标错人」。拼音库 315KB 只在真正用到编号或拼音搜索时才拉(按需加载)。
|
|
102
|
+
var pyReady = false, pyLoading = null, pyCache = {}, codeMap = {};
|
|
103
|
+
var pyStore = lsGet('c_py', {}) || {};
|
|
104
|
+
function loadPinyin() {
|
|
105
|
+
if (window.pinyinPro) { pyReady = true; return Promise.resolve(true); }
|
|
106
|
+
if (pyLoading) return pyLoading;
|
|
107
|
+
pyLoading = new Promise(function (res) {
|
|
108
|
+
var s = document.createElement('script');
|
|
109
|
+
s.src = '/pinyin-pro.js'; s.async = true;
|
|
110
|
+
s.onload = function () {
|
|
111
|
+
pyReady = !!window.pinyinPro;
|
|
112
|
+
if (pyReady) { buildPy(S.roster); buildCodes(S.roster); paintRep(); }
|
|
113
|
+
res(pyReady);
|
|
114
|
+
};
|
|
115
|
+
s.onerror = function () { pyLoading = null; res(false); };
|
|
116
|
+
document.body.appendChild(s);
|
|
117
|
+
});
|
|
118
|
+
return pyLoading;
|
|
119
|
+
}
|
|
120
|
+
function pyForms(n) { // 与批改页 pyForms 逐字一致
|
|
121
|
+
var pp = window.pinyinPro; if (!pp) return null;
|
|
122
|
+
try {
|
|
123
|
+
var arr = pp.pinyin(n, { toneType: 'none', type: 'array', v: true });
|
|
124
|
+
var ini = pp.pinyin(n, { pattern: 'first', toneType: 'none', type: 'array' });
|
|
125
|
+
var full = (Array.isArray(arr) ? arr.join('') : '').toLowerCase();
|
|
126
|
+
var init = (Array.isArray(ini) ? ini.join('') : '').toLowerCase();
|
|
127
|
+
var flex = '';
|
|
128
|
+
if (Array.isArray(arr) && arr.length >= 2) {
|
|
129
|
+
var t = '';
|
|
130
|
+
for (var i = 0; i < arr.length - 1; i++) t += (ini[i] || String(arr[i])[0] || '');
|
|
131
|
+
flex = (t + String(arr[arr.length - 1])).toLowerCase();
|
|
132
|
+
}
|
|
133
|
+
return { full: full, init: init, flex: flex };
|
|
134
|
+
} catch (e) { return null; }
|
|
135
|
+
}
|
|
136
|
+
function buildPy(names) {
|
|
137
|
+
names.forEach(function (n) {
|
|
138
|
+
if (pyCache[n]) return;
|
|
139
|
+
if (pyStore[n]) { pyCache[n] = pyStore[n]; return; }
|
|
140
|
+
var f = pyForms(n); if (f) { pyCache[n] = f; pyStore[n] = f; }
|
|
141
|
+
});
|
|
142
|
+
try { localStorage.setItem('c_py', JSON.stringify(pyStore)); } catch (e) { }
|
|
143
|
+
// 「拼音就绪」判定:有真库,**或**本机缓存已覆盖全班每一个人(缓存来自上次会话,同样是真拼音)。
|
|
144
|
+
// 任何一个人拿不到真拼音就保持不就绪 —— 编号宁可空着,也绝不错位(错位比不编号更难查)。
|
|
145
|
+
if (!pyReady) {
|
|
146
|
+
pyReady = names.length > 0 && names.every(function (n) { var p = pyCache[n]; return !!(p && p.full && p.full !== n); });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function pad2s(s) { s = String(s == null ? '' : s); return s.length < 2 ? ('0' + s) : s; } // 老浏览器没有 padStart
|
|
150
|
+
function clsNumPrefix(cls) { // 班级名里最后一组数字的后两位('35' / '七年级35班' → '35')
|
|
151
|
+
var m = String(cls || '').match(/\d+/g);
|
|
152
|
+
if (!m) return '';
|
|
153
|
+
return pad2s(m[m.length - 1].slice(-2));
|
|
154
|
+
}
|
|
155
|
+
function sortKeyOf(n) { var p = pyCache[n]; return p ? p.full : n; }
|
|
156
|
+
function buildCodes(names) { // 与批改页 buildCodes 逐字一致:整班拼音就绪才算,宁可空也不错位
|
|
157
|
+
codeMap = {};
|
|
158
|
+
if (!names || !names.length) return;
|
|
159
|
+
var pfx = clsNumPrefix(S.cls);
|
|
160
|
+
if (!pfx || !pyReady) return;
|
|
161
|
+
var ordered = names.slice().sort(function (x, y) { var kx = sortKeyOf(x), ky = sortKeyOf(y); return kx < ky ? -1 : kx > ky ? 1 : 0; });
|
|
162
|
+
for (var i = 0; i < ordered.length; i++) { var p = pyCache[ordered[i]]; if (!p || p.full === ordered[i]) return; }
|
|
163
|
+
ordered.forEach(function (n, i) { codeMap[n] = pfx + pad2s(i + 1); });
|
|
164
|
+
}
|
|
165
|
+
function codeOf(n) { return codeMap[n] || ''; }
|
|
166
|
+
function codeHit(code, q, pfx) { // 编号命中:完整/前缀,或只输序号后两位(01 也能命中 3501)
|
|
167
|
+
if (!code) return false;
|
|
168
|
+
var s = String(q == null ? '' : q);
|
|
169
|
+
if (!s) return false;
|
|
170
|
+
if (code.indexOf(s) === 0) return true;
|
|
171
|
+
var p = String(pfx == null ? '' : pfx);
|
|
172
|
+
if (!p || code.length <= p.length) return false;
|
|
173
|
+
return code.slice(p.length).indexOf(pad2s(s)) === 0;
|
|
174
|
+
}
|
|
175
|
+
function nameHit(n, q) { // 搜索:姓名 / 全拼 / 首字母 / 编号
|
|
176
|
+
var ql = String(q || '').trim().toLowerCase();
|
|
177
|
+
if (!ql) return true;
|
|
178
|
+
if (/^\d+$/.test(ql)) return codeHit(codeOf(n), ql, clsNumPrefix(S.cls));
|
|
179
|
+
if (n.toLowerCase().indexOf(ql) >= 0) return true;
|
|
180
|
+
var p = pyCache[n];
|
|
181
|
+
if (p) return (p.full.indexOf(ql) >= 0) || (p.init.indexOf(ql) === 0) || (p.flex.indexOf(ql) >= 0);
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
function filtered() { return S.roster.filter(function (n) { return nameHit(n, S.q); }); }
|
|
185
|
+
// 扫码(编号)→ 标已交。找不到就返回 false(绝不猜人)
|
|
186
|
+
function applyScan(raw) {
|
|
187
|
+
var code = String(raw || '').trim();
|
|
188
|
+
if (!/^\d+$/.test(code)) return false;
|
|
189
|
+
var pfx = clsNumPrefix(S.cls), hit = '';
|
|
190
|
+
S.roster.forEach(function (n) {
|
|
191
|
+
if (!hit && codeOf(n) && codeHit(codeOf(n), code, pfx)) hit = n;
|
|
192
|
+
});
|
|
193
|
+
if (!hit) return false;
|
|
194
|
+
var was = S.marks[hit] || '';
|
|
195
|
+
S.marks[hit] = 'y';
|
|
196
|
+
repTouch();
|
|
197
|
+
toast('✅ ' + hit + '(' + code + ')已标已交' + (was === 'y' ? '(原本就是)' : ''));
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
96
201
|
// ================= 点名 =================
|
|
97
202
|
function paintRoll() {
|
|
98
203
|
var h = '';
|
|
@@ -139,24 +244,30 @@
|
|
|
139
244
|
|
|
140
245
|
// ================= 收缴 =================
|
|
141
246
|
function paintRep() {
|
|
247
|
+
var list = filtered();
|
|
142
248
|
var h = '';
|
|
143
|
-
|
|
144
|
-
var mk = S.marks[nm] || '';
|
|
249
|
+
list.forEach(function (nm) {
|
|
250
|
+
var mk = S.marks[nm] || '', note = S.notes[nm] || '', cd = codeOf(nm);
|
|
145
251
|
h += '<div class="row" data-mark="' + mk + '" data-nm="' + esc(nm) + '">' +
|
|
252
|
+
(cd ? '<span class="code" title="学生编号(扫码枪扫这个)">' + esc(cd) + '</span>' : '') +
|
|
146
253
|
'<span class="nm">' + esc(nm) + '</span>' +
|
|
254
|
+
(note ? '<span class="note" title="' + esc(note) + '">' + esc(note) + '</span>' : '') +
|
|
255
|
+
'<button class="btn sm" data-note="1" title="备注">📝</button>' +
|
|
147
256
|
'<button class="btn sm' + (mk === 'y' ? ' y' : '') + '" data-m="y">已交</button>' +
|
|
148
257
|
'<button class="btn sm' + (mk === 'n' ? ' n' : '') + '" data-m="n">未交</button>' +
|
|
149
258
|
'</div>';
|
|
150
259
|
});
|
|
151
|
-
$('repList').innerHTML = h || '<div class="hint"
|
|
260
|
+
$('repList').innerHTML = h || '<div class="hint">' + (S.roster.length ? ('没有匹配「' + esc(S.q) + '」的同学。') : '这个班还没有名单。') + '</div>';
|
|
152
261
|
var cy = 0, cn = 0;
|
|
153
262
|
Object.keys(S.marks).forEach(function (k) { if (S.marks[k] === 'y') cy++; else if (S.marks[k] === 'n') cn++; });
|
|
154
|
-
$('footNote').textContent = S.roster.length
|
|
263
|
+
$('footNote').textContent = S.roster.length
|
|
264
|
+
? ('已交 ' + cy + ' · 未交 ' + cn + ' · 未标记 ' + (S.roster.length - cy - cn) + (S.q ? ('(筛出 ' + list.length + ' 人)') : ''))
|
|
265
|
+
: '';
|
|
155
266
|
}
|
|
156
267
|
var repTimer = 0;
|
|
157
268
|
function saveRep(quiet) {
|
|
158
269
|
if (!S.cls || !S.subject) { if (!quiet) toast('请先选科目'); return; }
|
|
159
|
-
var body = { cls: S.cls, date: S.date, subject: S.subject, marks: S.marks, notes:
|
|
270
|
+
var body = { cls: S.cls, date: S.date, subject: S.subject, marks: S.marks, notes: S.notes, noPush: true };
|
|
160
271
|
fetch('/api/submit', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
|
161
272
|
.then(function (r) { return r.json().catch(function () { return {}; }); })
|
|
162
273
|
.then(function (d) {
|
|
@@ -221,7 +332,11 @@
|
|
|
221
332
|
sel.innerHTML = all.map(function (s) { return '<option' + (s === S.subject ? ' selected' : '') + '>' + esc(s) + '</option>'; }).join('');
|
|
222
333
|
var it = (d && d.ok && d.items || []).filter(function (x) { return x.subject === S.subject; })[0];
|
|
223
334
|
S.marks = (it && it.marks) ? Object.assign({}, it.marks) : {};
|
|
335
|
+
S.notes = (it && it.notes) ? Object.assign({}, it.notes) : {};
|
|
336
|
+
buildPy(S.roster); // 先用本机缓存的拼音算编号(零下载);没有缓存也不会去拉库
|
|
337
|
+
buildCodes(S.roster);
|
|
224
338
|
paintRep();
|
|
339
|
+
scanHint();
|
|
225
340
|
if (d && d.__offline) toast('📴 离线:用本机缓存的登记结果');
|
|
226
341
|
});
|
|
227
342
|
}
|
|
@@ -231,7 +346,7 @@
|
|
|
231
346
|
['roll', 'rep', 'track'].forEach(function (k) { $('pane-' + k).classList.toggle('on', k === t); });
|
|
232
347
|
$('foot').style.display = t === 'track' ? 'none' : 'flex';
|
|
233
348
|
$('allHere').textContent = t === 'rep' ? '全班已交' : '全部到齐';
|
|
234
|
-
if (t === 'roll')
|
|
349
|
+
if (t === 'roll') openRoll(); else if (t === 'rep') loadRep(); else loadTrack();
|
|
235
350
|
}
|
|
236
351
|
|
|
237
352
|
// ---------- 事件 ----------
|
|
@@ -243,7 +358,11 @@
|
|
|
243
358
|
getJSON('/api/submit?cls=' + encodeURIComponent(S.cls) + '&date=' + S.date, 'submit_' + S.cls + '_' + S.date).then(function (d) {
|
|
244
359
|
var it = (d && d.ok && d.items || []).filter(function (x) { return x.subject === S.subject; })[0];
|
|
245
360
|
S.marks = (it && it.marks) ? Object.assign({}, it.marks) : {};
|
|
361
|
+
S.notes = (it && it.notes) ? Object.assign({}, it.notes) : {};
|
|
362
|
+
buildPy(S.roster); // 先用本机缓存的拼音算编号(零下载);没有缓存也不会去拉库
|
|
363
|
+
buildCodes(S.roster);
|
|
246
364
|
paintRep();
|
|
365
|
+
scanHint();
|
|
247
366
|
});
|
|
248
367
|
});
|
|
249
368
|
Array.prototype.forEach.call(document.querySelectorAll('#tabs button'), function (b) { b.addEventListener('click', function () { showTab(b.dataset.t); }); });
|
|
@@ -257,8 +376,19 @@
|
|
|
257
376
|
});
|
|
258
377
|
$('repList').addEventListener('click', function (e) {
|
|
259
378
|
var btn = e.target.closest('button'); if (!btn) return;
|
|
260
|
-
var
|
|
261
|
-
|
|
379
|
+
var row = btn.closest('.row'); if (!row) return;
|
|
380
|
+
var nm = row.dataset.nm;
|
|
381
|
+
if (btn.dataset.note) { // 📝 备注(存进 /api/submit 的 notes 字段)
|
|
382
|
+
var cur = S.notes[nm] || '';
|
|
383
|
+
var v = window.prompt('给「' + nm + '」写备注(可留空清除)', cur);
|
|
384
|
+
if (v === null) return;
|
|
385
|
+
v = String(v).slice(0, 60);
|
|
386
|
+
if (v) S.notes[nm] = v; else delete S.notes[nm];
|
|
387
|
+
repTouch();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
var m = btn.dataset.m, curm = S.marks[nm] || '';
|
|
391
|
+
if (curm === m) delete S.marks[nm]; else S.marks[nm] = m; // 再点一次 = 取消
|
|
262
392
|
repTouch();
|
|
263
393
|
});
|
|
264
394
|
$('allHere').addEventListener('click', function () {
|
|
@@ -271,6 +401,195 @@
|
|
|
271
401
|
$('save').addEventListener('click', function () { if (S.tab === 'track') return loadTrack(); if (S.tab === 'rep') { clearTimeout(repTimer); saveRep(false); } else saveAbsent(); });
|
|
272
402
|
$('trackGo').addEventListener('click', loadTrack);
|
|
273
403
|
|
|
404
|
+
// ================= 点名:优先加载完整点名小程序(iframe + 免登录票) =================
|
|
405
|
+
// 为什么用 iframe:dm.html 有 108 个 id、自带整套样式,直接塞进本页会与教室页 id/CSS 打架;
|
|
406
|
+
// iframe 天然隔离。免登录靠跨域一次性兑换码:本页已登录 → POST /api/handoff 签票 →
|
|
407
|
+
// 把它当 iframe 的 src,dm 域核销后就是登录态(cookie 是 host-only,各域不通用,只能走这条)。
|
|
408
|
+
// 拿不到票 / 9 秒还没 load → 优雅退回原来的简易点名,绝不让老师卡在空白页。
|
|
409
|
+
function toLite() {
|
|
410
|
+
S.rollLite = true;
|
|
411
|
+
$('rollFrameWrap').style.display = 'none';
|
|
412
|
+
$('rollLiteWrap').style.display = '';
|
|
413
|
+
loadRoll();
|
|
414
|
+
}
|
|
415
|
+
function openRoll() {
|
|
416
|
+
if (S.rollLite) { toLite(); return; }
|
|
417
|
+
$('rollFrameWrap').style.display = '';
|
|
418
|
+
$('rollLiteWrap').style.display = 'none';
|
|
419
|
+
if (S.rollFrame) return; // 已载入过 → 直接显示(不重复签票)
|
|
420
|
+
var f = $('rollFrame'), done = false;
|
|
421
|
+
var fall = setTimeout(function () {
|
|
422
|
+
if (!done) { done = true; toast('点名小程序启动慢,先给你简易点名'); toLite(); }
|
|
423
|
+
}, 9000);
|
|
424
|
+
f.onload = function () { done = true; clearTimeout(fall); };
|
|
425
|
+
fetch('/api/handoff', {
|
|
426
|
+
method: 'POST', credentials: 'same-origin',
|
|
427
|
+
headers: { 'Content-Type': 'application/json' },
|
|
428
|
+
body: JSON.stringify({ to: 'dm', path: '/dm' })
|
|
429
|
+
})
|
|
430
|
+
.then(function (r) { return r.json(); })
|
|
431
|
+
.then(function (d) {
|
|
432
|
+
if (!d || !d.ok || !d.url) throw new Error('handoff');
|
|
433
|
+
S.rollFrame = true;
|
|
434
|
+
$('rollNewWin').href = d.url;
|
|
435
|
+
f.src = d.url;
|
|
436
|
+
})
|
|
437
|
+
.catch(function () {
|
|
438
|
+
if (done) return;
|
|
439
|
+
done = true; clearTimeout(fall);
|
|
440
|
+
toast('没能免登录打开点名小程序,已用简易点名');
|
|
441
|
+
toLite();
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ================= 收缴:搜索 / 扫码 / 备注 =================
|
|
446
|
+
function scanHint() {
|
|
447
|
+
var h = [];
|
|
448
|
+
if (!pyReady) h.push('编号与拼音搜索:首次用到时才加载拼音库(按需)');
|
|
449
|
+
else if (!Object.keys(codeMap).length) h.push('⚠ 这个班算不出编号(拼音或班级名不全),仍可用姓名搜索');
|
|
450
|
+
h.push('扫码枪:光标停在上面的搜索框,扫作业本条码(编号+回车)即标「已交」');
|
|
451
|
+
$('scanHint').textContent = h.join(' · ');
|
|
452
|
+
}
|
|
453
|
+
// —— 摄像头扫码:原生解码器优先(零下载),没有再按需拉 zxing(教室机是 Windows 桌面 Chrome,通常走后者)
|
|
454
|
+
var camOn = false, camStream = null, camTimer = 0, camCanvas = null, camLast = {};
|
|
455
|
+
var CAM_COOL = 1500;
|
|
456
|
+
function openScan() {
|
|
457
|
+
$('scan').classList.add('on');
|
|
458
|
+
$('scanTip').textContent = '正在准备…';
|
|
459
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
460
|
+
$('scanTip').textContent = '这个浏览器没有摄像头接口:可改用扫码枪(编号+回车),或换新版 Chrome';
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }).then(function (st) {
|
|
464
|
+
camStream = st; camOn = true;
|
|
465
|
+
var v = $('scanVideo');
|
|
466
|
+
v.srcObject = st;
|
|
467
|
+
try { var p = v.play(); if (p && p.catch) p.catch(function () { }); } catch (e) { }
|
|
468
|
+
startDecode();
|
|
469
|
+
}).catch(function (e) {
|
|
470
|
+
$('scanTip').textContent = '打不开摄像头(' + ((e && e.name) || '') + '):可改用扫码枪,或检查系统摄像头权限';
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
function closeScan() {
|
|
474
|
+
camOn = false; clearTimeout(camTimer);
|
|
475
|
+
try { if (camStream) camStream.getTracks().forEach(function (t) { t.stop(); }); } catch (e) { }
|
|
476
|
+
camStream = null;
|
|
477
|
+
try { $('scanVideo').srcObject = null; } catch (e) { }
|
|
478
|
+
$('scan').classList.remove('on');
|
|
479
|
+
}
|
|
480
|
+
function onCamCode(raw) {
|
|
481
|
+
var code = String(raw || '').trim();
|
|
482
|
+
if (!/^\d+$/.test(code)) return; // 只认纯数字编号(与扫码枪一致)
|
|
483
|
+
var now = Date.now();
|
|
484
|
+
if (now - (camLast[code] || 0) < CAM_COOL) return; // 同码冷却,避免连帧重复
|
|
485
|
+
camLast[code] = now;
|
|
486
|
+
if (applyScan(code)) {
|
|
487
|
+
try { if (navigator.vibrate) navigator.vibrate(60); } catch (e) { }
|
|
488
|
+
$('scanTip').textContent = '✅ 已标记,继续扫下一个…';
|
|
489
|
+
} else {
|
|
490
|
+
$('scanTip').textContent = '读到 ' + code + ',但不在本班编号里(要先在批改页打印过该班条码)';
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function startDecode() {
|
|
494
|
+
var v = $('scanVideo');
|
|
495
|
+
if (window.BarcodeDetector) { // ① 原生(安卓/macOS Chrome):零依赖
|
|
496
|
+
try {
|
|
497
|
+
var bd = new BarcodeDetector({ formats: ['code_128'] });
|
|
498
|
+
$('scanTip').textContent = '把作业本上的编号条码对准取景框…';
|
|
499
|
+
(function tick() {
|
|
500
|
+
if (!camOn) return;
|
|
501
|
+
if (v.videoWidth) {
|
|
502
|
+
try {
|
|
503
|
+
if (!camCanvas) camCanvas = document.createElement('canvas');
|
|
504
|
+
camCanvas.width = v.videoWidth; camCanvas.height = v.videoHeight;
|
|
505
|
+
camCanvas.getContext('2d').drawImage(v, 0, 0);
|
|
506
|
+
bd.detect(camCanvas).then(function (rs) {
|
|
507
|
+
(rs || []).forEach(function (r) { if (r && r.text) onCamCode(r.text); });
|
|
508
|
+
}).catch(function () { });
|
|
509
|
+
} catch (e) { }
|
|
510
|
+
}
|
|
511
|
+
camTimer = setTimeout(tick, 200);
|
|
512
|
+
})();
|
|
513
|
+
return;
|
|
514
|
+
} catch (e) { }
|
|
515
|
+
}
|
|
516
|
+
$('scanTip').textContent = '正在加载扫码引擎(首次约 0.9MB,之后走本机缓存)…';
|
|
517
|
+
loadZXing().then(function (Z) {
|
|
518
|
+
if (!camOn) return;
|
|
519
|
+
if (!Z) { $('scanTip').textContent = '扫码引擎加载失败:改用扫码枪(编号+回车)即可'; return; }
|
|
520
|
+
$('scanTip').textContent = '把作业本上的编号条码对准取景框…';
|
|
521
|
+
(function tick() {
|
|
522
|
+
if (!camOn) return;
|
|
523
|
+
if (v.videoWidth) {
|
|
524
|
+
try {
|
|
525
|
+
var sc = Math.min(1, 800 / v.videoWidth);
|
|
526
|
+
if (!camCanvas) camCanvas = document.createElement('canvas');
|
|
527
|
+
camCanvas.width = Math.max(8, Math.round(v.videoWidth * sc));
|
|
528
|
+
camCanvas.height = Math.max(8, Math.round(v.videoHeight * sc));
|
|
529
|
+
camCanvas.getContext('2d', { willReadFrequently: true }).drawImage(v, 0, 0, camCanvas.width, camCanvas.height);
|
|
530
|
+
Z.readBarcodes(camCanvas, { formats: zxFormats(Z), tryHarder: true }).then(function (rs) {
|
|
531
|
+
(rs || []).forEach(function (r) { if (r && r.text) onCamCode(r.text); });
|
|
532
|
+
}).catch(function () { });
|
|
533
|
+
} catch (e) { }
|
|
534
|
+
}
|
|
535
|
+
camTimer = setTimeout(tick, 140);
|
|
536
|
+
})();
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
var zxLoading = null;
|
|
540
|
+
function loadZXing() {
|
|
541
|
+
if (window.ZXingWASM && window.ZXingWASM.readBarcodes) return Promise.resolve(window.ZXingWASM);
|
|
542
|
+
if (zxLoading) return zxLoading;
|
|
543
|
+
try { if (typeof WebAssembly !== 'object') return Promise.resolve(null); } catch (e) { return Promise.resolve(null); }
|
|
544
|
+
zxLoading = new Promise(function (res) {
|
|
545
|
+
var fin = false;
|
|
546
|
+
function done(z) { if (fin) return; fin = true; res(z); }
|
|
547
|
+
var s = document.createElement('script');
|
|
548
|
+
s.src = '/zxing-wasm-reader.js'; s.async = true;
|
|
549
|
+
s.onload = function () { done((window.ZXingWASM && window.ZXingWASM.readBarcodes) ? window.ZXingWASM : null); };
|
|
550
|
+
s.onerror = function () { zxLoading = null; done(null); };
|
|
551
|
+
document.body.appendChild(s);
|
|
552
|
+
setTimeout(function () { done((window.ZXingWASM && window.ZXingWASM.readBarcodes) ? window.ZXingWASM : null); }, 12000);
|
|
553
|
+
});
|
|
554
|
+
return zxLoading;
|
|
555
|
+
}
|
|
556
|
+
function zxFormats(Z) {
|
|
557
|
+
try { var f = Z.barcodeFormats || Z.READABLE_BARCODE_FORMATS || {}; return [f.Code128 || f.CODE_128 || 'code_128']; }
|
|
558
|
+
catch (e) { return []; }
|
|
559
|
+
}
|
|
560
|
+
function ensureCodes(then) { // 编号就绪后才谈扫码定位;拼音库按需拉
|
|
561
|
+
return loadPinyin().then(function () {
|
|
562
|
+
buildPy(S.roster);
|
|
563
|
+
buildCodes(S.roster);
|
|
564
|
+
paintRep(); scanHint();
|
|
565
|
+
if (then) then();
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ---------- 收缴增强的事件 ----------
|
|
570
|
+
$('repScan').addEventListener('click', function () { ensureCodes(openScan); });
|
|
571
|
+
$('scanClose').addEventListener('click', closeScan);
|
|
572
|
+
$('repFind').addEventListener('input', function () {
|
|
573
|
+
var raw = this.value || '';
|
|
574
|
+
S.q = raw.trim();
|
|
575
|
+
if (/[a-zA-Z]/.test(raw)) ensureCodes(null); else { paintRep(); scanHint(); }
|
|
576
|
+
});
|
|
577
|
+
$('repFind').addEventListener('keydown', function (e) {
|
|
578
|
+
if (e.key !== 'Enter' && e.keyCode !== 13) return;
|
|
579
|
+
var v = (this.value || '').trim();
|
|
580
|
+
if (!/^\d+$/.test(v)) return; // 非纯数字 → 普通搜索,不当作扫码
|
|
581
|
+
e.preventDefault();
|
|
582
|
+
var input = this;
|
|
583
|
+
ensureCodes(function () {
|
|
584
|
+
if (!applyScan(v)) toast('编号 ' + v + ' 不在本班名单里');
|
|
585
|
+
input.value = ''; S.q = ''; paintRep(); scanHint();
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
$('repFindClr').addEventListener('click', function () {
|
|
589
|
+
$('repFind').value = ''; S.q = ''; paintRep(); scanHint(); $('repFind').focus();
|
|
590
|
+
});
|
|
591
|
+
$('rollLite').addEventListener('click', function (e) { e.preventDefault(); toLite(); });
|
|
592
|
+
|
|
274
593
|
// ---------- 启动 ----------
|
|
275
594
|
$('lb').addEventListener('click', function () {
|
|
276
595
|
var u = $('lu').value.trim(), p = $('lp').value;
|
|
@@ -319,3 +638,39 @@
|
|
|
319
638
|
}
|
|
320
639
|
boot();
|
|
321
640
|
})();
|
|
641
|
+
|
|
642
|
+
// === [NC-CRED-PATCH v24] 离线同步凭据:同源 JSON 变更请求自动带 {__u,__h} ===
|
|
643
|
+
// 服务端(resolveInlineCred)由同一次请求同时完成「验权 + 上传」—— 断网/弱网批改后补传时,
|
|
644
|
+
// 不再要求先有会话 cookie(会话可能早过期了)。另:任一变更成功后,顺手用本地存的 hash
|
|
645
|
+
// 换一次真会话(/api/login 的凭据升级通道),之后的 GET 也就不再 401。
|
|
646
|
+
(function () {
|
|
647
|
+
try {
|
|
648
|
+
var cred = null; try { cred = JSON.parse(localStorage.getItem('nc_cred') || 'null'); } catch (e) { }
|
|
649
|
+
if (!cred || !cred.u || !cred.h) return;
|
|
650
|
+
var upgraded = false;
|
|
651
|
+
function upgrade() {
|
|
652
|
+
if (upgraded) return; upgraded = true;
|
|
653
|
+
try {
|
|
654
|
+
fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: cred.u, password: cred.h }) }).catch(function () { });
|
|
655
|
+
} catch (e) { }
|
|
656
|
+
}
|
|
657
|
+
var of = window.fetch; if (!of) return;
|
|
658
|
+
window.fetch = function (input, init) {
|
|
659
|
+
try {
|
|
660
|
+
var m = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
|
661
|
+
var u = (typeof input === 'string') ? input : ((input && input.url) || '');
|
|
662
|
+
var isApi = (u.indexOf('/api/') === 0) || (u.indexOf(location.origin + '/api/') === 0);
|
|
663
|
+
if (m !== 'GET' && m !== 'HEAD' && isApi && init && typeof init.body === 'string') {
|
|
664
|
+
var b = null; try { b = JSON.parse(init.body); } catch (e) { }
|
|
665
|
+
if (b && typeof b === 'object') { b.__u = cred.u; b.__h = cred.h; init.body = JSON.stringify(b); }
|
|
666
|
+
}
|
|
667
|
+
} catch (e) { }
|
|
668
|
+
var p = of.apply(this, arguments);
|
|
669
|
+
try {
|
|
670
|
+
var m2 = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
|
671
|
+
if (m2 !== 'GET' && m2 !== 'HEAD') { p.then(function (r) { if (r && r.ok) upgrade(); }).catch(function () { }); }
|
|
672
|
+
} catch (e) { }
|
|
673
|
+
return p;
|
|
674
|
+
};
|
|
675
|
+
} catch (e) { }
|
|
676
|
+
})();
|
package/dm-app.js
CHANGED
|
@@ -1985,3 +1985,39 @@ if (window.self !== window.top) {
|
|
|
1985
1985
|
loadAbsent();
|
|
1986
1986
|
})();
|
|
1987
1987
|
// === [ABSENT-QUIZ 模块终点] ===
|
|
1988
|
+
|
|
1989
|
+
// === [NC-CRED-PATCH v24] 离线同步凭据:同源 JSON 变更请求自动带 {__u,__h} ===
|
|
1990
|
+
// 服务端(resolveInlineCred)由同一次请求同时完成「验权 + 上传」—— 断网/弱网批改后补传时,
|
|
1991
|
+
// 不再要求先有会话 cookie(会话可能早过期了)。另:任一变更成功后,顺手用本地存的 hash
|
|
1992
|
+
// 换一次真会话(/api/login 的凭据升级通道),之后的 GET 也就不再 401。
|
|
1993
|
+
(function () {
|
|
1994
|
+
try {
|
|
1995
|
+
var cred = null; try { cred = JSON.parse(localStorage.getItem('nc_cred') || 'null'); } catch (e) { }
|
|
1996
|
+
if (!cred || !cred.u || !cred.h) return;
|
|
1997
|
+
var upgraded = false;
|
|
1998
|
+
function upgrade() {
|
|
1999
|
+
if (upgraded) return; upgraded = true;
|
|
2000
|
+
try {
|
|
2001
|
+
fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: cred.u, password: cred.h }) }).catch(function () { });
|
|
2002
|
+
} catch (e) { }
|
|
2003
|
+
}
|
|
2004
|
+
var of = window.fetch; if (!of) return;
|
|
2005
|
+
window.fetch = function (input, init) {
|
|
2006
|
+
try {
|
|
2007
|
+
var m = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
|
2008
|
+
var u = (typeof input === 'string') ? input : ((input && input.url) || '');
|
|
2009
|
+
var isApi = (u.indexOf('/api/') === 0) || (u.indexOf(location.origin + '/api/') === 0);
|
|
2010
|
+
if (m !== 'GET' && m !== 'HEAD' && isApi && init && typeof init.body === 'string') {
|
|
2011
|
+
var b = null; try { b = JSON.parse(init.body); } catch (e) { }
|
|
2012
|
+
if (b && typeof b === 'object') { b.__u = cred.u; b.__h = cred.h; init.body = JSON.stringify(b); }
|
|
2013
|
+
}
|
|
2014
|
+
} catch (e) { }
|
|
2015
|
+
var p = of.apply(this, arguments);
|
|
2016
|
+
try {
|
|
2017
|
+
var m2 = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
|
2018
|
+
if (m2 !== 'GET' && m2 !== 'HEAD') { p.then(function (r) { if (r && r.ok) upgrade(); }).catch(function () { }); }
|
|
2019
|
+
} catch (e) { }
|
|
2020
|
+
return p;
|
|
2021
|
+
};
|
|
2022
|
+
} catch (e) { }
|
|
2023
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "rollcall-classroom-assets",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Static assets for the classroom workbench (roll-call): app JS, pinyin/xlsx/zxing engines and icons. Published only so they can be served from a China-friendly CDN.",
|
|
5
|
-
"keywords": [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "rollcall-classroom-assets",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Static assets for the classroom workbench (roll-call): app JS, pinyin/xlsx/zxing engines and icons. Published only so they can be served from a China-friendly CDN.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"assets",
|
|
7
|
+
"static",
|
|
8
|
+
"cdn"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"registry": "https://registry.npmjs.org/"
|
|
13
|
+
}
|
|
14
|
+
}
|
package/zuoye-app.js
CHANGED
|
@@ -9086,3 +9086,39 @@ skinInit();
|
|
|
9086
9086
|
close(true);
|
|
9087
9087
|
});
|
|
9088
9088
|
})();
|
|
9089
|
+
|
|
9090
|
+
// === [NC-CRED-PATCH v24] 离线同步凭据:同源 JSON 变更请求自动带 {__u,__h} ===
|
|
9091
|
+
// 服务端(resolveInlineCred)由同一次请求同时完成「验权 + 上传」—— 断网/弱网批改后补传时,
|
|
9092
|
+
// 不再要求先有会话 cookie(会话可能早过期了)。另:任一变更成功后,顺手用本地存的 hash
|
|
9093
|
+
// 换一次真会话(/api/login 的凭据升级通道),之后的 GET 也就不再 401。
|
|
9094
|
+
(function () {
|
|
9095
|
+
try {
|
|
9096
|
+
var cred = null; try { cred = JSON.parse(localStorage.getItem('nc_cred') || 'null'); } catch (e) { }
|
|
9097
|
+
if (!cred || !cred.u || !cred.h) return;
|
|
9098
|
+
var upgraded = false;
|
|
9099
|
+
function upgrade() {
|
|
9100
|
+
if (upgraded) return; upgraded = true;
|
|
9101
|
+
try {
|
|
9102
|
+
fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: cred.u, password: cred.h }) }).catch(function () { });
|
|
9103
|
+
} catch (e) { }
|
|
9104
|
+
}
|
|
9105
|
+
var of = window.fetch; if (!of) return;
|
|
9106
|
+
window.fetch = function (input, init) {
|
|
9107
|
+
try {
|
|
9108
|
+
var m = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
|
9109
|
+
var u = (typeof input === 'string') ? input : ((input && input.url) || '');
|
|
9110
|
+
var isApi = (u.indexOf('/api/') === 0) || (u.indexOf(location.origin + '/api/') === 0);
|
|
9111
|
+
if (m !== 'GET' && m !== 'HEAD' && isApi && init && typeof init.body === 'string') {
|
|
9112
|
+
var b = null; try { b = JSON.parse(init.body); } catch (e) { }
|
|
9113
|
+
if (b && typeof b === 'object') { b.__u = cred.u; b.__h = cred.h; init.body = JSON.stringify(b); }
|
|
9114
|
+
}
|
|
9115
|
+
} catch (e) { }
|
|
9116
|
+
var p = of.apply(this, arguments);
|
|
9117
|
+
try {
|
|
9118
|
+
var m2 = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
|
9119
|
+
if (m2 !== 'GET' && m2 !== 'HEAD') { p.then(function (r) { if (r && r.ok) upgrade(); }).catch(function () { }); }
|
|
9120
|
+
} catch (e) { }
|
|
9121
|
+
return p;
|
|
9122
|
+
};
|
|
9123
|
+
} catch (e) { }
|
|
9124
|
+
})();
|