// ==UserScript==
// @name 网页元素屏蔽器
// @namespace http://tampermonkey.net/
// @version 0.2
// @description 屏蔽任意网站上的元素,支持缩略图记录和正则/简单模式屏蔽
// @author JerryChiang
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 添加样式
const style = document.createElement('style');
style.textContent = `
.highlight {
outline: 2px solid red !important;
background-color: rgba(255, 0, 0, 0.1) !important;
}
.blocker-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 1px solid #ccc;
z-index: 9999;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
border-radius: 8px;
font-family: Arial, sans-serif;
width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.blocker-popup p {
margin: 0 0 10px;
font-size: 16px;
color: #333;
}
.blocker-popup button {
margin: 5px;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.blocker-popup button:hover {
opacity: 0.9;
}
#regex-mode, #simple-mode {
background-color: #007bff;
color: white;
}
#preview-rule {
background-color: #28a745;
color: white;
}
#add-rule-row {
background-color: #17a2b8;
color: white;
}
#save-rules {
background-color: #17a2b8;
color: white;
}
#cancel-rule {
background-color: #dc3545;
color: white;
}
.blocker-popup input, .blocker-popup select {
margin: 5px;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.blocker-popup select {
width: 100px;
}
.blocker-popup input[type="text"], .blocker-popup input[type="number"] {
width: 200px;
}
.rule-row {
display: flex;
align-items: center;
margin: 5px 0;
padding: 5px;
border-bottom: 1px solid #eee;
}
.rule-row button {
background-color: #dc3545;
color: white;
}
.blocker-list {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 1px solid #ccc;
z-index: 9999;
max-height: 80vh;
overflow-y: auto;
width: 500px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
}
.blocker-list h3 {
margin: 0 0 15px;
font-size: 18px;
color: #333;
}
.blocker-list ul {
list-style: none;
padding: 0;
}
.blocker-list li {
margin: 10px 0;
display: flex;
align-items: center;
width: 100%;
padding: 5px;
border-bottom: 1px solid #eee;
}
.blocker-list img {
max-width: 400px;
max-height: 100px;
object-fit: contain;
border: 1px solid #ddd;
flex-shrink: 0;
}
.blocker-list button {
margin-left: auto;
flex-shrink: 0;
padding: 5px 10px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.blocker-list button:hover {
background-color: #c82333;
}
`;
document.head.appendChild(style);
// 注册菜单
GM_registerMenuCommand('屏蔽设置', startBlockingMode);
GM_registerMenuCommand('按规则屏蔽', showRegexBlockInput);
GM_registerMenuCommand('查看屏蔽记录', showBlockList);
// 进入元素选择模式
function startBlockingMode() {
// 移除 alert,直接进入高亮选择
document.body.addEventListener('mouseover', highlightElement);
document.body.addEventListener('click', selectElement, true);
}
// 高亮悬停元素
function highlightElement(event) {
if (window.lastHighlighted) {
window.lastHighlighted.classList.remove('highlight');
}
event.target.classList.add('highlight');
window.lastHighlighted = event.target;
}
// 选择元素并弹出确认窗口
function selectElement(event) {
event.preventDefault();
event.stopPropagation();
document.body.removeEventListener('mouseover', highlightElement);
document.body.removeEventListener('click', selectElement, true);
const selectedElement = event.target;
window.lastHighlighted.classList.remove('highlight');
showConfirmation(selectedElement);
}
// 显示确认弹窗
function showConfirmation(element) {
const popup = document.createElement('div');
popup.className = 'blocker-popup';
popup.innerHTML = `
<p>是否屏蔽此元素?</p>
<button id="confirm">确认屏蔽</button>
<button id="preview">预览</button>
<button id="cancel">取消</button>
`;
document.body.appendChild(popup);
let isPreviewHidden = false;
const confirmBtn = document.getElementById('confirm');
confirmBtn.addEventListener('click', async () => {
confirmBtn.disabled = true;
try {
await saveBlockWithThumbnail(element);
element.style.display = 'none';
document.body.removeChild(popup);
} catch (e) {
console.error('屏蔽失败:', e);
confirmBtn.disabled = false;
}
}, { once: true });
document.getElementById('preview').addEventListener('click', () => {
if (!isPreviewHidden) {
element.style.display = 'none';
isPreviewHidden = true;
} else {
element.style.display = '';
isPreviewHidden = false;
}
});
document.getElementById('cancel').addEventListener('click', () => {
document.body.removeChild(popup);
});
}
// 保存屏蔽信息并生成缩略图
async function saveBlockWithThumbnail(element) {
const domain = window.location.hostname;
const selector = getSelector(element);
const canvas = await html2canvas(element, { scale: 1 });
const originalWidth = canvas.width;
const originalHeight = canvas.height;
let scale = Math.min(400 / originalWidth, 100 / originalHeight, 1);
const thumbnailCanvas = document.createElement('canvas');
thumbnailCanvas.width = originalWidth * scale;
thumbnailCanvas.height = originalHeight * scale;
const ctx = thumbnailCanvas.getContext('2d');
ctx.drawImage(canvas, 0, 0, thumbnailCanvas.width, thumbnailCanvas.height);
const thumbnail = thumbnailCanvas.toDataURL('image/png');
let blocks = GM_getValue('blocks', {});
if (!blocks[domain]) {
blocks[domain] = [];
}
if (!blocks[domain].some(item => item.selector === selector)) {
blocks[domain].push({ selector, thumbnail });
GM_setValue('blocks', blocks);
}
}
// 生成简单 CSS 选择器
function getSelector(element) {
if (element.id) return `#${element.id}`;
let path = [];
while (element && element.nodeType === Node.ELEMENT_NODE) {
let selector = element.tagName.toLowerCase();
if (element.className && typeof element.className === 'string') {
selector += '.' + element.className.trim().replace(/\s+/g, '.');
}
path.unshift(selector);
element = element.parentElement;
}
return path.join(' > ');
}
// 应用屏蔽规则
function applyBlocks() {
const domain = window.location.hostname;
const blocks = GM_getValue('blocks', {});
if (blocks[domain]) {
blocks[domain].forEach(item => {
try {
document.querySelectorAll(item.selector).forEach(el => {
el.style.display = 'none';
});
} catch (e) {
console.error(`无法应用选择器: ${item.selector}`, e);
}
});
}
applyRegexBlocks();
}
// 显示屏蔽记录窗口
function showBlockList() {
const domain = window.location.hostname;
const blocks = GM_getValue('blocks', {});
const blockList = blocks[domain] || [];
const listWindow = document.createElement('div');
listWindow.className = 'blocker-list';
listWindow.innerHTML = `
<h3>当前域名屏蔽记录 (${domain})</h3>
<ul id="block-items"></ul>
<button id="close-list">关闭</button>
`;
document.body.appendChild(listWindow);
const ul = document.getElementById('block-items');
if (blockList.length === 0) {
ul.innerHTML = '<li>暂无屏蔽记录</li>';
} else {
blockList.forEach((item, index) => {
const li = document.createElement('li');
const img = document.createElement('img');
img.src = item.thumbnail;
const unblockBtn = document.createElement('button');
unblockBtn.textContent = '取消屏蔽';
unblockBtn.addEventListener('click', () => {
removeBlock(domain, index);
listWindow.remove();
applyBlocks();
showBlockList();
});
li.appendChild(img);
li.appendChild(unblockBtn);
ul.appendChild(li);
});
}
document.getElementById('close-list').addEventListener('click', () => {
document.body.removeChild(listWindow);
});
}
// 删除屏蔽记录
function removeBlock(domain, index) {
let blocks = GM_getValue('blocks', {});
if (blocks[domain] && blocks[domain][index]) {
blocks[domain].splice(index, 1);
if (blocks[domain].length === 0) {
delete blocks[domain];
}
GM_setValue('blocks', blocks);
}
}
// 显示规则屏蔽输入和管理窗口
function showRegexBlockInput() {
const domain = window.location.hostname;
let regexBlocks = GM_getValue('regexBlocks', {});
let rules = regexBlocks[domain] || [];
const popup = document.createElement('div');
popup.className = 'blocker-popup';
popup.innerHTML = `
<p>设置屏蔽规则(层级:0 表示当前元素,1 表示父元素,依此类推):</p>
<button id="regex-mode">正则模式</button>
<button id="simple-mode">简单模式</button>
<div id="input-container"></div>
<div id="rules-list">
<h3>当前规则列表</h3>
<div id="rules-rows"></div>
</div>
<div>
<button id="add-rule-row">新增规则</button>
<button id="save-rules">保存</button>
<button id="cancel-rule">取消</button>
</div>
`;
document.body.appendChild(popup);
const inputContainer = document.getElementById('input-container');
const rulesRows = document.getElementById('rules-rows');
let isSimpleMode = false;
let tempRules = [...rules]; // 临时存储规则,保存时写入
// 正则模式输入
function showRegexInput() {
inputContainer.innerHTML = `
<div class="rule-row">
<input type="text" class="regex-input" placeholder="正则规则" />
<input type="number" class="level-input" placeholder="层级" value="0" min="0" />
<button class="preview-rule">预览</button>
</div>
`;
attachPreviewListeners();
}
// 简单模式输入
function showSimpleInput() {
inputContainer.innerHTML = `
<div class="rule-row">
<select class="logic-select">
<option value="contains">包含</option>
<option value="not-contains">不包含</option>
<option value="equals">等于</option>
</select>
<input type="text" class="simple-input" placeholder="文本内容" />
<input type="number" class="level-input" placeholder="层级" value="0" min="0" />
<button class="preview-rule">预览</button>
</div>
`;
attachPreviewListeners();
}
// 切换模式
document.getElementById('regex-mode').addEventListener('click', () => {
isSimpleMode = false;
showRegexInput();
});
document.getElementById('simple-mode').addEventListener('click', () => {
isSimpleMode = true;
showSimpleInput();
});
// 默认显示正则模式
showRegexInput();
// 渲染现有规则
function renderRules() {
rulesRows.innerHTML = '';
if (tempRules.length === 0) {
rulesRows.innerHTML = '<p>暂无规则</p>';
return;
}
tempRules.forEach((rule, index) => {
const row = document.createElement('div');
row.className = 'rule-row';
row.innerHTML = `
<input type="text" class="rule-regex" value="${rule.regex}" />
<input type="number" class="rule-level" value="${rule.level}" min="0" />
<button class="delete-rule">删除</button>
`;
const regexInput = row.querySelector('.rule-regex');
const levelInput = row.querySelector('.rule-level');
const deleteBtn = row.querySelector('.delete-rule');
// 实时更新临时规则
regexInput.addEventListener('input', () => {
tempRules[index].regex = regexInput.value;
});
levelInput.addEventListener('input', () => {
tempRules[index].level = parseInt(levelInput.value, 10);
});
// 删除规则
deleteBtn.addEventListener('click', () => {
tempRules.splice(index, 1);
renderRules();
});
rulesRows.appendChild(row);
});
}
// 新增规则行
document.getElementById('add-rule-row').addEventListener('click', () => {
let regex, level;
if (isSimpleMode) {
const logic = inputContainer.querySelector('.logic-select').value;
const text = inputContainer.querySelector('.simple-input').value.trim();
level = parseInt(inputContainer.querySelector('.level-input').value, 10);
if (!text || isNaN(level) || level < 0) {
alert('请输入有效的文本和层级');
return;
}
regex = convertSimpleToRegex(logic, text);
} else {
regex = inputContainer.querySelector('.regex-input').value.trim();
level = parseInt(inputContainer.querySelector('.level-input').value, 10);
if (!regex || isNaN(level) || level < 0) {
alert('请输入有效的正则规则和层级');
return;
}
}
tempRules.push({ regex, level });
renderRules();
// 清空输入框
if (isSimpleMode) {
inputContainer.querySelector('.simple-input').value = '';
inputContainer.querySelector('.level-input').value = '0';
} else {
inputContainer.querySelector('.regex-input').value = '';
inputContainer.querySelector('.level-input').value = '0';
}
});
// 保存规则
document.getElementById('save-rules').addEventListener('click', () => {
regexBlocks[domain] = tempRules;
GM_setValue('regexBlocks', regexBlocks);
applyRegexBlocks();
document.body.removeChild(popup);
});
// 取消
document.getElementById('cancel-rule').addEventListener('click', () => {
document.body.removeChild(popup);
});
// 将简单模式转换为正则表达式
function convertSimpleToRegex(logic, text) {
const escapedText = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
switch (logic) {
case 'contains': return `.*${escapedText}.*`;
case 'not-contains': return `^(?!.*${escapedText}).*$`;
case 'equals': return `^${escapedText}$`;
default: return escapedText;
}
}
// 为预览按钮绑定事件
function attachPreviewListeners() {
const previewBtn = inputContainer.querySelector('.preview-rule');
let previewActive = false;
let affectedElements = [];
previewBtn.addEventListener('click', () => {
if (!previewActive) {
let regex, level;
if (isSimpleMode) {
const logic = inputContainer.querySelector('.logic-select').value;
const text = inputContainer.querySelector('.simple-input').value.trim();
level = parseInt(inputContainer.querySelector('.level-input').value, 10);
if (!text || isNaN(level) || level < 0) {
alert('请输入有效的文本和层级');
return;
}
regex = convertSimpleToRegex(logic, text);
} else {
regex = inputContainer.querySelector('.regex-input').value.trim();
level = parseInt(inputContainer.querySelector('.level-input').value, 10);
if (!regex || isNaN(level) || level < 0) {
alert('请输入有效的正则规则和层级');
return;
}
}
try {
const ruleRegex = new RegExp(regex);
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
let node;
affectedElements = [];
while (node = walker.nextNode()) {
if (ruleRegex.test(node.textContent)) {
let element = node.parentElement;
for (let i = 0; i < level; i++) {
if (element.parentElement) {
element = element.parentElement;
} else {
break;
}
}
element.style.display = 'none';
affectedElements.push(element);
}
}
previewActive = true;
previewBtn.textContent = '取消预览';
} catch (e) {
alert('正则表达式无效,请检查输入');
}
} else {
affectedElements.forEach(el => el.style.display = '');
affectedElements = [];
previewActive = false;
previewBtn.textContent = '预览';
}
});
}
// 初始渲染规则
renderRules();
}
// 应用规则屏蔽
function applyRegexBlocks() {
const domain = window.location.hostname;
const regexBlocks = GM_getValue('regexBlocks', {});
const rules = regexBlocks[domain] || [];
rules.forEach(rule => {
try {
const regex = new RegExp(rule.regex);
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
let node;
while (node = walker.nextNode()) {
if (regex.test(node.textContent)) {
let element = node.parentElement;
for (let i = 0; i < rule.level; i++) {
if (element.parentElement) {
element = element.parentElement;
} else {
break;
}
}
element.style.display = 'none';
}
}
} catch (e) {
console.error(`无法应用规则: ${rule.regex}`, e);
}
});
}
// 页面加载时立即应用屏蔽规则
applyBlocks();
const observer = new MutationObserver(() => applyBlocks());
observer.observe(document.body, { childList: true, subtree: true });
})();