Greasy Fork is available in English.
基于 IndexedDB 存储,支持大容量表单数据,弹窗选择填充,Ctrl+M开关
// ==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();
}
});
})();