您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
账号密码自动填充 支持多账号 两步登录
// ==UserScript== // @name 账号密码自动填充 // @description 账号密码自动填充 支持多账号 两步登录 // @version 1.0 // @author WJ // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @run-at document-end // @license MIT // @namespace https://greasyfork.org/users/914996 // ==/UserScript== (() => { 'use strict'; const STORAGE_KEY = 'siteCredentials'; /* ---------- 初始化 ---------- */ let Observer = null; const init = () => { const matched = loadAll().filter(it => location.href.startsWith(it.url)); if (!matched.length) return; const stopObserver = () => { Observer?.disconnect(); Observer = null }; stopObserver(); Observer = new MutationObserver(tryFill); Observer.observe(document.body, { childList: true, subtree: true, attributes: true }); addEventListener('beforeunload', stopObserver, { once: true }); tryFill(); }; /* ---------- 查找输入框 ---------- */ const findInputs = () => { const all = []; const walk = r => r.querySelectorAll('*').forEach(n => { if (n.tagName === 'INPUT') { const { display, visibility } = getComputedStyle(n); display !== 'none' && visibility !== 'hidden' && all.push(n); } n.shadowRoot && walk(n.shadowRoot); }); walk(document.body); const sort = a => a.sort((x, y) => (x.tabIndex || 0) - (y.tabIndex || 0))[0]; return { user: sort(all.filter(n => /^(text|email|tel|number)$/i.test(n.type) && /user|login|mail|phone|手机|邮箱|账号|用户名|账户|id/i.test(`${n.name}|${n.placeholder}|${n.id}`))), pwd: sort(all.filter(n => n.type === 'password')) }; }; /* ---------- 自动填充 ---------- */ let isFilling = false; const tryFill = async () => { if (isFilling) return; isFilling = true; const matched = loadAll().filter(it => location.href.startsWith(it.url)); const rec = matched.length === 0 ? null : matched.length === 1 ? matched[0] : await pickAccount(matched); if (rec) { const { user, pwd } = findInputs(); if (user && rec.account && !user.value && !user.hasAttribute('filled-u')) { setValue(user, rec.account); user.setAttribute('filled-u', ''); } if (pwd && rec.password && !pwd.value && !pwd.hasAttribute('filled-p')) { setValue(pwd, rec.password); pwd.setAttribute('filled-p', ''); } } isFilling = false; }; /* ---------- 扫描 ---------- */ const scanPage = () => { const url = location.origin + location.pathname; const title = document.title || '无标题'; const { user, pwd } = findInputs(); 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 = () => { try { return JSON.parse(GM_getValue(STORAGE_KEY, '[]')); } catch { return []; } }; const saveAll = arr => GM_setValue(STORAGE_KEY, JSON.stringify(arr)); const setValue = (el, val) => { if (!el) return; const set = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set; set ? (set.call(el, val), el.dispatchEvent(new Event('input', { bubbles: true }))) : (el.value = val, ['input', 'change', 'blur'].forEach(evt => el.dispatchEvent(new Event(evt, { bubbles: true })))); }; /* ---------- Toast ---------- */ const toast = (m, d=3000) => { const t = document.body.appendChild(document.createElement('div')); t.textContent = m; t.style.cssText = 'position:fixed;left:50%;bottom:80px;transform:translateX(-50%);background:rgba(0,0,0,.75);color:#fff;padding:12px 20px;border-radius:4px;font-size:18px;z-index:99999;transition:opacity .3s;border:2px solid #5FB66B'; setTimeout(() => (t.style.opacity = 0, setTimeout(t.remove, 300)), d); }; /* ---------- 底部卡片选择 ---------- */ 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.id = 'WJ_bottomCards'; wrap.style.cssText = ` position:fixed;left:0;right:0;bottom:0;z-index:99999; background:#1e1e1e;color:#eee;padding:12px 16px 20px; box-shadow:0 -2px 12px rgba(0,0,0,.6);font-family:sans-serif`; wrap.innerHTML = ` <div style="font-size:30px;font-weight:bold;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 onclick="this.resolve()" style="${oddLast ? 'grid-column:1/-1;' : ''}padding:14px 8px;cursor:pointer;display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:52px;transition:.2s" onmouseenter="this.style.background='#333'" onmouseleave="this.style.background=''"> <div style="font-size:20px;color:#4E6BF5;margin-bottom:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${rec.title || '未命名'}</div> <div style="font-size:15px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${rec.account || '(空账号)'}</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, ''); wrap.querySelectorAll('[onclick]').forEach((c, i) => { c.resolve = () => { sessionStorage[key] = JSON.stringify(list[i]); wrap.remove(); resolve(list[i]); }; }); document.body.appendChild(wrap); const t = setTimeout(() => wrap.remove(), 5000); wrap.onclick = () => clearTimeout(t); }); /* ---------- 管理面板 ---------- */ const openEditor = () => { const wrap = document.createElement('div'); wrap.id = 'WJ_tmCredEditor'; wrap.style.cssText = `position:fixed; inset:0; background:rgba(0,0,0,.5); z-index:9999; display:flex; align-items:center; justify-content:center; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;`; 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;`; 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:13px;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;}#WJ_jsonArea::placeholder{font-size:20px;text-align:center;} .WJ_scroll-box{height:50px;overflow:auto;background:#333;cursor:text} .WJ_center-box:focus,#WJ_jsonArea:focus,.WJ_bottom-bar button:focus{outline:none} .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;cursor:pointer;display:flex;align-items:center;justify-content:center;user-select:none;border:none;margin:0;padding:0;box-sizing:border-box} .WJ_delete-btn:hover{opacity:.9} .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;cursor:pointer} .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 = document.createElement('tr'); ['title', 'url', 'account', 'password'].forEach(key => { const td = document.createElement('td'); const scroll = document.createElement('div'); const inner = document.createElement('div'); scroll.className = 'WJ_scroll-box'; inner.className = 'WJ_center-box'; inner.contentEditable = true; inner.spellcheck = false; inner.textContent = row[key] || ''; inner.oninput = () => { const list = loadAll(); list[idx][key] = inner.textContent; saveAll(list); }; scroll.appendChild(inner); td.appendChild(scroll); tr.appendChild(td); }); const delTd = document.createElement('td'); const delBtn = document.createElement('div'); delBtn.textContent = '×'; delBtn.className = 'WJ_delete-btn'; delBtn.onclick = () => { const list = loadAll(); list.splice(idx, 1); saveAll(list); render(); toast('已删除'); }; delTd.appendChild(delBtn); tr.appendChild(delTd); tbody.appendChild(tr); }); }; render(); wrap.querySelector('#WJ_closeBtn').onclick = () => wrap.remove(); wrap.querySelector('#WJ_exportBtn').onclick = () => { const rows = loadAll().map(r => [r.title, r.url, r.account, r.password].join(',')).join('\n'); navigator.clipboard.writeText(rows).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 arr = val.split('\n').map(line => { const cells = line.split(','); if (cells.length !== 4) throw new Error('格式错误'); return { title: cells[0], url: cells[1], account: cells[2], password: cells[3] }; }); const list = loadAll(); arr.forEach(r => { const idx = list.findIndex(it => it.title === r.title); idx >= 0 ? (list[idx] = r) : list.push(r); }); saveAll(list); render(); ta.style.display='none'; ta.value=''; toast('导入成功'); } catch(e) { toast(e.message); } }; wrap.querySelector('#WJ_addBtn').onclick = () => { const list = loadAll(); list.push({ title: '', url: '', account: '', password: '' }); saveAll(list); render(); }; }; /* ---------- 启动 ---------- */ init(); GM_registerMenuCommand('扫描页面账号', scanPage); GM_registerMenuCommand('账号密码管理', openEditor); })();