TC开发增强

快速切换TC账号

当前为 2023-10-17 提交的版本,查看 最新版本

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