378 lines
11 KiB
TypeScript
378 lines
11 KiB
TypeScript
import { get } from './request'
|
||
|
||
/** 分页结果 */
|
||
export interface PageResult<T> {
|
||
list: T[]
|
||
page: number
|
||
pageSize: number
|
||
total: number
|
||
}
|
||
|
||
/**
|
||
* Event 单项结构(与 doc.json definitions["polymarket.PmEvent"] 对齐)
|
||
* 用于 /PmEvent/getPmEventPublic 列表项 与 /PmEvent/findPmEvent 的 data
|
||
*/
|
||
export interface PmEventListItem {
|
||
ID: number
|
||
createdAt?: string
|
||
updatedAt?: string
|
||
slug?: string
|
||
ticker?: string
|
||
title: string
|
||
description?: string
|
||
resolutionSource?: string
|
||
startDate?: string
|
||
endDate?: string
|
||
creationDate?: string
|
||
closedTime?: string
|
||
startTime?: string
|
||
image?: string
|
||
icon?: string
|
||
active?: boolean
|
||
archived?: boolean
|
||
closed?: boolean
|
||
featured?: boolean
|
||
new?: boolean
|
||
restricted?: boolean
|
||
enableOrderBook?: boolean
|
||
competitive?: number
|
||
liquidity?: number
|
||
liquidityAmm?: number
|
||
liquidityClob?: number
|
||
openInterest?: number
|
||
volume?: number
|
||
commentCount?: number
|
||
seriesSlug?: string
|
||
markets?: PmEventMarketItem[]
|
||
series?: PmEventSeriesItem[]
|
||
tags?: PmEventTagItem[]
|
||
[key: string]: unknown
|
||
}
|
||
|
||
/**
|
||
* 对应 definitions polymarket.PmMarket 常用字段
|
||
* - outcomes: 选项展示文案,如 ["Yes", "No"] 或 ["Up", "Down"],与 outcomePrices 一一对应
|
||
* - outcomePrices: 各选项价格(首项为第一选项概率,如 Yes/Up)
|
||
*/
|
||
export interface PmEventMarketItem {
|
||
/** 市场 ID(部分接口返回大写 ID) */
|
||
ID?: number
|
||
/** 市场 ID(部分接口或 JSON 序列化为小写 id) */
|
||
id?: number
|
||
question?: string
|
||
slug?: string
|
||
/** 选项展示文案,与 outcomePrices 顺序一致 */
|
||
outcomes?: string[]
|
||
/** 各选项价格,outcomes[0] 对应 outcomePrices[0] */
|
||
outcomePrices?: string[] | number[]
|
||
/** 市场对应的 clob token id 与 outcomePrices、outcomes 顺序一致,outcomes[0] 对应 clobTokenIds[0] */
|
||
clobTokenIds?: string[]
|
||
endDate?: string
|
||
volume?: number
|
||
[key: string]: unknown
|
||
}
|
||
|
||
/** 从市场项取 marketId,兼容 ID / id */
|
||
export function getMarketId(m: PmEventMarketItem | null | undefined): string | undefined {
|
||
if (!m) return undefined
|
||
const raw = m.ID ?? m.id
|
||
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
|
||
ticker?: string
|
||
title?: string
|
||
slug?: string
|
||
[key: string]: unknown
|
||
}
|
||
|
||
/** 对应 definitions polymarket.PmTag 常用字段 */
|
||
export interface PmEventTagItem {
|
||
label?: string
|
||
slug?: string
|
||
ID?: number
|
||
[key: string]: unknown
|
||
}
|
||
|
||
/** 接口统一响应 */
|
||
export interface PmEventListResponse {
|
||
code: number
|
||
data: PageResult<PmEventListItem>
|
||
msg: string
|
||
}
|
||
|
||
/**
|
||
* GET /PmEvent/getPmEventPublic 请求参数(与 doc.json 对齐)
|
||
*/
|
||
export interface GetPmEventListParams {
|
||
page?: number
|
||
pageSize?: number
|
||
keyword?: string
|
||
/** 创建时间范围,如 ['2025-01-01', '2025-12-31'] */
|
||
createdAtRange?: string[]
|
||
/** 标签 ID 列表,按分类筛选,传统数组方式传递 */
|
||
tagIds?: number[]
|
||
}
|
||
|
||
/**
|
||
* 分页获取 Event 列表(公开接口,不需要鉴权)
|
||
* GET /PmEvent/getPmEventPublic
|
||
*
|
||
* Query: page, pageSize, keyword, createdAtRange, tagIds
|
||
* doc.json: paths["/PmEvent/getPmEventPublic"].get.parameters
|
||
*/
|
||
export async function getPmEventPublic(
|
||
params: GetPmEventListParams = {},
|
||
): Promise<PmEventListResponse> {
|
||
const { page = 1, pageSize = 10, keyword, createdAtRange, tagIds } = params
|
||
const query: Record<string, string | number | number[] | string[] | undefined> = {
|
||
page,
|
||
pageSize,
|
||
}
|
||
if (keyword != null && keyword !== '') query.keyword = keyword
|
||
if (createdAtRange != null && createdAtRange.length) query.createdAtRange = createdAtRange
|
||
if (tagIds != null && tagIds.length > 0) {
|
||
query.tagIds = tagIds
|
||
}
|
||
return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query)
|
||
}
|
||
|
||
/**
|
||
* GET /PmEvent/findPmEvent 响应体(200)
|
||
* doc.json: responses["200"].schema = allOf [ response.Response, { data: polymarket.PmEvent, msg } ]
|
||
*/
|
||
export interface PmEventDetailResponse {
|
||
code: number
|
||
data: PmEventListItem
|
||
msg: string
|
||
}
|
||
|
||
/**
|
||
* findPmEvent 请求参数(Query)
|
||
* doc.json: ID 与 slug 支持同时传入,至少传一个
|
||
*/
|
||
export interface FindPmEventParams {
|
||
/** Event 主键(数字 ID) */
|
||
id?: number
|
||
/** Event 的 slug 标识 */
|
||
slug?: string
|
||
}
|
||
|
||
/**
|
||
* 用 id 和/或 slug 查询 Event 详情
|
||
* GET /PmEvent/findPmEvent
|
||
*
|
||
* 请求参数(Query):
|
||
* - ID: number,可选
|
||
* - slug: string,可选
|
||
* - ID 与 slug 至少传一个,可同时传
|
||
* 鉴权:需在 headers 中传 x-token、x-user-id
|
||
*
|
||
* 响应(200):PmEventDetailResponse { code, data: PmEventListItem, msg }
|
||
*/
|
||
export async function findPmEvent(
|
||
params: FindPmEventParams,
|
||
config?: { headers?: Record<string, string> },
|
||
): Promise<PmEventDetailResponse> {
|
||
const query: Record<string, string | number> = {}
|
||
if (params.id != null) query.ID = params.id
|
||
if (params.slug != null && params.slug !== '') query.slug = params.slug
|
||
if (Object.keys(query).length === 0) {
|
||
throw new Error('findPmEvent: 至少需要传 id 或 slug')
|
||
}
|
||
return get<PmEventDetailResponse>('/PmEvent/findPmEvent', query, config)
|
||
}
|
||
|
||
/** 多选项卡片中单个选项(用于左右滑动切换) */
|
||
export interface EventCardOutcome {
|
||
title: string
|
||
/** 第一选项概率(来自 outcomePrices[0]) */
|
||
chanceValue: number
|
||
/** Yes 价格 0–1,来自 outcomePrices[0],供交易组件使用 */
|
||
yesPrice?: number
|
||
/** No 价格 0–1,来自 outcomePrices[1],供交易组件使用 */
|
||
noPrice?: number
|
||
yesLabel?: string
|
||
noLabel?: string
|
||
marketId?: string
|
||
clobTokenIds?: string[]
|
||
}
|
||
|
||
/**
|
||
* 首页卡片项(与 mapEventItemToCard 返回结构一致,用于缓存)
|
||
* displayType:single = 单一 Yes/No,multi = 多个选项左右滑动
|
||
*/
|
||
export interface EventCardItem {
|
||
id: string
|
||
/** Event slug,用于 findPmEvent 传参 */
|
||
slug?: string
|
||
marketTitle: string
|
||
chanceValue: number
|
||
marketInfo: string
|
||
imageUrl: string
|
||
category: string
|
||
expiresAt: string
|
||
/** 展示类型:单一二元 或 多选项滑动 */
|
||
displayType: 'single' | 'multi'
|
||
/** 多选项时每个选项的标题与概率,按顺序滑动展示 */
|
||
outcomes?: EventCardOutcome[]
|
||
/** 单一类型时可选按钮文案,如 "Up"/"Down" */
|
||
yesLabel?: string
|
||
noLabel?: string
|
||
/** 是否显示 NEW 角标 */
|
||
isNew?: boolean
|
||
/** 当前市场 ID(单 market 时为第一个 market 的 ID,供交易/Split 使用) */
|
||
marketId?: string
|
||
/** 用于下单 tokenId,单 market 时取自 firstMarket.clobTokenIds */
|
||
clobTokenIds?: string[]
|
||
/** Yes 价格 0–1,来自 outcomePrices[0],供交易组件使用 */
|
||
yesPrice?: number
|
||
/** No 价格 0–1,来自 outcomePrices[1],供交易组件使用 */
|
||
noPrice?: number
|
||
}
|
||
|
||
/** 内存缓存:列表数据,切换页面时复用,下拉刷新时清空 */
|
||
let eventListCache: {
|
||
list: EventCardItem[]
|
||
page: number
|
||
total: number
|
||
pageSize: number
|
||
} | null = null
|
||
|
||
export function getEventListCache(): typeof eventListCache {
|
||
return eventListCache
|
||
}
|
||
|
||
export function setEventListCache(data: {
|
||
list: EventCardItem[]
|
||
page: number
|
||
total: number
|
||
pageSize: number
|
||
}) {
|
||
eventListCache = data
|
||
}
|
||
|
||
export function clearEventListCache(): void {
|
||
eventListCache = null
|
||
}
|
||
|
||
function marketChance(market: PmEventMarketItem): number {
|
||
const raw = market?.outcomePrices?.[0]
|
||
if (raw == null) return 17
|
||
const yesPrice = parseFloat(String(raw))
|
||
if (!Number.isFinite(yesPrice)) return 17
|
||
return Math.min(100, Math.max(0, Math.round(yesPrice * 100)))
|
||
}
|
||
|
||
/**
|
||
* 将 list 单项映射为首页 MarketCard 所需字段
|
||
* - 单一 market 或无 market:displayType single,当前逻辑
|
||
* - 多个 markets:displayType multi,outcomes 为每项标题+概率,卡片内左右滑动切换
|
||
*/
|
||
export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
||
const id = String(item.ID ?? '')
|
||
const marketTitle = item.title ?? ''
|
||
const imageUrl = item.image ?? item.icon ?? ''
|
||
|
||
const markets = item.markets ?? []
|
||
const multi = markets.length > 1
|
||
|
||
let chanceValue = 17
|
||
const firstMarket = markets[0]
|
||
if (firstMarket?.outcomePrices?.[0] != null) {
|
||
chanceValue = marketChance(firstMarket)
|
||
}
|
||
|
||
let marketInfo = '$0 Vol.'
|
||
if (item.volume != null && Number.isFinite(item.volume)) {
|
||
const v = item.volume
|
||
if (v >= 1000) {
|
||
marketInfo = `$${(v / 1000).toFixed(1)}k Vol.`
|
||
} else {
|
||
marketInfo = `$${Math.round(v)} Vol.`
|
||
}
|
||
}
|
||
|
||
let expiresAt = ''
|
||
if (item.endDate) {
|
||
try {
|
||
const d = new Date(item.endDate)
|
||
if (!Number.isNaN(d.getTime())) {
|
||
expiresAt = d.toLocaleDateString('en-US', {
|
||
month: 'short',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
})
|
||
}
|
||
} catch {
|
||
expiresAt = item.endDate
|
||
}
|
||
}
|
||
|
||
const category = item.series?.[0]?.title ?? item.tags?.[0]?.label ?? ''
|
||
|
||
function parseOutcomePrices(m: PmEventMarketItem): { yesPrice: number; noPrice: number } {
|
||
const y = m?.outcomePrices?.[0]
|
||
const n = m?.outcomePrices?.[1]
|
||
const yesPrice =
|
||
y != null && Number.isFinite(parseFloat(String(y)))
|
||
? Math.min(1, Math.max(0, parseFloat(String(y))))
|
||
: 0.5
|
||
const noPrice =
|
||
n != null && Number.isFinite(parseFloat(String(n)))
|
||
? Math.min(1, Math.max(0, parseFloat(String(n))))
|
||
: 1 - yesPrice
|
||
return { yesPrice, noPrice }
|
||
}
|
||
|
||
const outcomes: EventCardOutcome[] | undefined = multi
|
||
? markets.map((m) => {
|
||
const { yesPrice, noPrice } = parseOutcomePrices(m)
|
||
return {
|
||
title: m.question ?? '',
|
||
chanceValue: marketChance(m),
|
||
yesPrice,
|
||
noPrice,
|
||
yesLabel: m.outcomes?.[0] ?? 'Yes',
|
||
noLabel: m.outcomes?.[1] ?? 'No',
|
||
marketId: getMarketId(m),
|
||
clobTokenIds: m.clobTokenIds,
|
||
}
|
||
})
|
||
: undefined
|
||
|
||
const firstPrices = firstMarket ? parseOutcomePrices(firstMarket) : { yesPrice: 0.5, noPrice: 0.5 }
|
||
|
||
return {
|
||
id,
|
||
slug: item.slug ?? undefined,
|
||
marketTitle,
|
||
chanceValue,
|
||
marketInfo,
|
||
imageUrl,
|
||
category,
|
||
expiresAt,
|
||
displayType: multi ? 'multi' : 'single',
|
||
outcomes,
|
||
yesLabel: firstMarket?.outcomes?.[0] ?? 'Yes',
|
||
noLabel: firstMarket?.outcomes?.[1] ?? 'No',
|
||
isNew: item.new === true,
|
||
marketId: getMarketId(firstMarket),
|
||
clobTokenIds: firstMarket?.clobTokenIds,
|
||
yesPrice: firstPrices.yesPrice,
|
||
noPrice: firstPrices.noPrice,
|
||
}
|
||
}
|