新增:点击卡片的YES/NO弹出交易框

This commit is contained in:
ivan 2026-02-08 17:19:18 +08:00
parent 14c4ec322f
commit 642f7990dc
3 changed files with 234 additions and 10 deletions

View File

@ -31,12 +31,24 @@
</div>
</div>
<!-- Options Section -->
<!-- Options Section点击 Yes/No 弹出交易框阻止冒泡不触发卡片跳转 -->
<div class="options-section">
<v-btn class="option-yes" :color="'#e6f9e6'" :rounded="'sm'" :text="true">
<v-btn
class="option-yes"
:color="'#e6f9e6'"
:rounded="'sm'"
:text="true"
@click.stop="openTrade('yes')"
>
<span class="option-text-yes">Yes</span>
</v-btn>
<v-btn class="option-no" :color="'#ffe6e6'" :rounded="'sm'" :text="true">
<v-btn
class="option-no"
:color="'#ffe6e6'"
:rounded="'sm'"
:text="true"
@click.stop="openTrade('no')"
>
<span class="option-text-no">No</span>
</v-btn>
</div>
@ -58,6 +70,9 @@ import { computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const emit = defineEmits<{
openTrade: [side: 'yes' | 'no', market?: { id: string; title: string }]
}>()
const props = defineProps({
marketTitle: {
@ -114,6 +129,11 @@ const navigateToDetail = () => {
},
})
}
// Yes/No openTrade
function openTrade(side: 'yes' | 'no') {
emit('openTrade', side, { id: props.id, title: props.marketTitle })
}
</script>
<style scoped>

View File

@ -274,7 +274,132 @@
</template>
</v-card>
<!-- 移动端底部紧凑栏 + 底部弹出层 + 三点菜单 -->
<!-- 移动端且由首页卡片嵌入只渲染交易表单无底部栏无内部 sheet -->
<template v-else-if="embeddedInSheet">
<v-sheet class="trade-sheet-paper trade-sheet-paper--embedded" rounded="lg">
<div class="trade-component trade-sheet-inner">
<div class="header">
<v-tabs v-model="activeTab" class="buy-sell-tabs minimal-tabs" density="compact">
<v-tab value="buy" class="minimal-tab">Buy</v-tab>
<v-tab value="sell" class="minimal-tab">Sell</v-tab>
</v-tabs>
<v-menu class="limit-dropdown hide-in-mobile-sheet">
<template v-slot:activator="{ props: limitProps, isActive }">
<v-btn v-bind="limitProps" class="limit-btn" text end>
{{ limitType }}
<v-icon right>{{ isActive ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item @click="limitType = 'Market'"><v-list-item-title>Market</v-list-item-title></v-list-item>
<v-list-item @click="limitType = 'Limit'"><v-list-item-title>Limit</v-list-item-title></v-list-item>
<v-divider></v-divider>
<v-list-item><v-list-item-title>Merge</v-list-item-title></v-list-item>
<v-list-item><v-list-item-title>Split</v-list-item-title></v-list-item>
</v-list>
</v-menu>
</div>
<template v-if="isMarketMode">
<template v-if="balance > 0">
<div class="price-options hide-in-mobile-sheet">
<v-btn class="yes-btn" :class="{ active: selectedOption === 'yes' }" text @click="handleOptionChange('yes')">Yes {{ selectedOption === 'yes' ? '19¢' : '18¢' }}</v-btn>
<v-btn class="no-btn" :class="{ active: selectedOption === 'no' }" text @click="handleOptionChange('no')">No {{ selectedOption === 'no' ? '82¢' : '81¢' }}</v-btn>
</div>
<div class="total-section">
<template v-if="activeTab === 'buy'">
<div class="total-row"><span class="label">Total</span><span class="total-value">${{ totalPrice }}</span></div>
<div class="total-row"><span class="label">To win</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> $20</span></div>
</template>
<template v-else>
<div class="total-row"><span class="label">You'll receive</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{ totalPrice }}</span></div>
</template>
</div>
<v-btn class="action-btn" @click="submitOrder">{{ actionButtonText }}</v-btn>
</template>
<template v-else>
<div class="price-options hide-in-mobile-sheet">
<v-btn class="yes-btn" :class="{ active: selectedOption === 'yes' }" text @click="handleOptionChange('yes')">Yes {{ selectedOption === 'yes' ? '19¢' : '18¢' }}</v-btn>
<v-btn class="no-btn" :class="{ active: selectedOption === 'no' }" text @click="handleOptionChange('no')">No {{ selectedOption === 'no' ? '82¢' : '81¢' }}</v-btn>
</div>
<div class="input-group">
<div class="amount-header">
<div><span class="label amount-label">Amount</span><span class="balance-label">Balance ${{ balance.toFixed(2) }}</span></div>
<div class="amount-value">${{ amount.toFixed(2) }}</div>
</div>
<div class="amount-buttons">
<v-btn class="amount-btn" @click="adjustAmount(1)">+$1</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(20)">+$20</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn>
</div>
</div>
<v-btn class="deposit-btn" @click="deposit">Deposit</v-btn>
</template>
</template>
<template v-else>
<div class="price-options hide-in-mobile-sheet">
<v-btn class="yes-btn" :class="{ active: selectedOption === 'yes' }" text @click="handleOptionChange('yes')">Yes {{ selectedOption === 'yes' ? '19¢' : '18¢' }}</v-btn>
<v-btn class="no-btn" :class="{ active: selectedOption === 'no' }" text @click="handleOptionChange('no')">No {{ selectedOption === 'no' ? '82¢' : '81¢' }}</v-btn>
</div>
<div class="input-group limit-price-group">
<div class="limit-price-header">
<span class="label">Limit Price</span>
<div class="price-input">
<v-btn class="adjust-btn" icon @click="decreasePrice"><v-icon>mdi-minus</v-icon></v-btn>
<v-text-field v-model.number="limitPrice" type="number" min="0.01" step="0.01" class="price-input-field" hide-details density="compact"></v-text-field>
<v-btn class="adjust-btn" icon @click="increasePrice"><v-icon>mdi-plus</v-icon></v-btn>
</div>
</div>
</div>
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<div class="shares-input">
<v-text-field v-model.number="shares" type="number" min="0" class="shares-input-field" hide-details density="compact"></v-text-field>
</div>
</div>
<div v-if="activeTab === 'buy'" class="shares-buttons">
<v-btn class="share-btn" @click="adjustShares(-100)">-100</v-btn>
<v-btn class="share-btn" @click="adjustShares(-10)">-10</v-btn>
<v-btn class="share-btn" @click="adjustShares(10)">+10</v-btn>
<v-btn class="share-btn" @click="adjustShares(100)">+100</v-btn>
</div>
<div v-else class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
</div>
</div>
<div class="input-group expiration-group">
<div class="expiration-header">
<span class="label">Set expiration</span>
<v-switch v-model="expirationEnabled" class="expiration-switch" hide-details color="primary"></v-switch>
</div>
<v-select
v-if="expirationEnabled"
v-model="expirationTime"
:items="expirationOptions"
class="expiration-select"
hide-details
density="compact"
></v-select>
</div>
<div class="total-section">
<template v-if="activeTab === 'buy'">
<div class="total-row"><span class="label">Total</span><span class="total-value">${{ totalPrice }}</span></div>
<div class="total-row"><span class="label">To win</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> $20</span></div>
</template>
<template v-else>
<div class="total-row"><span class="label">You'll receive</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{ totalPrice }}</span></div>
</template>
</div>
<v-btn class="action-btn" @click="submitOrder">{{ actionButtonText }}</v-btn>
</template>
</div>
</v-sheet>
</template>
<!-- 移动端底部紧凑栏 + 底部弹出层 -->
<template v-else>
<div class="mobile-trade-bar-spacer" aria-hidden="true"></div>
<div class="mobile-trade-bar">
@ -427,11 +552,16 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, watch, onMounted } from 'vue'
import { useDisplay } from 'vuetify'
const { mobile } = useDisplay()
const props = withDefaults(
defineProps<{ initialOption?: 'yes' | 'no'; embeddedInSheet?: boolean }>(),
{ initialOption: undefined, embeddedInSheet: false }
)
//
const sheetOpen = ref(false)
@ -447,7 +577,7 @@ function openSheet(option: 'yes' | 'no') {
const activeTab = ref('buy')
const limitType = ref('Limit')
const expirationEnabled = ref(false)
const selectedOption = ref('no') // 'yes' or 'no'
const selectedOption = ref<'yes' | 'no'>(props.initialOption ?? 'no')
const limitPrice = ref(0.82) //
const shares = ref(20) //
const expirationTime = ref('5m') //
@ -484,6 +614,22 @@ const actionButtonText = computed(() => {
return `${activeTab.value} ${selectedOption.value.charAt(0).toUpperCase() + selectedOption.value.slice(1)}`
})
function applyInitialOption(option: 'yes' | 'no') {
selectedOption.value = option
if (option === 'yes') {
limitPrice.value = 0.19
} else {
limitPrice.value = 0.82
}
}
onMounted(() => {
if (props.initialOption) applyInitialOption(props.initialOption)
})
watch(() => props.initialOption, (option) => {
if (option) applyInitialOption(option)
}, { immediate: true })
// Methods
const handleOptionChange = (option: 'yes' | 'no') => {
selectedOption.value = option

View File

@ -13,7 +13,12 @@
<v-pull-to-refresh class="pull-to-refresh" @load="onRefresh">
<div class="pull-to-refresh-inner">
<div class="home-list">
<MarketCard v-for="id in listLength" :key="id" :id="String(id)" />
<MarketCard
v-for="id in listLength"
:key="id"
:id="String(id)"
@open-trade="onCardOpenTrade"
/>
</div>
<div class="load-more-footer">
<div ref="sentinelRef" class="load-more-sentinel" aria-hidden="true" />
@ -36,6 +41,33 @@
</div>
</v-pull-to-refresh>
</div>
<!-- PC对话框手机底部 sheet直接显示交易表单 -->
<v-dialog
v-if="!isMobile"
v-model="tradeDialogOpen"
max-width="420"
scrollable
content-class="trade-dialog trade-dialog--bare"
transition="dialog-transition"
@click:outside="tradeDialogOpen = false"
>
<TradeComponent
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
:initial-option="tradeDialogSide"
/>
</v-dialog>
<v-bottom-sheet
v-else
v-model="tradeDialogOpen"
content-class="trade-bottom-sheet"
>
<TradeComponent
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
:initial-option="tradeDialogSide"
embedded-in-sheet
/>
</v-bottom-sheet>
</v-container>
<footer class="home-footer">
@ -114,8 +146,13 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue'
import { useDisplay } from 'vuetify'
import MarketCard from '../components/MarketCard.vue'
import TradeComponent from '../components/TradeComponent.vue'
const { mobile } = useDisplay()
const isMobile = computed(() => mobile.value)
const activeTab = ref('overview')
@ -126,7 +163,16 @@ const maxItems = 50
const listLength = ref(INITIAL_COUNT)
const loadingMore = ref(false)
const footerLang = ref('English')
const tradeDialogOpen = ref(false)
const tradeDialogSide = ref<'yes' | 'no'>('yes')
const tradeDialogMarket = ref<{ id: string; title: string } | null>(null)
const scrollRef = ref<HTMLElement | null>(null)
function onCardOpenTrade(side: 'yes' | 'no', market?: { id: string; title: string }) {
tradeDialogSide.value = side
tradeDialogMarket.value = market ?? null
tradeDialogOpen.value = true
}
const sentinelRef = ref<HTMLElement | null>(null)
let observer: IntersectionObserver | null = null
@ -269,6 +315,18 @@ onUnmounted(() => {
margin-top: 4px;
}
.trade-dialog--bare :deep(.v-overlay__content) {
padding: 0;
overflow: visible;
}
.trade-dialog--bare :deep(.v-card) {
box-shadow: none;
}
.trade-bottom-sheet :deep(.v-overlay__content) {
padding: 0;
}
/* 大屏最多 4 列,避免过宽时出现 5 列以上 */
@media (min-width: 1320px) {
.home-list {