优化:下单锁单优化

This commit is contained in:
ivan 2026-02-28 23:08:48 +08:00
parent a084780180
commit 1ac53ffb79
9 changed files with 204 additions and 30 deletions

View File

@ -43,6 +43,7 @@ interface TradePositionItem {
- 25%/50%/Max 按钮独立一行(`sell-shares-buttons` - 25%/50%/Max 按钮独立一行(`sell-shares-buttons`
- 整体布局更清晰:`Shares Max: 2``[输入框]``[25%][50%][Max]` - 整体布局更清晰:`Shares Max: 2``[输入框]``[25%][50%][Max]`
- 调用 market API 下单、Split、Merge - 调用 market API 下单、Split、Merge
- **submitOrder size 统一乘 1_000_000**:无论 Market Buyamount、Market Sell、Limit Buy、Limit Sellshares传入 `pmOrderPlace``size` 均先取原始值再 `Math.round(rawSize * 1_000_000)`
- **合并/拆分成功后触发事件**`mergeSuccess``splitSuccess`,父组件监听后可刷新持仓列表 - **合并/拆分成功后触发事件**`mergeSuccess``splitSuccess`,父组件监听后可刷新持仓列表
- **401 权限错误提示**:通过 `useAuthError().formatAuthError` 统一处理,未登录显示「请先登录」,已登录显示「权限不足」 - **401 权限错误提示**:通过 `useAuthError().formatAuthError` 统一处理,未登录显示「请先登录」,已登录显示「权限不足」

View File

@ -21,6 +21,8 @@ export interface ClobPositionItem {
size?: string size?: string
/** 可用份额字符串6 位小数 */ /** 可用份额字符串6 位小数 */
available?: string available?: string
/** 锁单数量字符串6 位小数;>0 表示锁单中不可卖出 */
lock?: string
/** 成本字符串6 位小数 */ /** 成本字符串6 位小数 */
cost?: string cost?: string
/** 方向Yes | No */ /** 方向Yes | No */
@ -97,6 +99,12 @@ export interface PositionDisplayItem {
iconClass?: string iconClass?: string
outcomeTag?: string outcomeTag?: string
outcomePillClass?: string outcomePillClass?: string
/** 是否锁单lock > 0锁单不可卖出 */
locked?: boolean
/** 锁单数量(来自 lock 字段),用于展示「锁单 X shares」 */
lockedSharesNum?: number
/** 可卖份额数值(来自 availableavailable > 0 表示有订单可卖,此为最大可卖份额 */
availableSharesNum?: number
} }
/** /**
@ -107,8 +115,11 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
const id = String(pos.ID ?? '') const id = String(pos.ID ?? '')
const market = pos.marketID ?? '' const market = pos.marketID ?? ''
const sizeRaw = parsePosNum(pos.size ?? pos.available) const sizeRaw = parsePosNum(pos.size ?? pos.available)
const availableRaw = parsePosNum(pos.available)
const costRaw = parsePosNum(pos.cost) const costRaw = parsePosNum(pos.cost)
const lockRaw = parsePosNum(pos.lock)
const size = sizeRaw / SCALE const size = sizeRaw / SCALE
const availableNum = availableRaw / SCALE
const costUsd = costRaw / SCALE const costUsd = costRaw / SCALE
const shares = `${size} shares` const shares = `${size} shares`
const outcomeWord = pos.outcome === 'No' ? 'No' : 'Yes' const outcomeWord = pos.outcome === 'No' ? 'No' : 'Yes'
@ -117,6 +128,8 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
const bet = value const bet = value
const toWin = `$${size.toFixed(2)}` const toWin = `$${size.toFixed(2)}`
const outcomeTag = `${outcomeWord}` const outcomeTag = `${outcomeWord}`
const locked = lockRaw > 0
const lockedSharesNum = lockRaw > 0 ? lockRaw / SCALE : undefined
return { return {
id, id,
market, market,
@ -129,5 +142,8 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
outcomeWord, outcomeWord,
outcomeTag, outcomeTag,
outcomePillClass: pillClass, outcomePillClass: pillClass,
locked,
lockedSharesNum,
availableSharesNum: availableNum >= 0 ? availableNum : undefined,
} }
} }

View File

@ -382,6 +382,7 @@
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn> <v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setMaxShares">{{ t('trade.max') }}</v-btn> <v-btn class="share-btn" @click="setMaxShares">{{ t('trade.max') }}</v-btn>
</div> </div>
<p v-if="sellSharesExceedsMax" class="shares-exceeds-max-hint">{{ t('trade.sharesExceedsMax', { max: maxAvailableShares }) }}</p>
<div v-if="activeTab === 'buy'" class="matching-info"> <div v-if="activeTab === 'buy'" class="matching-info">
<v-icon size="14">mdi-information</v-icon> <v-icon size="14">mdi-information</v-icon>
<span>20.00 matching</span> <span>20.00 matching</span>
@ -1467,6 +1468,10 @@ export interface TradeMarketPayload {
clobTokenIds?: string[] clobTokenIds?: string[]
/** 选项展示文案,如 ["Yes","No"] 或 ["Up","Down"],用于 Buy Yes/No 按钮文字 */ /** 选项展示文案,如 ["Yes","No"] 或 ["Up","Down"],用于 Buy Yes/No 按钮文字 */
outcomes?: string[] outcomes?: string[]
/** 订单簿 Yes 买单最高价(美分),市价卖出时用于计算将收到金额 */
bestBidYesCents?: number
/** 订单簿 No 买单最高价(美分),市价卖出时用于计算将收到金额 */
bestBidNoCents?: number
} }
/** 持仓展示项(由父组件传入,用于计算可合并份额) */ /** 持仓展示项(由父组件传入,用于计算可合并份额) */
@ -1478,6 +1483,8 @@ export interface TradePositionItem {
shares: string shares: string
/** 份数数值(纯数字) */ /** 份数数值(纯数字) */
sharesNum?: number sharesNum?: number
/** 是否锁单,锁单不可卖出 */
locked?: boolean
} }
const props = withDefaults( const props = withDefaults(
@ -1597,9 +1604,36 @@ function openSheet(option: 'yes' | 'no') {
sheetOpen.value = true sheetOpen.value = true
} }
// State // //
const activeTab = ref('buy') const TRADE_PREFS_KEY = 'poly-trade-prefs'
const limitType = ref('Limit') function loadTradePrefs(): { activeTab: 'buy' | 'sell'; limitType: 'Market' | 'Limit' } {
try {
const raw = localStorage.getItem(TRADE_PREFS_KEY)
if (!raw) return { activeTab: 'buy', limitType: 'Limit' }
const data = JSON.parse(raw) as { activeTab?: string; limitType?: string }
return {
activeTab: data.activeTab === 'sell' ? 'sell' : 'buy',
limitType: data.limitType === 'Market' ? 'Market' : 'Limit',
}
} catch {
return { activeTab: 'buy', limitType: 'Limit' }
}
}
function saveTradePrefs() {
try {
localStorage.setItem(
TRADE_PREFS_KEY,
JSON.stringify({ activeTab: activeTab.value, limitType: limitType.value }),
)
} catch {
//
}
}
// State props.initialTab
const prefs = loadTradePrefs()
const activeTab = ref<'buy' | 'sell'>(prefs.activeTab)
const limitType = ref<'Market' | 'Limit'>(prefs.limitType)
const expirationEnabled = ref(false) const expirationEnabled = ref(false)
const selectedOption = ref<'yes' | 'no'>(props.initialOption ?? 'no') const selectedOption = ref<'yes' | 'no'>(props.initialOption ?? 'no')
const limitPrice = ref(0.82) // 01 const limitPrice = ref(0.82) // 01
@ -1622,6 +1656,8 @@ const balance = computed(() => {
const orderLoading = ref(false) const orderLoading = ref(false)
const orderError = ref('') const orderError = ref('')
/** 当前 orderError 是否为「无可用份额可卖」(用于仅清除该提示而不清除其他错误) */
const isNoAvailableSharesError = ref(false)
// Emits // Emits
const emit = defineEmits<{ const emit = defineEmits<{
@ -1650,6 +1686,14 @@ const currentPrice = computed(() => {
}) })
const totalPrice = computed(() => { const totalPrice = computed(() => {
if (activeTab.value === 'sell' && limitType.value === 'Market') {
const bestBidCents =
selectedOption.value === 'yes'
? (props.market?.bestBidYesCents ?? 0)
: (props.market?.bestBidNoCents ?? 0)
const price = bestBidCents / 100
return (price * shares.value).toFixed(2)
}
return (limitPrice.value * shares.value).toFixed(2) return (limitPrice.value * shares.value).toFixed(2)
}) })
@ -1681,6 +1725,29 @@ const actionButtonText = computed(() => {
return tab === 'buy' ? t('trade.buyLabel', { label }) : t('trade.sellLabel', { label }) return tab === 'buy' ? t('trade.buyLabel', { label }) : t('trade.sellLabel', { label })
}) })
/** 计算当前选中选项的持仓份额 */
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)
})
function applyInitialOption(option: 'yes' | 'no') { function applyInitialOption(option: 'yes' | 'no') {
selectedOption.value = option selectedOption.value = option
syncLimitPriceFromMarket() syncLimitPriceFromMarket()
@ -1722,6 +1789,7 @@ watch(
(m) => { (m) => {
if (m) { if (m) {
orderError.value = '' orderError.value = ''
isNoAvailableSharesError.value = false
if (props.initialOption) applyInitialOption(props.initialOption) if (props.initialOption) applyInitialOption(props.initialOption)
else syncLimitPriceFromMarket() else syncLimitPriceFromMarket()
} }
@ -1735,8 +1803,26 @@ watch(
if (newTab === 'sell') { if (newTab === 'sell') {
setMaxShares() setMaxShares()
} }
saveTradePrefs()
}, },
) )
/** 卖出时无可卖份额available 为 0则显示不可卖提示 */
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) {
orderError.value = ''
isNoAvailableSharesError.value = false
}
}
},
{ immediate: true },
)
watch(limitType, () => saveTradePrefs(), { flush: 'post' })
// Methods // Methods
const handleOptionChange = (option: 'yes' | 'no') => { const handleOptionChange = (option: 'yes' | 'no') => {
@ -1837,28 +1923,10 @@ const adjustShares = (amount: number) => {
shares.value = clampShares(shares.value + amount) shares.value = clampShares(shares.value + amount)
} }
/** 计算当前选中选项的持仓份额 */ /** 卖出时输入份额是否超过最大可卖 */
const currentOptionPositionShares = computed(() => { const sellSharesExceedsMax = computed(
const positions = props.positions ?? [] () => activeTab.value === 'sell' && maxAvailableShares.value >= 0 && shares.value > maxAvailableShares.value,
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使 // Sell使
const setSharesPercentage = (percentage: number) => { const setSharesPercentage = (percentage: number) => {
@ -1969,11 +2037,13 @@ async function submitOrder() {
if (!tokenId) { if (!tokenId) {
orderError.value = t('trade.pleaseSelectMarket') orderError.value = t('trade.pleaseSelectMarket')
isNoAvailableSharesError.value = false
return return
} }
const headers = userStore.getAuthHeaders() const headers = userStore.getAuthHeaders()
if (!headers) { if (!headers) {
orderError.value = t('trade.pleaseLogin') orderError.value = t('trade.pleaseLogin')
isNoAvailableSharesError.value = false
return return
} }
const uid = userStore?.user?.ID ?? 0 const uid = userStore?.user?.ID ?? 0
@ -1985,9 +2055,24 @@ async function submitOrder() {
: 0 : 0
if (!Number.isFinite(userIdNum) || userIdNum <= 0) { if (!Number.isFinite(userIdNum) || userIdNum <= 0) {
orderError.value = t('trade.userError') orderError.value = t('trade.userError')
isNoAvailableSharesError.value = false
return return
} }
if (activeTab.value === 'sell') {
const maxShares = maxAvailableShares.value
if (!Number.isFinite(maxShares) || maxShares <= 0) {
orderError.value = t('activity.noAvailableSharesToSell')
isNoAvailableSharesError.value = true
return
}
if (shares.value > maxShares) {
orderError.value = t('trade.sharesExceedsMax', { max: maxShares })
isNoAvailableSharesError.value = false
return
}
}
const isMarket = limitType.value === 'Market' const isMarket = limitType.value === 'Market'
const orderTypeNum = isMarket const orderTypeNum = isMarket
? OrderType.Market ? OrderType.Market
@ -2000,12 +2085,14 @@ async function submitOrder() {
? parseExpirationTimestamp(expirationTime.value) ? parseExpirationTimestamp(expirationTime.value)
: 0 : 0
const sizeValue = isMarket && activeTab.value === 'buy' const rawSize = isMarket && activeTab.value === 'buy'
? Math.round(amount.value * 1_000_000) ? amount.value
: clampShares(shares.value) : clampShares(shares.value)
const sizeValue = Math.round(rawSize * 1_000_000)
orderLoading.value = true orderLoading.value = true
orderError.value = '' orderError.value = ''
isNoAvailableSharesError.value = false
try { try {
const res = await pmOrderPlace( const res = await pmOrderPlace(
{ {
@ -2682,6 +2769,12 @@ async function submitOrder() {
margin: 8px 0 0; margin: 8px 0 0;
} }
.shares-exceeds-max-hint {
font-size: 12px;
color: #b45309;
margin: 6px 0 0;
}
.merge-dialog-actions { .merge-dialog-actions {
padding: 16px 20px 20px; padding: 16px 20px 20px;
padding-top: 8px; padding-top: 8px;

View File

@ -59,6 +59,7 @@
"max": "Max", "max": "Max",
"balanceLabel": "Balance", "balanceLabel": "Balance",
"maxShares": "Max shares", "maxShares": "Max shares",
"sharesExceedsMax": "Shares cannot exceed maximum available ({max})",
"pleaseLogin": "Please log in first", "pleaseLogin": "Please log in first",
"pleaseSelectMarket": "Please select a market (with clobTokenIds)", "pleaseSelectMarket": "Please select a market (with clobTokenIds)",
"userError": "User info error", "userError": "User info error",
@ -102,6 +103,10 @@
"openOrders": "Orders", "openOrders": "Orders",
"noPositionsInMarket": "No positions in this market.", "noPositionsInMarket": "No positions in this market.",
"noOpenOrdersInMarket": "No open orders in this market.", "noOpenOrdersInMarket": "No open orders in this market.",
"positionLocked": "Locked",
"positionLockedWithAmount": "Locked {n} shares",
"positionLockedCannotSell": "This position is locked and cannot be sold.",
"noAvailableSharesToSell": "No shares available to sell.",
"cancelOrder": "Cancel", "cancelOrder": "Cancel",
"noCommentsYet": "No comments yet.", "noCommentsYet": "No comments yet.",
"topHoldersPlaceholder": "Top holders will appear here.", "topHoldersPlaceholder": "Top holders will appear here.",

View File

@ -59,6 +59,7 @@
"max": "最大", "max": "最大",
"balanceLabel": "残高", "balanceLabel": "残高",
"maxShares": "最大シェア", "maxShares": "最大シェア",
"sharesExceedsMax": "入力したシェアは最大売却可能数(最大 {max})を超えられません",
"pleaseLogin": "先にログインしてください", "pleaseLogin": "先にログインしてください",
"pleaseSelectMarket": "市場を選択してくださいclobTokenIds が必要)", "pleaseSelectMarket": "市場を選択してくださいclobTokenIds が必要)",
"userError": "ユーザー情報エラー", "userError": "ユーザー情報エラー",
@ -102,6 +103,10 @@
"openOrders": "注文", "openOrders": "注文",
"noPositionsInMarket": "この市場にポジションはありません", "noPositionsInMarket": "この市場にポジションはありません",
"noOpenOrdersInMarket": "この市場に未約定注文はありません", "noOpenOrdersInMarket": "この市場に未約定注文はありません",
"positionLocked": "ロック",
"positionLockedWithAmount": "ロック {n} シェア",
"positionLockedCannotSell": "このポジションはロック中で売却できません",
"noAvailableSharesToSell": "売却可能なシェアがありません",
"cancelOrder": "キャンセル", "cancelOrder": "キャンセル",
"noCommentsYet": "コメントはまだありません", "noCommentsYet": "コメントはまだありません",
"topHoldersPlaceholder": "持倉トップがここに表示されます", "topHoldersPlaceholder": "持倉トップがここに表示されます",

View File

@ -59,6 +59,7 @@
"max": "최대", "max": "최대",
"balanceLabel": "잔액", "balanceLabel": "잔액",
"maxShares": "최대 주식", "maxShares": "최대 주식",
"sharesExceedsMax": "입력한 수량은 최대 매도 가능 수량({max})을 초과할 수 없습니다",
"pleaseLogin": "먼저 로그인하세요", "pleaseLogin": "먼저 로그인하세요",
"pleaseSelectMarket": "시장을 선택하세요 (clobTokenIds 필요)", "pleaseSelectMarket": "시장을 선택하세요 (clobTokenIds 필요)",
"userError": "사용자 정보 오류", "userError": "사용자 정보 오류",
@ -102,6 +103,10 @@
"openOrders": "주문", "openOrders": "주문",
"noPositionsInMarket": "이 시장에 포지션이 없습니다", "noPositionsInMarket": "이 시장에 포지션이 없습니다",
"noOpenOrdersInMarket": "이 시장에 미체결 주문이 없습니다", "noOpenOrdersInMarket": "이 시장에 미체결 주문이 없습니다",
"positionLocked": "잠금",
"positionLockedWithAmount": "잠금 {n} 수량",
"positionLockedCannotSell": "이 포지션은 잠금 상태로 매도할 수 없습니다",
"noAvailableSharesToSell": "매도 가능한 수량이 없습니다",
"cancelOrder": "취소", "cancelOrder": "취소",
"noCommentsYet": "아직 댓글이 없습니다", "noCommentsYet": "아직 댓글이 없습니다",
"topHoldersPlaceholder": "보유자 순위가 여기에 표시됩니다", "topHoldersPlaceholder": "보유자 순위가 여기에 표시됩니다",

View File

@ -59,6 +59,7 @@
"max": "最大", "max": "最大",
"balanceLabel": "余额", "balanceLabel": "余额",
"maxShares": "最大份额", "maxShares": "最大份额",
"sharesExceedsMax": "输入份额不能大于最大可卖份额(最大 {max}",
"pleaseLogin": "请先登录", "pleaseLogin": "请先登录",
"pleaseSelectMarket": "请先选择市场(需包含 clobTokenIds", "pleaseSelectMarket": "请先选择市场(需包含 clobTokenIds",
"userError": "用户信息异常", "userError": "用户信息异常",
@ -102,6 +103,10 @@
"openOrders": "限价", "openOrders": "限价",
"noPositionsInMarket": "本市场暂无持仓", "noPositionsInMarket": "本市场暂无持仓",
"noOpenOrdersInMarket": "本市场暂无未成交订单", "noOpenOrdersInMarket": "本市场暂无未成交订单",
"positionLocked": "锁单",
"positionLockedWithAmount": "锁单 {n} 份",
"positionLockedCannotSell": "该持仓为锁单状态,不可卖出",
"noAvailableSharesToSell": "当前无可卖份额",
"cancelOrder": "撤单", "cancelOrder": "撤单",
"noCommentsYet": "暂无评论", "noCommentsYet": "暂无评论",
"topHoldersPlaceholder": "持仓大户将在此显示", "topHoldersPlaceholder": "持仓大户将在此显示",

View File

@ -59,6 +59,7 @@
"max": "最大", "max": "最大",
"balanceLabel": "餘額", "balanceLabel": "餘額",
"maxShares": "最大份額", "maxShares": "最大份額",
"sharesExceedsMax": "輸入份額不能大於最大可賣份額(最大 {max}",
"pleaseLogin": "請先登入", "pleaseLogin": "請先登入",
"pleaseSelectMarket": "請先選擇市場(需包含 clobTokenIds", "pleaseSelectMarket": "請先選擇市場(需包含 clobTokenIds",
"userError": "用戶資訊異常", "userError": "用戶資訊異常",
@ -102,6 +103,10 @@
"openOrders": "限價", "openOrders": "限價",
"noPositionsInMarket": "本市場暫無持倉", "noPositionsInMarket": "本市場暫無持倉",
"noOpenOrdersInMarket": "本市場暫無未成交訂單", "noOpenOrdersInMarket": "本市場暫無未成交訂單",
"positionLocked": "鎖單",
"positionLockedWithAmount": "鎖單 {n} 份",
"positionLockedCannotSell": "該持倉為鎖單狀態,不可賣出",
"noAvailableSharesToSell": "目前無可賣份額",
"cancelOrder": "撤單", "cancelOrder": "撤單",
"noCommentsYet": "暫無評論", "noCommentsYet": "暫無評論",
"topHoldersPlaceholder": "持倉大戶將在此顯示", "topHoldersPlaceholder": "持倉大戶將在此顯示",

View File

@ -64,6 +64,9 @@
> >
<div class="position-row-main"> <div class="position-row-main">
<span :class="['position-outcome-pill', pos.outcomePillClass]">{{ pos.outcomeTag }}</span> <span :class="['position-outcome-pill', pos.outcomePillClass]">{{ pos.outcomeTag }}</span>
<span v-if="pos.locked" class="position-lock-badge">
{{ pos.lockedSharesNum != null && Number.isFinite(pos.lockedSharesNum) ? t('activity.positionLockedWithAmount', { n: pos.lockedSharesNum }) : t('activity.positionLocked') }}
</span>
<span class="position-shares">{{ pos.shares }}</span> <span class="position-shares">{{ pos.shares }}</span>
<span class="position-value">{{ pos.value }}</span> <span class="position-value">{{ pos.value }}</span>
<v-btn <v-btn
@ -71,6 +74,7 @@
size="small" size="small"
color="primary" color="primary"
class="position-sell-btn" class="position-sell-btn"
:disabled="!(pos.availableSharesNum != null && pos.availableSharesNum > 0)"
@click="openSellFromPosition(pos)" @click="openSellFromPosition(pos)"
> >
{{ t('trade.sell') }} {{ t('trade.sell') }}
@ -535,6 +539,18 @@ const orderBookLowestAskNoCents = computed(() => {
if (!asks.length) return 0 if (!asks.length) return 0
return Math.min(...asks.map((a) => a.price)) return Math.min(...asks.map((a) => a.price))
}) })
/** 订单簿 Yes 买单最高价(分),无数据时为 0市价卖出时用于计算将收到金额 */
const orderBookBestBidYesCents = computed(() => {
const bids = orderBookBidsYes.value
if (!bids.length) return 0
return Math.max(...bids.map((b) => b.price))
})
/** 订单簿 No 买单最高价(分),无数据时为 0市价卖出时用于计算将收到金额 */
const orderBookBestBidNoCents = computed(() => {
const bids = orderBookBidsNo.value
if (!bids.length) return 0
return Math.max(...bids.map((b) => b.price))
})
const clobLastPriceYes = computed(() => clobLastPriceByToken.value[0]) const clobLastPriceYes = computed(() => clobLastPriceByToken.value[0])
const clobLastPriceNo = computed(() => clobLastPriceByToken.value[1]) const clobLastPriceNo = computed(() => clobLastPriceByToken.value[1])
const clobSpreadYes = computed(() => clobSpreadByToken.value[0]) const clobSpreadYes = computed(() => clobSpreadByToken.value[0])
@ -680,6 +696,8 @@ const tradeMarketPayload = computed(() => {
const m = currentMarket.value const m = currentMarket.value
const yesPrice = orderBookLowestAskYesCents.value / 100 const yesPrice = orderBookLowestAskYesCents.value / 100
const noPrice = orderBookLowestAskNoCents.value / 100 const noPrice = orderBookLowestAskNoCents.value / 100
const bestBidYesCents = orderBookBestBidYesCents.value
const bestBidNoCents = orderBookBestBidNoCents.value
if (m) { if (m) {
return { return {
marketId: getMarketId(m), marketId: getMarketId(m),
@ -688,6 +706,8 @@ const tradeMarketPayload = computed(() => {
title: m.question, title: m.question,
clobTokenIds: m.clobTokenIds, clobTokenIds: m.clobTokenIds,
outcomes: m.outcomes, outcomes: m.outcomes,
bestBidYesCents,
bestBidNoCents,
} }
} }
const qId = route.query.marketId const qId = route.query.marketId
@ -697,6 +717,8 @@ const tradeMarketPayload = computed(() => {
yesPrice, yesPrice,
noPrice, noPrice,
title: (route.query.title as string) || undefined, title: (route.query.title as string) || undefined,
bestBidYesCents,
bestBidNoCents,
} }
} }
return undefined return undefined
@ -775,8 +797,12 @@ function onSplitSuccess() {
loadMarketPositions() loadMarketPositions()
} }
/** 从持仓项点击 Sell弹出交易组件并切到 Sell、对应 Yes/No。移动端直接开底部弹窗,桌面端开 Dialog */ /** 从持仓项点击 Sell弹出交易组件并切到 Sell、对应 Yes/No。是否可卖由 available 判断available > 0 才可卖。 */
function openSellFromPosition(pos: PositionDisplayItem) { function openSellFromPosition(pos: PositionDisplayItem) {
if (pos.availableSharesNum == null || pos.availableSharesNum <= 0) {
toastStore.show(t('activity.noAvailableSharesToSell'), 'warning')
return
}
const option = pos.outcomeWord === 'No' ? 'no' : 'yes' const option = pos.outcomeWord === 'No' ? 'no' : 'yes'
if (isMobile.value) { if (isMobile.value) {
tradeInitialOptionFromBar.value = option tradeInitialOptionFromBar.value = option
@ -808,13 +834,17 @@ const marketPositionsFiltered = computed(() =>
}), }),
) )
/** 转为 TradeComponent 所需的 TradePositionItem[],保证 outcomeWord 为 'Yes' | 'No'(仅含份额>0 */ /** 转为 TradeComponent 所需的 TradePositionItem[]sharesNum 用 available最大可卖份额available > 0 表示有订单可卖 */
const tradePositionsForComponent = computed<TradePositionItem[]>(() => const tradePositionsForComponent = computed<TradePositionItem[]>(() =>
marketPositionsFiltered.value.map((p) => ({ marketPositionsFiltered.value.map((p) => ({
id: p.id, id: p.id,
outcomeWord: (p.outcomeWord === 'No' ? 'No' : 'Yes') as 'Yes' | 'No', outcomeWord: (p.outcomeWord === 'No' ? 'No' : 'Yes') as 'Yes' | 'No',
shares: p.shares, shares: p.shares,
sharesNum: parseFloat(p.shares?.replace(/[^0-9.]/g, '')) || undefined, sharesNum:
p.availableSharesNum != null && Number.isFinite(p.availableSharesNum)
? p.availableSharesNum
: parseFloat(p.shares?.replace(/[^0-9.]/g, '')) || undefined,
locked: p.locked,
})) }))
) )
@ -1867,6 +1897,15 @@ onUnmounted(() => {
margin-left: auto; margin-left: auto;
} }
.position-lock-badge {
font-size: 11px;
color: #b45309;
background: #fef3c7;
padding: 2px 6px;
border-radius: 4px;
font-weight: 500;
}
.position-outcome-pill, .position-outcome-pill,
.order-side-pill { .order-side-pill {
font-size: 12px; font-size: 12px;