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