您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在百度搜索结果页右侧显示谷歌搜索结果。
// ==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); })();