// ==UserScript== // @name 上岸村机考·专项练习暂停 // @namespace http://tampermonkey.net/ // @version 1.0.2 // @description 给专项练习与全真模拟加一颗暂停/继续计时按钮,不怕有急事、可以练质量。 // @license MIT // @match https://pub.xdtech.top/mingshi/wxpage/tiku/gongan/zhuanxiang.html* // @match https://pub.xdtech.top/*/wxpage/tiku/gongan/zhuanxiang.html* // @match https://pub.xdtech.top/mingshi/wxpage/tiku/gongan/practice.html* // @match https://pub.xdtech.top/*/wxpage/tiku/gongan/practice.html* // @include *://*.xdtech.top/*/wxpage/tiku/gongan/zhuanxiang.html* // @include *://*.xdtech.top/*/wxpage/tiku/gongan/practice.html* // @grant none // @run-at document-idle // ==/UserScript== (function () { 'use strict'; var CSS = [ '.remain-time{width:auto!important}', '.gth-timer-btn{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;', 'height:30px;line-height:30px;padding:0 30px;margin-left:8px;border:0;border-radius:5px;', 'background:#fff;color:#2178db;font-size:16px;cursor:pointer;vertical-align:middle;user-select:none}', '.gth-timer-btn .gth-timer-ic{width:18px;height:18px;flex:0 0 auto}', '.gth-timer-btn .gth-timer-ic svg{width:100%;height:100%;display:block}', '.gth-timer-btn:hover{background:#d8e9fa}', '.gth-timer-btn.paused{background:#f59e0b;color:#fff}', '.gth-timer-btn.paused:hover{background:#d97706}', '.gth-timer-btn.dead{background:#5b6b7d;color:#dbe6f2;border:1px dashed rgba(255,255,255,.55)}', '.remain-time.gth-paused a{color:#fde68a}', '.remain-time.gth-paused a::after{content:"(已暂停)";font-size:13px}' ].join(''); var LUCIDE = { pause: '', play: '' }; function icon(name) { var d = LUCIDE[name]; if (!d) return ''; return ''; } var paused = false; var dead = ''; var scope = null; var restored = false; var ngSrc = ''; var ngErr = ''; var scopeCount = 0; var valveOn = 0; var valveWanted = 0; var holdSeen = null; var warned = ''; var storeErr = ''; var lastSig = ''; var frozenText = ''; var domTimer = null; var running = false; var lastTotal = -1; var SID = ''; try { SID = new URLSearchParams(location.search).get('submit_id') || ''; } catch (e) { SID = ''; } var LS_KEY = 'gth_timer_paused_' + (SID || location.pathname); function errName(e) { return e && e.name ? e.name : String(e); } function store(v) { try { if (v) localStorage.setItem(LS_KEY, '1'); else localStorage.removeItem(LS_KEY); storeErr = ''; } catch (e) { storeErr = '本地存储写不进(暂停状态刷新后不保留):' + errName(e); } } function stored() { try { return localStorage.getItem(LS_KEY) === '1'; } catch (e) { storeErr = '本地存储读不出(当作未暂停):' + errName(e); return false; } } function pad2(x) { return (x < 10 ? '0' : '') + x; } function hms(n) { if (typeof n !== 'number' || !(n >= 0)) return '--:--:--'; return pad2(Math.floor(n / 3600)) + ':' + pad2(Math.floor(n % 3600 / 60)) + ':' + pad2(n % 60); } function pickNg(w) { return (w && w.angular && typeof w.angular.element === 'function') ? w.angular : null; } function pageNg() { var wins = [ ['window', typeof window === 'undefined' ? null : window], ['unsafeWindow', typeof unsafeWindow === 'undefined' ? null : unsafeWindow] ]; var i, ng = null; for (i = 0; i < wins.length; i++) { ng = pickNg(wins[i][1]); if (ng) { ngSrc = wins[i][0]; ngErr = ''; return ng; } } if (typeof angular !== 'undefined' && angular && typeof angular.element === 'function') { ngSrc = 'angular'; ngErr = ''; return angular; } ngSrc = ''; return null; } function eachTimerScope(fn) { var ng = pageNg(); scopeCount = 0; if (!ng) { ngErr = 'window / unsafeWindow / 裸 angular 三条路上都没有 angular'; return false; } var root; try { root = ng.element(document.body).injector().get('$rootScope'); } catch (e) { ngErr = '拿不到 $rootScope:' + (e && e.message ? e.message : String(e)); return false; } if (!root) { ngErr = '$rootScope 是空的'; return false; } var seen = new WeakSet(); (function walk(s) { if (!s || seen.has(s)) return; seen.add(s); if (Object.prototype.hasOwnProperty.call(s, 'localStorage') && s.localStorage && typeof s.localStorage === 'object' && ('allInterval' in s || 'partInterval' in s)) { fn(s); scopeCount++; } for (var c = s.$$childHead; c; c = c.$$nextSibling) walk(c); })(root); if (!scopeCount) ngErr = '页面上没有 localStorage + allInterval 形状的计时器 scope'; return scopeCount > 0; } function guard(obj, key) { if (!obj || typeof obj !== 'object' || !(key in obj)) return ''; var d = Object.getOwnPropertyDescriptor(obj, key); if (!d) return ''; if (d.get) return 'on'; if (!d.configurable) return ''; var cur = obj[key]; Object.defineProperty(obj, key, { configurable: true, enumerable: true, get: function () { return cur; }, set: function (v) { if (!paused) cur = v; } }); return 'new'; } function mainValve() { var ls = scope && scope.localStorage; if (!ls) return false; var d = Object.getOwnPropertyDescriptor(ls, 'totalTime'); return !!(d && d.get); } function installGuards() { var scopes = []; if (scope) scopes.push(scope); eachTimerScope(function (s) { scopes.push(s); }); var seenS = new WeakSet(); valveOn = 0; valveWanted = 0; scopes.forEach(function (s) { if (!s || seenS.has(s)) return; seenS.add(s); var ls = s.localStorage || {}; var pairs = [[ls, 'totalTime'], [ls, 'elapsed_time']]; (ls.partData || []).forEach(function (p) { pairs.push([p, 'remainTime']); pairs.push([p, 'elapsed_time']); }); Object.keys(s.detail_elapsed_time || {}).forEach(function (k) { pairs.push([s.detail_elapsed_time, k]); }); pairs.forEach(function (pair) { valveWanted++; if (guard(pair[0], pair[1])) valveOn++; }); }); return valveOn; } function holdReason() { var ls = scope && scope.localStorage; if (!ls) return '页面计时器 scope 不在了'; if (!mainValve()) return '阀门没挂在 totalTime 上'; var v = ls.totalTime; if (typeof v !== 'number') return 'totalTime 不是数字'; if (holdSeen !== null && v !== holdSeen) return '暂停期间 totalTime 仍在变,还有第二条时钟'; holdSeen = v; return ''; } function warnDead() { if (warned === dead) return; warned = dead; console.warn('[计时暂停] ' + dead); } function release() { paused = false; holdSeen = null; freezeDom(false); store(false); } function applyPause(on) { if (!on) { dead = ''; release(); render(); return; } paused = true; holdSeen = null; installGuards(); dead = holdReason(); if (dead) { release(); warnDead(); } else { freezeDom(true); store(true); } render(); } function btn() { return document.querySelector('.gth-timer-btn'); } function timerHost() { return document.querySelector('.operation-dati .remain-time') || document.querySelector('.remain-time'); } function diag() { var s = '取页:' + (ngSrc || '没拿到'); if (ngErr) s += '(' + ngErr + ')'; s += ' · 计时器' + scopeCount + '台 · 阀门' + valveOn + '/' + valveWanted; if (paused) s += ' · 冻在 ' + hms(holdSeen); if (storeErr) s += ' · ' + storeErr; return s; } function label() { if (dead) return '不可暂停'; return paused ? icon('play') : icon('pause'); } function tip() { if (dead) return '取不到页面计时器,本页不可暂停:' + dead + ' · ' + diag(); return paused ? '已冻结,可以放心离开' : '点这里冻结计时'; } function pausedClass() { return !!(paused && !dead); } function render() { var b = btn(); if (!b) return; var host = timerHost(); var sig = [paused ? '1' : '0', dead, host ? 'h' : '-', pausedClass() ? 'p' : '-', diag()].join('|'); if (sig === lastSig) return; lastSig = sig; b.className = 'gth-timer-btn' + (paused ? ' paused' : '') + (dead ? ' dead' : ''); b.innerHTML = label(); b.title = tip(); if (host) host.classList.toggle('gth-paused', pausedClass()); } function ensureBtn() { var host = timerHost(); if (!host) return; var b = btn(); if (b) { if (b.parentNode !== host) host.appendChild(b); return; } b = document.createElement('button'); b.type = 'button'; b.className = 'gth-timer-btn'; b.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); applyPause(!paused); }); host.appendChild(b); lastSig = ''; } function remainAnchor() { return document.querySelector('.remain-time a'); } function remainText() { var a = remainAnchor(); return a ? (a.textContent || '') : ''; } function timeTextNode() { var a = remainAnchor(); if (!a) return null; var i, c; for (i = 0; i < a.childNodes.length; i++) { c = a.childNodes[i]; if (c.nodeType === 3 && /\d{1,2}:\d{2}:\d{2}/.test(c.nodeValue)) return c; } for (i = 0; i < a.childNodes.length; i++) { c = a.childNodes[i]; if (c.nodeType === 3) return c; } return null; } function freezeDom(on) { if (domTimer) { clearInterval(domTimer); domTimer = null; } frozenText = ''; if (!on) return; var n = timeTextNode(); if (!n) return; frozenText = n.nodeValue; domTimer = setInterval(function () { var nn = timeTextNode(); if (nn && frozenText && nn.nodeValue !== frozenText) nn.nodeValue = frozenText; }, 150); } function domSeconds() { var m = remainText().match(/(\d{1,2}):(\d{2}):(\d{2})/); if (!m) return -1; return (+m[1]) * 3600 + (+m[2]) * 60 + (+m[3]); } function findScope() { var want = domSeconds(); var best = null; var bestScore = -1; eachTimerScope(function (s) { var t = s.localStorage.totalTime; var score = 0; if (typeof t === 'number') { if (want >= 0 && Math.abs(t - want) <= 2) score = 1e6; else if (want >= 0) score = 1e5 - Math.min(Math.abs(t - want), 9999); else score = t; } if (score > bestScore) { best = s; bestScore = score; } }); return best; } function live() { var s = scope; var t = (s && s.localStorage) ? s.localStorage.totalTime : -1; if (s && lastTotal >= 0 && typeof t === 'number' && t < lastTotal) running = true; lastTotal = typeof t === 'number' ? t : -1; var parentAllTime = s && s.$parent && typeof s.$parent.allTime === 'string' ? s.$parent.allTime : ''; var segs = remainText().match(/(\d{1,2}:\d{2}:\d{2})/g) || []; var domLive = segs.some(function (seg) { return seg !== '00:00:00'; }); return running || !!(s && s.allInterval) || (typeof t === 'number' && t > 60) || (parentAllTime.length > 0 && !/00:00:00/.test(parentAllTime)) || domLive; } function tick() { var s = findScope(); if (s !== scope) { scope = s; restored = false; running = false; lastTotal = -1; holdSeen = null; } if (scope && scope.$$destroyed) { scope = null; restored = false; } if (scope && scope.isAlreadySubmit) { if (paused || stored()) release(); dead = ''; var old = btn(); if (old) old.remove(); return; } if (!timerHost()) return; ensureBtn(); var isLive = live(); render(); if (!isLive) return; if (!restored) { restored = true; if (stored()) applyPause(true); } if (paused) { installGuards(); dead = holdReason(); if (dead) { release(); warnDead(); } } render(); } var st = document.createElement('style'); st.textContent = CSS; document.head.appendChild(st); tick(); setInterval(tick, 1000); })();