Block Specified Websites with Control Panel (Vue & Element UI)
// ==UserScript==
// @name Block Specified Websites with Control Panel (Vue & Element UI)
// @namespace https://bbs.tampermonkey.net.cn/
// @version 1.7.1
// @description 更新了外观和调整了界面
// @author
// @match *://*/*
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
/* =======================
工具函数:加载外部资源
======================= */
function loadScript(url) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
function loadCSS(url) {
return new Promise((resolve, reject) => {
const link = document.createElement('link');
link.rel = "stylesheet";
link.href = url;
link.onload = resolve;
link.onerror = reject;
document.head.appendChild(link);
});
}
/* =======================
数据存储与基本判断
======================= */
let blockedSites = GM_getValue('blockedSites', null);
if (!blockedSites) {
blockedSites = ["youtube.com"];
GM_setValue('blockedSites', blockedSites);
}
const currentHost = window.location.hostname;
function isSiteBlocked(host) {
return blockedSites.some(site => host === site || host.endsWith('.' + site));
}
/* =======================
拦截页面
当当前网站在封禁列表中时,直接将 body 内容替换成拦截页面内容,
提示用户该网站已被屏蔽,并提醒利用右上角的控制面板解除屏蔽。
======================= */
function blockPage() {
document.body.innerHTML = `
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;
height: 100vh; background: #ff4444; color: white; font-family: sans-serif; text-align: center;">
<h1 style="font-size: 3em;">⚠️ 禁止访问此网站 ⚠️</h1>
<p style="font-size: 1.5em;">此网站已被屏蔽</p>
<p style="margin-top: 20px;">请使用右上角的控制面板解除屏蔽</p>
</div>
`;
}
if (isSiteBlocked(currentHost)) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', blockPage);
} else {
blockPage();
}
// 注意:这里不终止后续脚本执行,控制面板依然会初始化(容器挂在 document.documentElement 上),方便解除封禁。
}
/* =======================
为 Vue 应用创建容器
将容器添加到 <html> 元素中,避免被 body 替换后丢失。
======================= */
function createContainer() {
const container = document.createElement('div');
container.id = 'block-control-app';
document.documentElement.appendChild(container);
}
/* =======================
加载 Vue 与 Element UI 资源
======================= */
function loadLibraries() {
return Promise.all([
loadCSS('https://unpkg.com/element-ui/lib/theme-chalk/index.css'),
loadScript('https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js'),
loadScript('https://unpkg.com/element-ui/lib/index.js')
]);
}
/* =======================
自定义拖拽指令
用法说明:
- 若希望拖拽时移动的是父元素,则使用 v-drag:parent
- 否则直接 v-drag 使该元素自身可拖拽
======================= */
function registerDragDirective(Vue) {
Vue.directive('drag', {
bind(el, binding) {
const dragTarget = binding.arg === 'parent' ? el.parentElement : el;
el.style.cursor = 'move';
el.onmousedown = function(e) {
e.preventDefault();
const disX = e.clientX - dragTarget.offsetLeft;
const disY = e.clientY - dragTarget.offsetTop;
function onMouseMove(e) {
dragTarget.style.left = (e.clientX - disX) + 'px';
dragTarget.style.top = (e.clientY - disY) + 'px';
}
function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
}
});
}
/* =======================
初始化 Vue 应用
控制面板采用 Element UI 的卡片、输入框、折叠面板等组件构造,
并根据 visible 状态显示或隐藏;隐藏时显示一个可拖拽的悬浮小球。
此处所有按钮均使用了图标而非纯文字(例如:封禁/解除封禁按钮分别使用锁/开锁图标)。
======================= */
function initVueApp() {
new Vue({
el: '#block-control-app',
data: {
blockedSites: blockedSites,
newSite: '',
currentHost: currentHost,
visible: true // 控制面板是否显示
},
computed: {
isBlocked() {
return this.blockedSites.some(site => this.currentHost === site || this.currentHost.endsWith('.' + site));
}
},
methods: {
toggleCurrentSite() {
if (this.isBlocked) {
this.blockedSites = this.blockedSites.filter(site => !(this.currentHost === site || this.currentHost.endsWith('.' + site)));
GM_setValue('blockedSites', this.blockedSites);
alert("已解除封禁此网站,请刷新页面。");
location.reload();
} else {
this.blockedSites.push(this.currentHost);
GM_setValue('blockedSites', this.blockedSites);
alert("已封禁此网站,请刷新页面。");
location.reload();
}
},
addSite() {
const newSiteTrim = this.newSite.trim();
if (newSiteTrim && !this.blockedSites.includes(newSiteTrim)) {
this.blockedSites.push(newSiteTrim);
GM_setValue('blockedSites', this.blockedSites);
this.newSite = '';
}
},
removeSite(index) {
this.blockedSites.splice(index, 1);
GM_setValue('blockedSites', this.blockedSites);
location.reload();
},
hidePanel() {
this.visible = false;
},
showPanel() {
this.visible = true;
}
},
template: `
<div>
<!-- 控制面板 -->
<div v-if="visible" id="controlPanel" style="position: fixed; top: 50px; right: 20px; z-index: 100000;">
<el-card shadow="always">
<div slot="header" v-drag:parent style="cursor: move; user-select: none; display: flex; justify-content: space-between; align-items: center;">
<span><i class="el-icon-warning" style="margin-right: 5px;"></i> 网站屏蔽面板</span>
<el-button type="text" icon="el-icon-close" @click="hidePanel"></el-button>
</div>
<div>
<el-button type="primary" block @click="toggleCurrentSite">
<i :class="isBlocked ? 'el-icon-unlock' : 'el-icon-lock'" style="margin-right: 5px;"></i>
<!-- 如希望仅用图标,可将下面文本注释掉 -->
<!-- {{ isBlocked ? '解除封禁' : '封禁' }} -->
</el-button>
<el-collapse>
<el-collapse-item title="已封禁的网站列表" name="1">
<ul style="list-style: none; padding: 0; margin: 0;">
<li v-for="(site, index) in blockedSites" :key="site" style="display: flex; justify-content: space-between; align-items: center; padding: 5px 0; border-bottom: 1px solid #ebeef5;">
<span>{{ site }}</span>
<el-button type="text" size="mini" icon="el-icon-delete" @click="removeSite(index)"></el-button>
</li>
</ul>
</el-collapse-item>
</el-collapse>
<div style="margin-top: 10px; display: flex; align-items: center;">
<el-input placeholder="添加新网站" v-model="newSite" size="small"></el-input>
<el-button type="success" size="small" icon="el-icon-circle-plus" @click="addSite"></el-button>
</div>
</div>
</el-card>
</div>
<!-- 悬浮小球 -->
<div v-else id="floatingBall" v-drag
style="position: fixed; right: 20px; bottom: 20px; width: 40px; height: 40px; background: #333; border-radius: 50%;
color: white; display: flex; justify-content: center; align-items: center; cursor: pointer; z-index: 100000; font-size: 20px;"
@click="showPanel">
<i class="el-icon-view"></i>
</div>
</div>
`
});
}
/* =======================
主初始化流程
======================= */
function init() {
createContainer();
loadLibraries().then(() => {
registerDragDirective(Vue);
initVueApp();
}).catch(err => {
console.error('加载 Vue 或 Element UI 失败:', err);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();