// ==UserScript==
// @name 贴吧广告过滤登录去除
// @namespace noting
// @version 0.8.2
// @description 贴吧广告过滤,自动关闭登录弹窗,控制面板可拖动(优化版+分页快捷)
// @author Time (优化: 面板增强+分页功能)
// @match https://tieba.baidu.com/*
// @grant none
// @license MIT
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
// 配置参数
const options = {
expensionName: "贴吧广告过滤登录去除",
interval: 500,
development: false,
isDomRemove: false,
showAds: false,
matchingHost: "tieba.baidu.com",
loginDomId: "tiebaCustomPassLogin",
adTextClass: "label_text",
adText: "广告",
adIds: [
"pagelet_frs-aside/pagelet/fengchao_ad",
"banner_pb_customize",
"plat_recom_carousel"
],
adClasses: [
"fengchao-wrap-feed",
"head_banner",
"head_ad_pop",
"l_banner",
"j_couplet",
"card_banner",
"bus-top-activity-wrap"
],
page:{
container:"pb_list_pager", // 分页容器class
down:"下一页", // 下一页文本
up:"上一页" // 上一页文本
},
panelStyle: {
width: "240px",
minHeight: "240px", // 增加高度容纳分页功能
top: "120px",
left: "15px",
zIndex: 9999,
border: "1px solid #e5e7eb",
borderColor: "#e5e7eb",
bgColor: "#ffffff",
shadow: "0 4px 12px rgba(0,0,0,0.08)",
headerBg: "#2563eb",
headerColor: "#ffffff",
textColor: "#374151",
subTextColor: "#6b7280",
checkboxSize: "16px",
btnBg: "#f3f4f6",
btnHoverBg: "#e5e7eb",
btnRadius: "4px",
marginBottom: "12px",
padding: "12px"
}
};
// 工具函数 - 日志输出
const logger = {
log: (...args) => {
if (options.development) {
console.log(`[${options.expensionName}]`, ...args);
}
},
error: (...args) => {
console.error(`[${options.expensionName}]`, ...args);
}
};
// 广告检测与处理模块
const AdManager = {
detectedAds: new Set(),
isAdDetected: (element) => {
return AdManager.detectedAds.has(element);
},
addAdElement: (element) => {
if (!element || AdManager.isAdDetected(element)) return;
AdManager.detectedAds.add(element);
AdManager.handleAdElement(element);
const observer = new MutationObserver((mutations) => {
if (!document.body.contains(element)) {
AdManager.detectedAds.delete(element);
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
},
handleAdElement: (element) => {
if (!element || !element.style) return;
try {
if (options.showAds) {
element.style.display = "";
} else {
if (options.isDomRemove) {
element.remove();
AdManager.detectedAds.delete(element);
} else if (element.style.display !== "none") {
element.style.display = "none";
}
}
} catch (ex) {
logger.error("处理广告元素出错:", ex);
}
},
handleAllAds: () => {
AdManager.detectedAds.forEach(element => {
AdManager.handleAdElement(element);
});
}
};
// 检测器模块
const Detector = {
detectLogin: () => {
try {
const loginElement = document.getElementById(options.loginDomId);
if (loginElement) {
AdManager.addAdElement(loginElement);
}
} catch (ex) {
logger.error("检测登录弹窗出错:", ex);
}
},
detectBySelector: (type, selectors) => {
if (!selectors || !Array.isArray(selectors) || selectors.length === 0) return;
selectors.forEach(selector => {
try {
let elements;
if (type === 'id') {
const el = document.getElementById(selector);
if (el) elements = [el];
} else if (type === 'class') {
elements = document.getElementsByClassName(selector);
}
if (elements && elements.length > 0) {
Array.from(elements).forEach(el => AdManager.addAdElement(el));
}
} catch (ex) {
logger.error(`检测广告元素 ${selector} 出错:`, ex);
}
});
},
detectByAdText: () => {
try {
const adTextElements = document.getElementsByClassName(options.adTextClass);
Array.from(adTextElements).forEach(element => {
if (element.innerText.trim() === options.adText) {
let parent = element.closest('div, section, article');
if (parent) {
AdManager.addAdElement(parent);
}
}
});
} catch (ex) {
logger.error("检测文字标记广告出错:", ex);
}
},
detectAll: () => {
Detector.detectLogin();
Detector.detectBySelector('class', options.adClasses);
Detector.detectBySelector('id', options.adIds);
Detector.detectByAdText();
}
};
// 分页工具模块 - 处理上/下一页链接的查找与点击
const PaginationTool = {
/**
* 查找分页容器内的目标链接(上一页/下一页)
* @param {string} targetText - 目标文本(上一页/下一页)
* @returns {HTMLElement|null} - 找到的a标签,未找到则返回null
*/
findPageLink: (targetText) => {
try {
// 获取分页容器(class为pb_list_pager)
const pagerContainer = document.getElementsByClassName(options.page.container)[0];
if (!pagerContainer) {
logger.log("未找到分页容器(class: pb_list_pager)");
return null;
}
// 查找容器内所有a标签,筛选文本匹配的链接
const linkElements = pagerContainer.getElementsByTagName('a');
for (let link of linkElements) {
// 去除文本空格后匹配(避免换行/空格影响)
if (link.textContent.trim() === targetText) {
return link;
}
}
logger.log(`未找到文本为"${targetText}"的链接`);
return null;
} catch (ex) {
logger.error("查找分页链接出错:", ex);
return null;
}
},
/**
* 触发目标分页链接的点击(上一页/下一页)
* @param {string} targetText - 目标文本(上一页/下一页)
* @returns {boolean} - 点击成功返回true,失败返回false
*/
triggerPageClick: (targetText) => {
const targetLink = PaginationTool.findPageLink(targetText);
if (targetLink) {
// 模拟真实点击(兼容部分动态绑定事件的链接)
targetLink.click();
logger.log(`已触发"${targetText}"点击`);
return true;
}
return false;
},
/**
* 更新分页按钮状态(是否禁用)
* @param {HTMLButtonElement} prevBtn - 上一页按钮
* @param {HTMLButtonElement} nextBtn - 下一页按钮
*/
updateButtonStates: (prevBtn, nextBtn) => {
// 检查上一页链接是否存在
const hasPrev = PaginationTool.findPageLink(options.page.up) !== null;
prevBtn.disabled = !hasPrev;
prevBtn.style.opacity = hasPrev ? "1" : "0.6";
prevBtn.style.cursor = hasPrev ? "pointer" : "not-allowed";
// 检查下一页链接是否存在
const hasNext = PaginationTool.findPageLink(options.page.down) !== null;
nextBtn.disabled = !hasNext;
nextBtn.style.opacity = hasNext ? "1" : "0.6";
nextBtn.style.cursor = hasNext ? "pointer" : "not-allowed";
}
};
// 控制面板模块 - 包含分页功能
const ControlPanel = {
panelElement: null,
/**
* 创建控制面板,包含分页快捷操作
*/
createPanel: () => {
// 1. 创建面板容器
const panel = document.createElement("div");
panel.id = "ad-filter-panel";
panel.style.cssText = `
position: fixed;
width: ${options.panelStyle.width};
min-height: ${options.panelStyle.minHeight};
top: ${options.panelStyle.top};
left: ${options.panelStyle.left};
z-index: ${options.panelStyle.zIndex};
border: ${options.panelStyle.border};
border-radius: 6px;
background-color: ${options.panelStyle.bgColor};
box-shadow: ${options.panelStyle.shadow};
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
transition: box-shadow 0.2s ease;
`;
// 2. 面板头部(拖拽区)
const header = document.createElement("div");
header.className = "panel-header";
header.style.cssText = `
padding: ${options.panelStyle.padding};
background-color: ${options.panelStyle.headerBg};
color: ${options.panelStyle.headerColor};
font-size: 15px;
font-weight: 500;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
`;
header.innerHTML = `
${options.expensionName}
v${GM_info?.script?.version || '0.8.2'}
`;
// 3. 面板内容区
const content = document.createElement("div");
content.className = "panel-content";
content.style.cssText = `
padding: ${options.panelStyle.padding};
color: ${options.panelStyle.textColor};
font-size: 14px;
`;
// 3.1 广告显示/隐藏开关
const adToggleGroup = document.createElement("div");
adToggleGroup.style.cssText = `
display: flex;
align-items: center;
margin-bottom: ${options.panelStyle.marginBottom};
padding-bottom: ${options.panelStyle.marginBottom};
border-bottom: 1px solid #f3f4f6;
`;
const adToggle = document.createElement("input");
adToggle.type = "checkbox";
adToggle.id = "ad-toggle";
adToggle.checked = options.showAds;
adToggle.style.cssText = `
width: ${options.panelStyle.checkboxSize};
height: ${options.panelStyle.checkboxSize};
margin-right: 8px;
cursor: pointer;
`;
const adToggleLabel = document.createElement("label");
adToggleLabel.htmlFor = "ad-toggle";
adToggleLabel.textContent = "显示广告(默认隐藏)";
adToggleLabel.style.cursor = "pointer";
adToggleGroup.append(adToggle, adToggleLabel);
// 3.2 广告删除方式
const deleteModeGroup = document.createElement("div");
deleteModeGroup.style.cssText = `
display: flex;
align-items: center;
margin-bottom: ${options.panelStyle.marginBottom};
padding-bottom: ${options.panelStyle.marginBottom};
border-bottom: 1px solid #f3f4f6;
`;
const deleteModeToggle = document.createElement("input");
deleteModeToggle.type = "checkbox";
deleteModeToggle.id = "delete-mode-toggle";
deleteModeToggle.checked = options.isDomRemove;
deleteModeToggle.style.cssText = `
width: ${options.panelStyle.checkboxSize};
height: ${options.panelStyle.checkboxSize};
margin-right: 8px;
cursor: pointer;
`;
const deleteModeLabel = document.createElement("label");
deleteModeLabel.htmlFor = "delete-mode-toggle";
deleteModeLabel.innerHTML = `
彻底删除广告(默认隐藏)
注:彻底删除可减少页面占用,但可能影响部分页面布局
`;
deleteModeLabel.style.cursor = "pointer";
deleteModeGroup.append(deleteModeToggle, deleteModeLabel);
// 3.3 分页快捷操作区
const paginationGroup = document.createElement("div");
paginationGroup.style.cssText = `
margin-bottom: ${options.panelStyle.marginBottom};
padding-bottom: ${options.panelStyle.marginBottom};
border-bottom: 1px solid #f3f4f6;
`;
// 分页标题
const paginationTitle = document.createElement("div");
paginationTitle.style.cssText = `
font-size: 13px;
margin-bottom: 8px;
color: ${options.panelStyle.subTextColor};
`;
paginationTitle.textContent = "分页快捷操作";
// 上/下一页按钮容器
const pageBtnContainer = document.createElement("div");
pageBtnContainer.style.cssText = "display: flex; gap: 8px;";
// 上一页按钮
const prevPageBtn = document.createElement("button");
prevPageBtn.id = "prev-page-btn";
prevPageBtn.textContent = options.page.up;
prevPageBtn.style.cssText = `
flex: 1;
padding: 8px 0;
background-color: ${options.panelStyle.btnBg};
border: none;
border-radius: ${options.panelStyle.btnRadius};
color: ${options.panelStyle.textColor};
font-size: 13px;
cursor: pointer;
transition: background-color 0.2s ease;
`;
// 下一页按钮
const nextPageBtn = document.createElement("button");
nextPageBtn.id = "next-page-btn";
nextPageBtn.textContent = options.page.down;
nextPageBtn.style.cssText = `
flex: 1;
padding: 8px 0;
background-color: ${options.panelStyle.btnBg};
border: none;
border-radius: ${options.panelStyle.btnRadius};
color: ${options.panelStyle.textColor};
font-size: 13px;
cursor: pointer;
transition: background-color 0.2s ease;
`;
// 按钮hover效果
prevPageBtn.onmouseover = () => {
if (!prevPageBtn.disabled) {
prevPageBtn.style.backgroundColor = options.panelStyle.btnHoverBg;
}
};
prevPageBtn.onmouseout = () => {
if (!prevPageBtn.disabled) {
prevPageBtn.style.backgroundColor = options.panelStyle.btnBg;
}
};
nextPageBtn.onmouseover = () => {
if (!nextPageBtn.disabled) {
nextPageBtn.style.backgroundColor = options.panelStyle.btnHoverBg;
}
};
nextPageBtn.onmouseout = () => {
if (!nextPageBtn.disabled) {
nextPageBtn.style.backgroundColor = options.panelStyle.btnBg;
}
};
// 组装分页操作区
pageBtnContainer.append(prevPageBtn, nextPageBtn);
paginationGroup.append(paginationTitle, pageBtnContainer);
// 3.4 手动刷新广告检测
const refreshBtn = document.createElement("button");
refreshBtn.id = "refresh-ad-detect";
refreshBtn.textContent = "手动刷新广告检测";
refreshBtn.style.cssText = `
width: 100%;
padding: 8px 0;
background-color: ${options.panelStyle.btnBg};
border: none;
border-radius: ${options.panelStyle.btnRadius};
color: ${options.panelStyle.textColor};
font-size: 13px;
cursor: pointer;
transition: background-color 0.2s ease;
margin-bottom: ${options.panelStyle.marginBottom};
`;
refreshBtn.onmouseover = () => {
refreshBtn.style.backgroundColor = options.panelStyle.btnHoverBg;
};
refreshBtn.onmouseout = () => {
refreshBtn.style.backgroundColor = options.panelStyle.btnBg;
};
// 3.5 广告统计显示
const adCount = document.createElement("div");
adCount.id = "ad-count";
adCount.style.cssText = `
font-size: 12px;
color: ${options.panelStyle.subTextColor};
text-align: right;
padding-top: 8px;
border-top: 1px dashed #f3f4f6;
`;
adCount.textContent = `已过滤广告:${AdManager.detectedAds.size} 个`;
// 4. 组装内容区
content.append(adToggleGroup, deleteModeGroup, paginationGroup, refreshBtn, adCount);
panel.append(header, content);
document.body.appendChild(panel);
ControlPanel.panelElement = panel;
// 5. 绑定事件
ControlPanel.bindEvents(adToggle, deleteModeToggle, refreshBtn, adCount, prevPageBtn, nextPageBtn);
// 6. 初始化拖拽
ControlPanel.initDrag();
// 7. 面板hover效果
panel.onmouseover = () => {
panel.style.boxShadow = "0 6px 16px rgba(0,0,0,0.12)";
};
panel.onmouseout = () => {
panel.style.boxShadow = options.panelStyle.shadow;
};
return panel;
},
/**
* 绑定面板事件
*/
bindEvents: (adToggle, deleteModeToggle, refreshBtn, adCount, prevPageBtn, nextPageBtn) => {
// 广告显示/隐藏切换
adToggle.addEventListener('change', (e) => {
options.showAds = e.target.checked;
AdManager.handleAllAds();
});
// 广告删除模式切换
deleteModeToggle.addEventListener('change', (e) => {
options.isDomRemove = e.target.checked;
AdManager.handleAllAds();
if (options.isDomRemove) {
AdManager.detectedAds.forEach(el => {
if (document.body.contains(el)) el.remove();
});
AdManager.detectedAds.clear();
}
});
// 手动刷新广告检测
refreshBtn.addEventListener('click', () => {
refreshBtn.textContent = "检测中...";
refreshBtn.disabled = true;
Detector.detectAll();
AdManager.handleAllAds();
setTimeout(() => {
refreshBtn.textContent = "手动刷新广告检测";
refreshBtn.disabled = false;
adCount.textContent = `已过滤广告:${AdManager.detectedAds.size} 个`;
// 刷新分页按钮状态
PaginationTool.updateButtonStates(prevPageBtn, nextPageBtn);
}, 1000);
});
// 定时更新广告统计和分页按钮状态
setInterval(() => {
adCount.textContent = `已过滤广告:${AdManager.detectedAds.size} 个`;
PaginationTool.updateButtonStates(prevPageBtn, nextPageBtn);
}, 3000);
// 上一页按钮点击事件
prevPageBtn.addEventListener('click', () => {
if (!prevPageBtn.disabled) {
PaginationTool.triggerPageClick(options.page.up);
// 点击后短暂禁用按钮,防止重复点击
prevPageBtn.disabled = true;
setTimeout(() => {
PaginationTool.updateButtonStates(prevPageBtn, nextPageBtn);
}, 1000);
}
});
// 下一页按钮点击事件
nextPageBtn.addEventListener('click', () => {
if (!nextPageBtn.disabled) {
PaginationTool.triggerPageClick(options.page.down);
// 点击后短暂禁用按钮,防止重复点击
nextPageBtn.disabled = true;
setTimeout(() => {
PaginationTool.updateButtonStates(prevPageBtn, nextPageBtn);
}, 1000);
}
});
// 初始更新分页按钮状态
PaginationTool.updateButtonStates(prevPageBtn, nextPageBtn);
},
/**
* 初始化拖拽功能
*/
initDrag: () => {
const panel = ControlPanel.panelElement;
const dragHandle = panel.querySelector(".panel-header");
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
let isDragging = false;
dragHandle.addEventListener('mousedown', (e) => {
e.preventDefault();
isDragging = true;
panel.style.boxShadow = "0 8px 24px rgba(0,0,0,0.15)";
panel.style.transition = "box-shadow 0.1s ease";
pos3 = e.clientX;
pos4 = e.clientY;
document.addEventListener('mousemove', dragMove);
document.addEventListener('mouseup', dragEnd);
});
const dragMove = (e) => {
if (!isDragging) return;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
const newTop = panel.offsetTop - pos2;
const newLeft = panel.offsetLeft - pos1;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
panel.style.top = `${Math.max(0, Math.min(newTop, viewportHeight - panel.offsetHeight))}px`;
panel.style.left = `${Math.max(0, Math.min(newLeft, viewportWidth - panel.offsetWidth))}px`;
};
const dragEnd = () => {
isDragging = false;
panel.style.boxShadow = options.panelStyle.shadow;
panel.style.transition = "box-shadow 0.2s ease";
document.removeEventListener('mousemove', dragMove);
document.removeEventListener('mouseup', dragEnd);
};
}
};
// 初始化函数
const init = () => {
if (window.location.host.indexOf(options.matchingHost) === -1) {
logger.log("不匹配目标网站,脚本不执行");
return;
}
ControlPanel.createPanel();
const checkInterval = setInterval(() => {
if (document.readyState === 'unloaded') {
clearInterval(checkInterval);
return;
}
Detector.detectAll();
AdManager.handleAllAds();
}, options.interval);
window.addEventListener('beforeunload', () => {
clearInterval(checkInterval);
});
logger.log("脚本初始化完成");
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();