// ==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);
})();