// ==UserScript== // @name Via风格自定义规则拦截器 // @namespace http://tampermonkey.net/ // @version 5.2.0 // @description 完整恢复原始导入导出界面,支持域名级和规则级单独禁用,高性能局部更新 // @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,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMzIiIGN5PSIzMiIgcj0iMzIiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl8xXzY1XzApIi8+CjxwYXRoIGQ9Ik0xNyAyNEMxNyAyMC4xMzQgMjAuMTM0IDE3IDI0IDE3SDQwQzQzLjg2NiAxNyA0NyAyMC4xMzQgNDcgMjRWNDBDNDcgNDMuODY2IDQzLjg2NiA0NyA0MCA0N0gyNEMyMC4xMzQgNDcgMTcgNDMuODY2IDE3IDQwVjI0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTIzIDI3QzIzIDI0Ljc5MDkgMjQuNzkwOSAyMyAyNyAyM0gzN0MzOS4yMDkxIDIzIDQxIDI0Ljc5MDkgNDEgMjdWMzdDNDEgMzkuMjA5MSAzOS4yMDkxIDQxIDM3IDQxSDI3QzI0Ljc5MDkgNDEgMjMgMzkuMjA5MSAyMyAzN1YyN1oiIGZpbGw9InVybCgjcGFpbnQxX2xpbmVhcl8xXzY1XzApIi8+CjxwYXRoIGQ9Ik0yNSAyOUMyNSAyNy44OTU0IDI1Ljg5NTQgMjcgMjcgMjdIMzdDMzguMTA0NiAyNyAzOSAyNy44OTU0IDM5IDI5VjM1QzM5IDM2LjEwNDYgMzguMTA0NiAzNyAzNyAzN0gyN0MyNS44OTU0IDM3IDI1IDM2LjEwNDYgMjUgMzVWMjlaIiBmaWxsPSJ1cmwoI3BhaW50Ml9saW5lYXJfMV82NV8wKSIvPgo8cGF0aCBkPSJNMjcgMzJMMzIgMjdMMzcgMzJIMzJWMzdIMjdWMzJaIiBmaWxsPSJ3aGl0ZSIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzFfNjVfMCIgeDE9IjAiIHkxPSIwIiB4Mj0iNjQiIHkyPSI2NCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjNjY3ZWVhIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzc2NGJhMiIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MV9saW5lYXJfMV82NV8wIiB4MT0iMjMiIHkxPSIyMyIgeDI9IjQxIiB5Mj0iNDEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzY2N2VlYSIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM3NjRiYTIiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDJfbGluZWFyXzFfNjVfMCIgeDE9IjI1IiB5MT0iMjciIHgyPSIzOSIgeTI9IjM3IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiM2NjdlZGEiLz4KPHN0b9Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNzY0YmEyIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg== // ==/UserScript== (function() { 'use strict'; // ========== 存储与数据结构 ========== const STORAGE_KEY = 'custom_rules_v8'; const STORAGE_VERSION = 5; let dataCache = null; let currentDomain = window.location.hostname; let uiVisible = false; let shadowRoot = null; let styleElement = null; function getDefaultData() { return { version: STORAGE_VERSION, rules: [], domainStatus: {}, ruleStatus: {} }; } 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 || {}; 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]; }); dataCache = data; return data; } } catch(e) {} } // 迁移旧版 const oldKey = 'custom_rules_v7'; 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 || {} }; 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; }); 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; }); saveData(data); applyRules(); if (shadowRoot) refreshManagePanel(); } 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; } } // ========== UI 辅助 ========== 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 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()); if (sortByCurrent) { domains.sort((a, b) => { const aCur = domainMatches(a, currentDomain); const bCur = domainMatches(b, currentDomain); if (aCur && !bCur) return -1; if (!aCur && bCur) return 1; return a.localeCompare(b); }); } else { domains.sort(); } 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' : ''}`; 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} 条规则
${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 groups = shadowRoot.querySelectorAll('.domain-group'); groups.forEach(g => { const rulesContainer = g.querySelector('.rules-container'); const domain = g.getAttribute('data-domain'); if (rulesContainer && !rulesContainer.classList.contains('expanded') && domain) { collapsedDomains.add(domain); } }); container.innerHTML = renderManagePanel(); setTimeout(() => { shadowRoot.querySelectorAll('.domain-group').forEach(g => { const domain = g.getAttribute('data-domain'); if (domain && collapsedDomains.has(domain)) { const rulesContainer = g.querySelector('.rules-container'); const chevron = g.querySelector('.chevron'); if (rulesContainer) { rulesContainer.style.maxHeight = '0px'; rulesContainer.classList.remove('expanded'); if (chevron) chevron.classList.remove('down'); } } else { const isCurrent = domainMatches(domain, currentDomain); if (isCurrent) { const rulesContainer = g.querySelector('.rules-container'); const chevron = g.querySelector('.chevron'); if (rulesContainer && !rulesContainer.classList.contains('expanded')) { rulesContainer.style.maxHeight = rulesContainer.scrollHeight + 'px'; rulesContainer.classList.add('expanded'); if (chevron) chevron.classList.add('down'); const onEnd = () => { if (rulesContainer.classList.contains('expanded')) rulesContainer.style.maxHeight = 'none'; rulesContainer.removeEventListener('transitionend', onEnd); }; rulesContainer.addEventListener('transitionend', onEnd); } } } }); }, 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('domain-toggle-btn')) { e.stopPropagation(); const domain = target.getAttribute('data-domain-toggle'); if (domain) setDomainEnabled(domain, !isDomainEnabled(domain)); return; } if (target.classList.contains('delete-domain')) { e.stopPropagation(); const domain = target.getAttribute('data-domain'); if (domain && confirm(`确定要删除域名“${domain}”下的所有规则吗?\n此操作不可撤销。`)) { const newRules = getAllRules().filter(r => { const p = parseRule(r); return !p || p.domain !== domain; }); saveRules(newRules); showMessage(`已删除域名 ${domain} 的所有规则`); } return; } if (target.classList.contains('rule-toggle-btn')) { e.stopPropagation(); const rule = decodeURIComponent(target.getAttribute('data-rule-toggle')); if (rule) toggleRuleEnabled(rule); return; } if (target.classList.contains('copy-rule')) { 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.stopPropagation(); const rule = decodeURIComponent(target.getAttribute('data-rule')); if (rule && confirm(`确定要删除规则吗?\n\n${rule}`)) { const newRules = getAllRules().filter(r => r !== rule); saveRules(newRules); showMessage('规则已删除'); } return; } const header = target.closest('.domain-header'); if (header && !target.closest('.domain-actions')) { 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')) { rulesContainer.style.maxHeight = '0px'; rulesContainer.classList.remove('expanded'); if (chevron) chevron.classList.remove('down'); } else { rulesContainer.style.maxHeight = rulesContainer.scrollHeight + 'px'; rulesContainer.classList.add('expanded'); if (chevron) chevron.classList.add('down'); const onEnd = () => { if (rulesContainer.classList.contains('expanded')) rulesContainer.style.maxHeight = 'none'; rulesContainer.removeEventListener('transitionend', onEnd); }; rulesContainer.addEventListener('transitionend', onEnd); } } } return; } if (target.classList.contains('domain-name')) { 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构建(完整原始样式,包含所有对话框CSS) ========== 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; // 完整样式(包含原始4.9.1所有样式 + 对话框样式) 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: center; cursor: pointer; user-select: none; position: relative; z-index: 1; } .domain-info { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; } .status-indicator { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; } .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: 150px; 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; } .domain-actions { display: flex; gap: 8px; align-items: center; flex-shrink: 0; } .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; min-width: 48px; } .domain-toggle-btn.disabled { background: #9e9e9e; } .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 { 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; } .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: 12px; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #f0f0f0; } .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. 导入导出(在"工具"标签页): - 导出规则:复制到剪贴板或下载为文件 - 导入规则:从文件或文本粘贴,支持合并/替换模式 规则立即生效,无需刷新页面。`); }; } 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(); let lastUrl = location.href; const urlObserver = new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; currentDomain = location.hostname; applyRules(); 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 }; console.log('自定义规则管理器 v5.2 已启动(完整恢复导入导出界面,支持规则级禁用)'); })();