您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
增加 wiki js 左侧树形菜单
// ==UserScript== // @name Wiki 树形菜单 // @namespace http://tampermonkey.net/ // @version 2025-08-08 // @description 增加 wiki js 左侧树形菜单 // @author You // @match http://192.168.1.111:7001/* // @exclude http://192.168.1.111:7001/e/* // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // 查找 nav 下 button,获取第二个 const getTargetButton = () => { const buttons = document.querySelectorAll('nav button'); return buttons.length >= 2 ? buttons[1] : null; }; // 获取 button 下 span -> div 的文字 const getDivText = (button) => { const span = button?.querySelector('span'); const div = span?.querySelector('div'); return div?.textContent.trim(); }; // 获取当前 URL 中的 path const getCurrentPath = () => { const match = window.location.pathname.match(/^\/zh\/(.+)/); return match ? match[1] : ''; }; // 初始检查 const init = () => { const button = getTargetButton(); if (button) { const text = getDivText(button); if (text) { handleMenuDisplay(text); return text === '浏览'; } } return false; }; // 渲染菜单项 const renderMenuItems = (items, level = 0, parentNode = null, currentPath = '') => { const listDiv = document.createElement('div'); listDiv.className = 'v-list py-2 v-sheet theme--dark v-list--dense primary'; const pathParts = currentPath ? currentPath.split('/') : []; items.forEach(item => { const itemDiv = document.createElement('div'); itemDiv.tabIndex = 0; itemDiv.className = `v-list-item v-list-item--link theme--dark`; itemDiv.style.minHeight = '30px'; // 检查是否为当前路径的菜单项 //if (!item.isFolder && item.path === currentPath) { if (!item.isFolder && item.path === decodeURI(currentPath)) { itemDiv.classList.add('v-list-item--active'); } const avatarDiv = document.createElement('div'); avatarDiv.className = 'v-avatar v-list-item__avatar'; avatarDiv.style.height = '18px'; avatarDiv.style.minWidth = '18px'; avatarDiv.style.width = 'auto'; avatarDiv.style.paddingLeft = `${level * 8}px`; avatarDiv.style.margin = '0px 5px 0px 0px'; const icon = document.createElement('i'); icon.setAttribute('aria-hidden', 'true'); icon.className = `v-icon notranslate mdi mdi-${ item.isFolder ? 'folder' : 'text-box' } theme--dark`; icon.style.fontSize = '16px'; avatarDiv.appendChild(icon); const titleDiv = document.createElement('div'); titleDiv.className = 'v-list-item__title'; titleDiv.textContent = item.title; itemDiv.appendChild(avatarDiv); itemDiv.appendChild(titleDiv); // 为文件夹添加点击事件 if (item.isFolder) { let isExpanded = false; itemDiv.addEventListener('click', () => { const existingSubMenu = itemDiv.nextElementSibling?.classList.contains('v-list') ? itemDiv.nextElementSibling : null; if (isExpanded && existingSubMenu) { existingSubMenu.remove(); icon.className = 'v-icon notranslate mdi mdi-folder theme--dark'; icon.style.fontSize = '16px'; isExpanded = false; } else { getMenuByParent(item.id).then(response => { const subMenu = renderMenuItems(response, level + 1, null, currentPath); if (existingSubMenu) { existingSubMenu.remove(); } itemDiv.parentElement.insertBefore(subMenu, itemDiv.nextSibling); icon.className = 'v-icon notranslate mdi mdi-folder-open theme--dark'; icon.style.fontSize = '16px'; isExpanded = true; }); } }); // 自动展开包含当前路径的文件夹 // if (pathParts.length > 0 && item.path === pathParts.slice(0, level + 1).join('/')) { if (pathParts.length > 0 && item.path === decodeURI(pathParts.slice(0, level + 1).join('/'))) { getMenuByParent(item.id).then(response => { const subMenu = renderMenuItems(response, level + 1, null, currentPath); itemDiv.parentElement.insertBefore(subMenu, itemDiv.nextSibling); icon.className = 'v-icon notranslate mdi mdi-folder-open theme--dark'; icon.style.fontSize = '16px'; isExpanded = true; }); } } else { // 为非文件夹添加跳转事件 itemDiv.addEventListener('click', () => { window.location.href = `${window.location.origin}/zh/${item.path}`; }); } listDiv.appendChild(itemDiv); }); return listDiv; }; // 处理 custom_tree_menu 的显示/隐藏逻辑 const handleMenuDisplay = (text) => { const existingDiv = document.querySelector('div#custom_tree_menu'); const button = getTargetButton(); const parent = button?.parentElement?.parentElement; const currentPath = getCurrentPath(); if (text === '浏览') { if (!existingDiv && parent) { const newDiv = document.createElement('div'); newDiv.id = 'custom_tree_menu'; parent.appendChild(newDiv); getHomeMenu().then(response => { newDiv.appendChild(renderMenuItems(response, 0, null, currentPath)); }); } else if (existingDiv) { existingDiv.style.display = 'block'; getHomeMenu().then(response => { existingDiv.innerHTML = ''; existingDiv.appendChild(renderMenuItems(response, 0, null, currentPath)); }); } } else if (text === '主菜单' && existingDiv) { existingDiv.style.display = 'none'; } }; // 使用 MutationObserver 监听 div 内容变化 const observeDivChanges = () => { const button = getTargetButton(); const targetDiv = button?.querySelector('span div'); if (!targetDiv) return; const observer = new MutationObserver((mutations) => { mutations.forEach(() => { const text = getDivText(button); if (text) handleMenuDisplay(text); }); }); observer.observe(targetDiv, { characterData: true, subtree: true, childList: true }); }; // 获取最外层 menu async function getHomeMenu() { const currentUrl = window.location.origin; const uploadUrl = `${currentUrl}/graphql`; const jwt = document.cookie.split('; ') .find(row => row.startsWith('jwt=')) ?.split('=')[1]; const auth = `Bearer ${jwt}`; const response = await fetch(uploadUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': auth }, body: JSON.stringify([{ operationName: null, variables: { parent: "home", locale: "zh" }, extensions: {}, query: "query ($path: String, $locale: String!) {\n pages {\n tree(path: $path, mode: ALL, locale: $locale, includeAncestors: true) {\n id\n path\n title\n isFolder\n pageId\n parent\n locale\n __typename\n }\n __typename\n }\n}\n" }]) }); const result = await response.json(); const tree = result[0]?.data?.pages?.tree || []; return tree.map(item => ({ id: item.id, path: item.path, title: item.title, isFolder: item.isFolder, pageId: item.pageId, parent: item.parent })); } // 点击 folder 获取新的 menu async function getMenuByParent(parentId) { const currentUrl = window.location.origin; const uploadUrl = `${currentUrl}/graphql`; const jwt = document.cookie.split('; ') .find(row => row.startsWith('jwt=')) ?.split('=')[1]; const auth = `Bearer ${jwt}`; const response = await fetch(uploadUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': auth }, body: JSON.stringify([{ operationName: null, variables: { parent: parentId, locale: "zh" }, extensions: {}, query: "query ($parent: Int, $locale: String!) {\n pages {\n tree(parent: $parent, mode: ALL, locale: $locale) {\n id\n path\n title\n isFolder\n pageId\n parent\n locale\n __typename\n }\n __typename\n }\n}" }]) }); const result = await response.json(); const tree = result[0]?.data?.pages?.tree || []; return tree.map(item => ({ id: item.id, path: item.path, title: item.title, isFolder: item.isFolder, pageId: item.pageId, parent: item.parent })); } let intervalId = window.setInterval(function() { if (init()) { observeDivChanges(); window.clearInterval(intervalId); } }, 500); })();