feat: add ban record context actions
This commit is contained in:
+62
-4
@@ -47,6 +47,7 @@ const activeTab = reactive({ shop: 'items', lottery: 'events' });
|
|||||||
const toast = ref('');
|
const toast = ref('');
|
||||||
const confirmState = reactive({ open: false, title: '', message: '', confirmText: '确认', danger: false, resolver: null as null | ((value: boolean) => void) });
|
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 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 banDialog = reactive({ open: false, mode: 'ban' as 'ban' | 'delete_ban', item: null as ChatItem | null, duration: 0, customMinutes: '' });
|
||||||
const banDurationOptions = [
|
const banDurationOptions = [
|
||||||
{ label: '永久', value: 0 },
|
{ label: '永久', value: 0 },
|
||||||
@@ -627,6 +628,18 @@ function closeChatContextMenu() {
|
|||||||
chatContextMenu.open = false;
|
chatContextMenu.open = false;
|
||||||
chatContextMenu.item = null;
|
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() {
|
async function contextAddToRules() {
|
||||||
const item = chatContextMenu.item;
|
const item = chatContextMenu.item;
|
||||||
closeChatContextMenu();
|
closeChatContextMenu();
|
||||||
@@ -778,12 +791,14 @@ async function deleteChatMessage(item: ChatItem, ban = false, durationMinutes =
|
|||||||
item._deleting = false;
|
item._deleting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function unban(item: BanItem) {
|
async function unban(item: BanItem, confirmed = false) {
|
||||||
if (!await askConfirm('解除封禁', `确认解封 ${item.username || item.user_id}?`, { confirmText: '解封' })) return;
|
if (!confirmed && !await askConfirm('解除封禁', `确认解封 ${item.username || item.user_id}?`, { confirmText: '解封' })) return;
|
||||||
item._loading = true;
|
item._loading = true;
|
||||||
try {
|
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;
|
item.auto_unban = false;
|
||||||
|
await loadBans();
|
||||||
await loadStats();
|
await loadStats();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
showToast(`解封失败:${e.message}`);
|
showToast(`解封失败:${e.message}`);
|
||||||
@@ -791,6 +806,32 @@ async function unban(item: BanItem) {
|
|||||||
item._loading = false;
|
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() {
|
async function restartBot() {
|
||||||
if (!await askConfirm('重启 Bot', '确认只重启 Telegram Bot?Web 管理页不会重启。', { confirmText: '重启 Bot' })) return;
|
if (!await askConfirm('重启 Bot', '确认只重启 Telegram Bot?Web 管理页不会重启。', { confirmText: '重启 Bot' })) return;
|
||||||
restartingBot.value = true;
|
restartingBot.value = true;
|
||||||
@@ -839,13 +880,17 @@ async function bootstrapData() {
|
|||||||
}
|
}
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
window.addEventListener('click', closeChatContextMenu);
|
window.addEventListener('click', closeChatContextMenu);
|
||||||
|
window.addEventListener('click', closeBanContextMenu);
|
||||||
window.addEventListener('scroll', closeChatContextMenu, true);
|
window.addEventListener('scroll', closeChatContextMenu, true);
|
||||||
|
window.addEventListener('scroll', closeBanContextMenu, true);
|
||||||
await checkAuth();
|
await checkAuth();
|
||||||
if (authenticated.value) await bootstrapData();
|
if (authenticated.value) await bootstrapData();
|
||||||
});
|
});
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('click', closeChatContextMenu);
|
window.removeEventListener('click', closeChatContextMenu);
|
||||||
|
window.removeEventListener('click', closeBanContextMenu);
|
||||||
window.removeEventListener('scroll', closeChatContextMenu, true);
|
window.removeEventListener('scroll', closeChatContextMenu, true);
|
||||||
|
window.removeEventListener('scroll', closeBanContextMenu, true);
|
||||||
if (es) es.close();
|
if (es) es.close();
|
||||||
if (statTimer) window.clearInterval(statTimer);
|
if (statTimer) window.clearInterval(statTimer);
|
||||||
});
|
});
|
||||||
@@ -957,7 +1002,7 @@ onBeforeUnmount(() => {
|
|||||||
<template #extra><SButton size="xs" variant="outline" @click="loadBans">刷新</SButton></template>
|
<template #extra><SButton size="xs" variant="outline" @click="loadBans">刷新</SButton></template>
|
||||||
<div class="ban-list overview-ban-list v-overflow">
|
<div class="ban-list overview-ban-list v-overflow">
|
||||||
<div v-if="!bans.length" class="empty">暂无封禁记录</div>
|
<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">
|
<div class="ban-main">
|
||||||
<b>@{{ item.username || item.first_name || item.user_id }}</b>
|
<b>@{{ item.username || item.first_name || item.user_id }}</b>
|
||||||
<span>{{ item.reason || '-' }}</span>
|
<span>{{ item.reason || '-' }}</span>
|
||||||
@@ -974,6 +1019,19 @@ onBeforeUnmount(() => {
|
|||||||
</SCard>
|
</SCard>
|
||||||
</section>
|
</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
|
<div
|
||||||
v-if="chatContextMenu.open"
|
v-if="chatContextMenu.open"
|
||||||
class="chat-context-menu"
|
class="chat-context-menu"
|
||||||
|
|||||||
@@ -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 { margin-top: 12px; padding: 10px 12px; border-radius: 12px; background: #fff7ed; color: #9a3412; font-size: 13px; }
|
||||||
.ban-dialog-summary b { color: #7c2d12; }
|
.ban-dialog-summary b { color: #7c2d12; }
|
||||||
@media (max-width: 520px) { .ban-duration-grid { grid-template-columns: repeat(2, 1fr); } }
|
@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
@@ -1142,14 +1142,15 @@ async def api_scores(limit: int = Query(default=50, ge=1, le=200)):
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/api/unban/{user_id}")
|
@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
|
import config
|
||||||
|
target_chat_id = int(chat_id or config.TG_CHAT_ID)
|
||||||
try:
|
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 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).update(auto_unban=False, unban_at=now_tz().replace(tzinfo=None))
|
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", user_id=user_id, details={"method": "web"})
|
await Action.create(action="UNBAN", chat_id=target_chat_id, user_id=user_id, details={"method": "web"})
|
||||||
await broadcast_sse("unban", {"user_id": user_id})
|
await broadcast_sse("unban", {"chat_id": target_chat_id, "user_id": user_id})
|
||||||
return JSONResponse({"ok": True, "user_id": user_id})
|
return JSONResponse({"ok": True, "chat_id": target_chat_id, "user_id": user_id})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
|
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
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/icon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/static/icon.svg" />
|
||||||
<title>TG Spam Guard</title>
|
<title>TG Spam Guard</title>
|
||||||
<script type="module" crossorigin src="/static/assets/index-gkDPm5Bg.js"></script>
|
<script type="module" crossorigin src="/static/assets/index-Bm1Rc1aa.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/static/assets/index-CRDYB55K.css">
|
<link rel="stylesheet" crossorigin href="/static/assets/index-Kvx4PVK4.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user