// ==UserScript==
// @name 网页元素屏蔽器
// @namespace http://tampermonkey.net/
// @version 0.1
// @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);
}
.blocker-popup button {
margin: 0 5px;
}
.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;
}
.blocker-list ul {
list-style: none;
padding: 0;
}
.blocker-list li {
margin: 10px 0;
display: flex;
align-items: center;
width: 100%;
}
.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;
}
`;
document.head.appendChild(style);
// 注册菜单
GM_registerMenuCommand('屏蔽设置', startBlockingMode);
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);
// 使用 html2canvas 生成截图
const canvas = await html2canvas(element, { scale: 1 });
const originalWidth = canvas.width;
const originalHeight = canvas.height;
// 计算缩放比例,限制 width ≤ 400,height ≤ 100
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);
}
});
}
}
// 显示屏蔽记录窗口(仅缩略图)
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);
}
}
// 页面加载时立即应用屏蔽规则
applyBlocks();
const observer = new MutationObserver(() => applyBlocks());
observer.observe(document.body, { childList: true, subtree: true });
})();