聚合搜索引擎切换导航[自改]

在搜索结果页顶部或选中文本时显示一个聚合搜索引擎切换导航栏,方便在不同引擎间跳转。专注于移动端优化。

// ==UserScript==
// @name         聚合搜索引擎切换导航[自改]
// @namespace    http://tampermonkey.net/
// @icon         https://s2.loli.net/2025/03/08/OCtScJhM1biHEfB.png
// @version      2025.04.28
// @description  在搜索结果页顶部或选中文本时显示一个聚合搜索引擎切换导航栏,方便在不同引擎间跳转。专注于移动端优化。
// @author       PunkJet,tutrabbit,Gemini
// @match        *://*/*
// @grant        unsafeWindow
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-body
// @license      MIT
// ==/UserScript==

(function () {
  "use strict"; // 启用严格模式

  // --- 配置区域 ---

  // 默认在导航栏显示的搜索引擎名称列表(使用 '-' 分隔)
  const DEFAULT_ENGINES_ORDER = "必应-百度-谷歌-头条-F搜-夸克-搜狗-360";
  // 非搜索页面选中文本后工具栏自动隐藏的延迟时间(毫秒)
  const AUTO_HIDE_DELAY = 5000; // 默认5秒
  // 所有支持的搜索引擎名称列表(用于设置提示)
  const ALL_SUPPORTED_ENGINES =
    "必应-百度-谷歌-知乎-F搜-360-夸克-搜狗-头条-Yandex-Ecosia-DuckDuckGo-QwantLite-Swisscows";
  // 用户自定义排序设置在 GM 存储中的键名
  const GM_STORAGE_KEY = "punk_setup_search";

  // --- 搜索引擎配置 (保持不变) ---
  const SEARCH_ENGINES = [
    {
      name: "必应",
      searchUrl: "https://www.bing.com/search?q=",
      searchkeyName: ["q"],
      matchUrl: /bing\.com.*?search\?q=/i,
    },
    {
      name: "百度",
      searchUrl: "https://baidu.com/s?wd=",
      searchkeyName: ["wd", "word"],
      matchUrl: /baidu\.com.*?w(or)?d=/i,
    },
    {
      name: "谷歌",
      searchUrl: "https://www.google.com/search?q=",
      searchkeyName: ["q"],
      matchUrl: /google\..*?\/search.*?q=/i,
    },
    {
      name: "知乎",
      searchUrl: "https://www.zhihu.com/search?q=",
      searchkeyName: ["q"],
      matchUrl: /zhihu\.com\/search.*?q=/i,
    },
    {
      name: "F搜",
      searchUrl: "https://fsoufsou.com/search?q=",
      searchkeyName: ["q"],
      matchUrl: /fsoufsou\.com\/.*?q=/i,
    },
    {
      name: "360",
      searchUrl: "https://www.so.com/s?q=",
      searchkeyName: ["q"],
      matchUrl: /\.so\.com.*?q=/i,
    },
    {
      name: "夸克",
      searchUrl: "https://quark.sm.cn/s?q=",
      searchkeyName: ["q"],
      matchUrl: /sm\.cn.*?q=/i,
    },
    {
      name: "搜狗",
      searchUrl: "https://m.sogou.com/web/searchList.jsp?keyword=",
      searchkeyName: ["keyword"],
      matchUrl: /sogou\.com.*?keyword=/i,
    },
    {
      name: "头条",
      searchUrl: "https://so.toutiao.com/search/?keyword=",
      searchkeyName: ["keyword"],
      matchUrl: /toutiao\.com.*?keyword=/i,
    },
    {
      name: "Yandex",
      searchUrl: "https://yandex.com/search/touch/?text=",
      searchkeyName: ["text"],
      matchUrl: /((ya(ndex)?\.ru)|(yandex\.com)).*?text=/i,
    },
    {
      name: "DuckDuckGo",
      searchUrl: "https://duckduckgo.com/?q=",
      searchkeyName: ["q"],
      matchUrl: /duckduckgo\.com.*?q=/i,
    },
    {
      name: "Ecosia",
      searchUrl: "https://www.ecosia.org/search?q=",
      searchkeyName: ["q"],
      matchUrl: /ecosia\.org.*?q=/i,
    },
    {
      name: "QwantLite",
      searchUrl: "https://lite.qwant.com/?q=",
      searchkeyName: ["q"],
      matchUrl: /lite\.qwant\.com.*?q=/i,
    },
    {
      name: "Swisscows",
      searchUrl: "https://swisscows.com/en/web?query=",
      searchkeyName: ["query"],
      matchUrl: /swisscows\.com.*?query=/i,
    },
  ];
  // --- 社交及其他站点分类配置 (保持不变) ---
  const SOCIAL_SITES = [
    {
      tabName: "日常",
      tabList: [
        { name: "知乎", searchUrl: "https://www.zhihu.com/search?q=" },
        { name: "豆瓣", searchUrl: "https://m.douban.com/search/?query=" },
        {
          name: "微博",
          searchUrl: "https://m.weibo.cn/search?containerid=100103&q=",
        },
        {
          name: "哔哩哔哩",
          searchUrl: "https://m.bilibili.com/search?keyword=",
        },
        { name: "维基百科", searchUrl: "https://zh.m.wikipedia.org/wiki/" },
        { name: "安娜档案", searchUrl: "https://annas-archive.org/search?q=" },
        { name: "Unsplash", searchUrl: "https://unsplash.com/s/photos/" },
        {
          name: "火山翻译",
          searchUrl: "https://translate.volcengine.com/mobile?text=",
        },
        { name: "博客园", searchUrl: "https://zzk.cnblogs.com/s?w=" },
      ],
    },
    {
      tabName: "娱乐",
      tabList: [
        { name: "知乎", searchUrl: "https://www.zhihu.com/search?q=" },
        { name: "豆瓣", searchUrl: "https://m.douban.com/search/?query=" },
        {
          name: "微博",
          searchUrl: "https://m.weibo.cn/search?containerid=100103&q=",
        },
        {
          name: "哔哩哔哩",
          searchUrl: "https://m.bilibili.com/search?keyword=",
        },
        {
          name: "小红书",
          searchUrl: "https://m.sogou.com/web/xiaohongshu?keyword=",
        },
        {
          name: "微信文章",
          searchUrl: "https://weixin.sogou.com/weixinwap?type=2&query=",
        },
        { name: "推特", searchUrl: "https://mobile.twitter.com/search?q=" },
        { name: "豆瓣阅读", searchUrl: "https://read.douban.com/search?q=" },
        {
          name: "Malavida",
          searchUrl: "https://www.malavida.com/en/android/s/",
        },
        { name: "ApkPure", searchUrl: "https://m.apkpure.com/search?q=" },
        { name: "安娜档案", searchUrl: "https://annas-archive.org/search?q=" },
        { name: "人人影视", searchUrl: "https://www.renren.pro/search?wd=" },
        { name: "豌豆Pro", searchUrl: "https://wandou.la/search/" },
      ],
    },
    {
      tabName: "开发",
      tabList: [
        {
          name: "开发者搜索",
          searchUrl: "https://kaifa.baidu.com/searchPage?wd=",
        },
        { name: "GitHub", searchUrl: "https://github.com/search?q=" },
        { name: "Gitee", searchUrl: "https://search.gitee.com/?q=" },
        {
          name: "Stackoverflow",
          searchUrl: "https://stackoverflow.com/search?q=",
        },
        { name: "GreasyFork", searchUrl: "https://gf.qytechs.cn/scripts?q=" },
        { name: "MDN", searchUrl: "https://developer.mozilla.org/search?q=" },
        { name: "菜鸟教程", searchUrl: "https://www.runoob.com/?s=" },
        { name: "掘金", searchUrl: "https://juejin.cn/search?query=" },
        { name: "博客园", searchUrl: "https://zzk.cnblogs.com/s?w=" },
      ],
    },
    {
      tabName: "网盘",
      tabList: [
        { name: "阿里云盘", searchUrl: "https://alipansou.com/search?k=" },
        { name: "百度云盘", searchUrl: "https://xiongdipan.com/search?k=" },
        { name: "夸克网盘", searchUrl: "https://aipanso.com/search?k=" },
        {
          name: "罗马网盘",
          searchUrl: "https://www.luomapan.com/#/main/search?keyword=",
        },
      ],
    },
    {
      tabName: "翻译",
      tabList: [
        { name: "有道词典", searchUrl: "https://youdao.com/m/result?word=" },
        { name: "必应翻译", searchUrl: "https://cn.bing.com/dict/search?q=" },
        { name: "百度翻译", searchUrl: "https://fanyi.baidu.com/#zh/en/" },
        {
          name: "谷歌翻译",
          searchUrl: "https://translate.google.com/?sl=auto&tl=auto&text=",
        },
        {
          name: "火山翻译",
          searchUrl: "https://translate.volcengine.com/mobile?text=",
        },
        {
          name: "DeepL翻译",
          searchUrl: "https://www.deepl.com/translator-mobile#auto/auto/",
        },
      ],
    },
    {
      tabName: "图片",
      tabList: [
        {
          name: "谷歌搜图",
          searchUrl: "https://www.google.com/search?tbm=isch&q=",
        },
        {
          name: "必应搜图",
          searchUrl: "https://www.bing.com/images/search?q=",
        },
        { name: "Flickr", searchUrl: "https://www.flickr.com/search/?text=" },
        {
          name: "Pinterest",
          searchUrl: "https://www.pinterest.com/search/pins/?q=",
        },
        { name: "Pixabay", searchUrl: "https://pixabay.com/images/search/" },
        { name: "花瓣", searchUrl: "https://huaban.com/search/?q=" },
        { name: "Unsplash", searchUrl: "https://unsplash.com/s/photos/" },
      ],
    },
  ];

  // --- 辅助函数 ---

  /**
   * 创建并返回一个指定标签名和属性的 HTML 元素。
   * @param {string} tagName - HTML 标签名 (e.g., 'div', 'a').
   * @param {object} [attributes={}] - 一个包含属性键值对的对象 (e.g., { id: 'myId', class: 'myClass' }).
   * @param {string} [innerHTML=''] - 元素的 innerHTML 内容。
   * @returns {HTMLElement} 创建的 HTML 元素。
   */
  function createElement(tagName, attributes = {}, innerHTML = "") {
    const element = document.createElement(tagName);
    for (const key in attributes) {
      if (Object.hasOwnProperty.call(attributes, key)) {
        element.setAttribute(key, attributes[key]);
      }
    }
    if (innerHTML) {
      element.innerHTML = innerHTML;
    }
    return element;
  }

  /**
   * 从当前 URL 中提取搜索关键词。
   * 遍历 SEARCH_ENGINES 配置,匹配当前 URL 并查找对应的搜索参数。
   * @returns {string} 提取到的关键词 (已解码),如果未找到则返回空字符串。
   */
  function getKeywordsFromUrl() {
    const currentUrl = window.location.href;
    try {
      const urlParams = new URLSearchParams(window.location.search);
      for (const engine of SEARCH_ENGINES) {
        // 使用 try catch 包裹 matchUrl 匹配,防止正则错误导致脚本中断
        try {
          if (currentUrl.match(engine.matchUrl)) {
            for (const key of engine.searchkeyName) {
              if (urlParams.has(key)) {
                return decodeURIComponent(urlParams.get(key) || "");
              }
            }
            break; // 匹配到引擎但未找到 key,也跳出循环
          }
        } catch (regexError) {
          console.error(
            `聚合搜索:匹配引擎 ${engine.name} 的正则表达式出错:`,
            regexError
          );
          // 可以选择继续检查下一个引擎或直接返回
          continue;
        }
      }
    } catch (urlError) {
      console.error("聚合搜索:解析 URL 参数时出错:", urlError);
    }
    return "";
  }

  /**
   * 获取用户配置的或默认的搜索引擎显示顺序。
   * !!! 添加了对 GM_getValue 的可用性检查 !!!
   * @returns {string[]} 搜索引擎名称数组。
   */
  function getDisplayEngineNames() {
    let storedValue = DEFAULT_ENGINES_ORDER; // 默认值
    try {
      // 检查 GM_getValue 是否可用且为函数类型
      if (typeof GM_getValue === "function") {
        storedValue = GM_getValue(GM_STORAGE_KEY, DEFAULT_ENGINES_ORDER);
      } else {
        console.warn(
          "聚合搜索:GM_getValue 不可用或不是函数,将使用默认引擎排序。脚本是否在 Tampermonkey 等环境中运行?"
        );
      }
    } catch (e) {
      // 捕获调用 GM_getValue 时可能发生的其他错误
      console.error(
        "聚合搜索:读取 GM_getValue 时发生错误,将使用默认引擎排序。",
        e
      );
    }
    // 确保后续处理逻辑使用有效的值(读取到的或默认值)
    return storedValue
      .split("-")
      .map((name) => name.trim())
      .filter(Boolean);
  }

  /**
   * 构建包含搜索链接的 <li> 元素列表。
   * @param {Array<object>} linkList - 包含 { name, searchUrl } 对象的数组。
   * @param {string} [currentKeywords=''] - 当前搜索的关键词,用于预设链接。
   * @param {string} [activeEngineName=''] - 当前活动搜索引擎的名称,用于高亮。
   * @returns {DocumentFragment} 包含所有 <li> 元素的文档片段。
   */
  function createLinkListItems(
    linkList,
    currentKeywords = "",
    activeEngineName = ""
  ) {
    const fragment = document.createDocumentFragment();
    const encodedKeywords = encodeURIComponent(currentKeywords);

    for (const item of linkList) {
      // 确保 item 有 name 和 searchUrl
      if (!item || !item.name || typeof item.searchUrl !== "string") {
        console.warn(
          "聚合搜索: createLinkListItems 收到无效的 link item:",
          item
        );
        continue; // 跳过无效项
      }

      const isCurrentEngine = item.name === activeEngineName;
      // 生成一个相对安全的 ID
      const safeIdName = item.name.replace(/[^a-zA-Z0-9_-]/g, "-");
      const link = createElement(
        "a",
        {
          href: item.searchUrl + encodedKeywords,
          "data-base-url": item.searchUrl, // 存储基础 URL,供后续更新
          id: `punk-link-${safeIdName}`, // 使用处理过的名称生成 ID
          style: `color: ${
            isCurrentEngine ? "#5C6BC0" : "#666666"
          } !important; font-weight: ${isCurrentEngine ? "bold" : "normal"};`,
        },
        item.name
      );

      // 更新链接URL但不跳转
      const updateLinkHref = () => {
        const keywords = getCurrentSearchTerm();
        if (keywords) {
          link.href = link.getAttribute("data-base-url") + encodeURIComponent(keywords);
        }
      };

      // 点击时在新标签页打开
      link.addEventListener("click", (e) => {
        e.preventDefault();
        const keywords = getCurrentSearchTerm();
        if (keywords) {
          window.open(link.href, '_blank');
        } else {
          console.warn("聚合搜索:无关键词,将跳转到搜索引擎首页。");
        }
      });

      // 右键/长按也更新链接 (移动端长按触发 contextmenu)
      link.addEventListener("contextmenu", updateLinkHref);
      // 鼠标悬停时只更新链接URL,不跳转
      link.addEventListener("mouseenter", updateLinkHref);

      const listItem = createElement("li");
      listItem.appendChild(link);
      fragment.appendChild(listItem);
    }
    return fragment;
  }

  /**
   * 获取当前有效的搜索词 (优先从选中文本获取,其次从 URL 获取)
   * @returns {string} 当前搜索词
   */
  function getCurrentSearchTerm() {
    const selection = window.getSelection().toString().trim();
    // 如果没有选中,则尝试从 URL 获取
    return selection || getKeywordsFromUrl();
  }

  // --- UI 创建函数 ---

  /**
   * 创建并添加主搜索导航栏容器。
   * @returns {HTMLElement | null} 创建的导航栏容器元素,如果创建失败则返回 null。
   */
  function createMainContainer() {
    try {
      const mainContainer = createElement("div", {
        id: "punkjet-search-box",
        style: "display: none; font-size: 15px;", // 初始隐藏
      });
      // 确保 body 存在后再插入
      if (document.body) {
        document.body.insertAdjacentElement("afterbegin", mainContainer); // 插入到 body 开始处
        return mainContainer;
      } else {
        console.error(
          "聚合搜索:document.body 不存在,无法插入主容器。脚本可能运行过早。"
        );
        // 可以尝试延迟执行或监听 DOMContentLoaded
        // document.addEventListener('DOMContentLoaded', () => { /* ... */ });
        return null;
      }
    } catch (e) {
      console.error("聚合搜索:创建主容器时出错:", e);
      return null;
    }
  }

  /**
   * 创建导航栏的核心部分(链接列表、设置按钮、关闭按钮)。
   * @param {HTMLElement} parentContainer - 主容器元素。
   * @param {string} currentKeywords - 当前搜索关键词。
   * @param {string} activeEngineName - 当前活动引擎名称。
   */
  function createNavigationBar(
    parentContainer,
    currentKeywords,
    activeEngineName
  ) {
    if (!parentContainer) return; // 如果父容器无效则退出

    const naviBox = createElement("div", { id: "punk-search-navi-box" });
    const appBox = createElement("div", { id: "punk-search-app-box" });
    const ulList = createElement("ul");

    // 合并所有可搜索项(搜索引擎 + 社交站点)以便查找
    const allSearchableItems = [...SEARCH_ENGINES];
    SOCIAL_SITES.forEach((category) => {
      if (category && Array.isArray(category.tabList)) {
        category.tabList.forEach((site) => {
          if (site && site.name && typeof site.searchUrl === "string") {
            // 确保不重复添加(基于 name)
            if (
              !allSearchableItems.some(
                (existing) => existing.name === site.name
              )
            ) {
              allSearchableItems.push({ ...site, isSocial: true }); // 标记为社交站点
            }
          } else {
            console.warn(
              "聚合搜索: 无效的社交站点配置:",
              site,
              "in category",
              category.tabName
            );
          }
        });
      } else {
        console.warn("聚合搜索: 无效的社交站点分类:", category);
      }
    });

    // 获取要显示的引擎名称列表(已处理 GM_getValue 不可用的情况)
    const displayNames = getDisplayEngineNames();

    const displayItems = displayNames
      .map((name) => {
        // 查找时也需要健壮性
        const found = allSearchableItems.find(
          (item) => item && item.name === name
        );
        if (!found) {
          console.warn(`聚合搜索:在配置中未找到名为 "${name}" 的引擎或站点。`);
        }
        return found;
      })
      .filter(Boolean); // 过滤掉未找到的项

    // 创建链接列表
    ulList.appendChild(
      createLinkListItems(displayItems, currentKeywords, activeEngineName)
    );
    appBox.appendChild(ulList);

    // 创建设置按钮
    const settingsButton = createElement(
      "div",
      {
        id: "search-setting-box",
        title: "设置与更多选项",
      },
      `<span id="punkBtnSet">⛮</span>`
    );
    settingsButton.onclick = toggleJumpSearchBox; // 绑定点击事件

    // 创建关闭按钮
    const closeButton = createElement(
      "div",
      {
        id: "search-close-box",
        title: "隐藏导航栏",
      },
      `<span id="punkBtnClose">✕</span>`
    );
    closeButton.onclick = hideNavigationBar; // 绑定点击事件

    naviBox.appendChild(appBox);
    naviBox.appendChild(settingsButton);
    naviBox.appendChild(closeButton);
    parentContainer.appendChild(naviBox);
  }

  /**
   * 创建并添加 "展开" 按钮(初始隐藏)。
   * 当导航栏被关闭时显示,点击可重新打开导航栏。
   * @param {HTMLElement} referenceElement - "展开"按钮将插入到此元素之后。
   */
  function createOpenButton(referenceElement) {
    if (!referenceElement || !referenceElement.after) {
      console.error("聚合搜索:无法创建展开按钮,参考元素无效。");
      return;
    }
    const openButton = createElement("div", {
      id: "punk-search-open-box",
      title: "显示搜索导航",
      style: "display: none;", // 初始隐藏
    });
    openButton.onclick = showNavigationBar; // 绑定点击事件
    try {
      referenceElement.after(openButton); // 插入元素
    } catch (e) {
      console.error("聚合搜索:插入展开按钮时出错:", e);
      // 备选方案:插入到 body 末尾
      // document.body.appendChild(openButton);
    }
  }

  /**
   * 创建并添加 "跳转/设置" 面板(初始隐藏)。
   * 包含全部搜索引擎、分类站点和排序设置。
   * @param {HTMLElement} parentContainer - 主容器元素。
   */
  function createJumpSearchBox(parentContainer) {
    if (!parentContainer) return;

    const jumpBox = createElement("div", {
      id: "punk-search-jump-box",
      style: "display: none;", // 初始隐藏
    });

    const currentKeywords = getCurrentSearchTerm(); // 获取当前关键词

    // --- 全部搜索引擎 ---
    try {
      const allEnginesSection = createElement("div", {
        id: "punk-search-all-app",
      });
      allEnginesSection.appendChild(createElement("h1", {}, "✰ 全部搜索引擎"));
      const allEnginesUl = createElement("ul");
      allEnginesUl.appendChild(
        createLinkListItems(SEARCH_ENGINES, currentKeywords)
      );
      allEnginesSection.appendChild(allEnginesUl);
      jumpBox.appendChild(allEnginesSection);
    } catch (e) {
      console.error("聚合搜索:创建全部搜索引擎部分出错:", e);
    }

    // --- 分类站点 Tab ---
    try {
      const tabContainer = createElement("div", { id: "punk-tab-container" });
      const tabListDiv = createElement("div", { id: "punk-tablist" });
      tabListDiv.appendChild(createElement("h1", {}, "@ 分类站点"));
      const tabUl = createElement("ul");
      const tabContentDiv = createElement("div", { class: "tab-content" });

      const tabFragments = document.createDocumentFragment();
      const contentFragments = document.createDocumentFragment();

      SOCIAL_SITES.forEach((category, index) => {
        if (
          !category ||
          !category.tabName ||
          !Array.isArray(category.tabList)
        ) {
          console.warn("聚合搜索:跳过无效的分类站点配置:", category);
          return;
        }
        // 创建 Tab 标签
        const tabLi = createElement(
          "li",
          { "data-index": index },
          category.tabName
        );
        if (index === 0) tabLi.classList.add("punk-current");
        tabFragments.appendChild(tabLi);

        // 创建 Tab 内容面板
        const contentItem = createElement("div", {
          class: "punk-item",
          style: `display: ${index === 0 ? "block" : "none"};`,
        });
        const contentUl = createElement("ul");
        // 过滤掉 tabList 中的无效项
        const validTabList = category.tabList.filter(
          (site) => site && site.name && typeof site.searchUrl === "string"
        );
        contentUl.appendChild(
          createLinkListItems(validTabList, currentKeywords)
        );
        contentItem.appendChild(contentUl);
        contentFragments.appendChild(contentItem);
      });

      tabUl.appendChild(tabFragments);
      tabListDiv.appendChild(tabUl);
      tabContainer.appendChild(tabListDiv);
      tabContentDiv.appendChild(contentFragments);
      tabContainer.appendChild(tabContentDiv);
      jumpBox.appendChild(tabContainer);

      // 延迟执行 Tab 切换设置,确保元素已添加到 DOM
      // 使用 requestAnimationFrame 或短 setTimeout
      setTimeout(setupTabSwitching, 0);
    } catch (e) {
      console.error("聚合搜索:创建分类站点 Tab 部分出错:", e);
    }

    // --- 排序设置 ---
    try {
      jumpBox.appendChild(createElement("h1", {}, "■ 搜索引擎排序"));
      const sortDesc = createElement("div", { class: "jump-sort-discription" });
      sortDesc.innerHTML = `<a style="color:#666666 !important">说明:设置在导航栏中显示的搜索引擎及其顺序。<br>支持的格式:${ALL_SUPPORTED_ENGINES}</a>`;
      jumpBox.appendChild(sortDesc);

      const sortButton = createElement(
        "button",
        { class: "punk-jump-sort-btn" },
        "点击输入排序"
      );
      sortButton.onclick = promptAndSetSortOrder;
      jumpBox.appendChild(sortButton);

      const closeJumpButton = createElement(
        "button",
        { class: "punk-jump-sort-btn" },
        "收起面板"
      );
      closeJumpButton.onclick = () => {
        jumpBox.style.display = "none";
      };
      jumpBox.appendChild(closeJumpButton);
    } catch (e) {
      console.error("聚合搜索:创建排序设置部分出错:", e);
    }

    parentContainer.appendChild(jumpBox);
  }

  /**
   * 为分类站点的 Tab 添加点击切换功能。
   * 需要在 Tab 和内容元素创建后调用。
   */
  function setupTabSwitching() {
    try {
      const tabList = document.querySelector("#punk-tablist ul");
      const items = document.querySelectorAll("#punk-tab-container .punk-item");
      const tabs = document.querySelectorAll("#punk-tablist li");

      if (
        !tabList ||
        items.length === 0 ||
        tabs.length === 0 ||
        tabs.length !== items.length
      ) {
        console.warn(
          "聚合搜索:未能找到 Tab 相关元素或数量不匹配,无法设置切换功能。",
          {
            tabListExists: !!tabList,
            itemsCount: items.length,
            tabsCount: tabs.length,
          }
        );
        return;
      }

      // 使用事件委托
      tabList.addEventListener("click", (event) => {
        const clickedTab = event.target.closest("li[data-index]"); // 确保点击的是有效的 tab li
        if (!clickedTab) return;

        const index = clickedTab.getAttribute("data-index");

        // 更新 Tab 样式
        tabs.forEach((tab) => tab.classList.remove("punk-current"));
        clickedTab.classList.add("punk-current");

        // 更新内容显示
        items.forEach((item, i) => {
          item.style.display = i == index ? "block" : "none";
        });
      });
    } catch (e) {
      console.error("聚合搜索:设置 Tab 切换功能时出错:", e);
    }
  }

  /**
   * 注入脚本所需的 CSS 样式。
   * @param {HTMLElement} parentContainer - 主容器元素,样式将注入其中。
   */
  function injectStyle(parentContainer) {
    if (!parentContainer) return;
    const css = `
      /* --- 主容器与导航栏 --- */
      #punkjet-search-box { position: fixed; top: 0; left: 0; width: 100%; height: 35px; background-color: rgba(255, 255, 255, 0.85) !important; z-index: 9999999; display: flex; flex-direction: column; box-shadow: 0 1px 2px rgba(0,0,0,0.1); font-family: Helvetica Neue, Helvetica, Arial, sans-serif; }
      #punk-search-navi-box { display: flex; align-items: center; width: 100%; height: 100%; }
      #punk-search-app-box { flex: 1; overflow: hidden; display: flex; align-items: center; }
      #punk-search-app-box ul { margin: 0; padding: 0 5px; list-style: none; white-space: nowrap; overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; scrollbar-color: #aaa #eee; height: 100%; display: flex; align-items: center; }
      #punk-search-app-box ul::-webkit-scrollbar { height: 4px; background-color: #eee; } #punk-search-app-box ul::-webkit-scrollbar-thumb { background-color: #aaa; border-radius: 2px; }
      #punk-search-app-box li { display: inline-block; margin: 0 2px; vertical-align: middle; }
      #punk-search-app-box ul li a { display: block; padding: 6px 8px; text-decoration: none; font-size: 14px !important; color: #666666 !important; border-radius: 3px; transition: background-color 0.2s ease; }
      #punk-search-app-box ul li a:hover { background-color: #f0f0f0; }
      #punk-search-app-box ul li a[style*="font-weight: bold"] { color: #5C6BC0 !important; }

      /* --- 设置与关闭按钮 --- */
      #search-setting-box, #search-close-box { flex: 0 0 35px; display: flex; align-items: center; justify-content: center; height: 100%; cursor: pointer; font-size: 18px; color: #555; transition: background-color 0.2s ease; }
      #search-setting-box:hover, #search-close-box:hover { background-color: #f0f0f0; }

      /* --- 展开按钮 (浮动) --- */
      #punk-search-open-box { position: fixed; left: 15px; bottom: 70px; height: 40px; width: 40px; font-size: 15px; text-align: center; border-radius: 50%; z-index: 9999998; background: #005fbf url("data:image/svg+xml;utf8,%3Csvg width='48' height='48' xmlns='http://www.w3.org/2000/svg' stroke='null' style='vector-effect:non-scaling-stroke;' fill='none'%3E%3Cg id='Layer_1'%3E%3Ctitle%3ELayer 1%3C/title%3E%3Cpath stroke='%23000' id='svg_5' d='m1.97999,23.9675l0,0c0,-12.42641 10.0537,-22.5 22.45556,-22.5l0,0c5.95558,0 11.66724,2.37053 15.87848,6.5901c4.21123,4.21957 6.57708,9.94253 6.57708,15.9099l0,0c0,12.4264 -10.05369,22.5 -22.45555,22.5l0,0c-12.40186,0 -22.45556,-10.07359 -22.45556,-22.5zm22.45556,-22.5l0,45m-22.45556,-22.5l44.91111,0' stroke-width='0' fill='%23005fbf'/%3E%3Cpath stroke='%23000' id='svg_7' d='m13.95011,18.65388l0,0l0,-0.00203l0,0.00203zm0.00073,-0.00203l4.2148,5.84978l-4.21553,5.84775l1.54978,2.15123l5.76532,-8l-5.76532,-8l-1.54905,2.15123zm7.46847,13.70285l10.5308,0l0,-3.03889l-10.5308,0l0,3.03889zm3.16603,-6.33312l7.36476,0l0,-3.03889l-7.36476,0l0,3.03889zm-3.16603,-9.37302l0,3.04091l10.5308,0l0,-3.04091l-10.5308,0z' stroke-width='0' fill='%23ffffff'/%3E%3Cpath id='svg_8' d='m135.44834,59.25124l0,0l0,-0.00001l0,0.00001zm0.00004,-0.00001l0.23416,0.02887l-0.2342,0.02886l0.0861,0.01062l0.3203,-0.03948l-0.3203,-0.03948l-0.08606,0.01062zm0.41492,0.06762l0.58504,0l0,-0.015l-0.58504,0l0,0.015zm0.17589,-0.03125l0.40915,0l0,-0.015l-0.40915,0l0,0.015zm-0.17589,-0.04625l0,0.01501l0.58504,0l0,-0.01501l-0.58504,0z' stroke-width='0' stroke='%23000' fill='%23ffffff'/%3E%3C/g%3E%3C/svg%3E") no-repeat center; background-size: 60%; box-shadow: 0 2px 5px rgba(0,0,0,0.2); cursor: pointer; box-sizing: border-box !important; }
      #punk-search-open-box:hover { background-color: #004c99; }

      /* --- 跳转/设置面板 --- */
      #punk-search-jump-box { position: absolute; top: 35px; right: 0; width: 95%; max-width: 450px; max-height: calc(90vh - 40px); padding: 10px; background-color: #ffffff !important; box-shadow: 0 2px 8px rgba(0,0,0,0.15); border-radius: 0 0 5px 5px; overflow-y: auto; z-index: 9999998; scrollbar-width: thin; scrollbar-color: #ccc #f8f8f8; }
      #punk-search-jump-box::-webkit-scrollbar { width: 6px; background-color: #f8f8f8; } #punk-search-jump-box::-webkit-scrollbar-thumb { background-color: #ccc; border-radius: 3px; }
      #punk-search-jump-box h1 { font-size: 14px !important; color: #333 !important; font-weight: bold; margin: 12px 0 8px 4px; padding-bottom: 4px; border-bottom: 1px solid #eee; }
      #punk-search-jump-box ul { margin: 0 0 10px 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: 5px; }
      #punk-search-jump-box li { background-color: #f2f2f2 !important; border-radius: 3px; transition: background-color 0.2s ease; }
      #punk-search-jump-box li:hover { background-color: #e0e0e0 !important; }
      #punk-search-jump-box a { display: block; color: #333 !important; padding: 4px 8px; margin: 0; font-size: 13px; text-decoration: none; white-space: nowrap; }

      /* --- 设置面板 - Tab --- */
       #punk-tab-container { margin-bottom: 15px; }
       #punk-tablist ul { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; padding-left: 4px; }
       #punk-tablist li { list-style: none; cursor: pointer; padding: 4px 0; font-size: 13px; color: #666666 !important; border-bottom: 3px solid transparent; transition: color 0.2s ease, border-color 0.2s ease; }
       #punk-tablist li:hover { color: #005fbf !important; }
       #punk-tablist li.punk-current { color: #005fbf !important; font-weight: bold; border-bottom-color: #005fbf; }
       .tab-content .punk-item ul { gap: 5px; }

      /* --- 设置面板 - 排序 --- */
      .jump-sort-discription { margin: 5px 4px 10px; font-size: 12px; color: #666; line-height: 1.5; }
      .punk-jump-sort-btn { background-color: #007bff; border: none; color: white; padding: 8px 16px; text-align: center; text-decoration: none; display: block; width: calc(100% - 10px); font-size: 13px; margin: 8px auto; cursor: pointer; border-radius: 4px; transition: background-color 0.2s ease; }
      .punk-jump-sort-btn:hover { background-color: #0056b3; }
      .punk-jump-sort-btn:last-of-type { background-color: #6c757d; } .punk-jump-sort-btn:last-of-type:hover { background-color: #5a6268; }

      /* --- 页面主体调整 (使用 data-* 属性) --- */
      body[data-search-nav-active="true"] { padding-top: 35px !important; }
      /* 调整 fixed/sticky 元素,排除导航栏自身 */
      body[data-search-nav-active="true"] [style*="position: fixed"][style*="top: 0px"]:not(#punkjet-search-box):not([id^="punk-"]),
      body[data-search-nav-active="true"] [style*="position: sticky"][style*="top: 0px"]:not(#punkjet-search-box):not([id^="punk-"]) {
         top: 35px !important;
      }
      /* 恢复 */
      body:not([data-search-nav-active="true"]) [style*="position: fixed"][style*="top: 35px"]:not(#punkjet-search-box):not([id^="punk-"]),
      body:not([data-search-nav-active="true"]) [style*="position: sticky"][style*="top: 35px"]:not(#punkjet-search-box):not([id^="punk-"]) {
          /* 这个恢复逻辑可能不总是需要,取决于页面原始状态 */
         /* top: 0px !important; */
      }
    `;
    try {
      const styleElement = createElement("style", { type: "text/css" }, css);
      parentContainer.appendChild(styleElement);
    } catch (e) {
      console.error("聚合搜索:注入样式时出错:", e);
    }
  }

  // --- 事件处理与逻辑控制 ---

  /**
   * 显示导航栏,并调整页面布局。
   */
  function showNavigationBar() {
    try {
      const navBox = document.getElementById("punkjet-search-box");
      const openButton = document.getElementById("punk-search-open-box");
      if (navBox) {
        navBox.style.display = "flex";
        document.body.setAttribute("data-search-nav-active", "true"); // 添加标记属性到 body
      } else {
        console.warn("聚合搜索:无法找到导航栏元素 #punkjet-search-box");
      }
      if (openButton) {
        openButton.style.display = "none";
      }
      // adjustFixedElements(true); // CSS 会处理
    } catch (e) {
      console.error("聚合搜索:显示导航栏时出错:", e);
    }
  }

  /**
   * 隐藏导航栏,恢复页面布局,并显示 "展开" 按钮。
   */
  function hideNavigationBar() {
    try {
      const navBox = document.getElementById("punkjet-search-box");
      const openButton = document.getElementById("punk-search-open-box");
      const jumpBox = document.getElementById("punk-search-jump-box");
      if (navBox) {
        navBox.style.display = "none";
        document.body.removeAttribute("data-search-nav-active"); // 移除标记
      } else {
        console.warn("聚合搜索:无法找到导航栏元素 #punkjet-search-box");
      }
      if (openButton) {
        openButton.style.display = "block";
      } // 显示展开按钮
      if (jumpBox) {
        jumpBox.style.display = "none";
      } // 同时隐藏设置面板
      // adjustFixedElements(false); // CSS 会处理
    } catch (e) {
      console.error("聚合搜索:隐藏导航栏时出错:", e);
    }
  }

  /**
   * 切换 "跳转/设置" 面板的显示状态。
   */
  function toggleJumpSearchBox() {
    try {
      const jumpBox = document.getElementById("punk-search-jump-box");
      if (jumpBox) {
        jumpBox.style.display =
          jumpBox.style.display === "none" ? "block" : "none";
      } else {
        console.warn(
          "聚合搜索:无法找到跳转/设置面板元素 #punk-search-jump-box"
        );
      }
    } catch (e) {
      console.error("聚合搜索:切换设置面板显示时出错:", e);
    }
  }

  /**
   * 弹出提示框让用户输入新的排序,并保存设置,然后刷新页面。
   * !!! 添加了对 GM_getValue/GM_setValue 的可用性检查 !!!
   */
  function promptAndSetSortOrder() {
    let currentOrder = DEFAULT_ENGINES_ORDER; // 默认值
    let canAccessStorage = false; // 标记是否能访问存储

    try {
      // 检查 GM_getValue 是否可用
      if (typeof GM_getValue === "function") {
        currentOrder = GM_getValue(GM_STORAGE_KEY, DEFAULT_ENGINES_ORDER);
        canAccessStorage = true; // 确认可以访问
      } else {
        console.warn(
          "聚合搜索:GM_getValue 不可用,无法读取当前排序,将使用默认值。"
        );
        // 如果 GM_getValue 不可用,假定 GM_setValue 也不可用
      }
    } catch (e) {
      console.error(
        "聚合搜索:读取 GM_getValue 时发生错误,将使用默认排序。",
        e
      );
      // 出错也认为无法访问存储
    }

    const promptMessage = `请输入导航栏需要显示的引擎名称,用 '-' 分隔。\n可用引擎: ${ALL_SUPPORTED_ENGINES}\n当前设置:${
      canAccessStorage ? "" : " (无法读取存储,显示为默认值)"
    }`;

    const newUserSortOrder = prompt(promptMessage, currentOrder);

    if (newUserSortOrder !== null) {
      // 用户点击了确定
      const trimmedOrder = newUserSortOrder.trim();
      if (trimmedOrder !== currentOrder) {
        // 检查是否有更改
        if (canAccessStorage && typeof GM_setValue === "function") {
          // 再次检查 setValue 可用性
          try {
            GM_setValue(GM_STORAGE_KEY, trimmedOrder);
            alert("设置已保存,页面将刷新以应用更改。");
            setTimeout(() => location.reload(), 300);
          } catch (e) {
            console.error("聚合搜索:写入 GM_setValue 时发生错误。", e);
            alert("错误:保存设置时出错。");
          }
        } else {
          console.warn(
            "聚合搜索:GM_setValue 不可用或不是函数,无法保存设置。"
          );
          alert(
            "错误:无法保存设置。请确保脚本在 Tampermonkey 等环境中正确运行。"
          );
        }
      } else {
        console.log("排序设置未改变。");
      }
    } else {
      // 用户取消输入
      console.log("用户取消了排序设置。");
    }
  }

  /**
   * 更新导航栏中所有链接的 href,使用最新的关键词。
   * @param {string} keywords - 最新的搜索关键词。
   */
  function updateAllLinkKeywords(keywords) {
    if (typeof keywords !== "string") return; // 防御性编程
    try {
      const encodedKeywords = encodeURIComponent(keywords);
      // 选择器更精确,只选择导航栏内的链接
      const links = document.querySelectorAll(
        "#punk-search-navi-box a[data-base-url], #punk-search-jump-box a[data-base-url]"
      );
      links.forEach((link) => {
        const baseUrl = link.getAttribute("data-base-url");
        if (baseUrl) {
          link.href = baseUrl + encodedKeywords;
        }
      });
    } catch (e) {
      console.error("聚合搜索:更新链接关键词时出错:", e);
    }
  }

  // --- 初始化与执行 ---

  // 使用 try...catch 包裹整个初始化逻辑,防止意外错误导致脚本完全失败
  try {
    // 等待 DOM 基本就绪
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", initializeScript);
    } else {
      initializeScript();
    }
  } catch (globalError) {
    console.error("聚合搜索脚本初始化时发生严重错误:", globalError);
    alert("聚合搜索脚本初始化失败,请检查浏览器控制台获取更多信息。");
  }

  /**
   * 脚本的主要初始化逻辑
   */
  function initializeScript() {
    // 再次检查 body 是否存在
    if (!document.body) {
      console.error("聚合搜索:DOM Ready 后 body 仍然不存在,无法继续初始化。");
      return;
    }

    // 判断当前页面是否是已配置的搜索引擎的结果页
    const currentKeywords = getKeywordsFromUrl();
    const isSearchPage = Boolean(currentKeywords);
    let activeEngineName = "";
    if (isSearchPage) {
      const currentUrl = window.location.href;
      const currentEngine = SEARCH_ENGINES.find((engine) => {
        try {
          return currentUrl.match(engine.matchUrl);
        } catch (e) {
          console.warn(`检查引擎 ${engine.name} 匹配时正则出错:`, e);
          return false;
        }
      });
      if (currentEngine) {
        activeEngineName = currentEngine.name;
      }
    }

    // 创建 UI 元素
    const mainContainer = createMainContainer();
    // 确保主容器创建成功
    if (!mainContainer) {
      console.error("聚合搜索:主容器创建失败,无法继续创建 UI。");
      return;
    }

    createNavigationBar(mainContainer, currentKeywords, activeEngineName);
    createJumpSearchBox(mainContainer);
    createOpenButton(mainContainer); // 创建展开按钮,放在主容器之后
    injectStyle(mainContainer); // 注入样式

    // 根据页面类型和条件决定是否显示导航栏
    if (isSearchPage) {
      showNavigationBar(); // 在搜索结果页默认显示
    } else {
      // 非搜索页面:监听文本选中事件
      let debounceTimer;
      let autoHideTimer;
      document.addEventListener("selectionchange", () => {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(() => {
          try {
            const selection = window.getSelection().toString().trim();
            const navBox = document.getElementById("punkjet-search-box");

            if (selection && navBox) {
              updateAllLinkKeywords(selection); // 使用选中的文本更新链接
              if (navBox.style.display === "none") {
                showNavigationBar(); // 如果导航栏是隐藏的,则显示它
              }
              // 重置自动隐藏计时器
              clearTimeout(autoHideTimer);
              autoHideTimer = setTimeout(() => {
                if (!isSearchPage && navBox.style.display !== "none") {
                  hideNavigationBar();
                }
              }, AUTO_HIDE_DELAY);
            }
          } catch (e) {
            console.error("聚合搜索:处理 selectionchange 事件时出错:", e);
          }
        }, 250); // 延迟处理
      });

      // 当用户与工具栏交互时取消自动隐藏
      const navBox = document.getElementById("punkjet-search-box");
      if (navBox) {
        navBox.addEventListener("mouseenter", () => {
          clearTimeout(autoHideTimer);
        });
        navBox.addEventListener("mouseleave", () => {
          if (!isSearchPage && navBox.style.display !== "none") {
            clearTimeout(autoHideTimer);
            autoHideTimer = setTimeout(() => {
              hideNavigationBar();
            }, AUTO_HIDE_DELAY);
          }
        });
      }
      // 初始状态下,非搜索页面默认隐藏 (通过初始 style 实现)
    }

    // 添加 popstate 监听器
    window.addEventListener("popstate", () => {
      setTimeout(() => {
        try {
          const newKeywords = getKeywordsFromUrl();
          const newIsSearchPage = Boolean(newKeywords);
          const navBox = document.getElementById("punkjet-search-box");

          if (navBox) {
            // 检查导航栏是否存在
            if (navBox.style.display !== "none") {
              // 导航栏当前可见
              if (newIsSearchPage) {
                const newUrl = window.location.href;
                const newEngine = SEARCH_ENGINES.find((engine) => {
                  try {
                    return newUrl.match(engine.matchUrl);
                  } catch (e) {
                    return false;
                  }
                });
                const newActiveEngine = newEngine ? newEngine.name : "";
                updateAllLinkKeywords(newKeywords);
                // 重新渲染导航栏以更新高亮状态可能开销较大
                // 简单的做法是移除所有高亮,再给新的添加
                const links = navBox.querySelectorAll("#punk-search-app-box a");
                links.forEach((link) => {
                  link.style.fontWeight = "normal";
                  link.style.color = "#666666 !important";
                });
                const activeLink = navBox.querySelector(
                  `#punk-link-${newActiveEngine.replace(
                    /[^a-zA-Z0-9_-]/g,
                    "-"
                  )}`
                );
                if (activeLink) {
                  activeLink.style.fontWeight = "bold";
                  activeLink.style.color = "#5C6BC0 !important";
                }
                // adjustFixedElements(true); // CSS 处理
              } else {
                // 从搜索页导航到非搜索页,导航栏保持显示,让用户手动关闭
                // 或者选择隐藏: hideNavigationBar();
                document.body.setAttribute("data-search-nav-active", "true"); // 确保标记还在
              }
            } else if (newIsSearchPage) {
              // 导航栏当前隐藏,但新页面是搜索页
              updateAllLinkKeywords(newKeywords);
              showNavigationBar();
            }
          }
        } catch (e) {
          console.error("聚合搜索:处理 popstate 事件时出错:", e);
        }
      }, 100);
    });

    console.log("聚合搜索引擎切换导航[自改] 初始化完成。");
  } // end of initializeScript
})(); // 立即执行函数结束

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址