自动点击菜单

添加菜单,允许用户指定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实例
})();