Greasy Fork

来自缓存

Greasy Fork is available in English.

多套表单自动填充

基于 IndexedDB 存储,支持大容量表单数据,弹窗选择填充,Ctrl+M开关

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         多套表单自动填充
// @namespace    http://tampermonkey/
// @version      6.0
// @description  基于 IndexedDB 存储,支持大容量表单数据,弹窗选择填充,Ctrl+M开关
// @author       米奇不妙屋
// @match        *://*/*
// @grant        none
// @license MIT
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    const domain = location.hostname;
    const DB_NAME = 'FormAutoFillDB';
    const DB_VERSION = 1;
    const STORE_NAME = 'formSchemes';

    let db;
    let panel = null;
    let currentModal = null;

    // ==========================
    // IndexedDB 初始化
    // ==========================
    function initDB() {
        return new Promise((resolve, reject) => {
            const req = indexedDB.open(DB_NAME, DB_VERSION);
            req.onupgradeneeded = e => {
                db = e.target.result;
                if (!db.objectStoreNames.contains(STORE_NAME)) {
                    const store = db.createObjectStore(STORE_NAME, { keyPath: 'domain' });
                    store.createIndex('domain', 'domain', { unique: true });
                }
            };
            req.onsuccess = e => {
                db = e.target.result;
                resolve();
            };
            req.onerror = e => reject(e.target.error);
        });
    }

    // ==========================
    // IndexedDB 工具方法
    // ==========================
    async function getList() {
        await initDB();
        return new Promise(resolve => {
            const tx = db.transaction(STORE_NAME, 'readonly');
            const store = tx.objectStore(STORE_NAME);
            const req = store.get(domain);
            req.onsuccess = () => resolve(req.result?.list || []);
            req.onerror = () => resolve([]);
        });
    }

    async function setList(list) {
        await initDB();
        return new Promise(resolve => {
            const tx = db.transaction(STORE_NAME, 'readwrite');
            const store = tx.objectStore(STORE_NAME);
            store.put({ domain, list });
            tx.oncomplete = resolve;
        });
    }

    async function getAllDomainData() {
        await initDB();
        return new Promise(resolve => {
            const tx = db.transaction(STORE_NAME, 'readonly');
            const store = tx.objectStore(STORE_NAME);
            const req = store.getAll();
            req.onsuccess = () => resolve(req.result || []);
        });
    }

    async function importAllData(dataMap) {
        await initDB();
        return new Promise(resolve => {
            const tx = db.transaction(STORE_NAME, 'readwrite');
            const store = tx.objectStore(STORE_NAME);
            Object.keys(dataMap).forEach(dom => {
                store.put({ domain: dom, list: dataMap[dom] });
            });
            tx.oncomplete = resolve;
        });
    }

    // ==========================
    // 表单工具
    // ==========================
    const INPUT_SELECTOR = 'input:not([type="hidden"]), textarea, select';
    const ALLOWED_INPUT_TYPES = ['text','email','tel','password','number','search','url','date','month','time'];

    function getFields() {
        return Array.from(document.querySelectorAll(INPUT_SELECTOR)).filter(el => {
            if (el.tagName === 'SELECT' || el.tagName === 'TEXTAREA') return true;
            return ALLOWED_INPUT_TYPES.includes(el.type?.toLowerCase());
        });
    }

    function getFirstInput() {
        return document.querySelector(INPUT_SELECTOR);
    }

    function showToast(text) {
        const t = document.createElement('div');
        t.style.cssText = `
            position: fixed; left:50%; top:48%;
            transform:translate(-50%,-50%); z-index:1000001;
            padding:10px 18px; border-radius:8px;
            background:rgba(0,0,0,0.75); color:#fff;
            font-size:14px; white-space:nowrap;
        `;
        t.textContent = text;
        document.body.appendChild(t);
        setTimeout(() => t?.parentNode?.removeChild(t), 1500);
    }

    function closeAnyModal() {
        if (currentModal) { currentModal.remove(); currentModal = null; }
    }

    // ==========================
    // 面板开关 Ctrl+M
    // ==========================
    function togglePanel() {
        const old = document.getElementById('form-fill-panel');
        if (old) { old.remove(); closeAnyModal(); return; }
        createPanel();
    }

    function createPanel() {
        panel = document.createElement('div');
        panel.id = 'form-fill-panel';
        panel.style.cssText = `
            position: absolute;
            z-index: 999998;
            width: 170px;
            background: #fff;
            border-radius: 12px;
            box-shadow: 0 6px 24px rgba(0,0,0,0.08);
            padding: 12px;
            font-size: 14px;
            color: #333;
            user-select: none;
        `;
        const btnStyle = `
            width: 100%;
            border: none;
            border-radius: 8px;
            padding: 10px;
            margin: 4px 0;
            cursor: pointer;
            font-size: 14px;
            transition: 0.2s;
        `;

        const bSave = document.createElement('button');
        bSave.textContent = '💾 保存为新方案';
        bSave.style.cssText = btnStyle + 'background:#165DFF; color:white;';
        bSave.onclick = saveFormAsNew;

        const bOverwrite = document.createElement('button');
        bOverwrite.textContent = '🔄 一键覆盖最近方案';
        bOverwrite.style.cssText = btnStyle + 'background:#FF7D00; color:white;';
        bOverwrite.onclick = overwriteLatest;

        const bFill = document.createElement('button');
        bFill.textContent = '✅ 选择方案填充';
        bFill.style.cssText = btnStyle + 'background:#00B42A; color:white;';
        bFill.onclick = showFillModal;

        const bManage = document.createElement('button');
        bManage.textContent = '🗂 管理方案';
        bManage.style.cssText = btnStyle + 'background:#86909C; color:white;';
        bManage.onclick = showManageModal;

        const bImportExport = document.createElement('button');
        bImportExport.textContent = '📤 导入/导出数据';
        bImportExport.style.cssText = btnStyle + 'background:#722ED1; color:white;';
        bImportExport.onclick = showImportExport;

        const tip = document.createElement('div');
        tip.style.marginTop = '8px';
        tip.style.fontSize = '12px';
        tip.style.color = '#999';
        tip.textContent = 'Ctrl+M开关 | Ctrl+Shift+F填充最近';

        panel.append(bSave, bOverwrite, bFill, bManage, bImportExport, tip);
        document.body.appendChild(panel);

        const target = getFirstInput();
        if (target) {
            const r = target.getBoundingClientRect();
            panel.style.left = (r.right + 12 + window.scrollX) + 'px';
            panel.style.top = (r.top + window.scrollY) + 'px';
        } else {
            panel.style.left = '20px';
            panel.style.top = '100px';
        }
    }

    // ==========================
    // 保存为新方案
    // ==========================
    async function saveFormAsNew() {
        const fields = getFields();
        const data = {};
        fields.forEach(el => {
            const k = el.name || el.id || el.placeholder || el.className;
            if (k && el.value?.trim()) data[k] = el.value;
        });
        if (Object.keys(data).length === 0) return showToast('未检测到可保存内容');

        const input = document.createElement('input');
        input.placeholder = '输入方案名称';
        input.style.cssText = `
            position:fixed; z-index:999999; left:50%; top:50%;
            transform:translate(-50%,-50%); padding:12px 16px;
            border-radius:8px; border:1px solid #eee;
            box-shadow:0 6px 20px rgba(0,0,0,0.1);
            width:240px; font-size:14px; outline:none;
        `;
        document.body.appendChild(input);
        input.focus();

        input.onkeydown = async e => {
            if (e.key === 'Enter') await confirm();
            if (e.key === 'Escape') cancel();
        };

        async function confirm() {
            const name = input.value.trim() || '未命名方案';
            const list = await getList();
            list.push({ name, data, time: new Date().toLocaleString() });
            await setList(list);
            input.remove();
            showToast(`已保存:${name}`);
        }
        function cancel() { input.remove(); }
    }

    // ==========================
    // 一键覆盖最近方案
    // ==========================
    async function overwriteLatest() {
        const list = await getList();
        if (list.length === 0) return showToast('暂无方案可覆盖');

        const fields = getFields();
        const data = {};
        fields.forEach(el => {
            const k = el.name || el.id || el.placeholder || el.className;
            if (k && el.value?.trim()) data[k] = el.value;
        });
        if (Object.keys(data).length === 0) return showToast('无内容可覆盖');

        list[list.length - 1].data = data;
        list[list.length - 1].time = new Date().toLocaleString();
        await setList(list);
        showToast('✅ 最近方案已覆盖');
    }

    // ==========================
    // 填充弹窗
    // ==========================
    async function showFillModal() {
        closeAnyModal();
        const list = await getList();
        if (list.length === 0) return showToast('暂无保存方案');

        const modal = document.createElement('div');
        modal.style.cssText = `
            position: fixed; inset:0; z-index:999999;
            background:rgba(0,0,0,0.4); display:flex;
            align-items:center; justify-content:center;
        `;

        const card = document.createElement('div');
        card.style.cssText = `
            width: 90%; max-width:480px; background:#fff;
            border-radius:14px; overflow:hidden;
            box-shadow:0 10px 40px rgba(0,0,0,0.15);
            max-height:80vh; display:flex; flex-direction:column;
        `;

        const head = document.createElement('div');
        head.style.cssText = 'padding:16px; font-size:16px; font-weight:600; border-bottom:1px solid #eee;';
        head.textContent = `选择要填充的方案`;

        const wrap = document.createElement('div');
        wrap.style.cssText = 'padding:8px; overflow-y:auto; flex:1;';

        list.forEach((item, idx) => {
            const row = document.createElement('div');
            row.style.cssText = `
                display:flex; justify-content:space-between;
                align-items:center; padding:12px; border-radius:10px;
                margin:4px 0; background:#fafafa;
            `;
            const info = document.createElement('div');
            info.innerHTML = `
                <div style="font-weight:500">${item.name}</div>
                <div style="font-size:12px;color:#888">${item.time}</div>
            `;
            const btn = document.createElement('button');
            btn.textContent = '选择填充';
            btn.style.cssText = `
                padding:6px 12px; border-radius:8px;
                border:none; background:#00B42A; color:white;
                cursor:pointer; font-size:12px;
            `;
            btn.onclick = async () => {
                await fillItem(item);
                closeAnyModal();
            };
            row.append(info, btn);
            wrap.appendChild(row);
        });

        const foot = document.createElement('div');
        foot.style.cssText = 'padding:12px; text-align:right; border-top:1px solid #eee;';
        const closeBtn = document.createElement('button');
        closeBtn.textContent = '关闭';
        closeBtn.style.cssText = `
            padding:8px 16px; border-radius:8px;
            border:1px solid #eee; background:#f6f6f6; cursor:pointer;
        `;
        closeBtn.onclick = closeAnyModal;
        foot.appendChild(closeBtn);

        card.append(head, wrap, foot);
        modal.appendChild(card);
        document.body.appendChild(modal);
        currentModal = modal;
    }

    async function fillItem(item) {
        const fields = getFields();
        let count = 0;
        fields.forEach(el => {
            const k = el.name || el.id || el.placeholder || el.className;
            if (k && item.data[k]) {
                el.value = item.data[k];
                el.dispatchEvent(new Event('input', { bubbles: true }));
                el.dispatchEvent(new Event('change', { bubbles: true }));
                count++;
            }
        });
        showToast(`已填充「${item.name}」(${count}项)`);
    }

    // ==========================
    // 管理弹窗
    // ==========================
    async function showManageModal() {
        closeAnyModal();
        const list = await getList();

        const modal = document.createElement('div');
        modal.style.cssText = `
            position: fixed; inset:0; z-index:999999;
            background:rgba(0,0,0,0.4); display:flex;
            align-items:center; justify-content:center;
        `;

        const card = document.createElement('div');
        card.style.cssText = `
            width: 90%; max-width:480px; background:#fff;
            border-radius:14px; overflow:hidden;
            box-shadow:0 10px 40px rgba(0,0,0,0.15);
            max-height:80vh; display:flex; flex-direction:column;
        `;

        const head = document.createElement('div');
        head.style.cssText = 'padding:16px; font-size:16px; font-weight:600; border-bottom:1px solid #eee;';
        head.textContent = `表单方案管理 (${list.length})`;

        const wrap = document.createElement('div');
        wrap.style.cssText = 'padding:8px; overflow-y:auto; flex:1;';

        if (list.length === 0) {
            const empty = document.createElement('div');
            empty.style.cssText = 'padding:20px; text-align:center; color:#999;';
            empty.textContent = '暂无保存方案';
            wrap.appendChild(empty);
        } else {
            list.forEach((item, idx) => {
                const row = document.createElement('div');
                row.style.cssText = `
                    display:flex; justify-content:space-between;
                    align-items:center; padding:12px; border-radius:10px;
                    margin:4px 0; background:#fafafa;
                `;
                const info = document.createElement('div');
                info.innerHTML = `
                    <div style="font-weight:500">${item.name}</div>
                    <div style="font-size:12px;color:#888">${item.time}</div>
                `;
                const del = document.createElement('button');
                del.textContent = '删除';
                del.style.cssText = `
                    padding:6px 12px; border-radius:8px;
                    border:none; background:#F53F3F; color:white;
                    cursor:pointer; font-size:12px;
                `;
                del.onclick = async () => {
                    const newList = await getList();
                    newList.splice(idx, 1);
                    await setList(newList);
                    await showManageModal();
                    showToast('已删除');
                };
                row.append(info, del);
                wrap.appendChild(row);
            });
        }

        const foot = document.createElement('div');
        foot.style.cssText = 'padding:12px; text-align:right; border-top:1px solid #eee;';
        const closeBtn = document.createElement('button');
        closeBtn.textContent = '关闭';
        closeBtn.style.cssText = `
            padding:8px 16px; border-radius:8px;
            border:1px solid #eee; background:#f6f6f6; cursor:pointer;
        `;
        closeBtn.onclick = closeAnyModal;
        foot.appendChild(closeBtn);

        card.append(head, wrap, foot);
        modal.appendChild(card);
        document.body.appendChild(modal);
        currentModal = modal;
    }

    // ==========================
    // 导入导出 JSON
    // ==========================
    async function showImportExport() {
        closeAnyModal();
        const modal = document.createElement('div');
        modal.style.cssText = `
            position:fixed; inset:0; z-index:999999;
            background:rgba(0,0,0,0.5); display:flex;
            align-items:center; justify-content:center;
        `;
        const box = document.createElement('div');
        box.style.cssText = `
            background:#fff; border-radius:14px;
            padding:20px; width:90%; max-width:460px;
        `;
        const title = document.createElement('div');
        title.style.cssText = 'font-size:16px; font-weight:600; margin-bottom:12px;';
        title.textContent = '全局数据导入/导出';

        const tip = document.createElement('div');
        tip.style.cssText = 'font-size:12px; color:#999; margin-bottom:12px;';
        tip.textContent = '导出所有网站表单数据,可备份/迁移';

        const btnExport = document.createElement('button');
        btnExport.textContent = '📤 导出全部数据(JSON)';
        btnExport.style.cssText = `
            width:100%; padding:10px; border-radius:8px;
            background:#165DFF; color:white; border:none; margin-bottom:8px;
        `;
        btnExport.onclick = async () => {
            const all = await getAllDomainData();
            const map = {};
            all.forEach(item => map[item.domain] = item.list);
            const blob = new Blob([JSON.stringify(map, null, 2)], { type: 'application/json' });
            const a = document.createElement('a');
            a.href = URL.createObjectURL(blob);
            a.download = `form_backup_${new Date().getTime()}.json`;
            a.click();
            showToast('导出成功');
        };

        const fileInput = document.createElement('input');
        fileInput.type = 'file';
        fileInput.accept = '.json';
        fileInput.style.cssText = 'width:100%; margin-bottom:8px;';

        const btnImport = document.createElement('button');
        btnImport.textContent = '📥 导入选中文件';
        btnImport.style.cssText = `
            width:100%; padding:10px; border-radius:8px;
            background:#00B42A; color:white; border:none;
        `;
        btnImport.onclick = async () => {
            const file = fileInput.files[0];
            if (!file) return showToast('请选择JSON文件');
            const reader = new FileReader();
            reader.onload = async e => {
                try {
                    const data = JSON.parse(e.target.result);
                    await importAllData(data);
                    showToast('导入成功!');
                    modal.remove();
                } catch { showToast('导入失败'); }
            };
            reader.readAsText(file);
        };

        const closeBtn = document.createElement('button');
        closeBtn.textContent = '关闭';
        closeBtn.style.cssText = `
            margin-top:12px; padding:8px 16px; border-radius:8px;
            border:1px solid #eee; background:#f6f6f6; cursor:pointer;
        `;
        closeBtn.onclick = () => modal.remove();

        box.append(title, tip, btnExport, fileInput, btnImport, closeBtn);
        modal.appendChild(box);
        document.body.appendChild(modal);
    }

    // ==========================
    // 快捷键填充
    // ==========================
    async function fillLatest() {
        const list = await getList();
        if (list.length === 0) return showToast('暂无方案');
        await fillItem(list[list.length - 1]);
    }

    // ==========================
    // 全局快捷键
    // ==========================
    document.addEventListener('keydown', e => {
        if (e.ctrlKey && e.key.toLowerCase() === 'm') {
            e.preventDefault();
            togglePanel();
        }
        if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'f') {
            e.preventDefault();
            fillLatest();
        }
    });

})();