// ==UserScript== // @name 聚合搜索引擎切换导航(移动端优化)(自用) // @namespace http://tampermonkey.net/ // @version v1.38 // @author 晚风知我意 // @match *://*/*searchstring=* // @match *://*/*searchquery=* // @match *://*/*searchword=* // @match *://*/*searchterm=* // @match *://*/*searchtext=* // @match *://*/*searchkey=* // @match *://*/*keywords=* // @match *://*/*searchfor=* // @match *://*/*findword=* // @match *://*/*findtext=* // @match *://*/*findkey=* // @match *://*/*keyword=* // @match *://*/*question=* // @match *://*/*subject=* // @match *://*/*lookfor=* // @match *://*/*lookup=* // @match *://*/*request=* // @match *://*/*pattern=* // @match *://*/*search=* // @match *://*/*string=* // @match *://*/*phrase=* // @match *://*/*query=* // @match *://*/*terms=* // @match *://*/*value=* // @match *://*/*title=* // @match *://*/*topic=* // @match *://*/*seek=* // @match *://*/*word=* // @match *://*/*text=* // @match *://*/*find=* // @match *://*/*ask=* // @match *://*/*name=* // @match *://*/*web=* // @match *://*/*key=* // @match *://*/*wd=* // @match *://*/*kw=* // @match *://*/*q=* // @match *://*/*p=* // @match *://*/*s=* // @grant unsafeWindow // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @icon https://hub.gitmirror.com/https://raw.githubusercontent.com/qq5855144/greasyfork/main/shousuo.svg // @run-at document-body // @license MIT // @description * 搜索引擎快捷工具 * 核心功能:页面底部搜索引擎快捷栏、拖拽排序、自定义引擎管理、快捷搜索 、增加底部搜索引擎栏偏移设置(确保任何浏览器内搜索引擎导航栏都能够聚焦在输入法键盘上方) // ==/UserScript== const punkDeafultMark = "Bing-Google-Baidu-MetaSo-YandexSearch-Bilibili-ApkPure-Quark-Zhihu"; const defaultSearchEngines = [{ name: "谷歌", searchUrl: "https://www.google.com/search?q={keyword}", searchkeyName: ["q"], matchUrl: /google\.com.*?search.*?q=/g, mark: "Google", svgCode: ` ` }, { name: "必应", searchUrl: "https://www.bing.com/search?q={keyword}", searchkeyName: ["q"], matchUrl: /bing\.com.*?search\?q=?/g, mark: "Bing", svgCode: ` ` }, { name: "百度", searchUrl: "https://www.baidu.com/s?wd={keyword}", searchkeyName: ["wd", "word"], matchUrl: /baidu\.com.*?w(or)?d=?/g, mark: "Baidu", svgCode: ` ` }, { name: "密塔", searchUrl: "https://metaso.cn/?s=itab1&q={keyword}", searchkeyName: ["q"], matchUrl: /metaso\.cn.*?q=/g, mark: "MetaSo", svgCode: ` ` }, { name: "Yandex", searchUrl: "https://yandex.com/search/?text={keyword}", searchkeyName: ["text"], matchUrl: /yandex\.com.*?text=/g, mark: "YandexSearch", svgCode: ` ` }, { name: "ApkPure", searchUrl: "https://apkpure.com/search?q={keyword}", searchkeyName: ["q"], matchUrl: /apkpure\.com.*?q=?/g, mark: "ApkPure", svgCode: ` ` }, { name: "哔哩哔哩", searchUrl: "https://m.bilibili.com/search?keyword={keyword}", searchkeyName: ["keyword"], matchUrl: /bilibili\.com.*?keyword=/g, mark: "Bilibili", svgCode: ` ` }, { name: "夸克", searchUrl: "https://quark.sm.cn/s?q={keyword}", searchkeyName: ["q"], matchUrl: /quark\.sm\.cn.*?q=/g, mark: "Quark", svgCode: ` ` }, { name: "扩展搜索", searchUrl: "https://www.crxsoso.com/search?keyword={keyword}&store=chrome", searchkeyName: ["keyword"], matchUrl: /crxsoso\.com\/search\?keyword=/g, mark: "Crxsoso", svgCode: ` ` }, { name: "知乎", searchUrl: "https://www.zhihu.com/search?type=content&q={keyword}", searchkeyName: ["q"], matchUrl: /zhihu\.com.*?q=/g, mark: "Zhihu", svgCode: ` ` }, { name: "GitHub", searchUrl: "https://github.com/search?q={keyword}+is%3Apublic&type=repositories&s=stars&o=desc", searchkeyName: ["q"], matchUrl: /github\.com.*?search\?q=/, mark: "GitHub", svgCode: ` ` }, { name: "YouTube", searchUrl: "https://www.youtube.com/results?search_query={keyword}", searchkeyName: ["search_query"], matchUrl: "youtube\\.com.*?results\\?search_query=", mark: "YouTube", svgCode: ` ` }, { "name": "Baidu图片", "searchUrl": "https://www.baidu.com/sf/vsearch?pd=image_content&from={source}&atn=page&fr=tab&tn=vsearch&ss=100&sa=tb&rsv_sug4={suggestion}&inputT={input_time}&oq={original_query}&word={keyword}", "searchkeyName": ["keyword", "source", "suggestion", "input_time", "original_query"], "matchUrl": /baidu\.com\/sf\/vsearch.*?word=/g, "mark": "Baidutp", "svgCode": ` ` }, { name: "淘宝", searchUrl: "https://s.taobao.com/search?q={keyword}", searchkeyName: ["q"], matchUrl: "taobao\\.com.*?search\\?q=", mark: "TaoBao", svgCode: ` ` }, { name: "PubMed", searchUrl: "https://pubmed.ncbi.nlm.nih.gov/?term={keyword}", searchkeyName: ["term"], matchUrl: "pubmed\\.ncbi\\.nlm\\.nih\\.gov.*?term={keyword}", mark: "PubMed", svgCode: ` ` }, { name: "DuckDuckGo", searchUrl: "https://duckduckgo.com/?q={keyword}", searchkeyName: ["q"], matchUrl: "duckduckgo\\.com.*?q={keyword}", mark: "DuckDuckGo", svgCode: ` ` }, { name: "矢量图库", searchUrl: "https://www.iconfont.cn/search/index?searchType=icon&q={keyword}", searchkeyName: ["q"], matchUrl: /iconfont\.cn\/search\/index\?searchType=icon&q=/g, mark: "iconfont", svgCode: ` ` }, { name: "搜狗", searchUrl: "https://www.sogou.com/web?query={keyword}", searchkeyName: ["query"], matchUrl: /sogou\.com.*?query=/g, mark: "Sogou", svgCode: ` ` }, { name: "猫脚本", searchUrl: "https://scriptcat.org/zh-CN/search?keyword={keyword}", searchkeyName: ["keyword"], matchUrl: /scriptcat\.org\/zh-CN\/search\?keyword=/g, mark: "ScriptCat", svgCode: ` ` }, { name: "360搜索", searchUrl: "https://www.so.com/s?q={keyword}", searchkeyName: ["q"], matchUrl: /so\.com.*?q=/g, mark: "360Search", svgCode: ` ` }, { name: "Startpage", searchUrl: "https://www.startpage.com/sp/search?query={keyword}", searchkeyName: ["query"], matchUrl: /startpage\.com.*?query=/g, mark: "Startpage", svgCode: ` ` }, { name: "WolframAlpha", searchUrl: "https://www.wolframalpha.com/input?i={keyword}", searchkeyName: ["i"], matchUrl: /wolframalpha\.com.*?i=/g, mark: "WolframAlpha", svgCode: `` }, { name: "谷歌学术", searchUrl: "https://scholar.google.com/scholar?q={keyword}", searchkeyName: ["q"], matchUrl: /scholar\.google\..*?q=/g, mark: "GoogleScholar", svgCode: `` }, { name: "百度学术", searchUrl: "https://xueshu.baidu.com/s?wd={keyword}", searchkeyName: ["wd"], matchUrl: /xueshu\.baidu\.com.*?wd=/g, mark: "BaiduScholar", svgCode: `` }, { name: "CNKI", searchUrl: "https://search.cnki.net/search.aspx?q={keyword}", searchkeyName: ["q"], matchUrl: /cnki\.net.*?q=/g, mark: "CNKI", svgCode: `` }, { name: "StackOverflow", searchUrl: "https://stackoverflow.com/search?q={keyword}", searchkeyName: ["q"], matchUrl: /stackoverflow\.com.*?search\?q=/g, mark: "StackOverflow", svgCode: `` }, { name: "MDN", searchUrl: "https://developer.mozilla.org/zh-CN/search?q={keyword}", searchkeyName: ["q"], matchUrl: /developer\.mozilla\.org.*?q=/g, mark: "MDN", svgCode: ` ` }, { name: "Coursera", searchUrl: "https://www.coursera.org/search?query={keyword}", searchkeyName: ["query"], matchUrl: /coursera\.org.*?query=/g, mark: "Coursera", svgCode: `` }, { name: "京东", searchUrl: "https://search.jd.com/Search?keyword={keyword}", searchkeyName: ["keyword"], matchUrl: /jd\.com.*?keyword=/g, mark: "JD", svgCode: `` }, { name: "亚马逊", searchUrl: "https://www.amazon.com/s?k={keyword}", searchkeyName: ["k"], matchUrl: /amazon\..*?k=/g, mark: "Amazon", svgCode: ` ` }, { name: "AliExpress", searchUrl: "https://www.aliexpress.com/wholesale?SearchText={keyword}", searchkeyName: ["SearchText"], matchUrl: /aliexpress\.com.*?SearchText=/g, mark: "AliExpress", svgCode: `` }, { name: "微博", searchUrl: "https://s.weibo.com/weibo?q={keyword}", searchkeyName: ["q"], matchUrl: /weibo\.com.*?q=/g, mark: "Weibo", svgCode: `` }, { name: "抖音", searchUrl: "https://www.douyin.com/search/{keyword}", searchkeyName: ["keyword"], matchUrl: /douyin\.com.*?search/g, mark: "Douyin", svgCode: `` }, { name: "小红书", searchUrl: "https://www.xiaohongshu.com/search_result?keyword={keyword}", searchkeyName: ["keyword"], matchUrl: /xiaohongshu\.com.*?keyword=/g, mark: "Xiaohongshu", svgCode: `` }, { name: "豆瓣", searchUrl: "https://www.douban.com/search?q={keyword}", searchkeyName: ["q"], matchUrl: /douban\.com.*?q=/g, mark: "Douban", svgCode: `` }, { name: "IMDb", searchUrl: "https://www.imdb.com/find?q={keyword}", searchkeyName: ["q"], matchUrl: /imdb\.com.*?q=/g, mark: "IMDb", svgCode: `` }, { name: "RottenTomatoes", searchUrl: "https://www.rottentomatoes.com/search?search={keyword}", searchkeyName: ["search"], matchUrl: /rottentomatoes\.com.*?search=/g, mark: "RottenTomatoes", svgCode: `` }, { name: "Steam", searchUrl: "https://store.steampowered.com/search/?term={keyword}", searchkeyName: ["term"], matchUrl: /steampowered\.com.*?term=/g, mark: "Steam", svgCode: ` ` }, { name: "Spotify", searchUrl: "https://open.spotify.com/search/{keyword}", searchkeyName: ["q"], matchUrl: /open\.spotify\.com.*?search/g, mark: "Spotify", svgCode: `` }, { name: "网易云音乐", searchUrl: "https://music.163.com/#/search/m/?s={keyword}", searchkeyName: ["s"], matchUrl: /music\.163\.com.*?s=/g, mark: "NeteaseMusic", svgCode: `` }, { name: "Pinterest", searchUrl: "https://www.pinterest.com/search/pins/?q={keyword}", searchkeyName: ["q"], matchUrl: /pinterest\..*?q=/g, mark: "Pinterest", svgCode: `` }, { name: "Flickr", searchUrl: "https://www.flickr.com/search/?text={keyword}", searchkeyName: ["text"], matchUrl: /flickr\.com.*?text=/g, mark: "Flickr", svgCode: `` }, { name: "维基百科", searchUrl: "https://zh.wikipedia.org/w/index.php?search={keyword}", searchkeyName: ["search"], matchUrl: /wikipedia\.org.*?search=/g, mark: "Wikipedia", svgCode: `` }, { name: "ArchWiki", searchUrl: "https://wiki.archlinux.org/index.php?search={keyword}", searchkeyName: ["search"], matchUrl: /archlinux\.org.*?search=/g, mark: "ArchWiki", svgCode: `` }, { name: "微信读书", searchUrl: "https://weread.qq.com/web/search/books?keyword={keyword}", searchkeyName: ["keyword"], matchUrl: /weread\.qq\.com.*?keyword=/g, mark: "WeRead", svgCode: `` }, { name: "天眼查", searchUrl: "https://www.tianyancha.com/search?key={keyword}", searchkeyName: ["key"], matchUrl: /tianyancha\.com.*?key=/g, mark: "Tianyancha", svgCode: `` }, { name: "Ecosia", searchUrl: "https://www.ecosia.org/search?q={keyword}", searchkeyName: ["q"], matchUrl: "ecosia\\.org.*?search\\?q=", mark: "Ecosia", svgCode: ` ` }, ]; // ===== 常量定义区 ===== // 样式类名常量(统一管理,避免硬编码) const CLASS_NAMES = Object.freeze({ ENGINE_CONTAINER: 'engine-container', ENGINE_DISPLAY: 'engine-display', ENGINE_BUTTON: 'engine-button', HAMBURGER_MENU: 'punkjet-hamburger-menu', SEARCH_OVERLAY: 'punkjet-search-overlay', MANAGEMENT_PANEL: 'engine-management-panel', ENGINE_CARD: 'engine-card', DRAGGING: 'dragging', DRAG_OVER: 'drag-over' }); // 存储键名常量(统一管理GM存储键) const STORAGE_KEYS = Object.freeze({ USER_SEARCH_ENGINES: 'userSearchEngines', PUNK_SETUP_SEARCH: 'punk_setup_search', LAST_SUCCESSFUL_KEYWORDS: 'last_successful_keywords', CURRENT_INPUT: 'currentInput', ENGINE_BAR_OFFSET: 'engineBarOffset' // 新增,用于保存用户设置的偏移值 }); // 默认配置(抽离默认值,便于维护) const DEFAULT_CONFIG = { PUNK_DEFAULT_MARK: 'Bing-Google-Baidu-MetaSo-YandexSearch-Bilibili-ApkPure-Quark-Zhihu', SEARCH_PARAMS: ['q', 'query', 'search', 'keyword', 'keywords', 'wd', 'key'], MONITORED_INPUT_SELECTOR: 'input[type="text"], input[type="search"], textarea, input#kw', CHECK_SCOPE_INTERVAL: 1000, SHOW_SEARCH_BOX_DELAY: 10000, SCROLL_TIMEOUT_DURATION: 150, BAIDU_INPUT_DELAY: 500, DRAG_SORT_DELAY: 500, ENGINE_BAR_OFFSET_DEFAULT: 0 // 默认偏移为0 }; // ===== 全局状态管理 ===== const appState = { userSearchEngines: GM_getValue(STORAGE_KEYS.USER_SEARCH_ENGINES, []), searchUrlMap: [...defaultSearchEngines, ...GM_getValue(STORAGE_KEYS.USER_SEARCH_ENGINES, [])], lastScrollTop: 0, punkJetBoxVisible: true, currentInput: sessionStorage.getItem(STORAGE_KEYS.CURRENT_INPUT) || '', scriptLoaded: false, containerAdded: false, hasUnsavedChanges: false, scrollTimeout: null, isScrolling: false, hideTimeout: null, touchStartY: null, hamburgerMenuOpen: false, searchOverlayVisible: false, // 新增:标记是否正在与引擎按钮栏交互 isInteractingWithEngineBar: false }; // ===== 可访问性模块 ===== /** * 可访问性功能模块 - 键盘导航、ARIA标签、焦点管理 */ const accessibility = { /** * 初始化键盘导航支持 */ initKeyboardNavigation() { document.addEventListener('keydown', (e) => { // Alt+S 打开搜索框 if (e.altKey && e.key === 's') { e.preventDefault(); searchOverlay.showSearchOverlay(); } // ESC 关闭各种弹窗 if (e.key === 'Escape') { if (appState.searchOverlayVisible) { searchOverlay.hideSearchOverlay(); } if (appState.hamburgerMenuOpen) { hamburgerMenu.hideHamburgerMenu(); } const panel = document.getElementById(CLASS_NAMES.MANAGEMENT_PANEL); if (panel && panel.style.display === 'block') { managementPanel.closeManagementPanel(); } } // Alt+M 打开菜单 if (e.altKey && e.key === 'm') { e.preventDefault(); hamburgerMenu.toggleHamburgerMenu(); } // Alt+E 打开引擎管理 if (e.altKey && e.key === 'e') { e.preventDefault(); managementPanel.showManagementPanel(); } }); }, /** * 改进ARIA标签 */ enhanceAriaLabels() { const buttons = document.querySelectorAll(`.${CLASS_NAMES.ENGINE_BUTTON}`); buttons.forEach(button => { const engineName = button.getAttribute('title'); button.setAttribute('aria-label', `使用${engineName}搜索`); button.setAttribute('role', 'button'); button.setAttribute('tabindex', '0'); }); // 为汉堡菜单按钮添加ARIA const hamburgerButton = document.querySelector('.engine-hamburger-button'); if (hamburgerButton) { hamburgerButton.setAttribute('aria-label', '打开菜单'); hamburgerButton.setAttribute('aria-expanded', 'false'); hamburgerButton.setAttribute('aria-haspopup', 'true'); } // 为搜索遮罩添加ARIA const overlay = document.getElementById(CLASS_NAMES.SEARCH_OVERLAY); if (overlay) { const searchInput = overlay.querySelector('input'); if (searchInput) { searchInput.setAttribute('aria-label', '搜索关键词或网址'); } } }, /** * 更新汉堡菜单ARIA状态 */ updateHamburgerAriaState() { const hamburgerButton = document.querySelector('.engine-hamburger-button'); if (hamburgerButton) { hamburgerButton.setAttribute('aria-expanded', appState.hamburgerMenuOpen.toString()); } }, /** * 焦点管理 - 陷阱焦点在模态框内 * @param {HTMLElement} element - 模态框元素 */ trapFocus(element) { const focusableElements = element.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); if (focusableElements.length === 0) return; const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; const handleKeyDown = (e) => { if (e.key !== 'Tab') return; if (e.shiftKey) { // Shift + Tab if (document.activeElement === firstElement) { lastElement.focus(); e.preventDefault(); } } else { // Tab if (document.activeElement === lastElement) { firstElement.focus(); e.preventDefault(); } } }; element.addEventListener('keydown', handleKeyDown); // 存储事件处理器以便清理 if (!element._focusTrapHandler) { element._focusTrapHandler = handleKeyDown; } // 初始聚焦到第一个元素 setTimeout(() => firstElement.focus(), 100); }, /** * 移除焦点陷阱 * @param {HTMLElement} element - 模态框元素 */ removeFocusTrap(element) { if (element._focusTrapHandler) { element.removeEventListener('keydown', element._focusTrapHandler); delete element._focusTrapHandler; } }, /** * 初始化可访问性功能 */ init() { this.initKeyboardNavigation(); // 延迟执行ARIA标签增强,等待DOM加载 setTimeout(() => { this.enhanceAriaLabels(); }, 1000); // 监听DOM变化,动态增强ARIA标签 const observer = new MutationObserver(() => { this.enhanceAriaLabels(); }); observer.observe(document.body, { childList: true, subtree: true }); } }; // ===== 防抖工具模块 ===== /** * 防抖工具模块 - 更精细的防抖控制 */ const debounceUtils = { timers: new Map(), /** * 防抖函数 * @param {string} key - 防抖标识键 * @param {Function} fn - 要执行的函数 * @param {number} delay - 延迟时间(ms) * @param {boolean} immediate - 是否立即执行 */ debounce(key, fn, delay = 300, immediate = false) { // 清除现有定时器 if (this.timers.has(key)) { clearTimeout(this.timers.get(key)); } // 立即执行模式 if (immediate && !this.timers.has(key)) { fn(); this.timers.set(key, setTimeout(() => { this.timers.delete(key); }, delay)); } else { // 延迟执行模式 const timer = setTimeout(() => { fn(); this.timers.delete(key); }, delay); this.timers.set(key, timer); } }, /** * 节流函数 * @param {string} key - 节流标识键 * @param {Function} fn - 要执行的函数 * @param {number} limit - 时间限制(ms) */ throttle(key, fn, limit = 300) { if (!this.timers.has(key)) { fn(); this.timers.set(key, setTimeout(() => { this.timers.delete(key); }, limit)); } }, /** * 清除指定防抖定时器 * @param {string} key - 防抖标识键 */ cancel(key) { if (this.timers.has(key)) { clearTimeout(this.timers.get(key)); this.timers.delete(key); } }, /** * 清除所有防抖定时器 */ clearAll() { this.timers.forEach((timer, key) => { clearTimeout(timer); this.timers.delete(key); }); } }; // ===== 工具函数库 ===== /** * 工具函数集合 - 封装通用逻辑,提升复用性 */ const utils = { /** * 清除所有定时器 */ clearAllTimeouts() { if (appState.scrollTimeout) { clearTimeout(appState.scrollTimeout); appState.scrollTimeout = null; } if (appState.hideTimeout) { clearTimeout(appState.hideTimeout); appState.hideTimeout = null; } // 同时清除防抖定时器 debounceUtils.clearAll(); }, /** * 检查引擎容器是否已存在 * @returns {boolean} 存在返回true,否则false */ isEngineContainerExists() { return document.querySelector(`.${CLASS_NAMES.ENGINE_CONTAINER}`) !== null; }, /** * 检查当前页面是否在有效作用域内(匹配搜索引擎页面) * @returns {boolean} 有效返回true,否则false */ isValidScope() { return appState.searchUrlMap.some(item => window.location.href.match(item.matchUrl) !== null ); }, /** * 验证URL是否有效(http/https协议) * @param {string} string - 待验证的URL字符串 * @returns {boolean} 有效返回true,否则false */ isValidUrl(string) { try { const url = new URL(string); return url.protocol === 'http:' || url.protocol === 'https:'; } catch (_) { return false; } }, /** * 获取当前页面的搜索关键词(从URL参数、输入框、存储中优先级获取) * @returns {string} 搜索关键词 */ getKeywords() { try { // 1. 从URL参数中提取关键词 const url = new URL(window.location.href); const searchParams = url.searchParams; let keywords = ''; // 优先从通用参数中提取 for (const param of DEFAULT_CONFIG.SEARCH_PARAMS) { if (searchParams.has(param)) { keywords = searchParams.get(param).trim(); if (keywords) break; } } // 通用参数未提取到,从引擎配置的参数中提取 if (!keywords) { for (const urlItem of appState.searchUrlMap) { if (window.location.href.match(urlItem.matchUrl) !== null) { for (const keyItem of urlItem.searchkeyName) { if (searchParams.has(keyItem)) { keywords = searchParams.get(keyItem).trim(); if (keywords) break; } } if (keywords) break; } } } // 2. 关键词存在时更新存储,不存在时从存储中读取 if (keywords) { localStorage.setItem(STORAGE_KEYS.LAST_SUCCESSFUL_KEYWORDS, keywords); sessionStorage.setItem(STORAGE_KEYS.LAST_SUCCESSFUL_KEYWORDS, keywords); } else { keywords = sessionStorage.getItem(STORAGE_KEYS.LAST_SUCCESSFUL_KEYWORDS) || localStorage.getItem(STORAGE_KEYS.LAST_SUCCESSFUL_KEYWORDS) || ''; } return keywords; } catch (error) { console.error("获取关键词失败:", error.message, "当前URL:", window.location.href); return ""; } }, /** * 获取搜索关键词(整合遮罩层、输入框、存储多渠道) * @returns {string} 最终搜索关键词 */ getSearchKeywords() { let keywords = ""; // 1. 优先从搜索遮罩层输入框获取 if (appState.searchOverlayVisible) { const searchInput = document.getElementById("overlay-search-input"); if (searchInput && searchInput.value.trim()) { return searchInput.value.trim(); } } // 2. 从百度特定输入框获取 const baiduInput = document.querySelector('input#kw, input[name="wd"], input[name="word"]'); if (baiduInput && baiduInput.value.trim()) { keywords = baiduInput.value.trim(); return keywords; } // 3. 从页面所有输入框中获取 const allInputs = document.querySelectorAll(DEFAULT_CONFIG.MONITORED_INPUT_SELECTOR); for (const input of allInputs) { const inputVal = input.value.trim(); if (inputVal) { keywords = inputVal; break; } } // 4. 从工具函数提取的关键词中获取 if (!keywords) { keywords = this.getKeywords().trim(); } // 5. 最后从sessionStorage获取 if (!keywords) { keywords = sessionStorage.getItem(STORAGE_KEYS.CURRENT_INPUT) || ""; } return keywords; }, /** * 更新未保存更改状态(显示指示器、激活保存按钮) */ markUnsavedChanges() { appState.hasUnsavedChanges = true; const indicator = document.getElementById("unsaved-indicator"); const saveBtn = document.getElementById("panel-save-btn"); if (indicator) indicator.style.display = "block"; if (saveBtn) { saveBtn.style.opacity = "1"; saveBtn.style.pointerEvents = "auto"; saveBtn.style.background = "#e67e22"; saveBtn.innerHTML = this.createInlineSVG('save') + ' 保存更改'; // 统一hover事件处理 const handleHover = function(isEnter) { this.style.transform = isEnter ? "translateY(-2px)" : "translateY(0)"; this.style.boxShadow = isEnter ? "0 4px 8px rgba(0,0,0,0.2)" : "none"; }; // 移除旧事件,避免重复绑定 saveBtn.removeEventListener("mouseenter", () => {}); saveBtn.removeEventListener("mouseleave", () => {}); saveBtn.addEventListener("mouseenter", () => handleHover.call(saveBtn, true)); saveBtn.addEventListener("mouseleave", () => handleHover.call(saveBtn, false)); } }, /** * 清除未保存更改状态(隐藏指示器、禁用保存按钮) */ clearUnsavedChanges() { appState.hasUnsavedChanges = false; const indicator = document.getElementById("unsaved-indicator"); const saveBtn = document.getElementById("panel-save-btn"); if (indicator) indicator.style.display = "none"; if (saveBtn) { saveBtn.style.opacity = "0.7"; saveBtn.style.pointerEvents = "none"; saveBtn.style.background = "#95a5a6"; saveBtn.innerHTML = this.createInlineSVG('save') + ' 保存设置'; // 显示保存成功反馈 setTimeout(() => { if (!appState.hasUnsavedChanges) { saveBtn.innerHTML = this.createInlineSVG('check') + ' 已保存'; saveBtn.style.background = "#27ae60"; setTimeout(() => { if (!appState.hasUnsavedChanges) { saveBtn.innerHTML = this.createInlineSVG('save') + ' 保存设置'; saveBtn.style.background = "#95a5a6"; } }, 2000); } }, 100); } }, /** * 更新已选引擎数量显示 */ updateSelectedCount() { const checkboxes = document.querySelectorAll(`#engine-management-list input[type="checkbox"]:checked`); const countElement = document.getElementById("selected-count"); if (countElement) { countElement.innerHTML = this.createInlineSVG('check-circle') + ` 已选择 ${checkboxes.length} 个引擎`; } }, /** * 保存引擎按钮排序(更新到GM存储) */ saveButtonOrder() { const container = document.querySelector(`.${CLASS_NAMES.ENGINE_DISPLAY}`); if (!container) return; const buttons = container.querySelectorAll(`.${CLASS_NAMES.ENGINE_BUTTON}`); const newOrder = Array.from(buttons) .map(btn => btn.getAttribute('data-mark')) .filter(mark => mark !== null) .join('-'); GM_setValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, newOrder); }, /** * 创建内联SVG图标(替代Font Awesome) * @param {string} iconName - 图标名称 * @param {string} color - 图标颜色 * @returns {string} SVG字符串 */ createInlineSVG(iconName, color = 'currentColor') { const icons = { search: ` `, cog: ` `, sog: ``, save: ` `, check: ` `, 'check-circle': ` `, times: ` `, plus: ` `, globe: ` `, undo: ` `, eye: ` `, trash: ` `, list: ` `, magic: ` `, palette: ` `, circle: ` `, 'paper-plane': ` `, 'info-circle': ` ` }; return icons[iconName] || icons['circle']; }, /** * 获取用户设置的底部偏移值 * @returns {number} 偏移值(px) */ getEngineBarOffset() { return GM_getValue(STORAGE_KEYS.ENGINE_BAR_OFFSET, DEFAULT_CONFIG.ENGINE_BAR_OFFSET_DEFAULT); }, /** * 设置底部偏移值 * @param {number} value - 偏移值(px) */ setEngineBarOffset(value) { GM_setValue(STORAGE_KEYS.ENGINE_BAR_OFFSET, parseInt(value)); } }; // ===== DOM操作模块 ===== /** * DOM操作集合 - 封装DOM创建、样式注入、事件绑定等逻辑 */ const domHandler = { /** * 注入核心样式(确保只注入一次) */ injectStyle() { if (document.querySelector(`style#${CLASS_NAMES.ENGINE_CONTAINER}-style`)) return; const cssNode = document.createElement("style"); cssNode.id = `${CLASS_NAMES.ENGINE_CONTAINER}-style`; cssNode.textContent = ` .${CLASS_NAMES.ENGINE_CONTAINER} { display: flex; position: fixed; bottom: 0px; left: 2%; width: 96%; height: 36px; overflow: hidden; justify-content: center; align-items: center; z-index: 1000; background-color: rgba(255, 255, 255, 0); margin-top: 1px; transition: all 0.3s ease; transform: translateY(0); opacity: 1; overflow-y: hidden; overflow-x: visible; } .${CLASS_NAMES.ENGINE_CONTAINER}.hidden { transform: translateY(100%); opacity: 0; } .${CLASS_NAMES.ENGINE_DISPLAY} { display: flex; overflow-x: auto; overflow-y: hidden; white-space: nowrap; height: 100%; gap: 0px; flex-grow: 1; scrollbar-width: none; -ms-overflow-style: none; } .${CLASS_NAMES.ENGINE_DISPLAY}::-webkit-scrollbar { display: none; } .${CLASS_NAMES.ENGINE_BUTTON} { width: 55.5px; height: 32px; padding: 0; border: 1px solid #f0f0f0; border-radius: 8px; background-color: rgba(255, 255, 255, 1); color: transparent; font-size: 14px; cursor: pointer; margin: 2px; background-size: contain; background-repeat: no-repeat; background-position: center; backdrop-filter: blur(5px); box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1), 0px 0px 0px rgba(255, 255, 255, 0.5), 6px 6px 10px rgba(0, 0, 0, 0.1) inset, -6px -6px 10px rgba(255, 255, 255, 0) inset; transition: all 0.3s ease; flex-shrink: 0; overflow: hidden; } .${CLASS_NAMES.ENGINE_BUTTON}:focus { border: 2px dashed #2196F3; background-color: #f0f8ff; } .${CLASS_NAMES.ENGINE_BUTTON}.selected { border: 2px dashed #2196F3; background-color: #f0f8ff; } .${CLASS_NAMES.ENGINE_BUTTON}.${CLASS_NAMES.DRAGGING} { opacity: 0.5; transform: rotate(5deg); } .${CLASS_NAMES.ENGINE_BUTTON}.${CLASS_NAMES.DRAG_OVER} { border: 2px dashed #2196F3; background-color: #f0f8ff; } .${CLASS_NAMES.ENGINE_CARD} { transition: all 0.3s ease; } #${CLASS_NAMES.MANAGEMENT_PANEL} { animation: slideIn 0.3s ease; } #${CLASS_NAMES.HAMBURGER_MENU} { animation: slideInLeft 0.3s ease; } #${CLASS_NAMES.SEARCH_OVERLAY} { animation: fadeIn 0.3s ease; } @keyframes slideIn { from { opacity: 0; transform: translate(-50%, -48%); } to { opacity: 1; transform: translate(-50%, -50%); } } @keyframes slideInLeft { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } `; document.head.appendChild(cssNode); }, /** * 监控页面输入框(实时同步输入内容到sessionStorage) */ monitorInputFields() { const setupInputMonitoring = (input) => { if (input.dataset.monitored) return; input.dataset.monitored = true; const updateCurrentInput = (event) => { // 使用防抖优化输入监控 debounceUtils.debounce('input_monitor', () => { appState.currentInput = event.target.value.trim(); sessionStorage.setItem(STORAGE_KEYS.CURRENT_INPUT, appState.currentInput); }, 500); }; input.addEventListener('input', updateCurrentInput); input.addEventListener('change', updateCurrentInput); }; // 1. 初始化现有输入框监控 document.querySelectorAll(DEFAULT_CONFIG.MONITORED_INPUT_SELECTOR) .forEach(setupInputMonitoring); // 2. 监听动态添加的输入框(MutationObserver) const observer = new MutationObserver(() => { document.querySelectorAll(`${DEFAULT_CONFIG.MONITORED_INPUT_SELECTOR}:not([data-monitored])`) .forEach(setupInputMonitoring); }); observer.observe(document.body, { childList: true, subtree: true }); }, /** * 更新搜索框位置与显示状态(增强版,支持底部偏移) */ updateSearchBoxPosition() { const punkJetBox = document.getElementById("punkjet-search-box"); if (!punkJetBox) return; // 获取用户设置的偏移值 const offsetValue = utils.getEngineBarOffset(); // 判断是否需要应用偏移(输入法激活时) const shouldOffset = document.activeElement && ( document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA' ) && !appState.isInteractingWithEngineBar; // 新增:与引擎栏交互时不偏移 // 应用位置样式 punkJetBox.style.bottom = shouldOffset ? `${offsetValue}px` : '0px'; punkJetBox.style.left = '2%'; punkJetBox.style.width = '96%'; // 显示/隐藏状态切换 punkJetBox.style.transform = appState.punkJetBoxVisible ? "translateY(0)" : "translateY(100%)"; punkJetBox.style.opacity = appState.punkJetBoxVisible ? "1" : "0"; }, /** * 创建搜索引擎按钮 * @param {Object} item - 搜索引擎配置项 * @returns {HTMLButtonElement} 引擎按钮DOM元素 */ createEngineButton(item) { const button = document.createElement('button'); button.className = CLASS_NAMES.ENGINE_BUTTON; button.style.backgroundImage = `url('data:image/svg+xml;utf8,${encodeURIComponent(item.svgCode)}')`; button.setAttribute("url", item.searchUrl); button.setAttribute("title", item.name); button.setAttribute("data-mark", item.mark); button.innerHTML = ''; // 鼠标hover事件 const handleMouseEnter = () => { button.style.backgroundColor = 'rgba(241, 241, 241, 1)'; button.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; }; const handleMouseLeave = () => { button.style.backgroundColor = 'rgba(240, 240, 244, 1)'; button.style.boxShadow = '1px 1px 1px rgba(0, 0, 0, 0.1), 0px 0px 0px rgba(255, 255, 255, 0.5), 6px 6px 10px rgba(0, 0, 0, 0.1) inset, -6px -6px 10px rgba(255, 255, 255, 0) inset'; }; button.addEventListener('mouseover', handleMouseEnter); button.addEventListener('mouseout', handleMouseLeave); // 增强触摸事件处理 button.addEventListener('touchstart', (e) => { // 标记为引擎栏交互,防止输入框失焦 appState.isInteractingWithEngineBar = true; e.stopPropagation(); // 阻止事件冒泡 }, { passive: true }); button.addEventListener('touchend', (e) => { // 短暂延迟后重置状态 setTimeout(() => { appState.isInteractingWithEngineBar = false; }, 150); e.stopPropagation(); }, { passive: true }); // 点击事件(调用搜索逻辑) button.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); // 阻止事件冒泡 const url = button.getAttribute("url"); const keywords = utils.getSearchKeywords(); if (url && keywords) { const finalUrl = url.replace('{keyword}', encodeURIComponent(keywords)); window.open(finalUrl, '_blank'); if (appState.searchOverlayVisible) { searchOverlay.hideSearchOverlay(); } } else { searchOverlay.showSearchOverlay(); } }); return button; }, /** * 创建汉堡菜单按钮 */ createHamburgerButton() { const hamburgerButton = document.createElement('button'); hamburgerButton.className = "engine-hamburger-button"; hamburgerButton.innerHTML = utils.createInlineSVG('paper-plane'); hamburgerButton.title = "菜单 (Alt+M)"; hamburgerButton.style.cssText = ` width: 32px; height: 32px; border: 1px solid #f0f0f0; border-radius: 7px; background-color: rgba(255, 255, 255, 1); box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1), 0px 0px 0px rgba(255, 255, 255, 0.5), 6px 6px 10px rgba(0, 0, 0, 0.1) inset, -6px -6px 10px rgba(255, 255, 255, 0) inset; cursor: pointer; margin: 3px; flex-shrink: 0; display: flex; justify-content: center; align-items: center; font-size: 16px; color: #999999; transition: all 0.3s ease; padding: 0; outline: none; `; // 鼠标hover效果 hamburgerButton.addEventListener('mouseenter', () => { hamburgerButton.style.backgroundColor = 'rgba(241, 241, 241, 1)'; hamburgerButton.style.transform = 'translateY(-2px)'; hamburgerButton.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; }); hamburgerButton.addEventListener('mouseleave', () => { hamburgerButton.style.backgroundColor = 'white'; hamburgerButton.style.transform = 'translateY(0)'; hamburgerButton.style.boxShadow = '1px 1px 1px rgba(0, 0, 0, 0.1), 0px 0px 0px rgba(255, 255, 255, 0.5), 6px 6px 10px rgba(0, 0, 0, 0.1) inset, -6px -6px 10px rgba(255, 255, 255, 0) inset'; }); // 修复焦点问题:阻止默认焦点行为 hamburgerButton.addEventListener('mousedown', (e) => { e.preventDefault(); }); // 点击切换汉堡菜单 hamburgerButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); // 立即移除焦点 hamburgerButton.blur(); appState.hamburgerMenuOpen ? hamburgerMenu.hideHamburgerMenu() : hamburgerMenu.showHamburgerMenu(); }); return hamburgerButton; }, /** * 添加搜索框到页面(核心UI组件) */ addSearchBox() { try { if (utils.isEngineContainerExists()) return; // 1. 创建主容器 const punkJetBox = document.createElement("div"); punkJetBox.id = "punkjet-search-box"; punkJetBox.className = CLASS_NAMES.ENGINE_CONTAINER; punkJetBox.style.cssText = ` display: flex; z-index: 9999; position: fixed; transition: all 0.3s ease; `; this.updateSearchBoxPosition(); // 2. 创建引擎按钮容器(横向滚动) const ulList = document.createElement('div'); ulList.className = CLASS_NAMES.ENGINE_DISPLAY; ulList.style.cssText = ` overflow-x: auto; overflow-y: hidden; display: flex; flex-grow: 1; `; // 3. 添加汉堡菜单按钮 const hamburgerButton = this.createHamburgerButton(); punkJetBox.appendChild(hamburgerButton); // 4. 添加引擎按钮(从配置中读取) const fragment = document.createDocumentFragment(); const showList = GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK).split('-'); showList.forEach(showMark => { const item = appState.searchUrlMap.find(engine => engine.mark === showMark); if (item) { const button = this.createEngineButton(item); fragment.appendChild(button); } }); ulList.appendChild(fragment); punkJetBox.appendChild(ulList); document.body.appendChild(punkJetBox); // 5. 更新状态与绑定事件 appState.containerAdded = true; this.initScrollListener(); // 新增:绑定窗口大小变化和焦点事件 window.addEventListener('resize', () => this.updateSearchBoxPosition()); document.addEventListener('focusin', () => this.updateSearchBoxPosition()); document.addEventListener('focusout', () => this.updateSearchBoxPosition()); // 6. 点击页面其他区域关闭汉堡菜单 document.addEventListener('click', (e) => { if (!e.target.closest(`#${CLASS_NAMES.HAMBURGER_MENU}`) && !e.target.closest('.engine-hamburger-button')) { hamburgerMenu.hideHamburgerMenu(); } }); // 7. 延迟启用拖拽排序 setTimeout(() => this.enableDragAndSort(), DEFAULT_CONFIG.DRAG_SORT_DELAY); } catch (error) { console.error("添加搜索框失败:", error.message); } }, /** * 启用引擎按钮拖拽排序功能 */ enableDragAndSort() { const container = document.querySelector(`.${CLASS_NAMES.ENGINE_DISPLAY}`); if (!container) return; const buttons = container.querySelectorAll(`.${CLASS_NAMES.ENGINE_BUTTON}`); buttons.forEach(button => { button.draggable = true; // 拖拽开始 button.addEventListener('dragstart', (e) => { button.classList.add(CLASS_NAMES.DRAGGING); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', button.getAttribute('url')); }); // 拖拽结束 button.addEventListener('dragend', () => { button.classList.remove(CLASS_NAMES.DRAGGING); utils.saveButtonOrder(); }); // 拖拽经过 button.addEventListener('dragover', (e) => e.preventDefault()); // 拖拽进入 button.addEventListener('dragenter', (e) => { e.preventDefault(); button.classList.add(CLASS_NAMES.DRAG_OVER); }); // 拖拽离开 button.addEventListener('dragleave', () => { button.classList.remove(CLASS_NAMES.DRAG_OVER); }); // 拖拽放下 button.addEventListener('drop', (e) => { e.preventDefault(); button.classList.remove(CLASS_NAMES.DRAG_OVER); const draggingButton = document.querySelector(`.${CLASS_NAMES.DRAGGING}`); if (draggingButton && draggingButton !== button) { const buttonsArray = Array.from(container.querySelectorAll(`.${CLASS_NAMES.ENGINE_BUTTON}`)); const draggedIndex = buttonsArray.indexOf(draggingButton); const targetIndex = buttonsArray.indexOf(button); // 根据索引位置插入 if (draggedIndex < targetIndex) { container.insertBefore(draggingButton, button.nextSibling); } else { container.insertBefore(draggingButton, button); } utils.markUnsavedChanges(); } }); }); }, /** * 初始化滚动/触摸事件监听(控制搜索框显示/隐藏) */ initScrollListener() { const passiveOptions = { passive: true }; // 1. 滚动事件 - 使用防抖优化 const handleScroll = () => { const st = window.pageYOffset || document.documentElement.scrollTop; const isInteractingWithSearchBar = document.querySelector(`.${CLASS_NAMES.ENGINE_CONTAINER}:hover`) !== null; if (isInteractingWithSearchBar) return; utils.clearAllTimeouts(); appState.isScrolling = true; // 使用防抖处理滚动显示/隐藏 debounceUtils.debounce('scroll_hide', () => { // 向下滚动且距离顶部>50px:隐藏搜索框 if (st > appState.lastScrollTop && st > 50) { this.hideSearchBox(); } else { this.showSearchBoxImmediately(); } appState.lastScrollTop = st <= 0 ? 0 : st; }, 50); // 滚动停止后延迟显示搜索框 appState.scrollTimeout = setTimeout(() => { appState.isScrolling = false; this.showSearchBoxDelayed(); }, DEFAULT_CONFIG.SCROLL_TIMEOUT_DURATION); }; // 2. 触摸事件 - 增强版,防止引擎按钮栏触摸导致输入框失焦 const handleTouchStart = (e) => { // 检查是否触摸在引擎按钮栏上 const isTouchingEngineBar = e.target.closest(`.${CLASS_NAMES.ENGINE_CONTAINER}`) !== null; if (isTouchingEngineBar) { // 标记当前正在与引擎栏交互,防止失焦 appState.isInteractingWithEngineBar = true; // 如果是按钮,阻止默认行为避免失焦 if (e.target.closest(`.${CLASS_NAMES.ENGINE_BUTTON}`)) { e.preventDefault(); } } else { appState.isInteractingWithEngineBar = false; } appState.touchStartY = e.touches[0].clientY; }; const handleTouchMove = (e) => { // 如果正在与引擎栏交互,不处理滑动隐藏逻辑 if (appState.isInteractingWithEngineBar) { return; } if (appState.touchStartY === null) return; if (e.target.closest(`.${CLASS_NAMES.ENGINE_CONTAINER}`)) return; const touchY = e.touches[0].clientY; const diff = appState.touchStartY - touchY; // 使用节流处理触摸移动 debounceUtils.throttle('touch_move', () => { // 滑动距离>10px时触发显示/隐藏 if (Math.abs(diff) > 10) { diff > 0 ? this.hideSearchBox() : this.showSearchBoxImmediately(); } }, 100); }; const handleTouchEnd = (e) => { // 如果是引擎栏的触摸结束,短暂延迟后重置状态 if (appState.isInteractingWithEngineBar) { setTimeout(() => { appState.isInteractingWithEngineBar = false; }, 100); } appState.touchStartY = null; this.showSearchBoxDelayed(); }; // 3. 滚轮事件 const handleWheel = (e) => { // 如果滚轮事件发生在引擎栏上,不处理隐藏逻辑 if (e.target.closest(`.${CLASS_NAMES.ENGINE_CONTAINER}`)) { return; } setTimeout(() => { const st = window.pageYOffset || document.documentElement.scrollTop; if (st > appState.lastScrollTop && st > 50) { this.hideSearchBox(); } else { this.showSearchBoxImmediately(); } appState.lastScrollTop = st <= 0 ? 0 : st; this.showSearchBoxDelayed(); }, 10); }; // 4. 绑定事件 window.addEventListener('scroll', handleScroll, passiveOptions); window.addEventListener('wheel', handleWheel, passiveOptions); window.addEventListener('touchstart', handleTouchStart, passiveOptions); window.addEventListener('touchmove', handleTouchMove, passiveOptions); window.addEventListener('touchend', handleTouchEnd, passiveOptions); // 5. 新增:引擎按钮栏触摸事件处理,防止失焦 this.initEngineBarTouchHandling(); // 6. 点击事件:点击其他区域显示搜索框 document.addEventListener('click', (e) => { // 如果点击的是引擎按钮栏,不触发显示逻辑 if (e.target.closest(`.${CLASS_NAMES.ENGINE_CONTAINER}`)) { return; } if (!e.target.closest(`#${CLASS_NAMES.MANAGEMENT_PANEL}`) && !e.target.closest(`.${CLASS_NAMES.ENGINE_CONTAINER}`)) { this.showSearchBoxImmediately(); } }); // 7. 聚焦事件:输入框聚焦时显示搜索框 document.addEventListener('focusin', (e) => { if (e.target.matches('input, textarea')) { this.showSearchBoxImmediately(); } }); // 8. 鼠标进入事件:进入引擎容器时显示搜索框 document.addEventListener('mouseenter', (e) => { if (e.target.closest(`.${CLASS_NAMES.ENGINE_CONTAINER}`) || e.target.closest(`.${CLASS_NAMES.ENGINE_BUTTON}`)) { this.showSearchBoxImmediately(); } }, true); // 9. 阻止引擎容器内滚动事件冒泡 const stopPropagationHandler = (e) => { if (e.target.closest(`.${CLASS_NAMES.ENGINE_CONTAINER}`)) { e.stopPropagation(); } }; document.addEventListener('wheel', stopPropagationHandler, passiveOptions); document.addEventListener('touchmove', stopPropagationHandler, passiveOptions); }, /** * 初始化引擎按钮栏触摸事件处理 */ initEngineBarTouchHandling() { const engineContainer = document.querySelector(`.${CLASS_NAMES.ENGINE_CONTAINER}`); if (!engineContainer) return; // 阻止引擎栏内的触摸事件冒泡,避免影响输入框焦点 const preventPropagation = (e) => { e.stopPropagation(); }; // 为引擎容器和所有按钮添加触摸事件处理 const touchEvents = ['touchstart', 'touchmove', 'touchend', 'touchcancel']; touchEvents.forEach(eventType => { engineContainer.addEventListener(eventType, preventPropagation, { passive: true }); // 为所有引擎按钮也添加相同处理 const buttons = engineContainer.querySelectorAll(`.${CLASS_NAMES.ENGINE_BUTTON}`); buttons.forEach(button => { button.addEventListener(eventType, preventPropagation, { passive: true }); }); }); // 特殊处理按钮的触摸开始事件,避免触发页面滚动 engineContainer.addEventListener('touchstart', (e) => { if (e.target.closest(`.${CLASS_NAMES.ENGINE_BUTTON}`)) { // 标记为引擎栏交互状态 appState.isInteractingWithEngineBar = true; } }, { passive: true }); // 触摸结束后重置状态 engineContainer.addEventListener('touchend', () => { setTimeout(() => { appState.isInteractingWithEngineBar = false; }, 150); }, { passive: true }); }, /** * 立即显示搜索框 */ showSearchBoxImmediately() { utils.clearAllTimeouts(); if (!appState.punkJetBoxVisible) { appState.punkJetBoxVisible = true; this.updateSearchBoxPosition(); } }, /** * 延迟显示搜索框 */ showSearchBoxDelayed() { utils.clearAllTimeouts(); appState.hideTimeout = setTimeout(() => { this.showSearchBoxImmediately(); }, DEFAULT_CONFIG.SHOW_SEARCH_BOX_DELAY); }, /** * 隐藏搜索框 */ hideSearchBox() { if (appState.punkJetBoxVisible) { appState.punkJetBoxVisible = false; this.updateSearchBoxPosition(); } }, /** * 隐藏汉堡菜单 */ hideHamburgerMenu() { hamburgerMenu.hideHamburgerMenu(); }, /** * 显示汉堡菜单 */ showHamburgerMenu() { hamburgerMenu.showHamburgerMenu(); }, /** * 切换汉堡菜单 */ toggleHamburgerMenu() { hamburgerMenu.toggleHamburgerMenu(); } }; // ===== 搜索遮罩层模块 ===== /** * 搜索遮罩层功能模块 - 封装遮罩层创建、显示、隐藏、搜索逻辑 */ const searchOverlay = { /** * 创建搜索遮罩层(确保只创建一次) * @returns {HTMLDivElement} 遮罩层DOM元素 */ createSearchOverlay() { let overlay = document.getElementById(CLASS_NAMES.SEARCH_OVERLAY); if (overlay) return overlay; // 1. 创建遮罩层容器 overlay = document.createElement("div"); overlay.id = CLASS_NAMES.SEARCH_OVERLAY; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.98); z-index: 9998; display: none; flex-direction: column; backdrop-filter: blur(10px); overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; // 2. 创建外层滚动容器 const scrollContainer = document.createElement("div"); scrollContainer.style.cssText = ` width: 100%; height: 100%; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; padding: 10px 0; box-sizing: border-box; `; // 3. 创建搜索内容容器 const searchContainer = document.createElement("div"); searchContainer.style.cssText = ` width: 95%; max-width: 900px; min-height: min-content; background: linear-gradient(145deg, #f8f9fa, #ffffff); border-radius: 20px; padding: 25px 20px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1), 0 2px 10px rgba(0, 0, 0, 0.05); position: relative; border: 1px solid rgba(255, 255, 255, 0.5); margin: 10px auto; box-sizing: border-box; `; // 响应式调整 const updateSearchContainerStyle = () => { const isMobile = window.innerWidth <= 768; if (isMobile) { searchContainer.style.width = '92%'; searchContainer.style.padding = '20px 15px'; searchContainer.style.borderRadius = '16px'; searchContainer.style.margin = '5px auto'; } else { searchContainer.style.width = '95%'; searchContainer.style.padding = '25px 20px'; searchContainer.style.borderRadius = '20px'; searchContainer.style.margin = '10px auto'; } }; // 初始设置 updateSearchContainerStyle(); // 监听窗口大小变化 window.addEventListener('resize', updateSearchContainerStyle); // 4. 创建关闭按钮 const closeBtn = document.createElement("button"); closeBtn.innerHTML = utils.createInlineSVG('times'); closeBtn.setAttribute('aria-label', '关闭搜索'); closeBtn.style.cssText = ` position: absolute; top: 16px; right: 16px; background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); border: none; font-size: 18px; color: #64748b; cursor: pointer; padding: 3px; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.05); border: 1px solid rgba(255, 255, 255, 0.8); z-index: 1; backdrop-filter: blur(10px); `; // 关闭按钮交互效果 closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = 'linear-gradient(135deg, #ff4757 0%, #ff3742 100%)'; closeBtn.style.color = 'white'; closeBtn.style.transform = 'scale(1.1) rotate(90deg)'; closeBtn.style.boxShadow = '0 8px 25px rgba(255, 71, 87, 0.4)'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = 'linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%)'; closeBtn.style.color = '#64748b'; closeBtn.style.transform = 'scale(1) rotate(0deg)'; closeBtn.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.05)'; }); closeBtn.addEventListener('click', () => this.hideSearchOverlay()); // 5. 创建标题 const title = document.createElement("h2"); title.innerHTML = utils.createInlineSVG('search') + ' 快捷搜索 (Alt+S)'; title.style.cssText = ` margin: 0 0 20px 0; color: #2c3e50; text-align: center; font-size: clamp(18px, 4vw, 24px); text-shadow: 1px 1px 2px rgba(0,0,0,0.1); display: flex; align-items: center; justify-content: center; gap: 10px; flex-wrap: wrap; word-break: break-word; `; // 6. 创建搜索输入框 const searchInput = document.createElement("input"); searchInput.type = "text"; searchInput.placeholder = "输入关键词或网址..."; searchInput.id = "overlay-search-input"; searchInput.setAttribute('autocomplete', 'off'); searchInput.setAttribute('autocorrect', 'off'); searchInput.setAttribute('autocapitalize', 'off'); searchInput.setAttribute('spellcheck', 'false'); searchInput.style.cssText = ` width: 100%; padding: 20px 24px; box-sizing: border-box; background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%); border-radius: 16px; font-size: 18px; color: #1e293b; outline: none; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: inset 3px 3px 6px rgba(0, 0, 0, 0.04), inset -3px -3px 6px rgba(255, 255, 255, 0.8), 0 8px 30px rgba(0, 0, 0, 0.08); border: 2px solid transparent; margin-bottom: 28px; -webkit-appearance: none; font-weight: 500; line-height: 1.5; min-height: 64px; `; // 移动端优化 if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) { searchInput.style.fontSize = '16px'; searchInput.style.padding = '20px 22px'; searchInput.style.minHeight = '50px'; } // 输入框交互效果 searchInput.addEventListener('focus', () => { searchInput.style.boxShadow = 'inset 3px 3px 6px rgba(0, 0, 0, 0.06), inset -3px -3px 6px rgba(255, 255, 255, 0.9), 0 12px 40px rgba(99, 102, 241, 0.15)'; searchInput.style.borderColor = 'transparent'; searchInput.style.background = 'linear-gradient(135deg, #ffffff 0%, #fefefe 100%)'; searchInput.style.transform = 'translateY(-2px)'; }); searchInput.addEventListener('blur', () => { searchInput.style.boxShadow = 'inset 3px 3px 6px rgba(0, 0, 0, 0.04), inset -3px -3px 6px rgba(255, 255, 255, 0.8), 0 8px 30px rgba(0, 0, 0, 0.08)'; searchInput.style.borderColor = 'transparent'; searchInput.style.transform = 'translateY(0)'; }); // 7. 创建网址分类导航 const navigationSection = document.createElement("div"); navigationSection.style.cssText = ` margin-top: 10px; `; // 创建分类导航标题 const navTitle = document.createElement("h3"); navTitle.innerHTML = utils.createInlineSVG('globe') + ' 常用网站导航'; navTitle.style.cssText = ` color: #2c3e50; margin-bottom: 15px; font-size: clamp(16px, 3.5vw, 18px); display: flex; align-items: center; justify-content: center; gap: 8px; border-bottom: 2px solid #3498db; padding-bottom: 8px; text-align: center; flex-wrap: wrap; `; navigationSection.appendChild(navTitle); // 创建分类容器 const categoriesContainer = document.createElement("div"); categoriesContainer.style.cssText = ` display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; margin-top: 10px; `; // 响应式网格调整 const updateGridLayout = () => { const width = window.innerWidth; if (width <= 480) { categoriesContainer.style.gridTemplateColumns = '1fr'; categoriesContainer.style.gap = '12px'; } else if (width <= 768) { categoriesContainer.style.gridTemplateColumns = 'repeat(auto-fit, minmax(250px, 1fr))'; categoriesContainer.style.gap = '14px'; } else { categoriesContainer.style.gridTemplateColumns = 'repeat(auto-fit, minmax(280px, 1fr))'; categoriesContainer.style.gap = '16px'; } }; updateGridLayout(); window.addEventListener('resize', updateGridLayout); // 定义网站分类数据 const websiteCategories = [ { title: "🔧 逆向论坛区", sites: [ { name: "MT论坛", url: "https://bbs.binmt.cc" }, { name: "吾爱破解", url: "https://www.52pojie.cn" }, { name: "看雪论坛", url: "https://bbs.pediy.com" }, { name: "飘云阁", url: "https://www.chinapyg.com" }, { name: "卡饭论坛", url: "https://www.kafan.cn" }, { name: "绿盟科技社区", url: "https://www.nsfocus.net" }, { name: "乌云漏洞平台", url: "https://wooyun.x10sec.org" }, { name: "渗透测试论坛", url: "https://www.hetianlab.com" }, { name: "XDA Developers", url: "https://forum.xda-developers.com" }, { name: "Reddit ReverseEngineering", url: "https://www.reddit.com/r/ReverseEngineering" }, { name: "CrackWatch", url: "https://www.reddit.com/r/CrackWatch" } ] }, { title: "💎 软件资源区", sites: [ { name: "GETMODS", url: "https://getmodsapk.com/" }, { name: "APKdone", url: "https://apkdone.com/" }, { name: "LITEAPKS", url: "https://liteapks.com/" }, { name: "APKMODY", url: "https://apkmody.com/" }, { name: "423Down", url: "https://www.423down.com" }, { name: "果核剥壳", url: "https://www.ghxi.com" }, { name: "大眼仔旭", url: "https://www.dayanzai.me" }, { name: "ZD423", url: "https://www.zdfans.com" }, { name: "软件缘", url: "https://www.appcgn.com" }, { name: "小众软件", url: "https://www.appinn.com" }, { name: "Rutor", url: "http://rutor.info" }, { name: "RuTracker", url: "https://rutracker.org" } ] }, { title: "🤖 AI工具", sites: [ { name: "ChatGPT", url: "https://chat.openai.com" }, { name: "deepseek", url: "https://www.deepseek.com/" }, // 加上 { { name: "Claude", url: "https://claude.ai" }, { name: "文心一言", url: "https://yiyan.baidu.com" }, { name: "豆包", url: "https://www.doubao.com/chat/" }, { name: "讯飞星火", url: "https://xinghuo.xfyun.cn" }, { name: "智谱清言", url: "https://chatglm.cn" }, { name: "Midjourney", url: "https://www.midjourney.com" }, { name: "Stable Diffusion", url: "https://stability.ai" }, { name: "Notion AI", url: "https://www.notion.so" } ] }, { title: "🎬 影视区", sites: [ { name: "网飞猫", url: "https://www.ncat21.com/" }, { name: "毒舌电影", url: "https://www.ncat21.com/" }, { name: "诺影导航", url: "https://nuoin.com/" }, { name: "哔哩哔哩", url: "https://www.bilibili.com" }, { name: "YouTube", url: "https://www.youtube.com" }, { name: "Netflix", url: "https://www.netflix.com" }, { name: "低端影视", url: "https://ddys.tv" }, { name: "NT动漫", url: "https://ntdm8.com/" }, { name: "AGE动漫", url: "https://m.agedm.io/#/" }, { name: "樱花动漫", url: "https://www.yhdm.io" }, { name: "樱花动漫2", url: "https://www.295yhw.com/" }, { name: "腾讯视频", url: "https://v.qq.com" }, { name: "爱奇艺", url: "https://www.iqiyi.com" }, { name: "芒果TV", url: "https://www.mgtv.com" }, { name: "1905电影网", url: "https://www.1905.com" } ] }, { title: "🛠️ 工具区", sites: [ { name: "ProcessOn", url: "https://www.processon.com" }, { name: "SmallPDF", url: "https://smallpdf.com" }, { name: "iLovePDF", url: "https://www.ilovepdf.com" }, { name: "TinyPNG", url: "https://tinypng.com" }, { name: "RemoveBG", url: "https://www.remove.bg" }, { name: "Canva", url: "https://www.canva.com" }, { name: "草料二维码", url: "https://cli.im" }, { name: "石墨文档", url: "https://shimo.im" }, { name: "腾讯文档", url: "https://docs.qq.com" }, { name: "讯飞听见", url: "https://www.iflyrec.com" }, { name: "格式工厂在线版", url: "https://www.pcgeshi.com" }, { name: "Figma", url: "https://www.figma.com" }, { name: "Excalidraw", url: "https://excalidraw.com" }, { name: "Photopea", url: "https://www.photopea.com" } ] }, { title: "📚 学习资源", sites: [ { name: "知乎", url: "https://www.zhihu.com" }, { name: "豆瓣", url: "https://www.douban.com" }, { name: "慕课网", url: "https://www.imooc.com" }, { name: "B站学习区", url: "https://www.bilibili.com" }, { name: "Coursera", url: "https://www.coursera.org" }, { name: "网易云课堂", url: "https://study.163.com" }, { name: "腾讯课堂", url: "https://ke.qq.com" }, { name: "可汗学院", url: "https://www.khanacademy.org" }, { name: "中国大学MOOC", url: "https://www.icourse163.org" }, { name: "知乎大学", url: "https://www.zhihu.com/university" }, { name: "豆包文库", url: "https://www.docin.com" }, { name: "Library Genesis", url: "http://libgen.is" }, { name: "Z-Library", url: "https://z-lib.is" }, { name: "Sci-Hub", url: "https://sci-hub.se" } ] }, { title: "🛒 生活购物", sites: [ { name: "淘宝", url: "https://www.taobao.com" }, { name: "京东", url: "https://www.jd.com" }, { name: "拼多多", url: "https://www.pinduoduo.com" }, { name: "美团", url: "https://www.meituan.com" }, { name: "饿了么", url: "https://www.ele.me" }, { name: "苏宁易购", url: "https://www.suning.com" }, { name: "唯品会", url: "https://www.vip.com" }, { name: "闲鱼", url: "https://2.taobao.com" }, { name: "盒马鲜生", url: "https://www.hemaxiansheng.com" }, { name: "每日优鲜", url: "https://www.missfresh.cn" }, { name: "亚马逊", url: "https://www.amazon.cn" }, { name: "当当网", url: "https://www.dangdang.com" }, { name: "考拉海购", url: "https://www.kaola.com" } ] }, { title: "📰 新闻资讯", sites: [ { name: "微博", url: "https://weibo.com" }, { name: "今日头条", url: "https://www.toutiao.com" }, { name: "澎湃新闻", url: "https://www.thepaper.cn" }, { name: "虎嗅", url: "https://www.huxiu.com" }, { name: "36氪", url: "https://www.36kr.com" }, { name: "人民日报网", url: "https://www.people.com.cn" }, { name: "新华网", url: "https://www.xinhuanet.com" }, { name: "央视新闻", url: "https://news.cctv.com" }, { name: "财新网", url: "https://www.caixin.com" }, { name: "第一财经", url: "https://www.yicai.com" }, { name: "界面新闻", url: "https://www.jiemian.com" }, { name: "华尔街见闻", url: "https://wallstreetcn.com" }, { name: "雪球", url: "https://xueqiu.com" } ] }, { title: "🎵 音乐娱乐", sites: [ { name: "网易云音乐", url: "https://music.163.com" }, { name: "QQ音乐", url: "https://y.qq.com" }, { name: "酷狗音乐", url: "https://www.kugou.com" }, { name: "Spotify", url: "https://open.spotify.com" }, { name: "喜马拉雅", url: "https://www.ximalaya.com" }, { name: "酷我音乐", url: "https://www.kuwo.cn" }, { name: "咪咕音乐", url: "https://music.migu.cn" }, { name: "荔枝FM", url: "https://www.lizhi.fm" }, { name: "蜻蜓FM", url: "https://www.qingting.fm" }, { name: "网易云音乐播客", url: "https://music.163.com/podcast" }, { name: "Bandcamp(独立音乐)", url: "https://bandcamp.com" }, { name: "SoundCloud", url: "https://soundcloud.com" }, { name: "Audius", url: "https://audius.co" } ] }, { title: "💻 技术社区", sites: [ { name: "V2EX", url: "https://www.v2ex.com" }, { name: "掘金", url: "https://juejin.cn" }, { name: "SegmentFault", url: "https://segmentfault.com" }, { name: "CSDN", url: "https://www.csdn.net" }, { name: "开源中国", url: "https://www.oschina.net" }, { name: "GitHub", url: "https://github.com" }, { name: "GitLab", url: "https://about.gitlab.com" }, { name: "Stack Overflow", url: "https://stackoverflow.com" }, { name: "华为开发者联盟", url: "https://developer.huawei.com" }, { name: "小米开发者平台", url: "https://dev.mi.com" }, { name: "阿里开发者社区", url: "https://developer.aliyun.com" }, { name: "腾讯云开发者社区", url: "https://cloud.tencent.com/developer" }, { name: "字节跳动技术团队", url: "https://techblog.bytedance.com" } ] }, { title: "🎮 游戏区", sites: [ { name: "Steam", url: "https://store.steampowered.com" }, { name: "Epic Games", url: "https://www.epicgames.com" }, { name: "GOG", url: "https://www.gog.com" }, { name: "3DMGAME", url: "https://www.3dmgame.com" }, { name: "游民星空", url: "https://www.gamersky.com" }, { name: "游侠网", url: "https://www.ali213.net" }, { name: "NGA玩家社区", url: "https://bbs.nga.cn" }, { name: "TapTap", url: "https://www.taptap.cn" }, { name: "好游快爆", url: "https://www.3839.com" }, { name: "itch.io", url: "https://itch.io" }, { name: "GameJolt", url: "https://gamejolt.com" } ] }, { title: "🔐 网络安全", sites: [ { name: "FreeBuf", url: "https://www.freebuf.com" }, { name: "安全客", url: "https://www.anquanke.com" }, { name: "SecWiki", url: "https://www.sec-wiki.com" }, { name: "HackerOne", url: "https://www.hackerone.com" }, { name: "Bugcrowd", url: "https://www.bugcrowd.com" }, { name: "Exploit Database", url: "https://www.exploit-db.com" }, { name: "Metasploit", url: "https://www.metasploit.com" }, { name: "Kali Linux", url: "https://www.kali.org" }, { name: "OWASP", url: "https://owasp.org" }, { name: "SANS Institute", url: "https://www.sans.org" } ] }, { title: "📱 应用下载", sites: [ { name: "Google Play", url: "https://play.google.com" }, { name: "APKPure", url: "https://apkpure.com" }, { name: "APKMirror", url: "https://www.apkmirror.com" }, { name: "F-Droid", url: "https://f-droid.org" }, { name: "Aptoide", url: "https://www.aptoide.com" }, { name: "豌豆荚", url: "https://www.wandoujia.com" }, { name: "应用宝", url: "https://sj.qq.com" }, { name: "小米应用商店", url: "https://app.mi.com" }, { name: "华为应用市场", url: "https://appgallery.huawei.com" }, { name: "酷安", url: "https://www.coolapk.com" } ] }, { title: "🌐 开发者工具", sites: [ { name: "CodePen", url: "https://codepen.io" }, { name: "JSFiddle", url: "https://jsfiddle.net" }, { name: "Replit", url: "https://replit.com" }, { name: "Glitch", url: "https://glitch.com" }, { name: "CodeSandbox", url: "https://codesandbox.io" }, { name: "Postman", url: "https://www.postman.com" }, { name: "Swagger", url: "https://swagger.io" }, { name: "JSON Formatter", url: "https://jsonformatter.org" }, { name: "RegExr", url: "https://regexr.com" }, { name: "DevDocs", url: "https://devdocs.io" } ] }, { title: "🎨 设计资源", sites: [ { name: "Dribbble", url: "https://dribbble.com" }, { name: "Behance", url: "https://www.behance.net" }, { name: "UI中国", url: "https://www.ui.cn" }, { name: "站酷", url: "https://www.zcool.com.cn" }, { name: "花瓣网", url: "https://huaban.com" }, { name: "Pinterest", url: "https://www.pinterest.com" }, { name: "Unsplash", url: "https://unsplash.com" }, { name: "Pexels", url: "https://www.pexels.com" }, { name: "Iconfont", url: "https://www.iconfont.cn" }, { name: "Flaticon", url: "https://www.flaticon.com" } ] }, { title: "📊 数据资源", sites: [ { name: "Kaggle", url: "https://www.kaggle.com" }, { name: "天池大数据", url: "https://tianchi.aliyun.com" }, { name: "和鲸社区", url: "https://www.kesci.com" }, { name: "Data.gov", url: "https://www.data.gov" }, { name: "Google Dataset", url: "https://datasetsearch.research.google.com" }, { name: "UCI数据集", url: "https://archive.ics.uci.edu" }, { name: "国家统计局", url: "https://www.stats.gov.cn" }, { name: "世界银行数据", url: "https://data.worldbank.org" }, { name: "GitHub数据集", url: "https://github.com/awesomedata/awesome-public-datasets" } ] } ]; // 创建每个分类 websiteCategories.forEach(category => { const categoryElement = document.createElement("div"); categoryElement.style.cssText = ` background: rgba(255, 255, 255, 0.9); border-radius: 12px; padding: 16px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); border: 1px solid rgba(0, 0, 0, 0.06); transition: transform 0.2s ease; break-inside: avoid; `; // 分类卡片hover效果 categoryElement.addEventListener('mouseenter', () => { categoryElement.style.transform = 'translateY(-2px)'; categoryElement.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.12)'; }); categoryElement.addEventListener('mouseleave', () => { categoryElement.style.transform = 'translateY(0)'; categoryElement.style.boxShadow = '0 2px 12px rgba(0, 0, 0, 0.08)'; }); const categoryTitle = document.createElement("h4"); categoryTitle.textContent = category.title; categoryTitle.style.cssText = ` margin: 0 0 12px 0; color: #2c3e50; font-size: 14px; font-weight: 600; border-bottom: 1px solid #ecf0f1; padding-bottom: 8px; word-break: break-word; `; const sitesContainer = document.createElement("div"); sitesContainer.style.cssText = ` display: flex; flex-wrap: wrap; gap: 6px; `; // 创建每个网站链接 category.sites.forEach(site => { const siteLink = document.createElement("a"); siteLink.textContent = site.name; siteLink.href = site.url; siteLink.target = "_blank"; siteLink.rel = "noopener noreferrer"; siteLink.style.cssText = ` display: inline-block; padding: 6px 10px; background: linear-gradient(145deg, #f8f9fa, #ffffff); border: 1px solid #e9ecef; border-radius: 6px; text-decoration: none; color: #495057; font-size: 12px; transition: all 0.2s ease; cursor: pointer; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; flex-shrink: 0; `; // 链接hover效果 siteLink.addEventListener('mouseenter', () => { siteLink.style.background = 'linear-gradient(145deg, #3498db, #2980b9)'; siteLink.style.color = 'white'; siteLink.style.transform = 'translateY(-1px)'; siteLink.style.boxShadow = '0 3px 8px rgba(0, 0, 0, 0.15)'; siteLink.style.borderColor = '#2980b9'; }); siteLink.addEventListener('mouseleave', () => { siteLink.style.background = 'linear-gradient(145deg, #f8f9fa, #ffffff)'; siteLink.style.color = '#495057'; siteLink.style.transform = 'translateY(0)'; siteLink.style.boxShadow = '0 1px 3px rgba(0, 0, 0, 0.08)'; siteLink.style.borderColor = '#e9ecef'; }); // 触摸设备优化 siteLink.addEventListener('touchstart', () => { siteLink.style.background = 'linear-gradient(145deg, #3498db, #2980b9)'; siteLink.style.color = 'white'; }, { passive: true }); sitesContainer.appendChild(siteLink); }); categoryElement.appendChild(categoryTitle); categoryElement.appendChild(sitesContainer); categoriesContainer.appendChild(categoryElement); }); navigationSection.appendChild(categoriesContainer); // 8. 组装结构 searchContainer.appendChild(closeBtn); searchContainer.appendChild(title); searchContainer.appendChild(searchInput); searchContainer.appendChild(navigationSection); scrollContainer.appendChild(searchContainer); overlay.appendChild(scrollContainer); // 9. 绑定事件 searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { this.performOverlaySearch(); } }); // 点击遮罩层背景关闭 overlay.addEventListener('click', (e) => { if (e.target === overlay) { this.hideSearchOverlay(); } }); // 阻止内容区域点击事件冒泡 searchContainer.addEventListener('click', (e) => { e.stopPropagation(); }); document.body.appendChild(overlay); return overlay; }, /** * 显示搜索遮罩层 */ showSearchOverlay() { const overlay = this.createSearchOverlay(); const searchInput = document.getElementById("overlay-search-input"); overlay.style.display = 'flex'; appState.searchOverlayVisible = true; // 应用焦点陷阱 accessibility.trapFocus(overlay); // 延迟聚焦输入框(确保动画完成) setTimeout(() => { searchInput.focus(); searchInput.select(); }, 100); // 隐藏汉堡菜单 domHandler.hideHamburgerMenu(); // 阻止body滚动 document.body.style.overflow = 'hidden'; }, /** * 隐藏搜索遮罩层 */ hideSearchOverlay() { const overlay = document.getElementById(CLASS_NAMES.SEARCH_OVERLAY); if (overlay) { overlay.style.display = 'none'; appState.searchOverlayVisible = false; // 移除焦点陷阱 accessibility.removeFocusTrap(overlay); // 恢复body滚动 document.body.style.overflow = ''; } }, /** * 执行遮罩层搜索逻辑(URL直接跳转,关键词用默认引擎搜索) */ performOverlaySearch() { const searchInput = document.getElementById("overlay-search-input"); const query = searchInput.value.trim(); if (!query) { searchInput.focus(); return; } // 1. 是有效URL则直接打开 if (utils.isValidUrl(query)) { window.open(query, '_blank'); this.hideSearchOverlay(); return; } // 2. 是关键词则用默认引擎搜索 const showList = GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK).split('-'); if (showList.length > 0) { const firstEngine = appState.searchUrlMap.find(item => item.mark === showList[0]); if (firstEngine) { const searchUrl = firstEngine.searchUrl.replace('{keyword}', encodeURIComponent(query)); window.open(searchUrl, '_blank'); this.hideSearchOverlay(); } } } }; // ===== 汉堡菜单模块 ===== /** * 汉堡菜单功能模块 - 封装菜单创建、显示、隐藏逻辑 */ const hamburgerMenu = { /** * 创建汉堡菜单(确保只创建一次) */ createHamburgerMenu() { let menu = document.getElementById(CLASS_NAMES.HAMBURGER_MENU); if (menu) return menu; // 1. 创建菜单容器 menu = document.createElement("div"); menu.id = CLASS_NAMES.HAMBURGER_MENU; menu.style.cssText = ` position: fixed; bottom: 50px; left: 20px; background: rgba(255, 255, 255, 0.95); border-radius: 15px; box-shadow: 0 5px 25px rgba(0, 0, 0, 0.15); backdrop-filter: blur(5px); z-index: 10001; display: none; flex-direction: column; padding: 10px; gap: 5px; min-width: 180px; border: 1px solid rgba(255, 255, 255, 0.2); `; // 2. 定义菜单项配置 const menuItems = [ { icon: 'search', text: '快捷搜索 (Alt+S)', action: () => searchOverlay.showSearchOverlay() }, { icon: 'cog', text: '引擎管理 (Alt+E)', action: () => managementPanel.showManagementPanel() }, { icon: 'info-circle', text: '使用说明', action: () => this.showUsageGuide() } ]; // 3. 创建菜单项按钮 menuItems.forEach(item => { const menuItem = document.createElement("button"); menuItem.innerHTML = utils.createInlineSVG(item.icon) + ` ${item.text}`; menuItem.style.cssText = ` display: flex; align-items: center; gap: 10px; padding: 12px 15px; border: none; background: none; border-radius: 8px; cursor: pointer; font-size: 14px; color: #2c3e50; transition: all 0.3s ease; text-align: left; outline: none; `; // 菜单项hover效果 menuItem.addEventListener('mouseenter', () => { menuItem.style.background = 'rgba(52, 152, 219, 0.1)'; }); menuItem.addEventListener('mouseleave', () => { menuItem.style.background = 'none'; }); // 修复焦点问题:点击时移除焦点 menuItem.addEventListener('mousedown', (e) => { e.preventDefault(); // 防止按钮获得焦点 }); menuItem.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); // 立即移除焦点 menuItem.blur(); // 执行菜单项动作 item.action(); // 隐藏菜单 this.hideHamburgerMenu(); }); menu.appendChild(menuItem); }); // 4. 添加底部偏移设置按钮(同样修复焦点问题) const setOffsetButton = document.createElement('button'); setOffsetButton.innerHTML = utils.createInlineSVG('sog') + ' 设置底部偏移'; setOffsetButton.style.cssText = ` display: flex; align-items: center; gap: 10px; padding: 12px 15px; border: none; background: none; border-radius: 8px; cursor: pointer; font-size: 14px; color: #2c3e50; transition: all 0.3s ease; text-align: left; margin-top: 5px; outline: none; `; setOffsetButton.addEventListener('mouseenter', () => { setOffsetButton.style.background = 'rgba(52, 152, 219, 0.1)'; }); setOffsetButton.addEventListener('mouseleave', () => { setOffsetButton.style.background = 'none'; }); // 修复焦点问题 setOffsetButton.addEventListener('mousedown', (e) => { e.preventDefault(); }); setOffsetButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); setOffsetButton.blur(); const currentValue = utils.getEngineBarOffset(); const userValue = prompt(`请输入搜索栏在输入法弹出时的底部偏移(单位px):`, currentValue); if (userValue !== null && !isNaN(userValue)) { utils.setEngineBarOffset(userValue); alert(`偏移值已设置为 ${userValue}px`); domHandler.updateSearchBoxPosition(); } this.hideHamburgerMenu(); }); menu.appendChild(setOffsetButton); document.body.appendChild(menu); return menu; }, /** * 显示汉堡菜单 */ showHamburgerMenu() { const menu = this.createHamburgerMenu(); menu.style.display = 'flex'; appState.hamburgerMenuOpen = true; // 更新ARIA状态 accessibility.updateHamburgerAriaState(); // 应用焦点陷阱 accessibility.trapFocus(menu); }, /** * 隐藏汉堡菜单 */ hideHamburgerMenu() { const menu = document.getElementById(CLASS_NAMES.HAMBURGER_MENU); if (menu) { menu.style.display = 'none'; appState.hamburgerMenuOpen = false; // 更新ARIA状态 accessibility.updateHamburgerAriaState(); // 移除焦点陷阱 accessibility.removeFocusTrap(menu); } }, /** * 切换汉堡菜单显示/隐藏状态 */ toggleHamburgerMenu() { appState.hamburgerMenuOpen ? this.hideHamburgerMenu() : this.showHamburgerMenu(); }, /** * 显示使用说明界面(全屏简洁版) */ showUsageGuide() { this.hideHamburgerMenu(); // 创建使用说明遮罩层 - 全屏 const guideOverlay = document.createElement("div"); guideOverlay.id = "usage-guide-overlay"; guideOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: white; z-index: 10002; display: flex; flex-direction: column; animation: fadeIn 0.3s ease; overflow: hidden; `; // 头部栏 const header = document.createElement("div"); header.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 20px; background: #f8f9fa; border-bottom: 1px solid #e9ecef; flex-shrink: 0; `; const title = document.createElement("h1"); title.innerHTML = utils.createInlineSVG('info-circle', '#3498db') + ' 使用说明'; title.style.cssText = ` margin: 0; color: #2c3e50; font-size: 1.5em; font-weight: 600; display: flex; align-items: center; gap: 12px; `; const closeBtn = document.createElement("button"); closeBtn.innerHTML = utils.createInlineSVG('times'); closeBtn.style.cssText = ` background: none; border: none; font-size: 20px; color: #666; cursor: pointer; padding: 8px; border-radius: 8px; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; `; closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = '#e9ecef'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = 'none'; }); closeBtn.addEventListener('click', () => { guideOverlay.remove(); }); header.appendChild(title); header.appendChild(closeBtn); // 内容区域 - 全屏滚动 const content = document.createElement("div"); content.style.cssText = ` flex: 1; padding: 30px; overflow-y: auto; background: white; `; // 创建网格布局 const gridContainer = document.createElement("div"); gridContainer.style.cssText = ` display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 25px; max-width: 1200px; margin: 0 auto; `; // 快捷键卡片 const shortcutsCard = this.createCard('🎯 快捷键', this.createShortcutsContent()); // 核心功能卡片 const featuresCard = this.createCard('🚀 核心功能', this.createFeaturesContent()); // 使用技巧卡片 const tipsCard = this.createCard('💡 使用技巧', this.createTipsContent()); gridContainer.appendChild(shortcutsCard); gridContainer.appendChild(featuresCard); gridContainer.appendChild(tipsCard); content.appendChild(gridContainer); // 组装结构 guideOverlay.appendChild(header); guideOverlay.appendChild(content); document.body.appendChild(guideOverlay); // 绑定ESC键关闭 const handleKeyDown = (e) => { if (e.key === 'Escape') { guideOverlay.remove(); document.removeEventListener('keydown', handleKeyDown); } }; document.addEventListener('keydown', handleKeyDown); // 应用焦点陷阱 accessibility.trapFocus(guideOverlay); }, /** * 创建卡片容器 */ createCard(titleText, content) { const card = document.createElement("div"); card.style.cssText = ` background: white; border-radius: 12px; padding: 25px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); border: 1px solid #e9ecef; transition: transform 0.2s ease; `; card.addEventListener('mouseenter', () => { card.style.transform = 'translateY(-2px)'; }); card.addEventListener('mouseleave', () => { card.style.transform = 'translateY(0)'; }); const title = document.createElement("h2"); title.textContent = titleText; title.style.cssText = ` margin: 0 0 20px 0; color: #2c3e50; font-size: 1.3em; font-weight: 600; border-bottom: 2px solid #3498db; padding-bottom: 10px; `; card.appendChild(title); card.appendChild(content); return card; }, /** * 创建快捷键内容 */ createShortcutsContent() { const content = document.createElement("div"); content.style.cssText = ` display: flex; flex-direction: column; gap: 12px; `; const shortcuts = [ { keys: 'Alt + S', action: '打开搜索框' }, { keys: 'Alt + E', action: '打开引擎管理' }, { keys: 'Alt + M', action: '打开/关闭菜单' }, { keys: 'ESC', action: '关闭当前弹窗' } ]; shortcuts.forEach(shortcut => { const item = document.createElement("div"); item.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #3498db; `; const keys = document.createElement("span"); keys.textContent = shortcut.keys; keys.style.cssText = ` font-family: 'Courier New', monospace; font-weight: 600; color: #e67e22; font-size: 0.95em; `; const action = document.createElement("span"); action.textContent = shortcut.action; action.style.cssText = ` color: #2c3e50; font-weight: 500; `; item.appendChild(keys); item.appendChild(action); content.appendChild(item); }); return content; }, /** * 创建核心功能内容 */ createFeaturesContent() { const content = document.createElement("div"); content.style.cssText = ` display: flex; flex-direction: column; gap: 12px; `; const features = [ '🔍 底部搜索栏一键切换搜索引擎', '⚙️ 支持自定义添加和管理搜索引擎', '📱 智能隐藏,滚动时自动收起', '⌨️ 偏移设置-调整搜索引擎栏位置', '🔀 拖拽排序个性化布局', '🌐 自动识别页面搜索引擎' ]; features.forEach(feature => { const item = document.createElement("div"); item.style.cssText = ` display: flex; align-items: flex-start; gap: 10px; padding: 10px; background: #f8f9fa; border-radius: 8px; font-size: 0.95em; line-height: 1.5; `; const text = document.createElement("span"); text.textContent = feature; text.style.cssText = ` color: #2c3e50; `; item.appendChild(text); content.appendChild(item); }); return content; }, /** * 创建使用技巧内容 */ createTipsContent() { const content = document.createElement("div"); content.style.cssText = ` display: flex; flex-direction: column; gap: 12px; `; const tips = [ '💡 在搜索框直接输入网址可快速打开网站', '💡 拖动引擎按钮可调整顺序,常用引擎放前面', '💡 设置合适的底部偏移避免输入法遮挡', '💡 使用"自动添加"快速识别当前页面搜索引擎', '💡 搜索栏会在滚动时隐藏,鼠标移到底部显示', '💡 支持触摸屏滑动控制搜索栏显示隐藏' ]; tips.forEach(tip => { const item = document.createElement("div"); item.style.cssText = ` display: flex; align-items: flex-start; gap: 10px; padding: 10px; background: #fff3cd; border-radius: 8px; border-left: 4px solid #ffc107; font-size: 0.95em; line-height: 1.5; `; const text = document.createElement("span"); text.textContent = tip; text.style.cssText = ` color: #856404; `; item.appendChild(text); content.appendChild(item); }); return content; } }; // ===== 管理面板模块 ===== /** * 引擎管理面板模块 - 封装面板创建、引擎管理、配置保存等核心逻辑 */ const managementPanel = { /** * 创建操作按钮(通用按钮组件) * @param {string} html - 按钮内部HTML * @param {string} color - 按钮背景色 * @param {string} title - 按钮提示文本 * @returns {HTMLButtonElement} 操作按钮DOM元素 */ createActionButton(html, color, title) { const button = document.createElement("button"); button.innerHTML = html; button.title = title; button.style.cssText = ` padding: 10px 15px; background-color: ${color}; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; min-width: 120px; transition: all 0.3s ease; display: flex; align-items: center; gap: 5px; justify-content: center; `; // 按钮hover效果 button.addEventListener("mouseenter", () => { button.style.transform = "translateY(-2px)"; button.style.boxShadow = "0 4px 8px rgba(0,0,0,0.2)"; }); button.addEventListener("mouseleave", () => { button.style.transform = "translateY(0)"; button.style.boxShadow = "none"; }); return button; }, /** * 从当前页面提取搜索引擎信息(增强版自动识别) */ extractSearchEngineFromPage() { // 初始化返回结果 const searchInfo = { name: "", searchUrl: "", searchkeyName: [], matchUrl: "", mark: "", found: false }; try { // 方法1: 从搜索表单提取 const formResult = this.extractFromSearchForms(); if (formResult.found) { return { ...searchInfo, ...formResult }; } // 方法2: 从搜索输入框提取 const inputResult = this.extractFromSearchInputs(); if (inputResult.found) { return { ...searchInfo, ...inputResult }; } // 方法3: 从页面元数据提取 const metaResult = this.extractFromMetaTags(); if (metaResult.found) { return { ...searchInfo, ...metaResult }; } // 方法4: 从URL参数分析 const urlResult = this.extractFromURLParameters(); if (urlResult.found) { return { ...searchInfo, ...urlResult }; } // 方法5: 从常见搜索引擎结构识别 const commonResult = this.extractFromCommonPatterns(); if (commonResult.found) { return { ...searchInfo, ...commonResult }; } } catch (error) { console.warn('搜索引擎信息提取失败:', error); } return searchInfo; }, /** * 从搜索表单提取信息 */ extractFromSearchForms() { const searchForms = document.querySelectorAll('form'); const result = { found: false }; for (const form of searchForms) { // 检查表单是否包含搜索特征 const action = form.getAttribute('action') || ''; const method = (form.getAttribute('method') || 'get').toLowerCase(); // 搜索特征检测 const isSearchForm = this.isSearchForm(form, action); if (!isSearchForm) continue; // 提取基础URL const baseUrl = action.startsWith('http') ? action : new URL(action, window.location.origin).href; // 提取关键词参数 const keyParams = this.extractKeyParamsFromForm(form); if (keyParams.length === 0) continue; // 构建搜索URL const searchUrl = this.buildSearchUrl(baseUrl, method, keyParams); // 生成搜索引擎信息 const domain = new URL(baseUrl).hostname; const engineInfo = this.generateEngineInfo(domain, keyParams, searchUrl); return { ...engineInfo, found: true }; } return result; }, /** * 从搜索输入框提取信息 */ extractFromSearchInputs() { // 扩展搜索输入框选择器 const searchInputSelectors = [ 'input[type="search"]', 'input[name*="search"]', 'input[name*="query"]', 'input[name*="q"]', 'input[name*="keyword"]', 'input[name*="key"]', 'input[name*="wd"]', 'input[name*="kw"]', 'input[placeholder*="搜索"]', 'input[placeholder*="search"]', 'input[placeholder*="查询"]', 'input[aria-label*="搜索"]', 'input[aria-label*="search"]' ]; const searchInputs = document.querySelectorAll(searchInputSelectors.join(',')); const result = { found: false }; if (searchInputs.length > 0) { const input = searchInputs[0]; const name = input.getAttribute('name') || 'q'; const domain = window.location.hostname; // 尝试从输入框的form属性获取关联表单 let searchUrl = ''; const form = input.form; if (form && form.action) { const baseUrl = form.action.startsWith('http') ? form.action : new URL(form.action, window.location.origin).href; const method = (form.getAttribute('method') || 'get').toLowerCase(); searchUrl = this.buildSearchUrl(baseUrl, method, [name]); } else { // 默认生成搜索URL searchUrl = `${window.location.origin}/search?${name}={keyword}`; } const engineInfo = this.generateEngineInfo(domain, [name], searchUrl); return { ...engineInfo, found: true }; } return result; }, /** * 从元数据提取搜索引擎信息 */ extractFromMetaTags() { const result = { found: false }; // 检查Open Graph或Twitter Card中的搜索信息 const ogSiteName = document.querySelector('meta[property="og:site_name"]'); const applicationName = document.querySelector('meta[name="application-name"]'); if (ogSiteName || applicationName) { const siteName = (ogSiteName?.getAttribute('content') || applicationName?.getAttribute('content') || '').toLowerCase(); // 检查是否为知名搜索引擎 const knownEngines = ['google', 'bing', 'baidu', 'duckduckgo', 'yahoo', 'yandex']; const isKnownEngine = knownEngines.some(engine => siteName.includes(engine)); if (isKnownEngine) { const domain = window.location.hostname; const keyParams = this.guessKeyParameters(); const searchUrl = `${window.location.origin}/search?${keyParams[0]}={keyword}`; const engineInfo = this.generateEngineInfo(domain, keyParams, searchUrl); return { ...engineInfo, found: true }; } } return result; }, /** * 从URL参数分析搜索引擎 */ extractFromURLParameters() { const result = { found: false }; const urlParams = new URLSearchParams(window.location.search); // 检查URL中是否包含搜索参数 const searchParams = [ 'q', 'query', 'search', 'keyword', 'keywords', 'searchword', 'searchquery', 'searchterm', 'searchtext', 'searchkey', 'key', 'wd', 'kw', 'p', 's', 'string', 'phrase', 'terms', 'ask' ]; for (const param of searchParams) { if (urlParams.has(param)) { const domain = window.location.hostname; const searchUrl = `${window.location.origin}${window.location.pathname}?${param}={keyword}`; const engineInfo = this.generateEngineInfo(domain, [param], searchUrl); return { ...engineInfo, found: true }; } } return result; }, /** * 从常见搜索引擎模式识别 */ extractFromCommonPatterns() { const result = { found: false }; const domain = window.location.hostname; const knownPatterns = { 'google': { key: 'q', path: '/search' }, 'bing': { key: 'q', path: '/search' }, 'baidu': { key: 'wd', path: '/s' }, 'duckduckgo': { key: 'q', path: '/' }, 'yahoo': { key: 'p', path: '/search' }, 'yandex': { key: 'text', path: '/search' }, 'github': { key: 'q', path: '/search' } }; for (const [engine, pattern] of Object.entries(knownPatterns)) { if (domain.includes(engine)) { const searchUrl = `${window.location.origin}${pattern.path}?${pattern.key}={keyword}`; const engineInfo = this.generateEngineInfo(domain, [pattern.key], searchUrl); return { ...engineInfo, found: true }; } } return result; }, /** * 判断表单是否为搜索表单 */ isSearchForm(form, action) { const formHtml = form.outerHTML.toLowerCase(); const actionLower = action.toLowerCase(); // 搜索关键词检测 const searchIndicators = [ 'search', 'query', 'find', 'seek', 'lookup', 'q=' ]; // 检查表单属性 if (searchIndicators.some(indicator => actionLower.includes(indicator) || formHtml.includes(indicator))) { return true; } // 检查输入框 const inputs = form.querySelectorAll('input[type="text"], input[type="search"]'); for (const input of inputs) { const name = (input.getAttribute('name') || '').toLowerCase(); const placeholder = (input.getAttribute('placeholder') || '').toLowerCase(); if (searchIndicators.some(indicator => name.includes(indicator) || placeholder.includes(indicator))) { return true; } } return false; }, /** * 从表单提取关键词参数 */ extractKeyParamsFromForm(form) { const keyParams = []; const inputs = form.querySelectorAll('input[name]'); const searchParamPatterns = [ /^q$/, /^query/, /^search/, /^keyword/, /^key/, /^wd$/, /^kw$/, /^string/, /^phrase/, /^terms/, /^ask/, /^find/, /^seek/ ]; for (const input of inputs) { const name = input.getAttribute('name'); if (!name) continue; // 检查参数名是否符合搜索参数模式 const isSearchParam = searchParamPatterns.some(pattern => pattern.test(name)); if (isSearchParam) { keyParams.push(name); } } // 如果没有找到明确参数,使用第一个输入框的名称 if (keyParams.length === 0 && inputs.length > 0) { const firstName = inputs[0].getAttribute('name'); if (firstName) { keyParams.push(firstName); } } return keyParams; }, /** * 构建搜索URL */ buildSearchUrl(baseUrl, method, keyParams) { if (method === 'post') { return `${baseUrl}?${keyParams[0]}={keyword}`; } else { const separator = baseUrl.includes('?') ? '&' : '?'; return `${baseUrl}${separator}${keyParams[0]}={keyword}`; } }, /** * 生成搜索引擎信息 */ generateEngineInfo(domain, keyParams, searchUrl) { const cleanDomain = domain.replace('www.', ''); const name = cleanDomain.split('.')[0].charAt(0).toUpperCase() + cleanDomain.split('.')[0].slice(1); const mark = cleanDomain.replace(/\./g, '_'); return { name: name, searchUrl: searchUrl, searchkeyName: keyParams, matchUrl: `.*${cleanDomain}.*`, mark: mark }; }, /** * 猜测关键词参数 */ guessKeyParameters() { const commonParams = ['q', 'query', 'search', 'keyword', 'key', 'wd', 'kw']; return commonParams.slice(0, 1); // 返回最常用的参数 }, /** * 从当前页面提取引擎并填充到添加表单 */ extractFromCurrentPage() { const searchInfo = this.extractSearchEngineFromPage(); if (!searchInfo.found) { alert("无法自动识别当前页面的搜索引擎,请手动添加。"); return; } // 显示添加表单并填充数据 this.showAddForm(true); document.getElementById("engine-name").value = searchInfo.name; document.getElementById("engine-mark").value = searchInfo.mark; document.getElementById("engine-url").value = searchInfo.searchUrl; document.getElementById("engine-keys").value = searchInfo.searchkeyName.join(","); // 自动填充图标(从页面favicon提取) const favicon = document.querySelector('link[rel*="icon"]'); if (favicon) { const iconUrl = favicon.href; if (!iconUrl.startsWith('data:')) { document.getElementById("icon-type").value = "image"; document.getElementById("icon-input").value = iconUrl; this.previewIcon(); } } alert(`✅ 已自动识别 ${searchInfo.name} 搜索引擎!请检查并保存。`); }, /** * 显示/隐藏添加引擎表单 */ showAddForm(show) { const formSection = document.getElementById("add-engine-form"); const engineList = document.getElementById("engine-management-list"); const listTitle = formSection?.previousElementSibling; if (!formSection || !engineList || !listTitle) return; if (show) { formSection.style.display = "block"; engineList.style.display = "none"; listTitle.style.display = "none"; // 清空表单 document.getElementById("engine-name").value = ""; document.getElementById("engine-mark").value = ""; document.getElementById("engine-url").value = ""; document.getElementById("engine-keys").value = ""; document.getElementById("icon-input").value = ""; document.getElementById("icon-preview").innerHTML = ""; } else { formSection.style.display = "none"; engineList.style.display = "grid"; listTitle.style.display = "block"; } }, /** * 预览图标(根据图标类型渲染预览效果) */ previewIcon() { const type = document.getElementById("icon-type").value; const value = document.getElementById("icon-input").value.trim(); const preview = document.getElementById("icon-preview"); // 重置预览容器 preview.innerHTML = ""; preview.style.backgroundImage = "none"; preview.style.backgroundColor = "#ecf0f1"; if (!value) return; try { switch (type) { case "svg": // 验证SVG有效性 const parser = new DOMParser(); const svgDoc = parser.parseFromString(value, "image/svg+xml"); if (svgDoc.querySelector("parsererror")) { throw new Error("无效的SVG代码"); } preview.innerHTML = value; break; case "image": preview.style.backgroundImage = `url(${value})`; preview.style.backgroundSize = "contain"; preview.style.backgroundRepeat = "no-repeat"; preview.style.backgroundPosition = "center"; break; case "text": const displayText = value.length > 4 ? value.substring(0, 4) : value; preview.textContent = displayText; preview.style.fontSize = value.length > 4 ? "14px" : "18px"; preview.style.color = "#2c3e50"; preview.style.fontWeight = "bold"; break; case "emoji": preview.textContent = value; preview.style.fontSize = "24px"; break; } } catch (e) { alert(`图标预览失败: ${e.message}`); } }, /** * 保存新添加的搜索引擎 */ saveNewEngine() { // 1. 获取表单数据 const name = document.getElementById("engine-name").value.trim(); const mark = document.getElementById("engine-mark").value.trim(); const url = document.getElementById("engine-url").value.trim(); const keys = document.getElementById("engine-keys").value.split(',').map(k => k.trim()); const iconType = document.getElementById("icon-type").value; const iconValue = document.getElementById("icon-input").value.trim(); // 2. 表单验证 if (!name || !mark || !url || keys.length === 0) { alert("请填写所有必填字段"); return; } if (appState.searchUrlMap.some(engine => engine.mark === mark)) { alert("标识已存在,请使用其他标识"); return; } // 3. 组装引擎配置(处理图标) const newEngine = { name, searchUrl: url, searchkeyName: keys, matchUrl: new RegExp(`.*${new URL(url).hostname}.*`), mark, svgCode: "", custom: true // 标记为自定义引擎 }; // 根据图标类型生成SVG代码 if (iconValue) { switch (iconType) { case "svg": newEngine.svgCode = iconValue; break; case "image": newEngine.svgCode = `
`; break; case "text": newEngine.svgCode = ` ${iconValue} `; break; case "emoji": newEngine.svgCode = ` ${iconValue} `; break; } } // 4. 保存到存储并更新状态 appState.userSearchEngines.push(newEngine); GM_setValue(STORAGE_KEYS.USER_SEARCH_ENGINES, appState.userSearchEngines); // 更新引擎映射表 appState.searchUrlMap = [...defaultSearchEngines, ...appState.userSearchEngines]; // 更新激活引擎列表 const currentSetup = GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK); GM_setValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, `${currentSetup}-${mark}`); // 5. 反馈结果并刷新界面 utils.markUnsavedChanges(); alert("✅ 搜索引擎添加成功!"); this.showAddForm(false); this.refreshEngineList(); }, /** * 恢复默认搜索引擎配置(清除自定义引擎) */ resetToDefault() { if (confirm("⚠️ 确定要恢复默认设置吗?这将删除所有自定义搜索引擎。")) { // 清空自定义引擎存储 appState.userSearchEngines = []; GM_setValue(STORAGE_KEYS.USER_SEARCH_ENGINES, []); // 恢复默认激活引擎列表 GM_setValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK); // 更新引擎映射表 appState.searchUrlMap = [...defaultSearchEngines]; // 反馈结果并刷新界面 utils.markUnsavedChanges(); alert("✅ 已恢复默认设置"); this.refreshEngineList(); } }, /** * 刷新引擎列表(重新渲染管理面板中的引擎卡片) */ refreshEngineList() { const engineList = document.getElementById("engine-management-list"); const activeMarks = GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK).split("-"); if (!engineList) return; engineList.innerHTML = ""; // 清空列表 // 遍历引擎配置,创建卡片 appState.searchUrlMap.forEach((engine) => { const engineCard = document.createElement("div"); engineCard.className = CLASS_NAMES.ENGINE_CARD; engineCard.style.cssText = ` display: flex; align-items: center; padding: 15px; background: white; border: 2px solid ${activeMarks.includes(engine.mark) ? '#27ae60' : '#ecf0f1'}; border-radius: 10px; transition: all 0.3s ease; cursor: grab; min-height: 60px; box-sizing: border-box; `; // 卡片hover效果 engineCard.addEventListener("mouseenter", () => { engineCard.style.boxShadow = "0 4px 12px rgba(0,0,0,0.1)"; engineCard.style.transform = "translateY(-2px)"; }); engineCard.addEventListener("mouseleave", () => { engineCard.style.boxShadow = "none"; engineCard.style.transform = "translateY(0)"; }); // 1. 选择复选框 const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.dataset.mark = engine.mark; checkbox.checked = activeMarks.includes(engine.mark); checkbox.style.cssText = ` margin-right: 15px; transform: scale(1.2); `; // 复选框变更事件 checkbox.addEventListener("change", () => { utils.updateSelectedCount(); utils.markUnsavedChanges(); }); // 2. 图标预览 const iconPreview = document.createElement("div"); iconPreview.style.cssText = ` width: 40px; height: 25px; background-image: url('data:image/svg+xml;utf8,${encodeURIComponent(engine.svgCode)}'); background-size: contain; background-repeat: no-repeat; background-position: center; margin-right: 15px; border: 1px solid #eee; border-radius: 5px; flex-shrink: 0; `; // 3. 引擎信息容器 const infoContainer = document.createElement("div"); infoContainer.style.cssText = ` flex-grow: 1; min-width: 0; `; // 引擎名称 const name = document.createElement("div"); name.textContent = engine.name; name.style.cssText = ` font-weight: bold; color: #2c3e50; margin-bottom: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; `; // 引擎URL const url = document.createElement("div"); url.textContent = engine.searchUrl; url.style.cssText = ` font-size: 0.8em; color: #7f8c8d; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; `; infoContainer.appendChild(name); infoContainer.appendChild(url); // 4. 操作按钮(仅自定义引擎显示删除按钮) const actions = document.createElement("div"); actions.style.cssText = ` display: flex; gap: 5px; flex-shrink: 0; `; if (engine.custom) { const deleteBtn = document.createElement("button"); deleteBtn.innerHTML = utils.createInlineSVG('trash', 'white'); deleteBtn.title = "删除"; deleteBtn.style.cssText = ` padding: 8px 12px; border: none; background: #e74c3c; color: white; border-radius: 5px; cursor: pointer; flex-shrink: 0; display: flex; align-items: center; justify-content: center; `; actions.appendChild(deleteBtn); // 删除按钮点击事件 deleteBtn.addEventListener("click", (e) => { e.stopPropagation(); if (confirm(`确定要删除 ${engine.name} 吗?`)) { // 从自定义引擎列表中移除 appState.userSearchEngines = appState.userSearchEngines.filter(e => e.mark !== engine.mark); GM_setValue(STORAGE_KEYS.USER_SEARCH_ENGINES, appState.userSearchEngines); // 从激活列表中移除 const currentSetup = GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK); const newSetup = currentSetup.split("-").filter(m => m !== engine.mark).join("-"); GM_setValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, newSetup); // 更新引擎映射表 appState.searchUrlMap = [...defaultSearchEngines, ...appState.userSearchEngines]; // 反馈结果并刷新界面 utils.markUnsavedChanges(); this.refreshEngineList(); } }); } // 组装卡片结构 engineCard.appendChild(checkbox); engineCard.appendChild(iconPreview); engineCard.appendChild(infoContainer); engineCard.appendChild(actions); engineList.appendChild(engineCard); }); // 更新已选数量显示 utils.updateSelectedCount(); }, /** * 保存引擎配置(激活状态、排序等) */ saveEngineSettings() { const checkboxes = document.querySelectorAll('#engine-management-list input[type="checkbox"]'); const activeMarks = []; // 收集激活的引擎标识 checkboxes.forEach(checkbox => { if (checkbox.checked) { activeMarks.push(checkbox.dataset.mark); } }); // 验证至少选择一个引擎 if (activeMarks.length === 0) { alert("⚠️ 请至少选择一个搜索引擎"); return; } // 保存到存储 GM_setValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, activeMarks.join("-")); utils.clearUnsavedChanges(); // 延迟关闭面板并重新加载脚本 setTimeout(() => { this.closeManagementPanel(); appInitializer.reloadScript(); }, 1000); }, /** * 关闭管理面板(带未保存提示) */ closeManagementPanel() { const panel = document.getElementById(CLASS_NAMES.MANAGEMENT_PANEL); if (!panel) return; // 有未保存更改时提示 if (appState.hasUnsavedChanges && !confirm("⚠️ 您有未保存的更改,确定要关闭吗?")) { return; } panel.style.display = "none"; appState.hasUnsavedChanges = false; // 移除焦点陷阱 accessibility.removeFocusTrap(panel); }, /** * 创建管理面板DOM结构(核心配置界面) */ createManagementPanel() { let panel = document.getElementById(CLASS_NAMES.MANAGEMENT_PANEL); if (panel) return panel; // 1. 面板主容器 panel = document.createElement("div"); panel.id = CLASS_NAMES.MANAGEMENT_PANEL; panel.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 800px; height: 90vh; max-height: 90vh; background-color: #ffffff; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); padding: 0; z-index: 10000; display: none; overflow: hidden; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; flex-direction: column; box-sizing: border-box; `; // 2. 面板头部 const header = document.createElement("div"); header.style.cssText = ` height: 15vh; min-height: 80px; max-height: 120px; background-color: #2c3e50; color: white; padding: 20px; border-radius: 15px 15px 0 0; position: relative; box-sizing: border-box; flex-shrink: 0; `; const title = document.createElement("h2"); title.innerHTML = utils.createInlineSVG('cog', 'white') + ' 搜索引擎管理中心'; title.style.cssText = ` margin: 0; font-size: 1.5em; font-weight: 300; display: flex; align-items: center; gap: 10px; `; const subtitle = document.createElement("p"); subtitle.textContent = "管理您的搜索快捷方式"; subtitle.style.cssText = ` margin: 5px 0 0 0; opacity: 0.8; font-size: 0.9em; `; // 未保存更改指示器 const unsavedIndicator = document.createElement("div"); unsavedIndicator.id = "unsaved-indicator"; unsavedIndicator.innerHTML = utils.createInlineSVG('circle', '#e74c3c') + ' 有未保存的更改'; unsavedIndicator.style.cssText = ` position: absolute; top: 15px; right: 20px; color: #e74c3c; font-size: 0.8em; display: none; align-items: center; gap: 5px; `; header.appendChild(title); header.appendChild(subtitle); header.appendChild(unsavedIndicator); panel.appendChild(header); // 3. 面板内容区 const content = document.createElement("div"); content.style.cssText = ` height: 65vh; min-height: 300px; position: relative; overflow: hidden; padding: 0; box-sizing: border-box; display: flex; flex-direction: column; flex-shrink: 0; `; // 3.1 快捷操作栏 const quickActions = document.createElement("div"); quickActions.style.cssText = ` padding: 20px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: space-between; background-color: #ffffff; border-bottom: 1px solid #ecf0f1; box-sizing: border-box; flex-shrink: 0; `; // 左侧操作组 const leftActionGroup = document.createElement("div"); leftActionGroup.style.cssText = ` display: flex; gap: 10px; flex-wrap: wrap; `; const extractBtn = this.createActionButton(utils.createInlineSVG('globe') + ' 自动添加', "#3498db", "自动识别当前页面的搜索引擎"); const addBtn = this.createActionButton(utils.createInlineSVG('plus') + ' 手动添加', "#27ae60", "手动添加新的搜索引擎"); leftActionGroup.appendChild(extractBtn); leftActionGroup.appendChild(addBtn); // 右侧操作组 const rightActionGroup = document.createElement("div"); rightActionGroup.style.cssText = ` display: flex; gap: 10px; flex-wrap: wrap; `; const saveBtn = document.createElement("button"); saveBtn.id = "panel-save-btn"; saveBtn.innerHTML = utils.createInlineSVG('save') + ' 保存设置'; saveBtn.title = "保存当前设置"; saveBtn.style.cssText = ` padding: 10px 20px; background: #95a5a6; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 5px; transition: all 0.3s ease; opacity: 0.7; pointer-events: none; min-width: 120px; justify-content: center; `; const resetBtn = this.createActionButton(utils.createInlineSVG('undo') + ' 恢复默认', "#e74c3c", "恢复默认搜索引擎设置"); rightActionGroup.appendChild(saveBtn); rightActionGroup.appendChild(resetBtn); quickActions.appendChild(leftActionGroup); quickActions.appendChild(rightActionGroup); content.appendChild(quickActions); // 3.2 引擎列表区 const listSection = document.createElement("div"); listSection.style.cssText = ` flex: 1; overflow: hidden; padding: 0 20px; box-sizing: border-box; display: flex; flex-direction: column; overflow: auto; `; const listTitle = document.createElement("h3"); listTitle.innerHTML = utils.createInlineSVG('list') + ' 已配置的搜索引擎'; listTitle.style.cssText = ` color: #2c3e50; margin: 15px 0; font-weight: 500; flex-shrink: 0; display: flex; align-items: center; gap: 10px; `; const engineList = document.createElement("div"); engineList.id = "engine-management-list"; engineList.style.cssText = ` flex: 1; overflow-y: auto; overflow-x: hidden; display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); padding-bottom: 10px; box-sizing: border-box; `; listSection.appendChild(listTitle); listSection.appendChild(engineList); // 3.3 添加引擎表单 const formSection = document.createElement("div"); formSection.id = "add-engine-form"; formSection.style.cssText = ` display: none; background-color: #f8f9fa; padding: 20px; border-radius: 10px; margin: 10px 0; box-sizing: border-box; flex-shrink: 0; `; const formTitle = document.createElement("h3"); formTitle.innerHTML = utils.createInlineSVG('magic') + ' 添加新搜索引擎'; formTitle.style.cssText = ` color: #2c3e50; margin-bottom: 15px; display: flex; align-items: center; gap: 10px; `; formSection.appendChild(formTitle); // 表单字段容器 const form = document.createElement("div"); form.style.cssText = ` display: grid; gap: 15px; grid-template-columns: 1fr 1fr; `; // 表单字段配置 const fields = [{ label: "引擎名称", placeholder: "例如: Google", type: "text", id: "engine-name", required: true }, { label: "唯一标识", placeholder: "例如: google", type: "text", id: "engine-mark", required: true }, { label: "搜索URL", placeholder: "使用 {keyword} 作为占位符", type: "text", id: "engine-url", required: true, fullWidth: true }, { label: "关键词参数", placeholder: "例如: q,query,search", type: "text", id: "engine-keys", required: true, fullWidth: true } ]; // 创建表单字段 fields.forEach(field => { const container = document.createElement("div"); if (field.fullWidth) { container.style.gridColumn = "1 / -1"; } const label = document.createElement("label"); label.textContent = field.label; label.style.cssText = ` display: block; margin-bottom: 5px; font-weight: 500; color: #34495e; `; const input = document.createElement("input"); input.type = field.type; input.placeholder = field.placeholder; input.id = field.id; input.required = field.required; input.style.cssText = ` width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; `; container.appendChild(label); container.appendChild(input); form.appendChild(container); }); // 图标设置区域 const iconContainer = document.createElement("div"); iconContainer.style.gridColumn = "1 / -1"; const iconTitle = document.createElement("h4"); iconTitle.innerHTML = utils.createInlineSVG('palette') + ' 图标设置'; iconTitle.style.cssText = ` margin-bottom: 10px; color: #34495e; display: flex; align-items: center; gap: 10px; `; iconContainer.appendChild(iconTitle); // 图标设置网格 const iconGrid = document.createElement("div"); iconGrid.style.cssText = ` display: grid; grid-template-columns: 1fr 2fr 1fr; gap: 10px; align-items: end; `; // 图标类型选择 const typeGroup = document.createElement("div"); const typeLabel = document.createElement("label"); typeLabel.textContent = "图标类型"; typeLabel.style.cssText = ` display: block; margin-bottom: 5px; font-weight: 500; `; typeGroup.appendChild(typeLabel); const iconTypeSelect = document.createElement("select"); iconTypeSelect.id = "icon-type"; iconTypeSelect.style.cssText = ` width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; `; ["svg", "image", "text", "emoji"].forEach(type => { const option = document.createElement("option"); option.value = type; option.textContent = type.charAt(0).toUpperCase() + type.slice(1); iconTypeSelect.appendChild(option); }); typeGroup.appendChild(iconTypeSelect); // 图标内容输入 const inputGroup = document.createElement("div"); const inputLabel = document.createElement("label"); inputLabel.textContent = "图标内容"; inputLabel.style.cssText = ` display: block; margin-bottom: 5px; font-weight: 500; `; inputGroup.appendChild(inputLabel); const iconInput = document.createElement("input"); iconInput.type = "text"; iconInput.id = "icon-input"; iconInput.placeholder = "SVG代码、图片URL、文字或表情符号"; iconInput.style.cssText = ` width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; `; inputGroup.appendChild(iconInput); // 预览按钮 const previewGroup = document.createElement("div"); const previewButton = document.createElement("button"); previewButton.innerHTML = utils.createInlineSVG('eye') + ' 预览图标'; previewButton.style.cssText = ` width: 100%; padding: 10px; background-color: #3498db; color: white; border: none; border-radius: 5px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 5px; `; previewButton.id = "preview-icon"; previewGroup.appendChild(previewButton); // 组装图标设置网格 iconGrid.appendChild(typeGroup); iconGrid.appendChild(inputGroup); iconGrid.appendChild(previewGroup); iconContainer.appendChild(iconGrid); // 图标预览区域 const previewContainer = document.createElement("div"); previewContainer.style.gridColumn = "1 / -1"; previewContainer.style.cssText = ` margin-top: 15px; text-align: center; `; const previewLabel = document.createElement("label"); previewLabel.textContent = "图标预览 (推荐比例 8:5)"; previewLabel.style.cssText = ` display: block; margin-bottom: 10px; font-weight: 500; `; const iconPreview = document.createElement("div"); iconPreview.id = "icon-preview"; iconPreview.style.cssText = ` width: 88px; height: 55px; border: 2px dashed #bdc3c7; border-radius: 8px; margin: 0 auto; display: flex; justify-content: center; align-items: center; overflow: hidden; background: #ecf0f1; `; previewContainer.appendChild(previewLabel); previewContainer.appendChild(iconPreview); iconContainer.appendChild(previewContainer); form.appendChild(iconContainer); // 表单操作按钮 const formActions = document.createElement("div"); formActions.style.cssText = ` grid-column: 1 / -1; display: flex; gap: 10px; margin-top: 20px; `; const saveFormBtn = this.createActionButton(utils.createInlineSVG('save') + ' 保存引擎', "#27ae60", ""); const cancelFormBtn = this.createActionButton(utils.createInlineSVG('times') + ' 取消', "#95a5a6", ""); formActions.appendChild(saveFormBtn); formActions.appendChild(cancelFormBtn); formSection.appendChild(form); formSection.appendChild(formActions); listSection.appendChild(formSection); content.appendChild(listSection); panel.appendChild(content); // 4. 面板底部 const footer = document.createElement("div"); footer.style.cssText = ` height: 20vh; min-height: 60px; max-height: 90px; background-color: #ecf0f1; padding: 15px 20px; border-top: 1px solid #bdc3c7; display: flex; justify-content: space-between; align-items: center; box-sizing: border-box; flex-shrink: 0; border-radius: 0 0 15px 15px; `; const selectedCount = document.createElement("span"); selectedCount.id = "selected-count"; selectedCount.innerHTML = utils.createInlineSVG('check-circle') + ' 已选择 0 个引擎'; selectedCount.style.cssText = ` color: #7f8c8d; font-size: 0.9em; display: flex; align-items: center; gap: 5px; `; const footerActions = document.createElement("div"); footerActions.style.cssText = ` display: flex; gap: 10px; `; const closeBtn = this.createActionButton(utils.createInlineSVG('times') + ' 关闭', "#95a5a6", ""); footerActions.appendChild(closeBtn); footer.appendChild(selectedCount); footer.appendChild(footerActions); panel.appendChild(footer); // 5. 绑定事件 extractBtn.addEventListener("click", () => this.extractFromCurrentPage()); addBtn.addEventListener("click", () => this.showAddForm(true)); resetBtn.addEventListener("click", () => this.resetToDefault()); previewButton.addEventListener("click", () => this.previewIcon()); saveFormBtn.addEventListener("click", () => this.saveNewEngine()); cancelFormBtn.addEventListener("click", () => this.showAddForm(false)); saveBtn.addEventListener("click", () => this.saveEngineSettings()); closeBtn.addEventListener("click", () => this.closeManagementPanel()); // 点击面板背景关闭 panel.addEventListener("click", (e) => { if (e.target === panel) { this.closeManagementPanel(); } }); document.body.appendChild(panel); return panel; }, /** * 显示管理面板 */ showManagementPanel() { const panel = this.createManagementPanel(); // 重置未保存状态 appState.hasUnsavedChanges = false; utils.clearUnsavedChanges(); // 刷新引擎列表 this.refreshEngineList(); // 显示面板 panel.style.display = "block"; // 应用焦点陷阱 accessibility.trapFocus(panel); // 隐藏汉堡菜单 hamburgerMenu.hideHamburgerMenu(); }, /** * 创建管理面板DOM结构(核心配置界面) */ createManagementPanel() { let panel = document.getElementById(CLASS_NAMES.MANAGEMENT_PANEL); if (panel) return panel; // 1. 面板主容器 panel = document.createElement("div"); panel.id = CLASS_NAMES.MANAGEMENT_PANEL; panel.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 800px; height: 90vh; max-height: 90vh; background-color: #ffffff; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); padding: 0; z-index: 10000; display: none; overflow: hidden; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; flex-direction: column; box-sizing: border-box; `; // 2. 面板头部 const header = document.createElement("div"); header.style.cssText = ` height: 15vh; min-height: 80px; max-height: 120px; background-color: #2c3e50; color: white; padding: 20px; border-radius: 15px 15px 0 0; position: relative; box-sizing: border-box; flex-shrink: 0; `; const title = document.createElement("h2"); title.innerHTML = utils.createInlineSVG('cog', 'white') + ' 搜索引擎管理中心'; title.style.cssText = ` margin: 0; font-size: 1.5em; font-weight: 300; display: flex; align-items: center; gap: 10px; `; const subtitle = document.createElement("p"); subtitle.textContent = "管理您的搜索快捷方式"; subtitle.style.cssText = ` margin: 5px 0 0 0; opacity: 0.8; font-size: 0.9em; `; // 未保存更改指示器 const unsavedIndicator = document.createElement("div"); unsavedIndicator.id = "unsaved-indicator"; unsavedIndicator.innerHTML = utils.createInlineSVG('circle', '#e74c3c') + ' 有未保存的更改'; unsavedIndicator.style.cssText = ` position: absolute; top: 15px; right: 20px; color: #e74c3c; font-size: 0.8em; display: none; align-items: center; gap: 5px; `; header.appendChild(title); header.appendChild(subtitle); header.appendChild(unsavedIndicator); panel.appendChild(header); // 3. 面板内容区 const content = document.createElement("div"); content.style.cssText = ` height: 65vh; min-height: 300px; position: relative; overflow: hidden; padding: 0; box-sizing: border-box; display: flex; flex-direction: column; flex-shrink: 0; `; // 3.1 快捷操作栏 const quickActions = document.createElement("div"); quickActions.style.cssText = ` padding: 20px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: space-between; background-color: #ffffff; border-bottom: 1px solid #ecf0f1; box-sizing: border-box; flex-shrink: 0; `; // 左侧操作组 const leftActionGroup = document.createElement("div"); leftActionGroup.style.cssText = ` display: flex; gap: 10px; flex-wrap: wrap; `; const extractBtn = this.createActionButton(utils.createInlineSVG('globe') + ' 自动添加', "#3498db", "自动识别当前页面的搜索引擎"); const addBtn = this.createActionButton(utils.createInlineSVG('plus') + ' 手动添加', "#27ae60", "手动添加新的搜索引擎"); leftActionGroup.appendChild(extractBtn); leftActionGroup.appendChild(addBtn); // 右侧操作组 const rightActionGroup = document.createElement("div"); rightActionGroup.style.cssText = ` display: flex; gap: 10px; flex-wrap: wrap; `; const saveBtn = document.createElement("button"); saveBtn.id = "panel-save-btn"; saveBtn.innerHTML = utils.createInlineSVG('save') + ' 保存设置'; saveBtn.title = "保存当前设置"; saveBtn.style.cssText = ` padding: 10px 20px; background: #95a5a6; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 5px; transition: all 0.3s ease; opacity: 0.7; pointer-events: none; min-width: 120px; justify-content: center; `; const resetBtn = this.createActionButton(utils.createInlineSVG('undo') + ' 恢复默认', "#e74c3c", "恢复默认搜索引擎设置"); rightActionGroup.appendChild(saveBtn); rightActionGroup.appendChild(resetBtn); quickActions.appendChild(leftActionGroup); quickActions.appendChild(rightActionGroup); content.appendChild(quickActions); // 3.2 引擎列表区 const listSection = document.createElement("div"); listSection.style.cssText = ` flex: 1; overflow: hidden; padding: 0 20px; box-sizing: border-box; display: flex; flex-direction: column; overflow: auto; `; const listTitle = document.createElement("h3"); listTitle.innerHTML = utils.createInlineSVG('list') + ' 已配置的搜索引擎'; listTitle.style.cssText = ` color: #2c3e50; margin: 15px 0; font-weight: 500; flex-shrink: 0; display: flex; align-items: center; gap: 10px; `; const engineList = document.createElement("div"); engineList.id = "engine-management-list"; engineList.style.cssText = ` flex: 1; overflow-y: auto; overflow-x: hidden; display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); padding-bottom: 10px; box-sizing: border-box; `; listSection.appendChild(listTitle); listSection.appendChild(engineList); // 3.3 添加引擎表单 const formSection = document.createElement("div"); formSection.id = "add-engine-form"; formSection.style.cssText = ` display: none; background-color: #f8f9fa; padding: 20px; border-radius: 10px; margin: 10px 0; box-sizing: border-box; flex-shrink: 0; `; const formTitle = document.createElement("h3"); formTitle.innerHTML = utils.createInlineSVG('magic') + ' 添加新搜索引擎'; formTitle.style.cssText = ` color: #2c3e50; margin-bottom: 15px; display: flex; align-items: center; gap: 10px; `; formSection.appendChild(formTitle); // 表单字段容器 const form = document.createElement("div"); form.style.cssText = ` display: grid; gap: 15px; grid-template-columns: 1fr 1fr; `; // 表单字段配置 const fields = [{ label: "引擎名称", placeholder: "例如: Google", type: "text", id: "engine-name", required: true }, { label: "唯一标识", placeholder: "例如: google", type: "text", id: "engine-mark", required: true }, { label: "搜索URL", placeholder: "使用 {keyword} 作为占位符", type: "text", id: "engine-url", required: true, fullWidth: true }, { label: "关键词参数", placeholder: "例如: q,query,search", type: "text", id: "engine-keys", required: true, fullWidth: true } ]; // 创建表单字段 fields.forEach(field => { const container = document.createElement("div"); if (field.fullWidth) { container.style.gridColumn = "1 / -1"; } const label = document.createElement("label"); label.textContent = field.label; label.style.cssText = ` display: block; margin-bottom: 5px; font-weight: 500; color: #34495e; `; const input = document.createElement("input"); input.type = field.type; input.placeholder = field.placeholder; input.id = field.id; input.required = field.required; input.style.cssText = ` width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; `; container.appendChild(label); container.appendChild(input); form.appendChild(container); }); // 图标设置区域 const iconContainer = document.createElement("div"); iconContainer.style.gridColumn = "1 / -1"; const iconTitle = document.createElement("h4"); iconTitle.innerHTML = utils.createInlineSVG('palette') + ' 图标设置'; iconTitle.style.cssText = ` margin-bottom: 10px; color: #34495e; display: flex; align-items: center; gap: 10px; `; iconContainer.appendChild(iconTitle); // 图标设置网格 const iconGrid = document.createElement("div"); iconGrid.style.cssText = ` display: grid; grid-template-columns: 1fr 2fr 1fr; gap: 10px; align-items: end; `; // 图标类型选择 const typeGroup = document.createElement("div"); const typeLabel = document.createElement("label"); typeLabel.textContent = "图标类型"; typeLabel.style.cssText = ` display: block; margin-bottom: 5px; font-weight: 500; `; typeGroup.appendChild(typeLabel); const iconTypeSelect = document.createElement("select"); iconTypeSelect.id = "icon-type"; iconTypeSelect.style.cssText = ` width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; `; ["svg", "image", "text", "emoji"].forEach(type => { const option = document.createElement("option"); option.value = type; option.textContent = type.charAt(0).toUpperCase() + type.slice(1); iconTypeSelect.appendChild(option); }); typeGroup.appendChild(iconTypeSelect); // 图标内容输入 const inputGroup = document.createElement("div"); const inputLabel = document.createElement("label"); inputLabel.textContent = "图标内容"; inputLabel.style.cssText = ` display: block; margin-bottom: 5px; font-weight: 500; `; inputGroup.appendChild(inputLabel); const iconInput = document.createElement("input"); iconInput.type = "text"; iconInput.id = "icon-input"; iconInput.placeholder = "SVG代码、图片URL、文字或表情符号"; iconInput.style.cssText = ` width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; `; inputGroup.appendChild(iconInput); // 预览按钮 const previewGroup = document.createElement("div"); const previewButton = document.createElement("button"); previewButton.innerHTML = utils.createInlineSVG('eye') + ' 预览图标'; previewButton.style.cssText = ` width: 100%; padding: 10px; background-color: #3498db; color: white; border: none; border-radius: 5px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 5px; `; previewButton.id = "preview-icon"; previewGroup.appendChild(previewButton); // 组装图标设置网格 iconGrid.appendChild(typeGroup); iconGrid.appendChild(inputGroup); iconGrid.appendChild(previewGroup); iconContainer.appendChild(iconGrid); // 图标预览区域 const previewContainer = document.createElement("div"); previewContainer.style.gridColumn = "1 / -1"; previewContainer.style.cssText = ` margin-top: 15px; text-align: center; `; const previewLabel = document.createElement("label"); previewLabel.textContent = "图标预览 (推荐比例 8:5)"; previewLabel.style.cssText = ` display: block; margin-bottom: 10px; font-weight: 500; `; const iconPreview = document.createElement("div"); iconPreview.id = "icon-preview"; iconPreview.style.cssText = ` width: 88px; height: 55px; border: 2px dashed #bdc3c7; border-radius: 8px; margin: 0 auto; display: flex; justify-content: center; align-items: center; overflow: hidden; background: #ecf0f1; `; previewContainer.appendChild(previewLabel); previewContainer.appendChild(iconPreview); iconContainer.appendChild(previewContainer); form.appendChild(iconContainer); // 表单操作按钮 const formActions = document.createElement("div"); formActions.style.cssText = ` grid-column: 1 / -1; display: flex; gap: 10px; margin-top: 20px; `; const saveFormBtn = this.createActionButton(utils.createInlineSVG('save') + ' 保存引擎', "#27ae60", ""); const cancelFormBtn = this.createActionButton(utils.createInlineSVG('times') + ' 取消', "#95a5a6", ""); formActions.appendChild(saveFormBtn); formActions.appendChild(cancelFormBtn); formSection.appendChild(form); formSection.appendChild(formActions); listSection.appendChild(formSection); content.appendChild(listSection); panel.appendChild(content); // 4. 面板底部 const footer = document.createElement("div"); footer.style.cssText = ` height: 20vh; min-height: 60px; max-height: 90px; background-color: #ecf0f1; padding: 15px 20px; border-top: 1px solid #bdc3c7; display: flex; justify-content: space-between; align-items: center; box-sizing: border-box; flex-shrink: 0; border-radius: 0 0 15px 15px; `; const selectedCount = document.createElement("span"); selectedCount.id = "selected-count"; selectedCount.innerHTML = utils.createInlineSVG('check-circle') + ' 已选择 0 个引擎'; selectedCount.style.cssText = ` color: #7f8c8d; font-size: 0.9em; display: flex; align-items: center; gap: 5px; `; const footerActions = document.createElement("div"); footerActions.style.cssText = ` display: flex; gap: 10px; `; const closeBtn = this.createActionButton(utils.createInlineSVG('times') + ' 关闭', "#95a5a6", ""); footerActions.appendChild(closeBtn); footer.appendChild(selectedCount); footer.appendChild(footerActions); panel.appendChild(footer); // 5. 绑定事件 extractBtn.addEventListener("click", () => this.extractFromCurrentPage()); addBtn.addEventListener("click", () => this.showAddForm(true)); resetBtn.addEventListener("click", () => this.resetToDefault()); previewButton.addEventListener("click", () => this.previewIcon()); saveFormBtn.addEventListener("click", () => this.saveNewEngine()); cancelFormBtn.addEventListener("click", () => this.showAddForm(false)); saveBtn.addEventListener("click", () => this.saveEngineSettings()); closeBtn.addEventListener("click", () => this.closeManagementPanel()); // 点击面板背景关闭 panel.addEventListener("click", (e) => { if (e.target === panel) { this.closeManagementPanel(); } }); document.body.appendChild(panel); return panel; }, /** * 显示管理面板 */ showManagementPanel() { const panel = this.createManagementPanel(); // 重置未保存状态 appState.hasUnsavedChanges = false; utils.clearUnsavedChanges(); // 刷新引擎列表 this.refreshEngineList(); // 显示面板 panel.style.display = "block"; // 应用焦点陷阱 accessibility.trapFocus(panel); // 隐藏汉堡菜单 hamburgerMenu.hideHamburgerMenu(); } }; // ===== 应用初始化模块 ===== /** * 应用初始化模块 - 封装初始化、脚本重载、页面事件监听等入口逻辑 */ const appInitializer = { /** * 重新加载脚本(清理DOM、重置状态、重新初始化) */ reloadScript() { // 1. 清理所有创建的DOM元素 [ "#punkjet-search-box", `#${CLASS_NAMES.HAMBURGER_MENU}`, `#${CLASS_NAMES.SEARCH_OVERLAY}`, `#${CLASS_NAMES.MANAGEMENT_PANEL}` ].forEach(selector => { const element = document.querySelector(selector); if (element) { // 移除焦点陷阱 accessibility.removeFocusTrap(element); element.remove(); } }); // 2. 清除所有定时器和防抖器 utils.clearAllTimeouts(); debounceUtils.clearAll(); // 3. 移除全局事件监听器 const events = ['scroll', 'wheel', 'touchstart', 'touchmove', 'touchend']; events.forEach(event => { window.removeEventListener(event, () => {}); }); // 4. 重置应用状态 appState.scriptLoaded = false; appState.containerAdded = false; appState.hamburgerMenuOpen = false; appState.searchOverlayVisible = false; // 5. 重新初始化 this.init(); }, /** * 百度搜索特殊处理(延迟同步输入框内容) */ handleBaiduSpecialCase() { if (window.location.hostname.includes('baidu')) { setTimeout(() => { const baiduInput = document.querySelector('input#kw'); if (baiduInput && baiduInput.value) { appState.currentInput = baiduInput.value.trim(); sessionStorage.setItem(STORAGE_KEYS.CURRENT_INPUT, appState.currentInput); } }, DEFAULT_CONFIG.BAIDU_INPUT_DELAY); } }, /** * 初始化应用(核心入口函数) */ init() { try { // 前置校验:避免重复初始化或无效作用域初始化 if (appState.containerAdded || appState.scriptLoaded || !utils.isValidScope()) { return; } // 1. 初始化默认存储配置(若未设置过) if (!GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH)) { GM_setValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK); } // 2. 从sessionStorage恢复当前输入内容 appState.currentInput = sessionStorage.getItem(STORAGE_KEYS.CURRENT_INPUT) || ''; // 3. 执行初始化流程 domHandler.monitorInputFields(); // 监控输入框 domHandler.addSearchBox(); // 添加搜索框 domHandler.injectStyle(); // 注入样式 accessibility.init(); // 初始化可访问性功能 this.handleBaiduSpecialCase(); // 百度特殊处理 // 4. 更新初始化状态 appState.scriptLoaded = true; } catch (error) { console.error("应用初始化失败:", error.message); } }, /** * 初始化页面事件监听( visibilitychange、pageshow 等) */ initPageEventListeners() { // 1. 页面可见性变化时重新检查初始化 document.addEventListener("visibilitychange", () => { if (document.visibilityState === 'visible' && !appState.containerAdded) { this.init(); } }); // 2. 页面从缓存恢复时重新检查初始化 document.addEventListener("pageshow", (event) => { if (event.persisted && !appState.containerAdded) { this.init(); } }); // 3. DOM加载完成后初始化 document.addEventListener("DOMContentLoaded", () => { if (utils.isValidScope()) { this.init(); } }); // 4. 定期检查作用域(确保页面动态变化后仍能正常初始化) setInterval(() => { if (utils.isValidScope() && !appState.containerAdded) { this.init(); } else if (!utils.isValidScope() && appState.containerAdded) { this.reloadScript(); } }, DEFAULT_CONFIG.CHECK_SCOPE_INTERVAL); } }; // ===== 应用启动入口 ===== // 初始化页面事件监听并启动应用 appInitializer.initPageEventListeners();