// ==UserScript==
// @name 聚合搜索引擎切换导航(移动端优化)(自用)
// @namespace http://tampermonkey.net/
// @version v1.34
// @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.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, 1);
z-index: 9998;
display: none;
justify-content: center;
align-items: center;
backdrop-filter: blur(5px);
`;
// 2. 创建搜索内容容器
const searchContainer = document.createElement("div");
searchContainer.style.cssText = `
width: 90%;
max-width: 500px;
background: linear-gradient(145deg, #f0f0f0, #ffffff);
border-radius: 25px;
padding: 30px;
box-shadow:
20px 20px 60px rgba(0, 0, 0, 0.1),
-20px -20px 60px rgba(255, 255, 255, 0.8),
inset 1px 1px 2px rgba(255, 255, 255, 0.6),
inset -1px -1px 2px rgba(0, 0, 0, 0.05);
position: relative;
border: 1px solid rgba(255, 255, 255, 0.3);
`;
// 3. 创建关闭按钮
const closeBtn = document.createElement("button");
closeBtn.innerHTML = utils.createInlineSVG('times');
closeBtn.style.cssText = `
position: absolute;
top: 15px;
right: 15px;
background: linear-gradient(145deg, #e8e8e8, #ffffff);
border: none;
font-size: 20px;
color: #666;
cursor: pointer;
padding: 8px;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow:
5px 5px 10px rgba(0, 0, 0, 0.1),
-5px -5px 10px rgba(255, 255, 255, 0.8),
inset 1px 1px 2px rgba(255, 255, 255, 0.6);
border: 1px solid rgba(255, 255, 255, 0.3);
`;
// 关闭按钮hover效果
closeBtn.addEventListener('mouseenter', () => {
closeBtn.style.background = 'linear-gradient(145deg, #ff6b6b, #ff5252)';
closeBtn.style.color = 'white';
closeBtn.style.transform = 'translateY(-2px)';
});
closeBtn.addEventListener('mouseleave', () => {
closeBtn.style.background = 'linear-gradient(145deg, #e8e8e8, #ffffff)';
closeBtn.style.color = '#666';
closeBtn.style.transform = 'translateY(0)';
});
closeBtn.addEventListener('click', () => this.hideSearchOverlay());
// 4. 创建标题
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: 24px;
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
`;
// 5. 创建搜索输入框
const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.placeholder = "输入关键词或网址...";
searchInput.id = "overlay-search-input";
searchInput.style.cssText = `
width: 100%;
padding: 12px 15px;
box-sizing: border-box;
background: linear-gradient(145deg, #f8f9fa, #ffffff);
border-radius: 16px;
font-size: 16px;
color: #2c3e50;
outline: none;
transition: all 0.3s ease;
box-shadow:
inset 4px 4px 8px rgba(0, 0, 0, 0.05),
inset -4px -4px 8px rgba(255, 255, 255, 0.8),
5px 5px 15px rgba(0, 0, 0, 0.1);
height: 48px;
`;
// 输入框focus/blur效果
searchInput.addEventListener('focus', () => {
searchInput.style.boxShadow =
'inset 4px 4px 8px rgba(0, 0, 0, 0.08), inset -4px -4px 8px rgba(255, 255, 255, 0.9), 8px 8px 20px rgba(0, 0, 0, 0.15)';
});
searchInput.addEventListener('blur', () => {
searchInput.style.boxShadow =
'inset 4px 4px 8px rgba(0, 0, 0, 0.05), inset -4px -4px 8px rgba(255, 255, 255, 0.8), 5px 5px 15px rgba(0, 0, 0, 0.1)';
});
// 6. 创建提示文本
const tipText = document.createElement("p");
tipText.innerHTML = utils.createInlineSVG('info-circle') + ' 提示:输入关键词后按回车使用默认搜索引擎搜索,或点击下方搜索引擎按钮选择特定引擎';
tipText.style.cssText = `
margin: 15px 0 0 0;
color: #7f8c8d;
font-size: 12px;
text-align: center;
line-height: 1.4;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
`;
// 7. 组装结构
searchContainer.appendChild(closeBtn);
searchContainer.appendChild(title);
searchContainer.appendChild(searchInput);
searchContainer.appendChild(tipText);
overlay.appendChild(searchContainer);
// 8. 绑定事件
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.performOverlaySearch();
}
});
// 点击遮罩层背景关闭
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
this.hideSearchOverlay();
}
});
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();
},
/**
* 隐藏搜索遮罩层
*/
hideSearchOverlay() {
const overlay = document.getElementById(CLASS_NAMES.SEARCH_OVERLAY);
if (overlay) {
overlay.style.display = 'none';
appState.searchOverlayVisible = false;
// 移除焦点陷阱
accessibility.removeFocusTrap(overlay);
}
},
/**
* 执行遮罩层搜索逻辑(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 = ``;
break;
case "emoji":
newEngine.svgCode = ``;
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();