新增:限价单接口对接

This commit is contained in:
ivan 2026-02-11 21:25:50 +08:00
parent 297d2d1c56
commit b73a910b43
9 changed files with 331 additions and 63 deletions

View File

@ -105,8 +105,13 @@ Swagger UI 页面(如 [PmEvent findPmEvent](https://api.xtrader.vip/swagger/in
### 公开事件列表 `GET /PmEvent/getPmEventPublic`
- **Query**page、pageSize、keyword、createdAtRangearray
- **响应 200**`data``response.PageResult`list、page、pageSize、totallist 项为 `polymarket.PmEvent`,内含 markets、series、tags 等markets[].outcomePrices 为价格数组,首项对应 Yes 概率。
- **Query**page、pageSize、keyword、createdAtRangearray、tokenid可选来自 market.clobTokenIds 的值,可传单个或数组)。
- **响应 200**`data``response.PageResult`list、page、pageSize、totallist 项为 `polymarket.PmEvent`,内含 markets、series、tags 等markets[].outcomePrices 为价格数组,首项对应 Yes 概率markets[].clobTokenIds 与 outcomes/outcomePrices 顺序一致。
### 订单类型与交易方向(用于交易接口,`src/api/constants.ts`
- **OrderType**GTC=0一直有效直到取消、GTD=1指定时间内有效、FOK=2全部成交或取消、FAK=3立即成交剩余取消、Market=4市价单
- **Side**Buy=1、Sell=2。
### 通用响应与鉴权

30
src/api/constants.ts Normal file
View File

@ -0,0 +1,30 @@
/**
* OrderType
*
*/
export const OrderType = {
/** Good Till Cancelled: 一直有效直到取消 */
GTC: 0,
/** Good Till Date: 指定时间内有效 */
GTD: 1,
/** Fill Or Kill: 全部成交或立即取消(不允许部分成交) */
FOK: 2,
/** Fill And Kill (IOC): 立即成交,剩余部分取消 */
FAK: 3,
/** Market Order: 市价单,立即按最优价成交,剩余取消 (同 FAK但不限价) */
Market: 4,
} as const
export type OrderTypeValue = (typeof OrderType)[keyof typeof OrderType]
/**
* Side
*/
export const Side = {
/** 买入 */
Buy: 1,
/** 卖出 */
Sell: 2,
} as const
export type SideValue = (typeof Side)[keyof typeof Side]

View File

@ -65,8 +65,8 @@ export interface PmEventMarketItem {
outcomes?: string[]
/** 各选项价格outcomes[0] 对应 outcomePrices[0] */
outcomePrices?: string[] | number[]
/** 市场对应的 clob token id 与 outcomePrices 和outcomes顺序一致 */
clobTokenIds:string[]
/** 市场对应的 clob token id 与 outcomePrices、outcomes 顺序一致outcomes[0] 对应 clobTokenIds[0] */
clobTokenIds?: string[]
endDate?: string
volume?: number
[key: string]: unknown
@ -79,6 +79,16 @@ export function getMarketId(m: PmEventMarketItem | null | undefined): string | u
return raw != null ? String(raw) : undefined
}
/** 从市场项取 clobTokenIdoutcomeIndex 0=Yes/第一选项1=No/第二选项 */
export function getClobTokenId(
m: PmEventMarketItem | null | undefined,
outcomeIndex: 0 | 1 = 0
): string | undefined {
if (!m?.clobTokenIds?.length) return undefined
const id = m.clobTokenIds[outcomeIndex]
return id != null ? String(id) : undefined
}
/** 对应 definitions polymarket.PmSeries 常用字段 */
export interface PmEventSeriesItem {
ID?: number
@ -109,22 +119,30 @@ export interface GetPmEventListParams {
keyword?: string
/** 创建时间范围,如 ['2025-01-01', '2025-12-31'] */
createdAtRange?: string[]
/** clobTokenIds 对应的值,用于按市场 token 筛选;可从 market.clobTokenIds 获取 */
tokenid?: string | string[]
}
/**
* Event
* GET /PmEvent/getPmEventPublic
*
* Query: page, pageSize, keyword, createdAtRange, tokenid
* tokenid market.clobTokenIds
*/
export async function getPmEventPublic(
params: GetPmEventListParams = {}
): Promise<PmEventListResponse> {
const { page = 1, pageSize = 10, keyword, createdAtRange } = params
const { page = 1, pageSize = 10, keyword, createdAtRange, tokenid } = params
const query: Record<string, string | number | string[] | undefined> = {
page,
pageSize,
}
if (keyword != null && keyword !== '') query.keyword = keyword
if (createdAtRange != null && createdAtRange.length) query.createdAtRange = createdAtRange
if (tokenid != null) {
query.tokenid = Array.isArray(tokenid) ? tokenid : [tokenid]
}
return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query)
}
@ -186,6 +204,8 @@ export interface EventCardOutcome {
noLabel?: string
/** 可选,用于交易时区分 market */
marketId?: string
/** 用于下单 tokenId与 outcomes 顺序一致 */
clobTokenIds?: string[]
}
/**
@ -213,6 +233,8 @@ export interface EventCardItem {
isNew?: boolean
/** 当前市场 ID单 market 时为第一个 market 的 ID供交易/Split 使用) */
marketId?: string
/** 用于下单 tokenId单 market 时取自 firstMarket.clobTokenIds */
clobTokenIds?: string[]
}
/** 内存缓存:列表数据,切换页面时复用,下拉刷新时清空 */
@ -298,6 +320,7 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
yesLabel: m.outcomes?.[0] ?? 'Yes',
noLabel: m.outcomes?.[1] ?? 'No',
marketId: getMarketId(m),
clobTokenIds: m.clobTokenIds,
}))
: undefined
@ -316,5 +339,6 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
noLabel: firstMarket?.outcomes?.[1] ?? 'No',
isNew: item.new === true,
marketId: getMarketId(firstMarket),
clobTokenIds: firstMarket?.clobTokenIds,
}
}

View File

@ -7,6 +7,36 @@ export interface ApiResponse<T = unknown> {
msg: string
}
/**
* /clob/gateway/submitOrder
* tokenID market.clobTokenIdsoutcomeIndex 0=Yes 1=No
*/
export interface ClobSubmitOrderRequest {
expiration: number
feeRateBps: number
nonce: number
orderType: number
/** 价格(整数,已乘 10000 */
price: number
side: number
/** 数量(份额) */
size: number
taker: boolean
tokenID: string
userID: number
}
/**
* POST /clob/gateway/submitOrder
* / Yes No
*/
export async function pmOrderPlace(
data: ClobSubmitOrderRequest,
config?: { headers?: Record<string, string> }
): Promise<ApiResponse> {
return post<ApiResponse>('/clob/gateway/submitOrder', data, config)
}
/**
* Split /PmMarket/split
* USDC Yes+No 1 USDC 1 Yes + 1 No

View File

@ -171,7 +171,7 @@ const router = useRouter()
const emit = defineEmits<{
openTrade: [
side: 'yes' | 'no',
market?: { id: string; title: string; marketId?: string; outcomeTitle?: string }
market?: { id: string; title: string; marketId?: string; outcomeTitle?: string; clobTokenIds?: string[] }
]
}>()
@ -198,6 +198,8 @@ const props = withDefaults(
isNew?: boolean
/** 当前市场 ID单 market 时供交易/Split 使用) */
marketId?: string
/** 用于下单 tokenId单 market 时 */
clobTokenIds?: string[]
}>(),
{
marketTitle: 'Mamdan opens city-owned grocery store b...',
@ -289,7 +291,12 @@ const navigateToDetail = () => {
}
function openTradeSingle(side: 'yes' | 'no') {
emit('openTrade', side, { id: props.id, title: props.marketTitle, marketId: props.marketId })
emit('openTrade', side, {
id: props.id,
title: props.marketTitle,
marketId: props.marketId,
clobTokenIds: props.clobTokenIds,
})
}
function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
@ -298,6 +305,7 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
title: outcome.title,
marketId: outcome.marketId,
outcomeTitle: outcome.title,
clobTokenIds: outcome.clobTokenIds,
})
}
</script>

View File

@ -84,8 +84,9 @@
</template>
</div>
<p v-if="orderError" class="order-error">{{ orderError }}</p>
<!-- Action Button -->
<v-btn class="action-btn" @click="submitOrder">
<v-btn class="action-btn" :loading="orderLoading" :disabled="orderLoading" @click="submitOrder">
{{ actionButtonText }}
</v-btn>
</template>
@ -172,13 +173,17 @@
<v-icon>mdi-minus</v-icon>
</v-btn>
<v-text-field
v-model.number="limitPrice"
:model-value="limitPrice"
type="number"
min="0.01"
min="0"
max="1"
step="0.01"
class="price-input-field"
hide-details
density="compact"
@update:model-value="onLimitPriceInput"
@keydown="onLimitPriceKeydown"
@paste="onLimitPricePaste"
></v-text-field>
<v-btn class="adjust-btn" icon @click="increasePrice">
<v-icon>mdi-plus</v-icon>
@ -193,12 +198,15 @@
<span class="label">Shares</span>
<div class="shares-input">
<v-text-field
v-model.number="shares"
:model-value="shares"
type="number"
min="0"
min="1"
class="shares-input-field"
hide-details
density="compact"
@update:model-value="onSharesInput"
@keydown="onSharesKeydown"
@paste="onSharesPaste"
></v-text-field>
</div>
</div>
@ -267,8 +275,8 @@
</template>
</div>
<!-- Action Button -->
<v-btn class="action-btn" @click="submitOrder">
<p v-if="orderError" class="order-error">{{ orderError }}</p>
<v-btn class="action-btn" :loading="orderLoading" :disabled="orderLoading" @click="submitOrder">
{{ actionButtonText }}
</v-btn>
</template>
@ -314,7 +322,8 @@
<div class="total-row"><span class="label">You'll receive</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{ totalPrice }}</span></div>
</template>
</div>
<v-btn class="action-btn" @click="submitOrder">{{ actionButtonText }}</v-btn>
<p v-if="orderError" class="order-error">{{ orderError }}</p>
<v-btn class="action-btn" :loading="orderLoading" :disabled="orderLoading" @click="submitOrder">{{ actionButtonText }}</v-btn>
</template>
<template v-else>
<div class="price-options hide-in-mobile-sheet">
@ -346,7 +355,7 @@
<span class="label">Limit Price</span>
<div class="price-input">
<v-btn class="adjust-btn" icon @click="decreasePrice"><v-icon>mdi-minus</v-icon></v-btn>
<v-text-field v-model.number="limitPrice" type="number" min="0.01" step="0.01" class="price-input-field" hide-details density="compact"></v-text-field>
<v-text-field :model-value="limitPrice" type="number" min="0" max="1" step="0.01" class="price-input-field" hide-details density="compact" @update:model-value="onLimitPriceInput" @keydown="onLimitPriceKeydown" @paste="onLimitPricePaste"></v-text-field>
<v-btn class="adjust-btn" icon @click="increasePrice"><v-icon>mdi-plus</v-icon></v-btn>
</div>
</div>
@ -355,7 +364,7 @@
<div class="shares-header">
<span class="label">Shares</span>
<div class="shares-input">
<v-text-field v-model.number="shares" type="number" min="0" class="shares-input-field" hide-details density="compact"></v-text-field>
<v-text-field :model-value="shares" type="number" min="1" class="shares-input-field" hide-details density="compact" @update:model-value="onSharesInput" @keydown="onSharesKeydown" @paste="onSharesPaste"></v-text-field>
</div>
</div>
<div v-if="activeTab === 'buy'" class="shares-buttons">
@ -393,7 +402,8 @@
<div class="total-row"><span class="label">You'll receive</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{ totalPrice }}</span></div>
</template>
</div>
<v-btn class="action-btn" @click="submitOrder">{{ actionButtonText }}</v-btn>
<p v-if="orderError" class="order-error">{{ orderError }}</p>
<v-btn class="action-btn" :loading="orderLoading" :disabled="orderLoading" @click="submitOrder">{{ actionButtonText }}</v-btn>
</template>
</div>
</v-sheet>
@ -464,7 +474,8 @@
<div class="total-row"><span class="label">You'll receive</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{ totalPrice }}</span></div>
</template>
</div>
<v-btn class="action-btn" @click="submitOrder">{{ actionButtonText }}</v-btn>
<p v-if="orderError" class="order-error">{{ orderError }}</p>
<v-btn class="action-btn" :loading="orderLoading" :disabled="orderLoading" @click="submitOrder">{{ actionButtonText }}</v-btn>
</template>
<template v-else>
<div class="price-options hide-in-mobile-sheet">
@ -496,7 +507,7 @@
<span class="label">Limit Price</span>
<div class="price-input">
<v-btn class="adjust-btn" icon @click="decreasePrice"><v-icon>mdi-minus</v-icon></v-btn>
<v-text-field v-model.number="limitPrice" type="number" min="0.01" step="0.01" class="price-input-field" hide-details density="compact"></v-text-field>
<v-text-field :model-value="limitPrice" type="number" min="0" max="1" step="0.01" class="price-input-field" hide-details density="compact" @update:model-value="onLimitPriceInput" @keydown="onLimitPriceKeydown" @paste="onLimitPricePaste"></v-text-field>
<v-btn class="adjust-btn" icon @click="increasePrice"><v-icon>mdi-plus</v-icon></v-btn>
</div>
</div>
@ -505,7 +516,7 @@
<div class="shares-header">
<span class="label">Shares</span>
<div class="shares-input">
<v-text-field v-model.number="shares" type="number" min="0" class="shares-input-field" hide-details density="compact"></v-text-field>
<v-text-field :model-value="shares" type="number" min="1" class="shares-input-field" hide-details density="compact" @update:model-value="onSharesInput" @keydown="onSharesKeydown" @paste="onSharesPaste"></v-text-field>
</div>
</div>
<div v-if="activeTab === 'buy'" class="shares-buttons">
@ -543,7 +554,8 @@
<div class="total-row"><span class="label">You'll receive</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{ totalPrice }}</span></div>
</template>
</div>
<v-btn class="action-btn" @click="submitOrder">{{ actionButtonText }}</v-btn>
<p v-if="orderError" class="order-error">{{ orderError }}</p>
<v-btn class="action-btn" :loading="orderLoading" :disabled="orderLoading" @click="submitOrder">{{ actionButtonText }}</v-btn>
</template>
</div>
</v-sheet>
@ -648,7 +660,8 @@
import { ref, computed, watch, onMounted } from 'vue'
import { useDisplay } from 'vuetify'
import { useUserStore } from '../stores/user'
import { pmMarketMerge, pmMarketSplit } from '../api/market'
import { pmMarketMerge, pmMarketSplit, pmOrderPlace } from '../api/market'
import { OrderType, Side } from '../api/constants'
const { mobile } = useDisplay()
const userStore = useUserStore()
@ -658,6 +671,8 @@ export interface TradeMarketPayload {
yesPrice: number
noPrice: number
title?: string
/** 与 outcomes/outcomePrices 顺序一致,用于下单 tokenId0=Yes 1=No */
clobTokenIds?: string[]
}
const props = withDefaults(
@ -762,7 +777,7 @@ const limitType = ref('Limit')
const expirationEnabled = ref(false)
const selectedOption = ref<'yes' | 'no'>(props.initialOption ?? 'no')
const limitPrice = ref(0.82) //
const shares = ref(20) //
const shares = ref(20) //
const expirationTime = ref('5m') //
const expirationOptions = ref(['5m', '15m', '30m', '1h', '2h', '4h', '8h', '12h', '1d', '2d', '3d']) //
@ -771,6 +786,9 @@ const isMarketMode = computed(() => limitType.value === 'Market')
const amount = ref(0) // Market mode amount
const balance = ref(0) // Market mode balance
const orderLoading = ref(false)
const orderError = ref('')
// Emits
const emit = defineEmits<{
optionChange: [option: 'yes' | 'no']
@ -803,11 +821,15 @@ function applyInitialOption(option: 'yes' | 'no') {
syncLimitPriceFromMarket()
}
function clampLimitPrice(v: number): number {
return Math.min(1, Math.max(0, Number.isFinite(v) ? v : 0))
}
/** 根据当前 props.market 与 selectedOption 同步 limitPrice组件显示或 market 更新时调用) */
function syncLimitPriceFromMarket() {
const yesP = props.market?.yesPrice ?? 0.19
const noP = props.market?.noPrice ?? 0.82
limitPrice.value = selectedOption.value === 'yes' ? yesP : noP
limitPrice.value = clampLimitPrice(selectedOption.value === 'yes' ? yesP : noP)
}
onMounted(() => {
@ -820,6 +842,7 @@ watch(() => props.initialOption, (option) => {
watch(() => props.market, (m) => {
if (m) {
orderError.value = ''
if (props.initialOption) applyInitialOption(props.initialOption)
else syncLimitPriceFromMarket()
}
@ -830,29 +853,90 @@ const handleOptionChange = (option: 'yes' | 'no') => {
selectedOption.value = option
const yesP = props.market?.yesPrice ?? 0.19
const noP = props.market?.noPrice ?? 0.82
limitPrice.value = option === 'yes' ? yesP : noP
limitPrice.value = clampLimitPrice(option === 'yes' ? yesP : noP)
emit('optionChange', option)
}
//
/** 仅在值在 [0,1] 且为有效数字时更新,否则保持原值不变 */
function onLimitPriceInput(v: unknown) {
const num = v == null ? NaN : Number(v)
if (!Number.isFinite(num) || num < 0 || num > 1) return
limitPrice.value = num
}
/** 只允许数字和小数点输入 */
function onLimitPriceKeydown(e: KeyboardEvent) {
const key = e.key
const allowed = ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End']
if (allowed.includes(key)) return
if (e.ctrlKey || e.metaKey) {
if (['a', 'c', 'v', 'x'].includes(key.toLowerCase())) return
}
if (key >= '0' && key <= '9') return
if (key === '.' && !String((e.target as HTMLInputElement)?.value ?? '').includes('.')) return
e.preventDefault()
}
/** 粘贴时只接受有效数字 */
function onLimitPricePaste(e: ClipboardEvent) {
const text = e.clipboardData?.getData('text') ?? ''
const num = parseFloat(text)
if (!Number.isFinite(num) || num < 0 || num > 1) {
e.preventDefault()
}
}
// 01
const decreasePrice = () => {
limitPrice.value = Math.max(0.01, limitPrice.value - 0.01)
limitPrice.value = clampLimitPrice(limitPrice.value - 0.01)
}
const increasePrice = () => {
limitPrice.value += 0.01
limitPrice.value = clampLimitPrice(limitPrice.value + 0.01)
}
//
/** 将 shares 限制为正整数(>= 1 */
function clampShares(v: number): number {
const n = Math.floor(Number.isFinite(v) ? v : 1)
return Math.max(1, n)
}
/** 仅在值为正整数时更新 shares */
function onSharesInput(v: unknown) {
const num = v == null ? NaN : Number(v)
const n = Math.floor(num)
if (!Number.isFinite(num) || n < 1 || num !== n) return
shares.value = n
}
/** 只允许数字输入Shares 为正整数) */
function onSharesKeydown(e: KeyboardEvent) {
const key = e.key
const allowed = ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End']
if (allowed.includes(key)) return
if (e.ctrlKey || e.metaKey) {
if (['a', 'c', 'v', 'x'].includes(key.toLowerCase())) return
}
if (key >= '0' && key <= '9') return
e.preventDefault()
}
/** 粘贴时只接受正整数 */
function onSharesPaste(e: ClipboardEvent) {
const text = e.clipboardData?.getData('text') ?? ''
const num = parseInt(text, 10)
if (!Number.isFinite(num) || num < 1) e.preventDefault()
}
//
const adjustShares = (amount: number) => {
shares.value = Math.max(0, shares.value + amount)
shares.value = clampShares(shares.value + amount)
}
// Sell使
const setSharesPercentage = (percentage: number) => {
// 100
const maxShares = 100
shares.value = Math.round((maxShares * percentage) / 100)
shares.value = clampShares(Math.round((maxShares * percentage) / 100))
}
// Market mode methods
@ -870,17 +954,95 @@ const deposit = () => {
// API
}
// Set expiration
function submitOrder() {
emit('submit', {
/** 将 expirationTime 如 "5m" 转为 Unix 秒级时间戳GTD 用0 表示无过期 */
function parseExpirationTimestamp(expTime: string): number {
const m = /^(\d+)(m|h|d)$/i.exec(expTime)
if (!m) return 0
const [, num, unit] = m
const n = parseInt(num ?? '0', 10)
if (!Number.isFinite(n) || n <= 0) return 0
const now = Date.now()
let ms = n
if (unit?.toLowerCase() === 'm') ms = n * 60 * 1000
else if (unit?.toLowerCase() === 'h') ms = n * 3600 * 1000
else if (unit?.toLowerCase() === 'd') ms = n * 86400 * 1000
return Math.floor((now + ms) / 1000)
}
// Set expiration /clob/gateway/submitOrder
async function submitOrder() {
const marketId = props.market?.marketId
const clobIds = props.market?.clobTokenIds
const outcomeIndex = selectedOption.value === 'yes' ? 0 : 1
const tokenId = clobIds?.[outcomeIndex]
const payload = {
side: activeTab.value as 'buy' | 'sell',
option: selectedOption.value,
limitPrice: limitPrice.value,
shares: shares.value,
expirationEnabled: expirationEnabled.value,
expirationTime: expirationTime.value,
...(props.market?.marketId != null && { marketId: props.market.marketId }),
})
...(marketId != null && { marketId }),
}
emit('submit', payload)
if (!tokenId) {
orderError.value = '请先选择市场(需包含 clobTokenIds'
return
}
const headers = userStore.getAuthHeaders()
if (!headers) {
orderError.value = '请先登录'
return
}
const uid = userStore.user?.id ?? userStore.user?.ID
const userIdNum = uid != null ? Number(uid) : 0
if (!Number.isFinite(userIdNum) || userIdNum <= 0) {
orderError.value = '用户信息异常'
return
}
const isMarket = limitType.value === 'Market'
const orderTypeNum = isMarket
? OrderType.Market
: expirationEnabled.value
? OrderType.GTD
: OrderType.GTC
const sideNum = activeTab.value === 'buy' ? Side.Buy : Side.Sell
const expiration =
orderTypeNum === OrderType.GTD && expirationEnabled.value
? parseExpirationTimestamp(expirationTime.value)
: 0
orderLoading.value = true
orderError.value = ''
try {
const res = await pmOrderPlace(
{
expiration,
feeRateBps: 0,
nonce: 0,
orderType: orderTypeNum,
price: limitPrice.value,
side: sideNum,
size: clampShares(shares.value),
taker: true,
tokenID: tokenId,
userID: userIdNum,
},
{ headers }
)
if (res.code === 0 || res.code === 200) {
userStore.fetchUsdcBalance()
} else {
orderError.value = res.msg || '下单失败'
}
} catch (e) {
orderError.value = e instanceof Error ? e.message : 'Request failed'
} finally {
orderLoading.value = false
}
}
</script>
@ -1423,6 +1585,12 @@ function submitOrder() {
color: #dc2626;
}
.order-error {
font-size: 13px;
color: #dc2626;
margin: 8px 0 0;
}
.merge-dialog-actions {
padding: 16px 20px 20px;
padding-top: 8px;

View File

@ -156,6 +156,7 @@ const tradeMarketPayload = computed(() => {
yesPrice,
noPrice,
title: m.question,
clobTokenIds: m.clobTokenIds,
}
})

View File

@ -30,6 +30,7 @@
:no-label="card.noLabel"
:is-new="card.isNew"
:market-id="card.marketId"
:clob-token-ids="card.clobTokenIds"
@open-trade="onCardOpenTrade"
/>
<div v-if="eventList.length === 0 && !loadingMore" class="home-list-empty">
@ -202,7 +203,7 @@ const noMoreEvents = computed(() => {
const footerLang = ref('English')
const tradeDialogOpen = ref(false)
const tradeDialogSide = ref<'yes' | 'no'>('yes')
const tradeDialogMarket = ref<{ id: string; title: string; marketId?: string } | null>(null)
const tradeDialogMarket = ref<{ id: string; title: string; marketId?: string; clobTokenIds?: string[] } | null>(null)
const scrollRef = ref<HTMLElement | null>(null)
function onCardOpenTrade(side: 'yes' | 'no', market?: { id: string; title: string; marketId?: string }) {
@ -211,7 +212,7 @@ function onCardOpenTrade(side: 'yes' | 'no', market?: { id: string; title: strin
tradeDialogOpen.value = true
}
/** 传给 TradeComponent 的 marketHome 弹窗/底部栏),供 Split 等使用;优先用 marketId无则用事件 id */
/** 传给 TradeComponent 的 marketHome 弹窗/底部栏),供 Split、下单等使用 */
const homeTradeMarketPayload = computed(() => {
const m = tradeDialogMarket.value
if (!m) return undefined
@ -219,7 +220,7 @@ const homeTradeMarketPayload = computed(() => {
const chance = 50
const yesPrice = Math.min(1, Math.max(0, chance / 100))
const noPrice = 1 - yesPrice
return { marketId, yesPrice, noPrice, title: m.title }
return { marketId, yesPrice, noPrice, title: m.title, clobTokenIds: m.clobTokenIds }
})
const sentinelRef = ref<HTMLElement | null>(null)

View File

@ -265,6 +265,7 @@ const tradeMarketPayload = computed(() => {
yesPrice,
noPrice,
title: m.question,
clobTokenIds: m.clobTokenIds,
}
}
const qId = route.query.marketId