// ==UserScript== // @name 网页元素屏蔽器 // @namespace http://tampermonkey.net/ // @version 0.1.61 // @description 集成原生CSS极速注入、Shadow DOM隔离、DOM结构拦截、广告域封杀、正则文本拦截、动态资源域实时拦截、路径模式拦截与规则导入导出。支持积木组合模式、元素层级缩放选择与全局域名黑名单,彻底解决广告刷新复活。 // @author EFate // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @run-at document-start // @license MIT // ==/UserScript== (function () { 'use strict'; function debounce(func, wait, maxWait) { let timeout, lastExec = 0; return function (...args) { const now = Date.now(); clearTimeout(timeout); if (maxWait && now - lastExec >= maxWait) { lastExec = now; func.apply(this, args); } else { timeout = setTimeout(() => { lastExec = Date.now(); func.apply(this, args); }, wait); } }; } function escapeHTML(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } function escapeCSSAttr(s) { return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"'); } /** * 核心数据与配置管理模块 * 规则分类(前7类按域名隔离,domainBlock全局生效): * static / dynamic / regex / attribute / structural / complex / pathPattern / domainBlock */ class StorageManager { constructor() { this.domain = window.location.hostname; this.flashList = GM_getValue('pro_blocker_flash_domains', {}); } getData() { if (this._cachedData && this._cachedDataDomain === this.domain) return this._cachedData; this._cachedDataDomain = this.domain; this._cachedData = { static: GM_getValue('blocks', {})[this.domain] || [], dynamic: GM_getValue('dynamicBlocks', {})[this.domain] || [], regex: GM_getValue('regexBlocks', {})[this.domain] || [], attribute: GM_getValue('attrBlocks', {})[this.domain] || [], structural: GM_getValue('structBlocks', {})[this.domain] || [], complex: GM_getValue('complexBlocks', {})[this.domain] || [], pathPattern: GM_getValue('pathPatternBlocks', {})[this.domain] || [], config: GM_getValue('config', {})[this.domain] || { mode: 'auto' }, domainBlock: GM_getValue('domainBlocks', []) }; return this._cachedData; } invalidateDataCache() { this._cachedData = null; this._cachedDataDomain = null; } saveData(type, rules) { const keyMap = { 'static': 'blocks', 'dynamic': 'dynamicBlocks', 'regex': 'regexBlocks', 'attribute': 'attrBlocks', 'structural': 'structBlocks', 'complex': 'complexBlocks', 'pathPattern': 'pathPatternBlocks' }; const key = keyMap[type]; if (!key) return; const allData = GM_getValue(key, {}); if (rules.length === 0) delete allData[this.domain]; else allData[this.domain] = rules; GM_setValue(key, allData); this.invalidateDataCache(); BlockEngine.invalidateCache(); if (type !== 'regex' && type !== 'complex') BlockEngine.applyCSSRules(); } addRule(type, rule) { if (type === 'domainBlock') { const list = GM_getValue('domainBlocks', []); if (rule.domain && !list.includes(rule.domain)) { list.push(rule.domain); GM_setValue('domainBlocks', list); this.invalidateDataCache(); BlockEngine.invalidateCache(); BlockEngine.applyCSSRules(); } return; } const data = this.getData()[type]; const isDuplicate = data.some(item => (type === 'regex' && item.regex === rule.regex && item.level === rule.level) || (type === 'static' && item.selector === rule.selector) || (type === 'dynamic' && item.className === rule.className) || (type === 'attribute' && item.attrSelector === rule.attrSelector) || (type === 'structural' && item.structSelector === rule.structSelector) || (type === 'complex' && JSON.stringify(item.conditions) === JSON.stringify(rule.conditions) && item.level === rule.level) || (type === 'pathPattern' && item.pattern === rule.pattern) ); if (!isDuplicate) { data.push(rule); this.saveData(type, data); } } removeRule(type, index) { if (type === 'domainBlock') { const list = GM_getValue('domainBlocks', []); if (list[index]) { list.splice(index, 1); GM_setValue('domainBlocks', list); this.invalidateDataCache(); BlockEngine.invalidateCache(); BlockEngine.applyCSSRules(); } return; } const data = this.getData()[type]; if (data[index]) { data.splice(index, 1); this.saveData(type, data); } } clearDomain() { ['blocks', 'dynamicBlocks', 'regexBlocks', 'attrBlocks', 'structBlocks', 'complexBlocks', 'pathPatternBlocks', 'config'].forEach(key => { const data = GM_getValue(key, {}); delete data[this.domain]; GM_setValue(key, data); }); if (this.flashList[this.domain]) { delete this.flashList[this.domain]; GM_setValue('pro_blocker_flash_domains', this.flashList); } BlockEngine.invalidateCache(); } exportAll() { const exportData = {}; ['blocks', 'dynamicBlocks', 'regexBlocks', 'attrBlocks', 'structBlocks', 'complexBlocks', 'pathPatternBlocks', 'config', 'pro_blocker_flash_domains'].forEach(key => { exportData[key] = GM_getValue(key, {}); }); exportData['domainBlocks'] = GM_getValue('domainBlocks', []); exportData['__meta__'] = { version: '0.9', exportTime: new Date().toISOString(), exporter: '网页元素屏蔽器' }; return JSON.stringify(exportData, null, 2); } importAll(jsonStr, merge = true) { let importData; try { importData = JSON.parse(jsonStr); } catch (e) { throw new Error('JSON 格式错误:' + e.message); } if (!importData || typeof importData !== 'object') { throw new Error('导入数据格式无效'); } if (!merge && !confirm('覆盖导入将清除现有所有规则,确定继续?')) return; const dictKeys = ['blocks', 'dynamicBlocks', 'regexBlocks', 'attrBlocks', 'structBlocks', 'complexBlocks', 'pathPatternBlocks', 'config', 'pro_blocker_flash_domains']; dictKeys.forEach(key => { if (!importData[key] || typeof importData[key] !== 'object') return; if (merge) { const existing = GM_getValue(key, {}); for (let d in importData[key]) { if (!Object.prototype.hasOwnProperty.call(importData[key], d)) continue; if (!existing[d]) { existing[d] = importData[key][d]; } else if (Array.isArray(existing[d]) && Array.isArray(importData[key][d])) { importData[key][d].forEach(item => { if (item && typeof item === 'object' && !existing[d].some(x => JSON.stringify(x) === JSON.stringify(item))) { existing[d].push(item); } }); } else { existing[d] = importData[key][d]; } } GM_setValue(key, existing); } else { GM_setValue(key, importData[key]); } }); if (Array.isArray(importData['domainBlocks'])) { const validDomains = importData['domainBlocks'].filter(d => typeof d === 'string' && d.length > 0 && d.length < 200); if (merge) { const existing = GM_getValue('domainBlocks', []); validDomains.forEach(d => { if (!existing.includes(d)) existing.push(d); }); GM_setValue('domainBlocks', existing); } else { GM_setValue('domainBlocks', validDomains); } } BlockEngine.invalidateCache(); this.invalidateDataCache(); BlockEngine.applyCSSRules(); BlockEngine.applyRegexRules(); BlockEngine.applyComplexRules(); } markAsFlashing() { if (!this.flashList[this.domain]) { this.flashList[this.domain] = true; GM_setValue('pro_blocker_flash_domains', this.flashList); } } toggleMode() { const currentMode = this.getData().config.mode; const nextMode = currentMode === 'auto' ? 'preemptive' : 'auto'; const allConfig = GM_getValue('config', {}); allConfig[this.domain] = { mode: nextMode }; GM_setValue('config', allConfig); this.invalidateDataCache(); return nextMode; } } const storage = new StorageManager(); /** * 拦截引擎:DOM/CSS 控制 + 动态扫描 */ class BlockEngine { static styleElementId = 'pro-blocker-core-css'; static _cachedDomainList = null; static _cachedPathPatterns = null; static _loggedDomains = new Set(); static _loggedPatterns = new Set(); static _addedNodesBuffer = []; static invalidateCache() { this._cachedDomainList = null; this._cachedPathPatterns = null; } // 始终在 document-start 注入 CSS,确保广告在首次渲染前即被隐藏 static fastInject() { this.applyCSSRules(); } static applyCSSRules() { const data = storage.getData(); let cssText = ''; const hideCSS = '{ display: none !important; opacity: 0 !important; visibility: hidden !important; pointer-events: none !important; z-index: -2147483648 !important; height: 0 !important; width: 0 !important; position: absolute !important; }\n'; data.static.forEach(r => r.selector && (cssText += `${r.selector} ${hideCSS}`)); data.dynamic.forEach(r => r.className && (cssText += `.${CSS.escape(r.className)} ${hideCSS}`)); data.attribute.forEach(r => r.attrSelector && (cssText += `${r.attrSelector} ${hideCSS}`)); data.structural.forEach(r => r.structSelector && (cssText += `${r.structSelector} ${hideCSS}`)); // 全局域名黑名单:覆盖所有可能携带资源 URL 的属性(含 srcset) data.domainBlock.forEach(domain => { if (!domain) return; const esc = escapeCSSAttr(domain); cssText += `[src*="${esc}"] ${hideCSS}`; cssText += `[href*="${esc}"] ${hideCSS}`; cssText += `[data-src*="${esc}"] ${hideCSS}`; cssText += `[data-original*="${esc}"] ${hideCSS}`; cssText += `[poster*="${esc}"] ${hideCSS}`; cssText += `[srcset*="${esc}"] ${hideCSS}`; }); // 路径模式拦截:典型广告跳转路径,如 /000/flink/url.php data.pathPattern.forEach(r => { if (r.pattern) { const esc = escapeCSSAttr(r.pattern); cssText += `[href*="${esc}"] ${hideCSS}`; cssText += `[src*="${esc}"] ${hideCSS}`; cssText += `[data-src*="${esc}"] ${hideCSS}`; } }); if (!cssText) return; let styleEl = document.getElementById(this.styleElementId); if (!styleEl) { styleEl = document.createElement('style'); styleEl.id = this.styleElementId; (document.head || document.documentElement).appendChild(styleEl); } if (styleEl.textContent !== cssText) { styleEl.textContent = cssText; } } /** * 动态拦截核心:扫描新增节点的资源域与路径模式,命中则隐藏整个广告容器 * 解决"刷新就复活"——动态生成的广告无法靠固定CSS规则拦截 */ static scanAndBlockDynamic(node, cachedDomainList, cachedPathPatterns) { const domainList = cachedDomainList !== undefined ? cachedDomainList : (this._cachedDomainList !== null ? this._cachedDomainList : GM_getValue('domainBlocks', [])); const pathPatterns = cachedPathPatterns !== undefined ? cachedPathPatterns : (this._cachedPathPatterns !== null ? this._cachedPathPatterns : storage.getData().pathPattern); if (this._cachedDomainList === null) this._cachedDomainList = domainList; if (this._cachedPathPatterns === null) this._cachedPathPatterns = pathPatterns; if (domainList.length === 0 && pathPatterns.length === 0) return; if (!node || node.nodeType !== Node.ELEMENT_NODE) return; const elements = [node]; try { node.querySelectorAll && node.querySelectorAll('img, iframe, video, script, a, source, embed, object').forEach(el => elements.push(el)); } catch (e) { } const currentHost = window.location.hostname; elements.forEach(el => { let blocked = false; let matchedDomain = ''; let matchedPattern = ''; // 收集所有可能的资源 URL(含 srcset 多 URL 拆分) const urls = [ el.src, el.href, el.getAttribute && el.getAttribute('data-src'), el.getAttribute && el.getAttribute('data-original'), el.getAttribute && el.getAttribute('poster') ].filter(Boolean); const srcset = el.getAttribute && el.getAttribute('srcset'); if (srcset) { srcset.split(',').forEach(part => { const url = part.trim().split(/\s+/)[0]; if (url) urls.push(url); }); } for (let url of urls) { try { for (let p of pathPatterns) { if (p.pattern && url.includes(p.pattern)) { blocked = true; matchedPattern = p.pattern; break; } } if (blocked) break; let absUrl = url; if (url.startsWith('//')) absUrl = location.protocol + url; if (absUrl.startsWith('http')) { const urlObj = new URL(absUrl); if (urlObj.hostname && urlObj.hostname !== currentHost && !urlObj.hostname.endsWith('.' + currentHost)) { if (domainList.some(d => urlObj.hostname === d || urlObj.hostname.endsWith('.' + d))) { blocked = true; matchedDomain = urlObj.hostname; break; } } } } catch (e) { } } if (blocked) { const target = this.findSingleChildWrapper(el, 4); target.style.setProperty('display', 'none', 'important'); target.style.setProperty('opacity', '0', 'important'); target.style.setProperty('visibility', 'hidden', 'important'); target.style.setProperty('pointer-events', 'none', 'important'); if (matchedDomain && !this._loggedDomains.has(matchedDomain)) { this._loggedDomains.add(matchedDomain); console.info(`[Pro Blocker] 动态拦截域名: ${matchedDomain}`); } if (matchedPattern && !this._loggedPatterns.has(matchedPattern)) { this._loggedPatterns.add(matchedPattern); console.info(`[Pro Blocker] 动态拦截路径: ${matchedPattern}`); } } }); } static _regexCache = new Map(); static getCompiledRegex(pattern) { if (this._regexCache.has(pattern)) return this._regexCache.get(pattern); try { const regex = new RegExp(pattern); this._regexCache.set(pattern, regex); return regex; } catch (e) { this._regexCache.set(pattern, null); return null; } } static applyRegexRules(targetNode = document.body) { const data = storage.getData(); if (!data.regex || data.regex.length === 0 || !targetNode) return; data.regex.forEach(rule => { const regex = this.getCompiledRegex(rule.regex); if (!regex) return; try { const walker = document.createTreeWalker(targetNode, NodeFilter.SHOW_TEXT, null, false); let node; while ((node = walker.nextNode())) { if (regex.test(node.textContent)) { let element = node.parentElement; for (let i = 0; i < rule.level; i++) { if (element.parentElement && element.parentElement !== document.body) { element = element.parentElement; } else break; } if (element && element.style.display !== 'none') { element.style.setProperty('display', 'none', 'important'); } } } } catch (e) { console.error('[Pro Blocker] 正则解析异常:', e); } }); } static applyComplexRules(targetNode = document.body) { const data = storage.getData(); if (!data.complex || data.complex.length === 0 || !targetNode) return; const root = targetNode.nodeType === Node.ELEMENT_NODE ? targetNode : targetNode.parentElement; if (!root) return; data.complex.forEach(rule => { try { let baseSelector = '*'; if (rule.logic === 'AND') { let parts = []; rule.conditions.forEach(c => { if (c.type === 'class' && c.operator === 'contains' && /^[a-zA-Z0-9\-_]+$/.test(c.value)) parts.push(`.${c.value}`); if (c.type === 'id' && c.operator === 'equals' && /^[a-zA-Z0-9\-_]+$/.test(c.value)) parts.push(`#${c.value}`); }); if (parts.length > 0) baseSelector = parts.join(''); } const elements = baseSelector === '*' ? root.querySelectorAll('div, span, a, p, img, li, ul, iframe, section, article, aside') : root.querySelectorAll(baseSelector); elements.forEach(el => { if (baseSelector === '*' && (el.textContent || '').length > 3000) return; const results = rule.conditions.map(c => { let val = ''; if (c.type === 'text') val = el.textContent || ''; else if (c.type === 'class') val = typeof el.className === 'string' ? el.className : ''; else if (c.type === 'id') val = el.id || ''; if (c.operator === 'contains') return val.includes(c.value); if (c.operator === 'not_contains') return val !== '' && !val.includes(c.value); if (c.operator === 'equals') return val.trim() === c.value.trim(); return false; }); const isMatch = rule.logic === 'AND' ? results.every(r => r) : results.some(r => r); if (isMatch) { let target = el; for (let i = 0; i < rule.level; i++) { if (target.parentElement && target.parentElement !== document.body && target.parentElement !== document.documentElement) { target = target.parentElement; } else break; } if (target.style.display !== 'none') { target.style.setProperty('display', 'none', 'important'); target.style.setProperty('opacity', '0', 'important'); } } }); } catch (e) { console.error('[Pro Blocker] 积木规则执行错误:', e); } }); } static startObserver() { const debouncedDynamicApply = debounce(() => { const rawNodes = this._addedNodesBuffer; this._addedNodesBuffer = []; if (rawNodes.length === 0) { this.applyRegexRules(); this.applyComplexRules(); return; } // 过滤游离节点 + 去除嵌套(子节点会被父节点的子树扫描覆盖) const nodes = rawNodes.filter(n => document.contains(n) && !rawNodes.some(other => other !== n && other.contains(n)) ); if (nodes.length === 0) { this.applyRegexRules(); this.applyComplexRules(); } else { nodes.forEach(node => { this.applyRegexRules(node); this.applyComplexRules(node); }); } }, 300, 2000); const observer = new MutationObserver((mutations) => { let hasAddedNodes = false; const batchNodes = []; for (let mutation of mutations) { if (mutation.addedNodes.length > 0) { hasAddedNodes = true; mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { batchNodes.push(node); this._addedNodesBuffer.push(node); } }); } } if (hasAddedNodes) { // 批量扫描:缓存只读一次,所有节点共用 const domainList = this._cachedDomainList !== null ? this._cachedDomainList : GM_getValue('domainBlocks', []); const pathPatterns = this._cachedPathPatterns !== null ? this._cachedPathPatterns : storage.getData().pathPattern; if (this._cachedDomainList === null) this._cachedDomainList = domainList; if (this._cachedPathPatterns === null) this._cachedPathPatterns = pathPatterns; batchNodes.forEach(node => this.scanAndBlockDynamic(node, domainList, pathPatterns)); // 确保 style 元素始终在 body 末尾以获得最高优先级 const styleEl = document.getElementById(this.styleElementId); if (styleEl && document.body && styleEl.parentElement !== document.body) { document.body.appendChild(styleEl); storage.markAsFlashing(); } debouncedDynamicApply(); } }); // body 就绪后立即启动全量扫描 + 观察器(不等 DOMContentLoaded,消除监控盲区) const startObserving = () => { this.applyCSSRules(); this.applyRegexRules(); this.applyComplexRules(); this.scanAndBlockDynamic(document.body); observer.observe(document.body, { childList: true, subtree: true }); }; if (document.body) { startObserving(); } else { const bodyObserver = new MutationObserver(() => { if (document.body) { bodyObserver.disconnect(); startObserving(); } }); bodyObserver.observe(document.documentElement, { childList: true }); } // DOMContentLoaded 时做一次全量补充扫描 window.addEventListener('DOMContentLoaded', () => { this.applyCSSRules(); this.applyRegexRules(); this.applyComplexRules(); this.scanAndBlockDynamic(document.body); }); } static generateOptimalSelector(element) { if (element.id && !/^\d/.test(element.id) && !/[a-zA-Z0-9]{8,}/.test(element.id)) return `#${CSS.escape(element.id)}`; let path = []; let current = element; while (current && current.nodeType === Node.ELEMENT_NODE && current.tagName.toLowerCase() !== 'body' && current.tagName.toLowerCase() !== 'html') { let selector = current.tagName.toLowerCase(); if (current.className && typeof current.className === 'string') { const classes = current.className.trim().split(/\s+/).filter(c => /^[a-zA-Z][a-zA-Z0-9\-_]*$/.test(c) && !/[a-zA-Z0-9]{10,}/.test(c)); if (classes.length > 0) selector += '.' + classes.map(c => CSS.escape(c)).join('.'); } let sibling = current, nth = 1; while (sibling = sibling.previousElementSibling) { if (sibling.tagName.toLowerCase() === current.tagName.toLowerCase()) nth++; } if (nth > 1) selector += `:nth-of-type(${nth})`; path.unshift(selector); current = current.parentElement; } return path.join(' > '); } static generateStructuralSelector(element) { let path = []; let current = element; while (current && current.nodeType === Node.ELEMENT_NODE && current.tagName.toLowerCase() !== 'html') { let tagName = current.tagName.toLowerCase(); if (tagName === 'body') { path.unshift('body'); break; } if (current.id && !/^\d/.test(current.id) && !/[a-zA-Z0-9]{8,}/.test(current.id)) { path.unshift(`#${CSS.escape(current.id)}`); break; } let nth = 1, sibling = current.previousElementSibling; while (sibling) { if (sibling.tagName === current.tagName) nth++; sibling = sibling.previousElementSibling; } path.unshift(`${tagName}:nth-of-type(${nth})`); current = current.parentElement; } return path.join(' > '); } /** * 增强版资源域识别:递归查找元素内所有第三方资源域名(含 srcset) */ static extractResourceDomains(element) { const urls = new Set(); const domains = new Set(); if (!element) return { urls: [], domains: [] }; const collect = (el) => { if (!el || el.nodeType !== Node.ELEMENT_NODE) return; const attrs = ['src', 'href', 'data-src', 'data-original', 'poster']; attrs.forEach(attr => { const val = el.getAttribute(attr); if (val) urls.add(val); }); const srcset = el.getAttribute('srcset'); if (srcset) { srcset.split(',').forEach(part => { const url = part.trim().split(/\s+/)[0]; if (url) urls.add(url); }); } const bg = el.style && el.style.backgroundImage; if (bg && bg.includes('url(')) { const matches = bg.match(/url\(["']?([^"')]+)["']?\)/g); if (matches) matches.forEach(m => { const url = m.replace(/url\(["']?|["']?\)/g, ''); if (url) urls.add(url); }); } }; collect(element); try { element.querySelectorAll && element.querySelectorAll('img, iframe, video, source, embed, a, script').forEach(collect); } catch (e) { } const currentHost = window.location.hostname; urls.forEach(url => { try { if (url.startsWith('data:')) return; let absUrl = url; if (url.startsWith('//')) absUrl = location.protocol + url; if (!absUrl.startsWith('http')) return; const urlObj = new URL(absUrl); if (urlObj.hostname && urlObj.hostname !== currentHost && !urlObj.hostname.endsWith('.' + currentHost)) { domains.add(urlObj.hostname); } } catch (e) { } }); return { urls: Array.from(urls), domains: Array.from(domains) }; } static isSafeOutermost(element) { if (!element || !element.parentElement) return true; const parent = element.parentElement; if (parent === document.body || parent === document.documentElement) return true; return false; } /** * 沿单子链向上查找包裹容器:父级仅含一个元素子节点时继续向上 * 遇到多子分支或 body/html 时停止。maxDepth 防止极端深度 */ static findSingleChildWrapper(element, maxDepth = 6) { let target = element; let depth = 0; while (target.parentElement && target.parentElement !== document.body && target.parentElement !== document.documentElement && depth < maxDepth) { const parent = target.parentElement; if (parent.children.length === 1) { target = parent; depth++; } else break; } return target; } /** * 智能查找广告最外层容器:沿单子链向上,遇到多子分支即停止 */ static findOutermostAdContainer(element) { return this.findSingleChildWrapper(element, 50); } } /** * 用户交互界面:基于 Shadow DOM 隔离 */ class UIManager { constructor() { const existingHost = document.getElementById('pro-blocker-ui-host'); if (existingHost) existingHost.remove(); this.shadowHost = document.createElement('div'); this.shadowHost.id = 'pro-blocker-ui-host'; this.shadowHost.style.cssText = 'position: fixed; z-index: 2147483647; top: 0; left: 0; width: 0; height: 0; overflow: visible;'; this.shadowRoot = this.shadowHost.attachShadow({ mode: 'closed' }); this.injectStyles(); document.documentElement.appendChild(this.shadowHost); this.highlightEl = null; this.currentSelectedEl = null; this.originalSelectedEl = null; this.selectionStack = []; this._previewAffectedElements = []; this._actionPreview = { active: false, el: null }; this._contextmenuHandler = null; this._handleMouseOver = this._handleMouseOver.bind(this); this._handleClick = this._handleClick.bind(this); } injectStyles() { const style = document.createElement('style'); style.textContent = ` :host { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } .panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(25, 25, 30, 0.62); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); border: 1px solid rgba(255,255,255,0.14); padding: 20px; border-radius: 14px; box-shadow: 0 16px 56px rgba(0,0,0,0.35), inset 0 0 0 1px rgba(255,255,255,0.06); width: 480px; max-width: calc(100vw - 24px); max-height: 88vh; overflow-y: auto; color: #eee; text-shadow: 0 1px 2px rgba(0,0,0,0.8); } @media (max-width: 600px) { .panel { background: rgba(25, 25, 30, 0.52); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); max-width: calc(100vw - 48px); max-height: 70vh; padding: 16px; } } h3 { margin-top: 0; font-size: 17px; font-weight: 600; color: #fff; margin-bottom: 14px; border-bottom: 1px solid rgba(255,255,255,0.12); padding-bottom: 10px; cursor: grab; user-select: none; } h3:active { cursor: grabbing; } p { font-size: 13px; margin: 0 0 12px 0; color: #ccc; line-height: 1.5; word-break: break-all; } .code-box { background: rgba(0, 0, 0, 0.22); border: 1px solid rgba(255,255,255,0.1); padding: 8px 10px; border-radius: 6px; font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 11px; margin-top: 4px; display: block; max-height: 96px; overflow-y: auto; word-break: break-all; line-height: 1.5; color: #ddd; } .btn-group { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; } button { padding: 9px 12px; border: 1px solid rgba(255,255,255,0.18); border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 500; transition: filter 0.15s, transform 0.1s; flex: 1; display: flex; align-items: center; justify-content: center; line-height: 1.2; background: rgba(255,255,255,0.1); color: #fff; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); text-shadow: 0 1px 2px rgba(0,0,0,0.4); } button:hover:not(:disabled) { filter: brightness(1.15); transform: translateY(-1px); } button:active:not(:disabled) { transform: translateY(0); filter: brightness(0.95); } button:disabled { opacity: 0.25; cursor: not-allowed; } .btn-primary { background: rgba(0,122,255,0.72); color: #fff; } .btn-success { background: rgba(52,199,89,0.72); color: #fff; } .btn-danger { background: rgba(255,59,48,0.72); color: #fff; } .btn-warning { background: rgba(255,149,0,0.72); color: #fff; } .btn-dark { background: rgba(80,86,94,0.72); color: #fff; } .btn-info { background: rgba(23,162,184,0.72); color: #fff; } .btn-outline { background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.25); color: #fff; } label { font-size: 13px; font-weight: 600; display: block; margin-bottom: 6px; color: #ddd; } input[type="text"], input[type="number"], select, textarea { width: 100%; padding: 10px; margin-bottom: 14px; border: 1px solid rgba(255,255,255,0.14); border-radius: 8px; box-sizing: border-box; outline: none; font-size: 14px; transition: border-color 0.2s, box-shadow 0.2s; font-family: inherit; background: rgba(0,0,0,0.25); color: #eee; } input[type="text"]:focus, input[type="number"]:focus, select:focus, textarea:focus { border-color: #4aa3ff; box-shadow: 0 0 0 3px rgba(74,163,255,0.18); } textarea { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 12px; resize: vertical; min-height: 140px; } .rule-list { list-style: none; padding: 0; margin: 0; } .rule-item { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 10px 12px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; margin-bottom: 6px; font-size: 12px; word-break: break-all; color: #eee; } .rule-content { flex: 1; padding-right: 6px; } .tag { font-size: 11px; padding: 2px 7px; border-radius: 10px; background: rgba(255,255,255,0.12); margin-right: 6px; font-weight: bold; white-space: nowrap; color: #fff; } .tag.attr { background: rgba(225,190,231,0.35); color: #f3e5f5; } .tag.struct { background: rgba(255,224,130,0.35); color: #fff8e1; } .tag.complex { background: rgba(227,242,253,0.35); color: #e3f2fd; } .tag.domain { background: rgba(255,205,210,0.35); color: #ffebee; } .tag.path { background: rgba(200,230,201,0.35); color: #e8f5e9; } .status-bar { padding: 11px 12px; background: rgba(255,255,255,0.06); border-radius: 8px; margin-bottom: 14px; font-size: 12px; border: 1px solid rgba(255,255,255,0.1); line-height: 1.7; color: #ccc; } .zoom-bar { display: flex; gap: 6px; padding: 8px; background: rgba(255,193,7,0.1); border: 1px solid rgba(255,193,7,0.35); border-radius: 10px; margin-bottom: 12px; align-items: center; } .zoom-bar button { flex: 1; padding: 8px 6px; font-size: 13px; font-weight: 600; } .zoom-bar button#btn-zoom-reset { flex: 0 0 auto; padding: 8px 12px; } .selection-info { background: rgba(255,255,255,0.06); border-left: 4px solid #FF6F00; padding: 11px 12px; border-radius: 6px; margin-bottom: 12px; font-size: 12px; line-height: 1.6; color: #ddd; } .selection-info .info-row { margin: 4px 0; } .selection-info .info-label { font-weight: 600; color: #ffb74d; display: block; margin-top: 6px; margin-bottom: 2px; } .selection-info .info-row:first-child .info-label { margin-top: 0; } .selection-info .domain-item { display: inline-block; background: rgba(255,111,0,0.7); color: white; padding: 2px 9px; border-radius: 12px; margin: 3px 4px 0 0; font-size: 11px; word-break: break-all; font-weight: 500; cursor: pointer; transition: filter 0.15s; } .selection-info .domain-item:hover { filter: brightness(1.15); } .domain-scroll { display: flex; flex-direction: column; gap: 6px; max-height: 180px; overflow-y: auto; padding-right: 4px; } .domain-scroll .domain-item { display: flex; justify-content: space-between; align-items: center; width: 100%; box-sizing: border-box; margin: 0; padding: 6px 10px; border-radius: 8px; font-size: 12px; } .domain-scroll::-webkit-scrollbar { width: 4px; } .domain-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 2px; } .safe-flag { display: inline-block; background: rgba(40,167,69,0.75); color: white; padding: 2px 9px; border-radius: 12px; font-size: 10px; margin-left: 6px; font-weight: 600; } .level-info { color: #ffd54f; } .level-info b { color: #ff8a80; font-size: 13px; } .section-divider { height: 1px; background: rgba(255,255,255,0.1); margin: 14px 0 10px; } .empty-tip { text-align: center; color: #aaa; margin: 24px 0; font-size: 13px; } `; this.shadowRoot.appendChild(style); } injectHighlightStyle() { let style = document.getElementById('pro-blocker-highlight-style'); if (!style) { style = document.createElement('style'); style.id = 'pro-blocker-highlight-style'; style.textContent = ` .pro-blocker-highlight { outline: 3px solid #FF3B30 !important; outline-offset: -3px !important; background-color: rgba(255, 59, 48, 0.15) !important; cursor: crosshair !important; transition: outline 0.1s ease-in-out !important; box-shadow: 0 0 10px rgba(255,59,48,0.5) !important; } /* 当前选中元素:加粗红框 + 多层发光,十分明显 */ .pro-blocker-selected { outline: 6px solid #FF0000 !important; outline-offset: -6px !important; background-color: rgba(255, 0, 0, 0.03) !important; box-shadow: 0 0 0 3px #FF0000, 0 0 0 7px rgba(255,0,0,0.35), 0 0 30px rgba(255,0,0,0.9) !important; } `; (document.head || document.documentElement).appendChild(style); } } makeDraggable(panel) { const header = panel.querySelector('h3'); if (!header) return; let isDragging = false; let startX, startY, initialLeft, initialTop; const onMouseDown = (e) => { if (e.target.closest('button, input, select, textarea')) return; if (e.target !== header && !header.contains(e.target)) return; isDragging = true; const rect = panel.getBoundingClientRect(); panel.style.transform = 'none'; panel.style.left = rect.left + 'px'; panel.style.top = rect.top + 'px'; startX = e.clientX; startY = e.clientY; initialLeft = rect.left; initialTop = rect.top; e.preventDefault(); }; const onMouseMove = (e) => { if (!isDragging) return; panel.style.left = `${initialLeft + (e.clientX - startX)}px`; panel.style.top = `${initialTop + (e.clientY - startY)}px`; }; const onMouseUp = () => { isDragging = false; }; header.addEventListener('mousedown', onMouseDown); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); panel._cleanupDrag = () => { header.removeEventListener('mousedown', onMouseDown); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); isDragging = false; }; } _whenBodyReady(cb) { if (document.body) { cb(); return; } const observer = new MutationObserver(() => { if (document.body) { observer.disconnect(); cb(); } }); observer.observe(document.documentElement, { childList: true }); } startSelection() { this.stopSelection(); this.injectHighlightStyle(); // 存储引用以便 stopSelection 能正确移除 this._contextmenuHandler = (e) => { e.preventDefault(); this.stopSelection(); }; this._keydownHandler = (e) => { if (e.key === 'Escape') this.stopSelection(); }; // 移动端 touch 事件处理函数绑定(必须在 body 就绪前完成绑定以保持引用一致) this._handleTouchStart = this._handleTouchStart.bind(this); this._handleTouchMove = this._handleTouchMove.bind(this); this._handleTouchEnd = this._handleTouchEnd.bind(this); document.addEventListener('keydown', this._keydownHandler); this._whenBodyReady(() => { document.body.addEventListener('mouseover', this._handleMouseOver, true); document.body.addEventListener('click', this._handleClick, true); document.body.addEventListener('contextmenu', this._contextmenuHandler); // 移动端:拦截 touch 事件,防止广告通过触屏直接跳转;用 touchend 选定元素 document.body.addEventListener('touchstart', this._handleTouchStart, { capture: true, passive: false }); document.body.addEventListener('touchmove', this._handleTouchMove, { capture: true, passive: false }); document.body.addEventListener('touchend', this._handleTouchEnd, { capture: true, passive: false }); }); } stopSelection() { if (this._keydownHandler) { document.removeEventListener('keydown', this._keydownHandler); this._keydownHandler = null; } if (!document.body) return; document.body.removeEventListener('mouseover', this._handleMouseOver, true); document.body.removeEventListener('click', this._handleClick, true); if (this._contextmenuHandler) { document.body.removeEventListener('contextmenu', this._contextmenuHandler); this._contextmenuHandler = null; } // 移除移动端 touch 监听 if (this._handleTouchStart) { document.body.removeEventListener('touchstart', this._handleTouchStart, { capture: true }); } if (this._handleTouchMove) { document.body.removeEventListener('touchmove', this._handleTouchMove, { capture: true }); } if (this._handleTouchEnd) { document.body.removeEventListener('touchend', this._handleTouchEnd, { capture: true }); } if (this.highlightEl) { this.highlightEl.classList.remove('pro-blocker-highlight'); this.highlightEl = null; } } _handleMouseOver(e) { if (!e.target || !e.target.closest || e.target.closest('#pro-blocker-ui-host')) return; if (this.highlightEl) this.highlightEl.classList.remove('pro-blocker-highlight'); this.highlightEl = e.target; this.highlightEl.classList.add('pro-blocker-highlight'); } // 触屏移动设备:通过 touchmove 实时更新高亮(替代 mouseover) _handleTouchMove(e) { if (!e.touches || e.touches.length === 0) return; const touch = e.touches[0]; const target = document.elementFromPoint(touch.clientX, touch.clientY); if (!target || !target.closest || target.closest('#pro-blocker-ui-host')) return; if (this.highlightEl) this.highlightEl.classList.remove('pro-blocker-highlight'); this.highlightEl = target; this.highlightEl.classList.add('pro-blocker-highlight'); // 阻止页面滚动,确保手指抬起时位置仍是目标元素 e.preventDefault(); } // 触屏抬起时选定元素(移动端的"点击") _handleTouchEnd(e) { if (!e.changedTouches || e.changedTouches.length === 0) return; const touch = e.changedTouches[0]; const target = document.elementFromPoint(touch.clientX, touch.clientY); if (!target || !target.closest || target.closest('#pro-blocker-ui-host')) return; e.preventDefault(); e.stopPropagation(); this.stopSelection(); this.showActionPanel(target); } // 阻止 touchstart 默认行为,防止广告通过 touch 事件直接触发跳转 _handleTouchStart(e) { if (!e.target || !e.target.closest || e.target.closest('#pro-blocker-ui-host')) return; e.preventDefault(); e.stopPropagation(); } _handleClick(e) { e.preventDefault(); e.stopPropagation(); if (!e.target || !e.target.closest || e.target.closest('#pro-blocker-ui-host')) return; this.stopSelection(); this.showActionPanel(e.target); } _clearSelectionHighlight() { if (this.currentSelectedEl) { this.currentSelectedEl.classList.remove('pro-blocker-selected'); } } _isElementInDOM(el) { return el && document.contains(el); } _applySelectionHighlight(element) { this._clearSelectionHighlight(); this.currentSelectedEl = element; element.classList.add('pro-blocker-selected'); try { element.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } catch (e) { } } _resetActionPreview(panel) { if (!this._actionPreview.active) return; const el = this._actionPreview.el; if (el) { el.style.removeProperty('display'); el.classList.remove('pro-blocker-selected'); } this._actionPreview = { active: false, el: null }; const btn = panel.querySelector('#btn-preview'); if (btn) btn.textContent = '🔍 预览效果'; } _refreshSelectionInfo(panel) { const el = this.currentSelectedEl; if (!el) return; const selector = BlockEngine.generateOptimalSelector(el); const structSelector = BlockEngine.generateStructuralSelector(el); const resourceResult = BlockEngine.extractResourceDomains(el); const isSafeOuter = BlockEngine.isSafeOutermost(el); const canZoomOut = this.selectionStack.length > 0; const canZoomIn = !isSafeOuter && !!el.parentElement; const pathBox = panel.querySelector('#info-selector'); if (pathBox) pathBox.textContent = selector; const structBox = panel.querySelector('#info-struct'); if (structBox) structBox.textContent = structSelector; const levelInfo = panel.querySelector('#info-level'); if (levelInfo) { const depth = this.selectionStack.length; const depthText = depth === 0 ? '自身(初始选择)' : `向上 ${depth} 层`; levelInfo.innerHTML = `当前层级:${depth} · ${depthText}` + (isSafeOuter ? '✓ 已到 DOM 最顶层' : ''); } const domainBox = panel.querySelector('#info-domains'); if (domainBox) { if (resourceResult.domains.length > 0) { domainBox.innerHTML = '🔍 发现第三方资源域:' + resourceResult.domains.map(d => `${d}`).join(''); } else { domainBox.innerHTML = '🔍 当前框选范围内未发现第三方资源域'; } } const btnZoomIn = panel.querySelector('#btn-zoom-in'); const btnZoomOut = panel.querySelector('#btn-zoom-out'); const btnZoomReset = panel.querySelector('#btn-zoom-reset'); const btnAutoOuter = panel.querySelector('#btn-auto-outer'); if (btnZoomIn) btnZoomIn.disabled = !canZoomIn; if (btnZoomOut) btnZoomOut.disabled = !canZoomOut; if (btnZoomReset) btnZoomReset.disabled = (this.selectionStack.length === 0); if (btnAutoOuter) btnAutoOuter.disabled = isSafeOuter; const btnDomain = panel.querySelector('#btn-domain'); if (btnDomain) { if (resourceResult.domains.length > 0) { btnDomain.disabled = false; btnDomain.textContent = `🔥 彻底封杀 ${resourceResult.domains.length} 个广告域名(推荐)`; } else { btnDomain.disabled = true; btnDomain.textContent = '🔥 当前框选未发现第三方域名'; } } } showActionPanel(element) { this.clearPanel(); this.injectHighlightStyle(); this.originalSelectedEl = element; this.currentSelectedEl = element; this.selectionStack = []; this._actionPreview = { active: false, el: null }; const panel = document.createElement('div'); panel.className = 'panel'; panel.innerHTML = `
已扫描全页第三方资源域,点击域名标签可将其从封杀列表中移除。
通过组合条件、正则表达式或路径模式,实现对复杂动态广告的精准拦截。
当前暂无屏蔽规则
'; } else { rulesHTML += '下方文本框包含全部拦截规则与全局域名黑名单。复制后保存到任意位置,或在新设备的脚本中通过"导入规则"粘贴即可。
将之前导出的规则 JSON 文本粘贴到下方文本框,选择导入模式后点击"开始导入"。