diff --git a/src/App.vue b/src/App.vue index 7bd74e6..7d63f89 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,6 +2,7 @@ import { computed, onMounted, watch, nextTick } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useUserStore } from './stores/user' +import Toast from './components/Toast.vue' const route = useRoute() const router = useRouter() @@ -92,6 +93,7 @@ watch( + diff --git a/src/components/Toast.vue b/src/components/Toast.vue new file mode 100644 index 0000000..ca3648c --- /dev/null +++ b/src/components/Toast.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/src/components/TradeComponent.vue b/src/components/TradeComponent.vue index 6f12be9..364e81b 100644 --- a/src/components/TradeComponent.vue +++ b/src/components/TradeComponent.vue @@ -1461,6 +1461,7 @@ const orderError = ref('') // Emits const emit = defineEmits<{ optionChange: [option: 'yes' | 'no'] + orderSuccess: [] submit: [ payload: { side: 'buy' | 'sell' @@ -1764,6 +1765,7 @@ async function submitOrder() { ) if (res.code === 0 || res.code === 200) { userStore.fetchUsdcBalance() + emit('orderSuccess') } else { orderError.value = res.msg || '下单失败' } diff --git a/src/stores/toast.ts b/src/stores/toast.ts new file mode 100644 index 0000000..e83543b --- /dev/null +++ b/src/stores/toast.ts @@ -0,0 +1,105 @@ +import { ref } from 'vue' +import { defineStore } from 'pinia' + +export type ToastType = 'success' | 'error' | 'info' | 'warning' + +export interface ToastItem { + id: string + message: string + type: ToastType + count: number + addedAt: number +} + +const DEDUP_MS = 3000 +const MAX_VISIBLE = 3 +let idCounter = 0 + +function genId() { + return `toast-${++idCounter}` +} + +function normalizeMsg(msg: string) { + return msg.trim().replace(/\s+/g, ' ') +} + +export const useToastStore = defineStore('toast', () => { + /** 当前正在显示的 Toast(最多 MAX_VISIBLE 条) */ + const displaying = ref([]) + /** 待显示的队列 */ + const queue = ref([]) + + /** 每种 message+type 最后一次出现的时间(添加或合并时更新) */ + const lastSeenAt = ref>({}) + + function mergeKey(msg: string, type: ToastType) { + return `${type}:${normalizeMsg(msg)}` + } + + /** 尝试合并:同 message+type 且在 DEDUP_MS 内(以最后一次为准)合并为 count+1 */ + function tryMerge(item: ToastItem): boolean { + const now = Date.now() + const key = mergeKey(item.message, item.type) + const last = lastSeenAt.value[key] ?? 0 + if (now - last >= DEDUP_MS) return false + + const msg = normalizeMsg(item.message) + const inDisplayingIdx = displaying.value.findLastIndex( + (d) => normalizeMsg(d.message) === msg && d.type === item.type + ) + if (inDisplayingIdx >= 0) { + displaying.value = displaying.value.map((x, i) => + i === inDisplayingIdx + ? { ...x, count: x.count + 1, addedAt: now } + : x + ) + lastSeenAt.value[key] = now + return true + } + const inQueueReversed = [...queue.value].reverse() + const inQueueIdx = inQueueReversed.findIndex( + (q) => normalizeMsg(q.message) === msg && q.type === item.type + ) + if (inQueueIdx >= 0) { + const origIdx = queue.value.length - 1 - inQueueIdx + queue.value = queue.value.map((x, i) => + i === origIdx ? { ...x, count: x.count + 1, addedAt: now } : x + ) + lastSeenAt.value[key] = now + return true + } + return false + } + + function show(msg: string, toastType: ToastType = 'success') { + const item: ToastItem = { + id: genId(), + message: msg.trim(), + type: toastType, + count: 1, + addedAt: Date.now(), + } + if (tryMerge(item)) return + + const key = mergeKey(item.message, item.type) + lastSeenAt.value[key] = item.addedAt + + if (displaying.value.length < MAX_VISIBLE) { + displaying.value = [...displaying.value, item] + } else { + queue.value = [...queue.value, item] + } + } + + /** 某条 Toast 关闭后调用 */ + function remove(id: string) { + displaying.value = displaying.value.filter((d) => d.id !== id) + if (queue.value.length > 0) { + const next = queue.value[0] + queue.value = queue.value.slice(1) + displaying.value = [...displaying.value, next] + } + } + + return { displaying, queue, show, remove } +}) diff --git a/src/views/EventMarkets.vue b/src/views/EventMarkets.vue index 6e5f1dc..eea35f3 100644 --- a/src/views/EventMarkets.vue +++ b/src/views/EventMarkets.vue @@ -174,6 +174,7 @@ :initial-option="tradeInitialOption" embedded-in-sheet @submit="onTradeSubmit" + @order-success="onOrderSuccess" /> @@ -198,6 +199,7 @@ import { } from '../api/event' import { MOCK_EVENT_LIST } from '../api/mockEventList' import { useUserStore } from '../stores/user' +import { useToastStore } from '../stores/toast' const route = useRoute() const router = useRouter() @@ -559,6 +561,12 @@ function onTradeSubmit(payload: { console.log('Trade submit', payload) } +const toastStore = useToastStore() +function onOrderSuccess() { + tradeSheetOpen.value = false + toastStore.show('下单成功') +} + function marketChance(market: PmEventMarketItem): number { const raw = market?.outcomePrices?.[0] if (raw == null) return 0 diff --git a/src/views/Home.vue b/src/views/Home.vue index 42dd9a6..3b43ac9 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -207,6 +207,7 @@ :key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`" :market="homeTradeMarketPayload" :initial-option="tradeDialogSide" + @order-success="onOrderSuccess" /> @@ -215,6 +216,7 @@ :market="homeTradeMarketPayload" :initial-option="tradeDialogSide" embedded-in-sheet + @order-success="onOrderSuccess" /> @@ -321,6 +323,7 @@ import { type CategoryTreeNode, } from '../api/category' import { useSearchHistory } from '../composables/useSearchHistory' +import { useToastStore } from '../stores/toast' const { mobile } = useDisplay() const searchHistory = useSearchHistory() @@ -520,6 +523,12 @@ function onCardOpenTrade( tradeDialogOpen.value = true } +const toastStore = useToastStore() +function onOrderSuccess() { + tradeDialogOpen.value = false + toastStore.show('下单成功') +} + /** 传给 TradeComponent 的 market(Home 弹窗/底部栏),供 Split、下单等使用 */ const homeTradeMarketPayload = computed(() => { const m = tradeDialogMarket.value diff --git a/src/views/TradeDetail.vue b/src/views/TradeDetail.vue index 20665db..52c719e 100644 --- a/src/views/TradeDetail.vue +++ b/src/views/TradeDetail.vue @@ -181,6 +181,7 @@ :market="tradeMarketPayload" :initial-option="tradeInitialOptionFromBar" embedded-in-sheet + @order-success="onOrderSuccess" /> @@ -205,6 +206,7 @@ import { } from '../api/event' import { getClobWsUrl } from '../api/request' import { useUserStore } from '../stores/user' +import { useToastStore } from '../stores/toast' import { ClobSdk, type PriceSizePolyMsg, @@ -526,6 +528,12 @@ function openSplitFromBar() { }) } +const toastStore = useToastStore() +function onOrderSuccess() { + tradeSheetOpen.value = false + toastStore.show('下单成功') +} + // Comments / Top Holders / Activity const detailTab = ref('activity') const activityMinAmount = ref('0')