diff --git a/front/src/App.vue b/front/src/App.vue index 3c63942..73fac8b 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -22,6 +22,9 @@ const lotteryParticipants = ref[]>([]); const shopForm = reactive({ item_key: '', name: '', cost: 0, desc: '', enabled: true }); const lotteryForm = reactive({ id: 0, title: '', prize: '', description: '', condition_type: 'all', condition_value: 0, draw_type: 'people', end_type: 'people', end_value: 3, max_participants: 3, draw_at: '', status: 'draft' }); const activeModal = ref<'shop' | 'lottery' | 'bans' | null>(null); +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 newMessage = ref(''); const sending = ref(false); const restarting = ref(false); @@ -67,6 +70,24 @@ function tagColor(item: ChatItem) { return 'primary'; } +function showToast(message: string) { + toast.value = message; + window.setTimeout(() => { if (toast.value === message) toast.value = ''; }, 2600); +} +function askConfirm(title: string, message: string, options: { confirmText?: string; danger?: boolean } = {}) { + confirmState.open = true; + confirmState.title = title; + confirmState.message = message; + confirmState.confirmText = options.confirmText || '确认'; + confirmState.danger = Boolean(options.danger); + return new Promise((resolve) => { confirmState.resolver = resolve; }); +} +function closeConfirm(value: boolean) { + confirmState.open = false; + confirmState.resolver?.(value); + confirmState.resolver = null; +} + async function fetchJson(url: string, options?: RequestInit) { const res = await fetch(url, options); const data = await res.json().catch(() => ({})); @@ -115,7 +136,7 @@ async function loadShopTransactions() { try { shopTransactions.value = await fetchJson(`/api/shop-transactions${selectedQuery.value}`); } catch {} } async function saveShopItem() { - if (!shopForm.item_key || !shopForm.name) return alert('请填写商品编号和名称'); + if (!shopForm.item_key || !shopForm.name) return showToast('请填写商品编号和名称'); await fetchJson('/api/shop-items', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...shopForm, chat_id: selectedChatId.value || 0 }) @@ -125,7 +146,7 @@ async function saveShopItem() { } async function editShopItem(item: ShopItem) { Object.assign(shopForm, item); } async function deleteShopItem(item: ShopItem) { - if (!window.confirm(`确认删除商品 ${item.name}?`)) return; + if (!await askConfirm('删除商品', `确认删除商品「${item.name}」?交易记录会保留。`, { confirmText: '删除', danger: true })) return; await fetchJson(`/api/shop-items/${item.item_key}${selectedQuery.value}`, { method: 'DELETE' }); await loadShopItems(); } @@ -135,7 +156,7 @@ async function loadLotteries() { function resetLotteryForm() { Object.assign(lotteryForm, { id: 0, title: '', prize: '', description: '', condition_type: 'all', condition_value: 0, draw_type: 'people', end_type: 'people', end_value: 3, max_participants: 3, draw_at: '', status: 'draft' }); } function editLottery(item: LotteryItem) { Object.assign(lotteryForm, { ...item, id: item.id || 0 }); } async function saveLottery() { - if (!lotteryForm.title || !lotteryForm.prize) return alert('请填写抽奖标题和奖品'); + if (!lotteryForm.title || !lotteryForm.prize) return showToast('请填写抽奖标题和奖品'); lotteryForm.end_type = lotteryForm.draw_type; if (lotteryForm.draw_type === 'people') lotteryForm.max_participants = Number(lotteryForm.end_value || lotteryForm.max_participants || 1); const url = lotteryForm.id ? `/api/lotteries/${lotteryForm.id}` : '/api/lotteries'; @@ -144,7 +165,7 @@ async function saveLottery() { await loadLotteries(); } async function deleteLottery(item: LotteryItem) { - if (!window.confirm(`确认删除抽奖 ${item.title || item.prize}?`)) return; + if (!await askConfirm('删除抽奖', `确认删除抽奖「${item.title || item.prize}」?参与记录会一并移除。`, { confirmText: '删除', danger: true })) return; await fetchJson(`/api/lotteries/${item.id}`, { method: 'DELETE' }); await loadLotteries(); } @@ -152,12 +173,12 @@ async function loadLotteryParticipants(item: LotteryItem) { lotteryParticipants.value = await fetchJson(`/api/lotteries/${item.id}/participants`); } async function cancelLottery(item: LotteryItem) { - if (!window.confirm('确认取消这个抽奖?')) return; + if (!await askConfirm('取消抽奖', '确认取消这个抽奖?已参与用户不会自动补偿。', { confirmText: '取消抽奖', danger: true })) return; await fetchJson(`/api/lotteries/${item.id}/cancel`, { method: 'POST' }); await loadLotteries(); } async function drawLottery(item: LotteryItem) { - if (!window.confirm('确认立即开奖?')) return; + if (!await askConfirm('立即开奖', '确认现在开奖?开奖后状态会变更。', { confirmText: '立即开奖' })) return; await fetchJson(`/api/lotteries/${item.id}/draw`, { method: 'POST' }); await loadLotteries(); } @@ -218,14 +239,14 @@ async function sendMessage() { }); newMessage.value = ''; } catch (e: any) { - alert(`发送失败:${e.message}`); + showToast(`发送失败:${e.message}`); } finally { sending.value = false; } } async function deleteChatMessage(item: ChatItem, ban = false) { const action = ban ? '删除并封禁' : '删除'; - if (!window.confirm(`确认${action}这条消息?`)) return; + if (!await askConfirm(action, `确认${action}这条消息?`, { confirmText: action, danger: true })) return; item._deleting = true; try { await fetchJson(`/api/chat/${item.id}/delete`, { @@ -236,37 +257,37 @@ async function deleteChatMessage(item: ChatItem, ban = false) { Object.assign(item, { deleted: true, manually_deleted: true }); await loadStats(); } catch (e: any) { - alert(`${action}失败:${e.message}`); + showToast(`${action}失败:${e.message}`); } finally { item._deleting = false; } } async function unban(item: BanItem) { - if (!window.confirm(`确认解封 ${item.username || item.user_id}?`)) return; + if (!await askConfirm('解除封禁', `确认解封 ${item.username || item.user_id}?`, { confirmText: '解封' })) return; item._loading = true; try { await fetchJson(`/api/unban/${item.user_id}`, { method: 'POST' }); item.auto_unban = false; await loadStats(); } catch (e: any) { - alert(`解封失败:${e.message}`); + showToast(`解封失败:${e.message}`); } finally { item._loading = false; } } async function restartBot() { - if (!window.confirm('确认只重启 Telegram Bot?Web 管理页不会重启。')) return; + if (!await askConfirm('重启 Bot', '确认只重启 Telegram Bot?Web 管理页不会重启。', { confirmText: '重启 Bot' })) return; restartingBot.value = true; try { await fetchJson('/api/restart-bot', { method: 'POST' }); window.setTimeout(() => { restartingBot.value = false; loadLogs(); }, 5000); } catch (e: any) { restartingBot.value = false; - alert(`重启 Bot 失败:${e.message}`); + showToast(`重启 Bot 失败:${e.message}`); } } async function restartService() { - if (!window.confirm('确认重启 TG Spam Guard 服务?重启期间 Web 和 Bot 会短暂不可用。')) return; + if (!await askConfirm('重启服务', '确认重启 TG Spam Guard 服务?重启期间 Web 和 Bot 会短暂不可用。', { confirmText: '重启服务', danger: true })) return; restarting.value = true; try { await fetchJson('/api/restart', { method: 'POST' }); @@ -381,48 +402,43 @@ onBeforeUnmount(() => { 关闭 -