Whcms表单填充助手

使用RandomUser API自动生成并填充网页注册表单。

当前为 2025-07-12 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Whcms表单填充助手
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  使用RandomUser API自动生成并填充网页注册表单。
// @author       Assistant (Enhanced by AI)
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @connect      randomuser.me
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 国家代码 -> 中文名
    const countryNamesCN = {
        'AU': '澳大利亚', 'BR': '巴西', 'CA': '加拿大', 'CH': '瑞士', 'DE': '德国',
        'DK': '丹麦', 'ES': '西班牙', 'FI': '芬兰', 'FR': '法国', 'GB': '英国',
        'IE': '爱尔兰', 'IN': '印度', 'IR': '伊朗', 'MX': '墨西哥', 'NL': '荷兰',
        'NO': '挪威', 'NZ': '新西兰', 'RS': '塞尔维亚', 'TR': '土耳其', 'UA': '乌克兰', 'US': '美国'
    };

    // 国家代码 -> 电话区号
    const countryPhoneCodes = {
        'AU': '+61', 'BR': '+55', 'CA': '+1', 'CH': '+41', 'DE': '+49', 'DK': '+45',
        'ES': '+34', 'FI': '+358', 'FR': '+33', 'GB': '+44', 'IE': '+353', 'IN': '+91',
        'IR': '+98', 'MX': '+52', 'NL': '+31', 'NO': '+47', 'NZ': '+64', 'RS': '+381',
        'TR': '+90', 'UA': '+380', 'US': '+1'
    };

    let currentUserData = null;

    GM_addStyle(`
        #form-filler-panel {
            position: fixed; top: 20px; right: 20px; width: 280px; background: #fff;
            border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            font-size: 14px;
        }
        #form-filler-header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px 15px;
            border-radius: 8px 8px 0 0; cursor: move; display: flex; justify-content: space-between; align-items: center;
        }
        #form-filler-content { padding: 15px; }
        .form-group { margin-bottom: 12px; }
        .form-group label { display: block; margin-bottom: 4px; font-weight: bold; color: #333; }
        .form-group select, .form-group button {
            width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box;
        }
        .form-group button {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; cursor: pointer;
            font-weight: bold; transition: all 0.3s;
        }
        .form-group button:hover { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); }
        .form-group button:disabled { background: #ccc; cursor: not-allowed; transform: none; box-shadow: none; }
        #user-preview {
            background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; padding: 10px; margin-top: 12px;
            font-size: 12px; max-height: 200px; overflow-y: auto;
        }
        .preview-item { margin-bottom: 4px; display: flex; justify-content: space-between; align-items: flex-start; }
        .preview-label { font-weight: bold; color: #495057; margin-right: 8px; white-space: nowrap; }
        .preview-value { color: #6c757d; text-align: right; max-width: 170px; word-break: break-all; }
        .close-btn { background: none; border: none; color: white; font-size: 18px; cursor: pointer; padding: 0; width: 24px; height: 24px; text-align: center; line-height: 24px; }
        #form-filler-toggle {
            position: fixed; /* Use fixed positioning for the button */
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white; border: none; padding: 10px 15px; border-radius: 20px; cursor: move; /* Change cursor to move */
            z-index: 9999; font-weight: bold; box-shadow: 0 2px 8px rgba(0,0,0,0.2); display: none;
        }
        .status-message { padding: 8px; border-radius: 4px; margin-top: 8px; text-align: center; font-size: 12px; }
        .status-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
        .status-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
        .status-loading { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
    `);

    const fieldMappings = {
        firstName: ['firstname', 'first_name', 'first-name', 'fname', 'given_name', 'givenname'],
        lastName: ['lastname', 'last_name', 'last-name', 'lname', 'surname', 'family_name', 'familyname'],
        fullName: ['fullname', 'full_name', 'full-name', 'name', 'username', 'user_name', 'user-name'],
        phone: ['phone', 'phonenumber', 'phone_number', 'phone-number', 'tel', 'telephone', 'mobile', 'cell'],
        address: ['address', 'street', 'streetaddress', 'street_address', 'street-address', 'address1', 'addr1'],
        city: ['city', 'town', 'locality'],
        state: ['state', 'province', 'region', 'stateprovince', 'state_province', 'state-province'],
        postcode: ['postcode', 'zipcode', 'zip', 'postal', 'postalcode', 'postal_code', 'postal-code', 'zip_code'],
        country: ['country', 'nation', 'nationality'],
        gender: ['gender', 'sex']
    };


    function createPanel() {
        const toggleButton = document.createElement('button');
        toggleButton.id = 'form-filler-toggle';
        toggleButton.textContent = '📝 表单填充';

        const savedPos = GM_getValue('buttonPosition', null);
        if (savedPos) {
            toggleButton.style.top = savedPos.top;
            toggleButton.style.left = savedPos.left;
            toggleButton.style.right = 'auto';
            toggleButton.style.bottom = 'auto';
        } else {
            toggleButton.style.top = '20px';
            toggleButton.style.right = '20px';
        }

        document.body.appendChild(toggleButton);

        const panel = document.createElement('div');
        panel.id = 'form-filler-panel';
        panel.style.display = 'none';
        panel.innerHTML = `
            <div id="form-filler-header"><span>📝 智能表单填充</span><button class="close-btn" id="close-panel">×</button></div>
            <div id="form-filler-content">
                <div class="form-group"><label for="country-select">选择国家/地区:</label><select id="country-select">${Object.entries(countryNamesCN).map(([code, name]) => `<option value="${code}">${name} (${code})</option>`).join('')}</select></div>
                <div class="form-group"><button id="generate-btn">🎲 生成用户信息</button></div>
                <div class="form-group"><button id="fill-btn" disabled>🖊️ 自动填充表单</button></div>
                <div class="form-group"><button id="clear-btn">🗑️ 清空表单</button></div>
                <div id="user-preview" style="display: none;"></div><div id="status-message"></div>
            </div>`;
        document.body.appendChild(panel);

        const savedCountry = GM_getValue('selectedCountry', 'US');
        document.getElementById('country-select').value = savedCountry;

        bindEvents();
        makeDraggable(panel, 'form-filler-header');
        makeButtonDraggable(toggleButton);
    }

    function bindEvents() {
        document.getElementById('form-filler-toggle').addEventListener('click', (e) => {
            if (e.detail === 1) {
                togglePanel();
            }
        });
        document.getElementById('close-panel').addEventListener('click', hidePanel);
        document.getElementById('generate-btn').addEventListener('click', generateUserData);
        document.getElementById('fill-btn').addEventListener('click', fillForm);
        document.getElementById('clear-btn').addEventListener('click', clearForm);
        document.getElementById('country-select').addEventListener('change', function() { GM_setValue('selectedCountry', this.value); });
    }

    function generateUserData() {
        const countryCode = document.getElementById('country-select').value;
        const generateBtn = document.getElementById('generate-btn');
        const fillBtn = document.getElementById('fill-btn');
        generateBtn.disabled = true; generateBtn.textContent = '⏳ 生成中...';
        showStatus('正在生成用户信息...', 'loading');
        GM_xmlhttpRequest({
            method: 'GET', url: `https://randomuser.me/api/?nat=${countryCode}&inc=name,location,phone,cell,dob,gender`,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data.results && data.results.length > 0) {
                        currentUserData = data.results[0];
                        const phoneCode = countryPhoneCodes[countryCode] || '';
                        currentUserData.formattedPhone = phoneCode + (currentUserData.phone || currentUserData.cell).replace(/[^\d]/g, '');
                        displayUserPreview(); fillBtn.disabled = false; showStatus('✅ 用户信息生成成功!', 'success');
                    } else { throw new Error('API未返回有效用户数据'); }
                } catch (error) { console.error('解析用户数据失败:', error); showStatus('❌ 生成失败,请重试', 'error'); }
                finally { generateBtn.disabled = false; generateBtn.textContent = '🎲 生成用户信息'; }
            },
            onerror: function(error) {
                console.error('API请求失败:', error); showStatus('❌ 网络请求失败,请检查网络', 'error');
                generateBtn.disabled = false; generateBtn.textContent = '🎲 生成用户信息';
            }
        });
    }

    function displayUserPreview() {
        if (!currentUserData) return;
        const preview = document.getElementById('user-preview'), user = currentUserData, code = document.getElementById('country-select').value;
        preview.innerHTML = `
            <div class="preview-item"><span class="preview-label">姓名:</span> <span class="preview-value">${user.name.first} ${user.name.last}</span></div>
            <div class="preview-item"><span class="preview-label">性别:</span> <span class="preview-value">${user.gender}</span></div>
            <div class="preview-item"><span class="preview-label">电话:</span> <span class="preview-value">${user.formattedPhone}</span></div>
            <div class="preview-item"><span class="preview-label">地址:</span> <span class="preview-value">${user.location.street.number} ${user.location.street.name}</span></div>
            <div class="preview-item"><span class="preview-label">城市:</span> <span class="preview-value">${user.location.city}</span></div>
            <div class="preview-item"><span class="preview-label">州/省:</span> <span class="preview-value">${user.location.state}</span></div>
            <div class="preview-item"><span class="preview-label">邮编:</span> <span class="preview-value">${user.location.postcode}</span></div>
            <div class="preview-item"><span class="preview-label">国家:</span> <span class="preview-value">${user.location.country} (${code})</span></div>`;
        preview.style.display = 'block';
    }

    function findInputFields() {
        const inputs = document.querySelectorAll('input[type="text"], input[type="tel"], input[type="email"], input:not([type]), select, textarea');
        const fields = {};
        for (const [fieldType, patterns] of Object.entries(fieldMappings)) {
            if (fields[fieldType]) continue;
            for (const input of inputs) {
                const attrs = [input.name, input.id, input.placeholder, input.className, input.getAttribute('data-field'), input.getAttribute('autocomplete')].filter(Boolean).join(' ').toLowerCase();
                let label = input.closest('label') || (input.id && document.querySelector(`label[for="${input.id}"]`));
                const allText = `${attrs} ${label ? label.textContent.toLowerCase() : ''}`;
                if (fieldType !== 'fullName' && fieldType !== 'firstName' && fieldType !== 'lastName' && (input.type === 'email' || input.type === 'password' || allText.includes('email') || allText.includes('password'))) continue;
                for (const pattern of patterns) { if (allText.includes(pattern)) { fields[fieldType] = input; break; } }
                if (fields[fieldType]) break;
            }
        }
        return fields;
    }

    function setFieldValue(element, value) {
        if (!element || typeof value === 'undefined') return false;
        element.focus();
        if (element.tagName.toLowerCase() === 'select') {
            const valLower = String(value).toLowerCase(); let found = false;
            for (const opt of element.options) { if (String(opt.value).toLowerCase() === valLower || String(opt.textContent).toLowerCase() === valLower) { element.value = opt.value; found = true; break; } }
            if (!found) { for (const opt of element.options) { if (String(opt.textContent).toLowerCase().includes(valLower)) { element.value = opt.value; found = true; break; } } }
            if (!found) return false;
        } else { element.value = value; }
        ['input', 'change', 'blur', 'keyup'].forEach(e => element.dispatchEvent(new Event(e, { bubbles: true, cancelable: true })));
        element.style.backgroundColor = '#e8f5e8'; setTimeout(() => { element.style.backgroundColor = ''; }, 1500);
        return true;
    }

    function fillForm() {
        if (!currentUserData) { showStatus('❌ 请先生成用户信息', 'error'); return; }
        const fieldsToFill = findInputFields(), user = currentUserData; let filledCount = 0;
        if (fieldsToFill.firstName && fieldsToFill.lastName) delete fieldsToFill.fullName;
        const fillData = {
            firstName: user.name.first, lastName: user.name.last, fullName: `${user.name.first} ${user.name.last}`, phone: user.formattedPhone,
            address: `${user.location.street.number} ${user.location.street.name}`, city: user.location.city, state: user.location.state,
            postcode: user.location.postcode.toString(), country: user.location.country, gender: user.gender
        };
        const countryField = fieldsToFill.country, stateField = fieldsToFill.state;
        if (countryField && stateField && stateField.tagName.toLowerCase() === 'select') {
            if (setFieldValue(countryField, fillData.country)) filledCount++;
            const stateSelect = stateField, stateName = fillData.state;
            if (setFieldValue(stateSelect, stateName)) { filledCount++; } else {
                const observer = new MutationObserver((_, obs) => {
                    if (setFieldValue(stateSelect, stateName)) { filledCount++; obs.disconnect(); clearTimeout(timeoutId); }
                });
                observer.observe(stateSelect, { childList: true });
                const timeoutId = setTimeout(() => { observer.disconnect(); console.warn(`Observer timed out for state: ${stateName}`); }, 3000);
            }
            delete fieldsToFill.country; delete fieldsToFill.state;
        }
        for (const fieldType in fieldsToFill) { if (fieldsToFill[fieldType] && setFieldValue(fieldsToFill[fieldType], fillData[fieldType])) filledCount++; }
        setTimeout(() => { showStatus(filledCount > 0 ? `✅ 成功填充 ${filledCount} 个字段` : '⚠️ 未找到可填充的字段', filledCount > 0 ? 'success' : 'error'); }, 500);
    }

    function clearForm() {
        const inputs = document.querySelectorAll('input[type="text"], input[type="tel"], input:not([type]), select, textarea'); let clearedCount = 0;
        inputs.forEach(input => {
            const attrs = [input.name, input.id, input.placeholder, input.className].filter(Boolean).join(' ').toLowerCase();
            if (input.type === 'email' || input.type === 'password' || attrs.includes('email') || attrs.includes('password')) return;
            if (input.value.trim() !== '' || (input.tagName === 'SELECT' && input.selectedIndex > 0)) {
                input.value = (input.tagName === 'SELECT') ? input.options[0].value : '';
                ['input', 'change', 'blur'].forEach(e => input.dispatchEvent(new Event(e, { bubbles: true })));
                input.style.backgroundColor = '#ffe8e8'; setTimeout(() => { input.style.backgroundColor = ''; }, 500);
                clearedCount++;
            }
        });
        showStatus(clearedCount > 0 ? `🗑️ 已清空 ${clearedCount} 个字段` : 'ℹ️ 没有需要清空的字段', 'success');
    }

    function togglePanel() {
        const panel = document.getElementById('form-filler-panel'), toggleBtn = document.getElementById('form-filler-toggle');
        if (panel.style.display === 'none') { panel.style.display = 'block'; toggleBtn.style.display = 'none'; }
        else { panel.style.display = 'none'; updateButtonVisibility(); }
    }

    function hidePanel() {
        document.getElementById('form-filler-panel').style.display = 'none';
        updateButtonVisibility();
    }

    function makeDraggable(element, handleId) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        const handle = document.getElementById(handleId);
        if (handle) handle.onmousedown = dragMouseDown;
        function dragMouseDown(e) {
            e = e || window.event; e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY;
            document.onmouseup = closeDragElement; document.onmousemove = elementDrag;
        }
        function elementDrag(e) {
            e = e || window.event; e.preventDefault();
            pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY;
            element.style.top = (element.offsetTop - pos2) + "px"; element.style.left = (element.offsetLeft - pos1) + "px";
            element.style.right = 'auto'; element.style.bottom = 'auto';
        }
        function closeDragElement() { document.onmouseup = null; document.onmousemove = null; }
    }

    function makeButtonDraggable(button) {
        let offsetX, offsetY, isDragging = false;

        button.onmousedown = function(e) {
            isDragging = true;
            button.style.cursor = 'grabbing';
            e.preventDefault();
            offsetX = e.clientX - button.getBoundingClientRect().left;
            offsetY = e.clientY - button.getBoundingClientRect().top;
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        };

        function onMouseMove(e) {
            if (!isDragging) return;
            let newLeft = e.clientX - offsetX;
            let newTop = e.clientY - offsetY;
            button.style.left = `${newLeft}px`;
            button.style.top = `${newTop}px`;
            button.style.right = 'auto';
            button.style.bottom = 'auto';
        }

        function onMouseUp() {
            if (!isDragging) return;
            isDragging = false;
            button.style.cursor = 'move';
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseup', onMouseUp);
            GM_setValue('buttonPosition', { top: button.style.top, left: button.style.left });
        }
    }


    function showStatus(message, type = 'loading') {
        const statusDiv = document.getElementById('status-message');
        if (!statusDiv) return; statusDiv.innerHTML = `<div class="status-message status-${type}">${message}</div>`;
        if (type !== 'loading') setTimeout(() => { if (statusDiv) statusDiv.innerHTML = ''; }, 3000);
    }

    function shouldShowButton() {
        if (GM_getValue('forceShowButton', false)) return true;
        return Object.keys(findInputFields()).length >= 4;
    }

    function updateButtonVisibility() {
        const toggleBtn = document.getElementById('form-filler-toggle');
        if (toggleBtn) toggleBtn.style.display = shouldShowButton() ? 'block' : 'none';
    }

    function registerMenuCommands() {
        if (window.menuCommandId) GM_unregisterMenuCommand(window.menuCommandId);
        const isForced = GM_getValue('forceShowButton', false);
        const label = isForced ? '悬浮按钮: 手动 (点击切换为自动)' : '悬浮按钮: 自动 (点击切换为手动)';
        window.menuCommandId = GM_registerMenuCommand(label, () => {
            const newSetting = !GM_getValue('forceShowButton', false);
            GM_setValue('forceShowButton', newSetting);
            alert(`智能填充助手:悬浮按钮已切换为 "${newSetting ? '手动显示' : '自动检测'}" 模式。`);
            registerMenuCommands(); updateButtonVisibility();
        });
    }

    function init() {
        createPanel();
        registerMenuCommands();
        window.addEventListener('load', updateButtonVisibility);
    }

    init();
    console.log('Whcms表单填充助手已加载!');

})();