// ==UserScript== // @name 学生档案批量打印(安徽中职 zzxsgl) // @namespace local.batch-print-students // @version 1.2.0 // @description 在校生档案:列表→详情→打印预览→打印→回列表(zzxsgl.ahjygl.gov.cn)·支持 iframe 内表格 // @author you // @match http://zzxsgl.ahjygl.gov.cn/* // @match https://zzxsgl.ahjygl.gov.cn/* // @grant GM_setValue // @grant GM_getValue // @run-at document-idle // ==/UserScript== /** * 站点:安徽省全国中等职业学校学生管理信息系统 * * URL 示意(你已提供): * - 列表/查询:…/studInfoManage!… (含「查询结果」表格,非 goStuDetail) * - 详情: …/studInfoManage!goStuDetail.action?xsqb.id=… * - 预览/打印 …/zzZxsJbxxAction!showDetail.action?…&isPrint=0 (页面按钮为「打印」) * * 脚本猫请选「脚本」。本系统常在 iframe 内嵌「查询结果」表;1.2.0 起已移除 @noframes 以便在子框架内生效。 */ (function () { 'use strict'; /** @param {string} u */ function ahIsPrintPreviewUrl(u) { return ( /zzZxsJbxxAction!\s*showDetail\.action/i.test(u) && /[?&]isPrint=0(?:&|$)/i.test(u) ); } /** @param {string} u */ function ahIsDetailUrl(u) { return /studInfoManage!\s*goStuDetail\.action/i.test(u); } // ==================== 配置区 ==================== const CONFIG = { /** 首选:姓名列链接;还会在 collectStudentLinks 里自动叠加多套备用选择器 */ listStudentLinkSelector: '.grid-table tbody tr td:nth-child(2) a', /** 可自行追加列表姓名链选择器,如 F12 Copy selector 得到的唯一路径(字符串数组) */ extraListSelectors: [], isListPage() { const u = location.href; if (ahIsDetailUrl(u)) return false; if (/zzZxsJbxxAction!\s*showDetail\.action/i.test(u)) return false; if (/studInfoManage/i.test(u) && !/goStuDetail/i.test(u)) return true; const bodyTxt = document.body ? document.body.innerText || '' : ''; const looksLikeStudentGrid = /查询结果/.test(bodyTxt) || (/学籍号/.test(bodyTxt) && /姓名/.test(bodyTxt)); return looksLikeStudentGrid && collectStudentLinks().length > 0; }, /** 详情(基本信息页,右上角为「打印预览」) */ isDetailPage() { return ahIsDetailUrl(location.href); }, /** zzZxs 打印预览(右上角为「打印」) */ isPrintPreviewPage() { return ahIsPrintPreviewUrl(location.href); }, /** 详情页「打印预览」,留空则用文案查找,仅匹配「打印预览」,避免点在预览页的「打印」 */ detailPrintPreviewSelectorCss: '', /** * 预览页右上角「打印」——留空则用文案查找(节点文本规整后等于「打印」) */ printPagePrintButtonSelectorCss: '', /** 预览页少见的多余入口 */ printPageExtraPrintSelector: '', wait: { domReady: 10000, afterNavigation: 1200, afterClickPreview: 1500, printDialogDelay: 450, betweenStudents: 700, popupPoll: 200, }, /** 若在预览页未找到「打印」按钮,改为直接 window.print */ autoPrintOnPreviewPage: true, closePreviewWindowAfterPrint: true, /** * 本系统通常为:点击「打印预览」后同一标签进入 zzZxsJbxx…showDetail…isPrint=0 * 若你以后发现是新开窗口预览,改成 'popup' */ previewOpenMode: 'sameTab', previewIframeSelector: '', iframePrintButtonSelector: '', debug: false, }; const STORAGE_KEY = 'batchStudentPrint_state_v1'; const DEFAULT_STATE = { running: false, stopRequested: false, listUrl: '', /** @type {{ href: string, text: string }[]} */ queue: [], index: -1, phase: 'idle', _popupRef: null, }; function log(...args) { if (CONFIG.debug) console.log('[批量打印]', ...args); } function loadState() { try { const raw = GM_getValue(STORAGE_KEY, null); if (!raw || typeof raw !== 'object') return { ...DEFAULT_STATE }; return { ...DEFAULT_STATE, ...raw, _popupRef: null }; } catch { return { ...DEFAULT_STATE }; } } function saveState(partial) { const cur = loadState(); const next = { ...cur, ...partial }; const serializable = { ...next }; delete serializable._popupRef; GM_setValue(STORAGE_KEY, serializable); return next; } function clearState() { GM_setValue(STORAGE_KEY, null); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } function findClickableByText(root, text, tagNames = 'a,button,span,input') { const nodes = root.querySelectorAll(tagNames); for (const el of nodes) { if (el.textContent && el.textContent.trim().includes(text)) { return el.closest('a,button') || el; } } return null; } /** 预览页工具栏「打印」(排除文案中含「预览」的父级) */ function findPrintPageToolbarPrintEl() { const css = (CONFIG.printPagePrintButtonSelectorCss || '').trim(); if (css) { const el = document.querySelector(css); if (el) return el; } const nodes = document.querySelectorAll('a,button,span'); for (const el of nodes) { const raw = (el.textContent || '').replace(/\u00a0/g, ' '); const norm = raw.replace(/\s+/g, '').trim(); if (norm !== '打印') continue; const btn = el.closest('a,button') || el; const lbl = ((btn.textContent || '').replace(/\s+/g, '')); if (lbl.includes('打印预览')) continue; return btn; } return null; } function waitFor(predicate, timeoutMs, intervalMs = 80) { const start = Date.now(); return new Promise((resolve, reject) => { const tick = () => { try { const v = predicate(); if (v) return resolve(v); } catch (e) { return reject(e); } if (Date.now() - start > timeoutMs) return resolve(null); setTimeout(tick, intervalMs); }; tick(); }); } /** @param {Node} node @returns {HTMLAnchorElement | null} */ function unwrapAnchor(node) { if (node instanceof HTMLAnchorElement) return node; const p = node && node.closest && node.closest('a'); return p instanceof HTMLAnchorElement ? p : null; } /** 排除表头占位,要求链接在行 tbody 内 */ function anchorInDataRow(a) { const tr = a.closest('tr'); if (!tr || !tr.closest || !tr.closest('tbody')) return false; return true; } function collectStudentLinks() { const extra = Array.isArray(CONFIG.extraListSelectors) ? CONFIG.extraListSelectors.filter(Boolean) : []; const primary = (CONFIG.listStudentLinkSelector || '').trim(); /** @type {string[]} */ const selectors = [ ...extra, primary, '.grid-table tbody tr td:nth-child(2) a', '.ui-jqgrid-btable tbody tr td:nth-child(2) a', 'table.datagrid-btable tbody tr td:nth-child(2) a', '.datagrid-view tbody tr td:nth-child(2) a', 'table tbody tr td:nth-child(2) a', 'tbody tr td:nth-child(2) a', 'a[href*="goStuDetail.action"]', 'a[href*="goStuDetail"]', ].filter(Boolean); const uniq = [...new Set(selectors)]; const out = []; const seen = new Set(); for (const sel of uniq) { let nodes = []; try { nodes = Array.from(document.querySelectorAll(sel)); } catch { continue; } for (const node of nodes) { const a = unwrapAnchor(node); if (!a || !anchorInDataRow(a)) continue; let href = ''; try { href = a.href || ''; } catch (_) { continue; } if (!href || !/goStuDetail/i.test(href)) continue; if (seen.has(href)) continue; const text = ((a.textContent || '').trim()).replace(/\s+/g, ' '); if (!text || text === '——' || text === '--') continue; seen.add(href); out.push({ href, text }); } } return out; } function ensurePanel() { let el = document.getElementById('batch-print-student-panel'); if (el) return el; el = document.createElement('div'); el.id = 'batch-print-student-panel'; el.style.cssText = [ 'position:fixed', 'right:16px', 'bottom:16px', 'z-index:2147483646', 'font-family:system-ui,sans-serif', 'font-size:13px', 'background:#1e293b', 'color:#f8fafc', 'padding:12px 14px', 'border-radius:10px', 'box-shadow:0 8px 24px rgba(0,0,0,.35)', 'min-width:200px', ].join(';'); el.innerHTML = `
批量打印
就绪
仅当前页表格;预览为同标签时请保持本标签勿手动关闭。
`; document.documentElement.appendChild(el); return el; } function setPanelStatus(text) { const s = document.getElementById('bps-status'); if (s) s.textContent = text; } function setupListPage() { const panel = ensurePanel(); const btnStart = panel.querySelector('#bps-start'); const btnStop = panel.querySelector('#bps-stop'); const state = loadState(); if (state.running && state.phase === 'return_list' && state.queue.length) { const nextNo = Math.min(state.index + 2, state.queue.length); setPanelStatus(`继续:下一位 ${nextNo}/${state.queue.length}`); queueMicrotask(() => resumeListAfterReturn()); } else { setPanelStatus('就绪'); } btnStart.addEventListener('click', async () => { const links = collectStudentLinks(); if (!links.length) { alert( '未抓到姓名链接;请修正 CONFIG.listStudentLinkSelector(F12 右键「姓名」→ Copy → Copy selector)' ); return; } saveState({ running: true, stopRequested: false, listUrl: location.href, queue: links, index: -1, phase: 'goto_detail', }); setPanelStatus(`排队 ${links.length} 人…`); await sleep(CONFIG.wait.betweenStudents); goNextStudentFromList(); }); btnStop.addEventListener('click', () => { saveState({ running: false, stopRequested: true, phase: 'idle' }); setPanelStatus('已停止'); }); } function goNextStudentFromList() { const state = loadState(); if (!state.running || state.stopRequested) return; const nextIndex = state.index + 1; if (nextIndex >= state.queue.length) { clearState(); setPanelStatus('全部完成'); saveState({ running: false, phase: 'idle' }); return; } const item = state.queue[nextIndex]; saveState({ index: nextIndex, phase: 'goto_detail', }); setPanelStatus(`打开 ${nextIndex + 1}/${state.queue.length}:${item.text || ''}`); location.href = item.href; } async function resumeListAfterReturn() { const state = loadState(); if (!state.running || state.stopRequested) return; await sleep(CONFIG.wait.betweenStudents); goNextStudentFromList(); } let openHookInstalled = false; function installOpenHook() { if (openHookInstalled) return; openHookInstalled = true; const orig = window.open; window.open = function (url, name, features) { const w = orig.call(this, url, name, features); const cur = loadState(); if (w && cur.running && !cur.stopRequested) { const iv = setInterval(() => { const s = loadState(); if (!s.running || s.stopRequested) { clearInterval(iv); return; } try { if (w.closed) { clearInterval(iv); log('预览窗口已关闭,返回列表'); sleep(CONFIG.wait.afterNavigation).then(() => { saveState({ phase: 'return_list' }); const listUrl = loadState().listUrl; if (listUrl) location.href = listUrl; }); } } catch { clearInterval(iv); } }, CONFIG.wait.popupPoll); } return w; }; } function findPrintPreviewElOnDetail() { const css = (CONFIG.detailPrintPreviewSelectorCss || '').trim(); if (css) { const el = document.querySelector(css); if (el) return el; } return findClickableByText(document.body, '打印预览'); } async function runDetailPage() { const state = loadState(); if (!state.running || state.stopRequested) return; if (state.phase !== 'goto_detail' && state.phase !== 'on_detail') return; if (CONFIG.previewOpenMode === 'popup') installOpenHook(); saveState({ phase: 'on_detail' }); const btn = await waitFor( () => findPrintPreviewElOnDetail(), CONFIG.wait.domReady, 100 ); if (!btn) { alert('找不到「打印预览」。请填充 CONFIG.detailPrintPreviewSelectorCss 或稍后重试。'); saveState({ running: false, phase: 'idle' }); return; } // sameTab:导航会立刻卸载当前文档,必须先写入阶段 if (CONFIG.previewOpenMode === 'sameTab') saveState({ phase: 'goto_print' }); btn.click(); log('已点击 打印预览'); if (CONFIG.previewOpenMode === 'sameTab') return; await sleep(CONFIG.wait.afterClickPreview); if ( CONFIG.previewOpenMode === 'iframe' && CONFIG.previewIframeSelector ) { const iframe = await waitFor( () => document.querySelector(CONFIG.previewIframeSelector), CONFIG.wait.domReady ); if (iframe && iframe.contentDocument) { const doc = iframe.contentDocument; let pbtn = null; if (CONFIG.iframePrintButtonSelector) { pbtn = doc.querySelector(CONFIG.iframePrintButtonSelector); } if (!pbtn) pbtn = findClickableByText(doc.body, '打印'); if (pbtn && CONFIG.autoPrintOnPreviewPage) { pbtn.click(); await sleep(CONFIG.wait.printDialogDelay); } try { iframe.contentWindow.print(); } catch (_) {} } await sleep(500); const listUrl = loadState().listUrl; if (listUrl) { saveState({ phase: 'return_list' }); location.href = listUrl; } return; } let st = loadState(); if (CONFIG.isPrintPreviewPage()) await runPrintPage(); } async function runPrintPage() { const state = loadState(); if (!state.running || state.stopRequested) return; saveState({ phase: 'on_print' }); await sleep(CONFIG.wait.afterNavigation); if (CONFIG.printPageExtraPrintSelector) { const extra = document.querySelector(CONFIG.printPageExtraPrintSelector); if (extra) extra.click(); await sleep(300); } const toolbarPrint = findPrintPageToolbarPrintEl(); if (toolbarPrint) { toolbarPrint.click(); log('已点击 打印(工具栏)'); } else if (CONFIG.autoPrintOnPreviewPage) { await sleep(CONFIG.wait.printDialogDelay); try { window.print(); } catch (_) {} log('未找到按钮,使用 window.print()'); } window.addEventListener( 'afterprint', () => finishPrintAndReturn(), { once: true } ); setTimeout(() => finishPrintAndReturn(), 6500); } let _finishing = false; function finishPrintAndReturn() { if (_finishing) return; _finishing = true; const state = loadState(); // 若为 window.open 子窗口脚本也注入了 if (window.opener) { try { window.close(); } catch (_) {} return; } if (CONFIG.closePreviewWindowAfterPrint) { try { window.close(); } catch (_) {} } saveState({ phase: 'return_list' }); setTimeout(() => { if (!window.closed && state.listUrl) location.href = state.listUrl; }, 320); } function detectPage() { if (CONFIG.isListPage()) return 'list'; if (CONFIG.isPrintPreviewPage()) return 'print'; if (CONFIG.isDetailPage()) return 'detail'; return 'unknown'; } async function main() { const page = detectPage(); log('页面类型:', page, location.href); if (page === 'unknown' && CONFIG.debug) { console.warn( '[批量打印] URL 未被识别:', '\n详情应含 goStuDetail.action ', '\n预览应含 zzZxsJbxxAction!showDetail.action 且 isPrint=0' ); } if (page === 'list') { setupListPage(); return; } if (page === 'detail') { await sleep(CONFIG.wait.afterNavigation); await runDetailPage(); return; } if (page === 'print') { await runPrintPage(); return; } } main().catch((e) => console.error('[批量打印] 异常', e)); })();