快速切换多个账号
// ==UserScript== // @name NGA Account Switcher // @name:zh-CN NGA 账号切换 // @namespace https://greasyfork.org/users/263018 // @version 1.0.0 // @author snyssss // @description 快速切换多个账号 // @license MIT // @match *://bbs.nga.cn/* // @match *://ngabbs.com/* // @match *://nga.178.com/* // @require https://update.greasyfork.org/scripts/486070/1377381/NGA%20Library.js // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant unsafeWindow // @run-at document-start // ==/UserScript== (() => { // 声明泥潭主模块 let commonui; // 声明 UI let ui; // STYLE GM_addStyle(` .s-table-wrapper { max-height: 80vh; overflow-y: auto; } .s-table { margin: 0; } .s-table th, .s-table td { position: relative; white-space: nowrap; } .s-table th { position: sticky; top: 2px; z-index: 1; } .s-table input:not([type]), .s-table input[type="text"] { margin: 0; box-sizing: border-box; height: 100%; width: 100%; } .s-input-wrapper { position: absolute; top: 6px; right: 6px; bottom: 6px; left: 6px; } .s-text-ellipsis { display: flex; } .s-text-ellipsis > * { flex: 1; width: 1px; overflow: hidden; text-overflow: ellipsis; } .s-button-group { margin: -.1em -.2em; } `); /** * UI */ class UI { /** * 标签 */ static label = "账号切换"; /** * 弹出窗 */ window; /** * 视图元素 */ views = {}; /** * 初始化 */ constructor() { this.init(); } /** * 初始化,创建基础视图,初始化通用设置 */ init() { const tabs = this.createTabs({ className: "right_", }); const content = this.createElement("DIV", [], { style: "width: 400px;", }); const container = this.createElement("DIV", [tabs, content]); this.views = { tabs, content, container, }; } /** * 创建元素 * @param {String} tagName 标签 * @param {HTMLElement | HTMLElement[] | String} content 内容,元素或者 innerHTML * @param {*} properties 额外属性 * @returns {HTMLElement} 元素 */ createElement(tagName, content, properties = {}) { const element = document.createElement(tagName); // 写入内容 if (typeof content === "string") { element.innerHTML = content; } else { if (Array.isArray(content) === false) { content = [content]; } content.forEach((item) => { if (item === null) { return; } if (typeof item === "string") { element.append(item); return; } element.appendChild(item); }); } // 对 A 标签的额外处理 if (tagName.toUpperCase() === "A") { if (Object.hasOwn(properties, "href") === false) { properties.href = "javascript: void(0);"; } } // 附加属性 Object.entries(properties).forEach(([key, value]) => { element[key] = value; }); return element; } /** * 创建按钮 * @param {String} text 文字 * @param {Function} onclick 点击事件 * @param {*} properties 额外属性 */ createButton(text, onclick, properties = {}) { return this.createElement("BUTTON", text, { ...properties, onclick, }); } /** * 创建按钮组 * @param {Array} buttons 按钮集合 */ createButtonGroup(...buttons) { return this.createElement("DIV", buttons, { className: "s-button-group", }); } /** * 创建表格 * @param {Array} headers 表头集合 * @param {*} properties 额外属性 * @returns {HTMLElement} 元素和相关函数 */ createTable(headers, properties = {}) { const rows = []; const ths = headers.map((item, index) => this.createElement("TH", item.label, { ...item, className: `c${index + 1}`, }) ); const tr = ths.length > 0 ? this.createElement("TR", ths, { className: "block_txt_c0", }) : null; const thead = tr !== null ? this.createElement("THEAD", tr) : null; const tbody = this.createElement("TBODY", []); const table = this.createElement("TABLE", [thead, tbody], { ...properties, className: "s-table forumbox", }); const wrapper = this.createElement("DIV", table, { className: "s-table-wrapper", }); const intersectionObserver = new IntersectionObserver((entries) => { if (entries[0].intersectionRatio <= 0) return; const list = rows.splice(0, 10); if (list.length === 0) { return; } intersectionObserver.disconnect(); tbody.append(...list); intersectionObserver.observe(tbody.lastElementChild); }); const add = (...columns) => { const tds = columns.map((column, index) => { if (ths[index]) { const { center, ellipsis } = ths[index]; const properties = {}; if (center) { properties.style = "text-align: center;"; } if (ellipsis) { properties.className = "s-text-ellipsis"; } column = this.createElement("DIV", column, properties); } return this.createElement("TD", column, { className: `c${index + 1}`, }); }); const tr = this.createElement("TR", tds, { className: `row${(rows.length % 2) + 1}`, }); intersectionObserver.disconnect(); rows.push(tr); intersectionObserver.observe(tbody.lastElementChild || tbody); }; const update = (e, ...columns) => { const row = e.target.closest("TR"); if (row) { const tds = row.querySelectorAll("TD"); columns.map((column, index) => { if (ths[index]) { const { center, ellipsis } = ths[index]; const properties = {}; if (center) { properties.style = "text-align: center;"; } if (ellipsis) { properties.className = "s-text-ellipsis"; } column = this.createElement("DIV", column, properties); } if (tds[index]) { tds[index].innerHTML = ""; tds[index].append(column); } }); } }; const remove = (e) => { const row = e.target.closest("TR"); if (row) { tbody.removeChild(row); } }; const clear = () => { rows.splice(0); intersectionObserver.disconnect(); tbody.innerHTML = ""; }; Object.assign(wrapper, { add, update, remove, clear, }); return wrapper; } /** * 创建标签组 * @param {*} properties 额外属性 */ createTabs(properties = {}) { const tabs = this.createElement( "DIV", `<table class="stdbtn" cellspacing="0"> <tbody> <tr></tr> </tbody> </table>`, properties ); return this.createElement( "DIV", [ tabs, this.createElement("DIV", [], { className: "clear", }), ], { style: "display: none; margin-bottom: 5px;", } ); } /** * 创建标签 * @param {Element} tabs 标签组 * @param {String} label 标签名称 * @param {Number} order 标签顺序,重复则跳过 * @param {*} properties 额外属性 */ createTab(tabs, label, order, properties = {}) { const group = tabs.querySelector("TR"); const items = [...group.childNodes]; if (items.find((item) => item.order === order)) { return; } if (items.length > 0) { tabs.style.removeProperty("display"); } const tab = this.createElement("A", label, { ...properties, className: "nobr silver", onclick: () => { if (tab.className === "nobr") { return; } group.querySelectorAll("A").forEach((item) => { if (item === tab) { item.className = "nobr"; } else { item.className = "nobr silver"; } }); if (properties.onclick) { properties.onclick(); } }, }); const wrapper = this.createElement("TD", tab, { order, }); const anchor = items.find((item) => item.order > order); group.insertBefore(wrapper, anchor || null); return wrapper; } /** * 创建对话框 * @param {HTMLElement | null} anchor 要绑定的元素,如果为空,直接弹出 * @param {String} title 对话框的标题 * @param {HTMLElement} content 对话框的内容 */ createDialog(anchor, title, content) { let window; const show = () => { if (window === undefined) { window = commonui.createCommmonWindow(); } window._.addContent(null); window._.addTitle(title); window._.addContent(content); window._.show(); }; if (anchor) { anchor.onclick = show; } else { show(); } return window; } /** * 弹窗确认 * @param {String} message 提示信息 * @returns {Promise} */ confirm(message = "是否确认?") { return new Promise((resolve, reject) => { const r###lt = confirm(message); if (r###lt) { resolve(); return; } reject(); }); } /** * 渲染视图 */ renderView() { // 创建或打开弹出窗 if (this.window === undefined) { this.window = this.createDialog( this.views.anchor, this.constructor.label, this.views.container ); } else { this.window._.show(); } // 启用第一个模块 this.views.tabs.querySelector("A").click(); } /** * 渲染 */ render() { this.renderView(); } } /** * 基础模块 */ class Module { /** * 模块名称 */ static name; /** * 模块标签 */ static label; /** * 顺序 */ static order; /** * UI */ ui; /** * 视图元素 */ views = {}; /** * 初始化并绑定UI,注册 UI * @param {UI} ui UI */ constructor(ui) { this.ui = ui; this.init(); } /** * 获取列表 */ get list() { return GM_getValue(this.constructor.name, {}); } /** * 写入列表 */ set list(value) { GM_setValue(this.constructor.name, value); } /** * 初始化,创建基础视图和组件 */ init() { if (this.views.container) { this.destroy(); } const { ui } = this; const container = ui.createElement("DIV", []); this.views = { container, }; this.initComponents(); } /** * 初始化组件 */ initComponents() {} /** * 销毁 */ destroy() { Object.values(this.views).forEach((view) => { if (view.parentNode) { view.parentNode.removeChild(view); } }); this.views = {}; } /** * 渲染 * @param {HTMLElement} container 容器 */ render(container) { container.innerHTML = ""; container.appendChild(this.views.container); } } /** * 账号列表 */ class AccountList extends Module { /** * 模块名称 */ static name = "data"; /** * 模块标签 */ static label = "账号"; /** * 顺序 */ static order = 10; /** * 表格列 * @returns {Array} 表格列集合 */ columns() { return [ { label: "昵称" }, { label: "登录时间" }, { label: "操作", width: 1 }, ]; } /** * 表格项 * @param {Object} item 账号信息 * @returns {Array} 表格项集合 */ column(item) { const { ui } = this; const { table } = this.views; const { uid, username, timestamp } = item; // 昵称 const name = (() => { const label = username ? "@" + username : "#" + uid; return ui.createElement("A", `[${label}]`, { className: "b nobr", href: `/nuke.php?func=ucp&uid=${uid}`, }); })(); // 登录时间 const time = ui.createElement( "SPAN", commonui.time2dis(timestamp / 1000), { className: "nobr", } ); // 操作 const buttons = (() => { const toggle = ui.createButton("切换", (e) => { loadData(uid).catch((err) => { alert(err.message); removeData(uid); table.remove(e); }); }); const remove = ui.createButton("删除", (e) => { ui.confirm().then(() => { removeData(uid); table.remove(e); }); }); if (unsafeWindow.__CURRENT_UID === uid) { return ui.createButtonGroup(remove); } return ui.createButtonGroup(toggle, remove); })(); return [name, time, buttons]; } /** * 初始化组件 */ initComponents() { super.initComponents(); const { tabs, content } = this.ui.views; const table = this.ui.createTable(this.columns()); const tab = this.ui.createTab( tabs, this.constructor.label, this.constructor.order, { onclick: () => { this.render(content); }, } ); Object.assign(this.views, { tab, table, }); this.views.container.appendChild(table); } /** * 渲染 * @param {HTMLElement} container 容器 */ render(container) { super.render(container); const { list } = this; const { table } = this.views; if (table) { const { add, clear } = table; clear(); Object.values(list).forEach((item) => { const column = this.column(item); add(...column); }); } } } /** * 渲染 UI */ const renderUI = () => { if (commonui && commonui.mainMenuItems) { if (ui === undefined) { ui = new UI(); new AccountList(ui); } ui.render(); } }; /** * 处理 commonui 模块 * @param {*} value commonui */ const handleCommonui = (value) => { // 绑定主模块 commonui = value; // 拦截 mainMenu 模块,处理 init 事件 Tools.interceptProperty(commonui, "mainMenu", { afterSet: (mainMenu) => { // 加入菜单 if (mainMenu && mainMenu.addItemOnTheFly) { mainMenu.addItemOnTheFly(`账号切换`, null, renderUI); } }, }); }; /** * 注册脚本菜单 */ const registerMenu = () => { GM_registerMenuCommand(`账号切换`, renderUI); }; /** * 拦截登录页面 */ const handleLogin = () => { if (unsafeWindow.document.title === "账号操作") { // 处理 __API 模块 const handleLoginAPI = (value) => { if (value) { // 拦截 get 方法,从中取得登录成功后的信息 Tools.interceptProperty(value, "get", { beforeGet: (...args) => { if (args[0] === "loginSuccess") { const { uid, username, token } = JSON.parse(args[1]); saveData(uid, username, token); } return args; }, }); } }; if (unsafeWindow.__API) { handleLoginAPI(unsafeWindow.__API); return; } Tools.interceptProperty(unsafeWindow, "__API", { afterSet: (value) => { handleLoginAPI(value); }, }); } }; /** * 载入数据 * @param {String} uid 用户 ID */ const loadData = async (uid) => { const list = GM_getValue(AccountList.name, {}); const item = list[uid]; if (item) { const { cid } = item; const url = `/nuke.php?__lib=login&__act=login_set_cookie_quick`; const form = new FormData(); form.append("uid", uid); form.append("cid", cid); const response = await fetch(url, { method: "POST", body: form, }); const r###lt = await Tools.readForumData(response, false); const parser = new DOMParser(); const doc = parser.parseFromString(r###lt, "text/html"); const message = doc.body.innerText.replace(/\s/g, ""); if (message === "SUCCESS") { unsafeWindow.location.reload(); return; } } throw new Error("登录状态失效,请重新登录"); }; /** * 保存数据 */ const saveData = (uid, username, cid) => { const list = GM_getValue(AccountList.name, {}); list[uid] = { uid, username, cid, timestamp: new Date().getTime(), }; GM_setValue(AccountList.name, list); }; /** * 删除数据 * @param {String} uid 用户 ID */ const removeData = (uid) => { const list = GM_getValue(AccountList.name, {}); delete list[uid]; GM_setValue(AccountList.name, list); }; // 主函数 (async () => { // 注册脚本菜单 registerMenu(); // 拦截登录页面 handleLogin(); // 处理 commonui 模块 if (unsafeWindow.commonui) { handleCommonui(unsafeWindow.commonui); return; } Tools.interceptProperty(unsafeWindow, "commonui", { afterSet: (value) => { handleCommonui(value); }, }); })(); })();