// ==UserScript== // @name 安徽中职学籍-批量打印学生基本信息表 // @namespace https://scriptcat.org/ // @version 1.2.2 // @description 在全国中等职业学校学生管理信息系统(安徽)批量打印学生基本信息表 // @author you // @match http://zzxsgl.ahjygl.gov.cn/* // @match https://zzxsgl.ahjygl.gov.cn/* // @match http://zzxsgl.ahjyt.gov.cn/* // @match https://zzxsgl.ahjyt.gov.cn/* // @grant GM_addStyle // @run-at document-idle // @run-in all-frames // ==/UserScript== (function () { 'use strict'; const PRINT_PATH = 'zxs/zzZxsJbxxAction!showDetail.action?zzZxsJbxxQB.id={id}&isPrint=0'; let listDoc = null; let listBaseUrl = location.href; function isListPageIn(doc) { if (!doc || !doc.body) return false; const hasNameHeader = [...doc.querySelectorAll('th, td')].some( (el) => el.textContent.trim() === '姓名' ); const hasStudentLink = [...doc.querySelectorAll('a')].some((a) => /xsqb\.id=/i.test(a.getAttribute('href') || '') ); return hasNameHeader && hasStudentLink; } function collectAccessibleDocs() { const docs = []; const seen = new Set(); const add = (doc) => { if (!doc || seen.has(doc)) return; seen.add(doc); docs.push(doc); }; add(document); try { const topDoc = window.top.document; add(topDoc); for (const frame of topDoc.querySelectorAll('iframe, frame')) { try { add(frame.contentDocument); } catch { /* 跨域 iframe 无法访问 */ } } } catch { /* 无法访问 top */ } for (const frame of document.querySelectorAll('iframe, frame')) { try { add(frame.contentDocument); } catch { /* ignore */ } } return docs; } function findListContext() { for (const doc of collectAccessibleDocs()) { if (isListPageIn(doc)) { let baseUrl = location.href; try { baseUrl = doc.defaultView?.location?.href || baseUrl; } catch { /* ignore */ } return { doc, baseUrl }; } } return null; } function getPanelRoot() { try { return window.top.document.body; } catch { return document.body; } } function getPanelDocument() { try { return window.top.document; } catch { return document; } } function injectStyles() { const pdoc = getPanelDocument(); if (pdoc.getElementById('bp-panel-style')) return; const style = pdoc.createElement('style'); style.id = 'bp-panel-style'; style.textContent = ` #bp-panel { position: fixed; right: 16px; bottom: 16px; z-index: 2147483647; width: 340px; background: #fff; border: 1px solid #dcdfe6; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,.15); font: 13px/1.5 "Microsoft YaHei", sans-serif; color: #303133; } #bp-panel .bp-head { padding: 10px 12px; background: #409eff; color: #fff; border-radius: 8px 8px 0 0; cursor: move; user-select: none; } #bp-panel .bp-body { padding: 12px; } #bp-panel label { display: block; margin-bottom: 8px; } #bp-panel input[type="number"] { width: 72px; margin-left: 6px; padding: 2px 6px; border: 1px solid #dcdfe6; border-radius: 4px; } #bp-panel .bp-btns { display: flex; gap: 8px; margin-top: 10px; } #bp-panel button { flex: 1; padding: 8px 0; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; } #bp-panel .bp-start { background: #409eff; color: #fff; } #bp-panel .bp-stop { background: #f56c6c; color: #fff; } #bp-panel .bp-start:disabled, #bp-panel .bp-stop:disabled { opacity: .55; cursor: not-allowed; } #bp-panel .bp-log { margin-top: 10px; max-height: 160px; overflow: auto; padding: 8px; background: #f5f7fa; border-radius: 4px; font-size: 12px; white-space: pre-wrap; word-break: break-all; } #bp-panel .bp-tip { margin-top: 8px; font-size: 11px; color: #909399; } `; pdoc.head.appendChild(style); } const state = { running: false, stopRequested: false }; function buildPrintUrl(xsqbId) { const rel = PRINT_PATH.replace('{id}', xsqbId); return new URL(rel, listBaseUrl).href; } function parseStudentLink(link) { const href = link.getAttribute('href') || ''; const m = href.match(/xsqb\.id=([A-F0-9]+)/i); if (!m) return null; return { xsqbId: m[1], name: link.textContent.trim() }; } function collectJobs() { if (!listDoc) return []; const jobs = []; const seen = new Set(); const checkedRows = [...listDoc.querySelectorAll('table tr')].filter((tr) => { const cb = tr.querySelector('input[type="checkbox"]'); return cb && cb.checked && !cb.disabled; }); const rows = checkedRows.length ? checkedRows : [...listDoc.querySelectorAll('table tbody tr, table tr')]; for (const row of rows) { const link = row.querySelector('a[href*="xsqb.id="]'); if (!link) continue; const info = parseStudentLink(link); if (!info || !info.name) continue; if (seen.has(info.xsqbId)) continue; seen.add(info.xsqbId); jobs.push({ name: info.name, xsqbId: info.xsqbId, printUrl: buildPrintUrl(info.xsqbId), }); } return jobs; } function log(msg) { const box = getPanelDocument().getElementById('bp-log'); if (!box) return; box.textContent += (box.textContent ? '\n' : '') + `[${new Date().toLocaleTimeString()}] ${msg}`; box.scrollTop = box.scrollHeight; } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function printInIframe(url) { return new Promise((resolve, reject) => { const root = getPanelRoot(); const iframe = getPanelDocument().createElement('iframe'); iframe.style.cssText = 'position:fixed;left:-9999px;top:0;width:900px;height:900px;border:0;'; let done = false; const finish = (err) => { if (done) return; done = true; clearTimeout(timer); setTimeout(() => iframe.remove(), 2000); err ? reject(err) : resolve(); }; const timer = setTimeout(() => finish(new Error('加载打印页超时')), 35000); iframe.onload = () => { try { const win = iframe.contentWindow; const doc = iframe.contentDocument; if (!win || !doc) { finish(new Error('无法访问打印页,请确认仍在本系统且已登录')); return; } const bodyText = doc.body?.innerText || ''; if (/登录|session|超时|错误/i.test(bodyText.slice(0, 300))) { finish(new Error('登录已失效,请刷新页面重新登录')); return; } setTimeout(() => { try { win.focus(); win.print(); finish(); } catch (e) { finish(e); } }, 1500); } catch (e) { finish(e); } }; iframe.onerror = () => finish(new Error('iframe 加载失败')); root.appendChild(iframe); iframe.src = url; }); } async function runBatch() { if (state.running) return; const ctx = findListContext(); if (ctx) { listDoc = ctx.doc; listBaseUrl = ctx.baseUrl; } const jobs = collectJobs(); state.stopRequested = false; state.running = true; updateButtons(); if (!jobs.length) { log('未找到学生。请先点「查询」出列表;勾选行则只打勾选的。'); state.running = false; updateButtons(); return; } const delay = Math.max(3000, Number(getPanelDocument().getElementById('bp-delay').value) || 6000); log(`共 ${jobs.length} 人,间隔 ${delay / 1000} 秒`); for (let i = 0; i < jobs.length; i++) { if (state.stopRequested) { log('已停止'); break; } const job = jobs[i]; log(`(${i + 1}/${jobs.length}) ${job.name}`); try { await printInIframe(job.printUrl); log(` ✓ 已唤起打印`); } catch (e) { log(` ✗ 失败:${e.message}`); } if (i < jobs.length - 1 && !state.stopRequested) await sleep(delay); } if (!state.stopRequested) log('全部完成'); state.running = false; updateButtons(); } function updateButtons() { const pdoc = getPanelDocument(); const start = pdoc.getElementById('bp-start'); const stop = pdoc.getElementById('bp-stop'); if (!start || !stop) return; start.disabled = state.running; stop.disabled = !state.running; } function createPanel() { const pdoc = getPanelDocument(); if (pdoc.getElementById('bp-panel')) return; injectStyles(); const panel = pdoc.createElement('div'); panel.id = 'bp-panel'; panel.innerHTML = `
--kiosk-printing