diff --git a/front/src/App.vue b/front/src/App.vue index 96fbc70..a9d7db5 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -6,8 +6,8 @@ type ChatItem = Record & { _deleting?: boolean }; type BanItem = Record & { _loading?: boolean }; type LogItem = { time?: string; level?: string; message?: string }; type GroupItem = { chat_id: number; name: string }; -type ShopItem = Record; -type LotteryItem = Record; +type ShopItem = Record & { _loading?: boolean }; +type LotteryItem = Record & { _loading?: boolean }; const sseConnected = ref(false); const chatLogs = ref([]); @@ -29,6 +29,10 @@ const authReady = ref(false); const authenticated = ref(false); const loginForm = reactive({ username: '', password: '' }); const loggingIn = ref(false); +const shopSaving = ref(false); +const lotterySaving = ref(false); +const logsRefreshing = ref(false); +const copyingLogs = ref(false); const newMessage = ref(''); const sending = ref(false); const restarting = ref(false); @@ -174,57 +178,81 @@ async function loadGroups() { } catch {} } async function loadShopItems() { - try { shopItems.value = await fetchJson(`/api/shop-items${selectedQuery.value}`); } catch {} + try { shopItems.value = (await fetchJson(`/api/shop-items${selectedQuery.value}`)).map((x: ShopItem) => ({ ...x, _loading: false })); } catch {} } async function loadShopTransactions() { try { shopTransactions.value = await fetchJson(`/api/shop-transactions${selectedQuery.value}`); } catch {} } async function saveShopItem() { 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 }) - }); - Object.assign(shopForm, { item_key: '', name: '', cost: 0, desc: '', enabled: true }); - await loadShopItems(); + if (shopSaving.value) return; + shopSaving.value = true; + try { + await fetchJson('/api/shop-items', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...shopForm, chat_id: selectedChatId.value || 0 }) + }); + Object.assign(shopForm, { item_key: '', name: '', cost: 0, desc: '', enabled: true }); + await loadShopItems(); + } catch (e: any) { showToast(`保存商品失败:${e.message}`); } + finally { shopSaving.value = false; } } async function editShopItem(item: ShopItem) { Object.assign(shopForm, item); } async function deleteShopItem(item: ShopItem) { + if (item._loading) return; if (!await askConfirm('删除商品', `确认删除商品「${item.name}」?交易记录会保留。`, { confirmText: '删除', danger: true })) return; - await fetchJson(`/api/shop-items/${item.item_key}${selectedQuery.value}`, { method: 'DELETE' }); - await loadShopItems(); + item._loading = true; + try { + await fetchJson(`/api/shop-items/${item.item_key}${selectedQuery.value}`, { method: 'DELETE' }); + await loadShopItems(); + } catch (e: any) { showToast(`删除商品失败:${e.message}`); } + finally { item._loading = false; } } async function loadLotteries() { - try { lotteries.value = await fetchJson(`/api/lotteries${selectedQuery.value}`); } catch {} + try { lotteries.value = (await fetchJson(`/api/lotteries${selectedQuery.value}`)).map((x: LotteryItem) => ({ ...x, _loading: false })); } catch {} } 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 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'; - await fetchJson(url, { method: lotteryForm.id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...lotteryForm, chat_id: selectedChatId.value || 0 }) }); - resetLotteryForm(); - await loadLotteries(); + if (lotterySaving.value) return; + lotterySaving.value = true; + try { + 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'; + await fetchJson(url, { method: lotteryForm.id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...lotteryForm, chat_id: selectedChatId.value || 0 }) }); + resetLotteryForm(); + await loadLotteries(); + } catch (e: any) { showToast(`保存抽奖失败:${e.message}`); } + finally { lotterySaving.value = false; } } async function deleteLottery(item: LotteryItem) { + if (item._loading) return; if (!await askConfirm('删除抽奖', `确认删除抽奖「${item.title || item.prize}」?参与记录会一并移除。`, { confirmText: '删除', danger: true })) return; - await fetchJson(`/api/lotteries/${item.id}`, { method: 'DELETE' }); - await loadLotteries(); + item._loading = true; + try { await fetchJson(`/api/lotteries/${item.id}`, { method: 'DELETE' }); await loadLotteries(); } + catch (e: any) { showToast(`删除抽奖失败:${e.message}`); } + finally { item._loading = false; } } async function loadLotteryParticipants(item: LotteryItem) { lotteryParticipants.value = await fetchJson(`/api/lotteries/${item.id}/participants`); } async function cancelLottery(item: LotteryItem) { + if (item._loading) return; if (!await askConfirm('取消抽奖', '确认取消这个抽奖?已参与用户不会自动补偿。', { confirmText: '取消抽奖', danger: true })) return; - await fetchJson(`/api/lotteries/${item.id}/cancel`, { method: 'POST' }); - await loadLotteries(); + item._loading = true; + try { await fetchJson(`/api/lotteries/${item.id}/cancel`, { method: 'POST' }); await loadLotteries(); } + catch (e: any) { showToast(`取消抽奖失败:${e.message}`); } + finally { item._loading = false; } } async function drawLottery(item: LotteryItem) { + if (item._loading) return; if (!await askConfirm('立即开奖', '确认现在开奖?开奖后状态会变更。', { confirmText: '立即开奖' })) return; - await fetchJson(`/api/lotteries/${item.id}/draw`, { method: 'POST' }); - await loadLotteries(); + item._loading = true; + try { await fetchJson(`/api/lotteries/${item.id}/draw`, { method: 'POST' }); await loadLotteries(); } + catch (e: any) { showToast(`开奖失败:${e.message}`); } + finally { item._loading = false; } } function reloadByGroup() { loadChatHistory(); loadBans(); loadStats(); loadShopItems(); loadShopTransactions(); loadLotteries(); } @@ -367,7 +395,7 @@ onBeforeUnmount(() => {