自动点击菜单

添加菜单,允许用户指定ID、类名或文本进行自动点击,并基于网址保存不同选项

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         自动点击菜单
// @namespace    http://tampermonkey.net/
// @version      1.42
// @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; // 当前网址的根URL
        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: 10px;
            }
            .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;
            }
        `;
        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); // 为每个保存的数据项添加输入字段
        });
    }

    saveData() {
        const data = this.menuItems.map(item => item.getData()); // 获取所有菜单项的数据
        GM_setValue(this.currentUrl, data); // 保存数据
    }

    addInputField(type = 'id', value = '', enabled = false, interval = 1000) {
        const menuItem = new MenuItem(type, value, enabled, interval, this); // 创建新的菜单项
        this.menuItems.push(menuItem); // 将菜单项添加到菜单项数组中
        this.inputContainer.appendChild(menuItem.createElement()); // 将菜单项的元素添加到输入容器中
    }

    applyAutoClick() {
        const autoClick = () => {
            if (this.autoClickEnabled) { // 如果自动点击功能启用
                const currentTime = Date.now(); // 获取当前时间
                this.menuItems.forEach(item => item.autoClick(currentTime, this.lastUpdateTime)); // 为每个菜单项应用自动点击功能
            }
            requestAnimationFrame(autoClick); // 请求下一帧执行autoClick函数
        };

        requestAnimationFrame(autoClick); // 请求第一帧执行autoClick函数
    }
}

class MenuItem {
    constructor(type, value, enabled, interval, menu) {
        this.type = type; // 元素类型(id、class、text)
        this.value = value; // 元素值
        this.enabled = enabled; // 是否启用自动点击
        this.interval = interval; // 自动点击间隔时间
        this.menu = menu; // 关联的菜单对象
    }

    createElement() {
        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 = '文本';
        this.select.appendChild(optionId);
        this.select.appendChild(optionClass);
        this.select.appendChild(optionText);
        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';
        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';
            } else {
                this.selectButton.disabled = false; // 启用选取按钮
                this.selectButton.style.backgroundColor = '';
                this.selectButton.style.borderColor = '';
            }
        });

        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); // 设置按钮的data-enabled属性
        this.toggleInputClickButton.addEventListener('click', (e) => {
            e.stopPropagation();
            const isEnabled = this.toggleInputClickButton.innerText === '开始';
            this.toggleInputClickButton.innerText = isEnabled ? '暂停' : '开始'; // 切换按钮文本
            this.toggleInputClickButton.setAttribute('data-enabled', isEnabled); // 切换data-enabled属性
        });
        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 80px 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 = '30px';
        this.intervalInput.style.width = '100%';
        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.progressBar = document.createElement('div');
        this.progressBar.className = 'yuohira-progress-bar';
        inputWrapper.appendChild(this.progressBar);

        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();
        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}`; // 显示元素的ID
            } else if (this.select.value === 'class' && elements[0].className) {
                hoverBox.innerText = `Class: ${elements[0].className}`; // 显示元素的类名
            } else {
                hoverBox.innerText = '无ID或类名'; // 显示无ID或类名
            }
        };

        const clickHandler = (e) => {
            e.stopPropagation();
            e.preventDefault();
            const selectedElement = e.target;
            if (this.select.value === 'id' && selectedElement.id) {
                this.input.value = selectedElement.id; // 设置输入框的值为选中元素的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); // 添加点击事件监听器
    }

    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) // 返回自动点击间隔时间
        };
    }

    autoClick(currentTime, lastUpdateTime) {
        if (this.toggleInputClickButton.getAttribute('data-enabled') === 'true') { // 如果启用自动点击
            const lastTime = lastUpdateTime.get(this) || 0; // 获取上次更新时间
            const elapsedTime = currentTime - lastTime; // 计算已过去的时间

            if (elapsedTime >= this.interval) { // 如果已过去的时间大于等于间隔时间
                let elements = [];
                if (this.select.value === 'id') {
                    elements = Array.from(document.querySelectorAll(`#${this.input.value}`)); // 根据ID获取所有元素
                } else if (this.select.value === 'class') {
                    elements = Array.from(document.getElementsByClassName(this.input.value)); // 根据类名获取所有元素
                } else if (this.select.value === 'text') {
                    elements = this.findElementsByText(this.input.value); // 根据文本获取所有匹配的元素
                }
                elements.forEach(element => {
                    if (typeof element.click === 'function' && !this.menu.menuContainer.contains(element)) {
                        element.click(); // 如果元素存在且其click方法是一个函数,并且该元素不在菜单容器内,则执行点击操作
                    }
                });
                this.progressBar.style.width = '100%'; // 将进度条的宽度设置为100%
                lastUpdateTime.set(this, currentTime); // 更新最后的点击时间为当前时间
            } else {
                this.progressBar.style.width = `${(1 - elapsedTime / this.interval) * 100}%`; // 根据已过去的时间更新进度条的宽度
            }
        }
    }
}

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'; // 设置按钮的内边距为0
        toggleButton.style.boxSizing = 'border-box'; // 设置按钮的盒模型为border-box
        toggleButton.style.display = 'flex'; // 设置按钮的显示方式为flex
        toggleButton.style.alignItems = 'center'; // 设置按钮的垂直对齐方式为居中
        toggleButton.style.justifyContent = 'center'; // 设置按钮的水平对齐方式为居中

        document.body.appendChild(toggleButton); // 将按钮添加到文档的body中

        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; // 返回创建的按钮元素
    }
}

class InputField {
    constructor(type, value) {
        this.type = type; // 保存输入字段的类型
        this.value = value; // 保存输入字段的值
    }

    createElement() {
        const inputWrapper = document.createElement('div'); // 创建一个div元素作为输入字段的容器
        inputWrapper.className = 'yuohira-input-wrapper'; // 设置容器的类名

        this.select = document.createElement('select'); // 创建一个select元素
        const optionId = document.createElement('option'); // 创建一个option元素
        optionId.value = 'id'; // 设置option的值为'id'
        optionId.innerText = 'ID'; // 设置option的文本为'ID'
        const optionClass = document.createElement('option'); // 创建另一个option元素
        optionClass.value = 'class'; // 设置option的值为'class'
        optionClass.innerText = '类名'; // 设置option的文本为'类名'
        const optionText = document.createElement('option'); // 创建第三个option元素
        optionText.value = 'text'; // 设置option的值为'text'
        optionText.innerText = '文本'; // 设置option的文本为'文本'
        this.select.appendChild(optionId); // 将第一个option添加到select中
        this.select.appendChild(optionClass); // 将第二个option添加到select中
        this.select.appendChild(optionText); // 将第三个option添加到select中
        this.select.value = this.type; // 设置select的值为当前输入字段的类型
        this.select.className = 'yuohira-input'; // 设置select的类名
        inputWrapper.appendChild(this.select); // 将select添加到容器中

        this.input = document.createElement('input'); // 创建一个input元素
        this.input.type = 'text'; // 设置input的类型为'text'
        this.input.value = this.value; // 设置input的值为当前输入字段的值
        this.input.className = 'yuohira-input'; // 设置input的类名
        inputWrapper.appendChild(this.input); // 将input添加到容器中

        return inputWrapper; // 返回创建的输入字段容器
    }
}

(function () {
    'use strict';
    new AutoClickMenu(); // 创建并初始化AutoClickMenu实例
})();