由於Google Search近期似乎又改動版面,原本使用的Google Search Sidebar UserScript已無法正常顯示,想說請AI幫我寫一個左側浮動篩選工具列比較快,如果您正好也有此需求,可以再把內容貼給AI,請他再依您的需求再改一改囉!
DEMO
程式碼
// ==UserScript==
// @name Google Search Left Sidebar Filters (zh-TW) - v2.1
// @namespace http://tampermonkey.net/
// @version 2.1
// @description 為 Google 搜尋結果頁面新增一個可浮動的左側篩選側邊欄。包含語言、地區(僅限台灣)、時間篩選功能。在瀏覽器寬度較小時自動隱藏。已排除圖片和購物頁面。所有文本均為台灣正體中文。此版本修正了篩選條件無法疊加的錯誤,確保多重篩選能同時作用。
// @author Gemini
// @match https://www.google.com/search?*
// @match https://www.google.com.tw/search?*
// @grant GM_addStyle
// ==/UserScript==
// 使用立即執行函數表達式 (IIFE) 包裹整個腳本,避免污染全域作用域
(function() {
'use strict'; // 啟用嚴格模式,有助於撰寫更安全的 JavaScript 程式碼
console.log('Google Search Left Sidebar Filters Userscript v2.1 啟動中...');
// --- 核心邏輯:頁面類型檢查與早期退出 ---
// 目的:根據使用者需求,在圖片 (udm=2) 和購物 (udm=28) 頁面不顯示側邊欄。
const currentUrl = new URL(window.location.href); // 取得當前頁面的 URL 物件
const udmParam = currentUrl.searchParams.get('udm'); // 從 URL 中獲取 'udm' 參數的值
if (udmParam === '2' || udmParam === '28') {
// 如果 udm 參數是 2 (圖片搜尋) 或 28 (購物搜尋),則打印訊息並終止腳本
console.log(`當前頁面偵測到 udm=${udmParam} 參數。側邊欄將不會顯示。`);
return; // 立即停止腳本的後續執行,不進行任何 DOM 操作或樣式注入
}
// --- 設定常量:側邊欄尺寸與響應式閾值 ---
// 這些值是根據使用者先前的指示進行微調的。
const SIDEBAR_WIDTH = '125px'; // 側邊欄的固定寬度。這是使用者指定的值。
// MIN_BROWSER_WIDTH_FOR_SIDEBAR:當瀏覽器窗口寬度小於此值時,側邊欄將被隱藏。
// 這是實現響應式設計的關鍵點,確保在小螢幕上內容不會被遮擋。
const MIN_BROWSER_WIDTH_FOR_SIDEBAR = '1300px'; // 側邊欄顯示所需的最小瀏覽器寬度。這是使用者指定的值。
// --- 樣式注入:使用 GM_addStyle 函數添加 CSS 規則 ---
// GM_addStyle 是 Tampermonkey 提供的 API,用於將 CSS 樣式動態注入到頁面中。
// 這樣做的好處是樣式可以被腳本控制,並且不會影響原始頁面的 CSS 文件。
GM_addStyle(`
/* #my-custom-sidebar:側邊欄容器的整體樣式 */
#my-custom-sidebar {
width: ${SIDEBAR_WIDTH}; /* 設定側邊欄的寬度,從上方常量獲取 */
padding: 10px; /* 內部填充,使內容不緊貼邊緣 */
box-sizing: border-box; /* 設置為 border-box 確保 padding 不會增加寬度 */
border-right: 1px solid #dadce0; /* 右側邊框,視覺上區分側邊欄和主要內容 */
background-color: #f8f9fa; /* 背景顏色,Google 介面常用的淺灰色 */
position: fixed; /* 固定定位,使其在滾動時保持可見 */
left: 0; /* 固定在頁面的最左邊 */
top: 140px; /* 距離頁面頂部的距離。這是使用者確認的最終位置。 */
/* 最大高度:計算為視口高度減去頂部距離和底部預留空間。
這樣可以讓側邊欄在內容過多時顯示滾動條,而不是溢出。
100vh 是視口高度,150px = top (140px) + 底部預留空間 (10px) */
max-height: calc(100vh - 150px);
overflow-y: auto; /* 垂直方向內容溢出時顯示滾動條 */
overflow-x: hidden; /* 水平方向內容溢出時隱藏 */
z-index: 9999; /* 確保側邊欄位於其他元素之上 */
font-family: 'Google Sans', 'Roboto', Arial, sans-serif; /* 字體樣式,嘗試與 Google 官方字體保持一致 */
box-shadow: 2px 0 5px rgba(0,0,0,0.1); /* 輕微的陰影,增加立體感 */
display: none; /* 預設隱藏側邊欄,等待媒體查詢來顯示 */
}
/* 媒體查詢:控制側邊欄的響應式顯示/隱藏 */
/* 目的:當瀏覽器窗口寬度足夠大時才顯示側邊欄。 */
@media screen and (min-width: ${MIN_BROWSER_WIDTH_FOR_SIDEBAR}) {
#my-custom-sidebar {
display: block; /* 當螢幕寬度達到或超過設定閾值時,顯示側邊欄 */
}
}
/* #my-custom-sidebar h3:側邊欄內每個篩選區塊標題(如「語言」、「國家/地區」)的樣式 */
#my-custom-sidebar h3 {
font-size: 15px; /* 標題字體大小 */
color: #5f6368; /* 標題文字顏色 */
/* margin-top: 調整區塊之間的距離。
這是為了解決使用者「3 個區塊間增加少許距離」的需求。
對非第一個 h3 應用此值,使其與前一個區塊拉開距離。 */
margin-top: 20px;
/* margin-bottom: 調整標題與下方選項的距離。
這是為了解決使用者「區塊內標題跟選項減少距離」的需求。 */
margin-bottom: 4px;
padding-left: 0px; /* 確保左側沒有額外填充 */
font-weight: normal; /* 標題字體不加粗,與 Google 介面風格一致 */
}
/* #my-custom-sidebar .sidebar-section:first-child h3:針對第一個篩選區塊標題的特殊樣式 */
/* 目的:讓第一個區塊的標題(通常是「語言」)與側邊欄頂部的距離較小。
這是為了解決使用者「藍框標示的空白」減少的需求,同時不影響後續區塊的間距。 */
#my-custom-sidebar .sidebar-section:first-child h3 {
margin-top: 5px; /* 第一個標題距離側邊欄頂部較小 */
}
/* #my-custom-sidebar a:側邊欄內每個篩選選項連結的樣式 */
#my-custom-sidebar a {
display: block; /* 讓連結佔據整行,便於點擊 */
padding: 4px 0px; /* 上下填充,增加點擊區域和視覺空間 */
color: #1a0dab; /* 連結的預設顏色 */
text-decoration: none; /* 移除下劃線 */
font-size: 13px; /* 字體大小 */
border-radius: 4px; /* 輕微的圓角邊框,使選中效果更柔和 */
white-space: nowrap; /* 防止文本換行 */
overflow: hidden; /* 隱藏溢出內容 */
text-overflow: ellipsis; /* 溢出內容顯示省略號 */
}
/* #my-custom-sidebar a:hover:連結滑鼠懸停時的樣式 */
#my-custom-sidebar a:hover {
background-color: #e8f0fe; /* 背景色變淺藍色 */
color: #1a0dab; /* 文字顏色不變 */
}
/* #my-custom-sidebar a.selected:當前選中篩選連結的樣式 */
#my-custom-sidebar a.selected {
background-color: #e8f0fe; /* 選中時的背景色 */
font-weight: bold; /* 字體加粗,明確標示選中狀態 */
color: #1a0dab; /* 文字顏色不變 */
}
/* html, body:確保頁面整體沒有額外邊距,防止排版問題 */
html, body {
margin: 0 !important; /* 強制移除外部邊距 */
padding: 0 !important; /* 強制移除內部填充 */
min-width: 0 !important; /* 確保沒有最小寬度限制,避免水平滾動條 */
overflow-x: hidden !important; /* 隱藏水平滾動條,避免因側邊欄導致頁面寬度溢出 */
}
`);
console.log('自訂 CSS 樣式已注入 (v2.1)。');
// --- 函數:創建並填充側邊欄內容 ---
function createAndPopulateSidebar() {
console.log('嘗試創建並填充側邊欄內容 (v2.1)。');
// 檢查當前頁面是否為 Google 搜尋結果頁面所需的關鍵 DOM 元素是否存在
// 如果不存在,則表示不在預期的頁面,直接返回
if (!document.getElementById('search') && !document.getElementById('rcnt')) {
console.log('未偵測到 Google 搜尋結果頁面關鍵元素或不在搜尋頁面。退出 createAndPopulateSidebar。');
return;
}
// 獲取或創建側邊欄容器
let sidebar = document.getElementById('my-custom-sidebar');
if (!sidebar) {
// 如果側邊欄不存在,則創建一個新的 div 元素並設置 ID
sidebar = document.createElement('div');
sidebar.id = 'my-custom-sidebar';
// 將側邊欄添加到 <body> 的開頭,確保其在 DOM 中的位置。
// prepend 比 append 更適合浮動元素,通常能減少與頁面原有佈局的衝突。
document.body.prepend(sidebar);
console.log('側邊欄容器已創建並插入到頁面。');
} else {
// 如果側邊欄已經存在,則清空其內容,以便重新生成(適用於 SPA 頁面導航)
sidebar.innerHTML = '';
console.log('側邊欄容器已存在,已清除內容準備重新填充。');
}
// 再次從 URL 中獲取當前搜尋關鍵字 'q'
// 時間篩選選項只有在有搜尋關鍵字時才顯示
const currentQuery = currentUrl.searchParams.get('q');
// 輔助函數:向側邊欄添加一個篩選區塊 (如「語言」、「國家/地區」、「時間」)
function addSectionToSidebar(title, filters) {
const sectionDiv = document.createElement('div');
sectionDiv.className = 'sidebar-section'; // 為每個篩選區塊添加一個類名,方便 CSS 定義
const h3 = document.createElement('h3');
h3.textContent = title; // 設置區塊標題
sectionDiv.appendChild(h3); // 將標題添加到區塊 div 中
// 遍歷每個篩選選項,創建對應的連結
filters.forEach(filter => {
const link = document.createElement('a');
// 複製當前 URL,以便修改參數而不影響原始頁面狀態
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search); // 獲取 URL 的查詢參數
// --- 核心修正:只針對當前篩選類型修改或刪除參數,保留其他篩選條件 ---
if (filter.type === 'lang') {
if (filter.param) {
params.set('lr', filter.param); // 設定語言參數
} else { // 如果 param 為空,表示「不限語言」,則移除該參數
params.delete('lr');
}
} else if (filter.type === 'country') {
if (filter.param) {
params.set('cr', filter.param); // 設定國家/地區參數
} else { // 如果 param 為空,表示「不限國家/地區」,則移除該參數
params.delete('cr');
}
} else if (filter.type === 'time') {
if (filter.param) {
params.set('tbs', 'qdr:' + filter.param); // 設定時間參數 (格式為 qdr:參數值)
// 確保清除可能存在的其他時間相關參數,例如 as_qdr,以避免衝突
params.delete('as_qdr');
} else { // 如果 param 為空,表示「不限時間」,則移除該參數
params.delete('tbs');
params.delete('as_qdr'); // 同時移除 as_qdr
}
}
// --- 修正結束 ---
url.search = params.toString(); // 將修改後的參數重新設置回 URL
link.href = url.toString(); // 設置連結的 href 屬性
link.textContent = filter.text; // 設置連結顯示的文本
// 檢查當前篩選選項是否被選中,以便添加 'selected' 類名高亮顯示
const currentLr = currentUrl.searchParams.get('lr');
const currentCr = currentUrl.searchParams.get('cr');
const currentTbs = currentUrl.searchParams.get('tbs');
let isSelected = false;
if (filter.type === 'lang') {
// 如果當前語言參數與篩選選項匹配,或當前沒有語言參數且篩選選項為「不限語言」
isSelected = (currentLr === filter.param) || (!currentLr && filter.param === '');
} else if (filter.type === 'country') {
// 如果當前國家參數與篩選選項匹配,或當前沒有國家參數且篩選選項為「不限國家/地區」
isSelected = (currentCr === filter.param) || (!currentCr && filter.param === '');
} else if (filter.type === 'time') {
// 如果當前時間參數與篩選選項匹配,或當前沒有時間參數且篩選選項為「不限時間」
isSelected = (currentTbs === ('qdr:' + filter.param)) || (!currentTbs && filter.param === '');
}
if (isSelected) {
link.classList.add('selected'); // 如果被選中,添加 'selected' 類名
}
sectionDiv.appendChild(link); // 將連結添加到區塊 div 中
});
sidebar.appendChild(sectionDiv); // 將完整的篩選區塊添加到側邊欄中
console.log(`已添加篩選區塊: ${title}。篩選器數量: ${filters.length}`);
}
// --- 篩選選項的定義 ---
// 1. 語言篩選
addSectionToSidebar('語言', [
{ text: '不限語言', type: 'lang', param: '' }, // param 為空表示不設定語言參數
{ text: '所有中文網頁', type: 'lang', param: 'lang_zh-CN|lang_zh-TW' },
{ text: '繁體中文網頁', type: 'lang', param: 'lang_zh-TW' },
{ text: '英文網頁', type: 'lang', param: 'lang_en' }
]);
// 2. 國家/地區篩選
addSectionToSidebar('國家/地區', [
{ text: '不限國家/地區', type: 'country', param: '' }, // param 為空表示不設定國家參數
{ text: '台灣', type: 'country', param: 'countryTW' } // 台灣的國家代碼
]);
// 3. 時間篩選 (只有有搜尋關鍵字時才顯示)
// 目的:避免在沒有搜尋關鍵字時顯示時間篩選,因為那通常無意義。
if (currentQuery) { // 檢查是否有搜尋關鍵字
addSectionToSidebar('時間', [
{ text: '不限時間', type: 'time', param: '' }, // param 為空表示不設定時間參數
{ text: '過去 1 小時', type: 'time', param: 'h' },
{ text: '過去 24 小時', type: 'time', param: 'd' },
{ text: '過去 1 週', type: 'time', param: 'w' },
{ text: '過去 1 個月', type: 'time', param: 'm' },
{ text: '過去 1 年', type: 'time', param: 'y' }
]);
} else {
console.log('未偵測到搜尋關鍵字,跳過時間篩選區塊。');
}
console.log('側邊欄內容填充完成。');
}
// --- 腳本啟動與 DOM 載入時機控制 ---
// 目的:確保在頁面完全載入或關鍵 DOM 元素可用後才執行側邊欄的創建,避免操作不存在的元素。
// 使用 setTimeout 加上少量延遲,讓 Google 自己的頁面元素先渲染完成,減少衝突。
if (document.readyState === 'loading') {
// 如果 DOM 仍在載入中,則在 DOMContentLoaded 事件觸發後執行
window.addEventListener('DOMContentLoaded', () => {
setTimeout(createAndPopulateSidebar, 1500); // 延遲 1.5 秒執行
});
} else {
// 如果 DOM 已經載入完成,則直接延遲執行
setTimeout(createAndPopulateSidebar, 1500); // 延遲 1.5 秒執行
}
// --- MutationObserver:監聽 DOM 變化並重新創建側邊欄 ---
// 目的:Google 搜尋頁面是單頁應用程式 (SPA)。
// 當使用者在搜尋結果頁面內部導航 (例如點擊「新聞」、「影片」或使用上一頁/下一頁按鈕時),
// 頁面內容可能會動態更新而不觸發完整頁面重新載入。
// MutationObserver 監聽這些 DOM 變化,確保側邊欄在需要時重新生成和顯示。
const observer = new MutationObserver((mutationsList, observer) => {
let shouldRecreate = false; // 標誌變量,指示是否需要重新創建側邊欄
for (const mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// 檢查是否有新的節點被添加到 DOM 中
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE && (node.id === 'search' || node.id === 'rcnt' || node.id === 'main' || node.classList.contains('GyAeaf') || node.id === 'hdtb')) {
// 如果新增的節點是 Google 搜尋頁面的主要內容區域,則標記為需要重新創建
shouldRecreate = true;
break;
}
}
} else if (mutation.type === 'attributes' && (mutation.target.id === 'search' || mutation.target.id === 'rcnt' || mutation.target.id === 'hdtb-msb' || mutation.target.classList.contains('GyAeaf') || mutation.target.id === 'hdtb')) {
// 檢查主要內容區域的屬性是否有變化(例如,動態顯示/隱藏)
shouldRecreate = true;
}
if (shouldRecreate) break; // 如果已確定需要重新創建,則跳出循環
}
// 如果側邊欄不存在,或者檢測到需要重新創建的 DOM 變化
if (!document.getElementById('my-custom-sidebar') || shouldRecreate) {
console.log('偵測到 DOM 變化或側邊欄丟失。正在重新創建側邊欄...');
setTimeout(createAndPopulateSidebar, 200); // 延遲 200ms 後重新創建側邊欄
}
});
// 配置並啟動 MutationObserver,監聽 <body> 元素及其子樹的變化
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
console.log('MutationObserver 已啟動,用於監聽 DOM 變化。');
})();
如果您沒用過UserScript,可以先於瀏覽器安裝 Violentmonkey 或 Tampermonkey 延伸套件後,建立一個新的腳本,將程式碼貼入即可使用。