搜索引擎增强

搜索引擎导航增强,支持拖拽、缩放、折叠和状态记忆,加载更稳定。

// ==UserScript==
// @name              搜索引擎增强
// @namespace         search_enhance_namespace
// @version           4.0.0
// @description       搜索引擎导航增强,支持拖拽、缩放、折叠和状态记忆,加载更稳定。
// @author            zyh
// @match             *://www.baidu.com/*
// @match             *://www.so.com/s*
// @match             *://www.sogou.com/web*
// @match             *://cn.bing.com/search*
// @match             *://www.bing.com/search*
// @match             *://www.google.com/search*
// @match             *://www.google.com.hk/search*
// @grant             GM_getValue
// @grant             GM_setValue
// @license           MIT
// ==/UserScript==

(function() {
    'use strict';

    /**
     * SearchEnhancer 类,用于封装所有功能
     */
    class SearchEnhancer {
        /**
         * 构造函数,脚本的入口
         */
        constructor() {
            this.host = window.location.host;
            this.initData();

            // 检查当前网站是否是目标搜索引擎
            this.engineConfig = this.searchEnginesData.find(engine => this.host.includes(engine.host));
            if (!this.engineConfig) {
                return; // 如果不是,则不执行任何操作
            }

            // 使用 waitForElement 等待搜索框加载完成后,再初始化UI
            // 这是为了确保我们操作的DOM元素已经存在
            this.waitForElement(this.engineConfig.elementInput, () => {
                this.settings = this.loadSettings();
                // 确保折叠前的高度有一个合理的默认值
                this.lastExpandedHeight = this.settings.height > 40 ? this.settings.height : 400;
                this.initUI();
            });
        }

        /**
         * 初始化搜索引擎和导航链接的数据
         */
        initData() {
            this.searchEnginesData = [
                {host: "baidu.com", name: "百度", elementInput: "#kw"},
                {host: "so.com", name: "360搜索", elementInput: "#keyword"},
                {host: "sogou.com", name: "搜狗", elementInput: "#upquery"},
                {host: "bing.com", name: "必应", elementInput: "#sb_form_q"},
                {host: "google.com", name: "谷歌", elementInput: "input[name='q'],textarea[name='q']"}
            ];
            this.navigationData = [
                {"name": "搜索引擎", "list": [
                    {"name": "百度", "url": "https://www.baidu.com/s?wd=@@"},
                    {"name": "必应", "url": "https://cn.bing.com/search?q=@@"},
                    {"name": "Google", "url": "https://www.google.com/search?q=@@"}
                ]},
                {"name": "综合搜索", "list": [
                    {"name": "知乎", "url": "https://www.zhihu.com/search?q=@@"},              
                    {"name": "CSDN", "url": "https://so.csdn.net/so/search?q=@@"},
                    {"name": "GitHub", "url": "https://github.com/search?q=@@"},
                    {"name": "小红书", "url": "https://www.xiaohongshu.com/search_result?keyword=@@"}
                ]},
                 {"name": "视频搜索", "list": [
                    {"name": "B站", "url": "https://search.bilibili.com/all?keyword=@@"},
                    {"name": "抖音", "url": "https://www.douyin.com/search/@@"},
                    {"name": "YouTube", "url": "https://www.youtube.com/results?search_query=@@"}
                ]},
                 {"name": "学术搜索", "list": [
                    {"name": "谷粉学术", "url": "https://www.defineabc.com/scholar?hl=en&q=@@" },
                    {"name": "Aminer", "url": "https://www.aminer.cn/search?t=b&q=@@"}
                ]}
            ];
        }

        /**
         * 初始化UI:创建面板、应用样式、绑定事件
         */
        initUI() {
            if (document.getElementById('search-enhancer-panel')) return; // 防止重复创建
            this.createPanel();
            this.applyStyles();
            this.attachEventListeners();
        }

        /**
         * 从油猴存储中加载设置
         */
        loadSettings() {
            const defaults = { x: 20, y: 120, width: 280, height: 400, isCollapsed: false };
            const saved = GM_getValue(`enhancer_settings_${this.host}`) || {};
            return { ...defaults, ...saved }; // 合并默认值和已保存值
        }

        /**
         * 保存当前设置到油猴存储
         */
        saveSettings() {
            if (!this.panel) return;
            const currentSettings = {
                x: this.panel.offsetLeft,
                y: this.panel.offsetTop,
                width: this.panel.offsetWidth,
                height: this.lastExpandedHeight, // 关键:总是保存展开时的高度
                isCollapsed: this.panel.classList.contains('collapsed')
            };
            GM_setValue(`enhancer_settings_${this.host}`, currentSettings);
        }

        /**
         * 创建UI面板的HTML结构
         */
        createPanel() {
            let html = '<div class="nav-header"><div class="nav-title">搜索导航</div><button class="nav-toggle-btn">▲</button></div>';
            html += '<div class="nav-content">';
            this.navigationData.forEach(cat => {
                html += `<div class="nav-section"><div class="section-title">${cat.name}</div><div class="nav-links">`;
                cat.list.forEach(item => {
                    html += `<a href="#" data-url="${item.url}">${item.name}</a>`;
                });
                html += '</div></div>';
            });
            html += '</div><div class="resize-handle"></div>';

            this.panel = document.createElement('div');
            this.panel.id = 'search-enhancer-panel';
            this.panel.innerHTML = html;
            document.body.appendChild(this.panel);

            // 如果保存的状态是折叠的,则初始化为折叠状态
            if (this.settings.isCollapsed) {
                this.panel.classList.add('collapsed');
                this.panel.querySelector('.nav-content').style.display = 'none';
                this.panel.querySelector('.nav-toggle-btn').textContent = '▼';
            }
        }

        /**
         * 应用CSS样式
         */
        applyStyles() {
            const s = this.settings;
            const css = `
                #search-enhancer-panel {
                    position:fixed; top:${s.y}px; left:${s.x}px; width:${s.width}px; height:${s.isCollapsed ? 'auto' : `${s.height}px`};
                    min-width:200px; min-height:40px; z-index:999999; display:flex; flex-direction:column;
                    background:rgba(255,255,255,0.40); border-radius:10px; box-shadow:0 5px 20px rgba(0,0,0,0.12);
                    backdrop-filter:blur(8px); border:1px solid rgba(0,0,0,0.08); user-select:none; overflow:hidden; transition: height 0.2s ease-in-out;
                }
                #search-enhancer-panel.no-transition { transition: none !important; }
                #search-enhancer-panel.collapsed { height: auto !important; }
                .nav-header { display:flex; justify-content:space-between; align-items:center; padding:8px 12px; background:rgba(0,0,0,0.04); cursor:move; flex-shrink: 0; }
                .nav-title { font-weight:600; color:#333; }
                .nav-toggle-btn { border:none; background:none; cursor:pointer; font-size:16px; color:#555; padding:5px; }
                .nav-content { padding:10px 15px; overflow-y:auto; flex-grow:1; }
                .nav-section { margin-bottom:12px; }
                .section-title { font-size:13px; font-weight:500; color:#666; margin-bottom:8px; padding-bottom:4px; border-bottom:1px solid #eee; }
                .nav-links { display:flex; flex-wrap:wrap; gap:8px; }
                .nav-links a { padding:4px 9px; color:#333; text-decoration:none; font-size:13px; background:#f1f1f1; border-radius:5px; transition:all 0.2s; }
                .nav-links a:hover { background:#007bff; color:white; transform:translateY(-1px); }
                .resize-handle { position:absolute; bottom:0; right:0; width:15px; height:15px; cursor:se-resize; z-index:10; }
            `;
            const styleEl = document.createElement('style');
            styleEl.textContent = css;
            document.head.appendChild(styleEl);
        }

        /**
         * 绑定所有事件监听器
         */
        attachEventListeners() {
            const header = this.panel.querySelector('.nav-header');
            const toggleBtn = this.panel.querySelector('.nav-toggle-btn');
            const resizeHandle = this.panel.querySelector('.resize-handle');

            // 折叠/展开
            toggleBtn.addEventListener('click', e => {
                e.stopPropagation();
                const isCollapsed = this.panel.classList.toggle('collapsed');
                this.panel.querySelector('.nav-content').style.display = isCollapsed ? 'none' : 'block';
                toggleBtn.textContent = isCollapsed ? '▼' : '▲';
                if (!isCollapsed) {
                    this.panel.style.height = `${this.lastExpandedHeight}px`;
                }
                this.saveSettings();
            });

            // 链接点击
            this.panel.querySelectorAll('.nav-links a').forEach(link => {
                link.addEventListener('click', e => {
                    e.preventDefault();
                    const keywordInput = document.querySelector(this.engineConfig.elementInput);
                    const keyword = keywordInput ? keywordInput.value : '';
                    const url = e.target.dataset.url.replace('@@', encodeURIComponent(keyword));
                    window.open(url, '_blank');
                });
            });

            // 拖拽和缩放
            const dragOrResize = (e, type) => {
                e.preventDefault();
                this.panel.classList.add('no-transition'); // 拖拽时禁用过渡动画,防止卡顿

                let startX = e.clientX, startY = e.clientY;
                let initialX = this.panel.offsetLeft, initialY = this.panel.offsetTop;
                let initialW = this.panel.offsetWidth, initialH = this.panel.offsetHeight;
                let animationFrameId = null;

                const onMove = (moveEvent) => {
                    if (animationFrameId) {
                        cancelAnimationFrame(animationFrameId);
                    }
                    animationFrameId = requestAnimationFrame(() => {
                        let dx = moveEvent.clientX - startX, dy = moveEvent.clientY - startY;
                        if (type === 'drag') {
                            let newX = Math.max(0, Math.min(window.innerWidth - this.panel.offsetWidth, initialX + dx));
                            let newY = Math.max(0, Math.min(window.innerHeight - this.panel.offsetHeight, initialY + dy));
                            this.panel.style.left = `${newX}px`;
                            this.panel.style.top = `${newY}px`;
                        } else { // resize
                            let newW = Math.max(200, initialW + dx);
                            let newH = Math.max(100, initialH + dy);
                            this.panel.style.width = `${newW}px`;
                            if (!this.panel.classList.contains('collapsed')) {
                                this.panel.style.height = `${newH}px`;
                            }
                        }
                    });
                };

                const onEnd = () => {
                    if (animationFrameId) {
                        cancelAnimationFrame(animationFrameId);
                    }
                    if (!this.panel.classList.contains('collapsed')) {
                        this.lastExpandedHeight = this.panel.offsetHeight;
                    }
                    this.saveSettings();
                    document.removeEventListener('mousemove', onMove);
                    document.removeEventListener('mouseup', onEnd);
                    this.panel.classList.remove('no-transition'); // 拖拽结束后恢复过渡动画
                };

                document.addEventListener('mousemove', onMove);
                document.addEventListener('mouseup', onEnd);
            };

            header.addEventListener('mousedown', e => { if (e.target === header || e.target.classList.contains('nav-title')) dragOrResize(e, 'drag'); });
            resizeHandle.addEventListener('mousedown', e => dragOrResize(e, 'resize'));
        }

        /**
         * 等待指定元素出现在DOM中
         * @param {string} selector - CSS选择器
         * @param {function} callback - 元素出现后执行的回调函数
         */
        waitForElement(selector, callback) {
            const interval = setInterval(() => {
                if (document.querySelector(selector)) {
                    clearInterval(interval);
                    callback();
                }
            }, 200);
        }
    }

    // 确保在DOM加载完成后再执行脚本,这是最关键的一步
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => new SearchEnhancer());
    } else {
        // 如果DOM已经加载完成,则直接执行
        new SearchEnhancer();
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址