feat: add ban record context actions

This commit is contained in:
ngfchl
2026-07-07 23:57:31 +08:00
parent bbb33ef341
commit 5ee64bde60
7 changed files with 405 additions and 342 deletions
+62 -4
View File
@@ -47,6 +47,7 @@ const activeTab = reactive({ shop: 'items', lottery: 'events' });
const toast = ref('');
const confirmState = reactive({ open: false, title: '', message: '', confirmText: '确认', danger: false, resolver: null as null | ((value: boolean) => void) });
const chatContextMenu = reactive({ open: false, x: 0, y: 0, item: null as ChatItem | null });
const banContextMenu = reactive({ open: false, x: 0, y: 0, item: null as BanItem | null });
const banDialog = reactive({ open: false, mode: 'ban' as 'ban' | 'delete_ban', item: null as ChatItem | null, duration: 0, customMinutes: '' });
const banDurationOptions = [
{ label: '永久', value: 0 },
@@ -627,6 +628,18 @@ function closeChatContextMenu() {
chatContextMenu.open = false;
chatContextMenu.item = null;
}
function openBanContextMenu(event: MouseEvent, item: BanItem) {
banContextMenu.item = item;
banContextMenu.open = true;
const menuWidth = 180;
const menuHeight = 92;
banContextMenu.x = Math.min(event.clientX, window.innerWidth - menuWidth - 8);
banContextMenu.y = Math.min(event.clientY, window.innerHeight - menuHeight - 8);
}
function closeBanContextMenu() {
banContextMenu.open = false;
banContextMenu.item = null;
}
async function contextAddToRules() {
const item = chatContextMenu.item;
closeChatContextMenu();
@@ -778,12 +791,14 @@ async function deleteChatMessage(item: ChatItem, ban = false, durationMinutes =
item._deleting = false;
}
}
async function unban(item: BanItem) {
if (!await askConfirm('解除封禁', `确认解封 ${item.username || item.user_id}`, { confirmText: '解封' })) return;
async function unban(item: BanItem, confirmed = false) {
if (!confirmed && !await askConfirm('解除封禁', `确认解封 ${item.username || item.user_id}`, { confirmText: '解封' })) return;
item._loading = true;
try {
await fetchJson(`/api/unban/${item.user_id}`, { method: 'POST' });
const query = item.chat_id ? `?chat_id=${item.chat_id}` : '';
await fetchJson(`/api/unban/${item.user_id}${query}`, { method: 'POST' });
item.auto_unban = false;
await loadBans();
await loadStats();
} catch (e: any) {
showToast(`解封失败:${e.message}`);
@@ -791,6 +806,32 @@ async function unban(item: BanItem) {
item._loading = false;
}
}
async function contextUnban() {
const item = banContextMenu.item;
closeBanContextMenu();
if (item) await unban(item);
}
async function contextWhitelistAndUnban() {
const item = banContextMenu.item;
closeBanContextMenu();
if (!item?.user_id || !item?.chat_id) return showToast('缺少群组或用户 ID,无法加入白名单');
if (!await askConfirm('加入白名单并解封', `确认将 @${item.username || item.user_id} 加入当前群组白名单并立即解封?`, { confirmText: '加入并解封', danger: true })) return;
item._loading = true;
try {
await fetchJson('/api/whitelist', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: item.chat_id, user_id: item.user_id, username: item.username, first_name: item.first_name, reason: '封禁记录右键加入白名单' })
});
await unban(item, true);
await loadWhitelist();
showToast('已加入白名单并解封');
} catch (e: any) {
showToast(`加入白名单并解封失败:${e.message}`);
} finally {
item._loading = false;
}
}
async function restartBot() {
if (!await askConfirm('重启 Bot', '确认只重启 Telegram BotWeb 管理页不会重启。', { confirmText: '重启 Bot' })) return;
restartingBot.value = true;
@@ -839,13 +880,17 @@ async function bootstrapData() {
}
onMounted(async () => {
window.addEventListener('click', closeChatContextMenu);
window.addEventListener('click', closeBanContextMenu);
window.addEventListener('scroll', closeChatContextMenu, true);
window.addEventListener('scroll', closeBanContextMenu, true);
await checkAuth();
if (authenticated.value) await bootstrapData();
});
onBeforeUnmount(() => {
window.removeEventListener('click', closeChatContextMenu);
window.removeEventListener('click', closeBanContextMenu);
window.removeEventListener('scroll', closeChatContextMenu, true);
window.removeEventListener('scroll', closeBanContextMenu, true);
if (es) es.close();
if (statTimer) window.clearInterval(statTimer);
});
@@ -957,7 +1002,7 @@ onBeforeUnmount(() => {
<template #extra><SButton size="xs" variant="outline" @click="loadBans">刷新</SButton></template>
<div class="ban-list overview-ban-list v-overflow">
<div v-if="!bans.length" class="empty">暂无封禁记录</div>
<div v-for="(item, i) in bans" :key="item.id || i" class="ban-item">
<div v-for="(item, i) in bans" :key="item.id || i" class="ban-item" @contextmenu.prevent.stop="openBanContextMenu($event, item)">
<div class="ban-main">
<b>@{{ item.username || item.first_name || item.user_id }}</b>
<span>{{ item.reason || '-' }}</span>
@@ -974,6 +1019,19 @@ onBeforeUnmount(() => {
</SCard>
</section>
<div
v-if="banContextMenu.open"
class="chat-context-menu ban-context-menu"
:style="{ left: `${banContextMenu.x}px`, top: `${banContextMenu.y}px` }"
@click.stop
@contextmenu.prevent
>
<button @click="contextUnban">🔓 解封</button>
<button @click="contextWhitelistAndUnban">🛡 加入白名单并解封</button>
</div>
<div
v-if="chatContextMenu.open"
class="chat-context-menu"
+4
View File
@@ -659,3 +659,7 @@ body { background: #f3f6fb; }
.ban-dialog-summary { margin-top: 12px; padding: 10px 12px; border-radius: 12px; background: #fff7ed; color: #9a3412; font-size: 13px; }
.ban-dialog-summary b { color: #7c2d12; }
@media (max-width: 520px) { .ban-duration-grid { grid-template-columns: repeat(2, 1fr); } }
.ban-context-menu { width: 180px; }
.ban-context-menu button { white-space: nowrap; }
.overview-ban-list .ban-item { cursor: context-menu; }
+7 -6
View File
@@ -1142,14 +1142,15 @@ async def api_scores(limit: int = Query(default=50, ge=1, le=200)):
@app.post("/api/unban/{user_id}")
async def api_unban(user_id: int):
async def api_unban(user_id: int, chat_id: int | None = None):
import config
target_chat_id = int(chat_id or config.TG_CHAT_ID)
try:
await run_tg_request(lambda bot: bot.unban_chat_member(chat_id=config.TG_CHAT_ID, user_id=user_id), label="unban_chat_member")
await Ban.filter(user_id=user_id).update(auto_unban=False, unban_at=now_tz().replace(tzinfo=None))
await Action.create(action="UNBAN", user_id=user_id, details={"method": "web"})
await broadcast_sse("unban", {"user_id": user_id})
return JSONResponse({"ok": True, "user_id": user_id})
await run_tg_request(lambda bot: bot.unban_chat_member(chat_id=target_chat_id, user_id=user_id), label="unban_chat_member")
await Ban.filter(user_id=user_id, chat_id=target_chat_id).update(auto_unban=False, unban_at=now_tz().replace(tzinfo=None))
await Action.create(action="UNBAN", chat_id=target_chat_id, user_id=user_id, details={"method": "web"})
await broadcast_sse("unban", {"chat_id": target_chat_id, "user_id": user_id})
return JSONResponse({"ok": True, "chat_id": target_chat_id, "user_id": user_id})
except Exception as e:
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/static/icon.svg" />
<title>TG Spam Guard</title>
<script type="module" crossorigin src="/static/assets/index-gkDPm5Bg.js"></script>
<link rel="stylesheet" crossorigin href="/static/assets/index-CRDYB55K.css">
<script type="module" crossorigin src="/static/assets/index-Bm1Rc1aa.js"></script>
<link rel="stylesheet" crossorigin href="/static/assets/index-Kvx4PVK4.css">
</head>
<body>
<div id="app"></div>