// ==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();
})();