Baidu & Google 双引擎同屏

在百度搜索结果页右侧显示谷歌搜索结果。

// ==UserScript==
// @name         Baidu & Google 双引擎同屏
// @namespace    476321082
// @version      1.1
// @description  在百度搜索结果页右侧显示谷歌搜索结果。
// @author       476321082
// @license      MIT
// @match        https://www.baidu.com/s*
// @connect      www.google.com
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function() {
    'use strict';

    // --- 常量定义 ---
    const C = {
        GM_SETTINGS_KEY: 'BaiduGoogleDualSearchSettings',
        IDS: {
            container: 'google-results-container',
            baiduPageContainer: 'container',
            settingsModal: 'google-settings-modal',
            settingEnabled: 'setting-enabled',
            settingCount: 'setting-count',
            settingNewTab: 'setting-newtab',
            settingAutofit: 'setting-autofit',
            settingWideLeft: 'setting-wide-left',
            settingReset: 'google-settings-reset',
            settingSave: 'google-settings-save',
            baiduContent: 'content_left',
            fetchStatus: 'google-fetch-status',
        },
        CLASSES: {
            header: 'google-results-header',
            settingsIcon: 'google-settings-icon',
            content: 'google-results-content',
            resizeHandle: 'resize-handle-right',
            settingsModalContent: 'google-settings-modal-content',
            formItem: 'google-settings-form-item',
            buttons: 'google-settings-buttons',
            resultItem: 'google-result-item',
            url: 'url',
            description: 'description',
            loading: 'loading',
            error: 'error',
            status: 'status',
        },
        SELECTORS: {
            googleResult: 'div#rso > div > div > div',
            link: 'a[href]',
            keyword: 'em',
            title: 'h3',
        }
    };

    // --- 默认设置 ---
    const defaultSettings = {
        scriptEnabled: true,
        resultCount: 15,
        openInNewTab: true,
        autoFitHeight: false,
        panelPosition: { top: '140px', left: '58%' },
        panelPositionWide: { left: '65%' }, // 大屏幕时的横向位置
        panelSize: { width: '40%', height: '500px' }
    };

    let currentSettings = {};
    let lastQuery = "";
    // --- 性能优化:缓存设置弹窗的DOM引用 ---
    let settingsModalManager = null;


    // --- 设置管理 ---
    function loadSettings() {
        const savedSettings = GM_getValue(C.GM_SETTINGS_KEY, {});
        currentSettings = { ...defaultSettings, ...savedSettings };
        currentSettings.panelPosition = { ...defaultSettings.panelPosition, ...(savedSettings.panelPosition || {}) };
        currentSettings.panelPositionWide = { ...defaultSettings.panelPositionWide, ...(savedSettings.panelPositionWide || {}) };
        currentSettings.panelSize = { ...defaultSettings.panelSize, ...(savedSettings.panelSize || {}) };
    }

    function saveSettings() {
        GM_setValue(C.GM_SETTINGS_KEY, currentSettings);
    }

    function debounce(func, delay) {
        let timeout;
        return function(...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), delay);
        };
    }

    // --- 样式定义 ---
    function applyStyles() {
        GM_addStyle(`
            #${C.IDS.container} { position: absolute; top: ${currentSettings.panelPosition.top}; left: ${currentSettings.panelPosition.left}; width: ${currentSettings.panelSize.width}; min-width: 300px; min-height: 200px; background-color: #fff; border: 1px solid #e4e7ed; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1); z-index: 9999; display: flex; flex-direction: column; overflow: hidden; }
            .${C.CLASSES.header} { display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; border-bottom: 1px solid #ebeef5; cursor: move; background-color: #f7f7f7; }
            .${C.CLASSES.header} h2 { font-size: 16px; font-weight: 600; color: #303133; margin: 0; }
            .${C.CLASSES.settingsIcon} { cursor: pointer; font-size: 18px; }
            .${C.CLASSES.content} { padding: 15px; flex-grow: 1; overflow-y: auto; }
            .${C.CLASSES.resultItem} { margin-bottom: 18px; border-bottom: 1px solid #f0f2f5; padding-bottom: 15px; }
            .${C.CLASSES.resultItem}:last-child { border-bottom: none; }
            .${C.CLASSES.resultItem} a { font-size: 16px; font-weight: 500; color: #1a0dab; text-decoration: none; }
            .${C.CLASSES.resultItem} a:hover { text-decoration: underline; }
            .${C.CLASSES.resultItem} .${C.CLASSES.url} { font-size: 13px; color: #006621; padding-top: 2px; word-break: break-all; }
            .${C.CLASSES.resultItem} .${C.CLASSES.description} { font-size: 14px; color: #545454; line-height: 1.5; padding-top: 4px; }
            .${C.CLASSES.content} .${C.CLASSES.loading}, .${C.CLASSES.content} .${C.CLASSES.error}, .${C.CLASSES.content} .${C.CLASSES.status} { color: #909399; padding: 10px; text-align: center; }
            .${C.CLASSES.resultItem} em { color: rgb(247, 49, 49) !important; font-style: normal !important; font-weight: 500 !important; background: none !important; }
            #${C.IDS.container} em { color: rgb(247, 49, 49) !important; font-style: normal !important; font-weight: 500 !important; background: none !important; }
            #${C.IDS.settingsModal} { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.4); z-index: 10001; display: none; align-items: center; justify-content: center; }
            .${C.CLASSES.settingsModalContent} { background: white; padding: 20px; border-radius: 8px; width: 400px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); }
            .${C.CLASSES.settingsModalContent} h3 { margin-top: 0; }
            .${C.CLASSES.formItem} { margin-bottom: 15px; display: flex; align-items: center; }
            .${C.CLASSES.formItem} label { display: block; margin-bottom: 0; }
            .${C.CLASSES.formItem} input[type="number"] { width: 80px; }
            .${C.CLASSES.formItem} input[type="checkbox"] { margin-right: 10px; height: 16px; width: 16px; }
            .${C.CLASSES.buttons} { text-align: right; margin-top: 20px; }
            .${C.CLASSES.buttons} button { padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; margin-left: 10px; }
            #${C.IDS.settingSave} { background: #409eff; color: white; }
            #${C.IDS.settingReset} { background: #f56c6c; color: white; }
            .${C.CLASSES.resizeHandle} { position: absolute; right: 0; top: 0; width: 10px; height: 100%; cursor: col-resize; z-index: 1; }
        `);
    }

    // --- UI & 交互 ---
    function setupUI() {
        let container = document.getElementById(C.IDS.container);
        if (container) {
            container.style.display = 'flex';
            return container;
        }
        const parentElement = document.getElementById(C.IDS.baiduPageContainer) || document.body;
        container = document.createElement('div');
        container.id = C.IDS.container;
        parentElement.appendChild(container);
        if (parentElement.id !== C.IDS.baiduPageContainer) {
            container.style.position = 'fixed';
        }
        container.style.top = currentSettings.panelPosition.top;
        container.style.width = currentSettings.panelSize.width;
        container.style.height = currentSettings.panelSize.height;
        updatePositionByWidth();
        container.innerHTML = `
            <div class="${C.CLASSES.header}">
                <h2>Google 搜索结果</h2>
                <span class="${C.CLASSES.settingsIcon}">⚙️</span>
            </div>
            <div class="${C.CLASSES.content}"></div>
            <div class="${C.CLASSES.resizeHandle}"></div>
        `;
        const header = container.querySelector('.' + C.CLASSES.header);
        const settingsIcon = container.querySelector('.' + C.CLASSES.settingsIcon);
        const resizeHandle = container.querySelector('.' + C.CLASSES.resizeHandle);

        settingsIcon.onclick = (e) => { e.stopPropagation(); showSettingsModal(); };
        makeDraggable(container, header);
        makeResizable(container, resizeHandle);

        const debouncedSaveSettings = debounce(saveSettings, 500);
        new ResizeObserver(() => {
            if (document.getElementById(C.IDS.container)) {
                currentSettings.panelSize.width = container.style.width;
                if (!currentSettings.autoFitHeight) {
                    currentSettings.panelSize.height = container.style.height;
                }
                debouncedSaveSettings();
            }
        }).observe(container);
        updatePanelStyle(container);
        return container;
    }

    function updatePanelStyle(container) {
        if (!container) container = document.getElementById(C.IDS.container);
        if (!container) return;
        const contentDiv = container.querySelector('.' + C.CLASSES.content);
        container.style.resize = 'none';
        if (currentSettings.autoFitHeight) {
            container.style.height = 'auto';
            if(contentDiv) contentDiv.style.overflowY = 'visible';
        } else {
            container.style.height = currentSettings.panelSize.height;
            if(contentDiv) contentDiv.style.overflowY = 'auto';
        }
    }

    function makeResizable(element, handle) {
        let initialWidth = 0;
        let initialX = 0;

        handle.onmousedown = function(e) {
            e.preventDefault();
            e.stopPropagation();
            initialWidth = element.offsetWidth;
            initialX = e.clientX;
            document.onmousemove = resizeElement;
            document.onmouseup = stopResize;
        };

        function resizeElement(e) {
            const newWidth = initialWidth + (e.clientX - initialX);
            if (newWidth <= 300) return;
            requestAnimationFrame(() => {
                element.style.width = newWidth + 'px';
            });
        }

        function stopResize() {
            document.onmousemove = null;
            document.onmouseup = null;
        }
    }

    function makeDraggable(element, handle) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        handle.onmousedown = function(e) {
            e.preventDefault();
            pos3 = e.clientX; pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        };
        function elementDrag(e) {
            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;
            requestAnimationFrame(() => {
                element.style.top = (element.offsetTop - pos2) + "px";
                element.style.left = (element.offsetLeft - pos1) + "px";
            });
        }
        function closeDragElement() {
            document.onmouseup = null; document.onmousemove = null;
            currentSettings.panelPosition.top = element.style.top;
            const currentWidth = window.innerWidth;
            if (currentWidth > 1921) {
                currentSettings.panelPositionWide.left = element.style.left;
            } else {
                currentSettings.panelPosition.left = element.style.left;
            }
            saveSettings();
        }
    }

    // --- 性能优化:设置弹窗管理 ---
    function createSettingsModal() {
        const modal = document.createElement('div');
        modal.id = C.IDS.settingsModal;
        modal.innerHTML = `
        <div class="${C.CLASSES.settingsModalContent}" onclick="event.stopPropagation();">
            <h3>脚本设置</h3>
            <div class="${C.CLASSES.formItem}"><label><input type="checkbox" id="${C.IDS.settingEnabled}"> 启用脚本</label></div>
            <div class="${C.CLASSES.formItem}">
                <label for="${C.IDS.settingCount}">搜索结果数量</label>
                <input type="number" id="${C.IDS.settingCount}" min="1" max="50" step="1">
            </div>
            <div class="${C.CLASSES.formItem}"><label><input type="checkbox" id="${C.IDS.settingNewTab}"> 在新标签页中打开链接</label></div>
            <div class="${C.CLASSES.formItem}"><label><input type="checkbox" id="${C.IDS.settingAutofit}"> 自适应内容高度</label></div>
            <div class="${C.CLASSES.formItem}">
                <label>大屏幕横向位置 (>1921px)</label>
                <input type="text" id="${C.IDS.settingWideLeft}" placeholder="65%">
            </div>
            <div class="${C.CLASSES.buttons}">
                <button id="${C.IDS.settingReset}">恢复默认</button>
                <button id="${C.IDS.settingSave}">保存并关闭</button>
            </div>
        </div>
        `;
        document.body.appendChild(modal);

        const elements = {
            modal: modal,
            enabled: document.getElementById(C.IDS.settingEnabled),
            count: document.getElementById(C.IDS.settingCount),
            newTab: document.getElementById(C.IDS.settingNewTab),
            autofit: document.getElementById(C.IDS.settingAutofit),
            wideLeft: document.getElementById(C.IDS.settingWideLeft),
            saveBtn: document.getElementById(C.IDS.settingSave),
            resetBtn: document.getElementById(C.IDS.settingReset),
        };

        const hide = () => { elements.modal.style.display = 'none'; };
        elements.modal.onclick = hide;

        elements.saveBtn.onclick = () => {
            currentSettings.scriptEnabled = elements.enabled.checked;
            currentSettings.resultCount = parseInt(elements.count.value, 10);
            currentSettings.openInNewTab = elements.newTab.checked;
            currentSettings.autoFitHeight = elements.autofit.checked;
            currentSettings.panelPositionWide.left = elements.wideLeft.value;
            saveSettings();
            hide();
            runCheck({ forceUpdate: true });
            updatePositionByWidth();
        };

        elements.resetBtn.onclick = () => {
            if (confirm('确定要恢复所有默认设置吗?')) {
                GM_setValue(C.GM_SETTINGS_KEY, defaultSettings);
                loadSettings();
                hide();
                runCheck({ forceUpdate: true });
            }
        };

        return elements;
    }

    function showSettingsModal() {
        if (!settingsModalManager) {
            settingsModalManager = createSettingsModal();
        }

        // 每次打开前,都用当前设置更新UI
        settingsModalManager.enabled.checked = currentSettings.scriptEnabled;
        settingsModalManager.count.value = currentSettings.resultCount;
        settingsModalManager.newTab.checked = currentSettings.openInNewTab;
        settingsModalManager.autofit.checked = currentSettings.autoFitHeight;
        settingsModalManager.wideLeft.value = currentSettings.panelPositionWide.left;

        // 显示弹窗
        settingsModalManager.modal.style.display = 'flex';
    }


    // --- 关键词标红功能 ---
    function getBaiduKeywords() {
        const baiduResultContainer = document.getElementById(C.IDS.baiduContent);
        if (!baiduResultContainer) {
            return [];
        }
        const keywordElements = baiduResultContainer.querySelectorAll(C.SELECTORS.keyword);
        const keywords = new Set();
        keywordElements.forEach(em => {
            const text = em.textContent.trim();
            if (text) {
                keywords.add(text);
            }
        });
        return Array.from(keywords);
    }

    function highlightKeywords(text, keywordArray) {
        if (!text || !keywordArray || keywordArray.length === 0) {
            return text;
        }
        const regexPattern = keywordArray
            .map(keyword => keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) // Corrected Regex
            .join('|');
        if (!regexPattern) {
            return text;
        }
        const regex = new RegExp(`(${regexPattern})`, 'gi');
        return text.replace(regex, `<${C.SELECTORS.keyword}>$1</${C.SELECTORS.keyword}>`);
    }

    // --- 数据获取与渲染 (Async/Await 重构) ---
    function gmFetch(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                onload: resolve,
                onerror: reject
            });
        });
    }

    async function fetchAndDisplayGoogleResults(query) {
        const container = setupUI();
        if (!container) return;
        updatePanelStyle(container);

        const contentDiv = container.querySelector('.' + C.CLASSES.content);
        if (!contentDiv) return;
        contentDiv.innerHTML = `<div class="${C.CLASSES.loading}">正在加载...</div>`;

        const statusDiv = document.createElement('div');
        statusDiv.id = C.IDS.fetchStatus;
        statusDiv.className = C.CLASSES.status;
        contentDiv.appendChild(statusDiv);

        if (contentDiv.querySelector('.' + C.CLASSES.loading)) {
            contentDiv.querySelector('.' + C.CLASSES.loading).remove();
        }

        let renderedCount = 0;
        let startIndex = 0;
        const baiduKeywords = getBaiduKeywords();
        const highlightKeywordsList = baiduKeywords.length > 0 ? baiduKeywords : query.split(' ');

        try {
            while (renderedCount < currentSettings.resultCount) {
                statusDiv.innerHTML = `已获取 ${renderedCount} 条,正在加载第 ${startIndex + 1}-${startIndex + 10} 条...`;
                const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}&num=10&start=${startIndex}`;
                const response = await gmFetch(searchUrl);
                const parser = new DOMParser();
                const doc = parser.parseFromString(response.responseText, "text/html");
                const results = Array.from(doc.querySelectorAll(C.SELECTORS.googleResult));

                if (results.length === 0) {
                    if (renderedCount === 0) {
                         statusDiv.innerHTML = `未找到 Google 结果。`;
                    }
                    break;
                }

                // --- 性能优化:使用 DocumentFragment 批量更新DOM ---
                const fragment = document.createDocumentFragment();
                let newResultsFoundInPage = 0;

                results.forEach(result => {
                    if (renderedCount >= currentSettings.resultCount) return;

                    const link = result.querySelector(C.SELECTORS.link);
                    const title = result.querySelector(C.SELECTORS.title);

                    if (link && title && link.href) {
                        newResultsFoundInPage++;
                        if (link.getAttribute('href').startsWith('/')) {
                            link.href = 'https://www.google.com' + link.getAttribute('href');
                        }

                        const descriptionContainer = Array.from(result.querySelectorAll('div')).find(d =>
                            d.innerText && d.innerText.length > 40 && !d.querySelector('div')
                        );

                        const item = document.createElement('div');
                        item.className = C.CLASSES.resultItem;
                        const urlText = new URL(link.href).hostname;
                        const target = currentSettings.openInNewTab ? 'target="_blank"' : '';

                        const originalTitle = title.innerText;
                        const originalDescription = descriptionContainer ? descriptionContainer.innerText : '';

                        const highlightedTitle = highlightKeywords(originalTitle, highlightKeywordsList);
                        const highlightedDescription = highlightKeywords(originalDescription, highlightKeywordsList);

                        item.innerHTML = `<a href="${link.href}" ${target} rel="noopener noreferrer">${highlightedTitle}</a><div class="${C.CLASSES.url}">${urlText}</div><div class="${C.CLASSES.description}">${highlightedDescription}</div>`;
                        fragment.appendChild(item); // 添加到fragment,而非直接添加到DOM
                        renderedCount++;
                    }
                });

                // 将fragment一次性插入DOM
                contentDiv.insertBefore(fragment, statusDiv);

                if (newResultsFoundInPage === 0) {
                    break;
                }

                startIndex += 10;
            }
            statusDiv.innerHTML = `已加载全部 ${renderedCount} 条结果。`;
        } catch (error) {
            console.error('Gemini Script Error fetching Google results:', error);
            statusDiv.innerHTML = `请求第 ${startIndex + 1} 条起的结果时出错。`;
        }
    }

    // --- V3 主逻辑与监听 ---
    function getQuery() {
        return new URLSearchParams(window.location.search).get('wd');
    }

    function runCheck(options = {}) {
        loadSettings();
        const query = getQuery();
        if (!query) {
            const container = document.getElementById(C.IDS.container);
            if(container) container.style.display = 'none';
            return;
        }
        const mainContainer = setupUI();
        if (!currentSettings.scriptEnabled) {
            mainContainer.style.display = 'none';
            return;
        }
        mainContainer.style.display = 'flex';
        applyStyles();

        if (query !== lastQuery || options.forceUpdate) {
            lastQuery = query;
            fetchAndDisplayGoogleResults(query);
        }
    }

    // --- 宽度监听与动态定位 ---
    function updatePositionByWidth() {
        const container = document.getElementById(C.IDS.container);
        if (!container) return;

        const currentWidth = window.innerWidth;

        if (currentWidth > 1921) {
            container.style.left = currentSettings.panelPositionWide.left;
        } else {
            container.style.left = currentSettings.panelPosition.left;
        }
    }

    const debouncedUpdatePosition = debounce(updatePositionByWidth, 200);

    // --- Entry Point ---
    runCheck();

    const debouncedRunCheck = debounce(runCheck, 400);

    const observer = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList') {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === 1 && (node.id === C.IDS.baiduContent || node.querySelector('#' + C.IDS.baiduContent))) {
                        debouncedRunCheck();
                        return;
                    }
                }
            }
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    setTimeout(updatePositionByWidth, 500);

    window.addEventListener('resize', debouncedUpdatePosition);

})();