// ==UserScript== // @name 多套表单自动填充 // @namespace http://tampermonkey/ // @version 6.0 // @description 基于 IndexedDB 存储,支持大容量表单数据,弹窗选择填充,Ctrl+M开关 // @author 米奇不妙屋 // @match *://*/* // @grant none // @license MIT // @run-at document-end // @downloadURL https://update.greasyfork.icu/scripts/571797/%E5%A4%9A%E5%A5%97%E8%A1%A8%E5%8D%95%E8%87%AA%E5%8A%A8%E5%A1%AB%E5%85%85.user.js // @updateURL https://update.greasyfork.icu/scripts/571797/%E5%A4%9A%E5%A5%97%E8%A1%A8%E5%8D%95%E8%87%AA%E5%8A%A8%E5%A1%AB%E5%85%85.meta.js // ==/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 = `