Hi, Element Plus Component Dashboard🚀

将 Element Plus 菜单转换为 Dashboard 交互 (按 Shift 键点击可还原为默认菜单)

当前为 2025-03-27 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Hi, Element Plus Component Dashboard🚀
  3. // @namespace https://github.com/xianghongai/Tampermonkey-UserScript
  4. // @version 1.0.7
  5. // @description 将 Element Plus 菜单转换为 Dashboard 交互 (按 Shift 键点击可还原为默认菜单)
  6. // @author Nicholas Hsiang
  7. // @match *://element-plus.org/*
  8. // @icon https://avatars.githubusercontent.com/u/68583457
  9. // @grant GM_addStyle
  10. // @grant GM_info
  11. // @run-at document-end
  12. // @grant unsafeWindow
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. 'use strict';
  18. console.log(GM_info.script.name);
  19.  
  20. const logoSelector = '.logo-container img.logo';
  21. const navSelector = '.navbar-menu';
  22. const menuSelector = '.sidebar';
  23. const groupSelector = '.sidebar-group:not(:first-child)';
  24. const componentItemSelector = '.link';
  25. const titleSelector = '.sidebar-group__title';
  26.  
  27. let wrapperElement = null;
  28.  
  29. main();
  30.  
  31. /**
  32. * Main function to execute when the script is loaded.
  33. */
  34. function main() {
  35. ready(() => {
  36. poll(navSelector, handler, 500);
  37. });
  38. }
  39.  
  40. const wrapperId = 'x-menu-wrapper';
  41. const wrapperClassName = 'x-menu-wrapper';
  42. const toggleClassName = 'x-toggle';
  43.  
  44. /**
  45. * Toggle the target element.
  46. */
  47. function handler() {
  48. const toggleElement = document.createElement('span');
  49. toggleElement.className = toggleClassName;
  50. toggleElement.innerHTML = icon();
  51.  
  52. toggleElement.addEventListener('click', (event) => {
  53. // hold shift key to reset
  54. if (event.shiftKey) {
  55. wrapperElement.removeAttribute('id');
  56. wrapperElement.style.display = 'block';
  57. return;
  58. }
  59.  
  60. // init
  61. if (!wrapperElement || wrapperElement.id !== wrapperId) {
  62. wrapperElement = setMenuWrapper();
  63. // add event listener to component item
  64. componentItemClickEventListener(wrapperElement, componentItemSelector);
  65. // handle component page class (hide 'overview' menu item)
  66. handleComponentPageClass(wrapperElement);
  67. return;
  68. }
  69. wrapperElement.style.display = wrapperElement.style.display === 'none' ? 'grid' : 'none';
  70. });
  71.  
  72. document.body.appendChild(toggleElement);
  73. // add event listener to navbar
  74. navClickEventListener();
  75. }
  76.  
  77. /**
  78. * Click the navbar menu element, handle the component page (hide 'overview' menu item).
  79. */
  80. function navClickEventListener() {
  81. const navElement = document.querySelector(navSelector);
  82. if (navElement) {
  83. navElement.addEventListener('click', () => {
  84. wrapperElement = document.querySelector(menuSelector);
  85. setTimeout(() => {
  86. if (wrapperElement) {
  87. handleComponentPageClass(wrapperElement);
  88. }
  89. }, 100);
  90. });
  91. }
  92. }
  93.  
  94. /**
  95. * Handle the component page class.
  96. * @param {Element} wrapperElement - The wrapper element
  97. */
  98. function handleComponentPageClass(wrapperElement) {
  99. if (window.location.href.includes('component')) {
  100. wrapperElement.classList.add(wrapperClassName);
  101. } else {
  102. wrapperElement.classList.remove(wrapperClassName);
  103. }
  104. }
  105.  
  106. /**
  107. * Click the component item, hide the menu wrapper.
  108. * @param {Element} wrapperElement - The wrapper element
  109. * @param {string} componentItemSelector - The selector of the component item
  110. */
  111. function componentItemClickEventListener(wrapperElement, componentItemSelector) {
  112. wrapperElement.addEventListener('click', (event) => {
  113. if (matches(event.target, componentItemSelector)) {
  114. wrapperElement.style.display = 'none';
  115. }
  116. });
  117. }
  118.  
  119. /**
  120. * Set the menu wrapper element.
  121. * @returns {Element} - The menu wrapper element
  122. */
  123. function setMenuWrapper() {
  124. wrapperElement = document.querySelector(menuSelector);
  125. wrapperElement.setAttribute('id', wrapperId);
  126.  
  127. // 获取所有 sidebar-group 元素(排除第一个)
  128. const groupElements = Array.from(wrapperElement.querySelectorAll(groupSelector));
  129. const componentCounts = [];
  130.  
  131. groupElements.forEach((item) => {
  132. const itemSelector = 'a.link';
  133. const itemElements = Array.from(item.querySelectorAll(itemSelector));
  134. const length = itemElements.length;
  135. const titleElement = item.querySelector(titleSelector);
  136. const title = titleElement.textContent;
  137. titleElement.textContent = `${title} (${length})`;
  138. componentCounts.push(length);
  139. });
  140.  
  141. const totalCount = componentCounts.reduce((acc, curr) => acc + curr, 0);
  142. const totalText = `🚀 共有组件 ${totalCount} 个`;
  143. const logoElement = document.querySelector(logoSelector);
  144. if (logoElement) {
  145. logoElement.title = totalText;
  146. }
  147. console.log(totalText);
  148. return wrapperElement;
  149. }
  150.  
  151. /**
  152. * Execute a function when the document is ready.
  153. * @param {function} eventHandler - Function to execute when the document is ready
  154. */
  155. function ready(eventHandler) {
  156. if (document.readyState !== 'loading') {
  157. eventHandler();
  158. } else {
  159. document.addEventListener('DOMContentLoaded', eventHandler);
  160. }
  161. }
  162.  
  163. /**
  164. * Wait for an element to be found on the page using polling.
  165. * @param {string} selector - CSS selector for the element to wait for
  166. * @param {function} callback - Function to execute when the element is found
  167. * @param {number} maxAttempts - Maximum number of attempts to find the element
  168. * @returns {number} intervalId - ID of the interval used to poll for the element
  169. */
  170. function poll(selector, callback, maxAttempts = 10) {
  171. let attempts = 0;
  172.  
  173. const intervalId = setInterval(() => {
  174. attempts++;
  175. const element = document.querySelector(selector);
  176.  
  177. if (element) {
  178. clearInterval(intervalId);
  179. if (callback && typeof callback === 'function') {
  180. callback(element);
  181. }
  182. } else if (attempts >= maxAttempts) {
  183. clearInterval(intervalId);
  184. console.log(`Element ${selector} not found after ${maxAttempts} attempts.`);
  185. }
  186. }, 1000);
  187.  
  188. return intervalId;
  189. }
  190.  
  191. /**
  192. * Check if an element matches a CSS selector.
  193. * @param {Element} currentElement - The element to check for a match
  194. * @param {string} selector - CSS selector to match against
  195. * @returns {boolean} - True if the selector matches, false otherwise
  196. */
  197. function matches(currentElement, selector) {
  198. while (currentElement !== null && currentElement !== document.body) {
  199. if (currentElement.matches(selector)) {
  200. return true;
  201. }
  202. currentElement = currentElement.parentElement;
  203. }
  204.  
  205. // 检查 body 元素
  206. return document.body.matches(selector);
  207. }
  208.  
  209. function icon() {
  210. return `<?xml version="1.0" encoding="UTF-8"?><svg width="18" height="18" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 4H6C4.89543 4 4 4.89543 4 6V18C4 19.1046 4.89543 20 6 20H18C19.1046 20 20 19.1046 20 18V6C20 4.89543 19.1046 4 18 4Z" fill="#2F88FF" stroke="#333" stroke-width="3" stroke-linejoin="round"/><path d="M18 28H6C4.89543 28 4 28.8954 4 30V42C4 43.1046 4.89543 44 6 44H18C19.1046 44 20 43.1046 20 42V30C20 28.8954 19.1046 28 18 28Z" fill="#2F88FF" stroke="#333" stroke-width="3" stroke-linejoin="round"/><path d="M42 4H30C28.8954 4 28 4.89543 28 6V18C28 19.1046 28.8954 20 30 20H42C43.1046 20 44 19.1046 44 18V6C44 4.89543 43.1046 4 42 4Z" fill="#2F88FF" stroke="#333" stroke-width="3" stroke-linejoin="round"/><path d="M28 28H44" stroke="#333" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M36 36H44" stroke="#333" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M28 44H44" stroke="#333" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
  211. }
  212.  
  213. const style = `
  214. .${toggleClassName} {
  215. position: fixed;
  216. top: 18px;
  217. right: 16px;
  218. z-index: 99999;
  219. cursor: pointer;
  220. opacity: 0.8;
  221. transition: opacity 0.3s ease-in-out;
  222. }
  223.  
  224. .${toggleClassName}:hover {
  225. opacity: 1;
  226. }
  227.  
  228. #${wrapperId} {
  229. position: fixed !important;
  230. top: 55px !important;
  231. right: 0 !important;
  232. bottom: 0 !important;
  233. left: 0 !important;
  234. z-index: 9999 !important;
  235. max-width: 100% !important;
  236. width: 100% !important;
  237. max-height: calc(100vh - 55px) !important;
  238. padding: 0 !important;
  239. background: #fff !important;
  240. /* border-block-start: 1px solid rgba(5, 5, 5, 0.06) !important; */
  241. }
  242.  
  243. #${wrapperId} .sidebar-groups {
  244. display: grid !important;
  245. grid-auto-flow: column !important;
  246. grid-auto-columns: max-content !important;
  247. max-width: max-content !important;
  248. gap: 16px !important;
  249. overflow: auto;
  250. margin-inline: auto !important;
  251. padding-block-end: 0 !important;
  252. border-inline-end: none !important;
  253. }
  254.  
  255. #${wrapperId} .doc-content-side {
  256. display: none !important;
  257. }
  258.  
  259. #${wrapperId} .sidebar-group__title {
  260. font-size: 12px !important;
  261. margin-block-end: 4px !important;
  262. }
  263.  
  264. #${wrapperId}.${wrapperClassName} .sidebar-group:nth-child(1) {
  265. display: none !important;
  266. }
  267.  
  268. #${wrapperId} .sidebar-group {
  269. padding-block-start: 16px !important;
  270. }
  271.  
  272. #${wrapperId} .sidebar-group .link {
  273. padding: 6px 8px !important;
  274. }
  275.  
  276. #${wrapperId} .sidebar-group .link-text {
  277. font-size: 12px !important;
  278. font-weight: 400 !important;
  279. }
  280. `;
  281. GM_addStyle(style);
  282. })();