iClick

Auto-click any button on any page. Supports interval, time limit, click limit, and persistent run across page navigation.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         iClick
// @namespace    https://github.com/
// @version      2.1
// @description  Auto-click any button on any page. Supports interval, time limit, click limit, and persistent run across page navigation.
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function () {
  'use strict';

  // ── 暗色模式 ──
  const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  const C = dark
    ? { bg: '#1f1f1f', border: '#333', text: '#ccc', sub: '#888', badge: '#2a2a2a', sep: '#333' }
    : { bg: '#fff',    border: '#ddd', text: '#333', sub: '#555', badge: '#f5f5f5', sep: '#eee' };

  // ── 共享顶栏 ──
  function getTopbar() {
    let bar = document.getElementById('kiro-topbar');
    if (!bar) {
      bar = document.createElement('div');
      bar.id = 'kiro-topbar';
      bar.style.cssText = `position:fixed;top:0;left:0;right:0;z-index:99999;background:${C.bg};border-bottom:1px solid ${C.border};display:flex;align-items:stretch;min-height:36px;box-shadow:0 2px 6px rgba(0,0,0,.15);font-size:13px;font-family:sans-serif;`;
      document.body.appendChild(bar);
      document.body.style.paddingTop = '36px';
    }
    return bar;
  }

  function addSection(id) {
    const bar = getTopbar();
    if (bar.querySelector(`#${id}`)) return bar.querySelector(`#${id}`);
    if (bar.children.length > 0) {
      const sep = document.createElement('div');
      sep.style.cssText = `width:1px;background:${C.sep};margin:6px 0;flex-shrink:0;`;
      bar.appendChild(sep);
    }
    const spacer = document.createElement('div');
    spacer.style.cssText = 'flex:1;';
    bar.appendChild(spacer);
    const sec = document.createElement('div');
    sec.id = id;
    sec.style.cssText = 'display:flex;align-items:center;gap:8px;padding:0 14px;flex-shrink:0;overflow:hidden;';
    bar.appendChild(sec);
    return sec;
  }

  // ── 跨页持久运行:页面加载时检查是否应自动恢复 ──
  const PERSIST_KEY = 'ac_persist';
  const savedState = JSON.parse(GM_getValue(PERSIST_KEY, 'null'));
  const shouldResume = savedState && savedState.running && savedState.target;

  const savedInterval = GM_getValue('ac_interval', 1500);

  let running = false, timer = null, clockTimer = null;
  let count = 0, elapsed = 0;

  // ── UI ──
  const sec = addSection('kiro-clicker');

  const label = el('span', `font-weight:700;white-space:nowrap;color:${C.text};font-size:13px;letter-spacing:.5px;`, 'iClick');

  const targetSelect = document.createElement('select');
  targetSelect.style.cssText = `padding:2px 8px;border:1px solid ${C.border};border-radius:12px;font-size:12px;background:${C.badge};color:${C.text};max-width:120px;flex-shrink:0;outline:none;cursor:pointer;`;

  const intervalInput = document.createElement('input');
  intervalInput.type = 'number'; intervalInput.value = savedInterval; intervalInput.min = 100;
  intervalInput.title = '间隔(ms)';
  intervalInput.style.cssText = `width:64px;padding:2px 8px;border:1px solid ${C.border};border-radius:12px;font-size:12px;background:${C.badge};color:${C.text};outline:none;text-align:center;`;

  const limitInput = document.createElement('input');
  limitInput.type = 'number'; limitInput.value = GM_getValue('ac_limit', 0); limitInput.min = 0;
  limitInput.title = '时限(分钟,0=不限)';
  limitInput.style.cssText = `width:54px;padding:2px 8px;border:1px solid ${C.border};border-radius:12px;font-size:12px;background:${C.badge};color:${C.text};outline:none;text-align:center;`;

  const maxInput = document.createElement('input');
  maxInput.type = 'number'; maxInput.value = GM_getValue('ac_max', 0); maxInput.min = 0;
  maxInput.title = '点击上限(0=不限)';
  maxInput.style.cssText = `width:54px;padding:2px 8px;border:1px solid ${C.border};border-radius:12px;font-size:12px;background:${C.badge};color:${C.text};outline:none;text-align:center;`;

  const status = el('span', `color:${C.sub};white-space:nowrap;font-size:12px;`, '就绪');
  const toggleBtn = btn('▶ 开始', '#1677ff');

  const lbInterval = el('span', `color:${C.sub};font-size:11px;white-space:nowrap;`, 'ms');
  const lbLimit    = el('span', `color:${C.sub};font-size:11px;white-space:nowrap;`, 'min');
  const lbMax      = el('span', `color:${C.sub};font-size:11px;white-space:nowrap;`, 'max');

  sec.append(label, targetSelect, lbInterval, intervalInput, lbLimit, limitInput, lbMax, maxInput, status, toggleBtn);

  // 扫描按钮
  const refreshButtons = () => {
    const cur = targetSelect.value || (savedState && savedState.target) || '';
    targetSelect.innerHTML = '';
    [...new Set(
      [...document.querySelectorAll('button,input[type=button],input[type=submit],[role=button]')]
        .filter(b => b.offsetParent && !document.getElementById('kiro-clicker')?.contains(b))
        .map(b => (b.textContent || b.value || '').trim())
        .filter(Boolean)
    )].forEach(text => {
      const o = document.createElement('option');
      o.value = o.textContent = text;
      if (text === cur) o.selected = true;
      targetSelect.appendChild(o);
    });
  };
  refreshButtons();
  targetSelect.onfocus = refreshButtons;

  const findTarget = () =>
    [...document.querySelectorAll('button,input[type=button],input[type=submit],[role=button]')]
      .find(b => (b.textContent || b.value || '').trim() === targetSelect.value
        && !b.disabled && b.offsetParent
        && !document.getElementById('kiro-clicker')?.contains(b));

  const stop = (reason) => {
    running = false;
    clearInterval(timer); clearInterval(clockTimer);
    toggleBtn.textContent = '▶ 开始'; toggleBtn.style.background = '#1677ff';
    GM_setValue(PERSIST_KEY, JSON.stringify({ running: false, target: targetSelect.value }));
    if (reason) status.textContent = reason;
  };

  const start = () => {
    const interval = Math.max(100, parseInt(intervalInput.value) || 1500);
    const limitSec = (parseFloat(limitInput.value) || 0) * 60;
    const maxClicks = parseInt(maxInput.value) || 0;
    GM_setValue('ac_interval', interval);
    GM_setValue('ac_limit', limitInput.value);
    GM_setValue('ac_max', maxInput.value);
    GM_setValue(PERSIST_KEY, JSON.stringify({ running: true, target: targetSelect.value }));

    count = 0; elapsed = 0; running = true;
    toggleBtn.textContent = '⏹ 停止'; toggleBtn.style.background = '#ff4d4f';

    clockTimer = setInterval(() => {
      elapsed++;
      const h = Math.floor(elapsed / 3600), m = Math.floor((elapsed % 3600) / 60), s = elapsed % 60;
      const timeStr = `${h ? h + 'h ' : ''}${m ? m + 'm ' : ''}${s}s`;
      status.textContent = `已点击 ${count} 次 · ${timeStr}`;
      if (limitSec > 0 && elapsed >= limitSec) stop('⏱ 已达时限,自动停止');
    }, 1000);

    timer = setInterval(() => {
      const el = findTarget();
      if (el) {
        el.click(); count++;
        if (maxClicks > 0 && count >= maxClicks) stop(`✅ 已达上限 ${maxClicks} 次,自动停止`);
      } else {
        status.textContent = `⚠️ 未找到按钮「${targetSelect.value}」`;
      }
    }, interval);
  };

  toggleBtn.onclick = () => running ? stop() : start();

  // 自动恢复
  if (shouldResume) {
    // 等 DOM 稳定后恢复
    setTimeout(() => { refreshButtons(); start(); status.textContent = '已自动恢复运行'; }, 1500);
  }

  function btn(text, color) {
    const b = document.createElement('button');
    b.textContent = text;
    b.style.cssText = `padding:3px 14px;background:${color};color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:12px;white-space:nowrap;flex-shrink:0;font-weight:500;letter-spacing:.3px;transition:opacity .15s;`;
    b.onmouseover = () => b.style.opacity = '0.8';
    b.onmouseout  = () => b.style.opacity = '1';
    return b;
  }

  function el(tag, css, text) {
    const e = document.createElement(tag);
    e.style.cssText = css;
    if (text !== undefined) e.textContent = text;
    return e;
  }
})();