自动点击菜单

自动点击菜单,支持按ID、类名、文本、位置自动点击,可设定执行次数

  1. // ==UserScript==
  2. // @name 自动点击菜单
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.47.2
  5. // @description 自动点击菜单,支持按ID、类名、文本、位置自动点击,可设定执行次数
  6. // @author YuoHira
  7. // @license MIT
  8. // @match *://*/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=github.io
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @require https://cdn.jsdelivr.net/npm/animejs@3.2.1/lib/anime.min.js
  13. // ==/UserScript==
  14.  
  15. // --- css.js ---
  16. const AUTO_CLICK_MENU_CSS = `
  17. /* ====== 主题变量 ====== */
  18. :root {
  19. --macaron-blue1: #7ecfff; /* 马卡龙主蓝 */
  20. --macaron-blue2: #aee2ff; /* 马卡龙浅蓝 */
  21. --macaron-bg: #fafdff; /* 背景淡蓝 */
  22. --macaron-border: #e0e6ed;/* 边框灰蓝 */
  23. --macaron-text: #2d3a4a; /* 主要字体色 */
  24. --macaron-shadow: 0 4px 24px 0 rgba(126,207,255,0.18); /* 柔和阴影 */
  25. }
  26.  
  27. /* ====== 菜单主容器 ====== */
  28. .yuohira-container {
  29. /* 渐变淡蓝背景,圆角,柔和阴影 */
  30. background: linear-gradient(135deg, #eaf6ff 0%, #fafdff 100%);
  31. border: none;
  32. border-radius: 16px;
  33. padding: 22px 24px 18px 24px;
  34. box-shadow: var(--macaron-shadow);
  35. display: flex;
  36. flex-direction: column;
  37. align-items: center;
  38. min-width: 320px;
  39. max-width: 95vw;
  40. transition: box-shadow 0.2s, transform 0.15s;
  41. }
  42. .yuohira-container:hover {
  43. box-shadow: 0 8px 32px 0 rgba(126,207,255,0.28);
  44. transform: scale(1.015);
  45. }
  46.  
  47. /* ====== 菜单标题 ====== */
  48. .yuohira-title {
  49. color: var(--macaron-blue1);
  50. font-family: 'Segoe UI', 'PingFang SC', 'Arial', sans-serif;
  51. font-size: 21px;
  52. font-weight: bold;
  53. margin-bottom: 18px;
  54. letter-spacing: 1px;
  55. }
  56.  
  57. /* ====== 通用按钮 ====== */
  58. .yuohira-button {
  59. /* 渐变蓝背景,圆角,阴影,动效 */
  60. background: linear-gradient(90deg, var(--macaron-blue1) 0%, var(--macaron-blue2) 100%);
  61. border: none;
  62. color: #fff;
  63. border-radius: 9px;
  64. padding: 8px 22px;
  65. cursor: pointer;
  66. font-size: 16px;
  67. margin: 7px 10px;
  68. font-weight: 500;
  69. box-shadow: 0 2px 8px 0 rgba(126,207,255,0.13);
  70. transition: background 0.2s, color 0.2s, filter 0.2s, transform 0.13s;
  71. outline: none;
  72. }
  73. .yuohira-button:hover {
  74. filter: brightness(1.09);
  75. transform: scale(1.06);
  76. }
  77. .yuohira-button:active {
  78. filter: brightness(0.98);
  79. transform: scale(0.96);
  80. }
  81.  
  82. /* ====== 右上角小圆球(菜单开关) ====== */
  83. .yuohira-toggle-button {
  84. background: linear-gradient(135deg, var(--macaron-blue1) 60%, var(--macaron-blue2) 100%);
  85. border: none;
  86. color: #fff;
  87. border-radius: 50%;
  88. padding: 0;
  89. cursor: pointer;
  90. font-size: 20px;
  91. width: 38px;
  92. height: 38px;
  93. position: fixed;
  94. top: 18px;
  95. right: 18px;
  96. z-index: 10001;
  97. opacity: 0.85;
  98. box-shadow: 0 2px 8px 0 rgba(126,207,255,0.18);
  99. display: flex;
  100. align-items: center;
  101. justify-content: center;
  102. transition: opacity 0.3s, background 0.2s, transform 0.13s;
  103. }
  104. .yuohira-toggle-button:hover {
  105. opacity: 1;
  106. transform: scale(1.12);
  107. }
  108. .yuohira-toggle-button:active {
  109. transform: scale(0.92);
  110. }
  111.  
  112. /* ====== 输入框和下拉框 ====== */
  113. .yuohira-input {
  114. /* 渐变淡蓝背景,圆角,阴影,动效 */
  115. border: 1.5px solid var(--macaron-blue1);
  116. border-radius: 8px;
  117. padding: 7px 12px;
  118. margin: 7px 10px;
  119. background: linear-gradient(90deg, #fafdff 60%, #eaf6ff 100%);
  120. color: var(--macaron-text);
  121. font-size: 15px;
  122. outline: none;
  123. box-shadow: 0 1.5px 6px 0 rgba(126,207,255,0.13);
  124. transition: border 0.2s, box-shadow 0.2s, transform 0.13s;
  125. appearance: none;
  126. -webkit-appearance: none;
  127. -moz-appearance: none;
  128. position: relative;
  129. }
  130. .yuohira-input:focus, .yuohira-input:hover {
  131. /* 深蓝高亮描边,阴影增强,缩放 */
  132. border-color: #3fa6e8;
  133. box-shadow: 0 3px 14px 0 rgba(126,207,255,0.22);
  134. transform: scale(1.045);
  135. }
  136. /* 下拉框专属美化,自定义蓝色箭头 */
  137. select.yuohira-input {
  138. background: #fff url('data:image/svg+xml;utf8,<svg fill="%237ecfff" height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M7.293 7.293a1 1 0 011.414 0L10 8.586l1.293-1.293a1 1 0 111.414 1.414l-2 2a1 1 0 01-1.414 0l-2-2a1 1 0 010-1.414z"/></svg>') no-repeat right 10px center/18px 18px;
  139. padding-right: 36px;
  140. cursor: pointer;
  141. min-width: 80px;
  142. }
  143. select.yuohira-input::-ms-expand {
  144. display: none;
  145. }
  146.  
  147. /* ====== 输入项整体包裹 ====== */
  148. .yuohira-input-wrapper {
  149. display: flex;
  150. align-items: center;
  151. margin-bottom: 10px;
  152. position: relative;
  153. padding-bottom: 18px;
  154. background: none;
  155. border-radius: 0;
  156. }
  157.  
  158. /* ====== 进度条 ====== */
  159. .yuohira-progress-bar {
  160. height: 5px;
  161. position: absolute;
  162. bottom: 0;
  163. left: 0;
  164. background: linear-gradient(90deg, var(--macaron-blue1), var(--macaron-blue2));
  165. border-radius: 2.5px;
  166. transition: width 0.3s;
  167. }
  168.  
  169. /* ====== 警告提示 ====== */
  170. .yuohira-warning {
  171. color: #3fa6e8;
  172. font-size: 12px;
  173. position: absolute;
  174. left: 0;
  175. bottom: -13px;
  176. width: 100%;
  177. text-align: left;
  178. z-index: 2;
  179. pointer-events: none;
  180. font-weight: 500;
  181. }
  182.  
  183. /* ====== 屏幕取点遮罩 ====== */
  184. .yuohira-crosshair-overlay {
  185. position: fixed;
  186. top: 0; left: 0; right: 0; bottom: 0;
  187. z-index: 99999;
  188. pointer-events: auto;
  189. background: rgba(126,207,255, 0.08);
  190. }
  191. .yuohira-crosshair-line {
  192. position: absolute;
  193. background: var(--macaron-blue2);
  194. z-index: 999999;
  195. }
  196. .yuohira-crosshair-label {
  197. position: absolute;
  198. background: var(--macaron-blue1);
  199. color: #fff;
  200. font-size: 13px;
  201. padding: 3px 8px;
  202. border-radius: 4px;
  203. z-index: 999999;
  204. pointer-events: none;
  205. transform: translateY(-150%);
  206. font-weight: 500;
  207. }
  208. `;
  209.  
  210. (function(){
  211.  
  212. // --- AutoClickMenu.js ---
  213. // == 自动点击菜单主控类 ==
  214. // 负责菜单的创建、样式注入、数据持久化、菜单项管理、自动点击主循环、动画、窗口拖动与位置保存等
  215. class AutoClickMenu {
  216. /**
  217. * 构造函数,初始化主流程
  218. * - 记录当前页面URL
  219. * - 读取自动点击开关状态
  220. * - 初始化菜单项列表
  221. * - 启动初始化流程
  222. */
  223. constructor() {
  224. this.currentUrl = window.location.origin; // 当前页面域名,用于数据隔离
  225. this.autoClickEnabled = GM_getValue(`${this.currentUrl}_autoClickEnabled`, false); // 自动点击开关
  226. this.lastUpdateTime = new Map(); // 记录每个菜单项的上次点击时间
  227. this.menuItems = []; // 菜单项对象列表
  228. this.init();
  229. }
  230.  
  231. /**
  232. * 初始化主流程,页面加载后执行
  233. * - 注入样式
  234. * - 创建菜单容器和小圆球
  235. * - 恢复上次保存的位置
  236. * - 添加各类按钮和输入区
  237. * - 加载本地保存的菜单项
  238. * - 启动自动点击主循环
  239. */
  240. init() {
  241. window.onload = () => {
  242. this.createStyles(); // 注入样式
  243. this.menuContainer = this.createMenuContainer(); // 创建菜单主容器
  244. this.toggleButton = new ToggleButton(this).createElement(); // 创建右上角小圆球
  245. document.body.appendChild(this.menuContainer);
  246. document.body.appendChild(this.toggleButton);
  247. // 恢复上次保存的位置(如有)
  248. const savedPos = GM_getValue('yuohira_menu_position', null);
  249. if (savedPos && typeof savedPos.top === 'number' && typeof savedPos.right === 'number') {
  250. this.menuContainer.style.top = savedPos.top + 'px';
  251. this.menuContainer.style.right = savedPos.right + 'px';
  252. this.toggleButton.style.top = savedPos.top + 'px';
  253. this.toggleButton.style.right = savedPos.right + 'px';
  254. }
  255. this.addMenuTitle(this.menuContainer); // 添加标题
  256. // 保存按钮,保存菜单项和位置
  257. this.saveButton = this.addButton(this.menuContainer, '保存', 'yuohira-button', (e) => {
  258. e.stopPropagation();
  259. this.saveData();
  260. // 保存当前位置
  261. const top = parseInt(this.menuContainer.style.top) || 10;
  262. const right = parseInt(this.menuContainer.style.right) || 10;
  263. GM_setValue('yuohira_menu_position', { top, right });
  264. });
  265. // 重置位置按钮
  266. this.resetButton = this.addButton(this.menuContainer, '重置位置', 'yuohira-button', (e) => {
  267. e.stopPropagation();
  268. this.menuContainer.style.top = '10px';
  269. this.menuContainer.style.right = '10px';
  270. this.toggleButton.style.top = '10px';
  271. this.toggleButton.style.right = '10px';
  272. });
  273. // 新增菜单项按钮
  274. this.addButtonElement = this.addButton(this.menuContainer, '+', 'yuohira-button', (e) => {
  275. e.stopPropagation();
  276. this.addInputField();
  277. });
  278. // 自动点击开关按钮
  279. this.toggleAutoClickButton = this.addButton(this.menuContainer, this.autoClickEnabled ? '暂停' : '开始', 'yuohira-button', (e) => {
  280. e.stopPropagation();
  281. this.autoClickEnabled = !this.autoClickEnabled;
  282. this.toggleAutoClickButton.innerText = this.autoClickEnabled ? '暂停' : '开始';
  283. GM_setValue(`${this.currentUrl}_autoClickEnabled`, this.autoClickEnabled);
  284. });
  285. // 输入区容器
  286. this.inputContainer = document.createElement('div');
  287. this.menuContainer.appendChild(this.inputContainer);
  288. this.loadSavedData(); // 加载本地保存的菜单项
  289. this.applyAutoClick(); // 启动自动点击主循环
  290. };
  291. }
  292.  
  293. /**
  294. * 注入样式到页面
  295. */
  296. createStyles() {
  297. const style = document.createElement('style');
  298. style.innerHTML = AUTO_CLICK_MENU_CSS;
  299. document.head.appendChild(style);
  300. }
  301.  
  302. /**
  303. * 创建菜单主容器
  304. * @returns {HTMLDivElement} 菜单容器元素
  305. */
  306. createMenuContainer() {
  307. const menuContainer = document.createElement('div');
  308. menuContainer.className = 'yuohira-container';
  309. menuContainer.style.position = 'fixed';
  310. menuContainer.style.top = '10px';
  311. menuContainer.style.right = '10px';
  312. menuContainer.style.zIndex = '10000';
  313. menuContainer.style.display = 'none';
  314. menuContainer.style.opacity = '0';
  315. menuContainer.style.transform = 'translateY(-20px)';
  316. document.body.appendChild(menuContainer);
  317. // 阻止冒泡,防止误触页面其它元素
  318. menuContainer.addEventListener('click', (e) => {
  319. e.stopPropagation();
  320. });
  321. return menuContainer;
  322. }
  323.  
  324. /**
  325. * 添加菜单标题
  326. * @param {HTMLElement} container 目标容器
  327. */
  328. addMenuTitle(container) {
  329. const menuTitle = document.createElement('h3');
  330. menuTitle.innerText = '自动点击菜单';
  331. menuTitle.className = 'yuohira-title';
  332. container.appendChild(menuTitle);
  333. }
  334.  
  335. /**
  336. * 添加按钮
  337. * @param {HTMLElement} container 按钮父容器
  338. * @param {string} text 按钮文本
  339. * @param {string} className 按钮样式类
  340. * @param {function} onClick 点击回调
  341. * @returns {HTMLButtonElement}
  342. */
  343. addButton(container, text, className, onClick) {
  344. const button = document.createElement('button');
  345. button.innerText = text;
  346. button.className = className;
  347. button.addEventListener('click', onClick);
  348. container.appendChild(button);
  349. return button;
  350. }
  351.  
  352. /**
  353. * 加载本地保存的菜单项配置
  354. */
  355. loadSavedData() {
  356. const savedData = GM_getValue(this.currentUrl, []);
  357. savedData.forEach(item => {
  358. this.addInputField(item.type, item.value, item.enabled, item.interval, item.count);
  359. });
  360. }
  361.  
  362. /**
  363. * 保存当前菜单项配置到本地
  364. */
  365. saveData() {
  366. const data = this.menuItems.map(item => item.getData());
  367. GM_setValue(this.currentUrl, data);
  368. }
  369.  
  370. /**
  371. * 新增一个菜单项输入区
  372. * @param {string} type 目标类型
  373. * @param {string} value 目标值
  374. * @param {boolean} enabled 是否启用
  375. * @param {number} interval 间隔
  376. * @param {number} count 执行次数
  377. */
  378. addInputField(type = 'id', value = '', enabled = false, interval = 1000, count = -1) {
  379. const menuItem = new MenuItem(type, value, enabled, interval, this, count);
  380. this.menuItems.push(menuItem);
  381. this.inputContainer.appendChild(menuItem.createElement());
  382. }
  383.  
  384. /**
  385. * 自动点击主循环,定时遍历所有启用的菜单项并执行点击
  386. */
  387. applyAutoClick() {
  388. const autoClick = () => {
  389. if (this.autoClickEnabled && this.menuItems.some(item => item.isEnabled())) {
  390. const currentTime = Date.now();
  391. this.menuItems.forEach(item => item.autoClick(currentTime, this.lastUpdateTime));
  392. }
  393. requestAnimationFrame(autoClick);
  394. };
  395. requestAnimationFrame(autoClick);
  396. }
  397.  
  398. /**
  399. * 展开菜单,带动画
  400. */
  401. showMenu() {
  402. if (!this.menuContainer) return;
  403. this.menuContainer.style.display = 'block';
  404. this.menuContainer.style.overflow = 'hidden';
  405. // 先测量内容高度
  406. this.menuContainer.style.maxHeight = 'none';
  407. const fullHeight = this.menuContainer.scrollHeight;
  408. this.menuContainer.style.maxHeight = '0px';
  409. anime({
  410. targets: this.menuContainer,
  411. opacity: [0, 1],
  412. translateY: [-20, 0],
  413. maxHeight: [0, fullHeight],
  414. duration: 450,
  415. easing: 'easeOutCubic',
  416. update: anim => {
  417. // 防止高度动画卡住
  418. this.menuContainer.style.maxHeight = this.menuContainer.style.maxHeight;
  419. },
  420. complete: () => {
  421. this.menuContainer.style.opacity = '1';
  422. this.menuContainer.style.transform = 'translateY(0)';
  423. this.menuContainer.style.maxHeight = 'none';
  424. this.menuContainer.style.overflow = '';
  425. }
  426. });
  427. }
  428.  
  429. /**
  430. * 收起菜单,带动画
  431. */
  432. hideMenu() {
  433. if (!this.menuContainer) return;
  434. const fullHeight = this.menuContainer.scrollHeight;
  435. this.menuContainer.style.overflow = 'hidden';
  436. this.menuContainer.style.maxHeight = fullHeight + 'px';
  437. anime({
  438. targets: this.menuContainer,
  439. opacity: [1, 0],
  440. translateY: [0, -20],
  441. maxHeight: [fullHeight, 0],
  442. duration: 350,
  443. easing: 'easeInCubic',
  444. complete: () => {
  445. this.menuContainer.style.display = 'none';
  446. this.menuContainer.style.opacity = '0';
  447. this.menuContainer.style.transform = 'translateY(-20px)';
  448. this.menuContainer.style.maxHeight = '0px';
  449. this.menuContainer.style.overflow = '';
  450. }
  451. });
  452. }
  453. }
  454.  
  455. // --- MenuItem.js ---
  456. // == 菜单项配置类(MenuItem)==
  457. // 负责单个自动点击目标的输入、启用、间隔、次数、进度、选取等功能
  458. class MenuItem {
  459. /**
  460. * 构造函数
  461. * @param {string} type 目标类型(id/class/text/position)
  462. * @param {string} value 目标值
  463. * @param {boolean} enabled 是否启用
  464. * @param {number} interval 间隔时间
  465. * @param {AutoClickMenu} menu 主菜单实例
  466. * @param {number} count 执行次数,-1为无限
  467. */
  468. constructor(type, value, enabled, interval, menu, count = -1) {
  469. this.type = type;
  470. this.value = value;
  471. this.enabled = enabled;
  472. this.interval = interval;
  473. this.menu = menu;
  474. this.count = (typeof count === "number" ? count : -1);
  475. }
  476.  
  477. /**
  478. * 创建菜单项输入区 DOM 元素,包含类型选择、目标输入、启用/暂停、间隔、次数、进度、警告、删除等
  479. * @returns {HTMLDivElement}
  480. */
  481. createElement() {
  482. const MIN_INTERVAL = 1;
  483. const inputWrapper = document.createElement('div');
  484. inputWrapper.className = 'yuohira-input-wrapper';
  485.  
  486. // 下拉选择目标类型
  487. this.select = document.createElement('select');
  488. const optionId = document.createElement('option');
  489. optionId.value = 'id';
  490. optionId.innerText = 'ID';
  491. const optionClass = document.createElement('option');
  492. optionClass.value = 'class';
  493. optionClass.innerText = '类名';
  494. const optionText = document.createElement('option');
  495. optionText.value = 'text';
  496. optionText.innerText = '文本';
  497. const optionPosition = document.createElement('option');
  498. optionPosition.value = 'position';
  499. optionPosition.innerText = '位置';
  500. this.select.appendChild(optionId);
  501. this.select.appendChild(optionClass);
  502. this.select.appendChild(optionText);
  503. this.select.appendChild(optionPosition);
  504. this.select.value = this.type;
  505. this.select.className = 'yuohira-input';
  506. inputWrapper.appendChild(this.select);
  507.  
  508. // 目标输入框
  509. this.input = document.createElement('input');
  510. this.input.type = 'text';
  511. this.input.value = this.value;
  512. this.input.className = 'yuohira-input';
  513. this.input.placeholder = 'ID/类名/文本/坐标';
  514. inputWrapper.appendChild(this.input);
  515.  
  516. // 选取按钮(支持屏幕取点/元素高亮)
  517. this.selectButton = document.createElement('button');
  518. this.selectButton.innerText = '选取';
  519. this.selectButton.className = 'yuohira-button';
  520. this.selectButton.addEventListener('click', (e) => this.selectElement(e));
  521. inputWrapper.appendChild(this.selectButton);
  522.  
  523. // 类型切换时,动态调整输入提示和按钮状态
  524. this.select.addEventListener('change', () => {
  525. if (this.select.value === 'text') {
  526. this.selectButton.disabled = true;
  527. this.selectButton.style.backgroundColor = '#d3d3d3';
  528. this.selectButton.style.borderColor = '#a9a9a9';
  529. this.input.placeholder = '请输入文本';
  530. } else if (this.select.value === 'position') {
  531. this.selectButton.disabled = false;
  532. this.selectButton.style.backgroundColor = '';
  533. this.selectButton.style.borderColor = '';
  534. this.input.placeholder = '点击"选取"后屏幕定位';
  535. } else {
  536. this.selectButton.disabled = false;
  537. this.selectButton.style.backgroundColor = '';
  538. this.selectButton.style.borderColor = '';
  539. this.input.placeholder = '请输入ID/类名';
  540. }
  541. });
  542.  
  543. // 初始禁用选取按钮(文本模式)
  544. if (this.type === 'text') {
  545. this.selectButton.disabled = true;
  546. this.selectButton.style.backgroundColor = '#d3d3d3';
  547. this.selectButton.style.borderColor = '#a9a9a9';
  548. }
  549.  
  550. // 启用/暂停按钮
  551. this.toggleInputClickButton = document.createElement('button');
  552. this.toggleInputClickButton.className = 'yuohira-button toggle-input-click-button';
  553. this.toggleInputClickButton.innerText = this.enabled ? '暂停' : '开始';
  554. this.toggleInputClickButton.setAttribute('data-enabled', this.enabled);
  555. this.toggleInputClickButton.addEventListener('click', (e) => {
  556. e.stopPropagation();
  557. const isEnabled = this.toggleInputClickButton.innerText === '开始';
  558. this.toggleInputClickButton.innerText = isEnabled ? '暂停' : '开始';
  559. this.toggleInputClickButton.setAttribute('data-enabled', isEnabled);
  560. this.warningMsg && (this.warningMsg.style.display = 'none');
  561. });
  562. inputWrapper.appendChild(this.toggleInputClickButton);
  563.  
  564. // 间隔输入区
  565. const intervalWrapper = document.createElement('div');
  566. intervalWrapper.style.position = 'relative';
  567. intervalWrapper.style.display = 'inline-block';
  568. intervalWrapper.style.width = '100px';
  569. intervalWrapper.style.padding = '0px 140px 0px 0px';
  570.  
  571. this.intervalInput = document.createElement('input');
  572. this.intervalInput.type = 'number';
  573. this.intervalInput.value = this.interval;
  574. this.intervalInput.className = 'yuohira-input';
  575. this.intervalInput.placeholder = '间隔';
  576. this.intervalInput.style.paddingRight = '10px';
  577. this.intervalInput.style.width = '100px';
  578. this.intervalInput.min = MIN_INTERVAL;
  579. this.intervalInput.addEventListener('input', () => {
  580. let val = parseInt(this.intervalInput.value, 10) || 0;
  581. if (val < MIN_INTERVAL) {
  582. val = MIN_INTERVAL;
  583. this.intervalInput.value = MIN_INTERVAL;
  584. }
  585. this.interval = val;
  586. });
  587. intervalWrapper.appendChild(this.intervalInput);
  588.  
  589. // 间隔单位
  590. const intervalSuffix = document.createElement('span');
  591. intervalSuffix.innerText = 'ms';
  592. intervalSuffix.style.color = '#0099cc';
  593. intervalSuffix.style.position = 'absolute';
  594. intervalSuffix.style.right = '2px';
  595. intervalSuffix.style.top = '50%';
  596. intervalSuffix.style.transform = 'translateY(-50%)';
  597. intervalSuffix.style.pointerEvents = 'none';
  598. intervalSuffix.style.zIndex = '1';
  599. intervalWrapper.appendChild(intervalSuffix);
  600.  
  601. inputWrapper.appendChild(intervalWrapper);
  602.  
  603. // ==== 新增:执行次数输入框 ====
  604. this.countInput = document.createElement('input');
  605. this.countInput.type = 'number';
  606. this.countInput.value = this.count;
  607. this.countInput.className = 'yuohira-input';
  608. this.countInput.style.width = '60px';
  609. this.countInput.style.marginLeft = '8px';
  610. this.countInput.placeholder = '-1为无限';
  611. this.countInput.title = '执行次数,-1为无限';
  612. this.countInput.addEventListener('input', () => {
  613. let val = parseInt(this.countInput.value, 10);
  614. if (isNaN(val)) val = -1;
  615. this.count = val;
  616. });
  617. inputWrapper.appendChild(this.countInput);
  618.  
  619. // 次数单位
  620. const countLabel = document.createElement('span');
  621. countLabel.innerText = '次';
  622. countLabel.style.color = '#0099cc';
  623. countLabel.style.marginLeft = '2px';
  624. inputWrapper.appendChild(countLabel);
  625. // ==== 新增结束 ====
  626.  
  627. // 进度条
  628. this.progressBar = document.createElement('div');
  629. this.progressBar.className = 'yuohira-progress-bar';
  630. inputWrapper.appendChild(this.progressBar);
  631.  
  632. // 警告信息
  633. this.warningMsg = document.createElement('div');
  634. this.warningMsg.className = 'yuohira-warning';
  635. this.warningMsg.style.display = 'none';
  636. inputWrapper.appendChild(this.warningMsg);
  637.  
  638. // 删除按钮
  639. const removeButton = document.createElement('button');
  640. removeButton.innerText = '-';
  641. removeButton.className = 'yuohira-button';
  642. removeButton.addEventListener('click', () => {
  643. inputWrapper.remove();
  644. this.menu.menuItems = this.menu.menuItems.filter(item => item !== this);
  645. });
  646. inputWrapper.appendChild(removeButton);
  647.  
  648. return inputWrapper;
  649. }
  650.  
  651. /**
  652. * 选取目标元素或屏幕坐标
  653. * @param {Event} event
  654. */
  655. selectElement(event) {
  656. event.stopPropagation();
  657. if (this.select.value === 'position') {
  658. // 显示全屏十字准星
  659. this.showCrosshairSelector();
  660. return;
  661. }
  662. document.body.style.cursor = 'crosshair';
  663. this.selectButton.disabled = true;
  664.  
  665. // 悬浮提示框
  666. const hoverBox = document.createElement('div');
  667. hoverBox.style.position = 'fixed';
  668. hoverBox.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
  669. hoverBox.style.color = 'white';
  670. hoverBox.style.padding = '5px';
  671. hoverBox.style.borderRadius = '5px';
  672. hoverBox.style.pointerEvents = 'none';
  673. hoverBox.style.zIndex = '10002';
  674. document.body.appendChild(hoverBox);
  675.  
  676. // 鼠标移动时高亮元素并显示提示
  677. const mouseMoveHandler = (e) => {
  678. const elements = document.elementsFromPoint(e.clientX, e.clientY);
  679. elements.forEach((el) => {
  680. el.style.outline = '2px solid red';
  681. });
  682. document.addEventListener('mouseout', () => {
  683. elements.forEach((el) => {
  684. el.style.outline = '';
  685. });
  686. });
  687.  
  688. hoverBox.style.left = `${e.clientX + 10}px`;
  689. hoverBox.style.top = `${e.clientY + 10}px`;
  690. if (this.select.value === 'id' && elements[0].id) {
  691. hoverBox.innerText = `ID: ${elements[0].id}`;
  692. } else if (this.select.value === 'class' && elements[0].className) {
  693. hoverBox.innerText = `Class: ${elements[0].className}`;
  694. } else {
  695. hoverBox.innerText = '无ID或类名';
  696. }
  697. };
  698.  
  699. // 点击选中目标
  700. const clickHandler = (e) => {
  701. e.stopPropagation();
  702. e.preventDefault();
  703. const selectedElement = e.target;
  704. if (this.select.value === 'id' && selectedElement.id) {
  705. this.input.value = selectedElement.id;
  706. } else if (this.select.value === 'class' && selectedElement.className) {
  707. this.input.value = selectedElement.className;
  708. }
  709. document.body.style.cursor = 'default';
  710. document.removeEventListener('mousemove', mouseMoveHandler);
  711. document.removeEventListener('click', clickHandler, true);
  712. this.selectButton.disabled = false;
  713. document.body.removeChild(hoverBox);
  714. };
  715.  
  716. document.addEventListener('mousemove', mouseMoveHandler);
  717. document.addEventListener('click', clickHandler, true);
  718. }
  719.  
  720. /**
  721. * 屏幕取点模式,显示全屏遮罩和十字准星,点击后写入坐标
  722. */
  723. showCrosshairSelector() {
  724. // 创建全屏遮罩和十字准星
  725. const overlay = document.createElement('div');
  726. overlay.className = 'yuohira-crosshair-overlay';
  727.  
  728. // 横线
  729. const hLine = document.createElement('div');
  730. hLine.className = 'yuohira-crosshair-line';
  731. hLine.style.height = '1px';
  732. hLine.style.width = '100vw';
  733. hLine.style.top = '50%';
  734. hLine.style.left = '0';
  735. hLine.style.background = '#e74c3c';
  736.  
  737. // 竖线
  738. const vLine = document.createElement('div');
  739. vLine.className = 'yuohira-crosshair-line';
  740. vLine.style.width = '1px';
  741. vLine.style.height = '100vh';
  742. vLine.style.left = '50%';
  743. vLine.style.top = '0';
  744. vLine.style.background = '#e74c3c';
  745.  
  746. // 坐标显示
  747. const label = document.createElement('div');
  748. label.className = 'yuohira-crosshair-label';
  749. label.innerText = '点击以选取位置';
  750. label.style.left = '50%';
  751. label.style.top = '50%';
  752.  
  753. overlay.appendChild(hLine);
  754. overlay.appendChild(vLine);
  755. overlay.appendChild(label);
  756. document.body.appendChild(overlay);
  757.  
  758. // 鼠标移动时更新准星位置和坐标
  759. const moveHandler = (e) => {
  760. hLine.style.top = `${e.clientY}px`;
  761. vLine.style.left = `${e.clientX}px`;
  762. label.style.left = `${e.clientX + 10}px`;
  763. label.style.top = `${e.clientY + 10}px`;
  764. label.innerText = `X: ${e.clientX}, Y: ${e.clientY}`;
  765. };
  766.  
  767. overlay.addEventListener('mousemove', moveHandler);
  768.  
  769. // 点击写入坐标
  770. const clickHandler = (e) => {
  771. e.stopPropagation();
  772. e.preventDefault();
  773. this.input.value = `${e.clientX},${e.clientY}`;
  774. document.body.removeChild(overlay);
  775. overlay.removeEventListener('mousemove', moveHandler);
  776. overlay.removeEventListener('click', clickHandler);
  777. };
  778. overlay.addEventListener('click', clickHandler);
  779. }
  780.  
  781. /**
  782. * 根据文本查找页面元素
  783. * @param {string} text
  784. * @returns {Element[]}
  785. */
  786. findElementsByText(text) {
  787. const elements = document.querySelectorAll('*');
  788. const matchingElements = [];
  789. elements.forEach(element => {
  790. if (element.textContent.trim() === text) {
  791. matchingElements.push(element);
  792. }
  793. });
  794. return matchingElements;
  795. }
  796.  
  797. /**
  798. * 获取当前菜单项的所有配置数据
  799. * @returns {Object}
  800. */
  801. getData() {
  802. return {
  803. type: this.select.value,
  804. value: this.input.value,
  805. enabled: this.toggleInputClickButton.getAttribute('data-enabled') === 'true',
  806. interval: parseInt(this.intervalInput.value, 10),
  807. count: parseInt(this.countInput.value, 10)
  808. };
  809. }
  810.  
  811. /**
  812. * 判断当前菜单项是否启用
  813. * @returns {boolean}
  814. */
  815. isEnabled() {
  816. return this.toggleInputClickButton.getAttribute('data-enabled') === 'true';
  817. }
  818.  
  819. /**
  820. * 模拟鼠标点击目标元素
  821. * @param {Element} element
  822. */
  823. simulateMouseClick(element) {
  824. const rect = element.getBoundingClientRect();
  825. const x = rect.left + rect.width / 2;
  826. const y = rect.top + rect.height / 2;
  827. const opts = { bubbles: true, cancelable: true, clientX: x, clientY: y };
  828.  
  829. element.dispatchEvent(new PointerEvent('pointerdown', opts));
  830. element.dispatchEvent(new MouseEvent('mousedown', opts));
  831. element.dispatchEvent(new PointerEvent('pointerup', opts));
  832. element.dispatchEvent(new MouseEvent('mouseup', opts));
  833. element.dispatchEvent(new MouseEvent('click', opts));
  834. }
  835.  
  836. /**
  837. * 自动点击主逻辑,定时查找目标并点击,支持次数、进度、异常提示
  838. * @param {number} currentTime 当前时间戳
  839. * @param {Map} lastUpdateTime 上次更新时间Map
  840. */
  841. autoClick(currentTime, lastUpdateTime) {
  842. if (typeof this.count !== 'number') this.count = -1;
  843. if (this.count === 0) return;
  844. if (!this.isEnabled()) return;
  845.  
  846. const lastTime = lastUpdateTime.get(this) || 0;
  847. const elapsedTime = currentTime - lastTime;
  848.  
  849. if (elapsedTime >= this.interval) {
  850. let elements = [];
  851. let inputVal = (this.input.value || '').trim();
  852. let clicked = false;
  853. if (this.select.value === 'id') {
  854. if (inputVal) {
  855. elements = Array.from(document.querySelectorAll(`#${CSS.escape(inputVal)}`));
  856. }
  857. } else if (this.select.value === 'class') {
  858. if (inputVal) {
  859. elements = Array.from(document.getElementsByClassName(inputVal));
  860. }
  861. } else if (this.select.value === 'text') {
  862. if (inputVal) {
  863. elements = this.findElementsByText(inputVal);
  864. }
  865. } else if (this.select.value === 'position') {
  866. const pos = inputVal.split(',');
  867. if (pos.length === 2) {
  868. const x = parseInt(pos[0].trim(), 10);
  869. const y = parseInt(pos[1].trim(), 10);
  870. if (!isNaN(x) && !isNaN(y)) {
  871. const el = document.elementFromPoint(x, y);
  872.  
  873. if (el && !this.menu.menuContainer.contains(el)) {
  874. elements = [el];
  875. }
  876. }
  877. }
  878. }
  879.  
  880. if (this.select.value !== 'position') {
  881. elements.forEach(element => {
  882. if (!this.menu.menuContainer.contains(element)) {
  883. this.simulateMouseClick(element);
  884. clicked = true;
  885. }
  886. });
  887. } else if (elements.length > 0) {
  888. this.simulateMouseClick(elements[0]);
  889. clicked = true;
  890. }
  891.  
  892. // 点击成功后减少次数
  893. if (clicked && this.count > 0) {
  894. this.count--;
  895. this.countInput.value = this.count;
  896. }
  897.  
  898. // 异常提示处理
  899. if (this.select.value !== 'position') {
  900. if (inputVal && elements.length === 0) {
  901. this.warningMsg.innerText = '未找到目标元素';
  902. this.warningMsg.style.display = 'block';
  903. } else {
  904. this.warningMsg.style.display = 'none';
  905. }
  906. } else {
  907. this.warningMsg.style.display = 'none';
  908. }
  909. this.progressBar.style.width = '100%';
  910. lastUpdateTime.set(this, currentTime);
  911. } else {
  912. let percent = (1 - elapsedTime / this.interval) * 100;
  913. if (percent < 0) percent = 0;
  914. if (percent > 100) percent = 100;
  915. this.progressBar.style.width = `${percent}%`;
  916. }
  917. }
  918. }
  919.  
  920. // --- ToggleButton.js ---
  921. // == 菜单右上角小圆球(ToggleButton)==
  922. // 负责菜单的显示/隐藏切换、拖动窗口、动效等
  923. class ToggleButton {
  924. /**
  925. * 构造函数
  926. * @param {AutoClickMenu} menu 主菜单实例
  927. */
  928. constructor(menu) {
  929. this.menu = menu;
  930. }
  931.  
  932. /**
  933. * 创建小圆球 DOM 元素,并绑定点击/拖动等事件
  934. * @returns {HTMLButtonElement}
  935. */
  936. createElement() {
  937. const toggleButton = document.createElement('button');
  938. toggleButton.innerText = '>';
  939. toggleButton.className = 'yuohira-toggle-button';
  940. // 固定定位,初始位置与菜单一致
  941. toggleButton.style.position = 'fixed';
  942. toggleButton.style.top = this.menu.menuContainer.style.top || '10px';
  943. toggleButton.style.right = this.menu.menuContainer.style.right || '10px';
  944. toggleButton.style.zIndex = '10001';
  945. toggleButton.style.width = '15px';
  946. toggleButton.style.height = '15px';
  947. toggleButton.style.fontSize = '10px';
  948. toggleButton.style.textAlign = 'center';
  949. toggleButton.style.lineHeight = '15px';
  950. toggleButton.style.padding = '0';
  951. toggleButton.style.boxSizing = 'border-box';
  952. toggleButton.style.display = 'flex';
  953. toggleButton.style.alignItems = 'center';
  954. toggleButton.style.justifyContent = 'center';
  955.  
  956. document.body.appendChild(toggleButton);
  957.  
  958. // 点击切换菜单显隐,带缩放动画
  959. toggleButton.addEventListener('click', (e) => {
  960. e.stopPropagation();
  961. // 按钮缩放动画(anime.js,更丝滑)
  962. anime.remove(toggleButton);
  963. anime({
  964. targets: toggleButton,
  965. scale: [1, 1.18, 0.95, 1],
  966. duration: 320,
  967. easing: 'easeInOutCubic'
  968. });
  969. if (this.menu.menuContainer.style.display === 'none') {
  970. this.menu.showMenu();
  971. toggleButton.innerText = '<';
  972. } else {
  973. this.menu.hideMenu();
  974. toggleButton.innerText = '>';
  975. }
  976. });
  977.  
  978. // 拖动功能:按住小圆球可拖动菜单和小圆球整体
  979. let isDragging = false;
  980. let dragStartX = 0, dragStartY = 0;
  981. let offsetToCenterX = 0, offsetToCenterY = 0;
  982. let startTop = 0, startRight = 0;
  983. const btnRect = () => toggleButton.getBoundingClientRect();
  984. toggleButton.addEventListener('mousedown', (e) => {
  985. if (e.button !== 0) return; // 只响应左键
  986. isDragging = true;
  987. document.body.style.userSelect = 'none';
  988. toggleButton.style.cursor = 'move';
  989. this.menu.menuContainer.style.transition = 'none';
  990. toggleButton.style.transition = 'none';
  991. // 记录鼠标到小圆球中心的偏移
  992. const rect = btnRect();
  993. offsetToCenterX = e.clientX - (rect.left + rect.width / 2);
  994. offsetToCenterY = e.clientY - (rect.top + rect.height / 2);
  995. // 记录当前 top/right
  996. startTop = rect.top;
  997. startRight = window.innerWidth - rect.right;
  998. });
  999. document.addEventListener('mousemove', (e) => {
  1000. if (!isDragging) return;
  1001. // 让小圆球中心跟随鼠标
  1002. let newCenterX = e.clientX - offsetToCenterX;
  1003. let newCenterY = e.clientY - offsetToCenterY;
  1004. // 计算新 top/right
  1005. let btnWidth = btnRect().width;
  1006. let btnHeight = btnRect().height;
  1007. let newTop = newCenterY - btnHeight / 2;
  1008. let newRight = window.innerWidth - (newCenterX + btnWidth / 2);
  1009. // 限制范围:上下左右都要有 10px 间距
  1010. const minTop = 10;
  1011. const minRight = 10;
  1012. const maxTop = window.innerHeight - this.menu.menuContainer.offsetHeight - 10;
  1013. const maxRight = window.innerWidth - 60 - 10;
  1014. newTop = Math.max(minTop, Math.min(maxTop, newTop));
  1015. newRight = Math.max(minRight, Math.min(maxRight, newRight));
  1016. this.menu.menuContainer.style.top = newTop + 'px';
  1017. this.menu.menuContainer.style.right = newRight + 'px';
  1018. toggleButton.style.top = newTop + 'px';
  1019. toggleButton.style.right = newRight + 'px';
  1020. });
  1021. document.addEventListener('mouseup', () => {
  1022. if (isDragging) {
  1023. isDragging = false;
  1024. document.body.style.userSelect = '';
  1025. toggleButton.style.cursor = '';
  1026. this.menu.menuContainer.style.transition = '';
  1027. toggleButton.style.transition = '';
  1028. }
  1029. });
  1030.  
  1031. return toggleButton;
  1032. }
  1033. }
  1034.  
  1035. // --- index.js ---
  1036. // == 自动点击菜单入口 ==
  1037. // 仅负责实例化主控类,启动脚本
  1038. 'use strict';
  1039. new AutoClickMenu();
  1040.  
  1041. })();