// ==UserScript== // @name 刷题计时器(公考平台通用) // @version 1.4.0 // @description 在粉笔/华图/中公等主流公考平台显示;开始后停留本页计时,离开/遮挡即暂停,可收缩为圆点 // @author Dpower // @match *://fenbi.com/* // @match *://*.fenbi.com/* // @match *://huatu.com/* // @match *://*.huatu.com/* // @match *://offcn.com/* // @match *://*.offcn.com/* // @match *://eoffcn.com/* // @match *://*.eoffcn.com/* // @match *://gongkaoleida.com/* // @match *://*.gongkaoleida.com/* // @license MIT // @grant none // @run-at document-idle // @tag 公考刷题计时器 主流公考平台通用 页面置顶显示计时 离开暂停计时 // ==/UserScript== (function () { 'use strict'; if (window.__STUDY_TIMER__) return; window.__STUDY_TIMER__ = true; let active = false, total = 0, segStart = 0, timerId = null; function accrue() { if (active && segStart) { total += performance.now() - segStart; segStart = 0; } } function beginSeg() { if (active) segStart = performance.now(); } function elapsed() { return total + (active && segStart ? performance.now() - segStart : 0); } function fmt(ms) { const t = Math.floor(ms / 1000); return [Math.floor(t / 3600), Math.floor((t % 3600) / 60), t % 60] .map(n => String(n).padStart(2, '0')).join(':'); } function fmtCompact(ms) { const t = Math.floor(ms / 1000); const h = Math.floor(t / 3600), m = Math.floor((t % 3600) / 60), s = t % 60; return h > 0 ? h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0') : String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); } // ---------- 完整面板 ---------- const panel = document.createElement('div'); panel.style.cssText = 'position:fixed;top:16px;right:16px;z-index:2147483647;' + 'background:#1f2937;color:#fff;border-radius:12px;padding:12px 16px;' + 'box-shadow:0 8px 24px rgba(0,0,0,.35);display:flex;flex-direction:column;gap:6px;' + 'font-family:system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;user-select:none;'; const timeEl = document.createElement('div'); timeEl.style.cssText = 'font-size:28px;font-weight:700;text-align:center;' + 'font-variant-numeric:tabular-nums;letter-spacing:1px;'; timeEl.textContent = '00:00:00'; const statusEl = document.createElement('div'); statusEl.style.cssText = 'font-size:11px;text-align:center;color:#9ca3af;'; statusEl.textContent = '未开始'; const bar = document.createElement('div'); bar.style.cssText = 'display:flex;gap:6px;justify-content:center;margin-top:2px;'; function mkBtn(txt, color) { const b = document.createElement('button'); b.textContent = txt; b.style.cssText = 'border:none;border-radius:6px;padding:6px 12px;cursor:pointer;' + 'font-size:13px;color:#fff;background:' + color + ';'; b.addEventListener('mouseenter', () => b.style.opacity = '.85'); b.addEventListener('mouseleave', () => b.style.opacity = '1'); return b; } const toggleBtn = mkBtn('开始', '#10b981'); const resetBtn = mkBtn('重置', '#6b7280'); const minBtn = mkBtn('—', '#4b5563'); const closeBtn = mkBtn('×', '#ef4444'); bar.append(toggleBtn, resetBtn, minBtn, closeBtn); panel.append(timeEl, statusEl, bar); // ---------- 小圆点 ---------- const dot = document.createElement('div'); dot.style.cssText = 'position:fixed;top:16px;right:16px;z-index:2147483647;' + 'width:50px;height:50px;border-radius:50%;display:none;align-items:center;justify-content:center;' + 'cursor:pointer;color:#fff;background:#374151;box-shadow:0 4px 14px rgba(0,0,0,.35);' + 'font-family:system-ui,sans-serif;font-size:11px;font-weight:700;' + 'font-variant-numeric:tabular-nums;user-select:none;'; const dotText = document.createElement('span'); dotText.textContent = '00:00'; dot.appendChild(dotText); // ---------- 拖动 ---------- let dragTarget = null, dx = 0, dy = 0, downX = 0, downY = 0, moved = false; function startDrag(el, e) { dragTarget = el; dx = e.clientX - el.offsetLeft; dy = e.clientY - el.offsetTop; downX = e.clientX; downY = e.clientY; moved = false; } panel.addEventListener('mousedown', e => { if (e.target.tagName === 'BUTTON') return; startDrag(panel, e); }); dot.addEventListener('mousedown', e => startDrag(dot, e)); document.addEventListener('mousemove', e => { if (!dragTarget) return; if (Math.abs(e.clientX - downX) > 3 || Math.abs(e.clientY - downY) > 3) moved = true; dragTarget.style.left = (e.clientX - dx) + 'px'; dragTarget.style.top = (e.clientY - dy) + 'px'; dragTarget.style.right = 'auto'; }); document.addEventListener('mouseup', () => { if (dragTarget === dot && !moved) expand(); dragTarget = null; }); // ---------- 收缩 / 展开 ---------- function collapse() { const r = panel.getBoundingClientRect(); panel.style.display = 'none'; dot.style.left = r.left + 'px'; dot.style.top = r.top + 'px'; dot.style.right = 'auto'; dot.style.display = 'flex'; } function expand() { const r = dot.getBoundingClientRect(); dot.style.display = 'none'; panel.style.display = 'flex'; panel.style.left = r.left + 'px'; panel.style.top = r.top + 'px'; panel.style.right = 'auto'; } minBtn.addEventListener('click', collapse); // ---------- 逻辑 ---------- function render() { const counting = active && segStart !== 0; timeEl.textContent = fmt(elapsed()); if (!active && total === 0) { statusEl.textContent = '未开始'; statusEl.style.color = '#9ca3af'; } else if (counting) { statusEl.textContent = '▶ 计时中'; statusEl.style.color = '#34d399'; } else { statusEl.textContent = '⏸ 已暂停'; statusEl.style.color = '#fbbf24'; } dotText.textContent = fmtCompact(elapsed()); if (!active && total === 0) dot.style.background = '#374151'; else if (counting) dot.style.background = '#059669'; else dot.style.background = '#b45309'; } function start() { if (active) return; active = true; beginSeg(); toggleBtn.textContent = '暂停'; toggleBtn.style.background = '#f59e0b'; render(); } function pause() { accrue(); active = false; toggleBtn.textContent = '继续'; toggleBtn.style.background = '#10b981'; render(); } function reset() { active = false; segStart = 0; total = 0; toggleBtn.textContent = '开始'; toggleBtn.style.background = '#10b981'; render(); } toggleBtn.addEventListener('click', () => active ? pause() : start()); resetBtn.addEventListener('click', reset); closeBtn.addEventListener('click', () => { accrue(); active = false; clearInterval(timerId); document.removeEventListener('visibilitychange', onVisibility); window.removeEventListener('blur', onBlur); window.removeEventListener('focus', onFocus); panel.remove(); dot.remove(); window.__STUDY_TIMER__ = false; }); // ---------- 暂停时机 ---------- function onVisibility() { if (document.hidden) accrue(); else beginSeg(); } function onBlur() { if (!document.hidden) accrue(); } function onFocus() { if (!document.hidden) beginSeg(); } document.addEventListener('visibilitychange', onVisibility); window.addEventListener('blur', onBlur); window.addEventListener('focus', onFocus); timerId = setInterval(render, 250); document.body.append(panel, dot); })();