Greasy Fork is available in English.

来吧来吧

自动填写表单,支持证件号码、姓名、手机号等字段的智能识别与填写

// ==UserScript==
// @name         来吧来吧
// @namespace    http://tampermonkey.net/
// @version      2.0.2
// @description  自动填写表单,支持证件号码、姓名、手机号等字段的智能识别与填写
// @author       Your Name
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    
    // 默认配置
    const defaultConfig = {
        name: '张三',
        phone: '13800138000',
        idCard: '110101199001011234',
        enabled: true,
        autoFill: true,
        simulateTyping: true,
        collapsed: false
    };

    // 字段识别规则
    const FIELD_PATTERNS = {
        name: [
            'name', '姓名', 'username', 'fullname', 'truename',
            'realname', 'nickname', 'uname', '名字', '真实姓名',
            'chinese-name', '中文名'
        ],
        phone: [
            'phone', 'mobile', '手机', '电话', 'tel', 'telephone',
            'cellphone', 'phonenumber', 'mobilenumber', '联系方式',
            'contact', '联系电话', '手机号码', '电话号码'
        ],
        idCard: [
            'idcard', 'identity', '身份证', '证件号', '证件号码',
            'identno', 'zjhm', 'sfz', 'idnumber', 'cardno',
            'identitycard', '身份证号', '身份证号码',
            'id_card', 'id_no', 'credentialno', 'credential',
            'idennum', 'iden_num', 'idennumber',
            'crdt_no', 'crdtno', 'crdt', 'cert_no', 'certno',
            'identification', 'documentno', 'document_no',
            'id-number', 'id_number', 'idcode', 'id-code',
            'resident-id', 'residentid', 'citizenid',
            '居民身份证', '居民证', '公民身份号码',
            'input[maxlength="18"]',
            'input[data-type="idcard"]',
            'input[data-format="idcard"]',
            'input[pattern="[0-9Xx]{18}"]'
        ]
    };

    // 验证码输入框特征
    const CAPTCHA_PATTERNS = [
        'captcha', 'vcode', 'verify', 'verifycode',
        'validcode', 'seccode', 'checkcode', '验证码',
        '图形码', '验证'
    ];

    // 添加样式
GM_addStyle(`
        .auto-form-panel {
            position: fixed;
            top: 20px;
            right: 20px;
            width: 300px;
            background: rgba(255, 255, 255, 0.9);
            border-radius: 12px;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
            z-index: 10000;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            transition: all 0.3s ease;
            resize: both;
            overflow: auto;
        }
    
    .panel-header {
        padding: 12px 15px;
        background: rgba(240, 240, 240, 0.9);
        border-radius: 12px 12px 0 0;
        display: flex;
        justify-content: space-between;
        align-items: center;
        border-bottom: 1px solid #ddd;
        cursor: move;
    }
    
    .panel-controls {
        display: flex;
        gap: 8px;
        align-items: center;
    }
    
    .panel-btn {
        background: none;
        border: none;
        cursor: pointer;
        padding: 4px;
        color: #666;
        font-size: 16px;
        line-height: 1;
        border-radius: 4px;
        transition: all 0.2s ease;
    }
    
    .panel-btn:hover {
        background: rgba(0, 0, 0, 0.1);
        color: #333;
    }
        
        .panel-title {
            font-size: 16px;
            font-weight: bold;
            color: #333;
        }
        
        .panel-body {
            padding: 15px;
        }
        
        .form-group {
            margin-bottom: 15px;
        }
        
        .form-group label {
            display: block;
            margin-bottom: 5px;
            font-size: 14px;
            color: #555;
        }
        
        .form-group input {
            width: 100%;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 6px;
            font-size: 14px;
            box-sizing: border-box;
            background: rgba(255, 255, 255, 0.8);
        }
        
        .form-group input:focus {
            border-color: #4A90E2;
            outline: none;
            background: rgba(255, 255, 255, 1);
        }
        
        .switch {
            position: relative;
            display: inline-block;
            width: 40px;
            height: 20px;
        }
        
        .switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }
        
        .slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #ccc;
            transition: .4s;
            border-radius: 20px;
        }
        
        .slider:before {
            position: absolute;
            content: "";
            height: 16px;
            width: 16px;
            left: 2px;
            bottom: 2px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
        }
        
        input:checked + .slider {
            background-color: #4A90E2;
        }
        
        input:checked + .slider:before {
            transform: translateX(20px);
        }
        
        .btn {
            padding: 8px 16px;
            border: none;
            border-radius: 6px;
            font-size: 14px;
            cursor: pointer;
            transition: all 0.3s ease;
        }
        
        .btn-primary {
            background: #4A90E2;
            color: white;
        }
        
        .btn-primary:hover {
            background: #357ABD;
        }
        
        .panel-footer {
            padding: 12px 15px;
            border-top: 1px solid #ddd;
            display: flex;
            justify-content: flex-end;
            gap: 10px;
        }
        
        .auto-form-panel.collapsed {
            width: auto !important;
            height: auto !important;
        }
        
        .auto-form-panel.collapsed .panel-body,
        .auto-form-panel.collapsed .panel-footer {
            display: none;
        }
        
        .auto-form-panel.collapsed .panel-header {
            border-radius: 12px;
            border-bottom: none;
        }
        
        .collapse-btn {
            background: none;
            border: none;
            cursor: pointer;
            padding: 4px;
            color: #666;
            font-size: 16px;
            line-height: 1;
            border-radius: 4px;
            transition: all 0.2s ease;
        }
        
        .collapse-btn:hover {
            background: rgba(0, 0, 0, 0.1);
            color: #333;
        }
    `);

    // 创建UI面板
function createPanel() {
    const panel = document.createElement('div');
    panel.className = 'auto-form-panel';
    panel.innerHTML = `
        <div class="panel-header">
            <span class="panel-title">自动填写助手</span>
            <div class="panel-controls">
                <button class="collapse-btn" id="collapseBtn" title="展开/收起">⇕</button>
                <label class="switch">
                    <input type="checkbox" id="enableSwitch" ${getConfig().enabled ? 'checked' : ''}>
                    <span class="slider"></span>
                </label>
            </div>
        </div>
            <div class="panel-body">
                <div class="form-group">
                    <label>姓名</label>
                    <div style="display: flex; align-items: center;">
                        <input type="text" id="nameInput" value="${getConfig().name}">
                        <button class="copy-btn" data-target="nameInput">复制</button>
                    </div>
                </div>
                <div class="form-group">
                    <label>手机号</label>
                    <div style="display: flex; align-items: center;">
                        <input type="text" id="phoneInput" value="${getConfig().phone}">
                        <button class="copy-btn" data-target="phoneInput">复制</button>
                    </div>
                </div>
                <div class="form-group">
                    <label>身份证号</label>
                    <div style="display: flex; align-items: center;">
                        <input type="text" id="idCardInput" value="${getConfig().idCard}">
                        <button class="copy-btn" data-target="idCardInput">复制</button>
                    </div>
                </div>
                <div class="form-group">
                    <label>
                        <input type="checkbox" id="autoFillSwitch" ${getConfig().autoFill ? 'checked' : ''}>
                        自动填写新表单
                    </label>
                </div>
            </div>
            <div class="panel-footer">
                <button class="btn btn-primary" id="saveConfig">保存设置</button>
            </div>
    `;
    
    document.body.appendChild(panel);
    
        // 绑定事件
        panel.querySelector('#saveConfig').addEventListener('click', saveConfig);
        panel.querySelector('#enableSwitch').addEventListener('change', toggleEnabled);

        // 绑定复制按钮事件
        panel.querySelectorAll('.copy-btn').forEach(button => {
            button.addEventListener('click', () => {
                const targetId = button.getAttribute('data-target');
                const input = document.getElementById(targetId);
                input.select();
                document.execCommand('copy');
                showNotification('已复制到剪贴板');
            });
        });

        // 绑定拖动事件
        let isDragging = false;
        let startX, startY, startLeft, startTop;

        panel.querySelector('.panel-header').addEventListener('mousedown', (e) => {
            isDragging = true;
            startX = e.clientX;
            startY = e.clientY;
            startLeft = panel.offsetLeft;
            startTop = panel.offsetTop;
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        });

        function onMouseMove(e) {
            if (!isDragging) return;
            const dx = e.clientX - startX;
            const dy = e.clientY - startY;
            panel.style.left = `${startLeft + dx}px`;
            panel.style.top = `${startTop + dy}px`;
        }

        function onMouseUp() {
            isDragging = false;
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseup', onMouseUp);
        }
    
    // 添加收缩/展开按钮事件处理
    const collapseBtn = panel.querySelector('#collapseBtn');
    collapseBtn.addEventListener('click', () => {
        panel.classList.toggle('collapsed');
        // 保存当前的收缩状态
        const config = getConfig();
        config.collapsed = panel.classList.contains('collapsed');
        GM_setValue('formConfig', config);
    });

    // 恢复上次的收缩状态
    if (getConfig().collapsed) {
        panel.classList.add('collapsed');
    }
    }

    // 获取配置
    function getConfig() {
        return Object.assign({}, defaultConfig, GM_getValue('formConfig', {}));
    }

    // 保存配置
    function saveConfig() {
        const config = {
            name: document.querySelector('#nameInput').value,
            phone: document.querySelector('#phoneInput').value,
            idCard: document.querySelector('#idCardInput').value,
            enabled: document.querySelector('#enableSwitch').checked,
            autoFill: document.querySelector('#autoFillSwitch').checked
        };
        
        GM_setValue('formConfig', config);
        showNotification('设置已保存');
    }

    // 切换启用状态
    function toggleEnabled(e) {
        const config = getConfig();
        config.enabled = e.target.checked;
        GM_setValue('formConfig', config);
        
        if (config.enabled) {
            startAutoFill();
        }
    }

    // 显示通知
    function showNotification(message) {
        const notification = document.createElement('div');
        notification.style.cssText = `
            position: fixed;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0,0,0,0.8);
            color: white;
            padding: 10px 20px;
            border-radius: 4px;
            font-size: 14px;
            z-index: 10001;
        `;
        notification.textContent = message;
        document.body.appendChild(notification);
        
        setTimeout(() => notification.remove(), 2000);
    }

    // 添加证件号码格式验证函数
    function validateIdCard(idCard) {
        const reg = /(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
        return reg.test(idCard);
    }

    // 修改 identifyFieldType 函数,增加特殊判断逻辑
    function identifyFieldType(element) {
        const fieldString = [
            element.name,
            element.id,
            element.placeholder,
            element.getAttribute('aria-label'),
            element.getAttribute('data-type'),
            element.getAttribute('data-format'),
            element.getAttribute('pattern'),
            element.closest('label')?.textContent,
            element.closest('.form-group')?.textContent,
            element.closest('.field')?.textContent,
            element.closest('.form-item')?.textContent,
            element.closest('[class*="form"]')?.textContent
        ].filter(Boolean).join(' ').toLowerCase();

        if (element.maxLength === 18 && !fieldString.includes('手机') && !fieldString.includes('电话')) {
            return 'idCard';
        }

        if (CAPTCHA_PATTERNS.some(pattern => fieldString.includes(pattern))) {
            return 'captcha';
        }

        for (const [type, patterns] of Object.entries(FIELD_PATTERNS)) {
            if (patterns.some(pattern => {
                if (pattern.startsWith('input[')) {
                    return element.matches(pattern);
                }
                return fieldString.includes(pattern);
            })) {
                return type;
            }
        }

        return null;
    }

    // 添加模拟手动输入函数
    async function simulateManualInput(element, value) {
        element.focus();
        element.value = '';
        
        for (let i = 0; i < value.length; i++) {
            await new Promise(resolve => setTimeout(resolve, 50 + Math.random() * 100));
            element.value = value.substring(0, i + 1);
            
            const events = [
                new Event('input', { bubbles: true }),
                new KeyboardEvent('keydown', { key: value[i], bubbles: true }),
                new KeyboardEvent('keypress', { key: value[i], bubbles: true }),
                new KeyboardEvent('keyup', { key: value[i], bubbles: true })
            ];
            
            events.forEach(event => element.dispatchEvent(event));
        }
        
        element.dispatchEvent(new Event('change', { bubbles: true }));
        element.dispatchEvent(new Event('blur', { bubbles: true }));
    }

    // 修改 autoFillForm 函数,增加智能填写逻辑
    function autoFillForm() {
        const config = getConfig();
        if (!config.enabled) return;

        const inputs = document.querySelectorAll(`
            input[type="text"],
            input[type="tel"],
            input[type="number"],
            input:not([type]),
            input[data-type="idcard"],
            input[data-format="idcard"],
            input[pattern*="[0-9Xx]{18}"],
            input[maxlength="18"]
        `);
        
        inputs.forEach(async input => {
            const fieldType = identifyFieldType(input);
            if (!fieldType || fieldType === 'captcha') return;

            const value = config[fieldType];
            if (value) {
                let formattedValue = value;
                if (fieldType === 'idCard') {
                    if (input.getAttribute('pattern')?.includes('X')) {
                        formattedValue = value.toUpperCase();
                    } else if (input.getAttribute('pattern')?.includes('x')) {
                        formattedValue = value.toLowerCase();
                    }
                }

                try {
                    input.value = formattedValue;
                    triggerEvents(input);
                    
                    if (input.value !== formattedValue) {
                        await simulateManualInput(input, formattedValue);
                    }
                } catch (e) {
                    await simulateManualInput(input, formattedValue);
                }

                if (input.matches('[ng-model]')) {
                    triggerAngularUpdate(input);
                } else if (input.__vue__) {
                    triggerVueUpdate(input, formattedValue);
                }
            }
        });
    }

    // 添加框架特定的更新函数
    function triggerAngularUpdate(element) {
        if (window.angular) {
            const scope = angular.element(element).scope();
            if (scope) {
                scope.$apply();
            }
        }
    }

    function triggerVueUpdate(element, value) {
        if (element.__vue__) {
            element.__vue__.$emit('input', value);
            element.__vue__.$emit('change', value);
        }
    }

    // 扩展 triggerEvents 函数
    function triggerEvents(element) {
        const events = [
            'input',
            'change',
            'blur',
            'keyup',
            'keydown',
            'focus',
            'update'
        ];

        events.forEach(eventType => {
            const event = new Event(eventType, { bubbles: true });
            element.dispatchEvent(event);
        });

        if (element._reactProps) {
            const nativeEvent = new Event('input', { bubbles: true });
            element._reactProps.onChange?.({ target: element, nativeEvent });
        }
    }

    // 开始自动填写
    function startAutoFill() {
        const config = getConfig();
        if (!config.enabled) return;

        autoFillForm();

        const observer = new MutationObserver((mutations) => {
            if (config.autoFill) {
                autoFillForm();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // 初始化
    function init() {
        createPanel();
        startAutoFill();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();