新增:购买成功后的提示
This commit is contained in:
parent
84ed7de42c
commit
359059bff6
@ -2,6 +2,7 @@
|
|||||||
import { computed, onMounted, watch, nextTick } from 'vue'
|
import { computed, onMounted, watch, nextTick } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useUserStore } from './stores/user'
|
import { useUserStore } from './stores/user'
|
||||||
|
import Toast from './components/Toast.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -92,6 +93,7 @@ watch(
|
|||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
</v-main>
|
</v-main>
|
||||||
|
<Toast />
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
143
src/components/Toast.vue
Normal file
143
src/components/Toast.vue
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div class="toast-container">
|
||||||
|
<TransitionGroup name="toast" tag="div" class="toast-stack">
|
||||||
|
<div
|
||||||
|
v-for="item in toastStore.displaying"
|
||||||
|
:key="item.id"
|
||||||
|
:class="['toast-item', `toast-item--${item.type}`]"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<span class="toast-message">
|
||||||
|
{{ item.count > 1 ? `${item.message} (×${item.count})` : item.message }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useToastStore } from '@/stores/toast'
|
||||||
|
import type { ToastItem } from '@/stores/toast'
|
||||||
|
|
||||||
|
const toastStore = useToastStore()
|
||||||
|
const TIMEOUT_MS = 3000
|
||||||
|
const timers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
|
function scheduleRemove(item: ToastItem) {
|
||||||
|
const t = setTimeout(() => {
|
||||||
|
toastStore.remove(item.id)
|
||||||
|
timers.delete(item.id)
|
||||||
|
}, TIMEOUT_MS)
|
||||||
|
timers.set(item.id, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncTimers() {
|
||||||
|
for (const item of toastStore.displaying) {
|
||||||
|
const existing = timers.get(item.id)
|
||||||
|
if (existing) clearTimeout(existing)
|
||||||
|
scheduleRemove(item)
|
||||||
|
}
|
||||||
|
for (const [id] of timers) {
|
||||||
|
if (!toastStore.displaying.some((d) => d.id === id)) {
|
||||||
|
clearTimeout(timers.get(id)!)
|
||||||
|
timers.delete(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
syncTimers()
|
||||||
|
const stop = toastStore.$subscribe(syncTimers)
|
||||||
|
onUnmounted(() => {
|
||||||
|
stop()
|
||||||
|
timers.forEach((t) => clearTimeout(t))
|
||||||
|
timers.clear()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
pointer-events: none;
|
||||||
|
/* 避开顶部导航栏(约 64px)+ 安全区域 */
|
||||||
|
padding: max(72px, calc(64px + env(safe-area-inset-top))) 16px 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: min(520px, calc(100vw - 32px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-item {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 28px;
|
||||||
|
border-radius: 8px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-item--success {
|
||||||
|
background: rgba(76, 175, 80, 0.65);
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-item--error {
|
||||||
|
background: rgba(244, 67, 54, 0.65);
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-item--warning {
|
||||||
|
background: rgba(255, 152, 0, 0.65);
|
||||||
|
color: rgba(33, 33, 33, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-item--info {
|
||||||
|
background: rgba(33, 150, 243, 0.65);
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-message {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transition */
|
||||||
|
.toast-enter-active,
|
||||||
|
.toast-leave-active {
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下方 Toast 在上方消失时平缓上移 */
|
||||||
|
.toast-move {
|
||||||
|
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1461,6 +1461,7 @@ const orderError = ref('')
|
|||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
optionChange: [option: 'yes' | 'no']
|
optionChange: [option: 'yes' | 'no']
|
||||||
|
orderSuccess: []
|
||||||
submit: [
|
submit: [
|
||||||
payload: {
|
payload: {
|
||||||
side: 'buy' | 'sell'
|
side: 'buy' | 'sell'
|
||||||
@ -1764,6 +1765,7 @@ async function submitOrder() {
|
|||||||
)
|
)
|
||||||
if (res.code === 0 || res.code === 200) {
|
if (res.code === 0 || res.code === 200) {
|
||||||
userStore.fetchUsdcBalance()
|
userStore.fetchUsdcBalance()
|
||||||
|
emit('orderSuccess')
|
||||||
} else {
|
} else {
|
||||||
orderError.value = res.msg || '下单失败'
|
orderError.value = res.msg || '下单失败'
|
||||||
}
|
}
|
||||||
|
|||||||
105
src/stores/toast.ts
Normal file
105
src/stores/toast.ts
Normal file
@ -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<ToastItem[]>([])
|
||||||
|
/** 待显示的队列 */
|
||||||
|
const queue = ref<ToastItem[]>([])
|
||||||
|
|
||||||
|
/** 每种 message+type 最后一次出现的时间(添加或合并时更新) */
|
||||||
|
const lastSeenAt = ref<Record<string, number>>({})
|
||||||
|
|
||||||
|
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 }
|
||||||
|
})
|
||||||
@ -174,6 +174,7 @@
|
|||||||
:initial-option="tradeInitialOption"
|
:initial-option="tradeInitialOption"
|
||||||
embedded-in-sheet
|
embedded-in-sheet
|
||||||
@submit="onTradeSubmit"
|
@submit="onTradeSubmit"
|
||||||
|
@order-success="onOrderSuccess"
|
||||||
/>
|
/>
|
||||||
</v-bottom-sheet>
|
</v-bottom-sheet>
|
||||||
</template>
|
</template>
|
||||||
@ -198,6 +199,7 @@ import {
|
|||||||
} from '../api/event'
|
} from '../api/event'
|
||||||
import { MOCK_EVENT_LIST } from '../api/mockEventList'
|
import { MOCK_EVENT_LIST } from '../api/mockEventList'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
|
import { useToastStore } from '../stores/toast'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -559,6 +561,12 @@ function onTradeSubmit(payload: {
|
|||||||
console.log('Trade submit', payload)
|
console.log('Trade submit', payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toastStore = useToastStore()
|
||||||
|
function onOrderSuccess() {
|
||||||
|
tradeSheetOpen.value = false
|
||||||
|
toastStore.show('下单成功')
|
||||||
|
}
|
||||||
|
|
||||||
function marketChance(market: PmEventMarketItem): number {
|
function marketChance(market: PmEventMarketItem): number {
|
||||||
const raw = market?.outcomePrices?.[0]
|
const raw = market?.outcomePrices?.[0]
|
||||||
if (raw == null) return 0
|
if (raw == null) return 0
|
||||||
|
|||||||
@ -207,6 +207,7 @@
|
|||||||
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
|
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
|
||||||
:market="homeTradeMarketPayload"
|
:market="homeTradeMarketPayload"
|
||||||
:initial-option="tradeDialogSide"
|
:initial-option="tradeDialogSide"
|
||||||
|
@order-success="onOrderSuccess"
|
||||||
/>
|
/>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
<v-bottom-sheet v-else v-model="tradeDialogOpen" content-class="trade-bottom-sheet">
|
<v-bottom-sheet v-else v-model="tradeDialogOpen" content-class="trade-bottom-sheet">
|
||||||
@ -215,6 +216,7 @@
|
|||||||
:market="homeTradeMarketPayload"
|
:market="homeTradeMarketPayload"
|
||||||
:initial-option="tradeDialogSide"
|
:initial-option="tradeDialogSide"
|
||||||
embedded-in-sheet
|
embedded-in-sheet
|
||||||
|
@order-success="onOrderSuccess"
|
||||||
/>
|
/>
|
||||||
</v-bottom-sheet>
|
</v-bottom-sheet>
|
||||||
</v-container>
|
</v-container>
|
||||||
@ -321,6 +323,7 @@ import {
|
|||||||
type CategoryTreeNode,
|
type CategoryTreeNode,
|
||||||
} from '../api/category'
|
} from '../api/category'
|
||||||
import { useSearchHistory } from '../composables/useSearchHistory'
|
import { useSearchHistory } from '../composables/useSearchHistory'
|
||||||
|
import { useToastStore } from '../stores/toast'
|
||||||
|
|
||||||
const { mobile } = useDisplay()
|
const { mobile } = useDisplay()
|
||||||
const searchHistory = useSearchHistory()
|
const searchHistory = useSearchHistory()
|
||||||
@ -520,6 +523,12 @@ function onCardOpenTrade(
|
|||||||
tradeDialogOpen.value = true
|
tradeDialogOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toastStore = useToastStore()
|
||||||
|
function onOrderSuccess() {
|
||||||
|
tradeDialogOpen.value = false
|
||||||
|
toastStore.show('下单成功')
|
||||||
|
}
|
||||||
|
|
||||||
/** 传给 TradeComponent 的 market(Home 弹窗/底部栏),供 Split、下单等使用 */
|
/** 传给 TradeComponent 的 market(Home 弹窗/底部栏),供 Split、下单等使用 */
|
||||||
const homeTradeMarketPayload = computed(() => {
|
const homeTradeMarketPayload = computed(() => {
|
||||||
const m = tradeDialogMarket.value
|
const m = tradeDialogMarket.value
|
||||||
|
|||||||
@ -181,6 +181,7 @@
|
|||||||
:market="tradeMarketPayload"
|
:market="tradeMarketPayload"
|
||||||
:initial-option="tradeInitialOptionFromBar"
|
:initial-option="tradeInitialOptionFromBar"
|
||||||
embedded-in-sheet
|
embedded-in-sheet
|
||||||
|
@order-success="onOrderSuccess"
|
||||||
/>
|
/>
|
||||||
</v-bottom-sheet>
|
</v-bottom-sheet>
|
||||||
</template>
|
</template>
|
||||||
@ -205,6 +206,7 @@ import {
|
|||||||
} from '../api/event'
|
} from '../api/event'
|
||||||
import { getClobWsUrl } from '../api/request'
|
import { getClobWsUrl } from '../api/request'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
|
import { useToastStore } from '../stores/toast'
|
||||||
import {
|
import {
|
||||||
ClobSdk,
|
ClobSdk,
|
||||||
type PriceSizePolyMsg,
|
type PriceSizePolyMsg,
|
||||||
@ -526,6 +528,12 @@ function openSplitFromBar() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toastStore = useToastStore()
|
||||||
|
function onOrderSuccess() {
|
||||||
|
tradeSheetOpen.value = false
|
||||||
|
toastStore.show('下单成功')
|
||||||
|
}
|
||||||
|
|
||||||
// Comments / Top Holders / Activity
|
// Comments / Top Holders / Activity
|
||||||
const detailTab = ref('activity')
|
const detailTab = ref('activity')
|
||||||
const activityMinAmount = ref<string>('0')
|
const activityMinAmount = ref<string>('0')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user