Greasy Fork

自动点击菜单

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

目前为 2025-04-18 提交的版本。查看 最新版本

// ==UserScript==
// @name         自动点击菜单
// @namespace    http://tampermonkey.net/
// @version      1.47.1
// @description  自动点击菜单,支持按ID、类名、文本、位置自动点击,可设定执行次数
// @author       YuoHira
// @license      MIT
// @match        *://*/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=github.io
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

class AutoClickMenu {
    constructor() {
        this.currentUrl = window.location.origin;
        this.autoClickEnabled = GM_getValue(`${this.currentUrl}_autoClickEnabled`, false);
        this.lastUpdateTime = new Map();
        this.menuItems = [];
        this.init();
    }

    init() {
        window.onload = () => {
            this.createStyles();
            this.toggleButton = new ToggleButton(this).createElement();
            this.menuContainer = this.createMenuContainer();
            this.addMenuTitle(this.menuContainer);
            this.saveButton = this.addButton(this.menuContainer, '保存', 'yuohira-button', (e) => {
                e.stopPropagation();
                this.saveData();
            });
            this.addButtonElement = this.addButton(this.menuContainer, '+', 'yuohira-button', (e) => {
                e.stopPropagation();
                this.addInputField();
            });
            this.toggleAutoClickButton = this.addButton(this.menuContainer, this.autoClickEnabled ? '暂停' : '开始', 'yuohira-button', (e) => {
                e.stopPropagation();
                this.autoClickEnabled = !this.autoClickEnabled;
                this.toggleAutoClickButton.innerText = this.autoClickEnabled ? '暂停' : '开始';
                GM_setValue(`${this.currentUrl}_autoClickEnabled`, this.autoClickEnabled);
            });
            this.inputContainer = document.createElement('div');
            this.menuContainer.appendChild(this.inputContainer);
            this.loadSavedData();
            this.applyAutoClick();
        };
    }

    createStyles() {
        const style = document.createElement('style');
        style.innerHTML = `
            .yuohira-button {
                background-color: #6cb2e8;
                border: 1px solid #0099cc;
                color: #fff;
                border-radius: 5px;
                padding: 5px 10px;
                cursor: pointer;
                font-size: 14px;
                margin: 5px;
                box-shadow: 0 0 10px #6cb2e8;
            }
            .yuohira-button:hover {
                background-color: #0099cc;
            }
            .yuohira-container {
                background-color: #b2ebf2;
                border: 1px solid #0099cc;
                border-radius: 10px;
                padding: 10px;
                box-shadow: 0 0 20px #6cb2e8;
                display: flex;
                flex-direction: column;
                align-items: center;
            }
            .yuohira-title {
                color: #0099cc;
                font-family: 'Courier New', Courier, monospace;
                margin-bottom: 10px;
            }
            .yuohira-input {
                border: 1px solid #0099cc;
                border-radius: 5px;
                padding: 5px;
                margin: 5px;
                background-color: #a0d3e0;
                color: #0099cc;
            }
            .yuohira-toggle-button {
                background-color: #6cb2e8;
                border: 1px solid #0099cc;
                color: #fff;
                border-radius: 50%;
                padding: 5px;
                cursor: pointer;
                font-size: 14px;
                width: 30px;
                height: 30px;
                position: fixed;
                top: 10px;
                right: 10px;
                z-index: 10001;
                opacity: 0.5;
                transition: opacity 0.3s;
                box-shadow: 0 0 10px #6cb2e8;
            }
            .yuohira-toggle-button:hover {
                opacity: 1;
            }
            .yuohira-input-wrapper {
                display: flex;
                align-items: center;
                margin-bottom: 5px;
                position: relative;
                padding-bottom: 18px;
            }
            .yuohira-progress-bar {
                height: 5px;
                position: absolute;
                bottom: 0;
                left: 0;
                background-color: #6cb2e8;
                clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
                border-radius: 2.5px;
            }
            .yuohira-warning {
                color: #e74c3c;
                font-size: 12px;
                position: absolute;
                left: 0;
                bottom: -13px;
                width: 100%;
                text-align: left;
                z-index: 2;
                pointer-events: none;
            }
            .yuohira-crosshair-overlay {
                position: fixed;
                top: 0; left: 0; right: 0; bottom: 0;
                z-index: 99999;
                pointer-events: auto;
                background: rgba(0,0,0,0.05);
            }
            .yuohira-crosshair-line {
                position: absolute;
                background: #e74c3c;
                z-index: 999999;
            }
            .yuohira-crosshair-label {
                position: absolute;
                background: #222;
                color: #fff;
                font-size: 12px;
                padding: 2px 6px;
                border-radius: 3px;
                z-index: 999999;
                pointer-events: none;
                transform: translateY(-150%);
            }
        `;
        document.head.appendChild(style);
    }

    createMenuContainer() {
        const menuContainer = document.createElement('div');
        menuContainer.className = 'yuohira-container';
        menuContainer.style.position = 'fixed';
        menuContainer.style.top = '10px';
        menuContainer.style.right = '10px';
        menuContainer.style.zIndex = '10000';
        menuContainer.style.display = 'none';
        document.body.appendChild(menuContainer);
        menuContainer.addEventListener('click', (e) => {
            e.stopPropagation();
        });
        return menuContainer;
    }

    addMenuTitle(container) {
        const menuTitle = document.createElement('h3');
        menuTitle.innerText = '自动点击菜单';
        menuTitle.className = 'yuohira-title';
        container.appendChild(menuTitle);
    }

    addButton(container, text, className, onClick) {
        const button = document.createElement('button');
        button.innerText = text;
        button.className = className;
        button.addEventListener('click', onClick);
        container.appendChild(button);
        return button;
    }

    loadSavedData() {
        const savedData = GM_getValue(this.currentUrl, []);
        savedData.forEach(item => {
            this.addInputField(item.type, item.value, item.enabled, item.interval, item.count);
        });
    }

    saveData() {
        const data = this.menuItems.map(item => item.getData());
        GM_setValue(this.currentUrl, data);
    }

    addInputField(type = 'id', value = '', enabled = false, interval = 1000, count = -1) {
        const menuItem = new MenuItem(type, value, enabled, interval, this, count);
        this.menuItems.push(menuItem);
        this.inputContainer.appendChild(menuItem.createElement());
    }

    applyAutoClick() {
        const autoClick = () => {
            if (this.autoClickEnabled && this.menuItems.some(item => item.isEnabled())) {
                const currentTime = Date.now();
                this.menuItems.forEach(item => item.autoClick(currentTime, this.lastUpdateTime));
            }
            requestAnimationFrame(autoClick);
        };
        requestAnimationFrame(autoClick);
    }
}

class MenuItem {
    constructor(type, value, enabled, interval, menu, count = -1) {
        this.type = type;
        this.value = value;
        this.enabled = enabled;
        this.interval = interval;
        this.menu = menu;
        this.count = (typeof count === "number" ? count : -1);
    }

    createElement() {
        const MIN_INTERVAL = 1;
        const inputWrapper = document.createElement('div');
        inputWrapper.className = 'yuohira-input-wrapper';

        this.select = document.createElement('select');
        const optionId = document.createElement('option');
        optionId.value = 'id';
        optionId.innerText = 'ID';
        const optionClass = document.createElement('option');
        optionClass.value = 'class';
        optionClass.innerText = '类名';
        const optionText = document.createElement('option');
        optionText.value = 'text';
        optionText.innerText = '文本';
        const optionPosition = document.createElement('option');
        optionPosition.value = 'position';
        optionPosition.innerText = '位置';
        this.select.appendChild(optionId);
        this.select.appendChild(optionClass);
        this.select.appendChild(optionText);
        this.select.appendChild(optionPosition);
        this.select.value = this.type;
        this.select.className = 'yuohira-input';
        inputWrapper.appendChild(this.select);

        this.input = document.createElement('input');
        this.input.type = 'text';
        this.input.value = this.value;
        this.input.className = 'yuohira-input';
        this.input.placeholder = 'ID/类名/文本/坐标';
        inputWrapper.appendChild(this.input);

        this.selectButton = document.createElement('button');
        this.selectButton.innerText = '选取';
        this.selectButton.className = 'yuohira-button';
        this.selectButton.addEventListener('click', (e) => this.selectElement(e));
        inputWrapper.appendChild(this.selectButton);

        this.select.addEventListener('change', () => {
            if (this.select.value === 'text') {
                this.selectButton.disabled = true;
                this.selectButton.style.backgroundColor = '#d3d3d3';
                this.selectButton.style.borderColor = '#a9a9a9';
                this.input.placeholder = '请输入文本';
            } else if (this.select.value === 'position') {
                this.selectButton.disabled = false;
                this.selectButton.style.backgroundColor = '';
                this.selectButton.style.borderColor = '';
                this.input.placeholder = '点击“选取”后屏幕定位';
            } else {
                this.selectButton.disabled = false;
                this.selectButton.style.backgroundColor = '';
                this.selectButton.style.borderColor = '';
                this.input.placeholder = '请输入ID/类名';
            }
        });

        if (this.type === 'text') {
            this.selectButton.disabled = true;
            this.selectButton.style.backgroundColor = '#d3d3d3';
            this.selectButton.style.borderColor = '#a9a9a9';
        }

        this.toggleInputClickButton = document.createElement('button');
        this.toggleInputClickButton.className = 'yuohira-button toggle-input-click-button';
        this.toggleInputClickButton.innerText = this.enabled ? '暂停' : '开始';
        this.toggleInputClickButton.setAttribute('data-enabled', this.enabled);
        this.toggleInputClickButton.addEventListener('click', (e) => {
            e.stopPropagation();
            const isEnabled = this.toggleInputClickButton.innerText === '开始';
            this.toggleInputClickButton.innerText = isEnabled ? '暂停' : '开始';
            this.toggleInputClickButton.setAttribute('data-enabled', isEnabled);
            this.warningMsg && (this.warningMsg.style.display = 'none');
        });
        inputWrapper.appendChild(this.toggleInputClickButton);

        const intervalWrapper = document.createElement('div');
        intervalWrapper.style.position = 'relative';
        intervalWrapper.style.display = 'inline-block';
        intervalWrapper.style.width = '100px';
        intervalWrapper.style.padding = '0px 140px 0px 0px';

        this.intervalInput = document.createElement('input');
        this.intervalInput.type = 'number';
        this.intervalInput.value = this.interval;
        this.intervalInput.className = 'yuohira-input';
        this.intervalInput.placeholder = '间隔';
        this.intervalInput.style.paddingRight = '10px';
        this.intervalInput.style.width = '100px';
        this.intervalInput.min = MIN_INTERVAL;
        this.intervalInput.addEventListener('input', () => {
            let val = parseInt(this.intervalInput.value, 10) || 0;
            if (val < MIN_INTERVAL) {
                val = MIN_INTERVAL;
                this.intervalInput.value = MIN_INTERVAL;
            }
            this.interval = val;
        });
        intervalWrapper.appendChild(this.intervalInput);

        const intervalSuffix = document.createElement('span');
        intervalSuffix.innerText = 'ms';
        intervalSuffix.style.color = '#0099cc';
        intervalSuffix.style.position = 'absolute';
        intervalSuffix.style.right = '10px';
        intervalSuffix.style.top = '50%';
        intervalSuffix.style.transform = 'translateY(-50%)';
        intervalSuffix.style.pointerEvents = 'none';
        intervalSuffix.style.zIndex = '1';
        intervalWrapper.appendChild(intervalSuffix);

        inputWrapper.appendChild(intervalWrapper);

        // ==== 新增:执行次数输入框 ====
        this.countInput = document.createElement('input');
        this.countInput.type = 'number';
        this.countInput.value = this.count;
        this.countInput.className = 'yuohira-input';
        this.countInput.style.width = '60px';
        this.countInput.style.marginLeft = '8px';
        this.countInput.placeholder = '-1为无限';
        this.countInput.title = '执行次数,-1为无限';
        this.countInput.addEventListener('input', () => {
            let val = parseInt(this.countInput.value, 10);
            if (isNaN(val)) val = -1;
            this.count = val;
        });
        inputWrapper.appendChild(this.countInput);

        const countLabel = document.createElement('span');
        countLabel.innerText = '次';
        countLabel.style.color = '#0099cc';
        countLabel.style.marginLeft = '2px';
        inputWrapper.appendChild(countLabel);
        // ==== 新增结束 ====

        this.progressBar = document.createElement('div');
        this.progressBar.className = 'yuohira-progress-bar';
        inputWrapper.appendChild(this.progressBar);

        // 警告信息
        this.warningMsg = document.createElement('div');
        this.warningMsg.className = 'yuohira-warning';
        this.warningMsg.style.display = 'none';
        inputWrapper.appendChild(this.warningMsg);

        const removeButton = document.createElement('button');
        removeButton.innerText = '-';
        removeButton.className = 'yuohira-button';
        removeButton.addEventListener('click', () => {
            inputWrapper.remove();
            this.menu.menuItems = this.menu.menuItems.filter(item => item !== this);
        });
        inputWrapper.appendChild(removeButton);

        return inputWrapper;
    }

    selectElement(event) {
        event.stopPropagation();
        if (this.select.value === 'position') {
            // 显示全屏十字准星
            this.showCrosshairSelector();
            return;
        }
        document.body.style.cursor = 'crosshair';
        this.selectButton.disabled = true;

        const hoverBox = document.createElement('div');
        hoverBox.style.position = 'fixed';
        hoverBox.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
        hoverBox.style.color = 'white';
        hoverBox.style.padding = '5px';
        hoverBox.style.borderRadius = '5px';
        hoverBox.style.pointerEvents = 'none';
        hoverBox.style.zIndex = '10002';
        document.body.appendChild(hoverBox);

        const mouseMoveHandler = (e) => {
            const elements = document.elementsFromPoint(e.clientX, e.clientY);
            elements.forEach((el) => {
                el.style.outline = '2px solid red';
            });
            document.addEventListener('mouseout', () => {
                elements.forEach((el) => {
                    el.style.outline = '';
                });
            });

            hoverBox.style.left = `${e.clientX + 10}px`;
            hoverBox.style.top = `${e.clientY + 10}px`;
            if (this.select.value === 'id' && elements[0].id) {
                hoverBox.innerText = `ID: ${elements[0].id}`;
            } else if (this.select.value === 'class' && elements[0].className) {
                hoverBox.innerText = `Class: ${elements[0].className}`;
            } else {
                hoverBox.innerText = '无ID或类名';
            }
        };

        const clickHandler = (e) => {
            e.stopPropagation();
            e.preventDefault();
            const selectedElement = e.target;
            if (this.select.value === 'id' && selectedElement.id) {
                this.input.value = selectedElement.id;
            } else if (this.select.value === 'class' && selectedElement.className) {
                this.input.value = selectedElement.className;
            }
            document.body.style.cursor = 'default';
            document.removeEventListener('mousemove', mouseMoveHandler);
            document.removeEventListener('click', clickHandler, true);
            this.selectButton.disabled = false;
            document.body.removeChild(hoverBox);
        };

        document.addEventListener('mousemove', mouseMoveHandler);
        document.addEventListener('click', clickHandler, true);
    }

    showCrosshairSelector() {
        // 创建全屏遮罩和十字准星
        const overlay = document.createElement('div');
        overlay.className = 'yuohira-crosshair-overlay';

        // 横线
        const hLine = document.createElement('div');
        hLine.className = 'yuohira-crosshair-line';
        hLine.style.height = '1px';
        hLine.style.width = '100vw';
        hLine.style.top = '50%';
        hLine.style.left = '0';
        hLine.style.background = '#e74c3c';

        // 竖线
        const vLine = document.createElement('div');
        vLine.className = 'yuohira-crosshair-line';
        vLine.style.width = '1px';
        vLine.style.height = '100vh';
        vLine.style.left = '50%';
        vLine.style.top = '0';
        vLine.style.background = '#e74c3c';

        // 坐标显示
        const label = document.createElement('div');
        label.className = 'yuohira-crosshair-label';
        label.innerText = '点击以选取位置';
        label.style.left = '50%';
        label.style.top = '50%';

        overlay.appendChild(hLine);
        overlay.appendChild(vLine);
        overlay.appendChild(label);
        document.body.appendChild(overlay);

        // 鼠标移动时更新准星位置和坐标
        const moveHandler = (e) => {
            hLine.style.top = `${e.clientY}px`;
            vLine.style.left = `${e.clientX}px`;
            label.style.left = `${e.clientX + 10}px`;
            label.style.top = `${e.clientY + 10}px`;
            label.innerText = `X: ${e.clientX}, Y: ${e.clientY}`;
        };

        overlay.addEventListener('mousemove', moveHandler);

        const clickHandler = (e) => {
            e.stopPropagation();
            e.preventDefault();
            this.input.value = `${e.clientX},${e.clientY}`;
            document.body.removeChild(overlay);
            overlay.removeEventListener('mousemove', moveHandler);
            overlay.removeEventListener('click', clickHandler);
        };
        overlay.addEventListener('click', clickHandler);
    }

    findElementsByText(text) {
        const elements = document.querySelectorAll('*');
        const matchingElements = [];
        elements.forEach(element => {
            if (element.textContent.trim() === text) {
                matchingElements.push(element);
            }
        });
        return matchingElements;
    }

    getData() {
        return {
            type: this.select.value,
            value: this.input.value,
            enabled: this.toggleInputClickButton.getAttribute('data-enabled') === 'true',
            interval: parseInt(this.intervalInput.value, 10),
            count: parseInt(this.countInput.value, 10)
        };
    }

    isEnabled() {
        return this.toggleInputClickButton.getAttribute('data-enabled') === 'true';
    }

    simulateMouseClick(element) {
        const rect = element.getBoundingClientRect();
        const x = rect.left + rect.width / 2;
        const y = rect.top + rect.height / 2;
        const opts = { bubbles: true, cancelable: true, clientX: x, clientY: y };

        element.dispatchEvent(new PointerEvent('pointerdown', opts));
        element.dispatchEvent(new MouseEvent('mousedown', opts));
        element.dispatchEvent(new PointerEvent('pointerup', opts));
        element.dispatchEvent(new MouseEvent('mouseup', opts));
        element.dispatchEvent(new MouseEvent('click', opts));        
    }


    autoClick(currentTime, lastUpdateTime) {
        if (typeof this.count !== 'number') this.count = -1;
        if (this.count === 0) return;
        if (!this.isEnabled()) return;

        const lastTime = lastUpdateTime.get(this) || 0;
        const elapsedTime = currentTime - lastTime;

        if (elapsedTime >= this.interval) {
            let elements = [];
            let inputVal = (this.input.value || '').trim();
            let clicked = false;
            if (this.select.value === 'id') {
                if (inputVal) {
                    elements = Array.from(document.querySelectorAll(`#${CSS.escape(inputVal)}`));
                }
            } else if (this.select.value === 'class') {
                if (inputVal) {
                    elements = Array.from(document.getElementsByClassName(inputVal));
                }
            } else if (this.select.value === 'text') {
                if (inputVal) {
                    elements = this.findElementsByText(inputVal);
                }
            } else if (this.select.value === 'position') {
                const pos = inputVal.split(',');
                if (pos.length === 2) {
                    const x = parseInt(pos[0].trim(), 10);
                    const y = parseInt(pos[1].trim(), 10);
                    if (!isNaN(x) && !isNaN(y)) {
                        const el = document.elementFromPoint(x, y);

                        if (el && !this.menu.menuContainer.contains(el)) {
                            elements = [el];
                        }
                    }
                }
            }

            if (this.select.value !== 'position') {
                elements.forEach(element => {
                    if (!this.menu.menuContainer.contains(element)) {
                        this.simulateMouseClick(element);
                        clicked = true;
                    }
                });
            } else if (elements.length > 0) {
                this.simulateMouseClick(elements[0]);
                clicked = true;
            }

            // 点击成功后减少次数
            if (clicked && this.count > 0) {
                this.count--;
                this.countInput.value = this.count;
            }

            // 异常提示处理
            if (this.select.value !== 'position') {
                if (inputVal && elements.length === 0) {
                    this.warningMsg.innerText = '未找到目标元素';
                    this.warningMsg.style.display = 'block';
                } else {
                    this.warningMsg.style.display = 'none';
                }
            } else {
                this.warningMsg.style.display = 'none';
            }
            this.progressBar.style.width = '100%';
            lastUpdateTime.set(this, currentTime);
        } else {
            let percent = (1 - elapsedTime / this.interval) * 100;
            if (percent < 0) percent = 0;
            if (percent > 100) percent = 100;
            this.progressBar.style.width = `${percent}%`;
        }
    }
}

class ToggleButton {
    constructor(menu) {
        this.menu = menu;
    }

    createElement() {
        const toggleButton = document.createElement('button');
        toggleButton.innerText = '>';
        toggleButton.className = 'yuohira-toggle-button';
        toggleButton.style.width = '15px';
        toggleButton.style.height = '15px';
        toggleButton.style.fontSize = '10px';
        toggleButton.style.textAlign = 'center';
        toggleButton.style.lineHeight = '15px';
        toggleButton.style.padding = '0';
        toggleButton.style.boxSizing = 'border-box';
        toggleButton.style.display = 'flex';
        toggleButton.style.alignItems = 'center';
        toggleButton.style.justifyContent = 'center';

        document.body.appendChild(toggleButton);

        toggleButton.addEventListener('click', (e) => {
            e.stopPropagation();
            if (this.menu.menuContainer.style.display === 'none') {
                this.menu.menuContainer.style.display = 'block';
                toggleButton.innerText = '<';
            } else {
                this.menu.menuContainer.style.display = 'none';
                toggleButton.innerText = '>';
            }
        });

        return toggleButton;
    }
}

(function () {
    'use strict';
    new AutoClickMenu();
})();