// ==UserScript== // @name Via风格自定义规则拦截器 // @namespace http://tampermonkey.net/ // @version 5.5.1 // @description 完全手动控制折叠/展开,支持域名级和规则级单独禁用,删除规则/域名时原地生效无闪烁,默认按拦截时间排序(当前域名优先),累计拦截次数统计(显示“拦截 X 次”) // @author Custom Rules Manager // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @grant unsafeWindow // @run-at document-start // @license MIT // @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMzIiIGN5PSIzMiIgcj0iMzAiIGZpbGw9InVybCgjZ3JhZCkiLz4KPHBvbHlsaW5lIHBvaW50cz0iMjQsMjQgMzIsNDAgNDAsMjQiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIi8+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQiIHgxPSIwIiB5MT0iMCIgeDI9IjY0IiB5Mj0iNjQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzY2N0VFQSIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM3NjRCQTIiLz4KPC9saW5lYXJHcmFkaWVudD4KPC9kZWZzPgo8L3N2Zz4= // ==/UserScript== (function() { 'use strict'; // ========== 存储与数据结构 ========== const STORAGE_KEY = 'custom_rules_v9'; const STORAGE_VERSION = 6; // 升级版本,支持累计拦截次数 let dataCache = null; let currentDomain = window.location.hostname; let uiVisible = false; let shadowRoot = null; let styleElement = null; let lastRecordedHref = ''; function getDefaultData() { return { version: STORAGE_VERSION, rules: [], domainStatus: {}, ruleStatus: {}, domainStats: {} // 结构: { lastIntercept: timestamp, totalCount: number } }; } // 迁移旧版本统计到新格式 function migrateStats(stats) { if (!stats) return { lastIntercept: null, totalCount: 0 }; let total = 0; if (typeof stats.totalCount === 'number') { total = stats.totalCount; } else { // 兼容旧格式:todayCount 和 lastDate 可能曾经存在,将它们累加到总次数(或者保留原有今天次数作为总次数) if (typeof stats.todayCount === 'number') total += stats.todayCount; if (typeof stats.lastDate === 'string' && stats.lastDate !== null) { // 旧格式中可能有历史数据,但未保存总次数,只保留今天次数作为保守估计 } } return { lastIntercept: stats.lastIntercept || null, totalCount: total }; } function loadData() { if (dataCache) return dataCache; const saved = GM_getValue(STORAGE_KEY, null); if (saved) { try { const data = JSON.parse(saved); if (data.version === STORAGE_VERSION) { data.ruleStatus = data.ruleStatus || {}; data.domainStatus = data.domainStatus || {}; data.domainStats = data.domainStats || {}; // 清理无效规则 const validRules = new Set(data.rules); Object.keys(data.ruleStatus).forEach(r => { if (!validRules.has(r)) delete data.ruleStatus[r]; }); const validDomains = new Set(); data.rules.forEach(r => { const p = parseRule(r); if(p) validDomains.add(p.domain); }); Object.keys(data.domainStatus).forEach(d => { if (!validDomains.has(d)) delete data.domainStatus[d]; }); // 迁移每个域名的统计 Object.keys(data.domainStats).forEach(d => { if (!validDomains.has(d)) delete data.domainStats[d]; else data.domainStats[d] = migrateStats(data.domainStats[d]); }); // 为新域名初始化统计 validDomains.forEach(d => { if (!data.domainStats[d]) data.domainStats[d] = { lastIntercept: null, totalCount: 0 }; }); dataCache = data; return data; } } catch(e) {} } // 尝试加载旧版本 v8 数据(包含 todayCount) const oldKey = 'custom_rules_v8'; const old = GM_getValue(oldKey, null); if (old) { try { const oldData = JSON.parse(old); if (oldData.rules) { const newData = { version: STORAGE_VERSION, rules: oldData.rules, domainStatus: oldData.domainStatus || {}, ruleStatus: oldData.ruleStatus || {}, domainStats: {} }; // 迁移统计 if (oldData.domainStats) { for (const [domain, stats] of Object.entries(oldData.domainStats)) { newData.domainStats[domain] = migrateStats(stats); } } newData.rules.forEach(r => { if (newData.ruleStatus[r] === undefined) newData.ruleStatus[r] = true; }); dataCache = newData; saveData(dataCache); return dataCache; } } catch(e) {} } dataCache = getDefaultData(); return dataCache; } function saveData(data) { dataCache = data; GM_setValue(STORAGE_KEY, JSON.stringify(data)); } function getAllRules() { return loadData().rules.slice(); } function saveRules(rulesArray) { const data = loadData(); const oldRuleStatus = data.ruleStatus; const newRuleStatus = {}; rulesArray.forEach(r => { newRuleStatus[r] = oldRuleStatus[r] !== undefined ? oldRuleStatus[r] : true; }); data.rules = rulesArray; data.ruleStatus = newRuleStatus; const validDomains = new Set(); rulesArray.forEach(r => { const p = parseRule(r); if(p) validDomains.add(p.domain); }); Object.keys(data.domainStatus).forEach(d => { if (!validDomains.has(d)) delete data.domainStatus[d]; }); validDomains.forEach(d => { if (data.domainStatus[d] === undefined) data.domainStatus[d] = true; }); validDomains.forEach(d => { if (!data.domainStats[d]) data.domainStats[d] = { lastIntercept: null, totalCount: 0 }; }); saveData(data); applyRules(); if (shadowRoot) refreshManagePanel(); } function replaceAllRules(rulesArray) { const data = getDefaultData(); data.rules = rulesArray; data.ruleStatus = {}; rulesArray.forEach(r => { data.ruleStatus[r] = true; }); const validDomains = new Set(); rulesArray.forEach(r => { const p = parseRule(r); if(p) validDomains.add(p.domain); }); validDomains.forEach(d => { data.domainStatus[d] = true; }); data.domainStats = {}; validDomains.forEach(d => { data.domainStats[d] = { lastIntercept: null, totalCount: 0 }; }); saveData(data); applyRules(); if (shadowRoot) refreshManagePanel(); } function updateInterceptStats(domain) { if (!domain) return; const data = loadData(); if (!data.domainStats[domain]) { data.domainStats[domain] = { lastIntercept: null, totalCount: 0 }; } const stats = data.domainStats[domain]; stats.totalCount = (stats.totalCount || 0) + 1; stats.lastIntercept = Date.now(); saveData(data); } function getDomainStats(domain) { const data = loadData(); const stats = data.domainStats[domain]; return stats ? { lastIntercept: stats.lastIntercept, totalCount: stats.totalCount || 0 } : { lastIntercept: null, totalCount: 0 }; } function formatLastIntercept(timestamp) { if (!timestamp) return '无拦截记录'; const now = Date.now(); const diff = now - timestamp; if (diff < 60000) return '刚刚'; if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`; const date = new Date(timestamp); return `${date.getMonth()+1}/${date.getDate()} ${date.getHours().toString().padStart(2,'0')}:${date.getMinutes().toString().padStart(2,'0')}`; } function hasActiveRulesForDomain(domain) { const data = loadData(); if (data.domainStatus[domain] === false) return false; for (const ruleStr of data.rules) { const rule = parseRule(ruleStr); if (rule && rule.domain === domain && data.ruleStatus[ruleStr] !== false) { return true; } } return false; } function recordInterceptionIfNeeded() { const domain = currentDomain; const href = location.href; if (lastRecordedHref === href) return; if (hasActiveRulesForDomain(domain)) { updateInterceptStats(domain); } lastRecordedHref = href; } function isDomainEnabled(domain) { const data = loadData(); return data.domainStatus[domain] !== false; } function setDomainEnabled(domain, enabled) { const data = loadData(); if (data.domainStatus[domain] === enabled) return; data.domainStatus[domain] = enabled; saveData(data); applyRules(); if (shadowRoot) { const group = shadowRoot.querySelector(`.domain-group[data-domain="${CSS.escape(domain)}"]`); if (group) { const isCurrent = domainMatches(domain, currentDomain); const statusClass = (isCurrent && enabled) ? 'status-active' : 'status-inactive'; const indicator = group.querySelector('.status-indicator'); if (indicator) indicator.className = `status-indicator ${statusClass}`; if (enabled) group.classList.remove('disabled'); else group.classList.add('disabled'); const toggleBtn = group.querySelector('.domain-toggle-btn'); if (toggleBtn) { toggleBtn.textContent = enabled ? '✓ 启用' : '✗ 禁用'; if (enabled) toggleBtn.classList.remove('disabled'); else toggleBtn.classList.add('disabled'); } updateStatsBar(); } else { refreshManagePanel(); } } } function isRuleEnabled(ruleStr) { const data = loadData(); return data.ruleStatus[ruleStr] !== false; } function setRuleEnabled(ruleStr, enabled) { const data = loadData(); if (data.ruleStatus[ruleStr] === enabled) return; data.ruleStatus[ruleStr] = enabled; saveData(data); applyRules(); if (shadowRoot) { const ruleItem = shadowRoot.querySelector(`.rule-item[data-rule-key="${CSS.escape(ruleStr)}"]`); if (ruleItem) { const toggleBtn = ruleItem.querySelector('.rule-toggle-btn'); if (toggleBtn) { toggleBtn.textContent = enabled ? '✓ 启用' : '✗ 禁用'; if (enabled) { toggleBtn.classList.remove('disabled'); ruleItem.classList.remove('rule-disabled'); } else { toggleBtn.classList.add('disabled'); ruleItem.classList.add('rule-disabled'); } } } updateStatsBar(); } } function toggleRuleEnabled(ruleStr) { setRuleEnabled(ruleStr, !isRuleEnabled(ruleStr)); } function parseRule(ruleString) { const rule = ruleString.trim(); if (!rule || rule.startsWith('!') || rule.startsWith('#')) return null; const match = rule.match(/^([^#]+)##(.+)$/); if (match) { return { domain: match[1].trim(), selector: match[2].trim(), raw: rule }; } return null; } function domainMatches(ruleDomain, currentDomain) { if (ruleDomain === '*') return true; if (ruleDomain.startsWith('*.')) { const base = ruleDomain.substring(2); return currentDomain === base || currentDomain.endsWith('.' + base); } return currentDomain === ruleDomain; } function applyRules() { const hostname = window.location.hostname; const data = loadData(); const cssParts = []; for (const ruleStr of data.rules) { const rule = parseRule(ruleStr); if (!rule) continue; if (data.domainStatus[rule.domain] !== false && data.ruleStatus[ruleStr] !== false && domainMatches(rule.domain, hostname)) { cssParts.push(`${rule.selector} { display: none !important; }`); } } const css = cssParts.join('\n'); if (!styleElement) { styleElement = document.getElementById('custom-rules-style'); if (!styleElement) { styleElement = document.createElement('style'); styleElement.id = 'custom-rules-style'; document.head.appendChild(styleElement); } } if (styleElement.textContent !== css) { styleElement.textContent = css; } } function escapeHtml(str) { return str.replace(/[&<>]/g, m => m === '&' ? '&' : m === '<' ? '<' : '>'); } function showMessage(msg, type = 'success') { if (shadowRoot) { const statusDiv = shadowRoot.querySelector('#status-message'); if (statusDiv) { statusDiv.textContent = msg; statusDiv.style.color = type === 'error' ? '#f44336' : '#4CAF50'; setTimeout(() => { if (statusDiv.textContent === msg) statusDiv.textContent = ''; }, 2500); } } } function copyToClipboard(text) { return new Promise(resolve => { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.select(); let success = false; try { success = document.execCommand('copy'); } catch(e) {} if (!success && navigator.clipboard) { navigator.clipboard.writeText(text).then(() => success = true).catch(() => {}); } document.body.removeChild(ta); resolve(success); }); } function downloadTextFile(text, filename) { const blob = new Blob([text], {type: 'text/plain'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); setTimeout(() => URL.revokeObjectURL(url), 100); } function updateStatsBar() { if (!shadowRoot) return; const data = loadData(); const totalRules = data.rules.length; const domains = new Set(); data.rules.forEach(r => { const p = parseRule(r); if(p) domains.add(p.domain); }); const activeRules = data.rules.filter(r => { const p = parseRule(r); return p && isDomainEnabled(p.domain) && isRuleEnabled(r) && domainMatches(p.domain, currentDomain); }).length; const statsDiv = shadowRoot.querySelector('.stats'); if (statsDiv) { statsDiv.innerHTML = `共 ${totalRules} 条规则,${domains.size} 个域名
当前域名: ${currentDomain} (${activeRules}条生效)`; } } function collapseGroup(group) { const rulesContainer = group.querySelector('.rules-container'); const chevron = group.querySelector('.chevron'); if (rulesContainer) { rulesContainer.style.maxHeight = '0px'; rulesContainer.classList.remove('expanded'); } if (chevron) chevron.classList.remove('down'); } function expandGroup(group) { const rulesContainer = group.querySelector('.rules-container'); const chevron = group.querySelector('.chevron'); if (!rulesContainer) return; const scrollHeight = rulesContainer.scrollHeight; rulesContainer.style.maxHeight = scrollHeight + 'px'; rulesContainer.classList.add('expanded'); if (chevron) chevron.classList.add('down'); const onTransitionEnd = () => { if (rulesContainer.classList.contains('expanded')) { rulesContainer.style.maxHeight = 'none'; } rulesContainer.removeEventListener('transitionend', onTransitionEnd); }; rulesContainer.addEventListener('transitionend', onTransitionEnd, { once: true }); if (scrollHeight === 0) onTransitionEnd(); } function deleteRuleLocally(ruleStr, ruleElement) { const group = ruleElement.closest('.domain-group'); if (!group) return false; const domain = group.getAttribute('data-domain'); let newRules = getAllRules().filter(r => r !== ruleStr); const data = loadData(); data.rules = newRules; delete data.ruleStatus[ruleStr]; const remainingDomains = new Set(); newRules.forEach(r => { const p = parseRule(r); if(p) remainingDomains.add(p.domain); }); if (!remainingDomains.has(domain)) { delete data.domainStatus[domain]; } saveData(data); applyRules(); ruleElement.remove(); const rulesContainer = group.querySelector('.rules-container'); const remainingRuleItems = rulesContainer ? rulesContainer.querySelectorAll('.rule-item') : []; if (remainingRuleItems.length === 0) { group.remove(); } else { const ruleCountSpan = group.querySelector('.rule-count'); if (ruleCountSpan) { ruleCountSpan.textContent = `${remainingRuleItems.length} 条规则`; } } updateStatsBar(); return true; } function deleteDomainLocally(domain, groupElement) { let newRules = getAllRules().filter(r => { const p = parseRule(r); return !p || p.domain !== domain; }); const data = loadData(); data.rules = newRules; Object.keys(data.ruleStatus).forEach(rule => { const p = parseRule(rule); if (p && p.domain === domain) delete data.ruleStatus[rule]; }); delete data.domainStatus[domain]; saveData(data); applyRules(); groupElement.remove(); updateStatsBar(); const contentArea = shadowRoot.querySelector('#tab-content'); if (contentArea && contentArea.querySelectorAll('.domain-group').length === 0) { refreshManagePanel(); } } async function copyDomainRules(domain) { const data = loadData(); const rules = data.rules.filter(ruleStr => { const p = parseRule(ruleStr); return p && p.domain === domain; }); if (rules.length === 0) { showMessage('该域名下没有规则', 'error'); return; } const text = rules.join('\n'); const success = await copyToClipboard(text); showMessage(success ? `已复制 ${rules.length} 条规则` : '复制失败', success ? 'success' : 'error'); } function sortDomainsByCurrentAndTime(domains, currentDomain, sortByCurrentEnabled) { const currentSet = new Set(); const otherSet = new Set(); for (const d of domains) { if (sortByCurrentEnabled && domainMatches(d, currentDomain)) { currentSet.add(d); } else { otherSet.add(d); } } const currentArr = Array.from(currentSet); const otherArr = Array.from(otherSet); otherArr.sort((a, b) => { const timeA = getDomainStats(a).lastIntercept || 0; const timeB = getDomainStats(b).lastIntercept || 0; if (timeA !== timeB) return timeB - timeA; return a.localeCompare(b); }); return currentArr.concat(otherArr); } function renderManagePanel() { const data = loadData(); const groups = new Map(); for (const ruleStr of data.rules) { const rule = parseRule(ruleStr); if (!rule) continue; if (!groups.has(rule.domain)) groups.set(rule.domain, []); groups.get(rule.domain).push({ ruleStr, selector: rule.selector }); } const sortByCurrent = GM_getValue('sort_by_current_domain', true); let domains = Array.from(groups.keys()); domains = sortDomainsByCurrentAndTime(domains, currentDomain, sortByCurrent); if (domains.length === 0) { return `
📝
暂无规则
点击"添加规则"标签创建第一条规则,或导入现有规则
`; } const sortHtml = `
排序选项:
`; let groupsHtml = []; for (const domain of domains) { const rules = groups.get(domain); const isCurrent = domainMatches(domain, currentDomain); const domainEnabled = isDomainEnabled(domain); const statusClass = (isCurrent && domainEnabled) ? 'status-active' : 'status-inactive'; const domainGroupClass = `domain-group ${isCurrent ? 'current-domain' : ''} ${!domainEnabled ? 'disabled' : ''}`; const toggleBtnText = domainEnabled ? '✓ 启用' : '✗ 禁用'; const toggleBtnClass = `domain-toggle-btn ${!domainEnabled ? 'disabled' : ''}`; const stats = getDomainStats(domain); const lastInterceptStr = formatLastIntercept(stats.lastIntercept); const totalCount = stats.totalCount || 0; let rulesHtml = []; for (const rule of rules) { const ruleEnabled = isRuleEnabled(rule.ruleStr); const ruleToggleText = ruleEnabled ? '✓ 启用' : '✗ 禁用'; const ruleToggleClass = `rule-toggle-btn ${!ruleEnabled ? 'disabled' : ''}`; const ruleItemClass = `rule-item ${!ruleEnabled ? 'rule-disabled' : ''}`; rulesHtml.push(`
${escapeHtml(domain)}##
${escapeHtml(rule.selector)}
`); } groupsHtml.push(`
▶
${escapeHtml(domain)} ${isCurrent ? '当前' : ''}
${rules.length} 条规则
⏱️ ${lastInterceptStr}
📊 拦截 ${totalCount} 次
${rulesHtml.join('')}
`); } return sortHtml + groupsHtml.join(''); } function refreshManagePanel() { if (!shadowRoot) return; const container = shadowRoot.querySelector('#tab-content'); if (!container) return; const collapsedDomains = new Set(); const currentGroups = shadowRoot.querySelectorAll('.domain-group'); currentGroups.forEach(group => { const domain = group.getAttribute('data-domain'); const rulesContainer = group.querySelector('.rules-container'); if (domain && rulesContainer && !rulesContainer.classList.contains('expanded')) { collapsedDomains.add(domain); } }); container.innerHTML = renderManagePanel(); setTimeout(() => { const newGroups = shadowRoot.querySelectorAll('.domain-group'); newGroups.forEach(group => { const domain = group.getAttribute('data-domain'); const rulesContainer = group.querySelector('.rules-container'); const chevron = group.querySelector('.chevron'); if (domain && collapsedDomains.has(domain)) { if (rulesContainer) { rulesContainer.style.maxHeight = '0px'; rulesContainer.classList.remove('expanded'); if (chevron) chevron.classList.remove('down'); } } else { if (rulesContainer) { rulesContainer.style.maxHeight = '0px'; rulesContainer.classList.remove('expanded'); if (chevron) chevron.classList.remove('down'); } } }); }, 20); updateStatsBar(); setupEventDelegation(); } function setupEventDelegation() { if (!shadowRoot) return; const content = shadowRoot.querySelector('#tab-content'); if (!content) return; if (content._delegateListener) content.removeEventListener('click', content._delegateListener); const clickHandler = (e) => { const target = e.target; if (target.classList.contains('delete-domain')) { e.preventDefault(); e.stopPropagation(); const domain = target.getAttribute('data-domain'); const group = target.closest('.domain-group'); if (domain && group && confirm(`确定要删除域名“${domain}”下的所有规则吗?\n此操作不可撤销。`)) { deleteDomainLocally(domain, group); showMessage(`已删除域名 ${domain} 的所有规则`); } return; } if (target.classList.contains('domain-toggle-btn')) { e.preventDefault(); e.stopPropagation(); const domain = target.getAttribute('data-domain-toggle'); if (domain) setDomainEnabled(domain, !isDomainEnabled(domain)); return; } if (target.classList.contains('copy-domain-rules')) { e.preventDefault(); e.stopPropagation(); const domain = target.getAttribute('data-domain-copy'); if (domain) copyDomainRules(domain); return; } if (target.classList.contains('rule-toggle-btn')) { e.preventDefault(); e.stopPropagation(); const rule = decodeURIComponent(target.getAttribute('data-rule-toggle')); if (rule) toggleRuleEnabled(rule); return; } if (target.classList.contains('copy-rule')) { e.preventDefault(); e.stopPropagation(); const rule = decodeURIComponent(target.getAttribute('data-rule')); if (rule) { copyToClipboard(rule).then(success => { showMessage(success ? `已复制:${rule.substring(0, 50)}${rule.length>50?'…':''}` : '复制失败', success ? 'success' : 'error'); }); } return; } if (target.classList.contains('delete-rule')) { e.preventDefault(); e.stopPropagation(); const rule = decodeURIComponent(target.getAttribute('data-rule')); const ruleItem = target.closest('.rule-item'); if (rule && ruleItem && confirm(`确定要删除规则吗?\n\n${rule}`)) { deleteRuleLocally(rule, ruleItem); showMessage('规则已删除'); } return; } const header = target.closest('.domain-header'); if (header && !target.closest('.domain-actions')) { e.preventDefault(); e.stopPropagation(); const group = header.closest('.domain-group'); if (group) { const rulesContainer = group.querySelector('.rules-container'); const chevron = header.querySelector('.chevron'); if (rulesContainer) { if (rulesContainer.classList.contains('expanded')) { collapseGroup(group); } else { expandGroup(group); } } } return; } if (target.classList.contains('domain-name')) { e.preventDefault(); e.stopPropagation(); const full = target.getAttribute('data-full-domain') || target.textContent; alert(`完整域名:${full}`); return; } }; content.addEventListener('click', clickHandler); content._delegateListener = clickHandler; const sortSwitch = shadowRoot.querySelector('#sort-switch'); if (sortSwitch && !sortSwitch._changeListener) { sortSwitch.addEventListener('change', function() { GM_setValue('sort_by_current_domain', this.checked); refreshManagePanel(); }); sortSwitch._changeListener = true; } } function renderAddTab() { return `
`; } function bindAddEvents() { if (!shadowRoot) return; const addBtn = shadowRoot.querySelector('#batch-add-btn'); const clearBtn = shadowRoot.querySelector('#batch-clear-btn'); const textarea = shadowRoot.querySelector('#batch-input'); if (addBtn) { addBtn.onclick = () => { const input = textarea ? textarea.value : ''; const newRules = input.split('\n') .map(l => l.trim()) .filter(l => l && !l.startsWith('#') && !l.startsWith('!') && l.includes('##')); if (newRules.length === 0) { showMessage('没有找到有效的规则', 'error'); return; } const existing = getAllRules(); const unique = newRules.filter(r => !existing.includes(r)); if (unique.length === 0) { showMessage('所有规则都已存在', 'error'); return; } saveRules([...existing, ...unique]); if (textarea) textarea.value = ''; showMessage(`成功添加 ${unique.length} 条新规则`); const manageTab = shadowRoot.querySelector('.tab-btn[data-tab="manage"]'); if (manageTab) manageTab.click(); }; } if (clearBtn) { clearBtn.onclick = () => { if (textarea) textarea.value = ''; showMessage('已清空输入框'); }; } } function renderToolsTab() { const rules = getAllRules(); return `

工具

📤
导出规则
将所有规则导出为文本或文件
📥
导入规则
通过文本或文件导入规则

统计信息

总规则数: ${rules.length}
当前域名: ${currentDomain}
生效规则: ${rules.filter(r => { const p = parseRule(r); return p && isDomainEnabled(p.domain) && isRuleEnabled(r) && domainMatches(p.domain, currentDomain); }).length} 条

⚠️ 注意事项

• 导入模式:合并导入(保留现有规则)
• 导出规则可以选择复制或下载
• 建议先导出备份再进行操作
• 域名开关:可临时关闭某个域名的所有规则
• 规则开关:可单独启用/禁用某条规则
• 拦截时间:每次访问该域名且有生效规则时自动记录
• 累计拦截次数:该域名下规则总共生效的次数
• 复制域名规则:复制该域名下所有规则到剪贴板
`; } // 导入导出对话框(保持不变,略) function showExportDialog() { const dialog = document.createElement('div'); dialog.className = 'dialog-overlay'; dialog.innerHTML = `

导出规则

共有 ${getAllRules().length} 条规则
📋
复制到剪贴板
将规则复制到剪贴板,然后可以粘贴到其他地方
💾
下载为文件
将规则下载为文本文件,方便备份和分享
`; shadowRoot.appendChild(dialog); setTimeout(() => dialog.classList.add('active'), 10); const closeDialog = () => { dialog.classList.remove('active'); setTimeout(() => dialog.remove(), 300); }; dialog.querySelector('#export-dialog-close')?.addEventListener('click', closeDialog); dialog.querySelector('#export-cancel-btn')?.addEventListener('click', closeDialog); dialog.querySelector('#export-copy-option')?.addEventListener('click', async () => { const rules = getAllRules(); if (rules.length === 0) { showMessage('没有规则可以导出', 'error'); closeDialog(); return; } const success = await copyToClipboard(rules.join('\n')); showMessage(success ? `成功复制 ${rules.length} 条规则` : '复制失败', success ? 'success' : 'error'); closeDialog(); }); dialog.querySelector('#export-download-option')?.addEventListener('click', () => { const rules = getAllRules(); if (rules.length === 0) { showMessage('没有规则可以导出', 'error'); closeDialog(); return; } const filename = `custom-rules-${new Date().toISOString().slice(0,10)}.txt`; downloadTextFile(rules.join('\n'), filename); showMessage(`已下载 ${rules.length} 条规则`); closeDialog(); }); dialog.addEventListener('click', (e) => { if (e.target === dialog) closeDialog(); }); } function showImportDialog() { const dialog = document.createElement('div'); dialog.className = 'dialog-overlay'; dialog.innerHTML = `

导入规则

选择导入方式:
📁
从文件导入
选择规则文件进行导入
📝
从文本导入
粘贴规则文本进行导入
`; shadowRoot.appendChild(dialog); setTimeout(() => dialog.classList.add('active'), 10); const closeDialog = () => { dialog.classList.remove('active'); setTimeout(() => dialog.remove(), 300); }; dialog.querySelector('#import-dialog-close')?.addEventListener('click', closeDialog); dialog.querySelector('#import-cancel-btn')?.addEventListener('click', closeDialog); dialog.querySelector('#import-file-option')?.addEventListener('click', () => { closeDialog(); const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.txt,.text'; fileInput.style.display = 'none'; fileInput.onchange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => showImportModeDialog(shadowRoot, ev.target.result, 'file'); reader.readAsText(file); document.body.removeChild(fileInput); }; document.body.appendChild(fileInput); fileInput.click(); }); dialog.querySelector('#import-text-option')?.addEventListener('click', () => { closeDialog(); showTextImportDialog(); }); dialog.addEventListener('click', (e) => { if (e.target === dialog) closeDialog(); }); } function showTextImportDialog() { const dialog = document.createElement('div'); dialog.className = 'dialog-overlay'; dialog.innerHTML = `

从文本导入规则

在下方文本框中粘贴规则,每行一条规则
`; shadowRoot.appendChild(dialog); setTimeout(() => dialog.classList.add('active'), 10); const closeDialog = () => { dialog.classList.remove('active'); setTimeout(() => dialog.remove(), 300); }; dialog.querySelector('#text-import-dialog-close')?.addEventListener('click', closeDialog); dialog.querySelector('#text-import-cancel-btn')?.addEventListener('click', closeDialog); dialog.querySelector('#text-import-next-btn')?.addEventListener('click', () => { const content = dialog.querySelector('#text-import-textarea').value; if (!content.trim()) { showMessage('请输入规则文本', 'error'); return; } closeDialog(); showImportModeDialog(shadowRoot, content, 'text'); }); dialog.addEventListener('click', (e) => { if (e.target === dialog) closeDialog(); }); } function showImportModeDialog(shadow, content, sourceType) { const dialog = document.createElement('div'); dialog.className = 'dialog-overlay'; dialog.innerHTML = `

选择导入模式

从${sourceType === 'file' ? '文件' : '文本'}中检测到规则
➕
合并导入
保留现有规则,只添加新规则
🔄
替换导入
清空现有规则,导入新规则
`; shadow.appendChild(dialog); setTimeout(() => dialog.classList.add('active'), 10); let selectedMode = 'merge'; const mergeOpt = dialog.querySelector('#merge-import-option'); const replaceOpt = dialog.querySelector('#replace-import-option'); mergeOpt.classList.add('selected'); mergeOpt.addEventListener('click', () => { mergeOpt.classList.add('selected'); replaceOpt.classList.remove('selected'); selectedMode = 'merge'; }); replaceOpt.addEventListener('click', () => { replaceOpt.classList.add('selected'); mergeOpt.classList.remove('selected'); selectedMode = 'replace'; }); const closeDialog = () => { dialog.classList.remove('active'); setTimeout(() => dialog.remove(), 300); }; dialog.querySelector('#import-mode-dialog-close')?.addEventListener('click', closeDialog); dialog.querySelector('#import-mode-cancel-btn')?.addEventListener('click', closeDialog); dialog.querySelector('#import-mode-confirm-btn')?.addEventListener('click', () => { const newRules = content.split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('#') && !line.startsWith('!') && line.includes('##')); if (newRules.length === 0) { showMessage('没有找到有效的规则', 'error'); closeDialog(); return; } if (selectedMode === 'merge') { const existing = getAllRules(); const unique = newRules.filter(r => !existing.includes(r)); if (unique.length === 0) { showMessage('没有新规则可添加', 'error'); closeDialog(); return; } saveRules([...existing, ...unique]); showMessage(`合并导入 ${unique.length} 条新规则,共 ${existing.length + unique.length} 条`); } else { replaceAllRules(newRules); showMessage(`替换导入 ${newRules.length} 条规则`); } closeDialog(); const manageTab = shadowRoot?.querySelector('.tab-btn[data-tab="manage"]'); if (manageTab) manageTab.click(); }); dialog.addEventListener('click', (e) => { if (e.target === dialog) closeDialog(); }); } function bindToolsEvents() { if (!shadowRoot) return; const exportCard = shadowRoot.querySelector('#export-card'); const importCard = shadowRoot.querySelector('#import-card'); if (exportCard) exportCard.onclick = () => showExportDialog(); if (importCard) importCard.onclick = () => showImportDialog(); } // 主UI构建(样式已包含垂直排列统计) function buildUI() { if (uiVisible) return; if (!document.body) { window.addEventListener('DOMContentLoaded', buildUI, { once: true }); return; } const oldHost = document.getElementById('custom-rules-host'); if (oldHost) oldHost.remove(); const host = document.createElement('div'); host.id = 'custom-rules-host'; const shadow = host.attachShadow({ mode: 'open' }); shadowRoot = shadow; const style = document.createElement('style'); style.textContent = ` :host { all: initial; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } * { box-sizing: border-box; margin: 0; padding: 0; } .overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 2147483647; opacity: 0; transition: opacity 0.3s; } .ui-container { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.9); width: 90%; max-width: 700px; height: 500px; max-height: 85vh; background: white; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); border: none !important; outline: none !important; z-index: 2147483647; opacity: 0; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); overflow: hidden; display: flex; flex-direction: column; } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 16px 20px; position: relative; flex-shrink: 0; } .title { margin: 0; font-size: 16px; font-weight: 600; } .close-btn { position: absolute; top: 12px; right: 12px; background: rgba(255,255,255,0.2); border: none; color: white; width: 28px; height: 28px; border-radius: 50%; cursor: pointer; font-size: 18px; line-height: 1; display: flex; align-items: center; justify-content: center; } .stats { margin-top: 8px; font-size: 12px; opacity: 0.9; line-height: 1.4; } .tabs { display: flex; background: #f8f9fa; border-bottom: 1px solid #e0e0e0; flex-shrink: 0; } .tab-btn { flex: 1; padding: 12px; border: none; background: none; cursor: pointer; font-size: 13px; font-weight: 500; color: #666; transition: all 0.2s; } .tab-btn.active { color: #667eea; border-bottom: 2px solid #667eea; } .main-content { flex: 1; overflow: hidden; display: flex; flex-direction: column; min-height: 0; } .content-area { flex: 1; overflow-y: auto; padding: 16px; position: relative; } .footer { padding: 12px 20px; border-top: 1px solid #f0f0f0; background: #fafafa; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; } .help-btn { background: none; border: none; color: #666; font-size: 12px; cursor: pointer; padding: 6px 12px; border-radius: 4px; transition: background-color 0.2s; } .help-btn:hover { background-color: #f0f0f0; } .status-message { font-size: 12px; color: #666; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .domain-group { margin-bottom: 12px; border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; background: white; transition: transform 0.2s, box-shadow 0.2s; } .domain-group.current-domain { border-color: #667eea; box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2); } .domain-group.current-domain .domain-header { background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); } .domain-group.disabled { opacity: 0.7; background: #f9f9f9; } .domain-group.disabled .rules-container .rule-item { filter: grayscale(0.2); opacity: 0.7; } .domain-header { background: #f5f5f5; padding: 12px 16px; display: flex; justify-content: space-between; align-items: flex-start; cursor: pointer; user-select: none; position: relative; z-index: 1; gap: 8px; } .status-indicator { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; margin-top: 4px; } .status-active { background-color: #4CAF50; box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2); } .status-inactive { background-color: #ccc; } .domain-content { flex: 1; display: flex; flex-direction: column; min-width: 0; } .domain-name-row { display: flex; align-items: center; gap: 8px; margin-bottom: 2px; min-width: 0; } .domain-name { font-weight: 500; color: #333; font-size: 14px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; transition: color 0.2s; } .domain-name:hover { color: #667eea; text-decoration: underline; } .current-domain-tag { background: #667eea; color: white; font-size: 10px; padding: 1px 6px; border-radius: 10px; font-weight: bold; margin-left: 4px; flex-shrink: 0; } .rule-count-row { display: flex; align-items: center; } .rule-count { font-size: 12px; color: #666; font-weight: normal; padding-left: 16px; } .stats-row { display: flex; flex-direction: column; gap: 2px; margin-top: 4px; padding-left: 16px; font-size: 11px; color: #888; } .stats-last, .stats-total { display: inline-flex; align-items: center; gap: 4px; } .domain-actions { display: flex; flex-direction: column; gap: 6px; align-items: stretch; flex-shrink: 0; } .domain-toggle-btn, .copy-domain-rules, .delete-domain { white-space: nowrap; text-align: center; min-width: 80px; } .domain-toggle-btn { background: #667eea; color: white; border: none; padding: 4px 10px; border-radius: 16px; cursor: pointer; font-size: 11px; font-weight: 500; transition: 0.2s; } .domain-toggle-btn.disabled { background: #9e9e9e; } .copy-domain-rules { background: #2196F3; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: 500; } .delete-domain { background: #ff4444; color: white; border: none; padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: 500; } .rules-container { background: white; max-height: 0; overflow: hidden; transition: max-height 0.3s ease; position: relative; z-index: 0; } .rules-container.expanded { max-height: none; } .rule-item { padding: 12px 16px; border-bottom: 1px solid #f5f5f5; display: flex; justify-content: space-between; align-items: center; background: #fafafa; transition: background 0.2s; } .rule-item.rule-disabled { background: #f5f5f5; opacity: 0.7; } .rule-item:nth-child(odd) { background: #fff; } .rule-item:nth-child(odd).rule-disabled { background: #f9f9f9; } .rule-item:hover { background: #e6f0ff !important; } .rule-info { flex: 1; } .rule-domain { font-size: 12px; color: #666; margin-bottom: 4px; } .rule-selector { font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; color: #333; word-break: break-all; background: rgba(0, 0, 0, 0.02); padding: 4px 8px; border-radius: 4px; border-left: 2px solid #667eea; } .rule-actions { display: flex; gap: 6px; flex-shrink: 0; align-items: center; } .rule-toggle-btn { background: #4CAF50; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: 500; min-width: 52px; } .rule-toggle-btn.disabled { background: #9e9e9e; } .copy-rule { background: #2196F3; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: 500; } .delete-rule { background: #ff9800; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: 500; } .copy-rule:hover, .delete-rule:hover, .rule-toggle-btn:hover, .copy-domain-rules:hover { opacity: 0.85; } .action-btn { flex: 1; padding: 12px; color: white; border: none; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; } .batch-textarea { width: 100%; height: 200px; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; box-sizing: border-box; margin-bottom: 10px; resize: vertical; } .empty-state { text-align: center; padding: 30px 16px; } .empty-icon { font-size: 36px; color: #ddd; margin-bottom: 12px; } .empty-text { color: #999; margin-bottom: 6px; font-size: 13px; } .empty-hint { color: #aaa; font-size: 12px; } .chevron { display: inline-block; transition: transform 0.3s; font-size: 10px; color: #666; flex-shrink: 0; margin-right: 8px; margin-top: 4px; } .chevron.down { transform: rotate(90deg); } .content-area::-webkit-scrollbar { width: 8px; } .content-area::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; } .content-area::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; } .content-area::-webkit-scrollbar-thumb:hover { background: #a8a8a8; } .sort-options { display: flex; align-items: center; gap: 16px; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #f0f0f0; flex-wrap: wrap; } .sort-label { font-size: 13px; color: #666; font-weight: 500; } .sort-toggle { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; padding: 4px 8px; border-radius: 4px; transition: background-color 0.2s; } .sort-toggle:hover { background-color: #f0f0f0; } .toggle-switch { position: relative; display: inline-block; width: 44px; height: 24px; } .toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px; } .toggle-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; box-shadow: 0 1px 3px rgba(0,0,0,0.2); } .toggle-switch input:checked + .toggle-slider { background-color: #667eea; } .toggle-switch input:checked + .toggle-slider:before { transform: translateX(20px); } .toggle-switch input { display: none; } .toggle-text { font-size: 13px; color: #333; font-weight: 500; } .tools-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 20px; } .tool-card { background: white; border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; cursor: pointer; transition: all 0.2s; text-align: center; } .tool-card:hover { border-color: #667eea; box-shadow: 0 4px 12px rgba(0,0,0,0.1); transform: translateY(-2px); } .tool-card.export { border-color: #2196F3; } .tool-card.import { border-color: #4CAF50; } .tool-icon { font-size: 32px; margin-bottom: 8px; } .tool-title { font-size: 14px; font-weight: 600; margin-bottom: 4px; color: #333; } .tool-desc { font-size: 12px; color: #666; line-height: 1.4; } .dialog-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 2147483648; display: flex; align-items: center; justify-content: center; padding: 20px; opacity: 0; transition: opacity 0.3s; pointer-events: none; } .dialog-overlay.active { opacity: 1; pointer-events: all; } .dialog-container { background: white; border-radius: 12px; width: 100%; max-width: 500px; max-height: 90vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.3); transform: translateY(20px); transition: transform 0.3s; } .dialog-overlay.active .dialog-container { transform: translateY(0); } .dialog-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 16px 20px; position: relative; border-radius: 12px 12px 0 0; } .dialog-header.export { background: #2196F3; } .dialog-header.import { background: #4CAF50; } .dialog-title { margin: 0; font-size: 16px; font-weight: 600; } .dialog-close { position: absolute; top: 12px; right: 12px; background: rgba(255,255,255,0.2); border: none; color: white; width: 28px; height: 28px; border-radius: 50%; cursor: pointer; font-size: 18px; line-height: 1; display: flex; align-items: center; justify-content: center; } .dialog-body { padding: 20px; } .dialog-info { margin-bottom: 16px; font-size: 14px; color: #333; text-align: center; } .export-options, .import-options, .import-mode-options { display: flex; flex-direction: column; gap: 12px; margin: 20px 0; } .export-option, .import-option, .import-mode-option { display: flex; align-items: center; padding: 12px; background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 8px; cursor: pointer; transition: all 0.2s; } .export-option:hover, .import-option:hover, .import-mode-option:hover { background: #e3f2fd; border-color: #2196F3; } .export-option-icon, .import-option-icon, .import-mode-option-icon { font-size: 24px; margin-right: 12px; width: 40px; text-align: center; } .export-option-content, .import-option-content, .import-mode-option-content { flex: 1; } .export-option-title, .import-option-title, .import-mode-option-title { font-size: 14px; font-weight: 600; color: #333; margin-bottom: 4px; } .export-option-desc, .import-option-desc, .import-mode-option-desc { font-size: 12px; color: #666; } .export-cancel-btn, .import-cancel-btn { width: 100%; padding: 12px; background: #f5f5f5; color: #666; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; margin-top: 10px; } .export-cancel-btn:hover, .import-cancel-btn:hover { background: #e0e0e0; } .text-import-area { margin: 20px 0; } .text-import-textarea { width: 100%; height: 200px; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; line-height: 1.5; resize: vertical; box-sizing: border-box; margin-bottom: 16px; } .text-import-textarea:focus { outline: none; border-color: #4CAF50; box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2); } .dialog-buttons { display: flex; gap: 10px; margin-top: 20px; } .dialog-button { flex: 1; padding: 12px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; } .dialog-button.primary { background: #4CAF50; color: white; } .dialog-button.primary:hover { background: #388E3C; } .dialog-button.secondary { background: #2196F3; color: white; } .dialog-button.secondary:hover { background: #0b7dda; } .dialog-button.cancel { background: #f5f5f5; color: #666; } .dialog-button.cancel:hover { background: #e0e0e0; } .import-mode-option.selected { background: #e3f2fd; border-color: #2196F3; } `; shadow.appendChild(style); const overlay = document.createElement('div'); overlay.className = 'overlay'; const ui = document.createElement('div'); ui.className = 'ui-container'; shadow.appendChild(overlay); shadow.appendChild(ui); document.body.appendChild(host); requestAnimationFrame(() => { overlay.style.opacity = '1'; ui.style.opacity = '1'; ui.style.transform = 'translate(-50%, -50%) scale(1)'; }); overlay.addEventListener('click', () => closeUI()); uiVisible = true; function closeUI() { overlay.style.opacity = '0'; ui.style.opacity = '0'; ui.style.transform = 'translate(-50%, -50%) scale(0.9)'; setTimeout(() => { host.remove(); uiVisible = false; shadowRoot = null; }, 300); } function renderMain() { const data = loadData(); const totalRules = data.rules.length; const domainsSet = new Set(); data.rules.forEach(r => { const p = parseRule(r); if(p) domainsSet.add(p.domain); }); const activeRules = data.rules.filter(r => { const p = parseRule(r); return p && isDomainEnabled(p.domain) && isRuleEnabled(r) && domainMatches(p.domain, currentDomain); }).length; ui.innerHTML = `

自定义规则管理器

共 ${totalRules} 条规则,${domainsSet.size} 个域名
当前域名: ${currentDomain} (${activeRules}条生效)
`; const closeBtn = ui.querySelector('#close-ui'); if (closeBtn) closeBtn.onclick = closeUI; const tabs = ui.querySelectorAll('.tab-btn'); const contentDiv = ui.querySelector('#tab-content'); const switchTab = (tabId) => { if (tabId === 'manage') { contentDiv.innerHTML = renderManagePanel(); setupEventDelegation(); } else if (tabId === 'add') { contentDiv.innerHTML = renderAddTab(); bindAddEvents(); } else if (tabId === 'tools') { contentDiv.innerHTML = renderToolsTab(); bindToolsEvents(); } }; tabs.forEach(btn => { btn.onclick = () => { tabs.forEach(b => b.classList.remove('active')); btn.classList.add('active'); switchTab(btn.getAttribute('data-tab')); }; }); switchTab('manage'); const helpBtn = ui.querySelector('#help-btn'); if (helpBtn) helpBtn.onclick = () => { alert(`自定义规则管理器使用说明: 规则格式: 域名##CSS选择器 示例: 1. 隐藏特定元素: www.123pan.com##div.ant-modal-root:has(.tuia-modal-wrap) 2. 使用通配符: *.baidu.com##.advertisement 匹配所有 baidu.com 的子域名 3. 常用选择器: .class-name # 按类名 #element-id # 按ID div[class*='ad'] # 按属性包含 div:has(.ad) # 按子元素 4. 管理规则: - 按域名分组显示,每条规则显示为两行(域名行 + 选择器行) - 点击域名分组头可展开/折叠规则列表 - 绿色圆点表示规则在当前页面生效(且域名开关和规则开关都开启) - 当前域名分组高亮显示,并带有"当前"标签 - 支持复制单条规则、删除单条规则、删除整个域名组 - 域名开关/复制规则/删除全部按钮垂直排列,互不遮挡 - 拦截统计信息(最后拦截时间、累计拦截次数)垂直排列 - 累计拦截次数:记录该域名下规则总共生效的次数 - 禁用后规则不会生效,规则项会变灰显示 - 域名过长会自动截断显示省略号,点击可弹出完整域名 - 排序选项可让当前域名规则置顶,其余域名默认按拦截时间排序(最近拦截的在前) - 规则项交替背景色,鼠标悬停显示浅蓝色反馈 - 所有域名分组默认折叠,点击头部手动展开 - 删除规则/域名时原地移除,无闪烁,不刷新整个列表 5. 拦截统计: - 每个域名下显示最后拦截时间和累计拦截次数 - 每次访问该域名且有生效规则时自动累加 6. 批量添加: - 每行一条规则,格式:域名##选择器 7. 导入导出: - 导出规则:复制到剪贴板或下载为文件 - 导入规则:从文件或文本粘贴,支持合并/替换模式 规则立即生效,无需刷新页面。`); }; } renderMain(); } // 初始化 if (document.head) { if (!document.getElementById('custom-rules-style')) { const style = document.createElement('style'); style.id = 'custom-rules-style'; document.head.appendChild(style); styleElement = style; } else { styleElement = document.getElementById('custom-rules-style'); } } else { const observer = new MutationObserver(() => { if (document.head) { observer.disconnect(); if (!document.getElementById('custom-rules-style')) { const style = document.createElement('style'); style.id = 'custom-rules-style'; document.head.appendChild(style); styleElement = style; } else { styleElement = document.getElementById('custom-rules-style'); } applyRules(); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); } applyRules(); setTimeout(() => recordInterceptionIfNeeded(), 100); let lastUrl = location.href; const urlObserver = new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; currentDomain = location.hostname; applyRules(); recordInterceptionIfNeeded(); if (uiVisible && shadowRoot) { updateStatsBar(); const activeTab = shadowRoot.querySelector('.tab-btn.active'); if (activeTab && activeTab.getAttribute('data-tab') === 'manage') { refreshManagePanel(); } } } }); urlObserver.observe(document, { subtree: true, childList: true }); GM_registerMenuCommand('📋 自定义规则', () => buildUI()); unsafeWindow.customRulesManager = { getAllRules, saveRules, replaceAllRules, applyRules, showUI: buildUI, isDomainEnabled, setDomainEnabled, isRuleEnabled, setRuleEnabled, toggleRuleEnabled, getDomainStats }; console.log('自定义规则管理器 v5.5.1 已启动(累计拦截次数显示为“拦截 X 次”,无闪烁删除)'); })();