新增:限价单接口对接
This commit is contained in:
parent
297d2d1c56
commit
b73a910b43
@ -105,8 +105,13 @@ Swagger UI 页面(如 [PmEvent findPmEvent](https://api.xtrader.vip/swagger/in
|
||||
|
||||
### 公开事件列表 `GET /PmEvent/getPmEventPublic`
|
||||
|
||||
- **Query**:page、pageSize、keyword、createdAtRange(array)。
|
||||
- **响应 200**:`data` 为 `response.PageResult`(list、page、pageSize、total);list 项为 `polymarket.PmEvent`,内含 markets、series、tags 等,markets[].outcomePrices 为价格数组,首项对应 Yes 概率。
|
||||
- **Query**:page、pageSize、keyword、createdAtRange(array)、tokenid(可选,来自 market.clobTokenIds 的值,可传单个或数组)。
|
||||
- **响应 200**:`data` 为 `response.PageResult`(list、page、pageSize、total);list 项为 `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
30
src/api/constants.ts
Normal 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]
|
||||
@ -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
|
||||
}
|
||||
|
||||
/** 从市场项取 clobTokenId,outcomeIndex 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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,36 @@ export interface ApiResponse<T = unknown> {
|
||||
msg: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 下单请求体(/clob/gateway/submitOrder)
|
||||
* tokenID 来自 market.clobTokenIds,outcomeIndex 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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 顺序一致,用于下单 tokenId:0=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()
|
||||
}
|
||||
}
|
||||
|
||||
// 限价调整方法(0–1 区间)
|
||||
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;
|
||||
|
||||
@ -156,6 +156,7 @@ const tradeMarketPayload = computed(() => {
|
||||
yesPrice,
|
||||
noPrice,
|
||||
title: m.question,
|
||||
clobTokenIds: m.clobTokenIds,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -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 的 market(Home 弹窗/底部栏),供 Split 等使用;优先用 marketId,无则用事件 id */
|
||||
/** 传给 TradeComponent 的 market(Home 弹窗/底部栏),供 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)
|
||||
|
||||
@ -265,6 +265,7 @@ const tradeMarketPayload = computed(() => {
|
||||
yesPrice,
|
||||
noPrice,
|
||||
title: m.question,
|
||||
clobTokenIds: m.clobTokenIds,
|
||||
}
|
||||
}
|
||||
const qId = route.query.marketId
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user