您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
账号密码自动填充 支持多账号 两步登录
// ==UserScript== // @name 账号密码自动填充 // @description 账号密码自动填充 支持多账号 两步登录 // @version 1.0 // @author WJ // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @run-at document-idle // @license MIT // @namespace https://greasyfork.org/users/914996 // ==/UserScript== setTimeout(() => { 'use strict'; /* ---------- 初始化 ---------- */ const init = () => { document.addEventListener('submit', e => baocun(e.target), true); if (!loadAll().some(it => it.url && location.href.startsWith(it.url) && (it.account || it.password))) return; new MutationObserver(tianc).observe(document.body, { childList: true, subtree: true }), tianc(); }; /* ---------- 自动保存 ---------- */ const baocun = form => { const { user, pwd } = chaZ(form); const u = user?.value, p = pwd?.value; if (!u || !p) return; const list = loadAll(); if (list.some(it => it.url.startsWith(location.origin) && it.account === u && it.password === p)) return; if (confirm(`是否保存账号/密码?\n账号:${u}\n密码:${p}`)) { list.push({ title: document.title || location.hostname, url: location.origin + location.pathname, account: u, password: p }); saveAll(list); toast('已保存'); } }; /* ---------- 查找输入框 ---------- */ const chaZ = () => { const inputs = document.body.querySelectorAll('input'); if (!inputs.length) return { user: null, pwd: null }; const kw = n => /user|login|mail|phone|手机|邮箱|账号|用户名|账户|id/i.test(`${n.name}|${n.placeholder}|${n.id}`); let user = null, pwd = null, userTab = Infinity, pwdTab = Infinity; for (const n of inputs) { const tab = n.tabIndex || 0; if (n.type === 'password' && tab < pwdTab) { pwd = n; pwdTab = tab; } if (/^(text|email|tel|number)$/i.test(n.type) && kw(n) && tab < userTab) { user = n; userTab = tab; } } return { user, pwd }; }; /* ---------- 自动填充 ---------- */ let suo = null; const tianc = () => suo ??= (async () => { const { user, pwd } = chaZ(); if (!user && !pwd) return; const list = loadAll().filter(it => it.url && location.href.startsWith(it.url) && (it.account || it.password)); const rec = list.length === 1 ? list[0] : list.length ? await pickAccount(list) : null; const fill = (el, val, attr) => el && val && !el.value && !el.hasAttribute(attr) && (setValue(el, val), el.setAttribute(attr, '')); fill(user, rec?.account, 'filled-u'); fill(pwd, rec?.password, 'filled-p'); })().finally(() => { suo = null; }); /* ---------- 扫描 ---------- */ const scanP = () => { const title = document.title || location.hostname; const url = location.origin + location.pathname; const { user, pwd } = chaZ(); const list = loadAll(); const idx = list.findIndex(it => it.title === title); const rec = { title, url, account: user?.value || '', password: pwd?.value || '' }; idx >= 0 ? (list[idx] = rec) : list.push(rec); saveAll(list); openEditor(); }; /* ---------- 数据操作 ---------- */ const loadAll = () => JSON.parse(GM_getValue('siteCredentials', '[]')); const saveAll = arr => GM_setValue('siteCredentials', JSON.stringify(arr)); const setValue = (el, val) => { Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set?.call(el, val); el.dispatchEvent(new Event('input', { bubbles: true })); }; /* ---------- Toast ---------- */ const toast = m => {const e = Object.assign(document.createElement('div'), { textContent: m,style: 'position:fixed;left:50%;bottom:80px;transform:translateX(-50%);background:#000c;color:#fff;padding:12px 20px;border-radius:4px;font-size:18px;z-index:99999;border:2px solid #5B6'}); document.body.append(e);setTimeout(() => e.remove(), 3000)}; /* ---------- 底部卡片选择 ---------- */ const pickAccount = list => new Promise(resolve => { const key = `picked_${location.origin}${location.pathname}`; if (sessionStorage[key]) return resolve(JSON.parse(sessionStorage[key])); const wrap = document.createElement('div'); wrap.style.cssText = 'font-family:system-ui;position:fixed;left:0;right:0;bottom:0;z-index:99999;background:#1e1e1e;color:#eee;padding:16px 0'; window.visualViewport?.addEventListener('resize', () => wrap.style.bottom = (window.innerHeight - window.visualViewport.height) + 'px'); wrap.style.bottom = (window.innerHeight - window.visualViewport.height) + 'px'; wrap.innerHTML = ` <div style="font-size:30px;text-align:center;margin-bottom:14px;color:#4E6BF5">选择账号登录</div> <div style="display:grid;grid-template-columns:1fr 1px 1fr;border-top:1px solid #444"> ${list.map((rec, i) => { const last = i === list.length - 1; const oddLast = last && (i + 1) % 2 === 1; return ` <div data-idx="${i}" style="${oddLast ? 'grid-column:1/-1;' : ''}padding:5px 5px;display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:52px"> <div style="font-size:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${rec.account || '(空账号)'}</div> <div style="font-size:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${rec.password || '(空密码)'}</div> </div> ${(i + 1) % 2 === 1 && !last ? '<div style="background:#444"></div>' : ''} ${(i + 1) % 2 === 0 || last ? '<div style="height:1px;background:#444;grid-column:1/-1"></div>' : ''}`; }).join('')} </div> `.replace(/\s*\n\s*/g, ''); document.body.appendChild(wrap); const t = setTimeout(() => (wrap.remove(), resolve(null)), 5000); wrap.addEventListener('click', e => { if (!e.target.closest('[data-idx]')) return; const selected = list[Number(e.target.closest('[data-idx]').dataset.idx)]; sessionStorage[key] = JSON.stringify(selected); clearTimeout(t); wrap.remove(); resolve(selected); }); }); /* ---------- 管理面板 ---------- */ const openEditor = () => { document.documentElement.style.overflow = 'hidden'; const box = document.createElement('div');box.style.cssText = `width:95%; max-width:600px; max-height:90vh; overflow:auto; background:#262626; color:#eee; border-radius:6px; padding:15px;`; const wrap = document.createElement('div');wrap.id = 'WJ_tmCredEditor';wrap.style.cssText = `position:fixed; inset:0; background:rgba(0,0,0,.8); z-index:9999; display:flex; align-items:center; justify-content:center; font-family:system-ui`; box.innerHTML = ` <style> #WJ_tmCredEditor table{width:100%;border-collapse:collapse;table-layout:fixed;margin:0 -15px;width:calc(100% + 30px)} #WJ_tmCredEditor thead th{border:1px solid #555;padding:6px 4px;font-size:18px;text-align:center;color:#0E5484} #WJ_tmCredEditor tbody td{border:1px solid #555;height:50px;padding:0;text-align:center;vertical-align:middle;position:relative} #WJ_jsonArea{width:100%;height:150px;margin-top:10px;outline:none}#WJ_jsonArea::placeholder{font-size:20px;text-align:center} .WJ_scroll-box{height:50px;overflow:auto;background:#333} .WJ_center-box{min-height:50px;display:flex;align-items:center;justify-content:center;padding:0 4px;box-sizing:border-box;color:#eee;font-size:13px;line-height:1.2;text-align:center;white-space:pre-wrap;word-break:break-all;outline:none} .WJ_delete-btn{position:absolute;inset:0;background:#5D4401;color:#fff;font-size:14px;display:flex;align-items:center;justify-content:center;user-select:none} .WJ_bottom-bar{display:flex;margin:15px -15px 0 -15px} .WJ_bottom-bar button{flex:1;height:48px;font-size:16px;color:#157530;background:none;border:1px solid #bbb;border-right:none} .WJ_bottom-bar button:last-child{border-right:1px solid #bbb} </style> <h2 style="margin:0 0 12px;text-align:center;font-size:26px;color:#0E5484">账号密码管理</h2> <table id="WJ_credTable"> <thead> <tr> <th style="width:18%">标题</th> <th style="width:37%">网址</th> <th style="width:25%">账号</th> <th style="width:20%">密码</th> <th style="width:10%">删除</th> </tr> </thead> <tbody></tbody> </table> <div class="WJ_bottom-bar"> <button id="WJ_addBtn">新增</button> <button id="WJ_exportBtn">导出</button> <button id="WJ_importBtn">导入</button> <button id="WJ_closeBtn">关闭</button> </div> <textarea id="WJ_jsonArea" style="display:none" placeholder="粘贴格式:标题,网址,账号,密码 百度,https://www.baidu.com,zhanghao,mima"></textarea>`; wrap.appendChild(box); document.body.appendChild(wrap); const render = () => { const tbody = wrap.querySelector('#WJ_credTable tbody'); tbody.innerHTML = ''; loadAll().forEach((row, idx) => { const tr = tbody.insertRow(); ['title', 'url', 'account', 'password'].forEach(key => { const cell = tr.insertCell(); cell.innerHTML = '<div class="WJ_scroll-box"><div class="WJ_center-box" contenteditable spellcheck="false"></div></div>'; const inner = cell.querySelector('.WJ_center-box'); inner.textContent = row[key] || ''; inner.oninput = () => { const list = loadAll(); list[idx][key] = inner.textContent; saveAll(list); }; }); const delCell = tr.insertCell(); delCell.innerHTML = '<div class="WJ_delete-btn">❌</div>'; delCell.firstChild.onclick = () => { const list = loadAll(); list.splice(idx, 1); saveAll(list); render(); toast('已删除'); }; }); }; render(); wrap.querySelector('#WJ_closeBtn').onclick = () => {document.documentElement.style.overflow = '';wrap.remove()}; wrap.querySelector('#WJ_addBtn').onclick = () => (saveAll(loadAll().concat({ title:'', url:'', account:'', password:'' })), render()); wrap.querySelector('#WJ_exportBtn').onclick = () => navigator.clipboard.writeText(loadAll().map(r => [r.title, r.url, r.account, r.password].join(',')).join('\n')).then(() => toast('已导出到剪贴板')); wrap.querySelector('#WJ_importBtn').onclick = () => { const ta = wrap.querySelector('#WJ_jsonArea'); if (ta.style.display === 'none') { ta.style.display = 'block'; return; } const val = ta.value.trim(); if (!val) { ta.style.display = 'none'; return; } try { const list = loadAll(); val.split('\n').forEach(line => { const cells = line.split(','); if (cells.length !== 4) throw new Error('格式错误'); const [title, url, account, password] = cells; const idx = list.findIndex(it => it.title === title); idx >= 0 ? (list[idx] = { title, url, account, password }) : list.push({ title, url, account, password }); }); saveAll(list); render(); ta.value = ''; ta.style.display = 'none'; toast('导入成功'); } catch (e) { toast(e.message); } }; }; /* ---------- 启动 ---------- */ init(); GM_registerMenuCommand('扫描页面账号', scanP); GM_registerMenuCommand('账号密码管理', openEditor); }, 500);