// ==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 = `