iClick

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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;
  }
})();