// ==UserScript== // @name 智教联盟论坛搜索修复 // @namespace https://github.com/ // @version 1.8 // @description 修复forum.smart-teach.cn论坛搜索功能,使用必应搜索引擎搜索论坛内容(已禁用系统通知) // @author 无不滑稽(以及DeepSeek,Grok和Claude组成的主力) // @match *://forum.smart-teach.cn/* // @icon https://forum.smart-teach.cn/favicon.ico // @grant GM_openInTab // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @license 随你咯,反正AI写的(?) // @run-at document-end // ==/UserScript== (function() { 'use strict'; // 配置 const CONFIG = { searchEngine: 'bing', searchEngines: { bing: 'https://www.bing.com/search?q={query}', baidu: 'https://www.baidu.com/s?wd={query}', google: 'https://www.google.com/search?q={query}' }, searchSelectors: [ 'header input[type="search"]', '.App-header input[type="search"]', '.Search-input', 'input[aria-label="搜索"]', 'input[placeholder*="搜索论坛"]', 'input[placeholder*="Search forum"]' ], debug: false, historySize: 10, autoAddSiteRestriction: true, siteRestriction: 'site:forum.smart-teach.cn' }; let searchHistory = []; function init() { log('智教联盟论坛搜索修复脚本已加载 (v1.8)'); loadSearchHistory(); cleanupInvalidPatches(); observeDOMChanges(); hookHistoryMethods(); setTimeout(findAndPatchSearchBox, 1000); setTimeout(findAndPatchSearchBox, 4000); addSettingsMenu(); } function cleanupInvalidPatches() { const patchedInputs = document.querySelectorAll('input[data-patched="true"]'); let cleanedCount = 0; patchedInputs.forEach(input => { const type = (input.type || '').toLowerCase(); const ariaLabel = (input.getAttribute('aria-label') || '').toLowerCase(); if (type !== 'search' || ariaLabel !== '搜索') { log(`清理误判的补丁: placeholder="${input.placeholder}"`); if (input.dataset.originalPlaceholder) { input.placeholder = input.dataset.originalPlaceholder; } delete input.dataset.patched; delete input.dataset.originalPlaceholder; input.removeEventListener('keydown', handleSearchKeyDown); cleanedCount++; } }); if (cleanedCount > 0) { log(`已清理 ${cleanedCount} 个误判的补丁`); } } function findAndPatchSearchBox() { log('正在查找搜索框...'); let searchInput = null; for (const selector of CONFIG.searchSelectors) { try { const elements = document.querySelectorAll(selector); for (const el of elements) { if (isValidSearchBox(el)) { searchInput = el; log(`找到搜索框: ${selector}`); break; } } if (searchInput) break; } catch (e) { log(`选择器 ${selector} 错误: ${e.message}`); } } if (!searchInput) { searchInput = findSearchBoxByTraversal(); } if (searchInput) { patchSearchBox(searchInput); return true; } log('未找到搜索框,等待后续变化...'); return false; } function isValidSearchBox(element) { if (!element || element.offsetParent === null) return false; const placeholder = (element.placeholder || '').toLowerCase(); const type = (element.type || '').toLowerCase(); const ariaLabel = (element.getAttribute('aria-label') || '').toLowerCase(); const excludeKeywords = [ '标题', 'title', 'subject', '帖子标题', 'topic title', '回复', 'reply', 'content', '正文', 'body', '编辑', 'edit', '用户名', 'username', 'email', '标签', 'tag', '主标签', '副标签', '请选择' ]; if (excludeKeywords.some(kw => placeholder.includes(kw) || ariaLabel.includes(kw))) { return false; } let parent = element.parentElement; let contextText = ''; for (let i = 0; i < 5 && parent; i++) { contextText += (parent.textContent || '').toLowerCase() + ' '; parent = parent.parentElement; } if (/发布.*主题|回复.*主题|编辑.*帖子|create.*discussion|reply|post|添加标签|选择标签/i.test(contextText)) { return false; } const style = window.getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false; } return type === 'search' && ariaLabel === '搜索'; } function findSearchBoxByTraversal() { const header = document.querySelector('header, .App-header, #header-primary'); if (header) { const inputs = header.querySelectorAll('input[type="search"][aria-label="搜索"]'); for (const input of inputs) { if (isValidSearchBox(input)) { log('在header区域找到搜索框'); return input; } } } return null; } function patchSearchBox(searchInput) { if (searchInput.dataset.patched === 'true') { return; } log(`修补搜索框: ${searchInput.outerHTML.substring(0, 100)}...`); searchInput.dataset.patched = 'true'; searchInput.dataset.originalPlaceholder = searchInput.placeholder || '搜索'; searchInput.removeEventListener('keydown', handleSearchKeyDown); searchInput.addEventListener('keydown', handleSearchKeyDown); addContextMenu(searchInput); updatePlaceholder(searchInput); if (searchHistory.length > 0) { addSearchHistoryDropdown(searchInput); } addStyles(); log('搜索框修补完成'); } function updatePlaceholder(searchInput) { const originalPlaceholder = searchInput.dataset.originalPlaceholder || '搜索'; const engineName = { bing: '必应', baidu: '百度', google: '谷歌' }[CONFIG.searchEngine] || '必应'; searchInput.placeholder = `${originalPlaceholder}(回车使用${engineName}站内搜索)`; } function handleSearchKeyDown(event) { if (event.key === 'Enter' || event.keyCode === 13) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); const searchText = event.target.value.trim(); if (!searchText) { showToast('请输入搜索内容'); return; } performSearch(searchText); } } function performSearch(searchText) { log(`执行搜索: ${searchText}`); saveToHistory(searchText); let query = searchText.trim(); if (CONFIG.autoAddSiteRestriction && !/site\s*[:=]/i.test(query)) { query += ` ${CONFIG.siteRestriction}`; } const encodedQuery = encodeURIComponent(query); const engineTemplate = CONFIG.searchEngines[CONFIG.searchEngine] || CONFIG.searchEngines.bing; const searchUrl = engineTemplate.replace('{query}', encodedQuery); log(`搜索URL: ${searchUrl}`); if (typeof GM_openInTab === 'function') { GM_openInTab(searchUrl, { active: true, insert: true }); } else { window.open(searchUrl, '_blank'); } const engineName = { bing: '必应', baidu: '百度', google: '谷歌' }[CONFIG.searchEngine] || '必应'; showToast(`使用${engineName}搜索:${searchText}`); } function saveToHistory(searchText) { searchHistory = searchHistory.filter(item => item !== searchText); searchHistory.unshift(searchText); if (searchHistory.length > CONFIG.historySize) { searchHistory = searchHistory.slice(0, CONFIG.historySize); } GM_setValue('searchHistory', JSON.stringify(searchHistory)); } function loadSearchHistory() { try { searchHistory = JSON.parse(GM_getValue('searchHistory', '[]')) || []; } catch (e) { searchHistory = []; } } /* ===== 全新设计的搜索历史下拉面板 ===== */ function addSearchHistoryDropdown(searchInput) { // 如果已存在则跳过 if (searchInput.dataset.historyDropdown === 'true') return; searchInput.dataset.historyDropdown = 'true'; const container = document.createElement('div'); container.className = 'sf-dropdown'; // 基础定位由 JS 动态设置,样式在样式表中统一控制 container.style.position = 'absolute'; container.style.display = 'none'; container.style.zIndex = '9999'; container.style.minWidth = searchInput.offsetWidth + 'px'; let parent = searchInput.parentNode; if (getComputedStyle(parent).position === 'static') { parent.style.position = 'relative'; } parent.appendChild(container); function renderList() { container.innerHTML = ''; if (searchHistory.length === 0) { const empty = document.createElement('div'); empty.textContent = '暂无搜索历史'; empty.classList.add('sf-dropdown-empty'); container.appendChild(empty); return; } const header = document.createElement('div'); header.classList.add('sf-dropdown-header'); header.textContent = '搜索历史'; container.appendChild(header); const list = document.createElement('div'); list.classList.add('sf-dropdown-list'); searchHistory.forEach((item) => { const row = document.createElement('div'); row.classList.add('sf-dropdown-item'); row.title = item; row.textContent = item.length > 40 ? item.substring(0, 40) + '…' : item; row.addEventListener('click', (e) => { e.stopPropagation(); searchInput.value = item; container.style.display = 'none'; searchInput.focus(); }); list.appendChild(row); }); container.appendChild(list); const footer = document.createElement('div'); footer.classList.add('sf-dropdown-footer'); footer.textContent = '清空历史'; footer.addEventListener('click', (e) => { e.stopPropagation(); if (confirm('确定清空搜索历史吗?')) { searchHistory = []; GM_setValue('searchHistory', '[]'); container.style.display = 'none'; showToast('搜索历史已清空'); } }); container.appendChild(footer); } // 焦点进入时显示并计算位置 searchInput.addEventListener('focus', () => { renderList(); const rect = searchInput.getBoundingClientRect(); const parentRect = searchInput.parentNode.getBoundingClientRect(); container.style.top = (rect.bottom - parentRect.top + 4) + 'px'; container.style.left = (rect.left - parentRect.left) + 'px'; container.style.width = rect.width + 'px'; container.style.display = 'block'; }); // 失焦后延迟隐藏,避免点击事件无法触发 searchInput.addEventListener('blur', () => { setTimeout(() => { if (!container.matches(':hover')) { container.style.display = 'none'; } }, 150); }); // 鼠标悬停保持显示 container.addEventListener('mouseenter', () => { container.style.display = 'block'; }); container.addEventListener('mouseleave', () => { setTimeout(() => { if (document.activeElement !== searchInput) { container.style.display = 'none'; } }, 100); }); // 点击外部隐藏 document.addEventListener('click', (e) => { if (!container.contains(e.target) && e.target !== searchInput) { container.style.display = 'none'; } }); } function addContextMenu(searchInput) { // 避免重复绑定 if (searchInput.dataset.contextMenu === 'true') return; searchInput.dataset.contextMenu = 'true'; searchInput.addEventListener('contextmenu', (e) => { e.preventDefault(); const menu = document.createElement('div'); menu.className = 'sf-context-menu'; // 使用固定定位相对于页面 menu.style.position = 'fixed'; menu.style.left = e.pageX + 'px'; menu.style.top = e.pageY + 'px'; const items = [ { text: '用百度搜索', engine: 'baidu' }, { text: '用必应搜索', engine: 'bing' }, { text: '用谷歌搜索', engine: 'google' }, { type: 'separator' }, { text: '清空搜索历史', action: () => clearHistoryForMenu() } ]; items.forEach(item => { if (item.type === 'separator') { const sep = document.createElement('div'); sep.classList.add('sf-menu-separator'); menu.appendChild(sep); } else { const menuItem = document.createElement('div'); menuItem.classList.add('sf-menu-item'); menuItem.textContent = item.text; menuItem.addEventListener('click', (e) => { e.stopPropagation(); if (item.engine) { searchWithEngine(item.engine); } else if (item.action) { item.action(); } document.body.removeChild(menu); }); menu.appendChild(menuItem); } }); document.body.appendChild(menu); const closeMenu = (e) => { if (!menu.contains(e.target)) { document.body.removeChild(menu); document.removeEventListener('click', closeMenu); } }; setTimeout(() => document.addEventListener('click', closeMenu), 100); }); } function searchWithEngine(engine) { const searchInput = document.querySelector('input[data-patched="true"]'); if (!searchInput) return; const searchText = searchInput.value.trim(); if (!searchText) { showToast('请输入搜索内容'); return; } const oldEngine = CONFIG.searchEngine; CONFIG.searchEngine = engine; performSearch(searchText); CONFIG.searchEngine = oldEngine; updatePlaceholder(searchInput); } function clearHistoryForMenu() { if (searchHistory.length > 0) { if (confirm('确定清空搜索历史吗?')) { searchHistory = []; GM_setValue('searchHistory', '[]'); showToast('搜索历史已清空'); } } else { showToast('搜索历史为空'); } } /* ===== 替换系统通知的页面内 Toast ===== */ function showToast(message) { // 移除了 GM_notification,统一使用页面内轻提示 const toast = document.createElement('div'); toast.className = 'sf-toast'; toast.textContent = message; document.body.appendChild(toast); // 触发过渡(通过 requestAnimationFrame 添加显示类) requestAnimationFrame(() => { toast.classList.add('sf-toast-show'); }); setTimeout(() => { toast.classList.remove('sf-toast-show'); toast.addEventListener('transitionend', () => toast.remove()); }, 2500); } /* ===== 菜单命令 ===== */ function addSettingsMenu() { if (typeof GM_registerMenuCommand !== 'function') return; GM_registerMenuCommand('切换搜索引擎', () => { const current = {bing:'必应',baidu:'百度',google:'谷歌'}[CONFIG.searchEngine] || '必应'; const newEngine = prompt(`当前:${current}\n请输入新引擎:bing / baidu / google`, CONFIG.searchEngine); if (newEngine && ['bing','baidu','google'].includes(newEngine.toLowerCase())) { CONFIG.searchEngine = newEngine.toLowerCase(); document.querySelectorAll('input[data-patched="true"]').forEach(updatePlaceholder); const name = {bing:'必应',baidu:'百度',google:'谷歌'}[CONFIG.searchEngine]; showToast(`已切换为${name}`); } }); GM_registerMenuCommand('查看搜索历史', () => { if (searchHistory.length === 0) { alert('暂无搜索历史'); } else { alert(`搜索历史 (${searchHistory.length}条):\n\n` + searchHistory.map((item,i) => `${i+1}. ${item}`).join('\n')); } }); GM_registerMenuCommand('清空搜索历史', () => { if (searchHistory.length > 0) { if (confirm('确定清空搜索历史吗?')) { searchHistory = []; GM_setValue('searchHistory', '[]'); showToast('搜索历史已清空'); } } else { showToast('搜索历史为空'); } }); GM_registerMenuCommand('切换调试模式', () => { CONFIG.debug = !CONFIG.debug; showToast(`调试模式${CONFIG.debug?'开启':'关闭'}`); }); } /* ===== 对 SPA 路由变化的监听 ===== */ function hookHistoryMethods() { const _pushState = history.pushState; const _replaceState = history.replaceState; function onUrlChange() { setTimeout(findAndPatchSearchBox, 300); } history.pushState = function() { _pushState.apply(this, arguments); onUrlChange(); }; history.replaceState = function() { _replaceState.apply(this, arguments); onUrlChange(); }; window.addEventListener('popstate', onUrlChange); } /* ===== DOM 变化监听 ===== */ function observeDOMChanges() { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { if (node.tagName === 'INPUT' && isValidSearchBox(node)) { setTimeout(() => patchSearchBox(node), 50); } else if (node.querySelectorAll) { node.querySelectorAll('input').forEach(input => { if (isValidSearchBox(input)) { setTimeout(() => patchSearchBox(input), 50); } }); } } }); } } }); observer.observe(document.body, { childList: true, subtree: true }); log('DOM变化监听已启用'); } /* ===== 样式表 ===== */ function addStyles() { if (document.getElementById('sf-styles')) return; const style = document.createElement('style'); style.id = 'sf-styles'; style.textContent = ` /* 搜索框补丁外观 */ input[data-patched="true"] { background-color: #1e1e1e !important; color: #ddd !important; border-color: #3a8cff !important; box-shadow: 0 0 0 1px rgba(58,140,255,0.3) !important; transition: box-shadow 0.2s, border-color 0.2s !important; } input[data-patched="true"]:focus { outline: none !important; border-color: #1a73e8 !important; box-shadow: 0 0 0 2px rgba(26,115,232,0.4) !important; } input[data-patched="true"]::placeholder { color: #999 !important; } /* 历史下拉面板 */ .sf-dropdown { background: #222; border: 1px solid #3a3a3a; border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.5); overflow: hidden; font-family: system-ui, -apple-system, sans-serif; transform-origin: top center; animation: sfFadeIn 0.15s ease; } @keyframes sfFadeIn { from { opacity: 0; transform: scaleY(0.95); } to { opacity: 1; transform: scaleY(1); } } .sf-dropdown-empty { padding: 20px 12px; text-align: center; color: #777; font-size: 13px; } .sf-dropdown-header { padding: 8px 12px; background: #2a2a2a; font-size: 12px; font-weight: 600; color: #aaa; border-bottom: 1px solid #333; letter-spacing: 0.5px; } .sf-dropdown-list { max-height: 200px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #444 #222; } .sf-dropdown-list::-webkit-scrollbar { width: 5px; } .sf-dropdown-list::-webkit-scrollbar-track { background: #222; } .sf-dropdown-list::-webkit-scrollbar-thumb { background: #555; border-radius: 4px; } .sf-dropdown-item { padding: 10px 12px; cursor: pointer; color: #ccc; font-size: 13px; border-bottom: 1px solid #2f2f2f; transition: background 0.1s; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .sf-dropdown-item:last-child { border-bottom: none; } .sf-dropdown-item:hover { background: #333; color: #fff; } .sf-dropdown-footer { padding: 10px 12px; text-align: center; color: #ff5c5c; font-size: 13px; font-weight: 500; cursor: pointer; border-top: 1px solid #333; transition: background 0.15s; } .sf-dropdown-footer:hover { background: #2c1e1e; } /* 右键菜单 */ .sf-context-menu { background: #222; border: 1px solid #3a3a3a; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.6); min-width: 160px; z-index: 10000; padding: 6px 0; font-family: system-ui, -apple-system, sans-serif; animation: sfFadeIn 0.12s ease; } .sf-menu-item { padding: 10px 16px; cursor: pointer; color: #ddd; font-size: 13px; transition: background 0.1s; } .sf-menu-item:hover { background: #333; } .sf-menu-separator { height: 1px; background: #3a3a3a; margin: 4px 8px; } /* Toast 轻提示 */ .sf-toast { position: fixed; top: 24px; left: 50%; transform: translateX(-50%) translateY(-20px); background: rgba(30,30,30,0.95); color: #f0f0f0; padding: 10px 24px; border-radius: 8px; font-size: 14px; z-index: 10001; white-space: nowrap; opacity: 0; transition: opacity 0.2s, transform 0.2s; backdrop-filter: blur(6px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); pointer-events: none; } .sf-toast.sf-toast-show { opacity: 1; transform: translateX(-50%) translateY(0); } `; document.head.appendChild(style); } function log(message) { if (CONFIG.debug) { console.log(`[论坛搜索修复] ${message}`); } } // 启动脚本 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();