新增:详情页面交易功能

This commit is contained in:
ivan 2026-02-08 14:02:25 +08:00
parent 6f633178de
commit 3124ef1795
3 changed files with 323 additions and 41 deletions

View File

@ -15,10 +15,6 @@
<v-tab value="up" class="trade-tab">Trade Up</v-tab> <v-tab value="up" class="trade-tab">Trade Up</v-tab>
<v-tab value="down" class="trade-tab">Trade Down</v-tab> <v-tab value="down" class="trade-tab">Trade Down</v-tab>
</v-tabs> </v-tabs>
<div class="maker-rebate">
<span class="maker-rebate-text">Maker Rebate</span>
<v-icon size="14">mdi-refresh</v-icon>
</div>
</div> </div>
<!-- Order Book Content --> <!-- Order Book Content -->
@ -270,19 +266,6 @@ const maxBidsTotal = computed(() => {
border: none; border: none;
} }
.maker-rebate {
margin-left: auto;
display: flex;
align-items: center;
gap: 4px;
}
.maker-rebate-text {
font-size: 14px;
color: #ff9800;
font-weight: 500;
}
.order-book-content { .order-book-content {
display: flex; display: flex;
padding: 12px 16px; padding: 12px 16px;

View File

@ -1,5 +1,6 @@
<template> <template>
<v-card class="trade-component"> <!-- 桌面端完整交易卡片 -->
<v-card v-if="!mobile" class="trade-component">
<!-- Header --> <!-- Header -->
<div class="header"> <div class="header">
<v-tabs v-model="activeTab" class="buy-sell-tabs minimal-tabs" density="compact"> <v-tabs v-model="activeTab" class="buy-sell-tabs minimal-tabs" density="compact">
@ -21,24 +22,12 @@
<v-list-item-title>Limit</v-list-item-title> <v-list-item-title>Limit</v-list-item-title>
</v-list-item> </v-list-item>
<v-divider></v-divider> <v-divider></v-divider>
<v-menu append :location="'end'"> <v-list-item>
<template v-slot:activator="{ props }"> <v-list-item-title>Merge</v-list-item-title>
<v-list-item v-bind="props" @click.stop> </v-list-item>
<v-list-item-title>More</v-list-item-title> <v-list-item>
<template v-slot:append> <v-list-item-title>Split</v-list-item-title>
<v-icon icon="mdi-chevron-right" size="x-small"></v-icon> </v-list-item>
</template>
</v-list-item>
</template>
<v-list>
<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>
</v-list> </v-list>
</v-menu> </v-menu>
</div> </div>
@ -96,7 +85,7 @@
</div> </div>
<!-- Action Button --> <!-- Action Button -->
<v-btn class="action-btn"> <v-btn class="action-btn" @click="submitOrder">
{{ actionButtonText }} {{ actionButtonText }}
</v-btn> </v-btn>
</template> </template>
@ -279,15 +268,180 @@
</div> </div>
<!-- Action Button --> <!-- Action Button -->
<v-btn class="action-btn"> <v-btn class="action-btn" @click="submitOrder">
{{ actionButtonText }} {{ actionButtonText }}
</v-btn> </v-btn>
</template> </template>
</v-card> </v-card>
<!-- 移动端底部紧凑栏 + 底部弹出层 + 三点菜单 -->
<template v-else>
<div class="mobile-trade-bar-spacer" aria-hidden="true"></div>
<div class="mobile-trade-bar">
<v-btn
class="mobile-bar-btn mobile-bar-yes"
variant="flat"
color="success"
rounded="pill"
block
@click="openSheet('yes')"
>
Buy Yes {{ yesPriceCents }}¢
</v-btn>
<v-btn
class="mobile-bar-btn mobile-bar-no"
variant="flat"
color="error"
rounded="pill"
block
@click="openSheet('no')"
>
Buy No {{ noPriceCents }}¢
</v-btn>
</div>
<v-bottom-sheet v-model="sheetOpen" class="trade-sheet">
<v-sheet class="trade-sheet-paper" 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>
</v-bottom-sheet>
</template>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useDisplay } from 'vuetify'
const { mobile } = useDisplay()
//
const sheetOpen = ref(false)
const yesPriceCents = computed(() => 19)
const noPriceCents = computed(() => 82)
function openSheet(option: 'yes' | 'no') {
handleOptionChange(option)
sheetOpen.value = true
}
// State // State
const activeTab = ref('buy') const activeTab = ref('buy')
@ -305,7 +459,17 @@ const amount = ref(0) // Market mode amount
const balance = ref(0) // Market mode balance const balance = ref(0) // Market mode balance
// Emits // Emits
const emit = defineEmits(['optionChange']) const emit = defineEmits<{
optionChange: [option: 'yes' | 'no']
submit: [payload: {
side: 'buy' | 'sell'
option: 'yes' | 'no'
limitPrice: number
shares: number
expirationEnabled: boolean
expirationTime: string
}]
}>()
// Computed properties // Computed properties
const currentPrice = computed(() => { const currentPrice = computed(() => {
@ -367,6 +531,18 @@ const deposit = () => {
console.log('Depositing amount:', amount.value) console.log('Depositing amount:', amount.value)
// API // API
} }
// Set expiration
function submitOrder() {
emit('submit', {
side: activeTab.value as 'buy' | 'sell',
option: selectedOption.value,
limitPrice: limitPrice.value,
shares: shares.value,
expirationEnabled: expirationEnabled.value,
expirationTime: expirationTime.value,
})
}
</script> </script>
<style scoped> <style scoped>
@ -695,6 +871,68 @@ const deposit = () => {
text-transform: none; text-transform: none;
} }
/* 移动端底部交易栏(红框样式) */
.mobile-trade-bar-spacer {
height: 72px;
}
.mobile-trade-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
display: flex;
align-items: stretch;
gap: 8px;
width: 100%;
padding: 12px 16px;
padding-bottom: max(12px, env(safe-area-inset-bottom));
background: #fff;
border-top: 1px solid #eee;
}
.mobile-bar-btn {
border: none;
border-radius: 9999px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
flex-shrink: 0;
}
.mobile-bar-yes {
flex: 1;
min-width: 0;
background: #22c55e;
color: #fff;
padding: 14px 16px;
}
.mobile-bar-no {
flex: 1;
min-width: 0;
background: #ef4444;
color: #fff;
padding: 14px 16px;
}
.trade-sheet-paper {
padding: 0;
max-height: 85vh;
overflow-y: auto;
}
.trade-sheet-inner {
box-shadow: none;
max-width: 100%;
}
/* 手机端底部弹层内不显示Limit 下拉、Yes/No 按钮 */
.trade-sheet-inner .hide-in-mobile-sheet {
display: none !important;
}
/* 响应式调整 - 小屏幕设备 */ /* 响应式调整 - 小屏幕设备 */
@media (max-width: 600px) { @media (max-width: 600px) {
.header { .header {

View File

@ -1,7 +1,8 @@
<template> <template>
<v-container class="trade-detail-container"> <v-container class="trade-detail-container">
<v-row> <v-row align="stretch" class="trade-detail-row">
<v-col cols="12"> <!-- 左侧分时图 + 订单簿宽度弹性 -->
<v-col cols="12" class="chart-col">
<!-- 分时图卡片Polymarket 样式 --> <!-- 分时图卡片Polymarket 样式 -->
<v-card class="chart-card polymarket-chart" elevation="0" rounded="lg"> <v-card class="chart-card polymarket-chart" elevation="0" rounded="lg">
<!-- 顶部标题当前概率Past / 日期 --> <!-- 顶部标题当前概率Past / 日期 -->
@ -45,6 +46,13 @@
<OrderBook /> <OrderBook />
</v-card> </v-card>
</v-col> </v-col>
<!-- 右侧交易组件固定宽度 -->
<v-col cols="12" class="trade-col">
<div class="trade-sidebar">
<TradeComponent />
</div>
</v-col>
</v-row> </v-row>
</v-container> </v-container>
</template> </template>
@ -55,6 +63,7 @@ import { useRoute } from 'vue-router'
import * as echarts from 'echarts' import * as echarts from 'echarts'
import type { ECharts } from 'echarts' import type { ECharts } from 'echarts'
import OrderBook from '../components/OrderBook.vue' import OrderBook from '../components/OrderBook.vue'
import TradeComponent from '../components/TradeComponent.vue'
/** /**
* 分时图服务端推送数据格式约定 * 分时图服务端推送数据格式约定
@ -543,6 +552,58 @@ onUnmounted(() => {
border: 1px solid #e7e7e7; border: 1px solid #e7e7e7;
} }
/* 左右布局:左侧弹性,右侧固定 */
.trade-detail-row {
display: flex;
flex-wrap: wrap;
}
.chart-col {
flex: 1 1 0%;
min-width: 0;
}
.trade-col {
margin-top: 32px;
}
.trade-sidebar {
position: sticky;
top: 24px;
width: 400px;
max-width: 100%;
flex-shrink: 0;
}
@media (min-width: 960px) {
.trade-detail-row {
flex-wrap: nowrap;
}
.chart-col {
flex: 1 1 0% !important;
min-width: 0;
max-width: none !important;
}
.trade-col {
flex: 0 0 400px !important;
max-width: 400px !important;
margin-top: 32px;
}
.trade-sidebar {
width: 400px;
}
}
@media (max-width: 959px) {
.trade-sidebar {
position: static;
width: 100%;
}
}
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 599px) { @media (max-width: 599px) {
.trade-detail-container { .trade-detail-container {