您现在的位置是:网站首页 > 博客日记 >

根据xpath标识广告油猴脚本|屏蔽广告

作者:YXN-js 阅读量:346 发布日期:2025-02-26

localStorage版

// ==UserScript==
// @name         XPath元素阻止程序_localStorage版
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  localStorage版元素屏蔽工具,支持XPath规则管理
// @author       YXN
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 生成网站专属存储键名
    const storageKey = `xpath_blocker_${location.hostname}`;

    // 样式增强
    const css = `
        #blocker-btn {
            position: fixed;
            bottom: 30px;
            right: 30px;
            z-index: 2147483647;
            width: 45px;
            height: 45px;
            border-radius: 50%;
            background: #2196F3;
            color: white;
            border: none;
            cursor: pointer;
            box-shadow: 0 3px 6px rgba(0,0,0,0.16);
            font-size: 20px;
            transition: transform 0.2s;
        }
        .blocker-dialog {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 8px 24px rgba(0,0,0,0.2);
            z-index: 2147483646;
            min-width: 320px;
        }
        .rule-item {
            display: flex;
            align-items: center;
            padding: 8px;
            border-bottom: 1px solid #eee;
        }
        .rule-index {
            width: 30px;
            color: #666;
        }
        .rule-text {
            flex: 1;
            font-family: monospace;
            overflow-x: auto;
        }
        .delete-btn {
            color: #f44336;
            cursor: pointer;
            margin-left: 10px;
        }
    `;

    // 注入全局样式
    const style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    class BlockerSystem {
        constructor() {
            this.rules = this.loadRules();
            this.initUI();
            this.setupObserver();
        }

        // 从localStorage加载规则
        loadRules() {
            try {
                return JSON.parse(localStorage.getItem(storageKey)) || [];
            } catch {
                return [];
            }
        }

        // 保存规则到localStorage
        saveRules() {
            localStorage.setItem(storageKey, JSON.stringify(this.rules));
        }

        // 初始化界面
        initUI() {
            this.createButton();
            this.applyRules();
        }

        // 创建悬浮按钮
        createButton() {
            this.btn = document.createElement('button');
            this.btn.id = 'blocker-btn';
            this.btn.textContent = '✖';
            this.btn.addEventListener('click', () => this.showMainDialog());
            document.body.appendChild(this.btn);
        }

        // 显示主对话框
        showMainDialog() {
            const dialog = document.createElement('div');
            dialog.className = 'blocker-dialog';
            dialog.innerHTML = `
                <h3>元素屏蔽系统</h3>
                <div style="margin:15px 0">
                    <button id="add-rule">➕ 添加新规则</button>
                    <button id="manage-rules">???? 管理规则</button>
                </div>
            `;

            dialog.querySelector('#add-rule').addEventListener('click', () => this.showAddDialog());
            dialog.querySelector('#manage-rules').addEventListener('click', () => this.showRuleManager());

            this.showModal(dialog);
        }

        // 显示添加规则对话框
        showAddDialog() {
            const dialog = document.createElement('div');
            dialog.className = 'blocker-dialog';
            dialog.innerHTML = `
                <h3>添加XPath规则</h3>
                <textarea
                    id="xpath-input"
                    placeholder="输入XPath表达式(每行一个规则)"
                    style="width:100%; height:100px; margin:10px 0"
                ></textarea>
                <div style="text-align:right">
                    <button class="cancel">取消</button>
                    <button class="confirm">保存</button>
                </div>
            `;

            dialog.querySelector('.cancel').addEventListener('click', () => dialog.remove());
            dialog.querySelector('.confirm').addEventListener('click', () => {
                const input = dialog.querySelector('textarea').value;
                this.addRules(input.split('\n').map(x => x.trim()).filter(x => x));
                dialog.remove();
                this.applyRules();
            });

            this.showModal(dialog);
        }

        // 显示规则管理器
        showRuleManager() {
            const dialog = document.createElement('div');
            dialog.className = 'blocker-dialog';
            dialog.innerHTML = `
                <h3>已保存规则(共${this.rules.length}条)</h3>
                <div id="rule-list" style="max-height:300px;overflow-y:auto;margin:10px 0"></div>
                <div style="text-align:right">
                    <button class="cancel">关闭</button>
                </div>
            `;

            const listContainer = dialog.querySelector('#rule-list');
            this.rules.forEach((rule, index) => {
                const item = document.createElement('div');
                item.className = 'rule-item';
                item.innerHTML = `
                    <div class="rule-index">${index + 1}.</div>
                    <div class="rule-text">${rule}</div>
                    <div class="delete-btn" data-index="${index}">????️</div>
                `;
                item.querySelector('.delete-btn').addEventListener('click', (e) => {
                    const index = parseInt(e.target.dataset.index);
                    this.deleteRule(index);
                    item.remove();
                });
                listContainer.appendChild(item);
            });

            dialog.querySelector('.cancel').addEventListener('click', () => dialog.remove());
            this.showModal(dialog);
        }

        // 显示模态框
        showModal(content) {
            const overlay = document.createElement('div');
            overlay.style = `
                position:fixed;
                top:0;
                left:0;
                width:100%;
                height:100%;
                background:rgba(0,0,0,0.5);
                z-index:2147483645;
            `;
            overlay.appendChild(content);
            overlay.addEventListener('click', (e) => {
                if (e.target === overlay) overlay.remove();
            });
            document.body.appendChild(overlay);
        }

        // 添加新规则
        addRules(newRules) {
            newRules.forEach(rule => {
                if (!this.rules.includes(rule)) {
                    this.rules.push(rule);
                }
            });
            this.saveRules();
        }

        // 删除单条规则
        deleteRule(index) {
            if (index >= 0 && index < this.rules.length) {
                this.rules.splice(index, 1);
                this.saveRules();
                this.applyRules();
            }
        }

        // 应用所有规则
        applyRules() {
            this.rules.forEach(rule => {
                try {
                    const nodes = document.evaluate(
                        rule,
                        document,
                        null,
                        XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
                        null
                    );
                    for (let i = 0; i < nodes.snapshotLength; i++) {
                        const node = nodes.snapshotItem(i);
                        node.style.display = 'none';
                    }
                } catch (e) {
                    console.warn(`无效的XPath规则: ${rule}`, e);
                }
            });
        }

        // 设置DOM监听
        setupObserver() {
            new MutationObserver(() => {
                this.applyRules();
            }).observe(document, {
                childList: true,
                subtree: true
            });
        }
    }

    // 初始化系统
    new BlockerSystem();
})();

GM存储版

// ==UserScript==
// @name         元素隐藏
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  支持GM存储和规则管理的元素屏蔽工具,支持XPath、CSS选择器和正则域名匹配
// @author       yxn
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // 存储键名
    const RULES_KEY = 'element_blocker_rules';
    const SETTINGS_KEY = 'element_blocker_settings';

    // 样式配置
    GM_addStyle(`
        #blocker-btn {
            position: fixed;
            bottom: 30px;
            right: 30px;
            z-index: 2147483647;
            width: 45px;
            height: 45px;
            border-radius: 50%;
            background: #2196F3;
            color: white;
            border: none;
            cursor: pointer;
            box-shadow: 0 3px 6px rgba(0,0,0,0.16);
            font-size: 20px;
            transition: transform 0.2s;
        }
        #blocker-btn:hover {
            transform: scale(1.1);
        }
        .blocker-dialog {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 8px 24px rgba(0,0,0,0.2);
            z-index: 2147483646;
            min-width: 500px;
            max-width: 90vw;
            max-height: 90vh;
            overflow-y: auto;
        }
        .blocker-textarea {
            width: 100%;
            height: 120px;
            margin: 12px 0;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
            resize: vertical;
            font-family: monospace;
        }
        .blocker-input {
            width: 100%;
            margin: 8px 0;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-family: monospace;
        }
        .blocker-btns {
            display: flex;
            gap: 8px;
            justify-content: flex-end;
            margin-top: 15px;
        }
        .blocker-btn {
            padding: 8px 16px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        .blocker-confirm {
            background: #4CAF50;
            color: white;
        }
        .blocker-cancel {
            background: #f44336;
            color: white;
        }
        .blocker-selector-type {
            margin: 10px 0;
            display: flex;
            gap: 15px;
            align-items: center;
        }
        .blocker-rule-item {
            padding: 10px;
            border-bottom: 1px solid #eee;
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
        }
        .blocker-rule-content {
            flex: 1;
            word-break: break-all;
        }
        .blocker-rule-type {
            background: #e0e0e0;
            padding: 2px 6px;
            border-radius: 3px;
            font-size: 0.8em;
            margin-right: 8px;
        }
        .blocker-rule-domain {
            background: #bbdefb;
            padding: 2px 6px;
            border-radius: 3px;
            font-size: 0.8em;
            margin-right: 8px;
        }
        .blocker-rule-regex {
            background: #c8e6c9;
            padding: 2px 6px;
            border-radius: 3px;
            font-size: 0.8em;
            margin-right: 8px;
        }
        .blocker-rule-actions {
            display: flex;
            gap: 5px;
            margin-left: 10px;
        }
        .blocker-delete-btn {
            background: #ff4444;
            color: white;
            border: none;
            border-radius: 3px;
            padding: 2px 6px;
            cursor: pointer;
            font-size: 0.8em;
        }
        .blocker-edit-btn {
            background: #ff9800;
            color: white;
            border: none;
            border-radius: 3px;
            padding: 2px 6px;
            cursor: pointer;
            font-size: 0.8em;
        }
        .blocker-help {
            font-size: 0.9em;
            color: #666;
            margin: 5px 0;
        }
        .blocker-tabs {
            display: flex;
            border-bottom: 1px solid #ddd;
            margin-bottom: 15px;
        }
        .blocker-tab {
            padding: 8px 16px;
            cursor: pointer;
            border: none;
            background: none;
            border-bottom: 2px solid transparent;
        }
        .blocker-tab.active {
            border-bottom-color: #2196F3;
            color: #2196F3;
        }
        .blocker-tab-content {
            display: none;
        }
        .blocker-tab-content.active {
            display: block;
        }
        .blocker-domain-type {
            margin: 10px 0;
            display: flex;
            gap: 15px;
            align-items: center;
        }
        .blocker-examples {
            background: #f5f5f5;
            padding: 10px;
            border-radius: 4px;
            margin: 10px 0;
            font-size: 0.85em;
        }
        .blocker-example-item {
            margin: 5px 0;
            font-family: monospace;
        }
        .blocker-test-regex {
            background: #e3f2fd;
            padding: 10px;
            border-radius: 4px;
            margin: 10px 0;
        }
        .blocker-test-result {
            margin-top: 5px;
            padding: 5px;
            border-radius: 3px;
            font-weight: bold;
        }
        .blocker-test-valid {
            background: #c8e6c9;
            color: #2e7d32;
        }
        .blocker-test-invalid {
            background: #ffcdd2;
            color: #c62828;
        }
    `);

    class ElementBlocker {
        constructor() {
            this.rules = GM_getValue(RULES_KEY, []);
            this.settings = GM_getValue(SETTINGS_KEY, {
                enableGlobalRules: true
            });
            this.observer = null;
            this.currentHostname = location.hostname;
            this.init();
        }

        init() {
            // 初始化界面
            this.createButton();

            // 应用现有规则
            this.applyRules();

            // 监听动态内容
            this.setupMutationObserver();

            // 注册管理命令
            GM_registerMenuCommand('管理屏蔽规则', () => this.showRuleManager());
            GM_registerMenuCommand('设置', () => this.showSettings());
        }

        createButton() {
            this.btn = document.createElement('button');
            this.btn.id = 'blocker-btn';
            this.btn.textContent = '⛔';
            this.btn.title = '元素屏蔽器';
            this.btn.addEventListener('click', () => this.showDialog());
            document.body.appendChild(this.btn);
        }

        showDialog() {
            if (this.dialog) return;

            this.dialog = document.createElement('div');
            this.dialog.className = 'blocker-dialog';
            this.dialog.innerHTML = `
                <h3 style="margin:0 0 15px">元素屏蔽器</h3>
                <div class="blocker-tabs">
                    <button class="blocker-tab active" data-tab="add-rule">添加规则</button>
                    <button class="blocker-tab" data-tab="quick-rule">快速添加</button>
                </div>

                <div class="blocker-tab-content active" id="add-rule">
                    <div class="blocker-selector-type">
                        <label>
                            <input type="radio" name="selectorType" value="xpath" checked> XPath
                        </label>
                        <label>
                            <input type="radio" name="selectorType" value="css"> CSS选择器
                        </label>
                    </div>
                    <textarea class="blocker-textarea"
                        placeholder="输入要屏蔽的元素选择器(每行一个)&#10;例如XPath: //div[@class='ad']&#10;例如CSS: .ad-banner, #sidebar-ad"></textarea>

                    <div class="blocker-domain-type">
                        <label>
                            <input type="radio" name="domainType" value="normal" checked> 普通域名
                        </label>
                        <label>
                            <input type="radio" name="domainType" value="regex"> 正则表达式
                        </label>
                    </div>

                    <input type="text" class="blocker-input" id="domains-input"
                        placeholder="适用域名(多个用逗号分隔,留空表示当前域名)"
                        value="${this.currentHostname}">

                    <div class="blocker-test-regex" id="regex-test-area" style="display: none;">
                        <div>正则表达式测试:</div>
                        <input type="text" class="blocker-input" id="test-domain-input"
                            placeholder="输入要测试的域名" value="${this.currentHostname}">
                        <button class="blocker-btn" id="test-regex-btn" style="padding: 4px 8px; margin-top: 5px;">测试正则表达式</button>
                        <div id="regex-test-result"></div>
                    </div>

                    <div class="blocker-help" id="domain-help">
                        提示:每行输入一个选择器,支持XPath和CSS两种格式
                    </div>

                    <div class="blocker-examples" id="regex-examples" style="display: none;">
                        <strong>正则表达式示例:</strong>
                        <div class="blocker-example-item">^example\\.com$ - 精确匹配example.com</div>
                        <div class="blocker-example-item">.*\\.example\\.com$ - 匹配所有example.com的子域名</div>
                        <div class="blocker-example-item">(example\\.com|test\\.com) - 匹配example.com或test.com</div>
                        <div class="blocker-example-item">.*\\.(com|net)$ - 匹配所有.com或.net域名</div>
                        <div class="blocker-example-item">\\d+ - 匹配包含数字的域名</div>
                    </div>

                    <div class="blocker-btns">
                        <button class="blocker-btn blocker-cancel">取消</button>
                        <button class="blocker-btn blocker-confirm">确定</button>
                    </div>
                </div>

                <div class="blocker-tab-content" id="quick-rule">
                    <div class="blocker-selector-type">
                        <label>
                            <input type="radio" name="quickSelectorType" value="css" checked> CSS选择器
                        </label>
                    </div>
                    <input type="text" class="blocker-input" id="quick-rule-input"
                        placeholder="输入CSS选择器,如:.ad, #banner">

                    <div class="blocker-domain-type">
                        <label>
                            <input type="radio" name="quickDomainType" value="normal" checked> 普通域名
                        </label>
                        <label>
                            <input type="radio" name="quickDomainType" value="regex"> 正则表达式
                        </label>
                    </div>

                    <input type="text" class="blocker-input" id="quick-domains-input"
                        placeholder="适用域名(多个用逗号分隔,留空表示当前域名)"
                        value="${this.currentHostname}">

                    <div class="blocker-help">提示:快速添加单个CSS规则</div>

                    <div class="blocker-btns">
                        <button class="blocker-btn blocker-cancel">取消</button>
                        <button class="blocker-btn blocker-confirm" id="quick-confirm">确定</button>
                    </div>
                </div>
            `;

            // 域名类型切换
            const updateDomainHelp = () => {
                const domainType = this.dialog.querySelector('input[name="domainType"]:checked').value;
                const helpText = this.dialog.querySelector('#domain-help');
                const examples = this.dialog.querySelector('#regex-examples');
                const testArea = this.dialog.querySelector('#regex-test-area');

                if (domainType === 'regex') {
                    helpText.textContent = '提示:使用正则表达式匹配域名,支持复杂的匹配规则';
                    examples.style.display = 'block';
                    testArea.style.display = 'block';
                } else {
                    helpText.textContent = '提示:每行输入一个选择器,支持XPath和CSS两种格式';
                    examples.style.display = 'none';
                    testArea.style.display = 'none';
                }
            };

            this.dialog.querySelectorAll('input[name="domainType"]').forEach(radio => {
                radio.addEventListener('change', updateDomainHelp);
            });

            // 初始化帮助文本
            updateDomainHelp();

            // 正则表达式测试功能
            this.dialog.querySelector('#test-regex-btn').addEventListener('click', () => {
                this.testRegexPattern();
            });

            // 标签页切换
            this.dialog.querySelectorAll('.blocker-tab').forEach(tab => {
                tab.addEventListener('click', (e) => {
                    const tabName = e.target.getAttribute('data-tab');
                    this.dialog.querySelectorAll('.blocker-tab').forEach(t => t.classList.remove('active'));
                    this.dialog.querySelectorAll('.blocker-tab-content').forEach(c => c.classList.remove('active'));
                    e.target.classList.add('active');
                    this.dialog.querySelector(`#${tabName}`).classList.add('active');
                });
            });

            // 事件绑定
            this.dialog.querySelector('.blocker-cancel').addEventListener('click', () => this.closeDialog());
            this.dialog.querySelector('.blocker-confirm').addEventListener('click', () => {
                const textarea = this.dialog.querySelector('textarea');
                const selectorType = this.dialog.querySelector('input[name="selectorType"]:checked').value;
                const domainType = this.dialog.querySelector('input[name="domainType"]:checked').value;
                const domains = this.dialog.querySelector('#domains-input').value;
                this.processInput(textarea.value, selectorType, domains, domainType);
                this.closeDialog();
            });

            this.dialog.querySelector('#quick-confirm').addEventListener('click', () => {
                const ruleInput = this.dialog.querySelector('#quick-rule-input');
                const selectorType = 'css'; // 快速添加只支持CSS
                const domainType = this.dialog.querySelector('input[name="quickDomainType"]:checked').value;
                const domains = this.dialog.querySelector('#quick-domains-input').value;
                this.processInput(ruleInput.value, selectorType, domains, domainType);
                this.closeDialog();
            });

            document.body.appendChild(this.dialog);
        }

        testRegexPattern() {
            const regexInput = this.dialog.querySelector('#domains-input');
            const testDomainInput = this.dialog.querySelector('#test-domain-input');
            const resultDiv = this.dialog.querySelector('#regex-test-result');

            const regexPattern = regexInput.value.trim();
            const testDomain = testDomainInput.value.trim();

            if (!regexPattern) {
                resultDiv.innerHTML = '<div class="blocker-test-result blocker-test-invalid">请输入正则表达式</div>';
                return;
            }

            if (!testDomain) {
                resultDiv.innerHTML = '<div class="blocker-test-result blocker-test-invalid">请输入测试域名</div>';
                return;
            }

            try {
                const regex = new RegExp(regexPattern);
                const matches = regex.test(testDomain);

                if (matches) {
                    resultDiv.innerHTML = `<div class="blocker-test-result blocker-test-valid">✓ 正则表达式有效,匹配测试域名 "${testDomain}"</div>`;
                } else {
                    resultDiv.innerHTML = `<div class="blocker-test-result blocker-test-invalid">✗ 正则表达式有效,但不匹配测试域名 "${testDomain}"</div>`;
                }
            } catch (e) {
                resultDiv.innerHTML = `<div class="blocker-test-result blocker-test-invalid">✗ 无效的正则表达式: ${e.message}</div>`;
            }
        }

        closeDialog() {
            this.dialog.remove();
            this.dialog = null;
        }

        processInput(input, selectorType, domainsInput, domainType) {
            const domains = this.parseDomains(domainsInput, domainType);

            const newRules = input.split('\n')
                .map(rule => rule.trim())
                .filter(rule => rule.length > 0)
                .map(rule => ({
                    type: selectorType,
                    rule: rule,
                    domains: domains,
                    domainType: domainType,
                    id: Date.now() + Math.random().toString(36).substr(2, 9)
                }));

            if (newRules.length > 0) {
                this.rules = [...this.rules, ...newRules];
                this.saveRules();
                this.applyRules();
            }
        }

        parseDomains(domainsInput, domainType) {
            if (!domainsInput || domainsInput.trim() === '') {
                return [{
                    value: this.currentHostname,
                    type: 'normal'
                }];
            }

            return domainsInput.split(',')
                .map(domain => domain.trim())
                .filter(domain => domain.length > 0)
                .map(domain => ({
                    value: domain,
                    type: domainType
                }));
        }

        isRuleApplicable(rule) {
            if (!rule.domains || rule.domains.length === 0) {
                return true;
            }

            return rule.domains.some(domainObj => {
                if (domainObj.type === 'normal') {
                    // 普通域名匹配
                    const domain = domainObj.value;

                    // 处理通配符域名
                    if (domain.startsWith('*.')) {
                        const baseDomain = domain.substring(2);
                        return this.currentHostname === baseDomain || this.currentHostname.endsWith('.' + baseDomain);
                    }

                    // 精确匹配
                    if (this.currentHostname === domain) {
                        return true;
                    }

                    // 子域名匹配(如规则是 example.com,当前是 sub.example.com)
                    if (this.currentHostname.endsWith('.' + domain)) {
                        return true;
                    }

                    return false;
                } else if (domainObj.type === 'regex') {
                    // 正则表达式匹配
                    try {
                        const regex = new RegExp(domainObj.value);
                        return regex.test(this.currentHostname);
                    } catch (e) {
                        console.warn(`无效的正则表达式: ${domainObj.value}`, e);
                        return false;
                    }
                }

                return false;
            });
        }

        applyRules() {
            const applicableRules = this.rules.filter(rule => this.isRuleApplicable(rule));

            applicableRules.forEach(ruleObj => {
                try {
                    if (ruleObj.type === 'xpath') {
                        // XPath选择器
                        const nodes = document.evaluate(
                            ruleObj.rule,
                            document,
                            null,
                            XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
                            null
                        );

                        for (let i = 0; i < nodes.snapshotLength; i++) {
                            const node = nodes.snapshotItem(i);
                            if (node && node.style) {
                                node.style.display = 'none';
                            }
                        }
                    } else if (ruleObj.type === 'css') {
                        // CSS选择器
                        const elements = document.querySelectorAll(ruleObj.rule);
                        elements.forEach(element => {
                            if (element.style) {
                                element.style.display = 'none';
                            }
                        });
                    }
                } catch (e) {
                    console.warn(`无效的${ruleObj.type.toUpperCase()}规则: ${ruleObj.rule}`, e);
                }
            });
        }

        saveRules() {
            GM_setValue(RULES_KEY, this.rules);
        }

        saveSettings() {
            GM_setValue(SETTINGS_KEY, this.settings);
        }

        setupMutationObserver() {
            this.observer = new MutationObserver(mutations => {
                let shouldUpdate = false;
                mutations.forEach(mutation => {
                    if (mutation.addedNodes.length > 0) shouldUpdate = true;
                });
                if (shouldUpdate) this.applyRules();
            });

            this.observer.observe(document, {
                childList: true,
                subtree: true
            });
        }

        showRuleManager() {
            const applicableRules = this.rules.filter(rule => this.isRuleApplicable(rule));
            const allRules = this.rules;

            const manager = document.createElement('div');
            manager.className = 'blocker-dialog';
            manager.innerHTML = `
                <h3 style="margin:0 0 15px">屏蔽规则管理</h3>
                <div class="blocker-tabs">
                    <button class="blocker-tab active" data-tab="current-rules">当前网站规则</button>
                    <button class="blocker-tab" data-tab="all-rules">所有规则</button>
                </div>

                <div class="blocker-tab-content active" id="current-rules">
                    <h4>对 ${this.currentHostname} 有效的规则 (${applicableRules.length}条)</h4>
                    <div style="max-height: 300px; overflow-y: auto; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px;">
                        ${applicableRules.length > 0 ?
                            applicableRules.map((rule, index) => `
                                <div class="blocker-rule-item">
                                    <div class="blocker-rule-content">
                                        <span class="blocker-rule-type">${rule.type.toUpperCase()}</span>
                                        ${rule.domains.map(domain => `
                                            <span class="${domain.type === 'regex' ? 'blocker-rule-regex' : 'blocker-rule-domain'}">
                                                ${domain.value} ${domain.type === 'regex' ? '(正则)' : ''}
                                            </span>
                                        `).join('')}
                                        <code>${rule.rule}</code>
                                    </div>
                                    <div class="blocker-rule-actions">
                                        <button class="blocker-delete-btn" data-id="${rule.id}">删除</button>
                                    </div>
                                </div>
                            `).join('') :
                            '<div style="padding: 20px; text-align: center; color: #666;">暂无规则</div>'
                        }
                    </div>
                </div>

                <div class="blocker-tab-content" id="all-rules">
                    <h4>所有规则 (${allRules.length}条)</h4>
                    <div style="max-height: 300px; overflow-y: auto; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px;">
                        ${allRules.length > 0 ?
                            allRules.map((rule, index) => `
                                <div class="blocker-rule-item">
                                    <div class="blocker-rule-content">
                                        <span class="blocker-rule-type">${rule.type.toUpperCase()}</span>
                                        ${rule.domains.map(domain => `
                                            <span class="${domain.type === 'regex' ? 'blocker-rule-regex' : 'blocker-rule-domain'}">
                                                ${domain.value} ${domain.type === 'regex' ? '(正则)' : ''}
                                            </span>
                                        `).join('')}
                                        <code>${rule.rule}</code>
                                    </div>
                                    <div class="blocker-rule-actions">
                                        <button class="blocker-delete-btn" data-id="${rule.id}">删除</button>
                                    </div>
                                </div>
                            `).join('') :
                            '<div style="padding: 20px; text-align: center; color: #666;">暂无规则</div>'
                        }
                    </div>
                </div>

                <div class="blocker-btns">
                    <button class="blocker-btn blocker-cancel">关闭</button>
                    <button class="blocker-btn" style="background:#ff9800" id="clear-all">清空所有规则</button>
                </div>
            `;

            // 标签页切换
            manager.querySelectorAll('.blocker-tab').forEach(tab => {
                tab.addEventListener('click', (e) => {
                    const tabName = e.target.getAttribute('data-tab');
                    manager.querySelectorAll('.blocker-tab').forEach(t => t.classList.remove('active'));
                    manager.querySelectorAll('.blocker-tab-content').forEach(c => c.classList.remove('active'));
                    e.target.classList.add('active');
                    manager.querySelector(`#${tabName}`).classList.add('active');
                });
            });

            // 事件绑定
            manager.querySelector('.blocker-cancel').addEventListener('click', () => manager.remove());

            // 删除单个规则
            manager.querySelectorAll('.blocker-delete-btn').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    const ruleId = e.target.getAttribute('data-id');
                    this.rules = this.rules.filter(rule => rule.id !== ruleId);
                    this.saveRules();
                    this.applyRules();
                    manager.remove();
                    this.showRuleManager(); // 刷新管理界面
                });
            });

            // 清空所有规则
            manager.querySelector('#clear-all').addEventListener('click', () => {
                if (confirm('确定要清空所有屏蔽规则吗?')) {
                    this.rules = [];
                    this.saveRules();
                    this.applyRules();
                    manager.remove();
                }
            });

            document.body.appendChild(manager);
        }

        showSettings() {
            const settingsDialog = document.createElement('div');
            settingsDialog.className = 'blocker-dialog';
            settingsDialog.innerHTML = `
                <h3 style="margin:0 0 15px">设置</h3>
                <div>
                    <label>
                        <input type="checkbox" id="enable-global-rules" ${this.settings.enableGlobalRules ? 'checked' : ''}>
                        启用全局规则(无域名限制的规则)
                    </label>
                </div>
                <div class="blocker-help" style="margin-top: 10px;">
                    全局规则会对所有网站生效,请谨慎使用
                </div>
                <div class="blocker-btns">
                    <button class="blocker-btn blocker-cancel">取消</button>
                    <button class="blocker-btn blocker-confirm">保存</button>
                </div>
            `;

            // 事件绑定
            settingsDialog.querySelector('.blocker-cancel').addEventListener('click', () => settingsDialog.remove());
            settingsDialog.querySelector('.blocker-confirm').addEventListener('click', () => {
                this.settings.enableGlobalRules = settingsDialog.querySelector('#enable-global-rules').checked;
                this.saveSettings();
                this.applyRules();
                settingsDialog.remove();
            });

            document.body.appendChild(settingsDialog);
        }
    }

    // 启动脚本
    new ElementBlocker();
})();

 

YXN-js

2025-02-26