// ==UserScript==
// @name 功能增强型必应美化脚本 (Enhanced Bing Beautifier) - Refactored V6.3.1
// @namespace http://tampermonkey.net/
// @version 6.3.1
// @description V6.3.1: 修正时间筛选未读取最新查询词的问题 (Fix time filter not reading latest query)。V6.3: 修正 PJAX/SPA 翻页导致脚本失效的问题。功能:结构化重构、性能优化、专业粒子库点击动效、卡片重排、强力搜索增强、站点屏蔽持久化。
// @author Gemini & WJH
// @match https://www.bing.com/search*
// @match https://cn.bing.com/search*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-start
// ==/UserScript==
(() => {
'use strict';
// ---------- 常量与配置 ----------
const CFG = {
STORAGE_KEYS: {
BLOCKLIST: 'bing_blocked_sites_v3',
},
UI: {
FAB_ID: 'search-enhancer-fab',
PANEL_ID: 'search-enhancer-panel',
BLOCKLIST_MODAL_ID: 'blocklist-modal',
BLOCKLIST_TEXTAREA_ID: 'blocklist-textarea',
},
SELECTORS: {
MAIN_CONTENT: '#b_content',
RIGHT_RAIL: '#b_context',
RESULTS_LIST: '#b_results',
// 原始定义(含 :has),并提供降级选择器
CENTER_HAS: 'li.b_ans:has(#TechHelpInfoACFCard)',
CENTER_FALLBACK_PARENT: 'li.b_ans',
CENTER_FALLBACK_CHILD: '#TechHelpInfoACFCard',
MOVE_BOTTOM: 'li.b_vidAns',
SIDEBAR_HAS: 'li.b_ans:has(.df_alaskcarousel)',
SIDEBAR_FALLBACK_PARENT: 'li.b_ans',
SIDEBAR_FALLBACK_CHILD: '.df_alaskcarousel',
REMOVE: [
'li.b_ans:has(.b_rrsr)', '.b_msg.b_canvas', 'li.b_ans:has(#inline_rs)', 'li.b_ans:has(#brsv3)',
'li.b_ans:has(div.richrswrapper)', '#b_topw', '.b_ad', '.b_ads', '.sb_adsWv2', '.b_adTop',
'li.b_ans.b_ad', '.ad_sect_line', '.msan_ads_container', '#df_ad', '#b_pole', '.b_expandableAnswer',
'#brsv3', '#b_footer', '.b_inline_ajax_rs', '#inline_rs', '.richrswrapper', '.b_mrs',
'#monica-content-root', '#monica-search-enhance', '#doubao-ai-assistant', '#ciciai-shadow-container',
'doubao-ai-csui', 'kimi-web-extension', '#__infoflow_commercial', '#MAXAI_SEARCH_WITH_AI_ROOT_ID',
'cici-ai-csui', 'max-ai-minimum-app', 'use-chat-gpt-ai-content-menu', 'use-chat-gpt-ai',
'li.b_ans:has(.rqnaacfacc)',
'#adstop_gradiant_separator', // [V6.1 新增] 屏蔽广告分隔线
],
RESULT_BLOCKS: '#b_results li.b_algo, #b_context li.b_ans',
CITE: 'cite',
ENHANCER_BTN: '.enhancer-btn',
QUERY_INPUT: '#sb_form_q',
},
PRESETS: {
SITES: [
{ name: '知乎', domain: 'zhihu.com' },
{ name: '哔哩哔哩', domain: 'bilibili.com' },
{ name: 'GitHub', domain: 'github.com' },
{ name: '维基百科', domain: 'wikipedia.org' },
{ name: '豆瓣', domain: 'douban.com' },
{ name: '微博', domain: 'weibo.com' },
{ name: '少数派', domain: 'sspai.com' },
{ name: 'CSDN', domain: 'csdn.net' },
{ name: 'V2EX', domain: 'v2ex.com' },
{ name: 'Stack Overflow', domain: 'stackoverflow.com' },
{ name: 'Reddit', domain: 'reddit.com' },
],
FILETYPES: ['PDF', 'DOCX', 'PPTX', 'XLSX'],
},
ADS_ICON_MARKERS: [
'/9j/4AAQSkZJRgABAQ',
'iVBORw0KGgoAAAANSUhEUgAAABsAAAALCAYAAACOAvbO'
],
};
// ---------- 工具函数 ----------
const CSS_HAS_SUPPORTED = typeof CSS !== 'undefined' && CSS.supports && CSS.supports('selector(:has(*))');
const debounce = (fn, wait = 150) => {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn.apply(null, args), wait);
};
};
const once = (fn) => {
let called = false;
return (...args) => {
if (called) return;
called = true;
fn(...args);
};
};
const qs = (root, sel) => root.querySelector(sel);
const qsa = (root, sel) => root.querySelectorAll(sel);
const domReady = (cb) => {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', cb, { once: true });
} else {
cb();
}
};
// ---------- 存储封装(异步) ----------
const store = {
async get(key, fallback = '[]') {
try {
const v = await GM_getValue(key, fallback);
return typeof v === 'string' ? JSON.parse(v) : v;
} catch {
return JSON.parse(fallback);
}
},
async set(key, value) {
await GM_setValue(key, JSON.stringify(value));
},
};
// ---------- 动效抽象层(自动检测专业粒子库,提供回退) ----------
const Effects = (() => {
const injectRippleCSS = once(() => {
GM_addStyle(`
.click-ripple {
position: absolute; border-radius: 50%;
transform: translate(-50%, -50%); pointer-events: none;
width: 8px; height: 8px;
background: radial-gradient(circle, rgba(255,255,255,0.9) 0%, rgba(0,120,212,0.2) 70%, rgba(0,120,212,0) 100%);
animation: ripple-anim 600ms ease-out forwards;
z-index: 10000;
}
@keyframes ripple-anim {
to { opacity: 0; transform: translate(-50%, -50%) scale(16); }
}
`);
});
function rippleAt(x, y) {
injectRippleCSS();
const el = document.createElement('div');
el.className = 'click-ripple';
el.style.left = x + 'px';
el.style.top = y + 'px';
document.body.appendChild(el);
setTimeout(() => el.remove(), 620);
}
function confettiAt(x, y, opts = {}) {
const g = window;
const hasCanvasConfetti = typeof g.confetti === 'function' && !!g.confetti;
if (hasCanvasConfetti) {
const canvas = document.createElement('canvas');
canvas.style.cssText = 'position:fixed;pointer-events:none;inset:0;z-index:10001;';
document.body.appendChild(canvas);
const my = g.confetti.create(canvas, { resize: true, useWorker: true });
my({
particleCount: opts.particleCount ?? 120,
spread: opts.spread ?? 75,
startVelocity: opts.startVelocity ?? 55,
gravity: opts.gravity ?? 0.9,
ticks: opts.ticks ?? 200,
origin: {
x: x / window.innerWidth,
y: y / window.innerHeight
},
scalar: opts.scalar ?? 1.0,
shapes: opts.shapes,
colors: opts.colors,
});
setTimeout(() => canvas.remove(), 1200);
} else {
rippleAt(x, y);
}
}
return {
burst: confettiAt,
ripple: rippleAt,
};
})();
// ---------- 样式注入 ----------
function injectStyles() {
const S = CFG.SELECTORS;
GM_addStyle(`
:root {
--glass-bg: rgba(255, 255, 255, 0.6);
--glass-bg-dark: rgba(25, 25, 40, 0.7);
--glass-border: rgba(255, 255, 255, 0.25);
--glass-border-dark: rgba(255, 255, 255, 0.15);
--glass-shadow: 0 6px 15px rgba(0,0,0,0.15);
--card-radius: 16px;
--panel-radius: 12px;
--accent: #0078d4;
}
body {
background: url('https://raw.githubusercontent.com/WJH-makers/markdown_photos/main/images/sea.png') center/cover fixed no-repeat !important;
}
#b_header, .b_scopebar, ${S.MAIN_CONTENT} { background: transparent !important; }
.b_header_bg, #b_header_bg { display: none !important; }
${CFG.SELECTORS.REMOVE.join(',')} { display: none !important; }
#gemini_centered_container {
width: 100%; display: flex; flex-direction: column; align-items: center; margin-bottom: 20px;
}
#gemini_centered_container > li[data-centered="true"] {
width: 70%; max-width: 800px; margin: 0 0 20px 0 !important; padding: 20px !important;
border-radius: var(--card-radius);
background: var(--glass-bg); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
border: 1px solid var(--glass-border); box-shadow: var(--glass-shadow) !important;
}
body.b_dark #gemini_centered_container > li[data-centered="true"] {
background: var(--glass-bg-dark); border-color: var(--glass-border-dark);
}
#gemini_centered_container .thml_cnt, #gemini_centered_container #TechHelpInfoACFCard {
background: transparent !important; border: none !important; box-shadow: none !important; padding: 0 !important;
}
${S.MAIN_CONTENT} > li[data-relocated-bottom="true"] {
width: 90%; max-width: 1200px; margin: 40px auto 20px auto !important; padding: 20px !important;
border-radius: var(--card-radius);
background: var(--glass-bg); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
border: 1px solid var(--glass-border); box-shadow: var(--glass-shadow) !important;
}
body.b_dark ${S.MAIN_CONTENT} > li[data-relocated-bottom="true"] {
background: var(--glass-bg-dark); border-color: var(--glass-border-dark);
}
${S.MAIN_CONTENT} > li.b_vidAns .mc_vtvc { background: transparent !important; box-shadow: none !important; }
${CFG.SELECTORS.RIGHT_RAIL} > li[data-relocated="true"] {
display: block !important; width: 100%; box-sizing: border-box; margin: 0 0 20px 0; padding: 16px !important;
border-radius: 8px; background: var(--glass-bg);
backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
border: 1px solid var(--glass-border); box-shadow: 0 4px 12px rgba(0,0,0,0.1) !important;
}
body.b_dark ${CFG.SELECTORS.RIGHT_RAIL} > li[data-relocated="true"] {
background: var(--glass-bg-dark); border-color: var(--glass-border-dark);
}
${S.RESULTS_LIST} > li.b_algo:not(.b_nwsAns),
${S.RESULTS_LIST} > li.b_ans:not([data-relocated="true"]):not([data-centered="true"]),
${CFG.SELECTORS.RIGHT_RAIL} .b_ans:not([data-relocated="true"]) {
background-color: rgba(255,255,255,0.55) !important;
backdrop-filter: blur(16px) !important; -webkit-backdrop-filter: blur(16px) !important;
border: 1px solid var(--glass-border) !important; border-radius: var(--card-radius) !important;
padding: 20px !important; margin-bottom: 20px !important; box-shadow: 0 4px 12px rgba(0,0,0,0.1) !important;
transition: transform .25s ease, box-shadow .25s ease, background-color .25s ease;
}
${S.RESULTS_LIST} > li.b_algo:not(.b_nwsAns):hover,
${S.RESULTS_LIST} > li.b_ans:not([data-relocated="true"]):not([data-centered="true"]):hover,
${CFG.SELECTORS.RIGHT_RAIL} .b_ans:not([data-relocated="true"]):hover {
transform: translateY(-4px);
box-shadow: 0 10px 20px rgba(0,0,0,0.15) !important;
background-color: rgba(255,255,255,0.7) !important;
}
body.b_dark ${S.RESULTS_LIST} > li.b_algo:not(.b_nwsAns),
body.b_dark ${S.RESULTS_LIST} > li.b_ans:not([data-relocated="true"]):not([data-centered="true"]),
body.b_dark ${CFG.SELECTORS.RIGHT_RAIL} .b_ans:not([data-relocated="true"]) {
background: var(--glass-bg-dark) !important; border-color: var(--glass-border-dark) !important;
}
body.b_dark ${S.RESULTS_LIST} > li.b_algo:not(.b_nwsAns):hover,
body.b_dark ${S.RESULTS_LIST} > li.b_ans:not([data-relocated="true"]):not([data-centered="true"]):hover,
body.b_dark ${CFG.SELECTORS.RIGHT_RAIL} .b_ans:not([data-relocated="true"]):hover {
background: rgba(25,25,40,0.75) !important;
}
${S.RESULTS_LIST} > li, ${CFG.SELECTORS.RIGHT_RAIL} > li { border: none !important; box-shadow: none !important; }
.b_algo h2 a { color: #002D62 !important; }
body.b_dark .b_algo h2 a { color: #99ccff !important; }
.b_caption p, .b_caption .b_lineclamp3 { color: #333 !important; }
body.b_dark .b_caption p, body.b_dark .b_caption .b_lineclamp3 { color: #ccc !important; }
cite { color: #006400 !important; font-style: normal; }
body.b_dark cite { color: #8fbc8f !important; }
#search-enhancer-fab {
position: fixed; top: 120px; right: 20px; z-index: 9999; width: 50px; height: 50px;
background: rgba(0,120,212,0.85); color: #fff; border-radius: 50%;
text-align: center; line-height: 50px; font-size: 22px; cursor: pointer;
backdrop-filter: blur(10px); box-shadow: 0 4px 10px rgba(0,0,0,0.2); transition: transform .15s ease, box-shadow .2s ease;
}
#search-enhancer-fab:hover { transform: scale(1.08); box-shadow: 0 8px 24px rgba(0,0,0,0.25); }
#search-enhancer-panel {
position: fixed; top: 180px; right: 20px; z-index: 9998; width: 320px; padding: 14px;
background: rgba(240,240,240,0.7); backdrop-filter: blur(15px); border-radius: var(--panel-radius);
box-shadow: 0 4px 20px rgba(0,0,0,0.2); display: none; border: 1px solid rgba(255,255,255,0.4);
}
body.b_dark #search-enhancer-panel { background: rgba(30,30,50,0.7); border: 1px solid rgba(255,255,255,0.1); }
.enhancer-group { margin-bottom: 14px; }
.enhancer-group h3 {
font-size: 14px; margin: 0 0 8px 0; border-bottom: 1px solid #ccc; padding-bottom: 4px; color: #333;
}
body.b_dark .enhancer-group h3 { color: #ddd; border-bottom-color: #555; }
.enhancer-btn-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.enhancer-btn {
padding: 6px 10px; border-radius: 8px; border: none; cursor: pointer; background: var(--accent);
color: #fff; transition: transform .06s ease, filter .2s ease;
}
.enhancer-btn:hover { filter: brightness(1.05); }
.enhancer-btn:active { transform: scale(0.97); }
.enhancer-group .custom-input-group { display: flex; gap: 8px; }
.enhancer-group .custom-input-group input {
flex-grow: 1; padding: 5px; border-radius: 6px; border: 1px solid #ccc;
}
.block-site-btn {
margin-left: 8px; cursor: pointer; color: #d9534f; font-weight: bold; font-size: 12px; transition: color .2s;
}
.block-site-btn:hover { text-decoration: underline; color: #c9302c; }
.block-site-btn.blocked { color: #28a745 !important; cursor: default; font-weight: normal; }
.block-site-btn.blocked:hover { text-decoration: none; }
#blocklist-modal {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10001; width: 500px; padding: 20px;
background: rgba(255,255,255,0.8); backdrop-filter: blur(20px); border-radius: 16px;
box-shadow: 0 8px 30px rgba(0,0,0,0.2); display: none; border: 1px solid rgba(255,255,255,0.5);
}
body.b_dark #blocklist-modal { background: rgba(40,40,60,0.8); border-color: rgba(255,255,255,0.2); }
#blocklist-modal h2, #blocklist-modal p { color: #333; margin-top: 0; }
body.b_dark #blocklist-modal h2, body.b_dark #blocklist-modal p { color: #eee; }
#blocklist-textarea { width: 95%; height: 200px; padding: 10px; border-radius: 8px; border: 1px solid #ccc; }
.blocklist-buttons { margin-top: 15px; text-align: right; }
`);
}
// ---------- DOM 操作:居中 / 底部 / 右栏 ----------
const Containers = {
ensureCenteredHost() {
if (!document.getElementById('gemini_centered_container')) {
const c = document.createElement('div');
c.id = 'gemini_centered_container';
const main = qs(document, CFG.SELECTORS.MAIN_CONTENT);
if (main) main.before(c);
}
return document.getElementById('gemini_centered_container');
},
relocateToCenter(root) {
const host = this.ensureCenteredHost();
if (!host) return;
if (CSS_HAS_SUPPORTED) {
root.querySelectorAll(CFG.SELECTORS.CENTER_HAS).forEach(el => {
if (!el.dataset.centered) {
el.dataset.centered = 'true';
host.appendChild(el);
}
});
} else {
root.querySelectorAll(CFG.SELECTORS.CENTER_FALLBACK_PARENT).forEach(li => {
if (li.dataset.centered) return;
if (li.querySelector(CFG.SELECTORS.CENTER_FALLBACK_CHILD)) {
li.dataset.centered = 'true';
host.appendChild(li);
}
});
}
},
relocateToEnd(root) {
const main = qs(document, CFG.SELECTORS.MAIN_CONTENT);
if (!main) return;
root.querySelectorAll(CFG.SELECTORS.MOVE_BOTTOM).forEach(el => {
if (!el.dataset.relocatedBottom) {
el.dataset.relocatedBottom = 'true';
main.appendChild(el);
}
});
},
relocateToSidebar(root) {
const rail = qs(document, CFG.SELECTORS.RIGHT_RAIL);
if (!rail) return;
rail.style.display = 'inline-block';
rail.style.verticalAlign = 'top';
if (CSS_HAS_SUPPORTED) {
root.querySelectorAll(CFG.SELECTORS.SIDEBAR_HAS).forEach(el => {
if (!el.dataset.relocated) {
el.dataset.relocated = 'true';
rail.prepend(el);
}
});
} else {
root.querySelectorAll(CFG.SELECTORS.SIDEBAR_FALLBACK_PARENT).forEach(li => {
if (li.dataset.relocated) return;
if (li.querySelector(CFG.SELECTORS.SIDEBAR_FALLBACK_CHILD)) {
li.dataset.relocated = 'true';
rail.prepend(li);
}
});
}
},
};
// ---------- 动态广告选择器探测 ----------
let cachedAdSelectors = [];
let scannedSheets = false;
function scanStylesheetsForAdMarkers() {
if (scannedSheets || CFG.ADS_ICON_MARKERS.length === 0) return;
const set = new Set();
for (const sheet of document.styleSheets) {
try {
const rules = sheet.cssRules;
if (!rules) continue;
for (const rule of rules) {
if (rule.style && rule.style.content) {
const content = rule.style.content;
for (const marker of CFG.ADS_ICON_MARKERS) {
if (content.includes(marker)) {
const sels = rule.selectorText.split(',').map(s => s.replace(/::(before|after)/gi, '').trim());
sels.forEach(s => set.add(s));
}
}
}
}
} catch { /* ignore CORS */ }
}
cachedAdSelectors = Array.from(set);
scannedSheets = true;
}
function applyDynamicAdBlocking(root) {
if (!cachedAdSelectors.length) return;
cachedAdSelectors.forEach(sel => {
try {
root.querySelectorAll(sel).forEach(el => {
const block = el.closest('li.b_algo, .b_ans');
if (block && block.style.display !== 'none') block.style.display = 'none';
});
} catch { /* ignore invalid selectors */ }
});
}
// ---------- 站点屏蔽 ----------
let blockedSites = [];
function domainFromCite(citeEl) {
if (!citeEl || !citeEl.textContent) return null;
const t = citeEl.textContent.trim();
const m = t.match(/^(?:https?:\/\/)?(?:www\.)?([^:\/\n?]+)/i);
return m ? m[1].toLowerCase() : null;
}
// [V6.2 修正]
function applyStaticBlocking(root) {
const resultItemSelector = 'li.b_algo:not(.b_nwsAns), li.b_ans:not(.b_nwsAns)';
const itemsInRoot = Array.from(root.querySelectorAll(resultItemSelector));
let allItems = itemsInRoot;
if (root.nodeType === 1 && root.matches && root.matches(resultItemSelector)) {
allItems.push(root);
}
[...new Set(allItems)]
.filter(item => item.closest('#b_results, #b_context'))
.forEach(result => {
const cite = result.querySelector(CFG.SELECTORS.CITE);
const d = domainFromCite(cite);
if (d && blockedSites.includes(d)) {
result.style.display = 'none';
}
});
}
function addBlockButtons(root) {
const resultItemSelector = 'li.b_algo:not(.b_nwsAns), li.b_ans:not(.b_nwsAns)';
const itemsInRoot = Array.from(root.querySelectorAll(resultItemSelector));
let allItems = itemsInRoot;
if (root.nodeType === 1 && root.matches && root.matches(resultItemSelector)) {
allItems.push(root);
}
[...new Set(allItems)]
.filter(item => item.closest('#b_results, #b_context'))
.forEach(result => {
if (result.querySelector('.block-site-btn')) return;
const cite = result.querySelector(CFG.SELECTORS.CITE);
const d = domainFromCite(cite);
if (!d || !cite || !cite.parentNode) return;
const btn = document.createElement('span');
btn.className = 'block-site-btn';
if (blockedSites.includes(d)) {
btn.textContent = '[已屏蔽]';
btn.classList.add('blocked');
} else {
btn.textContent = '[屏蔽]';
btn.title = `屏蔽 ${d}`;
btn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
btn.textContent = '[已屏蔽]';
btn.classList.add('blocked');
btn.title = '';
Effects.burst(e.clientX, e.clientY, { particleCount: 60, spread: 55, colors: ['#22c55e', '#16a34a', '#4ade80'] });
if (!blockedSites.includes(d)) {
blockedSites.push(d);
}
await store.set(CFG.STORAGE_KEYS.BLOCKLIST, [...new Set(blockedSites)]);
}, { once: true });
}
cite.parentNode.appendChild(btn);
});
}
// ---------- 搜索增强 UI ----------
function currentQuery() {
const q = qs(document, CFG.SELECTORS.QUERY_INPUT);
let s = q ? q.value : '';
return s.replace(/"/g, '').replace(/-\S+/g, '').replace(/site:\S+/g, '').replace(/filetype:\S+/g, '').trim();
}
// [V6.3.1 修正] 移除 gotoWithParam 函数
function doSearch(str) {
const params = new URLSearchParams(window.location.search);
params.set('q', str.trim());
window.location.search = params.toString();
}
function createUI() {
// [V6.3 Idempotency Check]
if (document.getElementById(CFG.UI.FAB_ID)) {
return; // UI already exists, do not recreate
}
// FAB
const fab = document.createElement('div');
fab.id = CFG.UI.FAB_ID;
fab.innerHTML = '🛠️';
fab.title = '打开/关闭搜索增强工具';
document.body.appendChild(fab);
// 面板
const panel = document.createElement('div');
panel.id = CFG.UI.PANEL_ID;
panel.innerHTML = `
<div class="enhancer-group">
<h3>搜索模式</h3>
<div class="enhancer-btn-grid">
<button class="enhancer-btn" data-op="quotes">"精确匹配"</button>
<button class="enhancer-btn" data-op="broaden">扩大范围</button>
</div>
</div>
<div class="enhancer-group">
<h3>时间范围</h3>
<div class="enhancer-btn-grid">
<button class="enhancer-btn" data-filter-param="qft" data-filter-value="+filterui:age-lt1440">最近24小时</button>
<button class="enhancer-btn" data-filter-param="qft" data-filter-value="+filterui:age-lt10080">最近一周</button>
<button class="enhancer-btn" data-filter-param="qft" data-filter-value="+filterui:age-lt43200">最近一月</button>
<button class="enhancer-btn" data-filter-param="qft" data-filter-value="+filterui:age-lt525600">最近一年</button>
</div>
</div>
<div class="enhancer-group">
<h3>站内搜索</h3>
<div class="enhancer-btn-grid" id="site-preset-grid"></div>
<div class="custom-input-group" style="margin-top: 8px;">
<input id="custom-site-input" type="text" placeholder="自定义域名...">
<button id="custom-site-btn" class="enhancer-btn" data-op="custom-site">搜索</button>
</div>
</div>
<div class="enhancer-group">
<h3>文件类型</h3>
<div class="enhancer-btn-grid" id="filetype-preset-grid"></div>
</div>
<div class="enhancer-group">
<h3>内容屏蔽</h3>
<button id="enhancer-blocklist-btn" class="enhancer-btn">🚫 管理屏蔽列表</button>
</div>
`;
document.body.appendChild(panel);
// 预置按钮填充
const siteGrid = document.getElementById('site-preset-grid');
CFG.PRESETS.SITES.forEach(site => {
const b = document.createElement('button');
b.className = 'enhancer-btn';
b.textContent = site.name;
b.dataset.op = 'site';
b.dataset.value = site.domain;
siteGrid.appendChild(b);
});
const fileGrid = document.getElementById('filetype-preset-grid');
CFG.PRESETS.FILETYPES.forEach(type => {
const b = document.createElement('button');
b.className = 'enhancer-btn';
b.textContent = type;
b.dataset.op = 'filetype';
b.dataset.value = type.toLowerCase();
fileGrid.appendChild(b);
});
// 屏蔽列表弹窗
const modal = document.createElement('div');
modal.id = CFG.UI.BLOCKLIST_MODAL_ID;
modal.innerHTML = `
<h2>管理网站屏蔽列表</h2>
<p>每行输入一个要屏蔽的域名 (例如 zhihu.com)</p>
<textarea id="${CFG.UI.BLOCKLIST_TEXTAREA_ID}"></textarea>
<div class="blocklist-buttons">
<button id="blocklist-save-btn" class="enhancer-btn">保存</button>
<button id="blocklist-cancel-btn" class="enhancer-btn" style="background-color:#777;">取消</button>
</div>
`;
document.body.appendChild(modal);
// 交互:FAB
fab.addEventListener('click', (e) => {
panel.style.display = panel.style.display === 'block' ? 'none' : 'block';
Effects.burst(e.clientX, e.clientY, { particleCount: 80, spread: 65 });
}, { passive: true });
// 交互:面板按钮(事件委托)
panel.addEventListener('click', (ev) => {
const target = ev.target.closest('.enhancer-btn');
if (!target) return;
const { op, value, filterParam, filterValue } = target.dataset;
const base = currentQuery();
const x = ev.clientX || (target.getBoundingClientRect().left + 16);
const y = ev.clientY || (target.getBoundingClientRect().top + 8);
// ---------- [V6.3.1 修正] 开始 ----------
Effects.burst(x, y, { particleCount: 36, spread: 45 });
let nq = base;
switch (op) {
case 'quotes': nq = `"${base}"`; break;
case 'broaden': nq = base; break;
case 'site': nq = `${base} site:${value}`; break;
case 'custom-site':
const domain = document.getElementById('custom-site-input').value.trim();
if (domain) nq = `${base} site:${domain}`;
break;
case 'filetype': nq = `${base} filetype:${value}`; break;
}
if (filterParam && filterValue) {
// 时间筛选:使用当前搜索框的查询(nq)并应用时间参数
const params = new URLSearchParams(window.location.search);
params.set('q', nq); // 关键:使用从输入框获取的 nq (即 base)
params.set(filterParam, filterValue);
window.location.search = params.toString();
} else if (nq !== base || op === 'broaden') {
// 站点/精确/扩大 搜索:
// (op === 'broaden' 修正了 "扩大范围" 按钮在 nq === base 时不触发的问题)
doSearch(nq);
}
// ---------- [V6.3.1 修正] 结束 ----------
}, { passive: true });
// 交互:屏蔽列表
document.getElementById('enhancer-blocklist-btn').addEventListener('click', async (e) => {
const list = await store.get(CFG.STORAGE_KEYS.BLOCKLIST, '[]');
document.getElementById(CFG.UI.BLOCKLIST_TEXTAREA_ID).value = list.join('\n');
modal.style.display = 'block';
Effects.burst(e.clientX, e.clientY, { particleCount: 24, spread: 35 });
}, { passive: true });
document.getElementById('blocklist-save-btn').addEventListener('click', async (e) => {
const lines = document.getElementById(CFG.UI.BLOCKLIST_TEXTAREA_ID).value
.split('\n').map(s => s.trim()).filter(Boolean);
await store.set(CFG.STORAGE_KEYS.BLOCKLIST, [...new Set(lines)]);
blockedSites = await store.get(CFG.STORAGE_KEYS.BLOCKLIST, '[]');
modal.style.display = 'none';
Effects.burst(e.clientX, e.clientY, { particleCount: 40, spread: 55, colors: ['#22c55e', '#16a34a'] });
applyStaticBlocking(document);
}, { passive: true });
document.getElementById('blocklist-cancel-btn').addEventListener('click', (e) => {
modal.style.display = 'none';
Effects.ripple(e.clientX, e.clientY);
}, { passive: true });
}
// ---------- 统一节点处理 ----------
function processNode(root) {
Containers.relocateToCenter(root);
Containers.relocateToEnd(root);
Containers.relocateToSidebar(root);
applyDynamicAdBlocking(root);
applyStaticBlocking(root);
addBlockButtons(root);
}
// ---------- [V6.3] 初始化与观察 ----------
let globalObserver = null; // [V6.3] Store observer globally
const mutationHandler = debounce((mutations) => {
for (const m of mutations) {
if (m.type === 'childList' && m.addedNodes.length) {
m.addedNodes.forEach(n => {
if (n.nodeType === Node.ELEMENT_NODE) {
processNode(n);
}
});
}
}
}, 120);
function initialRun() {
Containers.ensureCenteredHost();
scanStylesheetsForAdMarkers();
processNode(document.body);
}
function setupObserver() {
// [V6.3] Disconnect old observer if it exists
if (globalObserver) {
globalObserver.disconnect();
}
const targets = [
qs(document, CFG.SELECTORS.MAIN_CONTENT),
qs(document, CFG.SELECTORS.RIGHT_RAIL),
qs(document, CFG.SELECTORS.RESULTS_LIST),
].filter(Boolean);
if (targets.length > 0) {
globalObserver = new MutationObserver(mutationHandler);
targets.forEach(t => globalObserver.observe(t, { childList: true, subtree: true }));
}
}
// [V6.3] This function is called on initial load AND subsequent page navigations
function mainInit() {
createUI(); // Ensure UI exists (idempotent)
initialRun(); // Re-process the entire document body
setupObserver(); // (Re-)attach observer to new containers
}
// ---------- [V6.3] PJAX/SPA 导航处理 ----------
function onPageChange() {
// Wait for PJAX content to settle
setTimeout(() => {
mainInit();
}, 300); // Delay to allow new DOM to be inserted
}
(function setupNavListener() {
let lastHref = window.location.href;
// Monkey-patch history methods to fire a custom event
['pushState', 'replaceState'].forEach(fn => {
const orig = history[fn];
if (orig.patched) return; // Prevent double-patching
history[fn] = function() {
const ret = orig.apply(this, arguments);
window.dispatchEvent(new Event('locationchange_internal'));
return ret;
};
history[fn].patched = true; // Mark as patched
});
// Listen for browser back/forward
window.addEventListener('popstate', onPageChange);
// Listen for patched history changes
window.addEventListener('locationchange_internal', onPageChange);
// Fallback detector for cases where history patching fails
setInterval(() => {
if (window.location.href !== lastHref) {
lastHref = window.location.href;
onPageChange();
}
}, 250);
})();
// ---------- 启动 ----------
injectStyles();
// [V6.3] Modified initial startup
(async () => {
blockedSites = await store.get(CFG.STORAGE_KEYS.BLOCKLIST, '[]');
domReady(() => {
// Run the main init logic for the first time
mainInit();
});
})();
})();