完成报到确认页完善:
增加勾选、全选本页、批量设置状态和统一备注;应用后点击“暂存当前页”保存。 修复 Excel 文件选择事件被错误放在键盘监听中的问题。 导入后显示读取行数、实际更新人数、未变化人数及状态变化明细;重复导入会明确提示“没有产生变化”。 “拍摄二维码”改为调用 getUserMedia 实时相机,图片选择仅作为备用方式。 扫码后先展示考生确认页,默认选择“确认报到”,点击“暂存”后才修改数据。 后端新增扫码预览接口,预览过程不会提前修改报到状态。
This commit is contained in:
@@ -17,6 +17,9 @@ let toastTimer;
|
||||
let noticeEditor;
|
||||
let ckeditorModulePromise;
|
||||
let tableSearchTimer;
|
||||
let reportingCameraStream;
|
||||
let reportingCameraFrame;
|
||||
let reportingCameraToken = 0;
|
||||
function toast(title, message = '') {
|
||||
const element = document.querySelector('#toast');
|
||||
element.querySelector('strong').textContent = title;
|
||||
@@ -38,7 +41,16 @@ function setReviewSubpage(content) {
|
||||
setTimeout(() => modalRoot.querySelector('button,input,textarea,select')?.focus(), 30);
|
||||
}
|
||||
|
||||
function stopReportingCamera() {
|
||||
reportingCameraToken += 1;
|
||||
if (reportingCameraFrame) cancelAnimationFrame(reportingCameraFrame);
|
||||
reportingCameraFrame = null;
|
||||
reportingCameraStream?.getTracks().forEach(track => track.stop());
|
||||
reportingCameraStream = null;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
stopReportingCamera();
|
||||
const editor = noticeEditor;
|
||||
noticeEditor = null;
|
||||
if (editor) editor.destroy().catch(error => console.error('CKEditor cleanup failed', error));
|
||||
@@ -46,6 +58,61 @@ function closeModal() {
|
||||
modalRoot.innerHTML = '';
|
||||
}
|
||||
|
||||
function reportingScanConfirmation(preview, rawCode) {
|
||||
stopReportingCamera();
|
||||
const row = preview.row;
|
||||
setModal(`<div class="modal-head reporting-confirm-head"><div><span>NOTICE VERIFICATION</span><h2>确认考生报到</h2><p>二维码已通过防伪核验。请核对考生信息,选择结论后暂存。</p></div><button type="button" data-action="close-modal">×</button></div><form class="reporting-confirm-form" data-form="admission-reporting-scan-confirm"><input type="hidden" name="examId" value="${h(preview.examId)}"><input type="hidden" name="code" value="${h(rawCode)}"><section class="reporting-candidate-card"><div class="candidate-stamp">核验通过</div><dl><div><dt>考生姓名</dt><dd>${h(row.name)}</dd></div><div><dt>报名号</dt><dd class="mono">${h(row.candidateNumber)}</dd></div><div><dt>通知书编号</dt><dd class="mono">${h(row.noticeNumber)}</dd></div><div><dt>录取类别</dt><dd>${h(row.categoryName || '—')}</dd></div></dl></section><fieldset class="reporting-decision-options"><legend>报到确认结果</legend><label class="selected"><input type="radio" name="status" value="reported" checked><span><strong>确认报到</strong><small>默认选项,暂存为 Y · 已报到</small></span></label><label><input type="radio" name="status" value="not_reported"><span><strong>确认未报到</strong><small>暂存为 N · 未报到</small></span></label><label><input type="radio" name="status" value="pending"><span><strong>暂待确认</strong><small>暂存为 P · 待确认</small></span></label></fieldset><label class="reporting-confirm-note"><span>报到备注</span><input name="note" value="${h(row.note || '')}" placeholder="选填,系统会为扫码操作生成默认备注"></label><div class="modal-foot"><button type="button" class="ghost-button" data-action="open-reporting-camera" data-exam-id="${h(preview.examId)}">重新扫描</button><button type="submit" class="solid-button">暂存</button></div></form>`);
|
||||
}
|
||||
|
||||
async function previewReportingScan(code, examId) {
|
||||
const preview = await api('/api/admission/reporting/scan-preview', { method: 'POST', body: { code, examId } });
|
||||
reportingScanConfirmation(preview, code);
|
||||
return preview;
|
||||
}
|
||||
|
||||
async function openReportingCamera(examId) {
|
||||
stopReportingCamera();
|
||||
const token = reportingCameraToken;
|
||||
setModal(`<div class="modal-head reporting-camera-head"><div><span>LIVE CAMERA</span><h2>扫描录取通知书二维码</h2><p>请将二维码放入取景框,识别后会进入报到确认页,不会直接保存。</p></div><button type="button" data-action="close-modal">×</button></div><section class="reporting-camera-stage"><video data-reporting-camera autoplay muted playsinline></video><div class="scan-frame"><i></i><i></i><i></i><i></i><span>正在等待二维码进入画面</span></div></section><p class="reporting-camera-status" data-reporting-camera-status>正在请求相机权限…</p><div class="reporting-camera-fallback"><form data-form="admission-reporting-scan-preview"><input type="hidden" name="examId" value="${h(examId)}"><input name="code" placeholder="无法使用相机时,可粘贴二维码链接或 AN 防伪码" required><button class="ghost-button" type="submit">核验</button></form><label class="qr-capture">选择二维码图片<input type="file" accept="image/*" data-reporting-qr-file data-exam-id="${h(examId)}" hidden></label></div>`);
|
||||
const video = modalRoot.querySelector('[data-reporting-camera]');
|
||||
const status = modalRoot.querySelector('[data-reporting-camera-status]');
|
||||
try {
|
||||
if (!navigator.mediaDevices?.getUserMedia) throw new Error('当前浏览器未提供相机访问能力');
|
||||
reportingCameraStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: 'environment' } }, audio: false });
|
||||
if (token !== reportingCameraToken) { reportingCameraStream.getTracks().forEach(track => track.stop()); return; }
|
||||
video.srcObject = reportingCameraStream;
|
||||
await video.play();
|
||||
if (!('BarcodeDetector' in window)) throw new Error('相机已打开,但当前浏览器不支持自动识别二维码,请使用下方防伪码核验');
|
||||
const detector = new BarcodeDetector({ formats: ['qr_code'] });
|
||||
status.textContent = '相机已开启,请将二维码对准取景框';
|
||||
let detecting = false;
|
||||
const scan = async () => {
|
||||
if (token !== reportingCameraToken || !reportingCameraStream) return;
|
||||
if (!detecting && video.readyState >= 2) {
|
||||
detecting = true;
|
||||
try {
|
||||
const codes = await detector.detect(video);
|
||||
const code = codes[0]?.rawValue;
|
||||
if (code) {
|
||||
status.textContent = '已识别二维码,正在核验…';
|
||||
await previewReportingScan(code, examId);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
if (token !== reportingCameraToken) return;
|
||||
status.textContent = error.message || '二维码核验失败,请重新对准取景框';
|
||||
} finally { detecting = false; }
|
||||
}
|
||||
reportingCameraFrame = requestAnimationFrame(scan);
|
||||
};
|
||||
scan();
|
||||
} catch (error) {
|
||||
if (token !== reportingCameraToken) return;
|
||||
status.textContent = `无法使用相机:${error.message}。可检查权限,或使用下方备用方式。`;
|
||||
status.classList.add('error');
|
||||
}
|
||||
}
|
||||
|
||||
function loadCKEditor() {
|
||||
if (!document.querySelector('link[data-ckeditor-styles]')) {
|
||||
const stylesheet = document.createElement('link');
|
||||
@@ -409,6 +476,25 @@ document.addEventListener('click', async event => {
|
||||
window.location.href = `/api/admission/reporting/export?examId=${encodeURIComponent(target.dataset.examId)}`;
|
||||
return;
|
||||
}
|
||||
if (action === 'open-reporting-camera') {
|
||||
await openReportingCamera(target.dataset.examId);
|
||||
return;
|
||||
}
|
||||
if (action === 'bulk-reporting-apply') {
|
||||
const toolbar = target.closest('[data-reporting-bulk]');
|
||||
const form = toolbar?.closest('form[data-form="admission-reporting-draft"]');
|
||||
const table = document.getElementById(toolbar?.dataset.tableId || '');
|
||||
const selected = [...(table?.querySelectorAll('[data-reporting-select]:checked') || [])];
|
||||
if (!selected.length) return toast('请先选择考生', '可勾选单人或全选当前页');
|
||||
const status = toolbar.querySelector('[data-reporting-bulk-status]').value;
|
||||
const note = toolbar.querySelector('[data-reporting-bulk-note]').value.trim();
|
||||
selected.forEach(input => {
|
||||
form.querySelector(`[data-reporting-status][data-placement-id="${CSS.escape(input.value)}"]`).value = status;
|
||||
if (note) form.querySelector(`[data-reporting-note][data-placement-id="${CSS.escape(input.value)}"]`).value = note;
|
||||
});
|
||||
toast('批量修改已应用', `已修改 ${selected.length} 名考生,请点击“暂存当前页”保存`);
|
||||
return;
|
||||
}
|
||||
if (action === 'submit-admission-reporting') {
|
||||
if (!window.confirm('确认提交本轮全部报到情况?提交后需先完成补录决定,才能交由超级管理员审批。')) return;
|
||||
await api('/api/admission/reporting/submit', { method: 'POST', body: { examId: target.dataset.examId } });
|
||||
@@ -791,6 +877,25 @@ document.addEventListener('change', event => {
|
||||
});
|
||||
updatePlacementSelection();
|
||||
}
|
||||
if (event.target.matches('[data-reporting-select]')) {
|
||||
const toolbar = event.target.closest('form')?.querySelector('[data-reporting-bulk]');
|
||||
const table = event.target.closest('table');
|
||||
const selected = table ? table.querySelectorAll('[data-reporting-select]:checked').length : 0;
|
||||
const total = table ? table.querySelectorAll('[data-reporting-select]').length : 0;
|
||||
const count = toolbar?.querySelector('[data-reporting-selected-count]');
|
||||
const selectAll = toolbar?.querySelector('[data-reporting-select-all]');
|
||||
if (count) count.textContent = `已选 ${selected} 人`;
|
||||
if (selectAll) { selectAll.checked = total > 0 && selected === total; selectAll.indeterminate = selected > 0 && selected < total; }
|
||||
}
|
||||
if (event.target.matches('[data-reporting-select-all]')) {
|
||||
const table = document.getElementById(event.target.dataset.tableId);
|
||||
table?.querySelectorAll('[data-reporting-select]').forEach(input => { input.checked = event.target.checked; });
|
||||
const count = event.target.closest('[data-reporting-bulk]')?.querySelector('[data-reporting-selected-count]');
|
||||
if (count) count.textContent = `已选 ${event.target.checked ? table?.querySelectorAll('[data-reporting-select]').length || 0 : 0} 人`;
|
||||
}
|
||||
if (event.target.matches('.reporting-decision-options input[type="radio"]')) {
|
||||
event.target.closest('fieldset').querySelectorAll('label').forEach(label => label.classList.toggle('selected', label.contains(event.target)));
|
||||
}
|
||||
if (event.target.matches('[data-action="result-bulk-exam"]')) {
|
||||
state.resultExamFilter = event.target.value;
|
||||
state.resultSubjectFilter = '';
|
||||
@@ -1047,10 +1152,15 @@ document.addEventListener('submit', async event => {
|
||||
if (!rows.length) return toast('当前页没有可暂存记录');
|
||||
await api('/api/admission/reporting/draft', { method: 'PUT', body: { examId: form.dataset.examId, rows } });
|
||||
toast('当前页已暂存', `${rows.length} 名考生的状态已保存,尚未正式提交`); renderRoute();
|
||||
} else if (kind === 'admission-reporting-scan') {
|
||||
} else if (kind === 'admission-reporting-scan-preview') {
|
||||
const body = formObject(form);
|
||||
await previewReportingScan(body.code, body.examId);
|
||||
} else if (kind === 'admission-reporting-scan-confirm') {
|
||||
const body = formObject(form);
|
||||
const result = await api('/api/admission/reporting/scan', { method: 'POST', body });
|
||||
toast('防伪二维码核验通过', `${result.row.name}已暂存为“已报到”`); renderRoute();
|
||||
closeModal();
|
||||
const label = body.status === 'reported' ? '已报到' : body.status === 'not_reported' ? '未报到' : '待确认';
|
||||
toast('扫码结果已暂存', `${result.row.name} · ${label},尚未正式提交`); renderRoute();
|
||||
} else if (kind === 'admission-reporting-decision') {
|
||||
const body = formObject(form);
|
||||
body.supplement = body.supplement === 'true';
|
||||
@@ -1464,6 +1574,9 @@ window.addEventListener('keydown', event => {
|
||||
form.requestSubmit(form.querySelector('[data-result-mode="draft"]'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('change', event => {
|
||||
if (event.target.matches('[data-admission-reporting-file]')) {
|
||||
const input = event.target;
|
||||
const file = input.files?.[0];
|
||||
@@ -1474,8 +1587,9 @@ window.addEventListener('keydown', event => {
|
||||
try {
|
||||
toast('正在导入报到状态', `${file.name} · 导入结果只会暂存`);
|
||||
const result = await api(`/api/admission/reporting/import?examId=${encodeURIComponent(examId)}`, { method: 'POST', headers: { 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, body: await file.arrayBuffer() });
|
||||
toast('Excel 已导入并暂存', `已更新 ${result.count} 名考生,提交前仍可修改`);
|
||||
renderRoute();
|
||||
state.reportingImportSummaries[examId] = result;
|
||||
toast(result.changedCount ? 'Excel 已导入并暂存' : 'Excel 已读取,状态没有变化', result.changedCount ? `读取 ${result.count} 行,实际更新 ${result.changedCount} 人,${result.unchangedCount} 人未变化` : `读取 ${result.count} 行,内容与当前暂存状态一致`);
|
||||
await renderRoute();
|
||||
} catch (error) { toast('Excel 导入失败', error.message); }
|
||||
})();
|
||||
}
|
||||
@@ -1491,9 +1605,7 @@ window.addEventListener('keydown', event => {
|
||||
const codes = await detector.detect(file);
|
||||
const code = codes[0]?.rawValue || '';
|
||||
if (!code) throw new Error('图片中没有识别到二维码');
|
||||
const result = await api('/api/admission/reporting/scan', { method: 'POST', body: { code } });
|
||||
toast('二维码核验通过', `${result.row.name}已暂存为“已报到”`);
|
||||
renderRoute();
|
||||
await previewReportingScan(code, input.dataset.examId || '');
|
||||
} catch (error) { toast('二维码识别失败', error.message); }
|
||||
})();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user