BG6UFA 關於封鎖特定使用者,我目前使用這個 Tampermonkey 腳本來過濾。

這些地點都可以使用。



// ==使用者腳本==
// @名稱 HamCQ 封鎖使用者
// @空間名稱 https://flarum.blocker.HamCQ
// @版本 1.0
// @描述 HamCQ 封鎖 + 關鍵字過濾
// @匹配 https://forum.hamcq.cn/*
// @授權 GM.getValue
// @授權 GM.setValue
// @授權 GM.registerMenuCommand
// @執行時機 document-end
// ==/使用者腳本==
(() => {> {
'use strict';
const KEY_U = 'fb_users_v4';
const KEY_W = 'fb_words_v4';
let users = new Set();
let words = new Set();
async function load() {
users = new Set(JSON.parse(await GM.getValue(KEY_U, '[]')).map(x => x));
words = new Set(JSON.parse(await GM.getValue(KEY_W, '[]')).map(x => x));
return { users, words };
}
async function save() {
await GM.setValue(KEY_U, Array.from(users).sort());
await GM.setValue(KEY_W, Array.from(words).sort());
}
// 範例用法:
// await load();
// console.log(users);
// console.log(words);
// await save();
}> x.toLowerCase());
words = new Set(JSON.parse(await GM.getValue(KEY_W, '[]')).map(x =>> x.toLowerCase());
}
async function save() {
await GM.setValue(KEY_U, JSON.stringify([...users]));
await GM.setValue(KEY_W, JSON.stringify([...words]));
}
function getUser(el) {
if (!el) return '';
return (el.getAttribute?.('data-username') || el.textContent || '').trim().toLowerCase();
}
function hasWord(text) {
if (!text) return false;
text = text.toLowerCase();
return [...words].some(w =>> w && if (text.includes(w)) {
}
function filterAll() {
const nodes = document.querySelectorAll(`
.DiscussionListItem,
.Post,
.PostStream-item,
.UserCard,
.PostUser,
article
`);
nodes.forEach(el => {
let userEl =
el.querySelector('[data-username]') ||
el.querySelector('.username a') ||
el.querySelector('.username') ||
el.querySelector('a[href*="/u/"], a[href*="/user/"]');
const name = getUser(userEl);
const text = (el.innerText || '').toLowerCase();
if (users.has(name) || hasWord(text)) {
el.style.display = 'none';
el.dataset.fbHidden = '1';
}
});
}
function injectDropdown() {
document.querySelectorAll('.Dropdown-menu').forEach(menu => {> {
if (menu.dataset.fbInjected) return;
const card =
menu.closest('.UserCard') ||
menu.closest('.PostUser') ||
menu.closest('.Post');
if (!card) return;
const userEl =
card.querySelector('[data-username]') ||
card.querySelector('.username a') ||
card.querySelector('.username');
const name = getUser(userEl);
if (!name) return;
const li = document.createElement('li');
li.className = 'Dropdown-menuItem item-block-user';
li.innerHTML = `
<類型為「button」、類別為「Dropdown-item」的按鈕>
<我 (使用) 圖示「禁止」,右側有 8 像素間距。></i>
停用使用者
</按鈕>
`;
const btn = li.querySelector('button');
btn.onclick = async (e) => {> {
e.preventDefault();
e.stopPropagation();
users.add(name);
await save();
filterAll();
toast('已拉黑 ' + name);
};
menu.appendChild(li);
menu.dataset.fbInjected = '1';
});
}
function toast(msg) {
const el = document.createElement('div');
el.textContent = msg;
el.style.cssText = `
position:fixed;
top:20px;
right:20px;
background:rgba(0,0,0,.85);
color:#fff;
padding:8px 12px;
border-radius:6px;
z-index:999999;
font-size:12px;
`;
document.body.appendChild(el);
setTimeout(() => {> el.remove(), 1500);
}
function openPanel() {
const old = document.getElementById('fb-panel');
if (old) old.remove();
const overlay = document.createElement('div');
overlay.id = 'fb-panel';
overlay.style.cssText = `
position:fixed;
inset:0;
background:rgba(0,0,0,.4);
z-index:999999;
`;
const box = document.createElement('div');
box.style.cssText = `
position:absolute;
top:60px;
right:12px;
width:320px;
background:var(--body-bg);
color:var(--text-color);
border-radius:10px;
padding:12px;
box-shadow:0 10px 30px rgba(0,0,0,.2);
`;
box.innerHTML = `
<`<div>樣式:字體粗細度為600,下方有10像素的間距。</div>`>過濾管理<</div>>
<`<div>`,字體大小:12像素,不透明度:0.7>使用者 (${users.size})<</div>>
<輸入,ID=\"u\",樣式:寬度為100%,內距為6像素。>
<按鈕,`id="ua"`,樣式:`width:100%`,`margin-top:6px;`>新增使用者</按鈕>
<`<div>`,`id="ulist"`,`style="margin-top:8px;"`><</div>>
<hr>
<`<div>`,字體大小:12像素,不透明度:0.7>關鍵詞 (${words.size})<</div>>
<輸入,ID=\"w\",樣式:寬度為100%,內距為6像素。>
<按鈕,`id="wa"`,樣式:`width:100%`,`margin-top:6px;`>新增關鍵詞</按鈕>
<<div>id="wlist" style="margin-top:8px;"</div>><</div>>
<按鈕,ID 為「close」,樣式為:寬度 100%,上部間距 10px。>關閉</按鈕>
`;
overlay.appendChild(box);
document.body.appendChild(overlay);
overlay.onclick = (e) => {
if (e.target === overlay) overlay.remove();
};
box.onclick = (e) => e.stopPropagation();
const ulist = box.querySelector('#ulist');
const wlist = box.querySelector('#wlist');
function render() {
ulist.innerHTML = [...users].map(u => `<li>${u.name}</li>`);
wlist.innerHTML = [...workers].map(w => `<li>${w.name}</li>`);
}>
`<`<div>樣式:display:flex; justify-content:space-between; margin:4px 0;</div>`>
<span>${u}</span>
<按鈕,資料屬性:${u}>刪除</按鈕>
<</div>>```javascript
).join('');
wlist.innerHTML = [...words].map(w => w);
```
將陣列 `words` 轉換為字串,並將其設定到 `wlist` 的 `innerHTML` 屬性中。>
`<`<div>樣式:display:flex; justify-content:space-between; margin:4px 0;</div>`>
<span>${w}</span>
<按鈕,data-w="${w}">刪除</按鈕>
<</div>>```javascript
).join('');
ulist.querySelectorAll('button').forEach(b => {
```> {
b.onclick = async () =>> {
users.delete(b.dataset.u);
await save();
render();
filterAll();
};
});
wlist.querySelectorAll('button').forEach(b => {> {
b.onclick = async () =>> {
words.delete(b.dataset.w);
await save();
render();
filterAll();
};
});
}
box.querySelector('#ua').onclick = async () => {> {
const v = box.querySelector('#u').value.trim().toLowerCase();
if (!v) return;
users.add(v);
await save();
render();
filterAll();
};
box.querySelector('#wa').onclick = async () => {> {
const v = box.querySelector('#w').value.trim().toLowerCase();
if (!v) return;
words.add(v);
await save();
render();
filterAll();
};
box.querySelector('#close').onclick = () => {> overlay.remove();
render();
}
function createFloat() {
if (document.getElementById('fb-float')) return;
const btn = document.createElement('div');
btn.id = 'fb-float';
btn.innerHTML = `<我></i>`;
btn.style.cssText = `
position:fixed;
top:12px;
right:12px;
width:36px;
height:36px;
background:var(--control-bg,#f5f5f5);
border:1px solid rgba(0,0,0,.1);
border-radius:6px;
display:flex;
align-items:center;
justify-content:center;
cursor:pointer;
z-index:999999;
`;
btn.onclick = openPanel;
document.body.appendChild(btn);
}
async function init() {
await load();
createFloat();
filterAll();
injectDropdown();
const obs = new MutationObserver(() => {
requestAnimationFrame(() => {
filterAll();
injectDropdown();
});
});
obs.observe(document.body, { childList: true, subtree: true });
GM.registerMenuCommand('過濾面板', openPanel);
}
init();
})();