// ==UserScript==
// @name 贴吧广告过滤登录去除
// @namespace noting
// @version 0.8.4
// @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: "广告",
feedbackEmail: "985951420@qq.com",
storageKey: "tiebaAdFilterPanelPosition", // 用于存储位置的localStorage键名
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",
down:"下一页",
up:"上一页"
},
panelStyle: {
width: "240px",
minHeight: "320px",
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 StorageUtil = {
// 保存面板位置到localStorage
savePosition: (position) => {
try {
const data = {
top: position.top,
left: position.left,
timestamp: Date.now() // 添加时间戳用于冲突解决
};
localStorage.setItem(options.storageKey, JSON.stringify(data));
} catch (ex) {
logger.error("保存面板位置失败:", ex);
}
},
// 从localStorage获取面板位置
getPosition: () => {
try {
const data = localStorage.getItem(options.storageKey);
if (data) {
return JSON.parse(data);
}
return null;
} catch (ex) {
logger.error("获取面板位置失败:", ex);
return null;
}
}
};
// 广告检测与处理模块
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 = {
findPageLink: (targetText) => {
try {
const pagerContainer = document.getElementsByClassName(options.page.container)[0];
if (!pagerContainer) {
logger.log("未找到分页容器(class: pb_list_pager)");
return null;
}
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;
}
},
triggerPageClick: (targetText) => {
const targetLink = PaginationTool.findPageLink(targetText);
if (targetLink) {
targetLink.click();
logger.log(`已触发"${targetText}"点击`);
return true;
}
return false;
},
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,
isProgrammaticMove: false, // 标记是否是程序触发的移动(非用户拖拽)
createPanel: () => {
// 1. 创建面板容器
const panel = document.createElement("div");
panel.id = "ad-filter-panel";
// 检查是否有保存的位置,有则使用保存的位置,否则使用默认位置
const savedPos = StorageUtil.getPosition();
const topPos = savedPos ? `${savedPos.top}px` : options.panelStyle.top;
const leftPos = savedPos ? `${savedPos.left}px` : options.panelStyle.left;
panel.style.cssText = `
position: fixed;
width: ${options.panelStyle.width};
min-height: ${options.panelStyle.minHeight};
top: ${topPos};
left: ${leftPos};
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.4'}
`;
// 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;
`;
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 feedbackGroup = document.createElement("div");
feedbackGroup.style.cssText = `
margin-bottom: ${options.panelStyle.marginBottom};
padding-bottom: ${options.panelStyle.marginBottom};
border-bottom: 1px solid #f3f4f6;
`;
const feedbackTitle = document.createElement("div");
feedbackTitle.style.cssText = `
font-size: 13px;
margin-bottom: 8px;
color: ${options.panelStyle.subTextColor};
`;
feedbackTitle.textContent = "反馈与建议";
const feedbackBtn = document.createElement("a");
feedbackBtn.href = `mailto:${options.feedbackEmail}?subject=贴吧广告过滤插件反馈&body=请描述您遇到的问题或建议...`;
feedbackBtn.textContent = "发送邮件反馈";
feedbackBtn.style.cssText = `
display: block;
width: 100%;
padding: 8px 0;
background-color: ${options.panelStyle.btnBg};
border: none;
border-radius: ${options.panelStyle.btnRadius};
color: #2563eb;
font-size: 13px;
cursor: pointer;
text-align: center;
text-decoration: none;
transition: background-color 0.2s ease;
`;
feedbackBtn.onmouseover = () => {
feedbackBtn.style.backgroundColor = options.panelStyle.btnHoverBg;
};
feedbackBtn.onmouseout = () => {
feedbackBtn.style.backgroundColor = options.panelStyle.btnBg;
};
feedbackGroup.append(feedbackTitle, feedbackBtn);
// 3.5 手动刷新广告检测
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.6 广告统计显示
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, feedbackGroup, 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. 监听storage事件,实现多标签页同步
ControlPanel.initStorageSync();
// 8. 面板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;
// 限制在视口内
const constrainedTop = Math.max(0, Math.min(newTop, viewportHeight - panel.offsetHeight));
const constrainedLeft = Math.max(0, Math.min(newLeft, viewportWidth - panel.offsetWidth));
panel.style.top = `${constrainedTop}px`;
panel.style.left = `${constrainedLeft}px`;
// 只有用户主动拖拽才保存位置(避免循环触发)
if (!ControlPanel.isProgrammaticMove) {
StorageUtil.savePosition({
top: constrainedTop,
left: constrainedLeft
});
}
};
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);
};
},
/**
* 初始化存储同步,监听其他标签页的位置变化
*/
initStorageSync: () => {
const panel = ControlPanel.panelElement;
window.addEventListener('storage', (e) => {
// 只处理我们关心的存储键
if (e.key !== options.storageKey) return;
try {
// 解析新的位置数据
const newPos = JSON.parse(e.newValue);
if (!newPos) return;
// 标记为程序触发的移动,避免触发保存逻辑
ControlPanel.isProgrammaticMove = true;
// 更新面板位置
panel.style.top = `${newPos.top}px`;
panel.style.left = `${newPos.left}px`;
// 短暂延迟后重置标记
setTimeout(() => {
ControlPanel.isProgrammaticMove = false;
}, 100);
logger.log("从其他标签页同步了面板位置");
} catch (ex) {
logger.error("同步面板位置失败:", ex);
}
});
}
};
// 初始化函数
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();
}
})();