优化:卖出订单份额默认取最大有效份额

This commit is contained in:
ivan 2026-02-28 23:31:33 +08:00
parent ff6c7a1877
commit df82732fad
5 changed files with 109 additions and 20 deletions

View File

@ -37,6 +37,7 @@ interface TradePositionItem {
- 事件处理:`onAmountInput``onAmountKeydown``onAmountPaste`
- 余额不足时 Buy 显示 Deposit 按钮
- 25%/50%/Max 快捷份额
- **Sell 模式份额默认取最大可卖**:切换至 Sell 或 maxAvailableShares 变化时自动调用 `setMaxShares()`,将 shares 设为最大可卖数量(无可卖时为 0
- **Sell 模式 UI 优化**
- Shares 标签与 Max shares 提示同行显示(`max-shares-inline`
- 输入框独占一行(`shares-input-wrapper`

View File

@ -17,6 +17,8 @@
- 移动端:底部栏 + `v-bottom-sheet` 嵌入 `TradeComponent`
- Merge/Split通过 `TradeComponent` 或底部菜单触发,成功后监听 `mergeSuccess`/`splitSuccess` 事件刷新持仓
- **401 权限错误**:加载详情失败时,通过 `useAuthError().formatAuthError` 统一提示「请先登录」或「权限不足」
- **Sell 弹窗 emitsOptions 竞态**`sellDialogRenderContent` 延迟 350ms 卸载 TradeComponent等 v-dialog transition 完成,避免 Vue patch 时组件实例为 null 的 `emitsOptions` 错误
- **底部栏 slot 竞态**`tradeSheetRenderContent` 延迟 50ms 挂载、350ms 卸载 TradeComponent避免 v-bottom-sheet transition 期间 VTextField 触发「Slot default invoked outside of the render function」警告
## 使用方式

View File

@ -1748,6 +1748,18 @@ const maxAvailableShares = computed(() => {
return Math.floor(currentOptionPositionShares.value)
})
/** 将 shares 限制为正整数(>= 1 */
function clampShares(v: number): number {
const n = Math.floor(Number.isFinite(v) ? v : 1)
return Math.max(1, n)
}
// Sell
const setMaxShares = () => {
const maxShares = currentOptionPositionShares.value
shares.value = maxShares > 0 ? clampShares(maxShares) : 0
}
function applyInitialOption(option: 'yes' | 'no') {
selectedOption.value = option
syncLimitPriceFromMarket()
@ -1769,6 +1781,7 @@ onMounted(() => {
if (props.initialOption) applyInitialOption(props.initialOption)
else if (props.market) syncLimitPriceFromMarket()
if (props.initialTab) activeTab.value = props.initialTab
if (activeTab.value === 'sell') setMaxShares()
})
watch(
() => props.initialOption,
@ -1810,14 +1823,18 @@ watch(
watch(
() => [activeTab.value, maxAvailableShares.value] as const,
([tab, max]) => {
if (tab === 'sell' && (!Number.isFinite(max) || max <= 0)) {
orderError.value = t('activity.noAvailableSharesToSell')
isNoAvailableSharesError.value = true
} else if (tab !== 'sell' || (Number.isFinite(max) && max > 0)) {
if (isNoAvailableSharesError.value) {
if (tab === 'sell') {
setMaxShares()
if (!Number.isFinite(max) || max <= 0) {
orderError.value = t('activity.noAvailableSharesToSell')
isNoAvailableSharesError.value = true
} else if (isNoAvailableSharesError.value) {
orderError.value = ''
isNoAvailableSharesError.value = false
}
} else if (isNoAvailableSharesError.value) {
orderError.value = ''
isNoAvailableSharesError.value = false
}
},
{ immediate: true },
@ -1885,12 +1902,6 @@ const increasePrice = () => {
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)
@ -1979,14 +1990,6 @@ 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

View File

@ -192,6 +192,7 @@
<template v-if="isMobile && markets.length > 0">
<v-bottom-sheet v-model="tradeSheetOpen" content-class="event-markets-trade-sheet">
<TradeComponent
v-if="tradeSheetRenderContent"
ref="tradeComponentRef"
:key="`trade-${selectedMarketIndex}-${tradeInitialOption}`"
:market="tradeMarketPayload"
@ -245,6 +246,8 @@ const selectedMarketIndex = ref(0)
const tradeInitialOption = ref<'yes' | 'no' | undefined>(undefined)
/** 移动端交易弹窗开关 */
const tradeSheetOpen = ref(false)
/** 控制底部栏内 TradeComponent 的渲染,延迟挂载以避免 slot 竞态警告 */
const tradeSheetRenderContent = ref(false)
/** 移动端底部栏三点菜单开关 */
const mobileMenuOpen = ref(false)
/** TradeComponent 引用,用于从底部栏触发 Merge/Split */
@ -714,11 +717,38 @@ onMounted(() => {
loadEventDetail()
window.addEventListener('resize', handleResize)
})
let tradeSheetUnmountTimer: ReturnType<typeof setTimeout> | undefined
let tradeSheetMountTimer: ReturnType<typeof setTimeout> | undefined
watch(tradeSheetOpen, (open) => {
if (open) {
if (tradeSheetUnmountTimer) {
clearTimeout(tradeSheetUnmountTimer)
tradeSheetUnmountTimer = undefined
}
if (tradeSheetMountTimer) clearTimeout(tradeSheetMountTimer)
tradeSheetMountTimer = setTimeout(() => {
tradeSheetRenderContent.value = true
tradeSheetMountTimer = undefined
}, 50)
} else {
if (tradeSheetMountTimer) {
clearTimeout(tradeSheetMountTimer)
tradeSheetMountTimer = undefined
}
tradeSheetUnmountTimer = setTimeout(() => {
tradeSheetRenderContent.value = false
tradeSheetUnmountTimer = undefined
}, 350)
}
}, { immediate: true })
onUnmounted(() => {
stopDynamicUpdate()
window.removeEventListener('resize', handleResize)
chartInstance?.dispose()
chartInstance = null
if (tradeSheetUnmountTimer) clearTimeout(tradeSheetUnmountTimer)
if (tradeSheetMountTimer) clearTimeout(tradeSheetMountTimer)
})
watch(

View File

@ -290,6 +290,7 @@
</div>
<v-bottom-sheet v-model="tradeSheetOpen" content-class="trade-detail-trade-sheet">
<TradeComponent
v-if="tradeSheetRenderContent"
ref="mobileTradeComponentRef"
:market="tradeMarketPayload"
:initial-option="tradeInitialOptionFromBar"
@ -311,7 +312,7 @@
transition="dialog-transition"
>
<TradeComponent
v-if="sellDialogOpen"
v-if="sellDialogRenderContent"
:market="tradeMarketPayload"
:initial-option="sellInitialOption"
:initial-tab="'sell'"
@ -740,8 +741,12 @@ const tradeInitialOptionFromBar = ref<'yes' | 'no' | undefined>(undefined)
const tradeInitialTabFromBar = ref<'buy' | 'sell' | undefined>(undefined)
/** 移动端交易弹窗开关 */
const tradeSheetOpen = ref(false)
/** 控制底部栏内 TradeComponent 的渲染,延迟挂载以避免 slot 竞态警告 */
const tradeSheetRenderContent = ref(false)
/** 从持仓 Sell 打开的弹窗 */
const sellDialogOpen = ref(false)
/** 控制 Sell 弹窗内 TradeComponent 的渲染,延迟卸载以避免 emitsOptions 竞态 */
const sellDialogRenderContent = ref(false)
/** 从持仓 Sell 时预选的 Yes/No */
const sellInitialOption = ref<'yes' | 'no'>('yes')
/** 移动端三点菜单开关 */
@ -823,6 +828,54 @@ function onSellOrderSuccess() {
onOrderSuccess()
}
// Sell dialog transition emitsOptions
let sellDialogUnmountTimer: ReturnType<typeof setTimeout> | undefined
watch(sellDialogOpen, (open) => {
if (open) {
if (sellDialogUnmountTimer) {
clearTimeout(sellDialogUnmountTimer)
sellDialogUnmountTimer = undefined
}
sellDialogRenderContent.value = true
} else {
sellDialogUnmountTimer = setTimeout(() => {
sellDialogRenderContent.value = false
sellDialogUnmountTimer = undefined
}, 350)
}
}, { immediate: true })
onUnmounted(() => {
if (sellDialogUnmountTimer) clearTimeout(sellDialogUnmountTimer)
if (tradeSheetUnmountTimer) clearTimeout(tradeSheetUnmountTimer)
if (tradeSheetMountTimer) clearTimeout(tradeSheetMountTimer)
})
// TradeComponent / transition slot
let tradeSheetUnmountTimer: ReturnType<typeof setTimeout> | undefined
let tradeSheetMountTimer: ReturnType<typeof setTimeout> | undefined
watch(tradeSheetOpen, (open) => {
if (open) {
if (tradeSheetUnmountTimer) {
clearTimeout(tradeSheetUnmountTimer)
tradeSheetUnmountTimer = undefined
}
if (tradeSheetMountTimer) clearTimeout(tradeSheetMountTimer)
tradeSheetMountTimer = setTimeout(() => {
tradeSheetRenderContent.value = true
tradeSheetMountTimer = undefined
}, 50)
} else {
if (tradeSheetMountTimer) {
clearTimeout(tradeSheetMountTimer)
tradeSheetMountTimer = undefined
}
tradeSheetUnmountTimer = setTimeout(() => {
tradeSheetRenderContent.value = false
tradeSheetUnmountTimer = undefined
}, 350)
}
}, { immediate: true })
// marketID
const currentMarketId = computed(() => getMarketId(currentMarket.value))