// ==UserScript== // @name 南昌市义务教育智慧入学数字平台批量双确认脚本【Playmaker-Decodetalker】 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 导入文件只需要新建一个Excel文件,粘贴两列信息,一列包含采集号(或身份证件号),一列包含验证码,程序会自行跳过第一行(标题)。仅供各位招生教师使用,注意学生信息保密。谢谢!|更新时间:2026-08-22 // @author User // @match https://xwyjc.nceea.cn:81/* // @match https://xwyjc.nceea.cn/* // @require https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js // @grant GM_addStyle // @run-at document-idle // @license GPL-3.0 // ==/UserScript== (function() { 'use strict'; const sleep = ms => new Promise(res => setTimeout(res, ms)); // ==========顶层主页面直接退出,子iframe才执行========== if (window.top === window.self) { return; } let studentArr = []; let running = false; let stopFlag = false; let triggerBtn = null; let panel = null; const cssText = ` #start-trigger-btn { position: fixed; top: 15px; right: 15px; z-index: 999999; width: 110px; height: 42px; border-radius: 8px; background: #409eff; color: #fff; border: none; font-size:14px; cursor: pointer; box-shadow: 0 2px 12px rgba(64,158,255,0.3); } #batch-admit-box { position: fixed; top: 65px; right: 15px; z-index: 999999; width: 360px; background: #ffffff; border: 1px solid #e4e7ed; border-radius: 12px; padding: 16px; box-shadow: 0 4px 20px rgba(0,0,0,0.12); display:none; } .panel-header{display:flex;justify-content: space-between;align-items:center;margin-bottom:12px;} .panel-header h4{margin:0;font-size:15px;color:#303133;} .close-btn{width:26px;height:26px;border-radius:50%;border:none;background:#f5f7fa;color:#606266;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;} .close-btn:hover{background:#e4e7ed;} #batch-log {height: 220px;overflow-y: auto;border: 1px solid #ebeef5;padding: 8px;font-size: 12px;margin: 10px 0;background: #fafafa;border-radius:6px;} .log-succ { color: #009933; } .log-fail { color: #e53935; } .log-info { color: #1976d2; } .btn-group{display:flex;gap:8px;margin:8px 0;} .btn-group-row2{display:flex;gap:8px;margin-top:8px;} .ctrl-btn {flex:1;padding:6px 0;border-radius:6px;border:none;cursor: pointer;font-size:13px;} #taskBegin{background:#409eff;color:#fff;} #taskBegin:disabled{background:#a0cfff;cursor:not-allowed;} #taskStop{background:#f56c6c;color:#fff;} #fullShutdown{background:#666;color:#fff;} input[type="file"] {width:100%;font-size:13px;} .progress-row{font-size:13px;color:#606266;} `; /** * 清理layui-layer弹窗以及内部iframe残留DOM * 【重点新增函数】:删除所有layui生成弹窗容器,释放内存,防止DOM堆积 */ function clearLayerResidueDom(){ // 注意:弹窗元素存在于父页面 window.parent.document const pDoc = window.parent.document; const layerList = pDoc.querySelectorAll('.layui-layer'); layerList.forEach(layerDom=>{ try{ layerDom.remove(); }catch(e){} }); } /** * 判断当前iframe是否处于layui-layer弹窗内 * 点击"确认录取"后弹出的验证码弹窗也是iframe,且URL匹配@match, * 脚本会在其中重复执行,需在此类弹窗内跳过UI生成。 */ function isInLayuiLayerIframe(){ try{ const frameEl = window.frameElement; if(!frameEl) return false; let node = frameEl.parentElement; while(node){ if(node.classList && node.classList.contains('layui-layer')){ return true; } node = node.parentElement; } return false; }catch(e){ return false; } } /** 清理脚本自身旧DOM,避免ID重复 */ function cleanOldDom(){ const oldBtn = document.querySelector('#start-trigger-btn'); const oldBox = document.querySelector('#batch-admit-box'); if(oldBtn) oldBtn.remove(); if(oldBox) oldBox.remove(); } // 页面刷新/关闭:停止任务,清理DOM残留 window.addEventListener('beforeunload', ()=>{ stopFlag = true; running = false; try{ cleanOldDom(); clearLayerResidueDom(); }catch(e){} }); async function delayedInitUI(){ // layui弹窗内的iframe不生成批量录取按钮,避免重复UI if(isInLayuiLayerIframe()) return; cleanOldDom(); while(!document.body){ await sleep(100); } await sleep(700); GM_addStyle(cssText); triggerBtn = document.createElement('button'); triggerBtn.id = 'start-trigger-btn'; triggerBtn.innerText = '批量录取'; document.body.appendChild(triggerBtn); panel = document.createElement('div'); panel.id = 'batch-admit-box'; panel.innerHTML = `

批量录取操作面板

进度:0/0
`; document.body.appendChild(panel); const logDom = document.querySelector('#batch-log'); const progressDom = document.querySelector('#progressText'); const fileInput = document.querySelector('#excelUpload'); const startBtn = document.querySelector('#taskBegin'); const stopBtn = document.querySelector('#taskStop'); const closeBtn = document.querySelector('#panelClose'); const shutdownBtn = document.querySelector('#fullShutdown'); function printLog(text, cls = "log-info") { const line = document.createElement('div'); line.className = cls; line.innerText = `[${new Date().toLocaleTimeString()}] ${text}`; logDom.appendChild(line); logDom.scrollTop = logDom.scrollHeight; } closeBtn.onclick = () => { panel.style.display = 'none'; printLog("面板已隐藏,如需完全释放请点击【彻底关闭工具】"); }; shutdownBtn.onclick = () => { stopFlag = true; running = false; studentArr = []; startBtn.disabled = true; logDom.innerHTML = ""; progressDom.textContent = "0/0"; panel.style.display = 'none'; // ==========彻底关闭工具:执行全局清理弹窗iframe残留 ========== clearLayerResidueDom(); printLog("✅所有程序已停止,已清理layui弹窗iframe残留,工具休眠", "log-succ"); }; triggerBtn.onclick = function() { panel.style.display = 'block'; //打印脚本版本与更新时间 const scriptMeta = GM_info.script; printLog(`脚本版本:${scriptMeta.version},上次修改时间:${new Date(scriptMeta.lastModified).toLocaleString()}`,"log-info"); printLog("✅批量工具已激活,上传含学生采集号/身份证件号及验证码的Excel的文件后,点击开始执行以启动批量录取双确认","log-info"); }; async function waitElement(selectorFn, timeout = 12000) { const start = Date.now(); while (Date.now() - start < timeout) { if(stopFlag) return null; const el = selectorFn(document); if (el) return el; await sleep(200); } return null; } function findCodeInput(doc) { let input = doc.querySelector('input[name="AdmissionConfirmCode"]'); if(input) return input; input = doc.querySelector('.layui-input[name="AdmissionConfirmCode"]'); return input; } function setLayuiInput(element, value) { element.focus(); element.value = ""; element.dispatchEvent(new Event('focus')); element.value = value; element.dispatchEvent(new Event('input')); element.dispatchEvent(new Event('change')); } fileInput.onchange = async function(e) { const file = e.target.files[0]; if (!file) return; printLog(`正在解析文件:${file.name}`); const buf = await file.arrayBuffer(); const workbook = XLSX.read(buf, { type: "array" }); const sheet = workbook.Sheets[workbook.SheetNames[0]]; const rawData = XLSX.utils.sheet_to_json(sheet, { header: 1 }); studentArr = []; for (let i = 1; i < rawData.length; i++) { const row = rawData[i]; const name = String(row[0] ?? "").trim(); const code = String(row[1] ?? "").trim(); if (!name || !code) continue; studentArr.push({ name, code }); } printLog(`✅读取完成,有效学生数据:${studentArr.length} 条`, "log-succ"); printLog(`点击开始执行以启动批量录取双确认`, "log-info"); progressDom.textContent = `0/${studentArr.length}`; startBtn.disabled = studentArr.length === 0; }; stopBtn.onclick = () => { stopFlag = true; printLog("⚠️已下达停止指令,当前学生处理完毕终止", "log-fail"); }; async function processOneStudent(item) { if(stopFlag) return false; const { name, code } = item; printLog(`------ 处理【${name}】 ------`); const searchInput = await waitElement(doc => { let i = doc.querySelector('input[placeholder*="姓名"]'); if(i) return i; return doc.querySelector('.layui-input'); }, 12000); const searchBtn = await waitElement(doc => { const btns = doc.querySelectorAll('button'); for(let b of btns) if(b.textContent.includes("搜索")) return b; return null; }, 10000); if (!searchInput || !searchBtn) { printLog(`❌【${name}】找不到搜索框/搜索按钮`, "log-fail"); return false; } printLog(`✅定位搜索框,填入姓名`); searchInput.value = ""; searchInput.dispatchEvent(new Event("input")); await sleep(200); searchInput.value = name; searchInput.dispatchEvent(new Event("input")); await sleep(200); searchBtn.click(); printLog(`执行搜索,等待表格加载`); await sleep(1200); const confirmLink = await waitElement(doc => { const rows = doc.querySelectorAll("tr"); for (const tr of rows) { const tds = tr.querySelectorAll("td"); let matchName = null; for(const td of tds){ if(td.innerText.trim() === name){ matchName = name; break; } } if(matchName === name){ return tr.querySelector('a[lay-event="confrimadmission"]'); } } return null; }, 3500); if (!confirmLink) { printLog(`ℹ️【${name}】无确认录取按钮,自动跳过该学生`, "log-info"); return true; } printLog(`✅找到确认录取按钮,点击弹窗`); confirmLink.click(); await sleep(400); const iframe = await waitElement(doc => doc.querySelector('.layui-layer-iframe iframe'), 4000); if (!iframe) { printLog(`❌【${name}】未找到弹窗iframe`, "log-fail"); // 找不到iframe也要清理残留弹窗DOM clearLayerResidueDom(); return false; } printLog(`✅检测到弹窗iframe,开始轮询iframe内部DOM`); let verifyInput = null; let submitBtn = null; const maxWaitMs = 4000; const startStamp = Date.now(); while (Date.now() - startStamp < maxWaitMs) { if(stopFlag) return false; let iframeDoc; try { iframeDoc = iframe.contentDocument || iframe.contentWindow.document; } catch (e) { await sleep(100); continue; } verifyInput = findCodeInput(iframeDoc); submitBtn = iframeDoc.querySelector('button[lay-submit][lay-filter="submit"]'); if (verifyInput && submitBtn) { break; } await sleep(100); } if (!verifyInput) { printLog(`❌【${name}】4秒内iframe内部未找到验证码输入框`, "log-fail"); const closeBtnDom = window.parent.document.querySelector('.layui-layer-close'); if (closeBtnDom) closeBtnDom.click(); clearLayerResidueDom(); // 失败也清理弹窗DOM return false; } if (!submitBtn) { printLog(`❌【${name}】iframe内部未找到提交按钮`, "log-fail"); const closeBtnDom = window.parent.document.querySelector('.layui-layer-close'); if (closeBtnDom) closeBtnDom.click(); clearLayerResidueDom(); // 失败也清理弹窗DOM return false; } printLog(`✅iframe内成功定位验证码输入框`); setLayuiInput(verifyInput, code); printLog(`验证码【${code}】填写完成`); await sleep(200); submitBtn.click(); printLog(`✅【${name}】提交录取请求`); await sleep(300); const closeBtnDom = await waitElement(doc => doc.querySelector('.layui-layer-close'), 3000); if(closeBtnDom){ closeBtnDom.click(); printLog(`已自动关闭验证码弹窗`); await sleep(200); }else{ printLog(`未检测到弹窗关闭按钮`); } // =========【新增】单个学生处理完毕,强制清理layui弹窗iframe残留DOM ========= clearLayerResidueDom(); printLog(`已清理当前学生弹窗iframe残留DOM`); return true; } startBtn.onclick = async function() { if (running || studentArr.length === 0) return; running = true; stopFlag = false; startBtn.disabled = true; printLog("========批量任务启动========", "log-info"); for (let i = 0; i < studentArr.length; i++) { if (stopFlag) { printLog("========任务手动终止========", "log-fail"); break; } await processOneStudent(studentArr[i]); progressDom.textContent = `${i+1}/${studentArr.length}`; await sleep(800); } running = false; startBtn.disabled = false; // 全部任务跑完,整体清理一遍残留弹窗 clearLayerResidueDom(); if (!stopFlag) printLog("========全部处理完毕,已清理弹窗残留DOM========", "log-succ"); }; } delayedInitUI(); })();