NGA Account Switcher

快速切换多个账号

  1. // ==UserScript==
  2. // @name NGA Account Switcher
  3. // @name:zh-CN NGA 账号切换
  4. // @namespace https://gf.qytechs.cn/users/263018
  5. // @version 1.0.0
  6. // @author snyssss
  7. // @description 快速切换多个账号
  8. // @license MIT
  9.  
  10. // @match *://bbs.nga.cn/*
  11. // @match *://ngabbs.com/*
  12. // @match *://nga.178.com/*
  13.  
  14. // @require https://update.gf.qytechs.cn/scripts/486070/1377381/NGA%20Library.js
  15.  
  16. // @grant GM_addStyle
  17. // @grant GM_setValue
  18. // @grant GM_getValue
  19. // @grant GM_registerMenuCommand
  20. // @grant unsafeWindow
  21.  
  22. // @run-at document-start
  23. // ==/UserScript==
  24.  
  25. (() => {
  26. // 声明泥潭主模块
  27. let commonui;
  28.  
  29. // 声明 UI
  30. let ui;
  31.  
  32. // STYLE
  33. GM_addStyle(`
  34. .s-table-wrapper {
  35. max-height: 80vh;
  36. overflow-y: auto;
  37. }
  38. .s-table {
  39. margin: 0;
  40. }
  41. .s-table th,
  42. .s-table td {
  43. position: relative;
  44. white-space: nowrap;
  45. }
  46. .s-table th {
  47. position: sticky;
  48. top: 2px;
  49. z-index: 1;
  50. }
  51. .s-table input:not([type]), .s-table input[type="text"] {
  52. margin: 0;
  53. box-sizing: border-box;
  54. height: 100%;
  55. width: 100%;
  56. }
  57. .s-input-wrapper {
  58. position: absolute;
  59. top: 6px;
  60. right: 6px;
  61. bottom: 6px;
  62. left: 6px;
  63. }
  64. .s-text-ellipsis {
  65. display: flex;
  66. }
  67. .s-text-ellipsis > * {
  68. flex: 1;
  69. width: 1px;
  70. overflow: hidden;
  71. text-overflow: ellipsis;
  72. }
  73. .s-button-group {
  74. margin: -.1em -.2em;
  75. }
  76. `);
  77.  
  78. /**
  79. * UI
  80. */
  81. class UI {
  82. /**
  83. * 标签
  84. */
  85. static label = "账号切换";
  86.  
  87. /**
  88. * 弹出窗
  89. */
  90. window;
  91.  
  92. /**
  93. * 视图元素
  94. */
  95. views = {};
  96.  
  97. /**
  98. * 初始化
  99. */
  100. constructor() {
  101. this.init();
  102. }
  103.  
  104. /**
  105. * 初始化,创建基础视图,初始化通用设置
  106. */
  107. init() {
  108. const tabs = this.createTabs({
  109. className: "right_",
  110. });
  111.  
  112. const content = this.createElement("DIV", [], {
  113. style: "width: 400px;",
  114. });
  115.  
  116. const container = this.createElement("DIV", [tabs, content]);
  117.  
  118. this.views = {
  119. tabs,
  120. content,
  121. container,
  122. };
  123. }
  124.  
  125. /**
  126. * 创建元素
  127. * @param {String} tagName 标签
  128. * @param {HTMLElement | HTMLElement[] | String} content 内容,元素或者 innerHTML
  129. * @param {*} properties 额外属性
  130. * @returns {HTMLElement} 元素
  131. */
  132. createElement(tagName, content, properties = {}) {
  133. const element = document.createElement(tagName);
  134.  
  135. // 写入内容
  136. if (typeof content === "string") {
  137. element.innerHTML = content;
  138. } else {
  139. if (Array.isArray(content) === false) {
  140. content = [content];
  141. }
  142.  
  143. content.forEach((item) => {
  144. if (item === null) {
  145. return;
  146. }
  147.  
  148. if (typeof item === "string") {
  149. element.append(item);
  150. return;
  151. }
  152.  
  153. element.appendChild(item);
  154. });
  155. }
  156.  
  157. // 对 A 标签的额外处理
  158. if (tagName.toUpperCase() === "A") {
  159. if (Object.hasOwn(properties, "href") === false) {
  160. properties.href = "javascript: void(0);";
  161. }
  162. }
  163.  
  164. // 附加属性
  165. Object.entries(properties).forEach(([key, value]) => {
  166. element[key] = value;
  167. });
  168.  
  169. return element;
  170. }
  171.  
  172. /**
  173. * 创建按钮
  174. * @param {String} text 文字
  175. * @param {Function} onclick 点击事件
  176. * @param {*} properties 额外属性
  177. */
  178. createButton(text, onclick, properties = {}) {
  179. return this.createElement("BUTTON", text, {
  180. ...properties,
  181. onclick,
  182. });
  183. }
  184.  
  185. /**
  186. * 创建按钮组
  187. * @param {Array} buttons 按钮集合
  188. */
  189. createButtonGroup(...buttons) {
  190. return this.createElement("DIV", buttons, {
  191. className: "s-button-group",
  192. });
  193. }
  194.  
  195. /**
  196. * 创建表格
  197. * @param {Array} headers 表头集合
  198. * @param {*} properties 额外属性
  199. * @returns {HTMLElement} 元素和相关函数
  200. */
  201. createTable(headers, properties = {}) {
  202. const rows = [];
  203.  
  204. const ths = headers.map((item, index) =>
  205. this.createElement("TH", item.label, {
  206. ...item,
  207. className: `c${index + 1}`,
  208. })
  209. );
  210.  
  211. const tr =
  212. ths.length > 0
  213. ? this.createElement("TR", ths, {
  214. className: "block_txt_c0",
  215. })
  216. : null;
  217.  
  218. const thead = tr !== null ? this.createElement("THEAD", tr) : null;
  219.  
  220. const tbody = this.createElement("TBODY", []);
  221.  
  222. const table = this.createElement("TABLE", [thead, tbody], {
  223. ...properties,
  224. className: "s-table forumbox",
  225. });
  226.  
  227. const wrapper = this.createElement("DIV", table, {
  228. className: "s-table-wrapper",
  229. });
  230.  
  231. const intersectionObserver = new IntersectionObserver((entries) => {
  232. if (entries[0].intersectionRatio <= 0) return;
  233.  
  234. const list = rows.splice(0, 10);
  235.  
  236. if (list.length === 0) {
  237. return;
  238. }
  239.  
  240. intersectionObserver.disconnect();
  241.  
  242. tbody.append(...list);
  243.  
  244. intersectionObserver.observe(tbody.lastElementChild);
  245. });
  246.  
  247. const add = (...columns) => {
  248. const tds = columns.map((column, index) => {
  249. if (ths[index]) {
  250. const { center, ellipsis } = ths[index];
  251.  
  252. const properties = {};
  253.  
  254. if (center) {
  255. properties.style = "text-align: center;";
  256. }
  257.  
  258. if (ellipsis) {
  259. properties.className = "s-text-ellipsis";
  260. }
  261.  
  262. column = this.createElement("DIV", column, properties);
  263. }
  264.  
  265. return this.createElement("TD", column, {
  266. className: `c${index + 1}`,
  267. });
  268. });
  269.  
  270. const tr = this.createElement("TR", tds, {
  271. className: `row${(rows.length % 2) + 1}`,
  272. });
  273.  
  274. intersectionObserver.disconnect();
  275.  
  276. rows.push(tr);
  277.  
  278. intersectionObserver.observe(tbody.lastElementChild || tbody);
  279. };
  280.  
  281. const update = (e, ...columns) => {
  282. const row = e.target.closest("TR");
  283.  
  284. if (row) {
  285. const tds = row.querySelectorAll("TD");
  286.  
  287. columns.map((column, index) => {
  288. if (ths[index]) {
  289. const { center, ellipsis } = ths[index];
  290.  
  291. const properties = {};
  292.  
  293. if (center) {
  294. properties.style = "text-align: center;";
  295. }
  296.  
  297. if (ellipsis) {
  298. properties.className = "s-text-ellipsis";
  299. }
  300.  
  301. column = this.createElement("DIV", column, properties);
  302. }
  303.  
  304. if (tds[index]) {
  305. tds[index].innerHTML = "";
  306. tds[index].append(column);
  307. }
  308. });
  309. }
  310. };
  311.  
  312. const remove = (e) => {
  313. const row = e.target.closest("TR");
  314.  
  315. if (row) {
  316. tbody.removeChild(row);
  317. }
  318. };
  319.  
  320. const clear = () => {
  321. rows.splice(0);
  322. intersectionObserver.disconnect();
  323.  
  324. tbody.innerHTML = "";
  325. };
  326.  
  327. Object.assign(wrapper, {
  328. add,
  329. update,
  330. remove,
  331. clear,
  332. });
  333.  
  334. return wrapper;
  335. }
  336.  
  337. /**
  338. * 创建标签组
  339. * @param {*} properties 额外属性
  340. */
  341. createTabs(properties = {}) {
  342. const tabs = this.createElement(
  343. "DIV",
  344. `<table class="stdbtn" cellspacing="0">
  345. <tbody>
  346. <tr></tr>
  347. </tbody>
  348. </table>`,
  349. properties
  350. );
  351.  
  352. return this.createElement(
  353. "DIV",
  354. [
  355. tabs,
  356. this.createElement("DIV", [], {
  357. className: "clear",
  358. }),
  359. ],
  360. {
  361. style: "display: none; margin-bottom: 5px;",
  362. }
  363. );
  364. }
  365.  
  366. /**
  367. * 创建标签
  368. * @param {Element} tabs 标签组
  369. * @param {String} label 标签名称
  370. * @param {Number} order 标签顺序,重复则跳过
  371. * @param {*} properties 额外属性
  372. */
  373. createTab(tabs, label, order, properties = {}) {
  374. const group = tabs.querySelector("TR");
  375.  
  376. const items = [...group.childNodes];
  377.  
  378. if (items.find((item) => item.order === order)) {
  379. return;
  380. }
  381.  
  382. if (items.length > 0) {
  383. tabs.style.removeProperty("display");
  384. }
  385.  
  386. const tab = this.createElement("A", label, {
  387. ...properties,
  388. className: "nobr silver",
  389. onclick: () => {
  390. if (tab.className === "nobr") {
  391. return;
  392. }
  393.  
  394. group.querySelectorAll("A").forEach((item) => {
  395. if (item === tab) {
  396. item.className = "nobr";
  397. } else {
  398. item.className = "nobr silver";
  399. }
  400. });
  401.  
  402. if (properties.onclick) {
  403. properties.onclick();
  404. }
  405. },
  406. });
  407.  
  408. const wrapper = this.createElement("TD", tab, {
  409. order,
  410. });
  411.  
  412. const anchor = items.find((item) => item.order > order);
  413.  
  414. group.insertBefore(wrapper, anchor || null);
  415.  
  416. return wrapper;
  417. }
  418.  
  419. /**
  420. * 创建对话框
  421. * @param {HTMLElement | null} anchor 要绑定的元素,如果为空,直接弹出
  422. * @param {String} title 对话框的标题
  423. * @param {HTMLElement} content 对话框的内容
  424. */
  425. createDialog(anchor, title, content) {
  426. let window;
  427.  
  428. const show = () => {
  429. if (window === undefined) {
  430. window = commonui.createCommmonWindow();
  431. }
  432.  
  433. window._.addContent(null);
  434. window._.addTitle(title);
  435. window._.addContent(content);
  436. window._.show();
  437. };
  438.  
  439. if (anchor) {
  440. anchor.onclick = show;
  441. } else {
  442. show();
  443. }
  444.  
  445. return window;
  446. }
  447.  
  448. /**
  449. * 弹窗确认
  450. * @param {String} message 提示信息
  451. * @returns {Promise}
  452. */
  453. confirm(message = "是否确认?") {
  454. return new Promise((resolve, reject) => {
  455. const result = confirm(message);
  456.  
  457. if (result) {
  458. resolve();
  459. return;
  460. }
  461.  
  462. reject();
  463. });
  464. }
  465.  
  466. /**
  467. * 渲染视图
  468. */
  469. renderView() {
  470. // 创建或打开弹出窗
  471. if (this.window === undefined) {
  472. this.window = this.createDialog(
  473. this.views.anchor,
  474. this.constructor.label,
  475. this.views.container
  476. );
  477. } else {
  478. this.window._.show();
  479. }
  480.  
  481. // 启用第一个模块
  482. this.views.tabs.querySelector("A").click();
  483. }
  484.  
  485. /**
  486. * 渲染
  487. */
  488. render() {
  489. this.renderView();
  490. }
  491. }
  492.  
  493. /**
  494. * 基础模块
  495. */
  496. class Module {
  497. /**
  498. * 模块名称
  499. */
  500. static name;
  501.  
  502. /**
  503. * 模块标签
  504. */
  505. static label;
  506.  
  507. /**
  508. * 顺序
  509. */
  510. static order;
  511.  
  512. /**
  513. * UI
  514. */
  515. ui;
  516.  
  517. /**
  518. * 视图元素
  519. */
  520. views = {};
  521.  
  522. /**
  523. * 初始化并绑定UI,注册(不可用) UI
  524. * @param {UI} ui UI
  525. */
  526. constructor(ui) {
  527. this.ui = ui;
  528.  
  529. this.init();
  530. }
  531.  
  532. /**
  533. * 获取列表
  534. */
  535. get list() {
  536. return GM_getValue(this.constructor.name, {});
  537. }
  538.  
  539. /**
  540. * 写入列表
  541. */
  542. set list(value) {
  543. GM_setValue(this.constructor.name, value);
  544. }
  545.  
  546. /**
  547. * 初始化,创建基础视图和组件
  548. */
  549. init() {
  550. if (this.views.container) {
  551. this.destroy();
  552. }
  553.  
  554. const { ui } = this;
  555.  
  556. const container = ui.createElement("DIV", []);
  557.  
  558. this.views = {
  559. container,
  560. };
  561.  
  562. this.initComponents();
  563. }
  564.  
  565. /**
  566. * 初始化组件
  567. */
  568. initComponents() {}
  569.  
  570. /**
  571. * 销毁
  572. */
  573. destroy() {
  574. Object.values(this.views).forEach((view) => {
  575. if (view.parentNode) {
  576. view.parentNode.removeChild(view);
  577. }
  578. });
  579.  
  580. this.views = {};
  581. }
  582.  
  583. /**
  584. * 渲染
  585. * @param {HTMLElement} container 容器
  586. */
  587. render(container) {
  588. container.innerHTML = "";
  589. container.appendChild(this.views.container);
  590. }
  591. }
  592.  
  593. /**
  594. * 账号列表
  595. */
  596. class AccountList extends Module {
  597. /**
  598. * 模块名称
  599. */
  600. static name = "data";
  601.  
  602. /**
  603. * 模块标签
  604. */
  605. static label = "账号";
  606.  
  607. /**
  608. * 顺序
  609. */
  610. static order = 10;
  611.  
  612. /**
  613. * 表格列
  614. * @returns {Array} 表格列集合
  615. */
  616. columns() {
  617. return [
  618. { label: "昵称" },
  619. { label: "登录(不可用)时间" },
  620. { label: "操作", width: 1 },
  621. ];
  622. }
  623.  
  624. /**
  625. * 表格项
  626. * @param {Object} item 账号信息
  627. * @returns {Array} 表格项集合
  628. */
  629. column(item) {
  630. const { ui } = this;
  631.  
  632. const { table } = this.views;
  633.  
  634. const { uid, username, timestamp } = item;
  635.  
  636. // 昵称
  637. const name = (() => {
  638. const label = username ? "@" + username : "#" + uid;
  639.  
  640. return ui.createElement("A", `[${label}]`, {
  641. className: "b nobr",
  642. href: `/nuke.php?func=ucp&uid=${uid}`,
  643. });
  644. })();
  645.  
  646. // 登录(不可用)时间
  647. const time = ui.createElement(
  648. "SPAN",
  649. commonui.time2dis(timestamp / 1000),
  650. {
  651. className: "nobr",
  652. }
  653. );
  654.  
  655. // 操作
  656. const buttons = (() => {
  657. const toggle = ui.createButton("切换", (e) => {
  658. loadData(uid).catch((err) => {
  659. alert(err.message);
  660.  
  661. removeData(uid);
  662.  
  663. table.remove(e);
  664. });
  665. });
  666.  
  667. const remove = ui.createButton("删除", (e) => {
  668. ui.confirm().then(() => {
  669. removeData(uid);
  670.  
  671. table.remove(e);
  672. });
  673. });
  674.  
  675. if (unsafeWindow.__CURRENT_UID === uid) {
  676. return ui.createButtonGroup(remove);
  677. }
  678.  
  679. return ui.createButtonGroup(toggle, remove);
  680. })();
  681.  
  682. return [name, time, buttons];
  683. }
  684.  
  685. /**
  686. * 初始化组件
  687. */
  688. initComponents() {
  689. super.initComponents();
  690.  
  691. const { tabs, content } = this.ui.views;
  692.  
  693. const table = this.ui.createTable(this.columns());
  694.  
  695. const tab = this.ui.createTab(
  696. tabs,
  697. this.constructor.label,
  698. this.constructor.order,
  699. {
  700. onclick: () => {
  701. this.render(content);
  702. },
  703. }
  704. );
  705.  
  706. Object.assign(this.views, {
  707. tab,
  708. table,
  709. });
  710.  
  711. this.views.container.appendChild(table);
  712. }
  713.  
  714. /**
  715. * 渲染
  716. * @param {HTMLElement} container 容器
  717. */
  718. render(container) {
  719. super.render(container);
  720.  
  721. const { list } = this;
  722.  
  723. const { table } = this.views;
  724.  
  725. if (table) {
  726. const { add, clear } = table;
  727.  
  728. clear();
  729.  
  730. Object.values(list).forEach((item) => {
  731. const column = this.column(item);
  732.  
  733. add(...column);
  734. });
  735. }
  736. }
  737. }
  738.  
  739. /**
  740. * 渲染 UI
  741. */
  742. const renderUI = () => {
  743. if (commonui && commonui.mainMenuItems) {
  744. if (ui === undefined) {
  745. ui = new UI();
  746.  
  747. new AccountList(ui);
  748. }
  749.  
  750. ui.render();
  751. }
  752. };
  753.  
  754. /**
  755. * 处理 commonui 模块
  756. * @param {*} value commonui
  757. */
  758. const handleCommonui = (value) => {
  759. // 绑定主模块
  760. commonui = value;
  761.  
  762. // 拦截 mainMenu 模块,处理 init 事件
  763. Tools.interceptProperty(commonui, "mainMenu", {
  764. afterSet: (mainMenu) => {
  765. // 加入菜单
  766. if (mainMenu && mainMenu.addItemOnTheFly) {
  767. mainMenu.addItemOnTheFly(`账号切换`, null, renderUI);
  768. }
  769. },
  770. });
  771. };
  772.  
  773. /**
  774. * 注册(不可用)脚本菜单
  775. */
  776. const registerMenu = () => {
  777. GM_registerMenuCommand(`账号切换`, renderUI);
  778. };
  779.  
  780. /**
  781. * 拦截登录(不可用)页面
  782. */
  783. const handleLogin = () => {
  784. if (unsafeWindow.document.title === "账号操作") {
  785. // 处理 __API 模块
  786. const handleLoginAPI = (value) => {
  787. if (value) {
  788. // 拦截 get 方法,从中取得登录(不可用)成功后的信息
  789. Tools.interceptProperty(value, "get", {
  790. beforeGet: (...args) => {
  791. if (args[0] === "loginSuccess") {
  792. const { uid, username, token } = JSON.parse(args[1]);
  793.  
  794. saveData(uid, username, token);
  795. }
  796.  
  797. return args;
  798. },
  799. });
  800. }
  801. };
  802.  
  803. if (unsafeWindow.__API) {
  804. handleLoginAPI(unsafeWindow.__API);
  805. return;
  806. }
  807.  
  808. Tools.interceptProperty(unsafeWindow, "__API", {
  809. afterSet: (value) => {
  810. handleLoginAPI(value);
  811. },
  812. });
  813. }
  814. };
  815.  
  816. /**
  817. * 载入数据
  818. * @param {String} uid 用户 ID
  819. */
  820. const loadData = async (uid) => {
  821. const list = GM_getValue(AccountList.name, {});
  822. const item = list[uid];
  823.  
  824. if (item) {
  825. const { cid } = item;
  826.  
  827. const url = `/nuke.php?__lib=login&__act=login_set_cookie_quick`;
  828.  
  829. const form = new FormData();
  830.  
  831. form.append("uid", uid);
  832. form.append("cid", cid);
  833.  
  834. const response = await fetch(url, {
  835. method: "POST",
  836. body: form,
  837. });
  838.  
  839. const result = await Tools.readForumData(response, false);
  840.  
  841. const parser = new DOMParser();
  842.  
  843. const doc = parser.parseFromString(result, "text/html");
  844.  
  845. const message = doc.body.innerText.replace(/\s/g, "");
  846.  
  847. if (message === "SUCCESS") {
  848. unsafeWindow.location.reload();
  849. return;
  850. }
  851. }
  852.  
  853. throw new Error("登录(不可用)状态失效,请重新登录(不可用)");
  854. };
  855.  
  856. /**
  857. * 保存数据
  858. */
  859. const saveData = (uid, username, cid) => {
  860. const list = GM_getValue(AccountList.name, {});
  861.  
  862. list[uid] = {
  863. uid,
  864. username,
  865. cid,
  866. timestamp: new Date().getTime(),
  867. };
  868.  
  869. GM_setValue(AccountList.name, list);
  870. };
  871.  
  872. /**
  873. * 删除数据
  874. * @param {String} uid 用户 ID
  875. */
  876. const removeData = (uid) => {
  877. const list = GM_getValue(AccountList.name, {});
  878.  
  879. delete list[uid];
  880.  
  881. GM_setValue(AccountList.name, list);
  882. };
  883.  
  884. // 主函数
  885. (async () => {
  886. // 注册(不可用)脚本菜单
  887. registerMenu();
  888.  
  889. // 拦截登录(不可用)页面
  890. handleLogin();
  891.  
  892. // 处理 commonui 模块
  893. if (unsafeWindow.commonui) {
  894. handleCommonui(unsafeWindow.commonui);
  895. return;
  896. }
  897.  
  898. Tools.interceptProperty(unsafeWindow, "commonui", {
  899. afterSet: (value) => {
  900. handleCommonui(value);
  901. },
  902. });
  903. })();
  904. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址