xtraderClient/src/components/TradeComponent.vue

1996 lines
56 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>
<p v-if="orderError" class="order-error">{{ orderError }}</p>
<!-- Action Button -->
<v-btn
class="action-btn"
:loading="orderLoading"
:disabled="orderLoading"
@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
:model-value="limitPrice"
type="number"
min="0"
max="1"
step="0.01"
class="price-input-field"
hide-details
density="compact"
@update:model-value="onLimitPriceInput"
@blur="onLimitPriceBlur"
@keydown="onLimitPriceKeydown"
@paste="onLimitPricePaste"
></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
:model-value="shares"
type="number"
min="1"
class="shares-input-field"
hide-details
density="compact"
@update:model-value="onSharesInput"
@keydown="onSharesKeydown"
@paste="onSharesPaste"
></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>
<p v-if="orderError" class="order-error">{{ orderError }}</p>
<v-btn
class="action-btn"
:loading="orderLoading"
:disabled="orderLoading"
@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>
<p v-if="orderError" class="order-error">{{ orderError }}</p>
<v-btn
class="action-btn"
:loading="orderLoading"
:disabled="orderLoading"
@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
:model-value="limitPrice"
type="number"
min="0"
max="1"
step="0.01"
class="price-input-field"
hide-details
density="compact"
@update:model-value="onLimitPriceInput"
@blur="onLimitPriceBlur"
@keydown="onLimitPriceKeydown"
@paste="onLimitPricePaste"
></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
:model-value="shares"
type="number"
min="1"
class="shares-input-field"
hide-details
density="compact"
@update:model-value="onSharesInput"
@keydown="onSharesKeydown"
@paste="onSharesPaste"
></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>
<p v-if="orderError" class="order-error">{{ orderError }}</p>
<v-btn
class="action-btn"
:loading="orderLoading"
:disabled="orderLoading"
@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>
<p v-if="orderError" class="order-error">{{ orderError }}</p>
<v-btn
class="action-btn"
:loading="orderLoading"
:disabled="orderLoading"
@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
:model-value="limitPrice"
type="number"
min="0"
max="1"
step="0.01"
class="price-input-field"
hide-details
density="compact"
@update:model-value="onLimitPriceInput"
@keydown="onLimitPriceKeydown"
@paste="onLimitPricePaste"
></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
:model-value="shares"
type="number"
min="1"
class="shares-input-field"
hide-details
density="compact"
@update:model-value="onSharesInput"
@keydown="onSharesKeydown"
@paste="onSharesPaste"
></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>
<p v-if="orderError" class="order-error">{{ orderError }}</p>
<v-btn
class="action-btn"
:loading="orderLoading"
:disabled="orderLoading"
@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, pmOrderPlace } from '../api/market'
import { OrderType, Side } from '../api/constants'
const { mobile } = useDisplay()
const userStore = useUserStore()
/** 限价单允许的 135 个价格档位01 区间规则19/1090/1009900/99109990/99919999 */
function buildAllowedLimitPrices(): number[] {
const list: number[] = []
for (let i = 1; i <= 9; i++) list.push(i / 10000)
for (let i = 10; i <= 90; i += 10) list.push(i / 10000)
for (let i = 100; i <= 9900; i += 100) list.push(i / 10000)
for (let i = 9910; i <= 9990; i += 10) list.push(i / 10000)
for (let i = 9991; i <= 9999; i++) list.push(i / 10000)
return list
}
const ALLOWED_LIMIT_PRICES = buildAllowedLimitPrices()
/** 将限价吸附到最近的允许档位 */
function snapToAllowedPrice(v: number): number {
const clamped = Math.min(1, Math.max(0, Number.isFinite(v) ? v : 0))
const list = ALLOWED_LIMIT_PRICES
if (list.length === 0) return clamped
let nearest = list[0] as number
let minDiff = Math.abs(clamped - nearest)
for (const p of list) {
const d = Math.abs(clamped - p)
if (d < minDiff) {
minDiff = d
nearest = p
}
}
return nearest
}
/** 获取当前价格在允许列表中的索引,-1 表示不在列表中 */
function indexOfAllowedPrice(v: number): number {
const snapped = snapToAllowedPrice(v)
const idx = ALLOWED_LIMIT_PRICES.findIndex((p) => Math.abs(p - snapped) < 1e-9)
return idx >= 0 ? idx : 0
}
export interface TradeMarketPayload {
marketId?: string
yesPrice: number
noPrice: number
title?: string
/** 与 outcomes/outcomePrices 顺序一致,用于下单 tokenId0=Yes 1=No */
clobTokenIds?: 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
const orderLoading = ref(false)
const orderError = ref('')
// 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()
}
/** 限价限制到 [0,1] 并吸附到 135 个允许档位之一 */
function clampLimitPrice(v: number): number {
return snapToAllowedPrice(Math.min(1, Math.max(0, Number.isFinite(v) ? v : 0)))
}
/** 根据当前 props.market 与 selectedOption 同步 limitPrice组件显示或 market 更新时调用) */
function syncLimitPriceFromMarket() {
const yesP = props.market?.yesPrice ?? 0.19
const noP = props.market?.noPrice ?? 0.82
limitPrice.value = clampLimitPrice(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) {
orderError.value = ''
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 = clampLimitPrice(option === 'yes' ? yesP : noP)
emit('optionChange', option)
}
/** 仅接受 135 个允许档位:输入值吸附到最近档位,非法值忽略 */
function onLimitPriceInput(v: unknown) {
const num = v == null ? NaN : Number(v)
if (!Number.isFinite(num) || num < 0 || num > 1) return
limitPrice.value = snapToAllowedPrice(num)
}
/** 失焦时吸附到允许档位 */
function onLimitPriceBlur() {
limitPrice.value = snapToAllowedPrice(limitPrice.value)
}
/** 只允许数字和小数点输入 */
function onLimitPriceKeydown(e: KeyboardEvent) {
const key = e.key
const allowed = ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End']
if (allowed.includes(key)) return
if (e.ctrlKey || e.metaKey) {
if (['a', 'c', 'v', 'x'].includes(key.toLowerCase())) return
}
if (key >= '0' && key <= '9') return
if (key === '.' && !String((e.target as HTMLInputElement)?.value ?? '').includes('.')) return
e.preventDefault()
}
/** 粘贴时只接受有效数字 */
function onLimitPricePaste(e: ClipboardEvent) {
const text = e.clipboardData?.getData('text') ?? ''
const num = parseFloat(text)
if (!Number.isFinite(num) || num < 0 || num > 1) {
e.preventDefault()
}
}
// 限价调整方法:在 135 个允许档位间上下切换
const decreasePrice = () => {
const idx = indexOfAllowedPrice(limitPrice.value)
const nextIdx = Math.max(0, idx - 1)
limitPrice.value = ALLOWED_LIMIT_PRICES[nextIdx] ?? limitPrice.value
}
const increasePrice = () => {
const idx = indexOfAllowedPrice(limitPrice.value)
const nextIdx = Math.min(ALLOWED_LIMIT_PRICES.length - 1, idx + 1)
limitPrice.value = ALLOWED_LIMIT_PRICES[nextIdx] ?? limitPrice.value
}
/** 将 shares 限制为正整数(>= 1 */
function clampShares(v: number): number {
const n = Math.floor(Number.isFinite(v) ? v : 1)
return Math.max(1, n)
}
/** 仅在值为正整数时更新 shares */
function onSharesInput(v: unknown) {
const num = v == null ? NaN : Number(v)
const n = Math.floor(num)
if (!Number.isFinite(num) || n < 1 || num !== n) return
shares.value = n
}
/** 只允许数字输入Shares 为正整数) */
function onSharesKeydown(e: KeyboardEvent) {
const key = e.key
const allowed = ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End']
if (allowed.includes(key)) return
if (e.ctrlKey || e.metaKey) {
if (['a', 'c', 'v', 'x'].includes(key.toLowerCase())) return
}
if (key >= '0' && key <= '9') return
e.preventDefault()
}
/** 粘贴时只接受正整数 */
function onSharesPaste(e: ClipboardEvent) {
const text = e.clipboardData?.getData('text') ?? ''
const num = parseInt(text, 10)
if (!Number.isFinite(num) || num < 1) e.preventDefault()
}
// 份额调整方法(保证正整数)
const adjustShares = (amount: number) => {
shares.value = clampShares(shares.value + amount)
}
// 份额百分比调整方法仅在Sell模式下使用
const setSharesPercentage = (percentage: number) => {
const maxShares = 100
shares.value = clampShares(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
}
/** 将 expirationTime 如 "5m" 转为 Unix 秒级时间戳GTD 用0 表示无过期 */
function parseExpirationTimestamp(expTime: string): number {
const m = /^(\d+)(m|h|d)$/i.exec(expTime)
if (!m) return 0
const [, num, unit] = m
const n = parseInt(num ?? '0', 10)
if (!Number.isFinite(n) || n <= 0) return 0
const now = Date.now()
let ms = n
if (unit?.toLowerCase() === 'm') ms = n * 60 * 1000
else if (unit?.toLowerCase() === 'h') ms = n * 3600 * 1000
else if (unit?.toLowerCase() === 'd') ms = n * 86400 * 1000
return Math.floor((now + ms) / 1000)
}
// 提交订单(含 Set expiration对接 /clob/gateway/submitOrder
async function submitOrder() {
const marketId = props.market?.marketId
const clobIds = props.market?.clobTokenIds
const outcomeIndex = selectedOption.value === 'yes' ? 0 : 1
const tokenId = clobIds?.[outcomeIndex]
const payload = {
side: activeTab.value as 'buy' | 'sell',
option: selectedOption.value,
limitPrice: limitPrice.value,
shares: shares.value,
expirationEnabled: expirationEnabled.value,
expirationTime: expirationTime.value,
...(marketId != null && { marketId }),
}
emit('submit', payload)
if (!tokenId) {
orderError.value = '请先选择市场(需包含 clobTokenIds'
return
}
const headers = userStore.getAuthHeaders()
if (!headers) {
orderError.value = '请先登录'
return
}
const uid = userStore.user?.id ?? userStore.user?.ID
const userIdNum = uid != null ? Number(uid) : 0
if (!Number.isFinite(userIdNum) || userIdNum <= 0) {
orderError.value = '用户信息异常'
return
}
const isMarket = limitType.value === 'Market'
const orderTypeNum = isMarket
? OrderType.Market
: expirationEnabled.value
? OrderType.GTD
: OrderType.GTC
const sideNum = activeTab.value === 'buy' ? Side.Buy : Side.Sell
const expiration =
orderTypeNum === OrderType.GTD && expirationEnabled.value
? parseExpirationTimestamp(expirationTime.value)
: 0
orderLoading.value = true
orderError.value = ''
try {
const res = await pmOrderPlace(
{
expiration,
feeRateBps: 0,
nonce: 0,
orderType: orderTypeNum,
price: limitPrice.value,
side: sideNum,
size: clampShares(shares.value),
taker: true,
tokenID: tokenId,
userID: userIdNum,
},
{ headers },
)
if (res.code === 0 || res.code === 200) {
userStore.fetchUsdcBalance()
} else {
orderError.value = res.msg || '下单失败'
}
} catch (e) {
orderError.value = e instanceof Error ? e.message : 'Request failed'
} finally {
orderLoading.value = false
}
}
</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;
}
.order-error {
font-size: 13px;
color: #dc2626;
margin: 8px 0 0;
}
.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>