// ==UserScript== // @name 百度贴吧广告过滤登录去除(优化版) // @namespace noting // @version 0.8.12 // @description 贴吧广告过滤,自动关闭登录弹窗,解决广告闪烁问题 // @author Time // @match *://tieba.baidu.com/* // @grant none // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/430157/%E7%99%BE%E5%BA%A6%E8%B4%B4%E5%90%A7%E5%B9%BF%E5%91%8A%E8%BF%87%E6%BB%A4%E7%99%BB%E5%BD%95%E5%8E%BB%E9%99%A4%EF%BC%88%E4%BC%98%E5%8C%96%E7%89%88%EF%BC%89.user.js // @updateURL https://update.greasyfork.icu/scripts/430157/%E7%99%BE%E5%BA%A6%E8%B4%B4%E5%90%A7%E5%B9%BF%E5%91%8A%E8%BF%87%E6%BB%A4%E7%99%BB%E5%BD%95%E5%8E%BB%E9%99%A4%EF%BC%88%E4%BC%98%E5%8C%96%E7%89%88%EF%BC%89.meta.js // ==/UserScript== (function() { 'use strict'; // 配置参数 const options = { expensionName: "百度贴吧广告过滤登录去除", interval: 500, development: false, isDomRemove: false, showAds: false, isCollapsed: false, matchingHost: "tieba.baidu.com", loginDomId: "tiebaCustomPassLogin", adTextClass: "label_text", adText: "广告", feedbackEmail: "tieba-filter-feedback@example.com", storageKey: "tiebaAdFilterPanelPosition", collapseStorageKey: "tiebaAdFilterPanelCollapsed", blockListStorageKey: "tiebaBlockedUsers", adIds: [ "pagelet_frs-aside/pagelet/fengchao_ad", "banner_pb_customize", "plat_recom_carousel" ], scoringText:"如果觉得好用麻烦在油猴给个好评亲!!!", adClasses: [ "fengchao-wrap-feed", "head_banner", "head_ad_pop", "l_banner", "j_couplet", "card_banner", "bus-top-activity-wrap", "rec_left", "ylh-ad-container" ], userSelectors: { threadAuthor: '.p_author_name', replyAuthor: '.d_name', threadContainer: '.j_thread_list', replyContainer: '.l_post' }, page:{ container:"pb_list_pager", down:"下一页", up:"上一页" }, panelStyle: { width: "240px", minHeight: "400px", collapsedHeight: "45px", 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" }, // 防闪烁配置 preHideStyle: 'display: none !important; visibility: hidden !important;', mutationObserverOptions: { childList: true, subtree: true, attributes: true, characterData: false }, preBlockSelectors: [ 'div[id^="fengchao_"]', 'div[class*="ad-"]', 'div[class*="banner"]', 'div[class*="ads"]', 'div[class*="promotion"]', 'div[data-ad]', 'iframe[src*="ad"]', 'img[src*="ad"]' ] }; // 工具函数 - 日志输出 const logger = { log: (...args) => { if (options.development) { console.log(`[${options.expensionName}]`, ...args); } }, error: (...args) => { console.error(`[${options.expensionName}]`, ...args); } }; // 样式工具 - 处理预隐藏样式 const StyleUtil = { // 提前注入全局隐藏样式 injectPreHideStyles() { try { // 检查是否已注入 if (document.getElementById('ad-pre-hide-style')) return; const style = document.createElement('style'); style.id = 'ad-pre-hide-style'; style.textContent = ` /* 预隐藏已知广告容器 */ #${options.loginDomId}, ${options.adIds.map(id => `#${id.replace(/\//g, '\\/')}`).join(',')}, ${options.adClasses.map(cls => `.${cls}`).join(',')}, ${options.preBlockSelectors.join(',')}, [data-ad-type], [ad-data], [ad-type], [data-ads] { ${options.preHideStyle} } `; // 优先插入到head最前面 if (document.head.firstChild) { document.head.insertBefore(style, document.head.firstChild); } else { document.head.appendChild(style); } } catch (ex) { logger.error("注入预隐藏样式失败:", ex); } }, // 为单个元素添加预隐藏样式 preHideElement(element) { if (!element || element.hasAttribute('data-ad-pre-hidden')) return; try { element.setAttribute('data-ad-pre-hidden', 'true'); // 直接设置style,优先级最高 element.style.cssText = options.preHideStyle + element.style.cssText; } catch (ex) { logger.error("预隐藏元素失败:", ex); } } }; // 存储工具 const StorageUtil = { 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); } }, getPosition: () => { try { const data = localStorage.getItem(options.storageKey); if (data) { return JSON.parse(data); } return null; } catch (ex) { logger.error("获取面板位置失败:", ex); return null; } }, saveCollapsedState: (isCollapsed) => { try { localStorage.setItem(options.collapseStorageKey, JSON.stringify({ isCollapsed, timestamp: Date.now() })); } catch (ex) { logger.error("保存折叠状态失败:", ex); } }, getCollapsedState: () => { try { const data = localStorage.getItem(options.collapseStorageKey); if (data) { return JSON.parse(data); } return { isCollapsed: options.isCollapsed }; } catch (ex) { logger.error("获取折叠状态失败:", ex); return { isCollapsed: options.isCollapsed }; } }, saveBlockedUsers: (users) => { try { localStorage.setItem(options.blockListStorageKey, JSON.stringify({ users: Array.from(users), timestamp: Date.now() })); } catch (ex) { logger.error("保存屏蔽用户失败:", ex); } }, getBlockedUsers: () => { try { const data = localStorage.getItem(options.blockListStorageKey); if (data) { const parsed = JSON.parse(data); return new Set(parsed.users || []); } return new Set(); } catch (ex) { logger.error("获取屏蔽用户失败:", ex); return new Set(); } } }; // 广告管理模块(优化防闪烁) const AdManager = { detectedAds: new Set(), processedElements: new WeakSet(), immediateHide(element) { if (!element || this.processedElements.has(element)) return; try { this.processedElements.add(element); StyleUtil.preHideElement(element); element.classList.add('ad-filter-hidden'); element.setAttribute('data-ad-hidden', 'true'); } catch (ex) { logger.error("立即隐藏元素失败:", ex); } }, isAdDetected: (element) => { return AdManager.detectedAds.has(element); }, addAdElement(element) { if (!element || this.detectedAds.has(element) || !document.body.contains(element)) return; try { this.immediateHide(element); this.detectedAds.add(element); const observer = new MutationObserver((mutations) => { if (!document.body.contains(element)) { this.detectedAds.delete(element); observer.disconnect(); return; } mutations.forEach(mutation => { if (mutation.type === 'attributes' && (mutation.attributeName === 'style' || mutation.attributeName === 'class')) { // 检查元素是否被重新显示 if (element.style.display !== 'none' || !element.classList.contains('ad-filter-hidden')) { this.immediateHide(element); } } }); }); observer.observe(element, { attributes: true, attributeFilter: ['style', 'class'] }); } catch (ex) { logger.error("添加广告元素失败:", ex); } }, handleAdElement: (element) => { if (!element || !element.style) return; try { if (options.showAds) { element.style.display = ""; element.style.visibility = ""; } else { if (options.isDomRemove) { if (document.body.contains(element)) { element.remove(); } this.detectedAds.delete(element); } else if (element.style.display !== "none") { this.immediateHide(element); } } } catch (ex) { logger.error("处理广告元素出错:", ex); } }, handleAllAds: () => { this.detectedAds.forEach(element => { this.handleAdElement(element); }); }, batchProcessElements(elements) { if (!elements || elements.length === 0) return; Array.from(elements).forEach(el => { this.addAdElement(el); }); } }; // 用户屏蔽模块 const UserBlocker = { blockedUsers: StorageUtil.getBlockedUsers(), blockedElements: new Set(), addUser: (username) => { if (!username || UserBlocker.blockedUsers.has(username)) return false; UserBlocker.blockedUsers.add(username); StorageUtil.saveBlockedUsers(UserBlocker.blockedUsers); UserBlocker.detectAndBlock(); return true; }, removeUser: (username) => { if (!username || !UserBlocker.blockedUsers.has(username)) return false; UserBlocker.blockedUsers.delete(username); StorageUtil.saveBlockedUsers(UserBlocker.blockedUsers); UserBlocker.showBlockedElements(username); return true; }, detectAndBlock: () => { if (UserBlocker.blockedUsers.size === 0) return; try { // 处理帖子 const threadAuthors = document.querySelectorAll(options.userSelectors.threadAuthor); threadAuthors.forEach(authorEl => { const username = authorEl.textContent.trim(); if (UserBlocker.blockedUsers.has(username)) { const threadEl = authorEl.closest(options.userSelectors.threadContainer); if (threadEl && !UserBlocker.blockedElements.has(threadEl)) { UserBlocker.blockElement(threadEl, username, 'thread'); } } }); // 处理回复 const replyAuthors = document.querySelectorAll(options.userSelectors.replyAuthor); replyAuthors.forEach(authorEl => { const username = authorEl.textContent.trim(); if (UserBlocker.blockedUsers.has(username)) { const replyEl = authorEl.closest(options.userSelectors.replyContainer); if (replyEl && !UserBlocker.blockedElements.has(replyEl)) { UserBlocker.blockElement(replyEl, username, 'reply'); } } }); } catch (ex) { logger.error("检测并屏蔽用户内容失败:", ex); } }, blockElement: (element, username, type) => { if (!element) return; try { element.__originalDisplay = element.style.display; element.__blockedBy = username; element.__blockedType = type; element.style.display = "none"; UserBlocker.blockedElements.add(element); logger.log(`已屏蔽${type === 'thread' ? '帖子' : '回复'} (作者: ${username})`); } catch (ex) { logger.error("屏蔽元素失败:", ex); } }, showBlockedElements: (username) => { UserBlocker.blockedElements.forEach(element => { if (element.__blockedBy === username) { try { element.style.display = element.__originalDisplay || ""; UserBlocker.blockedElements.delete(element); } catch (ex) { logger.error("显示被屏蔽元素失败:", ex); } } }); }, getBlockedList: () => { return Array.from(UserBlocker.blockedUsers); }, clearAll: () => { const users = UserBlocker.getBlockedList(); users.forEach(username => { UserBlocker.removeUser(username); }); } }; // 检测器模块 const Detector = { highPriorityObserver: null, initHighPriorityDetector() { try { this.detectAndHideExistingAds(); this.highPriorityObserver = new MutationObserver((mutations) => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType !== 1) return; if (this.isAdNode(node)) { AdManager.immediateHide(node); AdManager.addAdElement(node); } else { this.findAdsInSubtree(node); } }); }); }); this.highPriorityObserver.observe(document.documentElement, options.mutationObserverOptions); return this.highPriorityObserver; } catch (ex) { logger.error("初始化高优先级检测器失败:", ex); return null; } }, isAdNode(node) { try { if (options.adIds.includes(node.id)) return true; const classList = Array.from(node.classList); if (classList.some(cls => options.adClasses.includes(cls))) return true; if (node.classList.contains(options.adTextClass) && node.innerText.trim() === options.adText) return true; return options.preBlockSelectors.some(selector => { try { return node.matches(selector); } catch (e) { return false; } }); } catch (ex) { logger.error("检查广告节点失败:", ex); return false; } }, findAdsInSubtree(node) { try { // 检查登录弹窗 const loginEl = node.querySelector(`#${options.loginDomId}`); if (loginEl) AdManager.addAdElement(loginEl); // 检查广告id options.adIds.forEach(id => { const el = node.querySelector(`#${id.replace(/\//g, '\\/')}`); if (el) AdManager.addAdElement(el); }); // 检查广告类名 options.adClasses.forEach(cls => { const els = node.getElementsByClassName(cls); AdManager.batchProcessElements(els); }); // 检查广告文本标记 const adTextEls = node.getElementsByClassName(options.adTextClass); Array.from(adTextEls).forEach(el => { if (el.innerText.trim() === options.adText) { const adContainer = el.closest('div, section, article'); if (adContainer) AdManager.addAdElement(adContainer); } }); } catch (ex) { logger.error("在子树中查找广告失败:", ex); } }, detectAndHideExistingAds() { try { // 处理登录弹窗 const loginEl = document.getElementById(options.loginDomId); if (loginEl) AdManager.addAdElement(loginEl); // 处理id广告 options.adIds.forEach(id => { const el = document.getElementById(id); if (el) AdManager.addAdElement(el); }); // 处理类名广告 options.adClasses.forEach(cls => { const els = document.getElementsByClassName(cls); AdManager.batchProcessElements(els); }); // 处理文本标记广告 const adTextEls = document.getElementsByClassName(options.adTextClass); Array.from(adTextEls).forEach(el => { if (el.innerText.trim() === options.adText) { const adContainer = el.closest('div, section, article'); if (adContainer) AdManager.addAdElement(adContainer); } }); } catch (ex) { logger.error("检测并隐藏现有广告失败:", ex); } }, detectAll() { this.detectAndHideExistingAds(); UserBlocker.detectAndBlock(); } }; // 分页工具模块 const PaginationTool = { findPageLink: (targetText) => { try { const pagerContainer = document.getElementsByClassName(options.page.container)[0]; if (!pagerContainer) { logger.log("未找到分页容器"); 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) { try { targetLink.click(); logger.log(`已触发"${targetText}"点击`); return true; } catch (ex) { logger.error("触发分页点击失败:", ex); } } return false; }, updateButtonStates: (prevBtn, nextBtn) => { try { 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"; } catch (ex) { logger.error("更新分页按钮状态失败:", ex); } } }; // 控制面板模块 const ControlPanel = { panelElement: null, contentElement: null, collapseButton: null, isProgrammaticMove: false, blockedUsersListEl: null, blockUserInput: null, createPanel: () => { try { const savedCollapseState = StorageUtil.getCollapsedState(); options.isCollapsed = savedCollapseState.isCollapsed; // 创建面板容器 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.isCollapsed ? options.panelStyle.collapsedHeight : options.panelStyle.minHeight}; height: auto; 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: all 0.2s ease; `; // 面板头部 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; `; // 折叠/展开按钮 const collapseBtn = document.createElement("button"); collapseBtn.id = "collapse-btn"; collapseBtn.innerHTML = options.isCollapsed ? "▷" : "▽"; collapseBtn.style.cssText = ` background: none; border: none; color: ${options.panelStyle.headerColor}; font-size: 16px; cursor: pointer; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; padding: 0; opacity: 0.9; transition: transform 0.2s ease; `; collapseBtn.title = options.isCollapsed ? "展开面板" : "折叠面板"; ControlPanel.collapseButton = collapseBtn; header.innerHTML = ` ${options.expensionName} v0.8.12 `; header.appendChild(collapseBtn); // 面板内容区 const content = document.createElement("div"); content.className = "panel-content"; content.style.cssText = ` padding: ${options.panelStyle.padding}; color: ${options.panelStyle.textColor}; font-size: 14px; display: ${options.isCollapsed ? "none" : "block"}; transition: all 0.2s ease; `; ControlPanel.contentElement = content; // 广告显示/隐藏开关 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); // 广告删除方式 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); // 吧友屏蔽功能区 const userBlockGroup = document.createElement("div"); userBlockGroup.style.cssText = ` margin-bottom: ${options.panelStyle.marginBottom}; padding-bottom: ${options.panelStyle.marginBottom}; border-bottom: 1px solid #f3f4f6; `; const blockTitle = document.createElement("div"); blockTitle.style.cssText = ` font-size: 13px; margin-bottom: 8px; color: ${options.panelStyle.subTextColor}; `; blockTitle.textContent = "吧友屏蔽"; // 添加用户输入框和按钮 const addUserContainer = document.createElement("div"); addUserContainer.style.cssText = "display: flex; gap: 4px; margin-bottom: 8px;"; const blockUserInput = document.createElement("input"); blockUserInput.id = "block-user-input"; blockUserInput.placeholder = "输入用户名"; blockUserInput.style.cssText = ` flex: 1; padding: 6px 8px; font-size: 13px; border: 1px solid #ddd; border-radius: ${options.panelStyle.btnRadius}; `; ControlPanel.blockUserInput = blockUserInput; const addUserBtn = document.createElement("button"); addUserBtn.textContent = "添加"; addUserBtn.style.cssText = ` padding: 0 8px; 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; `; addUserBtn.onmouseover = () => { addUserBtn.style.backgroundColor = options.panelStyle.btnHoverBg; }; addUserBtn.onmouseout = () => { addUserBtn.style.backgroundColor = options.panelStyle.btnBg; }; // 绑定添加用户事件 addUserBtn.addEventListener('click', () => { const username = blockUserInput.value.trim(); if (username) { if (UserBlocker.addUser(username)) { blockUserInput.value = ""; ControlPanel.updateBlockedUsersList(); alert(`已添加屏蔽: ${username}`); } else { alert("该用户已在屏蔽列表中"); } } }); // 回车添加用户 blockUserInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { addUserBtn.click(); } }); // 屏蔽列表容器 const blockedListContainer = document.createElement("div"); blockedListContainer.style.cssText = ` max-height: 120px; overflow-y: auto; font-size: 13px; border: 1px solid #f0f0f0; border-radius: 4px; padding: 4px; margin-bottom: 8px; `; // 屏蔽列表 const blockedUsersList = document.createElement("ul"); blockedUsersList.style.cssText = ` list-style: none; margin: 0; padding: 0; `; ControlPanel.blockedUsersListEl = blockedUsersList; blockedListContainer.appendChild(blockedUsersList); // 清空按钮 const clearBlockListBtn = document.createElement("button"); clearBlockListBtn.textContent = "清空屏蔽列表"; clearBlockListBtn.style.cssText = ` width: 100%; padding: 4px 0; background-color: #fee2e2; border: none; border-radius: ${options.panelStyle.btnRadius}; color: #dc2626; font-size: 12px; cursor: pointer; transition: background-color 0.2s ease; `; clearBlockListBtn.onmouseover = () => { clearBlockListBtn.style.backgroundColor = "#fecaca"; }; clearBlockListBtn.onmouseout = () => { clearBlockListBtn.style.backgroundColor = "#fee2e2"; }; clearBlockListBtn.addEventListener('click', () => { if (confirm("确定要清空所有屏蔽用户吗?")) { UserBlocker.clearAll(); ControlPanel.updateBlockedUsersList(); } }); addUserContainer.append(blockUserInput, addUserBtn); userBlockGroup.append(blockTitle, addUserContainer, blockedListContainer, clearBlockListBtn); // 分页快捷操作区 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); // 反馈邮件区域 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); // 手动刷新广告检测 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; }; // 统计信息显示 const statsContainer = document.createElement("div"); statsContainer.style.cssText = ` font-size: 12px; color: ${options.panelStyle.subTextColor}; text-align: right; padding-top: 8px; border-top: 1px dashed #f3f4f6; `; const adCount = document.createElement("div"); adCount.id = "ad-count"; adCount.textContent = `已过滤广告:${AdManager.detectedAds.size} 个`; const blockedCount = document.createElement("div"); blockedCount.id = "blocked-count"; blockedCount.textContent = `已屏蔽用户:${UserBlocker.getBlockedList().length} 个`; statsContainer.append(adCount, blockedCount); // 用户评分区 const scoreContainer = document.createElement("div"); scoreContainer.style.cssText = ` width: 100%; overflow: hidden; white-space: nowrap; margin-top: 10px; `; const customerScoring = document.createElement("label"); customerScoring.id = "customer-scoring"; customerScoring.textContent = `${options.scoringText}`; customerScoring.style.cssText = ` display: inline-block; animation: marquee 15s linear infinite; color: #e63946; font-weight: bold; font-size: 13px; `; // 添加跑马灯动画样式 const styleSheet = document.createElement("style"); styleSheet.textContent = ` @keyframes marquee { 0% { transform: translateX(100%); } 100% { transform: translateX(-100%); } } `; document.head.appendChild(styleSheet); scoreContainer.appendChild(customerScoring); // 组装内容区 content.append(adToggleGroup, deleteModeGroup, userBlockGroup, paginationGroup, feedbackGroup, refreshBtn, statsContainer , scoreContainer); panel.append(header, content); document.body.appendChild(panel); ControlPanel.panelElement = panel; // 初始化屏蔽用户列表显示 ControlPanel.updateBlockedUsersList(); // 绑定事件 ControlPanel.bindEvents(adToggle, deleteModeToggle, refreshBtn, adCount, blockedCount, prevPageBtn, nextPageBtn); // 初始化拖拽 ControlPanel.initDrag(); // 初始化存储同步 ControlPanel.initStorageSync(); // 面板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; } catch (ex) { logger.error("创建控制面板失败:", ex); return null; } }, updateBlockedUsersList: () => { const listEl = ControlPanel.blockedUsersListEl; if (!listEl) return; try { listEl.innerHTML = ""; const blockedUsers = UserBlocker.getBlockedList(); if (blockedUsers.length === 0) { const emptyItem = document.createElement("li"); emptyItem.style.cssText = "padding: 4px; color: #9ca3af; text-align: center;"; emptyItem.textContent = "暂无屏蔽用户"; listEl.appendChild(emptyItem); return; } blockedUsers.forEach(username => { const listItem = document.createElement("li"); listItem.style.cssText = "display: flex; justify-content: space-between; align-items: center; padding: 4px; border-bottom: 1px solid #f3f4f6;"; const usernameSpan = document.createElement("span"); usernameSpan.textContent = username; usernameSpan.style.cssText = "overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"; const removeBtn = document.createElement("button"); removeBtn.textContent = "×"; removeBtn.style.cssText = ` width: 18px; height: 18px; border: none; border-radius: 50%; background-color: #ef4444; color: white; font-size: 12px; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; margin-left: 4px; `; removeBtn.title = `取消屏蔽 ${username}`; removeBtn.addEventListener('click', () => { if (UserBlocker.removeUser(username)) { ControlPanel.updateBlockedUsersList(); document.getElementById("blocked-count").textContent = `已屏蔽用户:${UserBlocker.getBlockedList().length} 个`; } }); listItem.append(usernameSpan, removeBtn); listEl.appendChild(listItem); }); document.getElementById("blocked-count").textContent = `已屏蔽用户:${blockedUsers.length} 个`; } catch (ex) { logger.error("更新屏蔽用户列表失败:", ex); } }, toggleCollapse: () => { try { const panel = ControlPanel.panelElement; const content = ControlPanel.contentElement; const collapseBtn = ControlPanel.collapseButton; options.isCollapsed = !options.isCollapsed; if (options.isCollapsed) { panel.style.minHeight = `${options.panelStyle.collapsedHeight}`; content.style.display = "none"; collapseBtn.innerHTML = "▷"; collapseBtn.title = "展开面板"; } else { panel.style.minHeight = `${options.panelStyle.minHeight}`; content.style.display = "block"; collapseBtn.innerHTML = "▽"; collapseBtn.title = "折叠面板"; } StorageUtil.saveCollapsedState(options.isCollapsed); } catch (ex) { logger.error("切换折叠状态失败:", ex); } }, syncCollapseState: (newState) => { if (options.isCollapsed === newState.isCollapsed) return; try { options.isCollapsed = newState.isCollapsed; const panel = ControlPanel.panelElement; const content = ControlPanel.contentElement; const collapseBtn = ControlPanel.collapseButton; if (options.isCollapsed) { panel.style.minHeight = `${options.panelStyle.collapsedHeight}`; content.style.display = "none"; collapseBtn.innerHTML = "▷"; collapseBtn.title = "展开面板"; } else { panel.style.minHeight = `${options.panelStyle.minHeight}`; content.style.display = "block"; collapseBtn.innerHTML = "▽"; collapseBtn.title = "折叠面板"; } } catch (ex) { logger.error("同步折叠状态失败:", ex); } }, bindEvents: (adToggle, deleteModeToggle, refreshBtn, adCount, blockedCount, prevPageBtn, nextPageBtn) => { try { // 绑定折叠按钮事件 ControlPanel.collapseButton.addEventListener('click', (e) => { e.stopPropagation(); ControlPanel.toggleCollapse(); }); // 广告显示/隐藏切换 adToggle.addEventListener('change', (e) => { options.showAds = e.target.checked; AdManager.handleAllAds(); adCount.textContent = `已过滤广告:${AdManager.detectedAds.size} 个`; }); // 广告删除模式切换 deleteModeToggle.addEventListener('change', (e) => { options.isDomRemove = e.target.checked; AdManager.handleAllAds(); }); // 手动刷新广告检测 refreshBtn.addEventListener('click', () => { refreshBtn.textContent = "检测中..."; refreshBtn.disabled = true; Detector.detectAll(); AdManager.handleAllAds(); setTimeout(() => { refreshBtn.textContent = "手动刷新广告检测"; refreshBtn.disabled = false; adCount.textContent = `已过滤广告:${AdManager.detectedAds.size} 个`; blockedCount.textContent = `已屏蔽用户:${UserBlocker.getBlockedList().length} 个`; PaginationTool.updateButtonStates(prevPageBtn, nextPageBtn); }, 1000); }); // 定时更新统计信息和分页按钮状态 setInterval(() => { adCount.textContent = `已过滤广告:${AdManager.detectedAds.size} 个`; blockedCount.textContent = `已屏蔽用户:${UserBlocker.getBlockedList().length} 个`; 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); } catch (ex) { logger.error("绑定面板事件失败:", ex); } }, initDrag: () => { const panel = ControlPanel.panelElement; if (!panel) return; const dragHandle = panel.querySelector(".panel-header"); if (!dragHandle) return; let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; let isDragging = false; try { 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); }; } catch (ex) { logger.error("初始化拖拽功能失败:", ex); } }, initStorageSync: () => { const panel = ControlPanel.panelElement; if (!panel) return; try { window.addEventListener('storage', (e) => { if (e.key === options.storageKey) { 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); } catch (ex) { logger.error("同步面板位置失败:", ex); } } else if (e.key === options.collapseStorageKey) { try { const newState = JSON.parse(e.newValue); if (newState) { ControlPanel.syncCollapseState(newState); } } catch (ex) { logger.error("同步折叠状态失败:", ex); } } else if (e.key === options.blockListStorageKey) { try { const newList = JSON.parse(e.newValue); if (newList && newList.users) { UserBlocker.blockedUsers = new Set(newList.users); ControlPanel.updateBlockedUsersList(); UserBlocker.detectAndBlock(); } } catch (ex) { logger.error("同步屏蔽列表失败:", ex); } } }); } catch (ex) { logger.error("初始化存储同步失败:", ex); } } }; // 初始化函数 const init = () => { try { if (window.location.host.indexOf(options.matchingHost) === -1) { logger.log("不匹配目标网站,脚本不执行"); return; } // 1. 优先注入预隐藏样式 StyleUtil.injectPreHideStyles(); // 2. 启动高优先级检测器 const highPriorityObserver = Detector.initHighPriorityDetector(); // 3. 创建控制面板 ControlPanel.createPanel(); // 4. 启动常规检测循环 const checkInterval = setInterval(() => { if (document.readyState === 'unloaded') { clearInterval(checkInterval); if (highPriorityObserver) highPriorityObserver.disconnect(); return; } Detector.detectAll(); AdManager.handleAllAds(); }, options.interval * 2); window.addEventListener('beforeunload', () => { clearInterval(checkInterval); if (highPriorityObserver) highPriorityObserver.disconnect(); }); logger.log("优化版脚本初始化完成,已启用防闪烁机制"); } catch (ex) { logger.error("脚本初始化失败:", ex); } }; // 启动初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();