// ==UserScript== // @name Vue路由一键切换助手 // @namespace http://tampermonkey.net/ // @version 1.0.2 // @description 在Vue项目中生成可拖拽菜单,显示所有路由,支持快速跳转 // @icon https://cn.vuejs.org/logo.svg // @author Fairly // @license MIT // @match *://*/* // @grant none // @downloadURL https://update.greasyfork.icu/scripts/575193/Vue%E8%B7%AF%E7%94%B1%E4%B8%80%E9%94%AE%E5%88%87%E6%8D%A2%E5%8A%A9%E6%89%8B.user.js // @updateURL https://update.greasyfork.icu/scripts/575193/Vue%E8%B7%AF%E7%94%B1%E4%B8%80%E9%94%AE%E5%88%87%E6%8D%A2%E5%8A%A9%E6%89%8B.meta.js // ==/UserScript== (function() { 'use strict'; // 等待Vue应用挂载并获取router实例 let routerInstance = null; let vueApp = null; // 检测Vue路由的方法(支持Vue2和Vue3) function findVueRouter() { // 通过DOM元素获取Vue实例(Vue2) const appElement = document.getElementById('app') || document.querySelector('#app'); if (appElement && appElement.__vue__) { const vueInstance = appElement.__vue__; if (vueInstance.$router) { routerInstance = vueInstance.$router; vueApp = vueInstance; return true; } } // 尝试获取全局Vue实例(某些Vue2项目) if (window.vueDevtools && window.vueDevtools.apps && window.vueDevtools.apps[0]) { const app = window.vueDevtools.apps[0]; if (app.$router) { routerInstance = app.$router; vueApp = app; return true; } } // 检测Vue3的__vue_app__属性 if (appElement && appElement.__vue_app__) { const app = appElement.__vue_app__; // Vue3的router通常挂在config.globalProperties上 if (app.config && app.config.globalProperties && app.config.globalProperties.$router) { routerInstance = app.config.globalProperties.$router; vueApp = app; return true; } } // 遍历window对象查找可能的router实例(兜底方案) for (let key in window) { try { if (window[key] && window[key].$router) { routerInstance = window[key].$router; vueApp = window[key]; return true; } } catch(e) {} } return false; } // 获取所有路由(递归解析路由配置) function getAllRoutes(routes, basePath = '') { let routeList = []; if (!routes) return routeList; for (let route of routes) { // 构建完整路径 let fullPath = basePath + (route.path || ''); // 处理动态路由参数(保留占位符便于识别,但跳转时动态填充暂时留空) let displayPath = fullPath; let displayName = route.name || route.path || '未命名路由'; // 存储路由信息 routeList.push({ path: fullPath, name: displayName, originalName: route.name || '' }); // 递归处理子路由 if (route.children && route.children.length > 0) { let childRoutes = getAllRoutes(route.children, fullPath + (fullPath.endsWith('/') ? '' : '/')); routeList.push(...childRoutes); } } return routeList; } // 刷新路由列表UI function buildDropdownContent() { if (!routerInstance || !routerInstance.options || !routerInstance.options.routes) { return '
未检测到路由配置
'; } const routes = getAllRoutes(routerInstance.options.routes); if (routes.length === 0) { return '
暂无路由信息
'; } let html = '
'; routes.forEach(route => { const currentPath = routerInstance.currentRoute?.path || window.location.hash.replace('#', '') || window.location.pathname; const isActive = (currentPath === route.path) || (currentPath === route.path + '/'); const activeStyle = isActive ? 'background: #e6f7ff; color: #1890ff;' : ''; html += `
${escapeHtml(route.name)}
${escapeHtml(route.path || '/')}
`; }); html += '
'; return html; } // 简单的防XSS function escapeHtml(str) { if (!str) return ''; return str.replace(/[&<>]/g, function(m) { if (m === '&') return '&'; if (m === '<') return '<'; if (m === '>') return '>'; return m; }).replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, function(c) { return c; }); } // 执行路由跳转 function jumpToRoute(path) { if (!routerInstance) { console.warn('路由实例未找到'); return; } try { // 移除可能开头的#或多余字符 let cleanPath = path; if (cleanPath.startsWith('#')) cleanPath = cleanPath.slice(1); // 使用router.push进行跳转 routerInstance.push(cleanPath).catch(err => { // 忽略重复路由导航错误 if (err.name !== 'NavigationDuplicated') { console.warn('路由跳转失败:', err); } }); } catch(e) { console.warn('跳转异常:', e); } // 关闭下拉菜单 const dropdown = document.querySelector('.vue-route-dropdown'); if (dropdown) dropdown.style.display = 'none'; } // 创建可拖拽菜单UI function createMenuUI() { // 移除已存在的菜单 const existingMenu = document.getElementById('vue-route-helper-menu'); if (existingMenu) existingMenu.remove(); // 创建容器 const menuContainer = document.createElement('div'); menuContainer.id = 'vue-route-helper-menu'; menuContainer.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 99999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; user-select: none; `; // 触发按钮 const triggerBtn = document.createElement('div'); triggerBtn.className = 'vue-route-trigger'; triggerBtn.innerHTML = '🗺️ 路由'; triggerBtn.style.cssText = ` background: #1890ff; color: white; padding: 8px 16px; border-radius: 24px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.15); font-size: 14px; font-weight: 500; transition: all 0.3s; text-align: center; backdrop-filter: blur(4px); background-color: rgba(24, 144, 255, 0.9); `; // 下拉内容 const dropdown = document.createElement('div'); dropdown.className = 'vue-route-dropdown'; dropdown.style.cssText = ` position: absolute; top: 100%; right: 0; margin-top: 8px; background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); min-width: 260px; max-width: 360px; overflow: hidden; display: none; z-index: 100000; border: 1px solid #e8e8e8; `; // 加载路由列表 function updateDropdownContent() { dropdown.innerHTML = buildDropdownContent(); // 绑定点击事件 setTimeout(() => { const items = dropdown.querySelectorAll('.vue-route-item'); items.forEach(item => { const path = item.getAttribute('data-path'); if (path) { item.addEventListener('click', (e) => { e.stopPropagation(); jumpToRoute(path); }); } }); }, 10); } updateDropdownContent(); // 切换下拉显示/隐藏 triggerBtn.addEventListener('click', (e) => { e.stopPropagation(); const isVisible = dropdown.style.display === 'block'; if (!isVisible) { // 重新获取最新的路由列表(路由可能动态变化) if (findVueRouter()) { updateDropdownContent(); } dropdown.style.display = 'block'; } else { dropdown.style.display = 'none'; } }); // 点击页面其他地方关闭下拉 document.addEventListener('click', function closeDropdown(e) { if (!menuContainer.contains(e.target)) { dropdown.style.display = 'none'; } }); menuContainer.appendChild(triggerBtn); menuContainer.appendChild(dropdown); document.body.appendChild(menuContainer); // 实现拖拽功能 let isDragging = false; let startX, startY, startRight, startTop; triggerBtn.addEventListener('mousedown', (e) => { if (e.button !== 0) return; // 只响应左键 e.preventDefault(); e.stopPropagation(); isDragging = true; const rect = menuContainer.getBoundingClientRect(); startX = e.clientX; startY = e.clientY; startRight = window.innerWidth - rect.right; startTop = rect.top; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); // 拖拽时临时改变样式 triggerBtn.style.cursor = 'grabbing'; menuContainer.style.transition = 'none'; }); function onMouseMove(e) { if (!isDragging) return; e.preventDefault(); let dx = e.clientX - startX; let dy = e.clientY - startY; let newRight = startRight - dx; let newTop = startTop + dy; // 边界限制,避免拖出屏幕 const maxRight = window.innerWidth - menuContainer.offsetWidth; const minRight = 0; const minTop = 0; const maxTop = window.innerHeight - menuContainer.offsetHeight; newRight = Math.min(maxRight, Math.max(minRight, newRight)); newTop = Math.min(maxTop, Math.max(minTop, newTop)); menuContainer.style.right = newRight + 'px'; menuContainer.style.top = newTop + 'px'; menuContainer.style.left = 'auto'; menuContainer.style.bottom = 'auto'; } function onMouseUp() { isDragging = false; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); triggerBtn.style.cursor = 'pointer'; menuContainer.style.transition = ''; } // 防止拖拽时触发按钮点击 let hasMoved = false; triggerBtn.addEventListener('click', (e) => { if (hasMoved) { hasMoved = false; e.stopPropagation(); return; } }); // 改进:监听拖拽移动标志 const originalMouseDown = triggerBtn.onmousedown; triggerBtn.addEventListener('mousemove', () => { if (isDragging) hasMoved = true; }); } // 周期性检测Vue路由,直到获取成功 let retryCount = 0; function initHelper() { if (findVueRouter() && routerInstance) { createMenuUI(); // 监听路由变化,可选:自动刷新菜单内容(当下拉打开时刷新即可,不做额外复杂逻辑) // 但为了更加健壮,当切换路由后重新获取路由配置,可以通过拦截或观察 // 简化处理: 每次打开下拉时重新获取路由列表已经在triggerBtn的click里调用了updateDropdownContent } else { retryCount++; if (retryCount < 20) { // 最多重试20次,约10秒 setTimeout(initHelper, 500); } else { console.warn('Vue路由一键切换助手: 未检测到Vue路由实例,请确保页面为Vue应用且路由已初始化'); // 创造降级菜单,提示未检测到 const failMenu = document.createElement('div'); failMenu.id = 'vue-route-helper-menu'; failMenu.style.cssText = 'position:fixed;top:20px;right:20px;z-index:99999;background:#ff4d4f;color:white;padding:6px 12px;border-radius:20px;font-size:12px;cursor:pointer;'; failMenu.innerText = '⚠️ 未检测到Vue路由'; failMenu.onclick = () => { failMenu.remove(); }; document.body.appendChild(failMenu); setTimeout(() => { if(failMenu.parentNode) failMenu.remove(); }, 5000); } } } // 页面加载完成后启动检测 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initHelper); } else { initHelper(); } // 监听SPA路由变化,如果页面发生了完全的路由切换导致DOM重建,简单重新检测菜单是否存在 let lastUrl = location.href; setInterval(() => { if (lastUrl !== location.href) { lastUrl = location.href; // 页面URL变化后,可能Vue实例重新挂载,重新检测路由实例 if (!document.getElementById('vue-route-helper-menu')) { if (findVueRouter()) { createMenuUI(); } else { // 延时重试 setTimeout(() => { if (findVueRouter() && !document.getElementById('vue-route-helper-menu')) { createMenuUI(); } }, 1000); } } else { // 如果菜单已存在但是router实例变化,更新内部引用 findVueRouter(); } } }, 800); })();