xtraderClient/src/components/TradeComponent.vue

1503 lines
44 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 桌面端完整交易卡片 -->
<v-card v-if="!mobile" class="trade-component">
<!-- Header -->
<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>
<template v-slot:activator="{ props, isActive }">
<v-btn v-bind="props" 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 @click="openMergeDialog">
<v-list-item-title>Merge</v-list-item-title>
</v-list-item>
<v-list-item @click="openSplitDialog">
<v-list-item-title>Split</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
<!-- Market Mode View -->
<template v-if="isMarketMode">
<!-- Balance > 0: Show Trade Interface -->
<template v-if="balance > 0">
<!-- Price Options -->
<div class="price-options">
<v-btn
class="yes-btn"
:class="{ active: selectedOption === 'yes' }"
text
@click="handleOptionChange('yes')"
>
Yes {{ yesPriceCents }}¢
</v-btn>
<v-btn
class="no-btn"
:class="{ active: selectedOption === 'no' }"
text
@click="handleOptionChange('no')"
>
No {{ noPriceCents }}¢
</v-btn>
</div>
<!-- Total and To Win (Buy模式) You'll receive (Sell模式) -->
<div class="total-section">
<!-- Buy模式 -->
<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>
<!-- Sell模式 -->
<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>
<!-- Action Button -->
<v-btn class="action-btn" @click="submitOrder">
{{ actionButtonText }}
</v-btn>
</template>
<!-- Balance <= 0: Show Deposit Interface -->
<template v-else>
<!-- Price Options -->
<div class="price-options">
<v-btn
class="yes-btn"
:class="{ active: selectedOption === 'yes' }"
text
@click="handleOptionChange('yes')"
>
Yes {{ yesPriceCents }}¢
</v-btn>
<v-btn
class="no-btn"
:class="{ active: selectedOption === 'no' }"
text
@click="handleOptionChange('no')"
>
No {{ noPriceCents }}¢
</v-btn>
</div>
<!-- Amount Section -->
<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>
<!-- Amount Buttons -->
<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>
<!-- Deposit Button -->
<v-btn
class="deposit-btn"
@click="deposit"
>
Deposit
</v-btn>
</template>
</template>
<!-- Limit Mode View -->
<template v-else>
<!-- Price Options -->
<div class="price-options">
<v-btn
class="yes-btn"
:class="{ active: selectedOption === 'yes' }"
text
@click="handleOptionChange('yes')"
>
Yes {{ yesPriceCents }}¢
</v-btn>
<v-btn
class="no-btn"
:class="{ active: selectedOption === 'no' }"
text
@click="handleOptionChange('no')"
>
No {{ noPriceCents }}¢
</v-btn>
</div>
<!-- Limit Price -->
<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>
<!-- Shares -->
<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>
<!-- Buy模式的份额调整按钮 -->
<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>
<v-btn class="share-btn" @click="adjustShares(200)">+200</v-btn>
</div>
<!-- Sell模式的份额调整按钮 -->
<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 v-if="activeTab === 'buy'" class="matching-info">
<v-icon size="14">mdi-information</v-icon>
<span>20.00 matching</span>
</div>
</div>
<!-- Set Expiration -->
<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></v-switch>
</div>
<!-- Expiration Time Dropdown -->
<v-select
v-if="expirationEnabled"
v-model="expirationTime"
:items="expirationOptions"
class="expiration-select"
hide-details
density="compact"
></v-select>
</div>
<!-- Total and To Win (Buy模式) You'll receive (Sell模式) -->
<div class="total-section">
<!-- Buy模式 -->
<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>
<!-- Sell模式 -->
<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>
<!-- Action Button -->
<v-btn class="action-btn" @click="submitOrder">
{{ actionButtonText }}
</v-btn>
</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 @click="openMergeDialog"><v-list-item-title>Merge</v-list-item-title></v-list-item>
<v-list-item @click="openSplitDialog"><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 {{ yesPriceCents }}¢</v-btn>
<v-btn class="no-btn" :class="{ active: selectedOption === 'no' }" text @click="handleOptionChange('no')">No {{ noPriceCents }}¢</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 {{ yesPriceCents }}¢</v-btn>
<v-btn class="no-btn" :class="{ active: selectedOption === 'no' }" text @click="handleOptionChange('no')">No {{ noPriceCents }}¢</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 {{ yesPriceCents }}¢</v-btn>
<v-btn class="no-btn" :class="{ active: selectedOption === 'no' }" text @click="handleOptionChange('no')">No {{ noPriceCents }}¢</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">
<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 @click="openMergeDialog"><v-list-item-title>Merge</v-list-item-title></v-list-item>
<v-list-item @click="openSplitDialog"><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 {{ yesPriceCents }}¢</v-btn>
<v-btn class="no-btn" :class="{ active: selectedOption === 'no' }" text @click="handleOptionChange('no')">No {{ noPriceCents }}¢</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 {{ yesPriceCents }}¢</v-btn>
<v-btn class="no-btn" :class="{ active: selectedOption === 'no' }" text @click="handleOptionChange('no')">No {{ noPriceCents }}¢</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 {{ yesPriceCents }}¢</v-btn>
<v-btn class="no-btn" :class="{ active: selectedOption === 'no' }" text @click="handleOptionChange('no')">No {{ noPriceCents }}¢</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>
<!-- Merge shares dialog与桌面/移动端分支并列始终挂载才能响应 openMergeDialog -->
<v-dialog v-model="mergeDialogOpen" max-width="440" persistent content-class="merge-dialog" transition="dialog-transition">
<v-card class="merge-dialog-card" rounded="lg">
<div class="merge-dialog-header">
<h3 class="merge-dialog-title">Merge shares</h3>
<v-btn icon variant="text" size="small" class="merge-dialog-close" @click="mergeDialogOpen = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<v-card-text class="merge-dialog-body">
<p class="merge-dialog-desc">
Merge a share of Yes and No to get 1 USDC. You can do this to save cost when trying to get rid of a position.
</p>
<div class="merge-amount-row">
<label class="merge-amount-label">Amount</label>
<v-text-field
v-model.number="mergeAmount"
type="number"
min="0"
hide-details
density="compact"
variant="outlined"
class="merge-amount-input"
/>
</div>
<p class="merge-available">
Available shares: {{ availableMergeShares }}
<button type="button" class="merge-max-link" @click="setMergeMax">Max</button>
</p>
<p v-if="!props.market?.marketId" class="merge-no-market">Please select a market first (e.g. click Buy Yes/No on a market).</p>
<p v-if="mergeError" class="merge-error">{{ mergeError }}</p>
</v-card-text>
<v-card-actions class="merge-dialog-actions">
<v-btn
color="primary"
variant="flat"
block
class="merge-submit-btn"
:loading="mergeLoading"
:disabled="mergeLoading || mergeAmount <= 0"
@click="submitMerge"
>
Merge Shares
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Split dialog对接 /PmMarket/split -->
<v-dialog v-model="splitDialogOpen" max-width="440" persistent content-class="split-dialog" transition="dialog-transition">
<v-card class="split-dialog-card" rounded="lg">
<div class="split-dialog-header">
<h3 class="split-dialog-title">Split</h3>
<v-btn icon variant="text" size="small" class="split-dialog-close" @click="splitDialogOpen = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<v-card-text class="split-dialog-body">
<p class="split-dialog-desc">
Use USDC to get one share of Yes and one share of No for this market. 1 USDC 1 complete set.
</p>
<div class="split-amount-row">
<label class="split-amount-label">Amount (USDC)</label>
<v-text-field
v-model.number="splitAmount"
type="number"
min="0"
step="1"
hide-details
density="compact"
variant="outlined"
class="split-amount-input"
/>
</div>
<p v-if="!props.market?.marketId" class="split-no-market">Please select a market first (e.g. click Buy Yes/No on a market).</p>
<p v-if="splitError" class="split-error">{{ splitError }}</p>
</v-card-text>
<v-card-actions class="split-dialog-actions">
<v-btn
color="primary"
variant="flat"
block
class="split-submit-btn"
:loading="splitLoading"
:disabled="splitLoading || splitAmount <= 0"
@click="submitSplit"
>
Split
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useDisplay } from 'vuetify'
import { useUserStore } from '../stores/user'
import { pmMarketMerge, pmMarketSplit } from '../api/market'
const { mobile } = useDisplay()
const userStore = useUserStore()
export interface TradeMarketPayload {
marketId?: string
yesPrice: number
noPrice: number
title?: string
}
const props = withDefaults(
defineProps<{
initialOption?: 'yes' | 'no'
embeddedInSheet?: boolean
/** 从外部传入的市场数据(如 EventMarkets 点击 Yes/No 传入yesPrice/noPrice 为 01 */
market?: TradeMarketPayload
}>(),
{ initialOption: undefined, embeddedInSheet: false, market: undefined }
)
// 移动端:底部栏与弹出层
const sheetOpen = ref(false)
// Merge shares dialog对接 /PmMarket/merge
const mergeDialogOpen = ref(false)
const mergeAmount = ref(0)
const availableMergeShares = ref(0)
const mergeLoading = ref(false)
const mergeError = ref('')
function openMergeDialog() {
mergeAmount.value = 0
mergeError.value = ''
mergeDialogOpen.value = true
}
function setMergeMax() {
mergeAmount.value = availableMergeShares.value
}
async function submitMerge() {
const marketId = props.market?.marketId
if (mergeAmount.value <= 0) return
if (!marketId) return
mergeLoading.value = true
mergeError.value = ''
try {
const res = await pmMarketMerge(
{ marketID: marketId, amount: String(mergeAmount.value) },
{ headers: userStore.getAuthHeaders() }
)
if (res.code === 0 || res.code === 200) {
mergeDialogOpen.value = false
userStore.fetchUsdcBalance()
} else {
mergeError.value = res.msg || 'Merge failed'
}
} catch (e) {
mergeError.value = e instanceof Error ? e.message : 'Request failed'
} finally {
mergeLoading.value = false
}
}
// Split dialog对接测试服务器 /PmMarket/split
const splitDialogOpen = ref(false)
const splitAmount = ref(0)
const splitLoading = ref(false)
const splitError = ref('')
function openSplitDialog() {
splitAmount.value = splitAmount.value > 0 ? splitAmount.value : 1
splitError.value = ''
splitDialogOpen.value = true
}
async function submitSplit() {
const marketId = props.market?.marketId
if (splitAmount.value <= 0) return
if (!marketId) return // 无市场时仅依赖模板中的 split-no-market 提示,不重复设置 splitError
splitLoading.value = true
splitError.value = ''
try {
const res = await pmMarketSplit(
{ marketID: marketId, usdcAmount: String(splitAmount.value) },
{ headers: userStore.getAuthHeaders() }
)
if (res.code === 0 || res.code === 200) {
splitDialogOpen.value = false
} else {
splitError.value = res.msg || 'Split failed'
}
} catch (e) {
splitError.value = e instanceof Error ? e.message : 'Request failed'
} finally {
splitLoading.value = false
}
}
const yesPriceCents = computed(() =>
props.market ? Math.round(props.market.yesPrice * 100) : 19
)
const noPriceCents = computed(() =>
props.market ? Math.round(props.market.noPrice * 100) : 82
)
function openSheet(option: 'yes' | 'no') {
handleOptionChange(option)
sheetOpen.value = true
}
// State
const activeTab = ref('buy')
const limitType = ref('Limit')
const expirationEnabled = ref(false)
const selectedOption = ref<'yes' | 'no'>(props.initialOption ?? 'no')
const limitPrice = ref(0.82) // 初始限价,单位:美元
const shares = ref(20) // 初始份额
const expirationTime = ref('5m') // 初始过期时间
const expirationOptions = ref(['5m', '15m', '30m', '1h', '2h', '4h', '8h', '12h', '1d', '2d', '3d']) // 过期时间选项
// Market mode state
const isMarketMode = computed(() => limitType.value === 'Market')
const amount = ref(0) // Market mode amount
const balance = ref(0) // Market mode balance
// Emits
const emit = defineEmits<{
optionChange: [option: 'yes' | 'no']
submit: [payload: {
side: 'buy' | 'sell'
option: 'yes' | 'no'
limitPrice: number
shares: number
expirationEnabled: boolean
expirationTime: string
marketId?: string
}]
}>()
// Computed properties
const currentPrice = computed(() => {
return `${(limitPrice.value * 100).toFixed(0)}¢`
})
const totalPrice = computed(() => {
return (limitPrice.value * shares.value).toFixed(2)
})
const actionButtonText = computed(() => {
return `${activeTab.value} ${selectedOption.value.charAt(0).toUpperCase() + selectedOption.value.slice(1)}`
})
function applyInitialOption(option: 'yes' | 'no') {
selectedOption.value = option
syncLimitPriceFromMarket()
}
/** 根据当前 props.market 与 selectedOption 同步 limitPrice组件显示或 market 更新时调用) */
function syncLimitPriceFromMarket() {
const yesP = props.market?.yesPrice ?? 0.19
const noP = props.market?.noPrice ?? 0.82
limitPrice.value = selectedOption.value === 'yes' ? yesP : noP
}
onMounted(() => {
if (props.initialOption) applyInitialOption(props.initialOption)
else if (props.market) syncLimitPriceFromMarket()
})
watch(() => props.initialOption, (option) => {
if (option) applyInitialOption(option)
}, { immediate: true })
watch(() => props.market, (m) => {
if (m) {
if (props.initialOption) applyInitialOption(props.initialOption)
else syncLimitPriceFromMarket()
}
}, { deep: true })
// Methods
const handleOptionChange = (option: 'yes' | 'no') => {
selectedOption.value = option
const yesP = props.market?.yesPrice ?? 0.19
const noP = props.market?.noPrice ?? 0.82
limitPrice.value = option === 'yes' ? yesP : noP
emit('optionChange', option)
}
// 限价调整方法
const decreasePrice = () => {
limitPrice.value = Math.max(0.01, limitPrice.value - 0.01)
}
const increasePrice = () => {
limitPrice.value += 0.01
}
// 份额调整方法
const adjustShares = (amount: number) => {
shares.value = Math.max(0, shares.value + amount)
}
// 份额百分比调整方法仅在Sell模式下使用
const setSharesPercentage = (percentage: number) => {
// 假设最大份额为100实际应用中可能需要根据用户的可用份额来计算
const maxShares = 100
shares.value = Math.round((maxShares * percentage) / 100)
}
// Market mode methods
const adjustAmount = (value: number) => {
amount.value += value
if (amount.value < 0) amount.value = 0
}
const setMaxAmount = () => {
amount.value = balance.value
}
const deposit = () => {
console.log('Depositing amount:', amount.value)
// 实际应用中这里应该调用存款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,
...(props.market?.marketId != null && { marketId: props.market.marketId }),
})
}
</script>
<style scoped>
.trade-component {
width: 100%;
max-width: 400px;
border-radius: 8px;
overflow: hidden;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border-bottom: 1px solid #e0e0e0;
}
.buy-sell-tabs {
flex: 1;
}
.minimal-tabs {
--tabs-height: 32px !important;
margin: 0 !important;
}
.minimal-tab {
height: 32px !important;
min-height: 32px !important;
padding: 4px 12px !important;
font-size: 14px !important;
}
/* 移除了.tab-btn样式使用v-tabs的默认样式 */
.limit-btn {
font-size: 14px;
text-transform: none;
color: #666666;
}
.more-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.price-options {
display: flex;
gap: 10px;
padding: 12px;
}
.yes-btn {
flex: 1;
background-color: #f0f0f0;
border-radius: 4px;
text-transform: none;
font-size: 14px;
}
.yes-btn.active {
background-color: #b8e0b8;
color: #006600;
}
.no-btn {
flex: 1;
background-color: #f0f0f0;
color: #000000;
border-radius: 4px;
text-transform: none;
font-size: 14px;
}
.no-btn.active {
background-color: #cc0000;
color: #ffffff;
}
.input-group {
padding: 12px;
border-top: 1px solid #e0e0e0;
}
/* Reduce height for Set Expiration section */
.expiration-group {
padding: 8px 12px !important;
margin: 0 !important;
}
/* Limit Price header flex layout */
.limit-price-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
/* Adjust label margin for flex layout */
.limit-price-header .label {
margin-bottom: 0;
flex-shrink: 0;
}
/* Adjust price input width for flex layout */
.limit-price-header .price-input {
flex: 1;
max-width: 200px;
margin-left: 12px;
}
.label {
font-size: 14px;
color: #666666;
margin-bottom: 6px;
display: block;
}
.price-input {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #f8f8f8;
border-radius: 4px;
padding: 6px 10px;
}
.adjust-btn {
color: #666666;
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
}
.price-value {
font-size: 15px;
font-weight: 500;
}
.price-input-field {
flex: 1;
text-align: center;
font-size: 15px;
font-weight: 500;
background-color: transparent;
border: none;
box-shadow: none;
max-width: 100px;
}
/* Shares header flex layout */
.shares-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 8px;
}
/* Adjust label margin for flex layout */
.shares-header .label {
margin-bottom: 0;
flex-shrink: 0;
}
/* Adjust shares input width for flex layout */
.shares-header .shares-input {
flex: 1;
max-width: 200px;
margin-left: 12px;
text-align: right;
}
.shares-input {
background-color: #f8f8f8;
border-radius: 4px;
margin-bottom: 8px;
}
.shares-value {
font-size: 15px;
font-weight: 500;
text-align: right;
}
.shares-input-field {
width: 100%;
text-align: right;
font-size: 15px;
font-weight: 500;
background-color: transparent;
border: none;
box-shadow: none;
}
.shares-buttons {
display: flex;
gap: 6px;
margin-bottom: 6px;
}
.share-btn {
flex: 1;
background-color: #f0f0f0;
border-radius: 4px;
text-transform: none;
font-size: 12px;
padding: 4px;
}
.matching-info {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #3d8b40;
}
.expiration-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.expiration-switch {
margin: 0;
}
.expiration-select {
margin-top: 8px;
width: 100%;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.total-section {
padding: 12px;
border-top: 1px solid #e0e0e0;
border-bottom: 1px solid #e0e0e0;
}
.total-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.total-value {
font-size: 15px;
font-weight: 500;
color: #0066cc;
}
.to-win-value {
font-size: 15px;
font-weight: 500;
color: #3d8b40;
}
.action-btn {
width: 100%;
background-color: #0066cc;
color: #ffffff;
border-radius: 0;
padding: 16px;
font-size: 15px;
font-weight: 500;
text-transform: none;
}
/* Market Mode Styles */
.amount-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 12px;
}
.amount-label {
font-size: 16px;
font-weight: 500;
margin-bottom: 4px;
}
.balance-label {
font-size: 12px;
color: #999999;
display: block;
}
.amount-value {
font-size: 24px;
font-weight: 500;
color: #000000;
}
.amount-buttons {
display: flex;
gap: 8px;
justify-content: space-between;
}
.amount-btn {
flex: 1;
background-color: #f0f0f0;
border-radius: 4px;
text-transform: none;
font-size: 14px;
padding: 8px;
}
.deposit-btn {
width: 100%;
background-color: #0066cc;
color: #ffffff;
border-radius: 0;
padding: 16px;
font-size: 15px;
font-weight: 500;
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: #16a34a;
color: #fff;
padding: 14px 16px;
}
.mobile-bar-no {
flex: 1;
min-width: 0;
background: #dc2626;
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) {
.header {
padding: 10px;
}
.price-options {
padding: 10px;
}
.input-group {
padding: 10px;
}
.total-section {
padding: 10px;
}
.label {
font-size: 13px;
margin-bottom: 5px;
}
.price-value,
.shares-value,
.total-value,
.to-win-value {
font-size: 14px;
}
.action-btn {
padding: 15px;
font-size: 14px;
}
.share-btn {
font-size: 11px;
padding: 3px;
}
.amount-label {
font-size: 14px;
}
.amount-value {
font-size: 20px;
}
.amount-btn {
font-size: 13px;
padding: 6px;
}
.deposit-btn {
padding: 15px;
font-size: 14px;
}
}
/* Merge shares dialog */
.merge-dialog-card {
padding: 0;
overflow: hidden;
}
.merge-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 20px 0;
}
.merge-dialog-title {
font-size: 18px;
font-weight: 700;
color: #111827;
margin: 0;
}
.merge-dialog-close {
color: #6b7280;
}
.merge-dialog-body {
padding: 16px 20px 8px;
}
.merge-dialog-desc {
font-size: 14px;
color: #374151;
line-height: 1.5;
margin: 0 0 20px;
}
.merge-amount-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.merge-amount-label {
font-size: 14px;
font-weight: 600;
color: #111827;
min-width: 60px;
}
.merge-amount-input {
flex: 1;
max-width: 160px;
}
.merge-amount-input :deep(.v-field) {
font-size: 14px;
}
.merge-available {
font-size: 13px;
color: #6b7280;
margin: 0;
}
.merge-max-link {
color: #2563eb;
background: none;
border: none;
cursor: pointer;
padding: 0;
margin-left: 4px;
font-size: 13px;
}
.merge-max-link:hover {
text-decoration: underline;
}
.merge-no-market,
.merge-error {
font-size: 13px;
margin: 8px 0 0;
}
.merge-no-market {
color: #6b7280;
}
.merge-error {
color: #dc2626;
}
.merge-dialog-actions {
padding: 16px 20px 20px;
padding-top: 8px;
}
.merge-submit-btn {
text-transform: none;
font-weight: 600;
}
/* Split dialog */
.split-dialog-card {
padding: 0;
overflow: hidden;
}
.split-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 0;
}
.split-dialog-title {
font-size: 18px;
font-weight: 700;
color: #111827;
margin: 0;
}
.split-dialog-close {
color: #6b7280;
}
.split-dialog-body {
padding: 16px 20px 8px;
}
.split-dialog-desc {
font-size: 14px;
color: #374151;
line-height: 1.5;
margin: 0 0 16px 0;
}
.split-amount-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.split-amount-label {
font-size: 14px;
font-weight: 600;
color: #111827;
flex-shrink: 0;
}
.split-amount-input {
flex: 1;
max-width: 160px;
}
.split-amount-input :deep(.v-field) {
font-size: 14px;
}
.split-no-market,
.split-error {
font-size: 13px;
margin: 8px 0 0;
}
.split-no-market {
color: #6b7280;
}
.split-error {
color: #dc2626;
}
.split-dialog-actions {
padding: 16px 20px 20px;
padding-top: 8px;
}
.split-submit-btn {
text-transform: none;
font-weight: 600;
}
</style>