From 25455e1a813a7c342f1f29f798c37052677d92e3 Mon Sep 17 00:00:00 2001 From: ivan Date: Sat, 14 Feb 2026 18:42:47 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/skills/xtrader-api-docs/SKILL.md | 42 +- .env | 2 +- src/App.vue | 1 + src/api/user.ts | 45 +++ src/components/MarketCard.vue | 6 + src/components/TradeComponent.vue | 489 +++++++++++++++++++---- src/stores/user.ts | 27 +- src/views/Home.vue | 10 +- src/views/Login.vue | 10 +- 9 files changed, 531 insertions(+), 101 deletions(-) diff --git a/.cursor/skills/xtrader-api-docs/SKILL.md b/.cursor/skills/xtrader-api-docs/SKILL.md index 2c79a82..747aedc 100644 --- a/.cursor/skills/xtrader-api-docs/SKILL.md +++ b/.cursor/skills/xtrader-api-docs/SKILL.md @@ -11,15 +11,43 @@ description: Interprets the XTrader API from the Swagger 2.0 spec at https://api ## ⚠️ 强制执行:接口接入三步流程 -**接入任意 XTrader 接口时,必须严格按以下顺序执行,不得跳过、不得调换、不得合并步骤。** +**接入任意 XTrader 接口时,必须严格按以下顺序执行。收到「对接 XXX 接口」请求后,立即按此流程执行,不得跳过、不得调换、不得合并。** -| 步骤 | 动作 | 强制要求 | -|------|------|----------| -| **第一步** | 从 doc.json 整理并**在对话中输出**请求参数表、响应参数表、definitions 完整结构 | 在输出第一步结果**之前**,不得写任何业务代码;必须用 `mcp_web_fetch` 或 curl 获取 doc.json,解析 `paths` 与 `definitions` | -| **第二步** | 根据第一步整理出的结构,在 `src/api/` 中定义 TypeScript 类型 | 必须等第一步输出完成后再执行;Model 必须与 definitions 对应 | -| **第三步** | 实现请求函数并集成到页面 | 必须等第二步完成后再执行 | +### 强制动作序列(必须依次执行) -**违反后果**:若跳过第一步直接写代码,会导致类型与接口文档不一致、遗漏字段或误用参数。 +1. **收到对接请求** → 立即用 `mcp_web_fetch` 或 `curl` 获取 `https://api.xtrader.vip/swagger/doc.json` +2. **第一步** → 解析 `paths[""][""]` 与 `definitions`,**在对话中输出**: + - 请求参数表(Query/Body/鉴权) + - 响应参数表(200 schema) + - data 的 `$ref` 对应 definitions 的**完整字段表** + - 输出后**明确标注「第一步完成」** +3. **第二步** → 在 `src/api/` 中根据第一步表格定义 TypeScript 类型,**不得在第一步完成前执行** +4. **第三步** → 实现请求函数并集成到页面,**不得在第二步完成前执行** + +### 禁止行为 + +- ❌ 在对话中输出第一步结果**之前**写任何 `src/api/` 或 `src/views/` 业务代码 +- ❌ 跳过第一步直接定义类型或实现请求 +- ❌ 合并步骤(如边输出边写代码) + +### 第一步输出模板(必须包含) + +``` +## 第一步:GET/POST 请求与响应参数 + +### 1. 请求参数 +| 类型 | 名称 | 类型 | 必填 | 说明 | +| ... | + +### 2. 响应参数(200) +根结构:{ code, data, msg } + +### 3. definitions["xxx"] 完整字段 +| 字段 | 类型 | 说明 | +| ... | + +第一步完成。 +``` ## 规范地址与格式 diff --git a/.env b/.env index b224459..23674f9 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ # API 基础地址,不设置时默认 https://api.xtrader.vip # 连接测试服务器 192.168.3.21:8888 时复制本文件为 .env 或 .env.local 并取消下一行注释: -VITE_API_BASE_URL=http://192.168.3.21:8888 +# VITE_API_BASE_URL=http://192.168.3.21:8888 # SSH 部署(npm run deploy),可选覆盖 # DEPLOY_HOST=38.246.250.238 diff --git a/src/App.vue b/src/App.vue index 4aac431..f6c49f5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -12,6 +12,7 @@ const currentRoute = computed(() => { onMounted(() => { if (userStore.isLoggedIn) { + userStore.fetchUserInfo() userStore.fetchUsdcBalance() } }) diff --git a/src/api/user.ts b/src/api/user.ts index f256cf5..d604a32 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -2,6 +2,51 @@ import { get } from './request' const USDC_DECIMALS = 1_000_000 +/** + * getUserInfo 返回的 data(definitions system.SysUser) + * doc.json definitions["system.SysUser"] 完整字段 + */ +export interface UserInfoData { + ID?: number + userName?: string + nickName?: string + headerImg?: string + uuid?: string + authorityId?: number + authority?: unknown + authorities?: unknown[] + createdAt?: string + updatedAt?: string + email?: string + phone?: string + enable?: number + walletAddress?: string + externalWalletAddress?: string + walletPrivEnc?: string + promotionCode?: string + puserId?: number + remark?: string + originSetting?: Record +} + +export interface GetUserInfoResponse { + code: number + data?: UserInfoData + msg?: string +} + +/** + * GET /user/getUserInfo + * 获取当前用户信息,需鉴权(x-token) + */ +export async function getUserInfo( + authHeaders: Record, +): Promise { + return get('/user/getUserInfo', undefined, { + headers: authHeaders, + }) +} + /** getUsdcBalance 返回的 data 结构 */ export interface UsdcBalanceData { amount: string diff --git a/src/components/MarketCard.vue b/src/components/MarketCard.vue index 336686a..6892b5d 100644 --- a/src/components/MarketCard.vue +++ b/src/components/MarketCard.vue @@ -177,6 +177,8 @@ const emit = defineEmits<{ marketId?: string outcomeTitle?: string clobTokenIds?: string[] + yesLabel?: string + noLabel?: string }, ] }>() @@ -300,6 +302,8 @@ function openTradeSingle(side: 'yes' | 'no') { title: props.marketTitle, marketId: props.marketId, clobTokenIds: props.clobTokenIds, + yesLabel: props.yesLabel, + noLabel: props.noLabel, }) } @@ -310,6 +314,8 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) { marketId: outcome.marketId, outcomeTitle: outcome.title, clobTokenIds: outcome.clobTokenIds, + yesLabel: outcome.yesLabel, + noLabel: outcome.noLabel, }) } diff --git a/src/components/TradeComponent.vue b/src/components/TradeComponent.vue index 535d5ff..675abb1 100644 --- a/src/components/TradeComponent.vue +++ b/src/components/TradeComponent.vue @@ -56,6 +56,33 @@ + + +
@@ -68,7 +95,7 @@ To win mdi-currency-usd - ${{ toWinValue }} + {{ toWinValue }}
@@ -78,15 +105,29 @@ You'll receive mdi-currency-usd - ${{ totalPrice }} + {{ totalPrice }} + + +
+ + Avg. Price {{ avgPriceCents }}¢ + mdi-information-outline

{{ orderError }}

- + + Deposit + + - + @@ -269,7 +362,7 @@ To win mdi-currency-usd - ${{ toWinValue }} + {{ toWinValue }} @@ -279,7 +372,7 @@ You'll receive mdi-currency-usd - ${{ totalPrice }} + {{ totalPrice }} @@ -348,6 +441,32 @@ >{{ noLabel }} {{ noPriceCents }}¢ + +
@@ -364,15 +483,29 @@
You'll receivemdi-currency-usd ${{ + >mdi-currency-usd {{ totalPrice }}
+
+ + Avg. Price {{ avgPriceCents }}¢ + mdi-information-outline + +

{{ orderError }}

+ Deposit + + {{ noLabel }} {{ noPriceCents }}¢ -
-
-
- AmountBalance ${{ balance.toFixed(2) }} + + + + @@ -530,7 +715,7 @@
You'll receivemdi-currency-usd ${{ totalPrice }}mdi-currency-usd {{ totalPrice }}
@@ -624,6 +809,32 @@ >{{ noLabel }} {{ noPriceCents }}¢
+ +
@@ -641,15 +852,29 @@
You'll receivemdi-currency-usd ${{ + >mdi-currency-usd {{ totalPrice }}
+
+ + Avg. Price {{ avgPriceCents }}¢ + mdi-information-outline + +

{{ orderError }}

+ Deposit + + {{ noLabel }} {{ noPriceCents }}¢
-
-
-
- AmountBalance ${{ balance.toFixed(2) }} + + + + @@ -806,7 +1083,7 @@
You'll receivemdi-currency-usd ${{ + >mdi-currency-usd {{ totalPrice }} @@ -850,8 +1127,8 @@

- Merge a share of Yes and No to get 1 USDC. You can do this to save cost when trying to get - rid of a position. + Merge a share of {{ yesLabel }} and {{ noLabel }} to get 1 USDC. You can do this to save + cost when trying to get rid of a position.

@@ -870,7 +1147,7 @@

- Please select a market first (e.g. click Buy Yes/No on a market). + Please select a market first (e.g. click Buy {{ yesLabel }}/{{ noLabel }} on a market).

{{ mergeError }}

@@ -913,8 +1190,8 @@

- Use USDC to get one share of Yes and one share of No for this market. 1 USDC ≈ 1 complete - set. + Use USDC to get one share of {{ yesLabel }} and one share of {{ noLabel }} for this + market. 1 USDC ≈ 1 complete set.

@@ -930,7 +1207,7 @@ />

- Please select a market first (e.g. click Buy Yes/No on a market). + Please select a market first (e.g. click Buy {{ yesLabel }}/{{ noLabel }} on a market).

{{ splitError }}

@@ -1117,7 +1394,12 @@ const expirationOptions = ref(['5m', '15m', '30m', '1h', '2h', '4h', '8h', '12h' // Market mode state const isMarketMode = computed(() => limitType.value === 'Market') const amount = ref(0) // Market mode amount -const balance = ref(0) // Market mode balance +/** 余额(从 userStore 同步) */ +const balance = computed(() => { + const s = userStore.balance + const n = parseFloat(String(s ?? '0')) + return Number.isFinite(n) ? n : 0 +}) const orderLoading = ref(false) const orderError = ref('') @@ -1147,6 +1429,12 @@ const totalPrice = computed(() => { return (limitPrice.value * shares.value).toFixed(2) }) +/** Sell 模式下的平均单价(¢) */ +const avgPriceCents = computed(() => { + const p = limitPrice.value + return (p * 100).toFixed(1) +}) + /** To win = 份数 × 1U = shares × 1 USDC */ const toWinValue = computed(() => { if (isMarketMode.value) { @@ -1162,7 +1450,9 @@ const toWinValue = computed(() => { const actionButtonText = computed(() => { const label = selectedOption.value === 'yes' ? yesLabel.value : noLabel.value - return `${activeTab.value} ${label}` + const tab = activeTab.value + const tabCapitalized = tab.charAt(0).toUpperCase() + tab.slice(1) + return `${tabCapitalized} ${label}` }) function applyInitialOption(option: 'yes' | 'no') { @@ -1316,6 +1606,17 @@ const setMaxAmount = () => { amount.value = balance.value } +/** Buy 模式:余额是否足够(>= 所需金额且不为 0)。cost:balance>0 时用 totalPrice,否则用 amount */ +const canAffordBuy = computed(() => { + const bal = balance.value + if (bal <= 0) return false + const cost = bal > 0 ? parseFloat(totalPrice.value) || 0 : amount.value + return bal >= cost +}) + +/** Buy 模式且余额不足时显示 Deposit,否则显示 Buy Yes/No */ +const showDepositForBuy = computed(() => !canAffordBuy.value) + const deposit = () => { console.log('Depositing amount:', amount.value) // 实际应用中,这里应该调用存款API @@ -1663,6 +1964,17 @@ async function submitOrder() { color: #3d8b40; } +.avg-price-row { + font-size: 12px; + color: #666; +} + +.avg-price-row .info-icon { + margin-left: 4px; + vertical-align: middle; + opacity: 0.7; +} + .expiration-header { display: flex; justify-content: space-between; @@ -1783,6 +2095,13 @@ async function submitOrder() { box-shadow: none !important; } +.deposit-btn--secondary { + margin-top: 8px; + background-color: transparent; + color: #0066cc; + border: 1px solid #0066cc; +} + /* 移动端底部交易栏(红框样式) */ .mobile-trade-bar-spacer { height: 72px; diff --git a/src/stores/user.ts b/src/stores/user.ts index bd205fe..0c850c5 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -1,6 +1,6 @@ import { ref, computed } from 'vue' import { defineStore } from 'pinia' -import { getUsdcBalance, formatUsdcBalance } from '@/api/user' +import { getUsdcBalance, formatUsdcBalance, getUserInfo } from '@/api/user' export interface UserInfo { /** 用户 ID(API 可能返回 id 或 ID) */ @@ -89,6 +89,30 @@ export const useUserStore = defineStore('user', () => { } } + /** 请求用户信息(需已登录),更新 store 中的 user */ + async function fetchUserInfo() { + const headers = getAuthHeaders() + if (!headers) return + try { + const res = await getUserInfo(headers) + console.log('[fetchUserInfo] 接口响应:', JSON.stringify(res, null, 2)) + if ((res.code === 0 || res.code === 200) && res.data) { + const u = res.data + user.value = { + id: u.ID, + ID: u.ID, + userName: u.userName, + nickName: u.nickName, + headerImg: u.headerImg, + ...u, + } + if (token.value && user.value) saveToStorage(token.value, user.value) + } + } catch (e) { + console.error('[fetchUserInfo] 请求失败:', e) + } + } + return { token, user, @@ -99,5 +123,6 @@ export const useUserStore = defineStore('user', () => { logout, getAuthHeaders, fetchUsdcBalance, + fetchUserInfo, } }) diff --git a/src/views/Home.vue b/src/views/Home.vue index 3590e31..42dd9a6 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -506,12 +506,14 @@ const tradeDialogMarket = ref<{ title: string marketId?: string clobTokenIds?: string[] + yesLabel?: string + noLabel?: string } | null>(null) const scrollRef = ref(null) function onCardOpenTrade( side: 'yes' | 'no', - market?: { id: string; title: string; marketId?: string }, + market?: { id: string; title: string; marketId?: string; yesLabel?: string; noLabel?: string }, ) { tradeDialogSide.value = side tradeDialogMarket.value = market ?? null @@ -526,7 +528,11 @@ 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, clobTokenIds: m.clobTokenIds } + const outcomes = + m.yesLabel != null || m.noLabel != null + ? [m.yesLabel ?? 'Yes', m.noLabel ?? 'No'] + : undefined + return { marketId, yesPrice, noPrice, title: m.title, clobTokenIds: m.clobTokenIds, outcomes } }) const sentinelRef = ref(null) diff --git a/src/views/Login.vue b/src/views/Login.vue index e278344..9dc214f 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -177,13 +177,13 @@ const connectWithWallet = async () => { walletAddress, }, ) - console.log('Login API response:', loginData) + // 调试:打印登录信息数据结构 + console.log('[walletLogin] 登录响应:', JSON.stringify(loginData, null, 2)) if (loginData.code === 0 && loginData.data) { - userStore.setUser({ - token: loginData.data.token, - user: loginData.data.user, - }) + const { token, user } = loginData.data + console.log('[walletLogin] 存入 store 的 user:', JSON.stringify(user, null, 2)) + userStore.setUser({ token, user }) userStore.fetchUsdcBalance() }