From 3e329c307ecc60076f68821193a9005a257d38fd Mon Sep 17 00:00:00 2001 From: ivan Date: Fri, 27 Feb 2026 15:15:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E6=8C=81=E4=BB=93?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=8E=A5=E5=8F=A3=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/skills/xtrader-api-docs/SKILL.md | 26 +++- src/api/order.ts | 69 ++++++++- src/api/position.ts | 122 ++++++++++++++++ src/api/user.ts | 56 +++++++- src/stores/user.ts | 49 ++++--- src/views/Wallet.vue | 172 ++++++++++++++++++++--- 6 files changed, 442 insertions(+), 52 deletions(-) create mode 100644 src/api/position.ts diff --git a/.cursor/skills/xtrader-api-docs/SKILL.md b/.cursor/skills/xtrader-api-docs/SKILL.md index 747aedc..1532cf8 100644 --- a/.cursor/skills/xtrader-api-docs/SKILL.md +++ b/.cursor/skills/xtrader-api-docs/SKILL.md @@ -16,19 +16,38 @@ description: Interprets the XTrader API from the Swagger 2.0 spec at https://api ### 强制动作序列(必须依次执行) 1. **收到对接请求** → 立即用 `mcp_web_fetch` 或 `curl` 获取 `https://api.xtrader.vip/swagger/doc.json` -2. **第一步** → 解析 `paths[""][""]` 与 `definitions`,**在对话中输出**: +2. **检查接口是否存在** → 若 `paths[""]` 或 `paths[""][""]` **不存在**,则: + - ❌ **禁止**自行猜测或虚构数据结构并自动对接 + - ✅ **必须**在对话中明确告知用户「该接口在 doc.json 中不存在」 + - ✅ **必须**向用户询问: + - 是否仍要对接?若对接,请提供**请求参数**与**响应数据结构**(或示例 JSON) + - 用户可选择**不对接**,则流程终止 + - 仅在用户明确提供数据结构或确认对接后,才可进入第二步 +3. **第一步**(接口存在时)→ 解析 `paths[""][""]` 与 `definitions`,**在对话中输出**: - 请求参数表(Query/Body/鉴权) - 响应参数表(200 schema) - data 的 `$ref` 对应 definitions 的**完整字段表** - 输出后**明确标注「第一步完成」** -3. **第二步** → 在 `src/api/` 中根据第一步表格定义 TypeScript 类型,**不得在第一步完成前执行** -4. **第三步** → 实现请求函数并集成到页面,**不得在第二步完成前执行** +4. **第二步** → 在 `src/api/` 中根据第一步表格(或用户提供的数据结构)定义 TypeScript 类型,**不得在第一步完成前执行** +5. **第三步** → 实现请求函数并集成到页面,**不得在第二步完成前执行** ### 禁止行为 - ❌ 在对话中输出第一步结果**之前**写任何 `src/api/` 或 `src/views/` 业务代码 - ❌ 跳过第一步直接定义类型或实现请求 - ❌ 合并步骤(如边输出边写代码) +- ❌ **接口文档不存在时**:自行猜测数据结构并自动对接;必须向用户询问后再决定是否对接 + +### 接口文档不存在时的处理流程 + +当 `paths[""]` 在 doc.json 中**不存在**时: + +1. **立即停止**:不写任何对接代码,不猜测数据结构 +2. **明确告知**:在对话中说明「该接口在 Swagger doc.json 中不存在」 +3. **向用户询问**: + - 是否仍要对接?若对接,请提供请求参数与响应数据结构(或示例 JSON) + - 用户可选择**不对接**,则流程终止 +4. **仅在用户明确提供**后,才继续执行第二步、第三步 ### 第一步输出模板(必须包含) @@ -163,6 +182,7 @@ Swagger UI 页面(如 [PmEvent findPmEvent](https://api.xtrader.vip/swagger/in ## 简要检查清单 +- [ ] **接口存在性**:若 doc.json 中无该 path,已向用户询问数据结构,未擅自猜测对接 - [ ] **按规范顺序**:已先列出请求参数与响应参数,再建 Model,最后集成到页面 - [ ] 规范 URL 使用 `https://api.xtrader.vip/swagger/doc.json`,或本地缓存与之一致 - [ ] 请求 path、method、query/body 与 `paths` 一致 diff --git a/src/api/order.ts b/src/api/order.ts index 4db1f42..c8c01c3 100644 --- a/src/api/order.ts +++ b/src/api/order.ts @@ -38,12 +38,21 @@ export interface OrderListResponse { msg: string } +/** 订单状态:1=未成交(Live),2=已成交,0=已取消等 */ +export const OrderStatus = { + Live: 1, + Matched: 2, + Cancelled: 0, +} as const + /** * GET /clob/order/getOrderList 请求参数 */ export interface GetOrderListParams { page?: number pageSize?: number + /** 订单状态筛选:1=未成交 */ + status?: number startCreatedAt?: string endCreatedAt?: string marketID?: string @@ -60,8 +69,10 @@ export async function getOrderList( params: GetOrderListParams = {}, config?: { headers?: Record }, ): Promise { - const { page = 1, pageSize = 10, startCreatedAt, endCreatedAt, marketID, tokenID, userID } = params + const { page = 1, pageSize = 10, status, startCreatedAt, endCreatedAt, marketID, tokenID, userID } = + params const query: Record = { page, pageSize } + if (status != null && Number.isFinite(status)) query.status = status if (startCreatedAt != null && startCreatedAt !== '') query.startCreatedAt = startCreatedAt if (endCreatedAt != null && endCreatedAt !== '') query.endCreatedAt = endCreatedAt if (marketID != null && marketID !== '') query.marketID = marketID @@ -136,3 +147,59 @@ export function mapOrderToHistoryItem(order: ClobOrderItem): HistoryDisplayItem shares: String(size), } } + +/** 钱包 Open Orders 展示项(与 Wallet.vue OpenOrder 一致) */ +export interface OpenOrderDisplayItem { + id: string + market: string + side: 'Yes' | 'No' + outcome: string + price: string + filled: string + total: string + expiration: string + actionLabel?: string + filledDisplay?: string + orderID?: number + tokenID?: string +} + +/** OrderType GTC=0 表示 Until Cancelled */ +const OrderType = { GTC: 0, GTD: 1 } as const + +/** + * 将 ClobOrderItem 映射为钱包 Open Orders 展示项(未成交订单) + * price 为整数(已乘 10000) + */ +export function mapOrderToOpenOrderItem(order: ClobOrderItem): OpenOrderDisplayItem { + const id = String(order.ID ?? '') + const market = order.market ?? '' + const sideNum = order.side ?? Side.Buy + const side = sideNum === Side.Sell ? 'No' : 'Yes' + const outcome = order.outcome || (side === 'Yes' ? 'Yes' : 'No') + const priceBps = order.price ?? 0 + const priceCents = Math.round(priceBps / 100) + const price = `${priceCents}¢` + const originalSize = order.originalSize ?? 0 + const sizeMatched = order.sizeMatched ?? 0 + const filled = `${sizeMatched}/${originalSize}` + const totalUsd = (priceBps / 10000) * originalSize + const total = `$${totalUsd.toFixed(2)}` + const expiration = + order.orderType === OrderType.GTC ? 'Until Cancelled' : order.expiration?.toString() ?? '' + const actionLabel = sideNum === Side.Buy ? `Buy ${outcome}` : `Sell ${outcome}` + return { + id, + market, + side, + outcome, + price, + filled, + total, + expiration, + actionLabel, + filledDisplay: filled, + orderID: order.ID, + tokenID: order.assetID, + } +} diff --git a/src/api/position.ts b/src/api/position.ts new file mode 100644 index 0000000..9b1e271 --- /dev/null +++ b/src/api/position.ts @@ -0,0 +1,122 @@ +import { get } from './request' + +/** 分页结果 */ +export interface PageResult { + list: T[] + page: number + pageSize: number + total: number +} + +/** + * 持仓项(与 doc.json definitions["model.ClobPosition"] 对齐) + * GET /clob/position/getPositionList 列表项 + */ +export interface ClobPositionItem { + ID?: number + available?: number + createdAt?: string + updatedAt?: string + marketID?: string + side?: number + size?: number + tokenId?: string + userID?: number + [key: string]: unknown +} + +/** 持仓列表响应 */ +export interface PositionListResponse { + code: number + data: PageResult + msg: string +} + +/** + * GET /clob/position/getPositionList 请求参数 + */ +export interface GetPositionListParams { + page?: number + pageSize?: number + startCreatedAt?: string + endCreatedAt?: string + marketID?: string + tokenID?: string + userID?: number +} + +/** + * 分页获取持仓列表 + * GET /clob/position/getPositionList + * 需鉴权:x-token、x-user-id + */ +export async function getPositionList( + params: GetPositionListParams = {}, + config?: { headers?: Record }, +): Promise { + const { page = 1, pageSize = 10, startCreatedAt, endCreatedAt, marketID, tokenID, userID } = params + const query: Record = { page, pageSize } + if (startCreatedAt != null && startCreatedAt !== '') query.startCreatedAt = startCreatedAt + if (endCreatedAt != null && endCreatedAt !== '') query.endCreatedAt = endCreatedAt + if (marketID != null && marketID !== '') query.marketID = marketID + if (tokenID != null && tokenID !== '') query.tokenID = tokenID + if (userID != null && Number.isFinite(userID)) query.userID = userID + return get('/clob/position/getPositionList', query, config) +} + +/** 钱包 Positions 展示项(与 Wallet.vue Position 一致) */ +export interface PositionDisplayItem { + id: string + market: string + shares: string + avgNow: string + bet: string + toWin: string + value: string + valueChange?: string + valueChangePct?: string + valueChangeLoss?: boolean + sellOutcome?: string + outcomeWord?: string + iconChar?: string + iconClass?: string + outcomeTag?: string + outcomePillClass?: string +} + +/** Side: Buy=1, Sell=2 */ +const Side = { Buy: 1, Sell: 2 } as const + +/** + * 将 ClobPositionItem 映射为钱包 Position 展示项 + * model.ClobPosition 仅有 size、available、marketID、side,无价格字段,部分展示用占位 + */ +export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplayItem { + const id = String(pos.ID ?? '') + const market = pos.marketID ?? '' + const size = pos.size ?? pos.available ?? 0 + const shares = `${size} shares` + const sideNum = pos.side ?? Side.Buy + const outcomeWord = sideNum === Side.Sell ? 'No' : 'Yes' + const pillClass = sideNum === Side.Sell ? 'pill-down' : 'pill-yes' + const sellOutcome = outcomeWord + const valueUsd = size + const value = `$${valueUsd.toFixed(2)}` + const bet = value + const toWin = `$${valueUsd.toFixed(2)}` + const avgNow = '—' + const outcomeTag = `${outcomeWord} —` + return { + id, + market, + shares, + avgNow, + bet, + toWin, + value, + sellOutcome, + outcomeWord, + outcomeTag, + outcomePillClass: pillClass, + } +} diff --git a/src/api/user.ts b/src/api/user.ts index d604a32..550a81b 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -3,11 +3,49 @@ import { get } from './request' const USDC_DECIMALS = 1_000_000 /** - * getUserInfo 返回的 data(definitions system.SysUser) - * doc.json definitions["system.SysUser"] 完整字段 + * getUserInfo 返回的 data 结构(2026 更新) + * data 包含 balance、orders、positions、userInfo */ + +/** 余额项(data.balance) */ +export interface UserInfoBalance { + ID?: number + CreatedAt?: string + UpdatedAt?: string + userID?: number + marketID?: string + tokenType?: string + tokenID?: string + amount?: string + available?: string + locked?: string + version?: number +} + +/** 订单项(data.orders[]) */ +export interface UserInfoOrder { + ID?: number + CreatedAt?: string + UpdatedAt?: string + userID?: number + market?: string + status?: number + assetID?: string + side?: number + price?: number + originalSize?: number + sizeMatched?: number + outcome?: string + expiration?: number + orderType?: number + feeRateBps?: number +} + +/** 用户信息(data.userInfo) */ export interface UserInfoData { ID?: number + CreatedAt?: string + UpdatedAt?: string userName?: string nickName?: string headerImg?: string @@ -15,8 +53,6 @@ export interface UserInfoData { authorityId?: number authority?: unknown authorities?: unknown[] - createdAt?: string - updatedAt?: string email?: string phone?: string enable?: number @@ -26,12 +62,20 @@ export interface UserInfoData { promotionCode?: string puserId?: number remark?: string - originSetting?: Record + originSetting?: Record | null +} + +/** getUserInfo 完整 data */ +export interface GetUserInfoData { + balance?: UserInfoBalance + orders?: UserInfoOrder[] + positions?: unknown[] + userInfo?: UserInfoData } export interface GetUserInfoResponse { code: number - data?: UserInfoData + data?: GetUserInfoData msg?: string } diff --git a/src/stores/user.ts b/src/stores/user.ts index 00be991..2af53b6 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -70,6 +70,7 @@ export const useUserStore = defineStore('user', () => { reconnectInterval: 2000, }) sdk.onBalanceUpdate((data: BalanceData) => { + if ((data.tokenType ?? '').toUpperCase() !== 'USDC') return const avail = data.available ?? data.amount if (avail != null) { balance.value = formatUsdcBalance(String(avail)) @@ -151,36 +152,38 @@ export const useUserStore = defineStore('user', () => { } } - /** 请求用户信息(需已登录),更新 store 中的 user */ + /** 请求用户信息(需已登录),更新 store 中的 user 与 balance */ async function fetchUserInfo() { const headers = getAuthHeaders() if (!headers) return try { const res = await getUserInfo(headers) const data = res.data as Record | undefined - // 接口返回 data.userInfo 或 data.user,取实际用户对象;若仍含 userInfo 则再取一层 - let u = (data?.userInfo ?? data?.user ?? data) as Record - if (u?.userInfo && (u.ID == null && u.id == null)) { - u = u.userInfo as Record - } - if ((res.code === 0 || res.code === 200) && u) { - const rawId = u.ID ?? u.id - const numId = - typeof rawId === 'number' - ? rawId - : rawId != null - ? parseInt(String(rawId), 10) - : undefined - user.value = { - ...u, - userName: (u.userName ?? u.username) as string | undefined, - nickName: (u.nickName ?? u.nickname) as string | undefined, - headerImg: (u.headerImg ?? u.avatar ?? u.avatarUrl) as string | undefined, - id: (rawId ?? numId) as number | string | undefined, - ID: Number.isFinite(numId) ? numId : undefined, - } as UserInfo - if (token.value && user.value) saveToStorage(token.value, user.value) + if (res.code !== 0 && res.code !== 200) return + // 更新余额:data.balance.available + const bal = data?.balance as { available?: string } | undefined + if (bal?.available != null) { + balance.value = formatUsdcBalance(String(bal.available)) } + // 更新用户信息:data.userInfo + const u = (data?.userInfo ?? data?.user ?? data) as Record | undefined + if (!u) return + const rawId = u.ID ?? u.id + const numId = + typeof rawId === 'number' + ? rawId + : rawId != null + ? parseInt(String(rawId), 10) + : undefined + user.value = { + ...u, + userName: (u.userName ?? u.username) as string | undefined, + nickName: (u.nickName ?? u.nickname) as string | undefined, + headerImg: (u.headerImg ?? u.avatar ?? u.avatarUrl) as string | undefined, + id: (rawId ?? numId) as number | string | undefined, + ID: Number.isFinite(numId) ? numId : undefined, + } as UserInfo + if (token.value && user.value) saveToStorage(token.value, user.value) } catch (e) { console.error('[fetchUserInfo] 请求失败:', e) } diff --git a/src/views/Wallet.vue b/src/views/Wallet.vue index 554d858..6ae7183 100644 --- a/src/views/Wallet.vue +++ b/src/views/Wallet.vue @@ -154,7 +154,10 @@