优化:最大份额显示

This commit is contained in:
ivan 2026-02-27 23:34:58 +08:00
parent ef4b0c8922
commit af22e1a91c
8 changed files with 253 additions and 145 deletions

View File

@ -35,6 +35,11 @@ interface TradePositionItem {
- **Buy 模式 Amount 区**:无论余额是否充足,均显示 Amount 标签、Balance、金额输入、+$1/+$20/+$100/Max 快捷按钮(桌面端、嵌入弹窗、移动端弹窗一致)
- 余额不足时 Buy 显示 Deposit 按钮
- 25%/50%/Max 快捷份额
- **Sell 模式 UI 优化**
- Shares 标签与 Max shares 提示同行显示(`max-shares-inline`
- 输入框独占一行(`shares-input-wrapper`
- 25%/50%/Max 按钮独立一行(`sell-shares-buttons`
- 整体布局更清晰:`Shares Max: 2``[输入框]``[25%][50%][Max]`
- 调用 market API 下单、Split、Merge
- **合并/拆分成功后触发事件**`mergeSuccess``splitSuccess`,父组件监听后可刷新持仓列表

View File

@ -24,6 +24,8 @@ export interface ClobSubmitOrderRequest {
taker: boolean
tokenID: string
userID: number
/** 市场 ID */
marketID: string
}
/**

View File

@ -77,26 +77,28 @@
<!-- Sell Market: Shares input + 25%/50%/Max -->
<template v-if="activeTab === 'sell'">
<div class="input-group shares-group">
<div class="shares-header">
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.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>
<span class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span>
</div>
<div class="shares-buttons">
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
class="shares-input-field"
hide-details
variant="outlined"
density="compact"
@update:model-value="onSharesInput"
@keydown="onSharesKeydown"
@paste="onSharesPaste"
></v-text-field>
</div>
<div class="shares-buttons sell-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)">{{ t('trade.max') }}</v-btn>
<v-btn class="share-btn" @click="setMaxShares">{{ t('trade.max') }}</v-btn>
</div>
</div>
</template>
@ -212,26 +214,28 @@
<!-- Sell: Shares + To receive + Avg. Price只显示 Sell Yes/No {{ t('trade.deposit') }} -->
<template v-else>
<div class="input-group shares-group">
<div class="shares-header">
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.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>
<span class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span>
</div>
<div class="shares-buttons">
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
variant="outlined"
class="shares-input-field"
hide-details
density="compact"
@update:model-value="onSharesInput"
@keydown="onSharesKeydown"
@paste="onSharesPaste"
></v-text-field>
</div>
<div class="shares-buttons sell-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)">{{ t('trade.max') }}</v-btn>
<v-btn class="share-btn" @click="setMaxShares">{{ t('trade.max') }}</v-btn>
</div>
</div>
<div class="total-section">
@ -298,6 +302,7 @@
min="0"
max="100"
step="0.01"
variant="outlined"
class="price-input-field"
hide-details
density="compact"
@ -316,21 +321,23 @@
<!-- Shares -->
<div class="input-group shares-group">
<div class="shares-header">
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.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>
<span v-if="activeTab === 'sell'" class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span>
</div>
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
variant="outlined"
class="shares-input-field"
hide-details
density="compact"
@update:model-value="onSharesInput"
@keydown="onSharesKeydown"
@paste="onSharesPaste"
></v-text-field>
</div>
<!-- Buy模式的份额调整按钮 -->
<div v-if="activeTab === 'buy'" class="shares-buttons">
@ -341,10 +348,10 @@
<v-btn class="share-btn" @click="adjustShares(200)">+200</v-btn>
</div>
<!-- Sell模式的份额调整按钮 -->
<div v-else class="shares-buttons">
<div v-else class="shares-buttons sell-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)">{{ t('trade.max') }}</v-btn>
<v-btn class="share-btn" @click="setMaxShares">{{ t('trade.max') }}</v-btn>
</div>
<div v-if="activeTab === 'buy'" class="matching-info">
<v-icon size="14">mdi-information</v-icon>
@ -483,26 +490,28 @@
<!-- Sell Market: Shares input + 25%/50%/Max -->
<template v-if="activeTab === 'sell'">
<div class="input-group shares-group">
<div class="shares-header">
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.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>
<span class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span>
</div>
<div class="shares-buttons">
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
class="shares-input-field"
hide-details
density="compact"
variant="outlined"
@update:model-value="onSharesInput"
@keydown="onSharesKeydown"
@paste="onSharesPaste"
></v-text-field>
</div>
<div class="shares-buttons sell-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)">{{ t('trade.max') }}</v-btn>
<v-btn class="share-btn" @click="setMaxShares">{{ t('trade.max') }}</v-btn>
</div>
</div>
</template>
@ -598,26 +607,28 @@
<!-- Sell: Shares + To receive + Avg. Price -->
<template v-else>
<div class="input-group shares-group">
<div class="shares-header">
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.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>
<span class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span>
</div>
<div class="shares-buttons">
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
class="shares-input-field"
hide-details
density="compact"
variant="outlined"
@update:model-value="onSharesInput"
@keydown="onSharesKeydown"
@paste="onSharesPaste"
></v-text-field>
</div>
<div class="shares-buttons sell-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)">{{ t('trade.max') }}</v-btn>
<v-btn class="share-btn" @click="setMaxShares">{{ t('trade.max') }}</v-btn>
</div>
</div>
<div class="total-section">
@ -676,6 +687,7 @@
min="0"
max="100"
step="0.01"
variant="outlined"
class="price-input-field"
hide-details
density="compact"
@ -692,21 +704,23 @@
</div>
</div>
<div class="input-group shares-group">
<div class="shares-header">
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.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>
<span v-if="activeTab === 'sell'" class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span>
</div>
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
class="shares-input-field"
hide-details
density="compact"
variant="outlined"
@update:model-value="onSharesInput"
@keydown="onSharesKeydown"
@paste="onSharesPaste"
></v-text-field>
</div>
<div v-if="activeTab === 'buy'" class="shares-buttons">
<v-btn class="share-btn" @click="adjustShares(-100)">-100</v-btn>
@ -714,10 +728,10 @@
<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">
<div v-else class="shares-buttons sell-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)">{{ t('trade.max') }}</v-btn>
<v-btn class="share-btn" @click="setMaxShares">{{ t('trade.max') }}</v-btn>
</div>
</div>
<div class="input-group expiration-group">
@ -872,26 +886,28 @@
<!-- Sell Market: Shares input + 25%/50%/Max -->
<template v-if="activeTab === 'sell'">
<div class="input-group shares-group">
<div class="shares-header">
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.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>
<span class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span>
</div>
<div class="shares-buttons">
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
class="shares-input-field"
hide-details
density="compact"
variant="outlined"
@update:model-value="onSharesInput"
@keydown="onSharesKeydown"
@paste="onSharesPaste"
></v-text-field>
</div>
<div class="shares-buttons sell-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)">{{ t('trade.max') }}</v-btn>
<v-btn class="share-btn" @click="setMaxShares">{{ t('trade.max') }}</v-btn>
</div>
</div>
</template>
@ -988,26 +1004,28 @@
<!-- Sell: Shares + To receive + Avg. Price -->
<template v-else>
<div class="input-group shares-group">
<div class="shares-header">
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.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>
<span class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span>
</div>
<div class="shares-buttons">
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
class="shares-input-field"
hide-details
variant="outlined"
density="compact"
@update:model-value="onSharesInput"
@keydown="onSharesKeydown"
@paste="onSharesPaste"
></v-text-field>
</div>
<div class="shares-buttons sell-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)">{{ t('trade.max') }}</v-btn>
<v-btn class="share-btn" @click="setMaxShares">{{ t('trade.max') }}</v-btn>
</div>
</div>
<div class="total-section">
@ -1069,6 +1087,7 @@
class="price-input-field"
hide-details
density="compact"
variant="outlined"
suffix="¢"
@update:model-value="onLimitPriceInput"
@blur="onLimitPriceBlur"
@ -1082,21 +1101,23 @@
</div>
</div>
<div class="input-group shares-group">
<div class="shares-header">
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.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>
<span v-if="activeTab === 'sell'" class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span>
</div>
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
class="shares-input-field"
hide-details
density="compact"
variant="outlined"
@update:model-value="onSharesInput"
@keydown="onSharesKeydown"
@paste="onSharesPaste"
></v-text-field>
</div>
<div v-if="activeTab === 'buy'" class="shares-buttons">
<v-btn class="share-btn" @click="adjustShares(-100)">-100</v-btn>
@ -1104,10 +1125,10 @@
<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">
<div v-else class="shares-buttons sell-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)">{{ t('trade.max') }}</v-btn>
<v-btn class="share-btn" @click="setMaxShares">{{ t('trade.max') }}</v-btn>
</div>
</div>
<div class="input-group expiration-group">
@ -1602,6 +1623,15 @@ watch(
{ deep: true },
)
watch(
() => activeTab.value,
(newTab) => {
if (newTab === 'sell') {
setMaxShares()
}
},
)
// Methods
const handleOptionChange = (option: 'yes' | 'no') => {
selectedOption.value = option
@ -1609,6 +1639,11 @@ const handleOptionChange = (option: 'yes' | 'no') => {
const noP = props.market?.noPrice ?? 0.82
limitPrice.value = clampLimitPrice(option === 'yes' ? yesP : noP)
emit('optionChange', option)
// Set max shares when option changes in sell mode
if (activeTab.value === 'sell') {
setMaxShares()
}
}
/** 输入为美分0100与 Yes/No 按钮单位一致 */
@ -1696,9 +1731,32 @@ const adjustShares = (amount: number) => {
shares.value = clampShares(shares.value + amount)
}
/** 计算当前选中选项的持仓份额 */
const currentOptionPositionShares = computed(() => {
const positions = props.positions ?? []
const currentOutcome = selectedOption.value === 'yes' ? 'Yes' : 'No'
let totalShares = 0
for (const pos of positions) {
if (pos.outcomeWord === currentOutcome) {
const num = pos.sharesNum ?? parseFloat(pos.shares?.replace(/[^0-9.]/g, ''))
if (Number.isFinite(num) && num > 0) {
totalShares += num
}
}
}
return totalShares
})
/** 最大可卖出份额 */
const maxAvailableShares = computed(() => {
return Math.floor(currentOptionPositionShares.value)
})
// Sell使
const setSharesPercentage = (percentage: number) => {
const maxShares = 100
const maxShares = currentOptionPositionShares.value || 100
shares.value = clampShares(Math.round((maxShares * percentage) / 100))
}
@ -1712,6 +1770,14 @@ const setMaxAmount = () => {
amount.value = balance.value
}
//
const setMaxShares = () => {
const maxShares = currentOptionPositionShares.value
if (maxShares > 0) {
shares.value = clampShares(maxShares)
}
}
/** Buy 模式:余额是否足够(>= 所需金额且不为 0。costbalance>0 时用 totalPrice否则用 amount */
const canAffordBuy = computed(() => {
const bal = balance.value
@ -1808,6 +1874,7 @@ async function submitOrder() {
taker: true,
tokenID: tokenId,
userID: userIdNum,
marketID: marketId || '',
},
{ headers },
)
@ -2067,6 +2134,35 @@ async function submitOrder() {
box-shadow: none !important;
}
/* Sell模式Max shares行内提示样式放在Shares标签右侧 */
.max-shares-inline {
font-size: 12px;
color: #6b7280;
font-weight: 500;
margin-left: auto;
}
/* Sell模式输入框独占一行 */
.shares-input-wrapper {
margin: 4px 0 8px;
}
.shares-input-wrapper .v-field {
border-radius: 4px;
}
/* Sell模式shares区域布局优化 */
.sell-shares-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.sell-shares-buttons {
margin-top: 8px;
}
.matching-info {
display: flex;
align-items: center;

View File

@ -58,6 +58,7 @@
"avgPrice": "Avg. Price",
"max": "Max",
"balanceLabel": "Balance",
"maxShares": "Max shares",
"pleaseLogin": "Please log in first",
"pleaseSelectMarket": "Please select a market (with clobTokenIds)",
"userError": "User info error",

View File

@ -58,6 +58,7 @@
"avgPrice": "平均価格",
"max": "最大",
"balanceLabel": "残高",
"maxShares": "最大シェア",
"pleaseLogin": "先にログインしてください",
"pleaseSelectMarket": "市場を選択してくださいclobTokenIds が必要)",
"userError": "ユーザー情報エラー",

View File

@ -58,6 +58,7 @@
"avgPrice": "평균 가격",
"max": "최대",
"balanceLabel": "잔액",
"maxShares": "최대 주식",
"pleaseLogin": "먼저 로그인하세요",
"pleaseSelectMarket": "시장을 선택하세요 (clobTokenIds 필요)",
"userError": "사용자 정보 오류",

View File

@ -58,6 +58,7 @@
"avgPrice": "平均价",
"max": "最大",
"balanceLabel": "余额",
"maxShares": "最大份额",
"pleaseLogin": "请先登录",
"pleaseSelectMarket": "请先选择市场(需包含 clobTokenIds",
"userError": "用户信息异常",

View File

@ -58,6 +58,7 @@
"avgPrice": "平均價",
"max": "最大",
"balanceLabel": "餘額",
"maxShares": "最大份額",
"pleaseLogin": "請先登入",
"pleaseSelectMarket": "請先選擇市場(需包含 clobTokenIds",
"userError": "用戶資訊異常",