AutoFill

自动填写表单数据的Tampermonkey脚本

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         AutoFill
// @name:zh      凯普顿自动填表
// @name:en      AutoFill
// @version      1.1
// @author       peng shaowei
// @description  自动填写表单数据的Tampermonkey脚本
// @description:en Tampermonkey Script for Autofilling Form Data
// @match        */*
// @grant        GM_setValue
// @grant        GM_getValue
// @license      GPL-3.0
// @history      1.1 自动填充用户填写的数据,新增恢复初始状态功能
// @history      1.0 初始版本,支持保存和恢复表单数据
// @namespace https://gitee.com/pengsw95
// ==/UserScript==

(function() {
    // 引入按钮组件样式
    const modalStyle = `
        /* 设置界面容器 */
        .settings-modal {
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
          z-index: 9999;
          width: 400px;
          padding: 20px;
          background-color: #fff;
          box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
          border-radius: 4px;
          display: flex;
          flex-direction: column;
          position: fixed;
          opacity: 0.8;
          transition: opacity 0.3s ease;
        }

        .settings-modal:hover {
          /* 鼠标悬停时的样式 */
          opacity: 1;
        }

        /* 标签样式 */
        .settings-label {
          font-weight: bold;
          margin-bottom: 10px;
          display: block;
        }

        /* 输入框样式 */
        .settings-input {
          width: 100%;
          padding: 8px;
          border: 1px solid #ccc;
          border-radius: 4px;
          background-color: #f8f8f8;
          color: #333;
          box-sizing: border-box;
          transition: border-color 0.3s ease;
        }

        .settings-input:focus {
          outline: none;
          border-color: #007bff;
          box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.1);
        }

        /* 按钮Div */
        .settings-buttons {
          display: flex;
        }

        /* 按钮样式 */
        .settings-button {
          flex: 1;
          padding: 8px 16px; /* 调整按钮的内边距 */
          margin-top: 10px;
          border: none;
          border-radius: 4px;
          color: #fff;
          background-color: #007bff;
          cursor: pointer;
          transition: background-color 0.3s ease;
          font-size: 14px;  /* 调整按钮的字体大小 */
        }

        .settings-button:not(:last-child) {
          margin-right: 20px;
        }

        .settings-button:hover {
          background-color: #0056b3;
        }

        .settings-button:active {
          background-color: #004f9d;
        }

        /* 关闭按钮样式 */
        .close-button {
          position: absolute;
          top: 10px;
          right: 10px;
          padding: 6px;
          border: none;
          background-color: transparent;
          color: #333;
          font-size: 18px;
          cursor: pointer;
        }

        .close-button:hover {
          color: #007bff;
        }
    `;

    // 统一字段选择器
    const FORM_SELECTOR = 'input, textarea, select';

    // 创建 Shadow DOM 容器
    const container = document.createElement('div');
    container.id = 'autofill-container';
    document.documentElement.appendChild(container);
    const shadow = container.attachShadow({mode: 'open'});

    // 创建设置按钮
    function createSettingsButton() {
        const button = document.createElement('button');
        button.innerText = '自动填表';
        button.style.cssText = `
            position: fixed;
            top: 10px;
            left: 50%;
            transform: translateX(-50%);
            z-index: 9999;
            opacity: 0.8;
            color: gray;
            text-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
            cursor: pointer;
        `.replace(/\n\s*/g, ' ').trim();
        button.addEventListener('click', createSettingsModal);
        shadow.appendChild(button);
    }

    // 创建设置模态框
    function createSettingsModal() {
        if (shadow.querySelector('.settings-modal')) return;

        const modal = document.createElement('div');
        modal.className = 'settings-modal';
        modal.style.zIndex = '9999'; // 解决部分网站会遮挡的问题
        
        const currentDiff = getDiffData();

        modal.innerHTML = `
            <style>${modalStyle}</style>
            <button class="close-button">×</button>
            <div class="settings-label">
                <span>表单数据:</span>
            </div>
            <input class="settings-input" type="text" value='${currentDiff}'>
            <div class="settings-buttons">
                <button class="settings-button btn-save">保存</button>
                <button class="settings-button btn-fill">恢复</button>
                <button class="settings-button btn-clear">恢复初始</button>
            </div>
        `;

        // 绑定事件
        modal.querySelector('.close-button').addEventListener('click', closeSettingsModal);
        modal.querySelector('.btn-save').addEventListener('click', saveFormData);
        modal.querySelector('.btn-fill').addEventListener('click', fillForm);
        modal.querySelector('.btn-clear').addEventListener('click', clearFormData);
        shadow.appendChild(modal);
    }

    // 关闭设置模态框
    function closeSettingsModal() {
        const modal = shadow.querySelector('.settings-modal');
        if (modal) modal.remove();
    }

    // 保存初始表单状态
    let initialFormState = {};

    // 获取字段唯一标识:优先用 name,其次 placeholder,再次 aria-label
    function getFieldKey(field) {
        return field.name || field.placeholder || field.getAttribute('aria-label') || '';
    }

    // 判断节点是否可见(自身及所有祖先均不含 display:none)
    function isVisible(el) {
        let node = el;
        while (node && node !== document.documentElement) {
            if (getComputedStyle(node).display === 'none') return false;
            node = node.parentElement;
        }
        return true;
    }

    // 触发 Vue 响应式更新(同时兼容普通表单)
    function setFieldValue(field, value) {
        const proto = field.tagName === 'TEXTAREA'
            ? window.HTMLTextAreaElement.prototype
            : window.HTMLInputElement.prototype;
        const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value');
        if (nativeSetter && nativeSetter.set) {
            nativeSetter.set.call(field, value);
        } else {
            field.value = value;
        }
        field.dispatchEvent(new Event('input',  { bubbles: true }));
        field.dispatchEvent(new Event('change', { bubbles: true }));
    }

    // 捕获初始状态
    function captureInitialState() {
        initialFormState = {};
        const formFields = document.querySelectorAll(FORM_SELECTOR);
        formFields.forEach(field => {
            const key = getFieldKey(field);
            if (key && isVisible(field)) {
                initialFormState[key] = field.value;
            }
        });
        // console.log('AutoFill: 基准状态已更新', initialFormState);
    }

    // 获取表单差异数据
    function getDiffData() {
        const formFields = document.querySelectorAll(FORM_SELECTOR);
        const diffData = {};
        formFields.forEach(field => {
            const key = getFieldKey(field);
            if (key && isVisible(field)) {
                const initialVal = initialFormState[key];
                if (field.value !== initialVal) {
                    diffData[key] = field.value;
                }
            }
        });
        return JSON.stringify(diffData);
    }

    // 保存表单
    function saveFormData() {
        const inputField = shadow.querySelector('.settings-input').value.trim();
        try {
            JSON.parse(inputField);
            localStorage.setItem('formData', inputField);
        } catch (e) {
            alert('保存失败:JSON 格式不正确。');
            return;
        }
        closeSettingsModal();
    }

    // 清空表单
    function clearFormData() {
        localStorage.removeItem('formData');
        document.querySelectorAll(FORM_SELECTOR).forEach(field => {
            const key = getFieldKey(field);
            if (key && key in initialFormState) {
                setFieldValue(field, initialFormState[key]);
            }
        });
        closeSettingsModal();
    }


    // 自动填充
    function fillForm() {
        const savedFormData = localStorage.getItem('formData');
        if (savedFormData) {
            try {
                const parsedData = JSON.parse(savedFormData);
                const formFields = document.querySelectorAll(FORM_SELECTOR);
                formFields.forEach(function(field) {
                    const key = getFieldKey(field);
                    if (key && key in parsedData) {
                        setFieldValue(field, parsedData[key]);
                    }
                });
            } catch (error) { console.error('AutoFill解析失败:', error); }
        }
        if (shadow.querySelector('.settings-modal')) closeSettingsModal();
    }

    // 在页面完全加载后执行
    window.addEventListener('load', function() {
        // 记录初始状态
        captureInitialState();
        // 添加设置按钮
        createSettingsButton();
    });
})();