// ==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 = `
${item.name}
${item.time}
`; 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 = `
${item.name}
${item.time}
`; 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(); } }); })();