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