// ==UserScript==
// @name 聚合搜索
// @namespace https://github.com/funcdfs
// @version 2.0
// @description 在搜索顶部显示一个聚合搜索切换导航
// @author funcdfs
// @include *
// @grant unsafeWindow
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-start
// @license MIT
// ==/UserScript==
// 默认搜索引擎排序
const defaultEngines = "Google-DuckDuckGo-Yandex-Sougou-Quark-360-Bing-Baidu-Zhihu-Bilibili";
// 搜索引擎配置
const searchEngines = [
{ name: "谷歌", searchUrl: "https://www.google.com/search?q=", searchKey: "q", matchUrl: /google\.com.*?search.*?q=/g, mark: "Google" },
{ name: "DuckDuckGo", searchUrl: "https://duckduckgo.com/?q=", searchKey: "q", matchUrl: /duckduckgo\.com.*?q=/g, mark: "DuckDuckGo" },
{ name: "Yandex", searchUrl: "https://yandex.com/search/?text=", searchKey: "text", matchUrl: /((ya(ndex)?\.ru)|(yandex\.com)).*?text=/g, mark: "Yandex" },
{ name: "搜狗", searchUrl: "https://www.sogou.com/web?query=", searchKey: ["query", "keyword"], matchUrl: /sogou\.com.*?(query|keyword)=/g, mark: "Sougou" },
{ name: "夸克", searchUrl: "https://quark.sm.cn/s?q=", searchKey: "q", matchUrl: /sm\.cn.*?q=/g, mark: "Quark" },
{ name: "360", searchUrl: "https://www.so.com/s?q=", searchKey: "q", matchUrl: /\.so\.com.*?q=/g, mark: "360" },
{ name: "必应", searchUrl: "https://www.bing.com/search?q=", searchKey: "q", matchUrl: /bing\.com.*?search\?q=?/g, mark: "Bing" },
{ name: "百度", searchUrl: "https://baidu.com/s?wd=", searchKey: ["wd", "word"], matchUrl: /baidu\.com.*?w(or)?d=?/g, mark: "Baidu" },
{ name: "知乎", searchUrl: "https://www.zhihu.com/search?q=", searchKey: "q", matchUrl: /zhihu\.com\/search.*?q=/g, mark: "Zhihu" },
{ name: "哔哩哔哩", searchUrl: "https://search.bilibili.com/all?keyword=", searchKey: "keyword", matchUrl: /search\.bilibili\.com.*?keyword=/g, mark: "Bilibili" },
];
// 面板选项配置
const panelOptions = [
{
title: "常用站点",
links: [
{ name: "维基百科", url: "https://zh.wikipedia.org/w/index.php?search=" },
{ name: "GitHub", url: "https://github.com/search?q=&type=repositories" },
{ name: "Stack Overflow", url: "https://stackoverflow.com/search?q=" },
]
},
{
title: "娱乐",
links: [
{ name: "抖音", url: "https://www.douyin.com/root/search/" },
{ name: "豆瓣", url: "https://www.douban.com/search?q=" },
{ name: "豆瓣阅读", url: "https://read.douban.com/search?q=" },
]
},
{
title: "翻译",
links: [
{ name: "DeepL翻译", url: "https://www.deepl.com/translator#zh/en/" },
{ name: "谷歌翻译", url: "https://translate.google.com/?text=" },
{ name: "火山翻译", url: "https://translate.volcengine.com/translate?text=" },
{ name: "百度翻译", url: "https://fanyi.baidu.com/#zh/en/" },
]
},
{
title: "网盘",
links: [
{ name: "阿里云盘", url: "https://alipansou.com/search?k=" },
{ name: "百度网盘", url: "https://xiongdipan.com/search?k=" },
{ name: "夸克网盘", url: "https://aipanso.com/search?k=" },
{ name: "Google网盘", url: "https://drive.google.com/drive/home?dmr=1&ec=wgc-drive-globalnav-goto" },
{ name: "Dropbox", url: "https://www.dropbox.com/home" },
{ name: "Mega网盘", url: "https://mega.nz/fm" }
]
}
];
// 获取当前搜索关键词
function getKeyword() {
for (const engine of searchEngines) {
if (window.location.href.match(engine.matchUrl)) {
const keys = Array.isArray(engine.searchKey) ? engine.searchKey : [engine.searchKey];
for (const key of keys) {
if (window.location.href.indexOf(key) >= 0) {
const url = new URL(window.location.href);
return url.searchParams.get(key);
}
}
}
}
return "";
}
// 添加主搜索栏
function addSearchBar() {
// 创建主容器
const container = document.createElement("div");
container.id = "search-bar-container";
container.className = "search-container";
// 创建搜索引擎列表
const engineList = document.createElement("div");
engineList.className = "engine-list";
// 创建展开按钮
const expandBtn = document.createElement("button");
expandBtn.className = "expand-btn";
expandBtn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
<span>展开</span>
`;
expandBtn.onclick = togglePanel;
// 创建搜索引擎项
const currentEngines = GM_getValue("search_engines") || defaultEngines;
const engineMarks = currentEngines.split("-");
const fragment = document.createDocumentFragment();
for (const mark of engineMarks) {
const engine = searchEngines.find(e => e.mark === mark);
if (!engine) continue;
const item = document.createElement("div");
item.className = "engine-item";
item.textContent = engine.name;
item.dataset.url = engine.searchUrl + (getKeyword() || "");
item.style.cursor = "pointer";
// 添加点击事件处理器
item.addEventListener("click", function (e) {
e.preventDefault();
// 如果是当前激活的搜索引擎,不执行任何操作
if (this.classList.contains("active")) {
return;
}
// 否则导航到目标URL
window.location.href = this.dataset.url;
});
// 标记当前使用的搜索引擎
if (window.location.href.match(engine.matchUrl)) {
item.classList.add("active");
}
fragment.appendChild(item);
}
// 组装元素
engineList.appendChild(fragment);
container.appendChild(engineList);
container.appendChild(expandBtn);
document.body.prepend(container);
// 创建但隐藏面板
createPanel();
}
// 创建侧边面板
function createPanel() {
const panel = document.createElement("div");
panel.id = "search-panel";
panel.className = "search-panel";
// 创建面板标题
const panelHeader = document.createElement("div");
panelHeader.className = "panel-header";
panelHeader.innerHTML = `
<h2>搜索工具箱</h2>
<button class="close-btn" aria-label="关闭面板">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
`;
panel.appendChild(panelHeader);
// 添加关闭按钮事件
panelHeader.querySelector('.close-btn').onclick = togglePanel;
// 创建面板内容容器
const panelContent = document.createElement("div");
panelContent.className = "panel-content";
panel.appendChild(panelContent);
// 创建面板内容
for (const section of panelOptions) {
const sectionEl = document.createElement("div");
sectionEl.className = "panel-section";
// 添加分类图标和标题
const iconMap = {
"常用站点": '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>',
"娱乐": '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>',
"翻译": '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path><line x1="4" y1="22" x2="4" y2="15"></line></svg>',
"网盘": '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>'
};
const titleEl = document.createElement("h3");
titleEl.innerHTML = `${iconMap[section.title] || ''} <span>${section.title}</span>`;
sectionEl.appendChild(titleEl);
const linkList = document.createElement("div");
linkList.className = "link-list";
for (const link of section.links) {
const linkEl = document.createElement("div");
linkEl.className = "panel-link";
linkEl.textContent = link.name;
linkEl.style.cursor = "pointer";
// 确定是否需要在链接后添加关键词
// 判断是否是网盘类别中的Google网盘、Dropbox或Mega网盘
const directAccessLinks = [
"https://drive.google.com/drive/home",
"https://www.dropbox.com/home",
"https://mega.nz/fm"
];
const isDirect = directAccessLinks.some(directUrl => link.url.startsWith(directUrl));
// 如果是直接访问链接则不添加关键词,否则添加
const url = isDirect ? link.url : link.url + (getKeyword() || "");
linkEl.dataset.url = url;
// 添加点击事件处理器 - 在新标签页中打开
linkEl.addEventListener("click", function () {
window.open(this.dataset.url, "_blank");
});
linkList.appendChild(linkEl);
}
sectionEl.appendChild(linkList);
panelContent.appendChild(sectionEl);
}
document.body.appendChild(panel);
}
// 切换面板显示/隐藏
function togglePanel() {
const panel = document.getElementById("search-panel");
if (panel.classList.contains("visible")) {
panel.classList.remove("visible");
} else {
panel.classList.add("visible");
}
}
// 添加样式
function addStyles() {
const css = `
/* 全局变量 */
:root {
--primary-color: #1a73e8;
--primary-light: rgba(26, 115, 232, 0.1);
--text-dark: #ffffff;
--text-light: #333333;
--text-hover-dark: #ffffff;
--text-hover-light: #555555;
--bg-hover-dark: rgba(255, 255, 255, 0.1);
--bg-hover-light: rgba(0, 0, 0, 0.05);
--border-dark: #333333;
--border-light: #eeeeee;
--item-bg-dark: #333333;
--item-bg-light: #f5f5f5;
--item-hover-dark: #3a3a3a;
--item-hover-light: #e9e9e9;
--header-height: 42px;
--radius-sm: 4px;
--radius-md: 8px;
--transition: all 0.2s ease;
--panel-bg-dark: #1a1a1a;
--panel-bg-light: #ffffff;
--header-bg-dark: #2e2e2e;
}
/* 搜索栏容器 */
.search-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: var(--header-height);
background-color: var(--header-bg-dark);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px;
z-index: 9999999;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* 搜索引擎列表 */
.engine-list {
display: flex;
gap: 8px;
overflow-x: auto;
white-space: nowrap;
padding: 0 8px;
scrollbar-width: none;
-ms-overflow-style: none;
margin-right: 8px;
flex: 1;
background-color: var(--header-bg-dark);
}
.engine-list::-webkit-scrollbar {
display: none;
}
/* 搜索引擎项 */
.engine-item {
color: var(--text-dark);
text-decoration: none;
font-size: 13px;
padding: 4px 12px;
border-radius: var(--radius-sm);
transition: var(--transition);
position: relative;
font-weight: 400;
letter-spacing: 0.2px;
line-height: 1.4;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
user-select: none;
}
.engine-item:hover {
color: var(--text-hover-dark);
background-color: var(--bg-hover-dark);
}
.engine-item.active {
color: #ffffff;
position: relative;
background-color: var(--primary-color);
font-weight: 500;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.engine-item.active:after {
content: none;
}
/* 展开按钮 */
.expand-btn {
margin-right: 30px;
background-color: rgba(255, 255, 255, 0.1);
color: var(--text-dark);
border: none;
padding: 6px 12px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 13px;
transition: var(--transition);
display: flex;
align-items: center;
gap: 6px;
height: 28px;
min-width: 68px;
}
.expand-btn svg {
width: 14px;
height: 14px;
}
.expand-btn:hover {
background-color: rgba(255, 255, 255, 0.15);
}
/* 侧边面板 */
.search-panel {
position: fixed;
top: var(--header-height);
right: -340px;
width: 340px;
height: calc(100vh - var(--header-height));
background-color: var(--panel-bg-dark);
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.2);
z-index: 9999998;
transition: right 0.25s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
display: flex;
flex-direction: column;
color: var(--text-dark);
}
.search-panel.visible {
right: 0;
margin-right: 20px;
border-radius: 8px 0 0 8px;
width: 320px;
}
/* 面板头部 */
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--border-dark);
}
.panel-header h2 {
margin: 0;
font-size: 16px;
font-weight: 500;
color: var(--text-dark);
}
/* 面板内容 */
.panel-content {
padding: 12px 16px;
overflow-y: auto;
flex: 1;
scrollbar-width: thin;
scrollbar-color: #555 transparent;
}
.panel-content::-webkit-scrollbar {
width: 4px;
}
.panel-content::-webkit-scrollbar-track {
background: transparent;
}
.panel-content::-webkit-scrollbar-thumb {
background-color: #555;
border-radius: 4px;
}
/* 面板部分 */
.panel-section {
margin-bottom: 20px;
}
.panel-section h3 {
font-size: 14px;
margin: 0 0 10px 0;
padding-bottom: 6px;
border-bottom: 1px solid var(--border-dark);
color: var(--text-dark);
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.panel-section h3 svg {
color: var(--primary-color);
opacity: 0.9;
}
/* 链接列表 */
.link-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
/* 面板链接 */
.panel-link {
display: inline-block;
color: var(--text-dark);
text-decoration: none;
background-color: var(--item-bg-dark);
padding: 5px 10px;
border-radius: var(--radius-sm);
font-size: 13px;
transition: var(--transition);
border: 1px solid rgba(255, 255, 255, 0.05);
cursor: pointer;
user-select: none;
}
.panel-link:hover {
background-color: var(--item-hover-dark);
color: var(--text-hover-dark);
border-color: rgba(255, 255, 255, 0.1);
}
/* 关闭按钮 */
.close-btn {
width: 28px;
height: 28px;
background: rgba(255, 255, 255, 0.1);
border: none;
color: var(--text-dark);
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
border-radius: 50%;
transition: var(--transition);
}
.close-btn:hover {
background-color: rgba(255, 255, 255, 0.2);
}
/* 页面内容调整 */
body {
margin-top: var(--header-height) !important;
}
/* 光明模式 */
@media (prefers-color-scheme: light) {
.search-container {
background-color: rgba(255, 255, 255, 0.97);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.engine-item {
color: var(--text-light);
}
.engine-item:hover {
color: var(--text-hover-light);
background-color: var(--bg-hover-light);
}
.engine-item.active {
color: #ffffff;
background-color: var(--primary-color);
}
.expand-btn {
background-color: rgba(0, 0, 0, 0.07);
color: var(--text-light);
}
.expand-btn:hover {
background-color: rgba(0, 0, 0, 0.12);
}
.search-panel {
background-color: var(--panel-bg-light);
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
}
.panel-header {
border-bottom: 1px solid var(--border-light);
}
.panel-header h2 {
color: var(--text-light);
}
.panel-section h3 {
border-bottom: 1px solid var(--border-light);
color: var(--text-light);
}
.panel-link {
background-color: var(--item-bg-light);
color: var(--text-light);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.panel-link:hover {
background-color: var(--item-hover-light);
color: var(--text-hover-light);
border-color: rgba(0, 0, 0, 0.1);
}
.close-btn {
background: rgba(0, 0, 0, 0.07);
color: var(--text-light);
}
.close-btn:hover {
background-color: rgba(0, 0, 0, 0.12);
}
}
/* 移动设备适配 */
@media (max-width: 768px) {
:root {
--header-height: 40px;
}
.search-container {
padding: 0 6px;
}
.engine-list {
gap: 4px;
padding: 0 4px;
}
.engine-item {
font-size: 12px;
padding: 4px 8px;
}
.expand-btn {
padding: 4px 8px;
font-size: 12px;
min-width: auto;
}
.expand-btn span {
display: none;
}
.search-panel {
width: 280px;
}
.search-panel.visible {
margin-right: 10px;
width: 260px;
}
.panel-header {
padding: 10px 12px;
}
.panel-content {
padding: 10px 12px;
}
.panel-link {
font-size: 12px;
padding: 4px 8px;
}
}
`;
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
}
// 初始化
(function () {
"use strict";
// 检查是否在搜索引擎页面
for (const engine of searchEngines) {
if (window.location.href.match(engine.matchUrl) && getKeyword()) {
if (!GM_getValue("search_engines")) {
GM_setValue("search_engines", defaultEngines);
}
// 页面加载完成后添加搜索栏
window.addEventListener("DOMContentLoaded", function () {
addStyles();
addSearchBar();
});
break;
}
}
})();