// ==UserScript==
// @name 综合网页搜索BZ
// @namespace http://tampermonkey.net/
// @version 0.1
// @description 搜索/检索单个网页并在原网页和弹窗显示多个搜索结果,支持动态搜索和显示方式选择,搜索不区分大小写,支持移动搜索容器和弹窗,点击弹窗结果滑动到对应位置并高亮
// @author 你自己
// @match *://*/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
// 创建搜索容器
const searchContainer = document.createElement('div');
searchContainer.style.position = 'fixed';
searchContainer.style.top = '10px';
searchContainer.style.left = '10px';
searchContainer.style.zIndex = '9999';
searchContainer.style.backgroundColor = '#f9f9f9';
searchContainer.style.padding = '15px';
searchContainer.style.border = '1px solid #ddd';
searchContainer.style.borderRadius = '8px';
searchContainer.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.1)';
makeDraggable(searchContainer);
// 创建搜索输入框
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = '输入关键词,用分号分隔';
searchInput.style.padding = '8px';
searchInput.style.border = '1px solid #ccc';
searchInput.style.borderRadius = '4px';
searchContainer.appendChild(searchInput);
// 创建添加关键词按钮
const addKeywordButton = document.createElement('button');
addKeywordButton.textContent = '添加关键词';
addKeywordButton.style.padding = '8px 12px';
addKeywordButton.style.marginLeft = '8px';
addKeywordButton.style.backgroundColor = '#007BFF';
addKeywordButton.style.color = 'white';
addKeywordButton.style.border = 'none';
addKeywordButton.style.borderRadius = '4px';
addKeywordButton.style.cursor = 'pointer';
addKeywordButton.addEventListener('click', function () {
const newKeyword = prompt('请输入要添加的关键词:');
if (newKeyword) {
const currentValue = searchInput.value;
searchInput.value = currentValue? currentValue + ';' + newKeyword : newKeyword;
}
searchInput.focus();
});
searchContainer.appendChild(addKeywordButton);
// 创建搜索动态数据按钮
const searchDynamicButton = document.createElement('button');
searchDynamicButton.textContent = '开启动态搜索';
searchDynamicButton.style.padding = '8px 12px';
searchDynamicButton.style.marginLeft = '8px';
searchDynamicButton.style.backgroundColor = '#ffc107';
searchDynamicButton.style.color = 'white';
searchDynamicButton.style.border = 'none';
searchDynamicButton.style.borderRadius = '4px';
searchDynamicButton.style.cursor = 'pointer';
searchContainer.appendChild(searchDynamicButton);
// 创建搜索按钮
const searchButton = document.createElement('button');
searchButton.textContent = '搜索';
searchButton.style.padding = '8px 12px';
searchButton.style.marginLeft = '8px';
searchButton.style.backgroundColor = '#dc3545';
searchButton.style.color = 'white';
searchButton.style.border = 'none';
searchButton.style.borderRadius = '4px';
searchButton.style.cursor = 'pointer';
searchContainer.appendChild(searchButton);
// 显示方式选择
const displayOptionSelect = document.createElement('select');
const originalOption = document.createElement('option');
originalOption.value = 'original';
originalOption.textContent = '在原网页显示';
const popupOption = document.createElement('option');
popupOption.value = 'popup';
popupOption.textContent = '弹窗显示';
displayOptionSelect.appendChild(originalOption);
displayOptionSelect.appendChild(popupOption);
displayOptionSelect.style.marginLeft = '5px';
searchContainer.appendChild(displayOptionSelect);
document.body.appendChild(searchContainer);
// 用于存储最新的文本节点
let textNodes = [];
// 收集文本节点的函数
function collectTextNodes(node) {
if (node.nodeType === Node.TEXT_NODE) {
textNodes.push(node);
} else {
const children = node.childNodes;
for (let i = 0; i < children.length; i++) {
collectTextNodes(children[i]);
}
}
}
// 搜索按钮点击事件
searchButton.addEventListener('click', function () {
// 重置状态
textNodes = [];
collectTextNodes(document.body);
// 清除之前原网页的高亮显示
clearHighlightedSpans();
// 清除之前的弹窗搜索结果容器
clearPopupResultsContainer();
const keywords = searchInput.value.split(';').map(keyword => keyword.trim().toLowerCase());
const searchDynamic = isDynamicSearchEnabled;
const displayOption = displayOptionSelect.value;
const searchResults = [];
keywords.forEach(keyword => {
const matches = findMatches(keyword, searchDynamic);
matches.forEach(match => {
searchResults.push({ keyword, match });
});
});
if (displayOption === 'popup') {
const resultsContainer = document.createElement('div');
resultsContainer.style.position = 'fixed';
resultsContainer.style.top = '70px';
resultsContainer.style.right = '10px';
resultsContainer.style.zIndex = '9999';
resultsContainer.style.backgroundColor = 'white';
resultsContainer.style.border = '1px solid #ddd';
resultsContainer.style.borderRadius = '5px';
resultsContainer.style.padding = '20px';
resultsContainer.style.width = '400px';
resultsContainer.style.height = '400px';
resultsContainer.style.overflow = 'auto';
resultsContainer.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
makeDraggable(resultsContainer);
const closeButton = document.createElement('button');
closeButton.textContent = '关闭';
closeButton.style.position = 'absolute';
closeButton.style.top = '10px';
closeButton.style.right = '10px';
closeButton.style.padding = '5px 10px';
closeButton.style.backgroundColor = '#dc3545';
closeButton.style.color = 'white';
closeButton.style.border = 'none';
closeButton.style.borderRadius = '3px';
closeButton.style.cursor = 'pointer';
closeButton.addEventListener('mouseover', function () {
closeButton.style.backgroundColor = '#c82333';
});
closeButton.addEventListener('mouseout', function () {
closeButton.style.backgroundColor = '#dc3545';
});
closeButton.addEventListener('click', function () {
document.body.removeChild(resultsContainer);
});
resultsContainer.appendChild(closeButton);
searchResults.forEach((result, index) => {
const text = result.match.node.textContent;
const originalKeyword = result.keyword;
const keyword = originalKeyword;
const indexOfKeyword = text.toLowerCase().indexOf(keyword);
let start = Math.max(0, indexOfKeyword - 20);
let end = Math.min(text.length, indexOfKeyword + keyword.length + 20);
let context = text.slice(start, end);
const resultDiv = document.createElement('div');
resultDiv.style.padding = '10px';
resultDiv.style.borderBottom = '1px solid #eee';
resultDiv.innerHTML = `${index + 1}. ${context.replace(originalKeyword, `${originalKeyword}`)}`;
resultDiv.addEventListener('click', function () {
// 清除之前所有的高亮显示
clearHighlightedSpans();
const targetElement = result.match.node.parentElement;
targetElement.scrollIntoView({ behavior:'smooth', block: 'center' });
const spans = targetElement.querySelectorAll('span');
spans.forEach(span => {
if (span.textContent.includes(originalKeyword)) {
span.style.backgroundColor = 'yellow';
}
});
});
resultDiv.addEventListener('mouseover', function () {
resultDiv.style.backgroundColor = '#f9f9f9';
});
resultDiv.addEventListener('mouseout', function () {
resultDiv.style.backgroundColor = 'white';
});
resultsContainer.appendChild(resultDiv);
});
document.body.appendChild(resultsContainer);
} else {
showOriginalResults(searchResults);
}
if (searchResults.length === 0) {
// 未找到结果提示
const existingAlert = document.querySelector('div[style*="position: fixed; top: 10px; left: 50%; transform: translateX(-50%); z-index: 9999;"]');
if (existingAlert) {
document.body.removeChild(existingAlert);
}
const alertDiv = document.createElement('div');
alertDiv.style.position = 'fixed';
alertDiv.style.top = '10px';
alertDiv.style.left = '50%';
alertDiv.style.transform = 'translateX(-50%)';
alertDiv.style.zIndex = '9999';
alertDiv.style.backgroundColor = 'red';
alertDiv.style.color = 'white';
alertDiv.style.padding = '10px';
alertDiv.style.borderRadius = '5px';
alertDiv.textContent = '未找到匹配结果。';
document.body.appendChild(alertDiv);
setTimeout(() => {
if (alertDiv.parentNode) {
document.body.removeChild(alertDiv);
}
}, 3000);
}
});
// 创建 MutationObserver 来监听页面变化
const observer = new MutationObserver((mutationsList) => {
textNodes = [];
collectTextNodes(document.body);
});
// 配置观察选项
const config = { childList: true, subtree: true };
// 动态搜索开关状态
let isDynamicSearchEnabled = false;
// 动态搜索开关按钮点击事件
searchDynamicButton.addEventListener('click', function () {
if (isDynamicSearchEnabled) {
observer.disconnect();
searchDynamicButton.textContent = '开启动态搜索';
searchDynamicButton.style.backgroundColor = '#ffc107';
} else {
observer.observe(document.body, config);
searchDynamicButton.textContent = '关闭动态搜索';
searchDynamicButton.style.backgroundColor = '#dc3545';
}
isDynamicSearchEnabled =!isDynamicSearchEnabled;
});
/**
* 查找匹配项
* @param {string} keyword - 搜索关键词
* @param {boolean} searchDynamic - 是否搜索动态数据
* @returns {Array} - 匹配结果数组
*/
function findMatches(keyword, searchDynamic) {
const matches = [];
textNodes.forEach(node => {
let nodeText = node.textContent.toLowerCase().trim();
keyword = keyword.trim().toLowerCase();
const index = nodeText.indexOf(keyword);
if (index!== -1) {
matches.push({ node, index });
}
});
if (searchDynamic) {
// 这里可以添加搜索动态数据的逻辑,例如监听 DOM 变化
}
return matches;
}
/**
* 在原网页中显示搜索结果
* @param {Array} results - 搜索结果数组
*/
function showOriginalResults(results) {
results.forEach((result, index) => {
const node = result.match.node;
const text = node.textContent;
const originalKeyword = result.keyword;
const keyword = originalKeyword;
const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(escapedKeyword, 'gi');
const newText = text.replace(regex, `${originalKeyword}`);
const range = document.createRange();
range.selectNodeContents(node);
const fragment = range.createContextualFragment(newText);
node.parentNode.replaceChild(fragment, node);
});
}
/**
* 清除原网页中高亮显示的 标签
*/
function clearHighlightedSpans() {
const highlightedSpans = document.querySelectorAll('span[style*="background-color"]');
highlightedSpans.forEach(span => {
const parent = span.parentNode;
const textNode = document.createTextNode(span.textContent);
parent.insertBefore(textNode, span);
parent.removeChild(span);
});
}
/**
* 清除弹窗中的搜索结果容器
*/
function clearPopupResultsContainer() {
const existingResultsContainer = document.querySelector('div[style*="position: fixed; top: 70px; right: 10px; z-index: 9999;"]');
if (existingResultsContainer) {
document.body.removeChild(existingResultsContainer);
}
}
/**
* 获取唯一的颜色
* @param {number} index - 结果索引
* @returns {string} 颜色值
*/
function getUniqueColor(index) {
const baseColors = ['#FFFF00', '#FF00FF', '#00FFFF', '#FFA500', '#00FF00'];
const hueShift = (index % baseColors.length) * 360 / baseColors.length;
const saturation = '80%';
const lightness = '50%';
return `hsl(${hueShift}, ${saturation}, ${lightness})`;
}
/**
* 使元素可拖动
* @param {HTMLElement} element - 要设置为可拖动的元素
*/
function makeDraggable(element) {
let isDragging = false;
let offsetX, offsetY;
element.addEventListener('mousedown', (e) => {
if (e.target === element) {
isDragging = true;
offsetX = e.clientX - parseInt(getComputedStyle(element).left);
offsetY = e.clientY - parseInt(getComputedStyle(element).top);
e.preventDefault();
}
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
element.style.left = (e.clientX - offsetX) + 'px';
element.style.top = (e.clientY - offsetY) + 'px';
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
}
})();