Compare commits
6 Commits
a084780180
...
ff7bc3b685
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff7bc3b685 | ||
|
|
0069ae8357 | ||
|
|
fffd7461df | ||
|
|
df82732fad | ||
|
|
ff6c7a1877 | ||
|
|
1ac53ffb79 |
23
docs/api/order.md
Normal file
23
docs/api/order.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# order.ts
|
||||||
|
|
||||||
|
**路径**:`src/api/order.ts`
|
||||||
|
|
||||||
|
## 功能用途
|
||||||
|
|
||||||
|
订单相关 API:获取订单列表、取消订单,以及将 `ClobOrderItem` 映射为展示项(History、Open Orders)。
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getOrderList, mapOrderToOpenOrderItem, mapOrderToHistoryItem, cancelOrder } from '@/api/order'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据单位约定
|
||||||
|
|
||||||
|
- **price**:整数,已乘 10000(bps),`priceCents = price / 100`
|
||||||
|
- **originalSize / sizeMatched**:按 6 位小数传(1_000_000 = 1 share),展示时除以 `ORDER_SIZE_SCALE` 转为实际份额
|
||||||
|
|
||||||
|
## 扩展方式
|
||||||
|
|
||||||
|
1. 新增订单状态或筛选参数时,更新 `GetOrderListParams` 与 `getOrderList`
|
||||||
|
2. 展示格式变更时,调整 `mapOrderToOpenOrderItem`、`mapOrderToHistoryItem` 的格式化逻辑
|
||||||
@ -37,12 +37,14 @@ interface TradePositionItem {
|
|||||||
- 事件处理:`onAmountInput`、`onAmountKeydown`、`onAmountPaste`
|
- 事件处理:`onAmountInput`、`onAmountKeydown`、`onAmountPaste`
|
||||||
- 余额不足时 Buy 显示 Deposit 按钮
|
- 余额不足时 Buy 显示 Deposit 按钮
|
||||||
- 25%/50%/Max 快捷份额
|
- 25%/50%/Max 快捷份额
|
||||||
|
- **Sell 模式份额默认取最大可卖**:切换至 Sell 或 maxAvailableShares 变化时自动调用 `setMaxShares()`,将 shares 设为最大可卖数量(无可卖时为 0)
|
||||||
- **Sell 模式 UI 优化**:
|
- **Sell 模式 UI 优化**:
|
||||||
- Shares 标签与 Max shares 提示同行显示(`max-shares-inline`)
|
- Shares 标签与 Max shares 提示同行显示(`max-shares-inline`)
|
||||||
- 输入框独占一行(`shares-input-wrapper`)
|
- 输入框独占一行(`shares-input-wrapper`)
|
||||||
- 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 Buy(amount)、Market Sell、Limit Buy、Limit Sell(shares),传入 `pmOrderPlace` 的 `size` 均先取原始值再 `Math.round(rawSize * 1_000_000)`
|
||||||
- **合并/拆分成功后触发事件**:`mergeSuccess`、`splitSuccess`,父组件监听后可刷新持仓列表
|
- **合并/拆分成功后触发事件**:`mergeSuccess`、`splitSuccess`,父组件监听后可刷新持仓列表
|
||||||
- **401 权限错误提示**:通过 `useAuthError().formatAuthError` 统一处理,未登录显示「请先登录」,已登录显示「权限不足」
|
- **401 权限错误提示**:通过 `useAuthError().formatAuthError` 统一处理,未登录显示「请先登录」,已登录显示「权限不足」
|
||||||
|
|
||||||
|
|||||||
@ -10,13 +10,15 @@
|
|||||||
## 核心能力
|
## 核心能力
|
||||||
|
|
||||||
- 分时图:ECharts 渲染,支持 Past、时间粒度切换
|
- 分时图:ECharts 渲染,支持 Past、时间粒度切换
|
||||||
- 订单簿:`OrderBook` 组件,通过 **ClobSdk** 对接 CLOB WebSocket 实时数据(全量快照、增量更新、成交推送)
|
- 订单簿:`OrderBook` 组件,通过 **ClobSdk** 对接 CLOB WebSocket 实时数据(全量快照、增量更新、成交推送);份额接口按 6 位小数传(1_000_000 = 1 share),`priceSizeToRows` 与 `mergeDelta` 会将 raw 值除以 `ORDER_BOOK_SIZE_SCALE` 转为展示值
|
||||||
- 交易:`TradeComponent`,传入 `market`、`initialOption`、`positions`(持仓数据)
|
- 交易:`TradeComponent`,传入 `market`、`initialOption`、`positions`(持仓数据)
|
||||||
- 持仓列表:通过 `getPositionList` 获取当前市场持仓,传递给 `TradeComponent` 用于计算可合并份额
|
- 持仓列表:通过 `getPositionList` 获取当前市场持仓,传递给 `TradeComponent` 用于计算可合并份额
|
||||||
- 限价订单:通过 `getOrderList` 获取当前市场未成交限价单,支持撤单
|
- 限价订单:通过 `getOrderList` 获取当前市场未成交限价单,支持撤单
|
||||||
- 移动端:底部栏 + `v-bottom-sheet` 嵌入 `TradeComponent`
|
- 移动端:底部栏 + `v-bottom-sheet` 嵌入 `TradeComponent`
|
||||||
- Merge/Split:通过 `TradeComponent` 或底部菜单触发,成功后监听 `mergeSuccess`/`splitSuccess` 事件刷新持仓
|
- Merge/Split:通过 `TradeComponent` 或底部菜单触发,成功后监听 `mergeSuccess`/`splitSuccess` 事件刷新持仓
|
||||||
- **401 权限错误**:加载详情失败时,通过 `useAuthError().formatAuthError` 统一提示「请先登录」或「权限不足」
|
- **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」警告
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Vite App</title>
|
<title>TestMarket</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@ -126,6 +126,9 @@ export interface HistoryDisplayItem {
|
|||||||
/** Side: Buy=1, Sell=2 */
|
/** Side: Buy=1, Sell=2 */
|
||||||
const Side = { Buy: 1, Sell: 2 } as const
|
const Side = { Buy: 1, Sell: 2 } as const
|
||||||
|
|
||||||
|
/** 订单份额接口按 6 位小数传(1_000_000 = 1 share),需除以该系数转为展示值 */
|
||||||
|
const ORDER_SIZE_SCALE = 1_000_000
|
||||||
|
|
||||||
function formatTimeAgo(createdAt: string | undefined): string {
|
function formatTimeAgo(createdAt: string | undefined): string {
|
||||||
if (!createdAt) return ''
|
if (!createdAt) return ''
|
||||||
const d = new Date(createdAt)
|
const d = new Date(createdAt)
|
||||||
@ -140,7 +143,7 @@ function formatTimeAgo(createdAt: string | undefined): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 ClobOrderItem 映射为钱包 History 展示项
|
* 将 ClobOrderItem 映射为钱包 History 展示项
|
||||||
* price 为整数(已乘 10000),sizeMatched 为已成交份额
|
* price 为整数(已乘 10000),size 按 6 位小数传(1_000_000 = 1 share)
|
||||||
*/
|
*/
|
||||||
export function mapOrderToHistoryItem(order: ClobOrderItem): HistoryDisplayItem {
|
export function mapOrderToHistoryItem(order: ClobOrderItem): HistoryDisplayItem {
|
||||||
const id = String(order.ID ?? '')
|
const id = String(order.ID ?? '')
|
||||||
@ -151,11 +154,12 @@ export function mapOrderToHistoryItem(order: ClobOrderItem): HistoryDisplayItem
|
|||||||
const activity = `${sideLabel} ${outcome}`
|
const activity = `${sideLabel} ${outcome}`
|
||||||
const priceBps = order.price ?? 0
|
const priceBps = order.price ?? 0
|
||||||
const priceCents = Math.round(priceBps / 100)
|
const priceCents = Math.round(priceBps / 100)
|
||||||
const size = order.sizeMatched ?? order.originalSize ?? 0
|
const sizeRaw = order.sizeMatched ?? order.originalSize ?? 0
|
||||||
|
const size = sizeRaw / ORDER_SIZE_SCALE
|
||||||
const valueUsd = (priceBps / 10000) * size
|
const valueUsd = (priceBps / 10000) * size
|
||||||
const value = `$${valueUsd.toFixed(2)}`
|
const value = `$${valueUsd.toFixed(2)}`
|
||||||
const verb = sideNum === Side.Sell ? 'Sold' : 'Bought'
|
const verb = sideNum === Side.Sell ? 'Sold' : 'Bought'
|
||||||
const activityDetail = `${verb} ${size} ${outcome} at ${priceCents}¢`
|
const activityDetail = `${verb} ${Math.floor(size)} ${outcome} at ${priceCents}¢`
|
||||||
const avgPrice = `${priceCents}¢`
|
const avgPrice = `${priceCents}¢`
|
||||||
const timeAgo = formatTimeAgo(order.createdAt)
|
const timeAgo = formatTimeAgo(order.createdAt)
|
||||||
return {
|
return {
|
||||||
@ -169,7 +173,7 @@ export function mapOrderToHistoryItem(order: ClobOrderItem): HistoryDisplayItem
|
|||||||
profitLossNegative: false,
|
profitLossNegative: false,
|
||||||
timeAgo,
|
timeAgo,
|
||||||
avgPrice,
|
avgPrice,
|
||||||
shares: String(size),
|
shares: String(Math.floor(size)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,6 +191,8 @@ export interface OpenOrderDisplayItem {
|
|||||||
filledDisplay?: string
|
filledDisplay?: string
|
||||||
orderID?: number
|
orderID?: number
|
||||||
tokenID?: string
|
tokenID?: string
|
||||||
|
/** 已成交数量达到原始总数量,不可撤单 */
|
||||||
|
fullyFilled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** OrderType GTC=0 表示 Until Cancelled */
|
/** OrderType GTC=0 表示 Until Cancelled */
|
||||||
@ -194,7 +200,7 @@ const OrderType = { GTC: 0, GTD: 1 } as const
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 ClobOrderItem 映射为钱包 Open Orders 展示项(未成交订单)
|
* 将 ClobOrderItem 映射为钱包 Open Orders 展示项(未成交订单)
|
||||||
* price 为整数(已乘 10000)
|
* price 为整数(已乘 10000),originalSize/sizeMatched 按 6 位小数传(1_000_000 = 1 share)
|
||||||
*/
|
*/
|
||||||
export function mapOrderToOpenOrderItem(order: ClobOrderItem): OpenOrderDisplayItem {
|
export function mapOrderToOpenOrderItem(order: ClobOrderItem): OpenOrderDisplayItem {
|
||||||
const id = String(order.ID ?? '')
|
const id = String(order.ID ?? '')
|
||||||
@ -205,14 +211,17 @@ export function mapOrderToOpenOrderItem(order: ClobOrderItem): OpenOrderDisplayI
|
|||||||
const priceBps = order.price ?? 0
|
const priceBps = order.price ?? 0
|
||||||
const priceCents = Math.round(priceBps / 100)
|
const priceCents = Math.round(priceBps / 100)
|
||||||
const price = `${priceCents}¢`
|
const price = `${priceCents}¢`
|
||||||
const originalSize = order.originalSize ?? 0
|
const originalSizeRaw = order.originalSize ?? 0
|
||||||
const sizeMatched = order.sizeMatched ?? 0
|
const sizeMatchedRaw = order.sizeMatched ?? 0
|
||||||
const filled = `${sizeMatched}/${originalSize}`
|
const originalSize = originalSizeRaw / ORDER_SIZE_SCALE
|
||||||
|
const sizeMatched = sizeMatchedRaw / ORDER_SIZE_SCALE
|
||||||
|
const filled = `${Math.floor(sizeMatched)}/${Math.floor(originalSize)}`
|
||||||
const totalUsd = (priceBps / 10000) * originalSize
|
const totalUsd = (priceBps / 10000) * originalSize
|
||||||
const total = `$${totalUsd.toFixed(2)}`
|
const total = `$${totalUsd.toFixed(2)}`
|
||||||
const expiration =
|
const expiration =
|
||||||
order.orderType === OrderType.GTC ? 'Until Cancelled' : order.expiration?.toString() ?? ''
|
order.orderType === OrderType.GTC ? 'Until Cancelled' : order.expiration?.toString() ?? ''
|
||||||
const actionLabel = sideNum === Side.Buy ? `Buy ${outcome}` : `Sell ${outcome}`
|
const actionLabel = sideNum === Side.Buy ? `Buy ${outcome}` : `Sell ${outcome}`
|
||||||
|
const fullyFilled = originalSize <= 0 || sizeMatched >= originalSize
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
market,
|
market,
|
||||||
@ -226,5 +235,6 @@ export function mapOrderToOpenOrderItem(order: ClobOrderItem): OpenOrderDisplayI
|
|||||||
filledDisplay: filled,
|
filledDisplay: filled,
|
||||||
orderID: order.ID,
|
orderID: order.ID,
|
||||||
tokenID: order.assetID,
|
tokenID: order.assetID,
|
||||||
|
fullyFilled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
/** 可卖份额数值(来自 available),available > 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,9 +43,9 @@
|
|||||||
:trackStyle="{ backgroundColor: 'transparent' }"
|
:trackStyle="{ backgroundColor: 'transparent' }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-price asks-price">{{ ask.price }}¢</div>
|
<div class="order-price asks-price">{{ formatOrderBookPrice(ask) }}¢</div>
|
||||||
<div class="order-shares">{{ ask.shares.toFixed(2) }}</div>
|
<div class="order-shares">{{ trimTrailingZeros(ask.shares.toFixed(2)) }}</div>
|
||||||
<div class="order-total">{{ ask.cumulativeTotal.toFixed(2) }}</div>
|
<div class="order-total">{{ trimTrailingZeros(ask.cumulativeTotal.toFixed(4)) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Bids Orders -->
|
<!-- Bids Orders -->
|
||||||
<div class="asks-label">{{ t('trade.orderBookAsks') }}</div>
|
<div class="asks-label">{{ t('trade.orderBookAsks') }}</div>
|
||||||
@ -63,9 +63,9 @@
|
|||||||
:trackStyle="{ backgroundColor: 'transparent' }"
|
:trackStyle="{ backgroundColor: 'transparent' }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-price bids-price">{{ bid.price }}¢</div>
|
<div class="order-price bids-price">{{ formatOrderBookPrice(bid) }}¢</div>
|
||||||
<div class="order-shares">{{ bid.shares.toFixed(2) }}</div>
|
<div class="order-shares">{{ trimTrailingZeros(bid.shares.toFixed(2)) }}</div>
|
||||||
<div class="order-total">{{ bid.cumulativeTotal.toFixed(2) }}</div>
|
<div class="order-total">{{ trimTrailingZeros(bid.cumulativeTotal.toFixed(4)) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -93,8 +93,12 @@ import { USE_MOCK_ORDER_BOOK } from '../config/mock'
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
export interface OrderBookRow {
|
export interface OrderBookRow {
|
||||||
|
/** 价格(美分),用于展示 */
|
||||||
price: number
|
price: number
|
||||||
|
/** 份额数量 */
|
||||||
shares: number
|
shares: number
|
||||||
|
/** 原始精度单价(美元/份),用于计算总价,避免舍入误差 */
|
||||||
|
priceRaw?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@ -223,6 +227,21 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** 去掉小数点后多余的 0(如 1.00 → 1,0.10 → 0.1) */
|
||||||
|
function trimTrailingZeros(s: string): string {
|
||||||
|
return s.replace(/\.?0+$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订单簿价格展示:小于 1 美分时最多显示两位小数,使用原始精度 priceRaw;小数点后全为 0 则不显示 */
|
||||||
|
function formatOrderBookPrice(row: OrderBookRow): string {
|
||||||
|
const cents =
|
||||||
|
row.priceRaw != null && Number.isFinite(row.priceRaw) ? row.priceRaw * 100 : row.price
|
||||||
|
if (!Number.isFinite(cents)) return '0'
|
||||||
|
if (cents < 1) return trimTrailingZeros(cents.toFixed(2))
|
||||||
|
if (cents < 100) return trimTrailingZeros(cents.toFixed(1))
|
||||||
|
return String(Math.round(cents))
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate cumulative total for asks
|
// Calculate cumulative total for asks
|
||||||
const asksWithCumulativeTotal = computed(() => {
|
const asksWithCumulativeTotal = computed(() => {
|
||||||
let cumulativeTotal = 0
|
let cumulativeTotal = 0
|
||||||
@ -231,8 +250,10 @@ const asksWithCumulativeTotal = computed(() => {
|
|||||||
|
|
||||||
return sortedAsks
|
return sortedAsks
|
||||||
.map((ask) => {
|
.map((ask) => {
|
||||||
// Calculate current ask's value
|
// 总价用原始精度单价(美元)计算,无则回退为 price/100
|
||||||
const askValue = (ask.price * ask.shares) / 100 // Convert cents to dollars
|
const priceDollars =
|
||||||
|
ask.priceRaw != null && Number.isFinite(ask.priceRaw) ? ask.priceRaw : ask.price / 100
|
||||||
|
const askValue = priceDollars * ask.shares
|
||||||
cumulativeTotal += askValue
|
cumulativeTotal += askValue
|
||||||
return {
|
return {
|
||||||
...ask,
|
...ask,
|
||||||
@ -249,8 +270,10 @@ const bidsWithCumulativeTotal = computed(() => {
|
|||||||
const sortedBids = [...bids.value].sort((a, b) => b.price - a.price)
|
const sortedBids = [...bids.value].sort((a, b) => b.price - a.price)
|
||||||
|
|
||||||
return sortedBids.map((bid) => {
|
return sortedBids.map((bid) => {
|
||||||
// Calculate current bid's value
|
// 总价用原始精度单价(美元)计算,无则回退为 price/100
|
||||||
const bidValue = (bid.price * bid.shares) / 100 // Convert cents to dollars
|
const priceDollars =
|
||||||
|
bid.priceRaw != null && Number.isFinite(bid.priceRaw) ? bid.priceRaw : bid.price / 100
|
||||||
|
const bidValue = priceDollars * bid.shares
|
||||||
cumulativeTotal += bidValue
|
cumulativeTotal += bidValue
|
||||||
return {
|
return {
|
||||||
...bid,
|
...bid,
|
||||||
|
|||||||
@ -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) // 内部存储 0–1,显示为美分与按钮一致
|
const limitPrice = ref(0.82) // 内部存储 0–1,显示为美分与按钮一致
|
||||||
@ -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<{
|
||||||
@ -1644,19 +1680,35 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
/** Limit Price 显示值(美分),与 Yes/No 按钮单位一致 */
|
/** Limit Price 显示值(美分),与 Yes/No 按钮单位一致 */
|
||||||
|
/** 去掉小数点后多余的 0(如 1.00 → 1,0.10 → 0.1) */
|
||||||
|
function trimTrailingZeros(s: string): string {
|
||||||
|
return s.replace(/\.?0+$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
const limitPriceCentsDisplay = computed(() => Math.round(limitPrice.value * 10000) / 100)
|
const limitPriceCentsDisplay = computed(() => Math.round(limitPrice.value * 10000) / 100)
|
||||||
const currentPrice = computed(() => {
|
const currentPrice = computed(() => {
|
||||||
return `${(limitPrice.value * 100).toFixed(0)}¢`
|
return `${trimTrailingZeros((limitPrice.value * 100).toFixed(0))}¢`
|
||||||
})
|
})
|
||||||
|
|
||||||
const totalPrice = computed(() => {
|
const totalPrice = computed(() => {
|
||||||
return (limitPrice.value * shares.value).toFixed(2)
|
let raw: number
|
||||||
|
if (activeTab.value === 'sell' && limitType.value === 'Market') {
|
||||||
|
const bestBidCents =
|
||||||
|
selectedOption.value === 'yes'
|
||||||
|
? (props.market?.bestBidYesCents ?? 0)
|
||||||
|
: (props.market?.bestBidNoCents ?? 0)
|
||||||
|
raw = (bestBidCents / 100) * shares.value
|
||||||
|
} else {
|
||||||
|
raw = limitPrice.value * shares.value
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(raw)) return '0'
|
||||||
|
return trimTrailingZeros(raw.toFixed(4))
|
||||||
})
|
})
|
||||||
|
|
||||||
/** Sell 模式下的平均单价(¢) */
|
/** Sell 模式下的平均单价(¢) */
|
||||||
const avgPriceCents = computed(() => {
|
const avgPriceCents = computed(() => {
|
||||||
const p = limitPrice.value
|
const p = limitPrice.value
|
||||||
return (p * 100).toFixed(1)
|
return trimTrailingZeros((p * 100).toFixed(1))
|
||||||
})
|
})
|
||||||
|
|
||||||
/** To win = 份数 × 1U = shares × 1 USDC */
|
/** To win = 份数 × 1U = shares × 1 USDC */
|
||||||
@ -1667,9 +1719,9 @@ const toWinValue = computed(() => {
|
|||||||
? (props.market?.yesPrice ?? 0.5)
|
? (props.market?.yesPrice ?? 0.5)
|
||||||
: (props.market?.noPrice ?? 0.5)
|
: (props.market?.noPrice ?? 0.5)
|
||||||
const sharesFromAmount = price > 0 ? amount.value / price : 0
|
const sharesFromAmount = price > 0 ? amount.value / price : 0
|
||||||
return sharesFromAmount.toFixed(2)
|
return trimTrailingZeros(sharesFromAmount.toFixed(2))
|
||||||
}
|
}
|
||||||
return (shares.value * 1).toFixed(2)
|
return trimTrailingZeros((shares.value * 1).toFixed(2))
|
||||||
})
|
})
|
||||||
|
|
||||||
const limitTypeDisplay = computed(() =>
|
const limitTypeDisplay = computed(() =>
|
||||||
@ -1681,6 +1733,41 @@ 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)
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 将 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') {
|
function applyInitialOption(option: 'yes' | 'no') {
|
||||||
selectedOption.value = option
|
selectedOption.value = option
|
||||||
syncLimitPriceFromMarket()
|
syncLimitPriceFromMarket()
|
||||||
@ -1702,6 +1789,7 @@ onMounted(() => {
|
|||||||
if (props.initialOption) applyInitialOption(props.initialOption)
|
if (props.initialOption) applyInitialOption(props.initialOption)
|
||||||
else if (props.market) syncLimitPriceFromMarket()
|
else if (props.market) syncLimitPriceFromMarket()
|
||||||
if (props.initialTab) activeTab.value = props.initialTab
|
if (props.initialTab) activeTab.value = props.initialTab
|
||||||
|
if (activeTab.value === 'sell') setMaxShares()
|
||||||
})
|
})
|
||||||
watch(
|
watch(
|
||||||
() => props.initialOption,
|
() => props.initialOption,
|
||||||
@ -1722,6 +1810,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 +1824,30 @@ watch(
|
|||||||
if (newTab === 'sell') {
|
if (newTab === 'sell') {
|
||||||
setMaxShares()
|
setMaxShares()
|
||||||
}
|
}
|
||||||
|
saveTradePrefs()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
/** 卖出时无可卖份额(available 为 0)则显示不可卖提示 */
|
||||||
|
watch(
|
||||||
|
() => [activeTab.value, maxAvailableShares.value] as const,
|
||||||
|
([tab, max]) => {
|
||||||
|
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 },
|
||||||
|
)
|
||||||
|
watch(limitType, () => saveTradePrefs(), { flush: 'post' })
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const handleOptionChange = (option: 'yes' | 'no') => {
|
const handleOptionChange = (option: 'yes' | 'no') => {
|
||||||
@ -1799,12 +1910,6 @@ const increasePrice = () => {
|
|||||||
limitPrice.value = ALLOWED_LIMIT_PRICES[nextIdx] ?? limitPrice.value
|
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 */
|
/** 仅在值为正整数时更新 shares */
|
||||||
function onSharesInput(v: unknown) {
|
function onSharesInput(v: unknown) {
|
||||||
const num = v == null ? NaN : Number(v)
|
const num = v == null ? NaN : Number(v)
|
||||||
@ -1837,28 +1942,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) => {
|
||||||
@ -1911,14 +1998,6 @@ const setMaxAmount = () => {
|
|||||||
amount.value = balance.value
|
amount.value = balance.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置最大份额(基于当前选项的持仓)
|
|
||||||
const setMaxShares = () => {
|
|
||||||
const maxShares = currentOptionPositionShares.value
|
|
||||||
if (maxShares > 0) {
|
|
||||||
shares.value = clampShares(maxShares)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Buy 模式:余额是否足够(>= 所需金额且不为 0)。cost:balance>0 时用 totalPrice,否则用 amount */
|
/** Buy 模式:余额是否足够(>= 所需金额且不为 0)。cost:balance>0 时用 totalPrice,否则用 amount */
|
||||||
const canAffordBuy = computed(() => {
|
const canAffordBuy = computed(() => {
|
||||||
const bal = balance.value
|
const bal = balance.value
|
||||||
@ -1969,11 +2048,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 +2066,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 +2096,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 +2780,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;
|
||||||
|
|||||||
@ -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.",
|
||||||
|
|||||||
@ -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": "持倉トップがここに表示されます",
|
||||||
|
|||||||
@ -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": "보유자 순위가 여기에 표시됩니다",
|
||||||
|
|||||||
@ -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": "持仓大户将在此显示",
|
||||||
|
|||||||
@ -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": "持倉大戶將在此顯示",
|
||||||
|
|||||||
@ -192,6 +192,7 @@
|
|||||||
<template v-if="isMobile && markets.length > 0">
|
<template v-if="isMobile && markets.length > 0">
|
||||||
<v-bottom-sheet v-model="tradeSheetOpen" content-class="event-markets-trade-sheet">
|
<v-bottom-sheet v-model="tradeSheetOpen" content-class="event-markets-trade-sheet">
|
||||||
<TradeComponent
|
<TradeComponent
|
||||||
|
v-if="tradeSheetRenderContent"
|
||||||
ref="tradeComponentRef"
|
ref="tradeComponentRef"
|
||||||
:key="`trade-${selectedMarketIndex}-${tradeInitialOption}`"
|
:key="`trade-${selectedMarketIndex}-${tradeInitialOption}`"
|
||||||
:market="tradeMarketPayload"
|
:market="tradeMarketPayload"
|
||||||
@ -245,6 +246,10 @@ const selectedMarketIndex = ref(0)
|
|||||||
const tradeInitialOption = ref<'yes' | 'no' | undefined>(undefined)
|
const tradeInitialOption = ref<'yes' | 'no' | undefined>(undefined)
|
||||||
/** 移动端交易弹窗开关 */
|
/** 移动端交易弹窗开关 */
|
||||||
const tradeSheetOpen = ref(false)
|
const tradeSheetOpen = ref(false)
|
||||||
|
/** 控制底部栏内 TradeComponent 的渲染,延迟挂载以避免 slot 竞态警告 */
|
||||||
|
const tradeSheetRenderContent = ref(false)
|
||||||
|
/** 从三点菜单点击 Merge/Split 时待打开的弹窗,等 TradeComponent 挂载后执行 */
|
||||||
|
const pendingMergeSplitDialog = ref<'merge' | 'split' | null>(null)
|
||||||
/** 移动端底部栏三点菜单开关 */
|
/** 移动端底部栏三点菜单开关 */
|
||||||
const mobileMenuOpen = ref(false)
|
const mobileMenuOpen = ref(false)
|
||||||
/** TradeComponent 引用,用于从底部栏触发 Merge/Split */
|
/** TradeComponent 引用,用于从底部栏触发 Merge/Split */
|
||||||
@ -580,19 +585,15 @@ function openSheetWithOption(side: 'yes' | 'no') {
|
|||||||
/** 从底部栏三点菜单打开 Merge 弹窗 */
|
/** 从底部栏三点菜单打开 Merge 弹窗 */
|
||||||
function openMergeFromBar() {
|
function openMergeFromBar() {
|
||||||
mobileMenuOpen.value = false
|
mobileMenuOpen.value = false
|
||||||
|
pendingMergeSplitDialog.value = 'merge'
|
||||||
tradeSheetOpen.value = true
|
tradeSheetOpen.value = true
|
||||||
nextTick(() => {
|
|
||||||
tradeComponentRef.value?.openMergeDialog?.()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从底部栏三点菜单打开 Split 弹窗 */
|
/** 从底部栏三点菜单打开 Split 弹窗 */
|
||||||
function openSplitFromBar() {
|
function openSplitFromBar() {
|
||||||
mobileMenuOpen.value = false
|
mobileMenuOpen.value = false
|
||||||
|
pendingMergeSplitDialog.value = 'split'
|
||||||
tradeSheetOpen.value = true
|
tradeSheetOpen.value = true
|
||||||
nextTick(() => {
|
|
||||||
tradeComponentRef.value?.openSplitDialog?.()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTradeSubmit(payload: {
|
function onTradeSubmit(payload: {
|
||||||
@ -714,11 +715,49 @@ onMounted(() => {
|
|||||||
loadEventDetail()
|
loadEventDetail()
|
||||||
window.addEventListener('resize', handleResize)
|
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
|
||||||
|
nextTick(() => {
|
||||||
|
const pending = pendingMergeSplitDialog.value
|
||||||
|
if (pending === 'merge') {
|
||||||
|
pendingMergeSplitDialog.value = null
|
||||||
|
tradeComponentRef.value?.openMergeDialog?.()
|
||||||
|
} else if (pending === 'split') {
|
||||||
|
pendingMergeSplitDialog.value = null
|
||||||
|
tradeComponentRef.value?.openSplitDialog?.()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 50)
|
||||||
|
} else {
|
||||||
|
pendingMergeSplitDialog.value = null
|
||||||
|
if (tradeSheetMountTimer) {
|
||||||
|
clearTimeout(tradeSheetMountTimer)
|
||||||
|
tradeSheetMountTimer = undefined
|
||||||
|
}
|
||||||
|
tradeSheetUnmountTimer = setTimeout(() => {
|
||||||
|
tradeSheetRenderContent.value = false
|
||||||
|
tradeSheetUnmountTimer = undefined
|
||||||
|
}, 350)
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopDynamicUpdate()
|
stopDynamicUpdate()
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
chartInstance?.dispose()
|
chartInstance?.dispose()
|
||||||
chartInstance = null
|
chartInstance = null
|
||||||
|
if (tradeSheetUnmountTimer) clearTimeout(tradeSheetUnmountTimer)
|
||||||
|
if (tradeSheetMountTimer) clearTimeout(tradeSheetMountTimer)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@ -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') }}
|
||||||
@ -104,7 +108,7 @@
|
|||||||
variant="text"
|
variant="text"
|
||||||
size="small"
|
size="small"
|
||||||
color="error"
|
color="error"
|
||||||
:disabled="cancelOrderLoading"
|
:disabled="cancelOrderLoading || ord.fullyFilled"
|
||||||
@click="cancelMarketOrder(ord)"
|
@click="cancelMarketOrder(ord)"
|
||||||
>
|
>
|
||||||
{{ t('activity.cancelOrder') }}
|
{{ t('activity.cancelOrder') }}
|
||||||
@ -286,6 +290,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<v-bottom-sheet v-model="tradeSheetOpen" content-class="trade-detail-trade-sheet">
|
<v-bottom-sheet v-model="tradeSheetOpen" content-class="trade-detail-trade-sheet">
|
||||||
<TradeComponent
|
<TradeComponent
|
||||||
|
v-if="tradeSheetRenderContent"
|
||||||
ref="mobileTradeComponentRef"
|
ref="mobileTradeComponentRef"
|
||||||
:market="tradeMarketPayload"
|
:market="tradeMarketPayload"
|
||||||
:initial-option="tradeInitialOptionFromBar"
|
:initial-option="tradeInitialOptionFromBar"
|
||||||
@ -307,7 +312,7 @@
|
|||||||
transition="dialog-transition"
|
transition="dialog-transition"
|
||||||
>
|
>
|
||||||
<TradeComponent
|
<TradeComponent
|
||||||
v-if="sellDialogOpen"
|
v-if="sellDialogRenderContent"
|
||||||
:market="tradeMarketPayload"
|
:market="tradeMarketPayload"
|
||||||
:initial-option="sellInitialOption"
|
:initial-option="sellInitialOption"
|
||||||
:initial-tab="'sell'"
|
:initial-tab="'sell'"
|
||||||
@ -505,7 +510,7 @@ const currentMarket = computed(() => {
|
|||||||
|
|
||||||
// --- CLOB WebSocket 订单簿与成交 ---
|
// --- CLOB WebSocket 订单簿与成交 ---
|
||||||
// 按 token 索引区分:0 = Yes,1 = No
|
// 按 token 索引区分:0 = Yes,1 = No
|
||||||
type OrderBookRows = { price: number; shares: number }[]
|
type OrderBookRows = { price: number; shares: number; priceRaw?: number }[]
|
||||||
const clobSdkRef = ref<ClobSdk | null>(null)
|
const clobSdkRef = ref<ClobSdk | null>(null)
|
||||||
const orderBookByToken = ref<Record<number, { asks: OrderBookRows; bids: OrderBookRows }>>({
|
const orderBookByToken = ref<Record<number, { asks: OrderBookRows; bids: OrderBookRows }>>({
|
||||||
0: { asks: [], bids: [] },
|
0: { asks: [], bids: [] },
|
||||||
@ -535,19 +540,41 @@ 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])
|
||||||
const clobSpreadNo = computed(() => clobSpreadByToken.value[1])
|
const clobSpreadNo = computed(() => clobSpreadByToken.value[1])
|
||||||
|
|
||||||
|
/** 订单簿份额接口按 6 位小数传(1_000_000 = 1 share),需除以该系数转为展示值 */
|
||||||
|
const ORDER_BOOK_SIZE_SCALE = 1_000_000
|
||||||
|
|
||||||
|
/** 接口 key p 为价格(与 price 美分同源,除以 100 为美分);priceRaw 为美元单价保留原始精度供订单簿算总价 */
|
||||||
function priceSizeToRows(record: Record<string, number> | undefined): OrderBookRows {
|
function priceSizeToRows(record: Record<string, number> | undefined): OrderBookRows {
|
||||||
if (!record) return []
|
if (!record) return []
|
||||||
return Object.entries(record)
|
return Object.entries(record)
|
||||||
.filter(([, shares]) => shares > 0)
|
.filter(([, rawShares]) => rawShares > 0)
|
||||||
.map(([p, shares]) => ({
|
.map(([p, rawShares]) => {
|
||||||
price: Math.round(parseFloat(p) / 100),
|
const numP = parseFloat(p)
|
||||||
shares,
|
const priceCents = Math.round(numP / 100)
|
||||||
}))
|
const priceRawDollars = Number.isFinite(numP) ? numP / 10000 : priceCents / 100
|
||||||
|
return {
|
||||||
|
price: priceCents,
|
||||||
|
shares: rawShares / ORDER_BOOK_SIZE_SCALE,
|
||||||
|
priceRaw: priceRawDollars,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTokenIndex(msg: PriceSizePolyMsg): number {
|
function getTokenIndex(msg: PriceSizePolyMsg): number {
|
||||||
@ -579,16 +606,24 @@ function applyPriceSizeDelta(msg: PriceSizePolyMsg) {
|
|||||||
delta: Record<string, number> | undefined,
|
delta: Record<string, number> | undefined,
|
||||||
asc: boolean,
|
asc: boolean,
|
||||||
) => {
|
) => {
|
||||||
const map = new Map(current.map((r) => [r.price, r.shares]))
|
const map = new Map<string, { price: number; shares: number; priceRaw: number }>()
|
||||||
|
const keyOf = (price: number, priceRaw: number) => priceRaw.toFixed(8)
|
||||||
|
current.forEach((r) => {
|
||||||
|
const pr = r.priceRaw ?? r.price / 100
|
||||||
|
map.set(keyOf(r.price, pr), { price: r.price, shares: r.shares, priceRaw: pr })
|
||||||
|
})
|
||||||
if (delta) {
|
if (delta) {
|
||||||
Object.entries(delta).forEach(([p, shares]) => {
|
Object.entries(delta).forEach(([p, rawShares]) => {
|
||||||
const price = Math.round(parseFloat(p) / 100)
|
const numP = parseFloat(p)
|
||||||
if (shares <= 0) map.delete(price)
|
const price = Math.round(numP / 100)
|
||||||
else map.set(price, shares)
|
const priceRaw = Number.isFinite(numP) ? numP / 10000 : price / 100
|
||||||
|
const shares = rawShares / ORDER_BOOK_SIZE_SCALE
|
||||||
|
const key = keyOf(price, priceRaw)
|
||||||
|
if (shares <= 0) map.delete(key)
|
||||||
|
else map.set(key, { price, shares, priceRaw })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return Array.from(map.entries())
|
return Array.from(map.values())
|
||||||
.map(([price, shares]) => ({ price, shares }))
|
|
||||||
.sort((a, b) => (asc ? a.price - b.price : b.price - a.price))
|
.sort((a, b) => (asc ? a.price - b.price : b.price - a.price))
|
||||||
}
|
}
|
||||||
const prev = orderBookByToken.value[idx] ?? { asks: [], bids: [] }
|
const prev = orderBookByToken.value[idx] ?? { asks: [], bids: [] }
|
||||||
@ -680,6 +715,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 +725,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 +736,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
|
||||||
@ -714,8 +755,14 @@ const tradeInitialOptionFromBar = ref<'yes' | 'no' | undefined>(undefined)
|
|||||||
const tradeInitialTabFromBar = ref<'buy' | 'sell' | undefined>(undefined)
|
const tradeInitialTabFromBar = ref<'buy' | 'sell' | undefined>(undefined)
|
||||||
/** 移动端交易弹窗开关 */
|
/** 移动端交易弹窗开关 */
|
||||||
const tradeSheetOpen = ref(false)
|
const tradeSheetOpen = ref(false)
|
||||||
|
/** 控制底部栏内 TradeComponent 的渲染,延迟挂载以避免 slot 竞态警告 */
|
||||||
|
const tradeSheetRenderContent = ref(false)
|
||||||
|
/** 从三点菜单点击 Merge/Split 时待打开的弹窗,等 TradeComponent 挂载后执行 */
|
||||||
|
const pendingMergeSplitDialog = ref<'merge' | 'split' | null>(null)
|
||||||
/** 从持仓 Sell 打开的弹窗 */
|
/** 从持仓 Sell 打开的弹窗 */
|
||||||
const sellDialogOpen = ref(false)
|
const sellDialogOpen = ref(false)
|
||||||
|
/** 控制 Sell 弹窗内 TradeComponent 的渲染,延迟卸载以避免 emitsOptions 竞态 */
|
||||||
|
const sellDialogRenderContent = ref(false)
|
||||||
/** 从持仓 Sell 时预选的 Yes/No */
|
/** 从持仓 Sell 时预选的 Yes/No */
|
||||||
const sellInitialOption = ref<'yes' | 'no'>('yes')
|
const sellInitialOption = ref<'yes' | 'no'>('yes')
|
||||||
/** 移动端三点菜单开关 */
|
/** 移动端三点菜单开关 */
|
||||||
@ -742,18 +789,14 @@ function openSheetWithOption(side: 'yes' | 'no') {
|
|||||||
|
|
||||||
function openMergeFromBar() {
|
function openMergeFromBar() {
|
||||||
mobileMenuOpen.value = false
|
mobileMenuOpen.value = false
|
||||||
|
pendingMergeSplitDialog.value = 'merge'
|
||||||
tradeSheetOpen.value = true
|
tradeSheetOpen.value = true
|
||||||
nextTick(() => {
|
|
||||||
mobileTradeComponentRef.value?.openMergeDialog?.()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSplitFromBar() {
|
function openSplitFromBar() {
|
||||||
mobileMenuOpen.value = false
|
mobileMenuOpen.value = false
|
||||||
|
pendingMergeSplitDialog.value = 'split'
|
||||||
tradeSheetOpen.value = true
|
tradeSheetOpen.value = true
|
||||||
nextTick(() => {
|
|
||||||
mobileTradeComponentRef.value?.openSplitDialog?.()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toastStore = useToastStore()
|
const toastStore = useToastStore()
|
||||||
@ -775,8 +818,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
|
||||||
@ -793,6 +840,65 @@ function onSellOrderSuccess() {
|
|||||||
onOrderSuccess()
|
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
|
||||||
|
nextTick(() => {
|
||||||
|
const pending = pendingMergeSplitDialog.value
|
||||||
|
if (pending === 'merge') {
|
||||||
|
pendingMergeSplitDialog.value = null
|
||||||
|
mobileTradeComponentRef.value?.openMergeDialog?.()
|
||||||
|
} else if (pending === 'split') {
|
||||||
|
pendingMergeSplitDialog.value = null
|
||||||
|
mobileTradeComponentRef.value?.openSplitDialog?.()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 50)
|
||||||
|
} else {
|
||||||
|
pendingMergeSplitDialog.value = null
|
||||||
|
if (tradeSheetMountTimer) {
|
||||||
|
clearTimeout(tradeSheetMountTimer)
|
||||||
|
tradeSheetMountTimer = undefined
|
||||||
|
}
|
||||||
|
tradeSheetUnmountTimer = setTimeout(() => {
|
||||||
|
tradeSheetRenderContent.value = false
|
||||||
|
tradeSheetUnmountTimer = undefined
|
||||||
|
}, 350)
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
// 当前市场的 marketID,用于筛选持仓和订单
|
// 当前市场的 marketID,用于筛选持仓和订单
|
||||||
const currentMarketId = computed(() => getMarketId(currentMarket.value))
|
const currentMarketId = computed(() => getMarketId(currentMarket.value))
|
||||||
|
|
||||||
@ -808,13 +914,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,
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -897,6 +1007,7 @@ async function loadMarketOpenOrders() {
|
|||||||
const cancelOrderLoading = ref(false)
|
const cancelOrderLoading = ref(false)
|
||||||
|
|
||||||
async function cancelMarketOrder(ord: OpenOrderDisplayItem) {
|
async function cancelMarketOrder(ord: OpenOrderDisplayItem) {
|
||||||
|
if (ord.fullyFilled) return
|
||||||
const orderID = ord.orderID ?? 0
|
const orderID = ord.orderID ?? 0
|
||||||
const tokenID = ord.tokenID ?? ''
|
const tokenID = ord.tokenID ?? ''
|
||||||
const uid = userStore.user?.id ?? userStore.user?.ID
|
const uid = userStore.user?.id ?? userStore.user?.ID
|
||||||
@ -1867,6 +1978,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;
|
||||||
|
|||||||
@ -358,7 +358,7 @@
|
|||||||
size="small"
|
size="small"
|
||||||
class="order-cancel-icon"
|
class="order-cancel-icon"
|
||||||
color="error"
|
color="error"
|
||||||
:disabled="cancelOrderLoading"
|
:disabled="cancelOrderLoading || ord.fullyFilled"
|
||||||
@click.stop="cancelOrder(ord)"
|
@click.stop="cancelOrder(ord)"
|
||||||
>
|
>
|
||||||
<v-icon size="20">mdi-close</v-icon>
|
<v-icon size="20">mdi-close</v-icon>
|
||||||
@ -404,7 +404,7 @@
|
|||||||
variant="text"
|
variant="text"
|
||||||
size="small"
|
size="small"
|
||||||
color="error"
|
color="error"
|
||||||
:disabled="cancelOrderLoading"
|
:disabled="cancelOrderLoading || ord.fullyFilled"
|
||||||
@click="cancelOrder(ord)"
|
@click="cancelOrder(ord)"
|
||||||
>Cancel</v-btn
|
>Cancel</v-btn
|
||||||
>
|
>
|
||||||
@ -718,6 +718,8 @@ interface OpenOrder {
|
|||||||
/** 取消订单 API 用 */
|
/** 取消订单 API 用 */
|
||||||
orderID?: number
|
orderID?: number
|
||||||
tokenID?: string
|
tokenID?: string
|
||||||
|
/** 已成交数量达到原始总数量,不可撤单 */
|
||||||
|
fullyFilled?: boolean
|
||||||
}
|
}
|
||||||
interface HistoryItem {
|
interface HistoryItem {
|
||||||
id: string
|
id: string
|
||||||
@ -977,6 +979,7 @@ const cancelOrderLoading = ref(false)
|
|||||||
const cancelOrderError = ref('')
|
const cancelOrderError = ref('')
|
||||||
const showCancelError = ref(false)
|
const showCancelError = ref(false)
|
||||||
async function cancelOrder(ord: OpenOrder) {
|
async function cancelOrder(ord: OpenOrder) {
|
||||||
|
if (ord.fullyFilled) return
|
||||||
const orderID = ord.orderID ?? 5
|
const orderID = ord.orderID ?? 5
|
||||||
const tokenID = ord.tokenID ?? MOCK_TOKEN_ID
|
const tokenID = ord.tokenID ?? MOCK_TOKEN_ID
|
||||||
const uid = userStore.user?.id ?? userStore.user?.ID
|
const uid = userStore.user?.id ?? userStore.user?.ID
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user