TC開發增强

快速切換TC帳號

  1. // ==UserScript==
  2. // @name TC dev enhance
  3. // @name:zh-CN TC开发增强
  4. // @name:zh-TW TC開發增强
  5. // @license MIT
  6. // @namespace Violentmonkey Scripts
  7. // @homepageURL https://github.com/kikyous/tc-dev-enhance
  8. // @match *://localhost:5000/*
  9. // @match *://lms-stg.tronclass.com.cn/*
  10. // @match *://lms-qa.tronclass.com.cn/*
  11. // @match *://lms-product.tronclass.com.cn/*
  12. // @match *://lms-university.tronclass.com.cn/*
  13. // @match *://lms-university-fmmu.tronclass.com.cn/*
  14. // @noframes
  15. // @version 2.6
  16. // @author chen
  17. // @description Switch TC accounts conveniently
  18. // @description:zh-CN 快速切换TC账号
  19. // @description:zh-TW 快速切換TC帳號
  20. // @grant GM.registerMenuCommand
  21. // ==/UserScript==
  22.  
  23.  
  24. const logout = () => {
  25. return fetch("/api/logout", {
  26. headers: { "content-type": "application/json;charset=UTF-8" },
  27. body: JSON.stringify({}),
  28. method: "POST",
  29. }).catch(() => true);
  30. };
  31.  
  32. const login = (username, password, orgId = '', credentials = 'same-origin') => {
  33. const data = {
  34. user_name: username,
  35. password: password,
  36. remember: true,
  37. };
  38. if (orgId) {
  39. data.org_id = orgId;
  40. }
  41. return fetch("/api/login", {
  42. headers: { "content-type": "application/json;charset=UTF-8" },
  43. credentials: credentials,
  44. body: JSON.stringify(data),
  45. method: "POST",
  46. }).then(async function (response) {
  47. if (!response.ok) {
  48. throw (await response.json());
  49. }
  50. return response.json();
  51. });
  52. };
  53.  
  54.  
  55.  
  56.  
  57. const style = `
  58. *, *:before, *:after {
  59. box-sizing: border-box;
  60. }
  61. :host {
  62. font-size: 13px;
  63. }
  64.  
  65. .user-input {
  66. color: red;
  67. border: 1px solid #cbcbcb;
  68. border-radius: 2px;
  69. outline: none;
  70. padding: 1px 4px;
  71. width: 100%;
  72. background: rgba(0, 0, 0, 0);
  73. }
  74.  
  75. form {
  76. margin: 0
  77. }
  78.  
  79. .orgs-wrapper label {
  80. margin: 8px 0;
  81. line-height: 1;
  82. display: flex;
  83. align-items: center;
  84. }
  85.  
  86. .orgs-wrapper input {
  87. margin: 0 5px;
  88. }
  89.  
  90. .orgs-wrapper {
  91. background: white;
  92. border: 1px solid #cbcbcb;
  93. padding: 0 5px;
  94. border-radius: 2px;
  95. }
  96.  
  97. .info {
  98. background: white;
  99. border-radius: 2px;
  100. border: 1px solid #cbcbcb;
  101. padding: 3px 10px;
  102. }
  103.  
  104. :host(.error) .user-input, :host(.error) .user-input:focus {
  105. border: red solid 2px;
  106. }
  107.  
  108. .slide-in-top {
  109. animation: slide-in-top 0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
  110. }
  111.  
  112. @keyframes slide-in-top {
  113. 0% {
  114. transform: translateY(-1000px);
  115. opacity: 0;
  116. }
  117. 100% {
  118. transform: translateY(0);
  119. opacity: 1;
  120. }
  121. }
  122.  
  123. @keyframes linearGradientMove {
  124. 100% {
  125. background-position: 4px 0, -4px 100%, 0 -4px, 100% 4px;
  126. }
  127. }
  128.  
  129. :host(.loading) .user-input {
  130. background:
  131. linear-gradient(90deg, red 50%, transparent 0) repeat-x,
  132. linear-gradient(90deg, red 50%, transparent 0) repeat-x,
  133. linear-gradient(0deg, red 50%, transparent 0) repeat-y,
  134. linear-gradient(0deg, red 50%, transparent 0) repeat-y;
  135. background-size: 4px 1px, 4px 1px, 1px 4px, 1px 4px;
  136. background-position: 0 0, 0 100%, 0 0, 100% 0;
  137. animation: linearGradientMove .3s infinite linear;
  138. }
  139. `;
  140.  
  141.  
  142. customElements.define('enhance-input', class extends HTMLElement {
  143. constructor() {
  144. super();
  145. const value = unsafeWindow.globalData?.user?.userNo || unsafeWindow.statisticsSettings?.user?.userNo || ''
  146.  
  147. const shadowRoot = this.attachShadow({ mode: 'open' });
  148. shadowRoot.innerHTML = `
  149. <style>${style}</style>
  150. <form>
  151. <input class='user-input' name='user_name' autocomplete="on" onfocus="this.select()" value="${value}" type=text />
  152. </form>
  153. `;
  154.  
  155. const form = shadowRoot.querySelector("form");
  156. const input = shadowRoot.querySelector("input");
  157.  
  158. form.addEventListener("submit", (event) => {
  159. event.preventDefault()
  160. this.submit(input.value)
  161. })
  162. }
  163.  
  164. showInfo(message) {
  165. this.shadowRoot.querySelector('.info')?.remove();
  166. const div = document.createElement("div");
  167. div.classList.add('info', 'slide-in-top')
  168. this.shadowRoot.appendChild(div);
  169. div.innerText = message
  170. setTimeout(() => {
  171. div.remove()
  172. }, 5000);
  173. }
  174.  
  175. selectOrgIfNeeded(result) {
  176. const form = this.shadowRoot.querySelector('form');
  177. this.shadowRoot.querySelector('.orgs-wrapper')?.remove()
  178.  
  179. return new Promise((resolve) => {
  180. const orgs = result.orgs;
  181. if (orgs) {
  182. const orgsHtml = orgs.map((org) => {
  183. return `<label><input type="radio" name="org" value="${org.id}"> <span> ${org.name} </span> </label>`
  184. }).join('')
  185. form.insertAdjacentHTML('beforeend', `<div class="orgs-wrapper slide-in-top">${orgsHtml}</div>`)
  186. form.querySelectorAll('.orgs-wrapper input[type="radio"]').forEach((radio) => {
  187. radio.addEventListener('change', (event) => {
  188. resolve({ org_id: event.target.value })
  189. })
  190. })
  191. } else {
  192. resolve(result)
  193. }
  194. })
  195. }
  196.  
  197. submit(value) {
  198. if (!value) {
  199. return
  200. }
  201.  
  202. let [username, password] = value.split(' ')
  203. if (!password) {
  204. password = localStorage.getItem('__pswd') || 'password'
  205. }
  206. const logoutAndLogin = (result) => {
  207. return logout()
  208. .then(() => login(username, password, result['org_id']))
  209. .then(() => {
  210. window.location.reload();
  211. })
  212. }
  213.  
  214. this.shadowRoot.querySelector('.orgs-wrapper')?.remove()
  215. this.classList.remove('error');
  216. this.classList.add('loading');
  217.  
  218.  
  219. const testLogin = () => {
  220. return login(username, password, '', 'omit').then((result) => {
  221. localStorage.setItem('__pswd', password);
  222. return result
  223. }).catch((result) => {
  224. const errors = result.errors || {};
  225. this.showInfo(errors['user_name']?.at(0) || errors['password']?.at(0));
  226. throw new Error('login failed');
  227. })
  228. }
  229.  
  230. // test account then logout
  231. testLogin().then(this.selectOrgIfNeeded.bind(this)).then(logoutAndLogin).catch(() => {
  232. this.classList.add("error");
  233. this.classList.remove('loading');
  234. })
  235. }
  236. });
  237.  
  238. const inject = (node) => {
  239. node.insertAdjacentHTML(
  240. "afterbegin",
  241. `<enhance-input style='width: 120px; position: fixed; top: 0; right: 0; z-index: 9999;'></enhance-input>`
  242. );
  243. };
  244.  
  245. inject(document.body);
  246.  
  247.  
  248. GM.registerMenuCommand('临时隐藏/显示登录(不可用)输入框', () => {
  249. const input = document.querySelector('enhance-input')
  250. if (input) {
  251. input.remove()
  252. } else {
  253. inject(document.body)
  254. }
  255. })
  256.  
  257.  
  258. fetch('/d/version').then(res => res.text()).then(result => {
  259. const buildIndex = result.match(/^\d+\.\d+\.(\w+)/)[1]
  260. document.title = `[${buildIndex}] ` + document.title
  261. })

QingJ © 2025

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