// ==UserScript== // @name 搜索引擎切换器 / Search Engine Switcher // @namespace http://tampermonkey.net/ // @version 0.9 // @description 一键切换多个搜索引擎!支持Google、Bing、百度、360搜索等主流平台。可拖拽、记忆位置,支持选中文本搜索。性能优化版本。 // @author AI优化版-原作者:WUJI // @match *://www.google.com*/search* // @match *://www.bing.com/search* // @match *://cn.bing.com/search* // @match *://*yandex.com/search* // @match *://www.baidu.com/s* // @match *://www.baidu.com/baidu* // @match *://www.sogou.com/web* // @match *://search.bilibili.com/all* // @match *://www.zhihu.com/search* // @match *://www.so.com/s* // @grant none // @run-at document-body // @license MIT // @icon data:image/svg+xml,🔎 // ==/UserScript== (function() { 'use strict'; // ==================== 配置项 ==================== const CONFIG = { ICON_SIZE: '32px', LIST_MAX_WIDTH: '160px', FONT_SIZE: '14px', STORAGE_KEY: 'searchSwitcherPos', HALF_HIDE_TRANSLATE: '-16px', DEBOUNCE_DELAY: 100, DRAG_THRESHOLD: 5, // 强制开启 GPU 加速的 CSS 片段 GPU_ACCEL_CSS: 'translateZ(0)' }; // ==================== 搜索引擎定义 (无正则版) ==================== const SEARCH_ENGINES = [ { name: "Bing", url: "https://www.bing.com/search?q=", keyName: "q", host: "bing.com", path: "/search" }, { name: "百度", url: "https://www.baidu.com/s?wd=", keyName: "wd", host: "baidu.com", path: "/s" }, { name: "Yandex", url: "https://yandex.com/search?text=", keyName: "text", host: "yandex.com", path: "/search" }, { name: "360搜索", url: "https://www.so.com/s?q=", keyName: "q", host: "so.com", path: "/s" }, { name: "Google", url: "https://www.google.com/search?q=", keyName: "q", host: "google.com", path: "/search" }, { name: "微信", url: "https://weixin.sogou.com/weixin?type=2&s_from=input&query=", keyName: "query", host: "sogou.com", path: "/weixin" }, { name: "哔站", url: "https://search.bilibili.com/all?keyword=", keyName: "keyword", host: "bilibili.com", path: "/all" }, { name: "知乎", url: "https://www.zhihu.com/search?q=", keyName: "q", host: "zhihu.com", path: "/search" } ]; // ==================== 工具函数 ==================== // 【优化】高效匹配:域名包含 + 路径前缀 function getCurrentEngine() { const hostname = window.location.hostname; const pathname = window.location.pathname; for (const engine of SEARCH_ENGINES) { if (!hostname.includes(engine.host)) continue; if (engine.name === "百度") { if (pathname.startsWith("/s") || pathname.startsWith("/baidu")) { return engine; } } else { if (pathname.startsWith(engine.path)) { return engine; } } } return null; } function extractKeywordFromUrl(engine) { try { const url = new URL(window.location.href); return url.searchParams.get(engine.keyName) || ''; } catch { return ''; } } // 【核心】点击时动态获取最新关键词 function getKeywords() { const currentEngine = getCurrentEngine(); if (currentEngine) { const kw = extractKeywordFromUrl(currentEngine); if (kw) return kw; } const selected = window.getSelection().toString().trim(); if (selected) return selected; return prompt('请输入搜索关键词:', '') || ''; } function isDarkMode() { return window.matchMedia('(prefers-color-scheme: dark)').matches; } function loadPosition() { try { const pos = JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEY)); if (pos && typeof pos.left === 'number' && typeof pos.top === 'number') { return pos; } } catch {} return { left: 0, top: 100 }; } function savePosition(left, top) { localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify({ left, top })); } function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // ==================== 样式创建 (GPU 加速优化) ==================== function createStyle() { const style = document.createElement('style'); style.textContent = ` :root { --list-bg-light: rgba(255, 255, 255, 0.95); --list-bg-dark: rgba(40, 40, 40, 0.95); --text-light: #333; --text-dark: #eee; --hover-light: rgba(0, 0, 0, 0.1); --hover-dark: rgba(255, 255, 255, 0.1); } #search-app-box { position: fixed; top: 0; left: 0; width: ${CONFIG.ICON_SIZE}; height: ${CONFIG.ICON_SIZE}; background: transparent !important; z-index: 2147483647; cursor: move; font-size: ${CONFIG.FONT_SIZE}; user-select: none; /* 初始状态:半隐藏 + GPU 层 */ transform: translateX(${CONFIG.HALF_HIDE_TRANSLATE}) ${CONFIG.GPU_ACCEL_CSS}; will-change: transform, left, top; backface-visibility: hidden; /* 默认过渡效果,拖动时会移除 */ transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), left 0.3s ease, top 0.3s ease; } #search-app-box.dragging { transition: none !important; /* 拖动时禁用过渡,防止滞后 */ cursor: grabbing; z-index: 2147483647; /* 确保拖动时在最上层 */ } #search-app-box:not(.half-hidden) { transform: translateX(0) ${CONFIG.GPU_ACCEL_CSS}; } #search-app-icon { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; font-size: ${CONFIG.ICON_SIZE}; background: transparent !important; color: inherit; pointer-events: none; /* 让事件穿透到 box */ } #search-engine-list { position: absolute; top: 2%; left: ${CONFIG.ICON_SIZE}; min-width: 80px; max-width: ${CONFIG.LIST_MAX_WIDTH}; max-height: 70vh; overflow-y: auto; background-color: var(--list-bg-light); border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); opacity: 0; visibility: hidden; transform: translateX(-10px); transition: opacity 0.2s, visibility 0.2s, transform 0.2s; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); white-space: nowrap; z-index: -1; } #search-engine-list a { display: block; padding: 10px 16px; color: var(--text-light); text-decoration: none; transition: background-color 0.15s; cursor: pointer; } #search-engine-list a:hover { background-color: var(--hover-light); } body.dark-mode #search-engine-list { background-color: var(--list-bg-dark); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); } body.dark-mode #search-engine-list a { color: var(--text-dark); } body.dark-mode #search-engine-list a:hover { background-color: var(--hover-dark); } /* 滚动条美化 */ #search-engine-list::-webkit-scrollbar { width: 6px; } #search-engine-list::-webkit-scrollbar-track { background: rgba(0,0,0,0.05); border-radius: 3px; } #search-engine-list::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.2); border-radius: 3px; } #search-engine-list::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.4); } `; document.head.appendChild(style); } // ==================== 创建 UI ==================== function createSearchBox() { const box = document.createElement('div'); box.id = 'search-app-box'; box.classList.add('half-hidden'); const savedPos = loadPosition(); box.style.left = savedPos.left + 'px'; box.style.top = savedPos.top + 'px'; const icon = document.createElement('div'); icon.id = 'search-app-icon'; icon.innerText = '🔎'; box.appendChild(icon); const listContainer = document.createElement('div'); listContainer.id = 'search-engine-list'; const currentEngine = getCurrentEngine(); // 构建列表:隐藏当前引擎 for (const engine of SEARCH_ENGINES) { if (currentEngine && engine.name === currentEngine.name) continue; const a = document.createElement('a'); a.innerText = engine.name; a.dataset.engineUrl = engine.url; a.title = `在${engine.name}中搜索`; listContainer.appendChild(a); } box.appendChild(listContainer); document.body.appendChild(box); // ==================== 拖拽逻辑 (高性能版) ==================== let isDragging = false; let isMouseOverBox = false; let rafId = null; // 拖拽状态变量 let startX, startY, startLeft, startTop; let currentOffsetX = 0, currentOffsetY = 0; let dragDistance = 0; function showAll() { box.classList.remove('half-hidden'); listContainer.style.opacity = '1'; listContainer.style.visibility = 'visible'; listContainer.style.transform = 'translateX(0)'; } function hideAll() { if (isDragging || isMouseOverBox) return; box.classList.add('half-hidden'); listContainer.style.opacity = '0'; listContainer.style.visibility = 'hidden'; listContainer.style.transform = 'translateX(-10px)'; } box.addEventListener('mouseenter', () => { isMouseOverBox = true; showAll(); }); box.addEventListener('mouseleave', () => { isMouseOverBox = false; hideAll(); }); box.addEventListener('mousedown', (e) => { if (e.button !== 0) return; // 仅左键 e.preventDefault(); // 防止选中文本 isDragging = true; dragDistance = 0; currentOffsetX = 0; currentOffsetY = 0; showAll(); box.classList.add('dragging'); startX = e.clientX; startY = e.clientY; // 仅在开始时读取一次 DOM 布局属性 startLeft = box.offsetLeft; startTop = box.offsetTop; function onMouseMove(e) { // 1. 快速更新数据 (无 DOM 操作) const dx = e.clientX - startX; const dy = e.clientY - startY; dragDistance = Math.sqrt(dx * dx + dy * dy); currentOffsetX = dx; currentOffsetY = dy; // 2. 请求动画帧进行 DOM 更新 (节流到屏幕刷新率) if (!rafId) { rafId = requestAnimationFrame(() => { // 使用 translate3d 强制 GPU 加速 box.style.transform = `translate3d(${currentOffsetX}px, ${currentOffsetY}px, 0)`; rafId = null; }); } } function onMouseUp(e) { // 清理 RAF if (rafId) { cancelAnimationFrame(rafId); rafId = null; } document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); // 判断是点击还是拖拽 if (dragDistance < CONFIG.DRAG_THRESHOLD) { isDragging = false; box.classList.remove('dragging'); box.style.transform = ''; // 清除 transform 以恢复 CSS 过渡 // 模拟点击行为 const elemAtMouse = document.elementFromPoint(e.clientX, e.clientY); if (box.contains(elemAtMouse)) { isMouseOverBox = true; showAll(); } else { isMouseOverBox = false; hideAll(); } return; } // 【高性能】直接计算最终位置,无需正则解析 style.transform const finalLeft = startLeft + currentOffsetX; const finalTop = startTop + currentOffsetY; const iconSize = parseInt(CONFIG.ICON_SIZE); const clampedLeft = Math.max(0, Math.min(finalLeft, window.innerWidth - iconSize)); const clampedTop = Math.max(0, Math.min(finalTop, window.innerHeight - iconSize)); // 应用最终位置 box.style.left = clampedLeft + 'px'; box.style.top = clampedTop + 'px'; box.style.transform = ''; // 清除 transform,回归文档流定位 isDragging = false; box.classList.remove('dragging'); savePosition(clampedLeft, clampedTop); // 处理鼠标释放后的悬停状态 const elemAtMouse = document.elementFromPoint(e.clientX, e.clientY); if (!box.contains(elemAtMouse)) { isMouseOverBox = false; hideAll(); } else { isMouseOverBox = true; } } // { passive: true } 提示浏览器不会阻止默认行为,提升滚动性能 document.addEventListener('mousemove', onMouseMove, { passive: true }); document.addEventListener('mouseup', onMouseUp); }); // ==================== 点击搜索逻辑 ==================== listContainer.addEventListener('click', (e) => { const target = e.target.closest('a'); if (!target) return; e.preventDefault(); // 1. 动态获取最新关键词 const latestKw = getKeywords(); // 2. 获取基础 URL const baseUrl = target.dataset.engineUrl; // 3. 拼接 const finalUrl = baseUrl + encodeURIComponent(latestKw); if (e.ctrlKey || e.metaKey || e.button === 1) { window.open(finalUrl, '_blank'); } else { window.location.href = finalUrl; } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !box.classList.contains('half-hidden')) { hideAll(); } }); } function updateTheme() { document.body.classList.toggle('dark-mode', isDarkMode()); } function init() { createStyle(); createSearchBox(); updateTheme(); const debouncedUpdateTheme = debounce(updateTheme, CONFIG.DEBOUNCE_DELAY); window.matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', debouncedUpdateTheme); } if (document.body) { init(); } else { window.addEventListener('DOMContentLoaded', init); } })();