Compare commits

...

2 Commits

Author SHA1 Message Date
ivan
25455e1a81 优化:交易组件优化 2026-02-14 18:42:47 +08:00
ivan
c0c423b46a 优化:UI调整 2026-02-14 17:52:48 +08:00
12 changed files with 677 additions and 140 deletions

View File

@ -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["<path>"]["<method>"]``definitions`**在对话中输出**
- 请求参数表Query/Body/鉴权)
- 响应参数表200 schema
- data 的 `$ref` 对应 definitions 的**完整字段表**
- 输出后**明确标注「第一步完成」**
3. **第二步** → 在 `src/api/` 中根据第一步表格定义 TypeScript 类型,**不得在第一步完成前执行**
4. **第三步** → 实现请求函数并集成到页面,**不得在第二步完成前执行**
### 禁止行为
- ❌ 在对话中输出第一步结果**之前**写任何 `src/api/``src/views/` 业务代码
- ❌ 跳过第一步直接定义类型或实现请求
- ❌ 合并步骤(如边输出边写代码)
### 第一步输出模板(必须包含)
```
## 第一步GET/POST <path> 请求与响应参数
### 1. 请求参数
| 类型 | 名称 | 类型 | 必填 | 说明 |
| ... |
### 2. 响应参数200
根结构:{ code, data, msg }
### 3. definitions["xxx"] 完整字段
| 字段 | 类型 | 说明 |
| ... |
第一步完成。
```
## 规范地址与格式 ## 规范地址与格式

2
.env
View File

@ -1,6 +1,6 @@
# API 基础地址,不设置时默认 https://api.xtrader.vip # API 基础地址,不设置时默认 https://api.xtrader.vip
# 连接测试服务器 192.168.3.21:8888 时复制本文件为 .env 或 .env.local 并取消下一行注释: # 连接测试服务器 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可选覆盖 # SSH 部署npm run deploy可选覆盖
# DEPLOY_HOST=38.246.250.238 # DEPLOY_HOST=38.246.250.238

View File

@ -12,6 +12,7 @@ const currentRoute = computed(() => {
onMounted(() => { onMounted(() => {
if (userStore.isLoggedIn) { if (userStore.isLoggedIn) {
userStore.fetchUserInfo()
userStore.fetchUsdcBalance() userStore.fetchUsdcBalance()
} }
}) })

View File

@ -2,6 +2,51 @@ import { get } from './request'
const USDC_DECIMALS = 1_000_000 const USDC_DECIMALS = 1_000_000
/**
* getUserInfo datadefinitions 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<string, unknown>
}
export interface GetUserInfoResponse {
code: number
data?: UserInfoData
msg?: string
}
/**
* GET /user/getUserInfo
* x-token
*/
export async function getUserInfo(
authHeaders: Record<string, string>,
): Promise<GetUserInfoResponse> {
return get<GetUserInfoResponse>('/user/getUserInfo', undefined, {
headers: authHeaders,
})
}
/** getUsdcBalance 返回的 data 结构 */ /** getUsdcBalance 返回的 data 结构 */
export interface UsdcBalanceData { export interface UsdcBalanceData {
amount: string amount: string

View File

@ -177,6 +177,8 @@ const emit = defineEmits<{
marketId?: string marketId?: string
outcomeTitle?: string outcomeTitle?: string
clobTokenIds?: string[] clobTokenIds?: string[]
yesLabel?: string
noLabel?: string
}, },
] ]
}>() }>()
@ -300,6 +302,8 @@ function openTradeSingle(side: 'yes' | 'no') {
title: props.marketTitle, title: props.marketTitle,
marketId: props.marketId, marketId: props.marketId,
clobTokenIds: props.clobTokenIds, clobTokenIds: props.clobTokenIds,
yesLabel: props.yesLabel,
noLabel: props.noLabel,
}) })
} }
@ -310,6 +314,8 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
marketId: outcome.marketId, marketId: outcome.marketId,
outcomeTitle: outcome.title, outcomeTitle: outcome.title,
clobTokenIds: outcome.clobTokenIds, clobTokenIds: outcome.clobTokenIds,
yesLabel: outcome.yesLabel,
noLabel: outcome.noLabel,
}) })
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<v-card class="order-book"> <v-card class="order-book" elevation="0">
<!-- Header --> <!-- Header -->
<div class="order-book-header"> <div class="order-book-header">
<h3 class="order-book-title">Order Book</h3> <h3 class="order-book-title">Order Book</h3>
@ -198,7 +198,8 @@ const maxBidsTotal = computed(() => {
width: 100%; width: 100%;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: none;
border: 1px solid #e7e7e7;
} }
.horizontal-progress-bar { .horizontal-progress-bar {

View File

@ -1,6 +1,6 @@
<template> <template>
<!-- 桌面端完整交易卡片 --> <!-- 桌面端完整交易卡片扁平化 -->
<v-card v-if="!mobile" class="trade-component"> <v-card v-if="!mobile" class="trade-component" elevation="0">
<!-- Header --> <!-- Header -->
<div class="header"> <div class="header">
<v-tabs v-model="activeTab" class="buy-sell-tabs minimal-tabs" density="compact"> <v-tabs v-model="activeTab" class="buy-sell-tabs minimal-tabs" density="compact">
@ -44,7 +44,7 @@
text text
@click="handleOptionChange('yes')" @click="handleOptionChange('yes')"
> >
Yes {{ yesPriceCents }}¢ {{ yesLabel }} {{ yesPriceCents }}¢
</v-btn> </v-btn>
<v-btn <v-btn
class="no-btn" class="no-btn"
@ -52,10 +52,37 @@
text text
@click="handleOptionChange('no')" @click="handleOptionChange('no')"
> >
No {{ noPriceCents }}¢ {{ noLabel }} {{ noPriceCents }}¢
</v-btn> </v-btn>
</div> </div>
<!-- Sell Market: Shares input + 25%/50%/Max -->
<template v-if="activeTab === 'sell'">
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<div class="shares-input">
<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 class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
</div>
</div>
</template>
<!-- Total and To Win (Buy模式) You'll receive (Sell模式) --> <!-- Total and To Win (Buy模式) You'll receive (Sell模式) -->
<div class="total-section"> <div class="total-section">
<!-- Buy模式 --> <!-- Buy模式 -->
@ -68,7 +95,7 @@
<span class="label">To win</span> <span class="label">To win</span>
<span class="to-win-value"> <span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon> <v-icon size="16" color="green">mdi-currency-usd</v-icon>
$20 {{ toWinValue }}
</span> </span>
</div> </div>
</template> </template>
@ -78,15 +105,29 @@
<span class="label">You'll receive</span> <span class="label">You'll receive</span>
<span class="to-win-value"> <span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon> <v-icon size="16" color="green">mdi-currency-usd</v-icon>
${{ totalPrice }} {{ totalPrice }}
</span>
</div>
<div class="total-row avg-price-row">
<span class="label">
Avg. Price {{ avgPriceCents }}¢
<v-icon size="14" class="info-icon">mdi-information-outline</v-icon>
</span> </span>
</div> </div>
</template> </template>
</div> </div>
<p v-if="orderError" class="order-error">{{ orderError }}</p> <p v-if="orderError" class="order-error">{{ orderError }}</p>
<!-- Action Button --> <!-- Action Button: Buy 余额足够显示 Buy Yes/No不足显示 DepositSell 只显示 Sell Yes/No -->
<v-btn <v-btn
v-if="activeTab === 'buy' && showDepositForBuy"
class="deposit-btn"
@click="deposit"
>
Deposit
</v-btn>
<v-btn
v-else
class="action-btn" class="action-btn"
:loading="orderLoading" :loading="orderLoading"
:disabled="orderLoading" :disabled="orderLoading"
@ -96,7 +137,7 @@
</v-btn> </v-btn>
</template> </template>
<!-- Balance <= 0: Show Deposit Interface --> <!-- Balance <= 0: Show Deposit Interface (Buy) Sell UI (Sell) -->
<template v-else> <template v-else>
<!-- Price Options --> <!-- Price Options -->
<div class="price-options"> <div class="price-options">
@ -106,7 +147,7 @@
text text
@click="handleOptionChange('yes')" @click="handleOptionChange('yes')"
> >
Yes {{ yesPriceCents }}¢ {{ yesLabel }} {{ yesPriceCents }}¢
</v-btn> </v-btn>
<v-btn <v-btn
class="no-btn" class="no-btn"
@ -114,11 +155,12 @@
text text
@click="handleOptionChange('no')" @click="handleOptionChange('no')"
> >
No {{ noPriceCents }}¢ {{ noLabel }} {{ noPriceCents }}¢
</v-btn> </v-btn>
</div> </div>
<!-- Amount Section --> <!-- Buy: Amount Section -->
<template v-if="activeTab === 'buy'">
<div class="input-group"> <div class="input-group">
<div class="amount-header"> <div class="amount-header">
<div> <div>
@ -135,10 +177,70 @@
<v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn> <v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn> <v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn>
</div> </div>
</div>
<!-- Deposit Button --> <!-- To win份数 × 1U -->
<v-btn class="deposit-btn" @click="deposit"> Deposit </v-btn> <div v-if="amount > 0" class="total-row amount-to-win-row">
<span class="label">To win</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ toWinValue }}
</span>
</div>
</div>
<!-- Buy 余额不足时显示 Deposit -->
<v-btn class="deposit-btn" @click="deposit">Deposit</v-btn>
</template>
<!-- Sell: Shares + To receive + Avg. Price只显示 Sell Yes/No Deposit -->
<template v-else>
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<div class="shares-input">
<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 class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
</div>
</div>
<div class="total-section">
<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>
<div class="total-row avg-price-row">
<span class="label">
Avg. Price {{ avgPriceCents }}¢
<v-icon size="14" class="info-icon">mdi-information-outline</v-icon>
</span>
</div>
</div>
<!-- Sell 只显示 Sell Yes/No -->
<v-btn
class="action-btn"
:loading="orderLoading"
:disabled="orderLoading"
@click="submitOrder"
>
{{ actionButtonText }}
</v-btn>
</template>
</template> </template>
</template> </template>
@ -152,7 +254,7 @@
text text
@click="handleOptionChange('yes')" @click="handleOptionChange('yes')"
> >
Yes {{ yesPriceCents }}¢ {{ yesLabel }} {{ yesPriceCents }}¢
</v-btn> </v-btn>
<v-btn <v-btn
class="no-btn" class="no-btn"
@ -160,7 +262,7 @@
text text
@click="handleOptionChange('no')" @click="handleOptionChange('no')"
> >
No {{ noPriceCents }}¢ {{ noLabel }} {{ noPriceCents }}¢
</v-btn> </v-btn>
</div> </div>
@ -260,7 +362,7 @@
<span class="label">To win</span> <span class="label">To win</span>
<span class="to-win-value"> <span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon> <v-icon size="16" color="green">mdi-currency-usd</v-icon>
$20 {{ toWinValue }}
</span> </span>
</div> </div>
</template> </template>
@ -270,7 +372,7 @@
<span class="label">You'll receive</span> <span class="label">You'll receive</span>
<span class="to-win-value"> <span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon> <v-icon size="16" color="green">mdi-currency-usd</v-icon>
${{ totalPrice }} {{ totalPrice }}
</span> </span>
</div> </div>
</template> </template>
@ -288,9 +390,9 @@
</template> </template>
</v-card> </v-card>
<!-- 移动端且由首页卡片嵌入只渲染交易表单无底部栏无内部 sheet --> <!-- 移动端且由首页卡片嵌入只渲染交易表单无底部栏无内部 sheet扁平化 -->
<template v-else-if="embeddedInSheet"> <template v-else-if="embeddedInSheet">
<v-sheet class="trade-sheet-paper trade-sheet-paper--embedded" rounded="lg"> <v-sheet class="trade-sheet-paper trade-sheet-paper--embedded" rounded="lg" elevation="0">
<div class="trade-component trade-sheet-inner"> <div class="trade-component trade-sheet-inner">
<div class="header"> <div class="header">
<v-tabs v-model="activeTab" class="buy-sell-tabs minimal-tabs" density="compact"> <v-tabs v-model="activeTab" class="buy-sell-tabs minimal-tabs" density="compact">
@ -329,16 +431,42 @@
:class="{ active: selectedOption === 'yes' }" :class="{ active: selectedOption === 'yes' }"
text text
@click="handleOptionChange('yes')" @click="handleOptionChange('yes')"
>Yes {{ yesPriceCents }}¢</v-btn >{{ yesLabel }} {{ yesPriceCents }}¢</v-btn
> >
<v-btn <v-btn
class="no-btn" class="no-btn"
:class="{ active: selectedOption === 'no' }" :class="{ active: selectedOption === 'no' }"
text text
@click="handleOptionChange('no')" @click="handleOptionChange('no')"
>No {{ noPriceCents }}¢</v-btn >{{ noLabel }} {{ noPriceCents }}¢</v-btn
> >
</div> </div>
<!-- Sell Market: Shares input + 25%/50%/Max -->
<template v-if="activeTab === 'sell'">
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<div class="shares-input">
<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 class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
</div>
</div>
</template>
<div class="total-section"> <div class="total-section">
<template v-if="activeTab === 'buy'"> <template v-if="activeTab === 'buy'">
<div class="total-row"> <div class="total-row">
@ -347,7 +475,7 @@
<div class="total-row"> <div class="total-row">
<span class="label">To win</span <span class="label">To win</span
><span class="to-win-value" ><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> $20</span ><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span
> >
</div> </div>
</template> </template>
@ -355,15 +483,29 @@
<div class="total-row"> <div class="total-row">
<span class="label">You'll receive</span <span class="label">You'll receive</span
><span class="to-win-value" ><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{ ><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{
totalPrice totalPrice
}}</span }}</span
> >
</div> </div>
<div class="total-row avg-price-row">
<span class="label">
Avg. Price {{ avgPriceCents }}¢
<v-icon size="14" class="info-icon">mdi-information-outline</v-icon>
</span>
</div>
</template> </template>
</div> </div>
<p v-if="orderError" class="order-error">{{ orderError }}</p> <p v-if="orderError" class="order-error">{{ orderError }}</p>
<v-btn <v-btn
v-if="activeTab === 'buy' && showDepositForBuy"
class="deposit-btn"
@click="deposit"
>
Deposit
</v-btn>
<v-btn
v-else
class="action-btn" class="action-btn"
:loading="orderLoading" :loading="orderLoading"
:disabled="orderLoading" :disabled="orderLoading"
@ -378,16 +520,18 @@
:class="{ active: selectedOption === 'yes' }" :class="{ active: selectedOption === 'yes' }"
text text
@click="handleOptionChange('yes')" @click="handleOptionChange('yes')"
>Yes {{ yesPriceCents }}¢</v-btn >{{ yesLabel }} {{ yesPriceCents }}¢</v-btn
> >
<v-btn <v-btn
class="no-btn" class="no-btn"
:class="{ active: selectedOption === 'no' }" :class="{ active: selectedOption === 'no' }"
text text
@click="handleOptionChange('no')" @click="handleOptionChange('no')"
>No {{ noPriceCents }}¢</v-btn >{{ noLabel }} {{ noPriceCents }}¢</v-btn
> >
</div> </div>
<!-- Buy: Amount Section -->
<template v-if="activeTab === 'buy'">
<div class="input-group"> <div class="input-group">
<div class="amount-header"> <div class="amount-header">
<div> <div>
@ -402,9 +546,66 @@
<v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn> <v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn> <v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn>
</div> </div>
<div v-if="amount > 0" class="total-row amount-to-win-row">
<span class="label">To win</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ toWinValue }}
</span>
</div>
</div> </div>
<v-btn class="deposit-btn" @click="deposit">Deposit</v-btn> <v-btn class="deposit-btn" @click="deposit">Deposit</v-btn>
</template> </template>
<!-- Sell: Shares + To receive + Avg. Price -->
<template v-else>
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<div class="shares-input">
<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 class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
</div>
</div>
<div class="total-section">
<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>
<div class="total-row avg-price-row">
<span class="label">
Avg. Price {{ avgPriceCents }}¢
<v-icon size="14" class="info-icon">mdi-information-outline</v-icon>
</span>
</div>
</div>
<v-btn
class="action-btn"
:loading="orderLoading"
:disabled="orderLoading"
@click="submitOrder"
>
{{ actionButtonText }}
</v-btn>
</template>
</template>
</template> </template>
<template v-else> <template v-else>
<div class="price-options hide-in-mobile-sheet"> <div class="price-options hide-in-mobile-sheet">
@ -413,14 +614,14 @@
:class="{ active: selectedOption === 'yes' }" :class="{ active: selectedOption === 'yes' }"
text text
@click="handleOptionChange('yes')" @click="handleOptionChange('yes')"
>Yes {{ yesPriceCents }}¢</v-btn >{{ yesLabel }} {{ yesPriceCents }}¢</v-btn
> >
<v-btn <v-btn
class="no-btn" class="no-btn"
:class="{ active: selectedOption === 'no' }" :class="{ active: selectedOption === 'no' }"
text text
@click="handleOptionChange('no')" @click="handleOptionChange('no')"
>No {{ noPriceCents }}¢</v-btn >{{ noLabel }} {{ noPriceCents }}¢</v-btn
> >
</div> </div>
<div class="input-group limit-price-group"> <div class="input-group limit-price-group">
@ -506,7 +707,7 @@
<div class="total-row"> <div class="total-row">
<span class="label">To win</span <span class="label">To win</span
><span class="to-win-value" ><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> $20</span ><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span
> >
</div> </div>
</template> </template>
@ -514,7 +715,7 @@
<div class="total-row"> <div class="total-row">
<span class="label">You'll receive</span <span class="label">You'll receive</span
><span class="to-win-value" ><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{ totalPrice }}</span ><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ totalPrice }}</span
> >
</div> </div>
</template> </template>
@ -544,7 +745,7 @@
block block
@click="openSheet('yes')" @click="openSheet('yes')"
> >
Buy Yes {{ yesPriceCents }}¢ Buy {{ yesLabel }} {{ yesPriceCents }}¢
</v-btn> </v-btn>
<v-btn <v-btn
class="mobile-bar-btn mobile-bar-no" class="mobile-bar-btn mobile-bar-no"
@ -554,7 +755,7 @@
block block
@click="openSheet('no')" @click="openSheet('no')"
> >
Buy No {{ noPriceCents }}¢ Buy {{ noLabel }} {{ noPriceCents }}¢
</v-btn> </v-btn>
</div> </div>
@ -598,16 +799,42 @@
:class="{ active: selectedOption === 'yes' }" :class="{ active: selectedOption === 'yes' }"
text text
@click="handleOptionChange('yes')" @click="handleOptionChange('yes')"
>Yes {{ yesPriceCents }}¢</v-btn >{{ yesLabel }} {{ yesPriceCents }}¢</v-btn
> >
<v-btn <v-btn
class="no-btn" class="no-btn"
:class="{ active: selectedOption === 'no' }" :class="{ active: selectedOption === 'no' }"
text text
@click="handleOptionChange('no')" @click="handleOptionChange('no')"
>No {{ noPriceCents }}¢</v-btn >{{ noLabel }} {{ noPriceCents }}¢</v-btn
> >
</div> </div>
<!-- Sell Market: Shares input + 25%/50%/Max -->
<template v-if="activeTab === 'sell'">
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<div class="shares-input">
<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 class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
</div>
</div>
</template>
<div class="total-section"> <div class="total-section">
<template v-if="activeTab === 'buy'"> <template v-if="activeTab === 'buy'">
<div class="total-row"> <div class="total-row">
@ -617,7 +844,7 @@
<div class="total-row"> <div class="total-row">
<span class="label">To win</span <span class="label">To win</span
><span class="to-win-value" ><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> $20</span ><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span
> >
</div> </div>
</template> </template>
@ -625,15 +852,29 @@
<div class="total-row"> <div class="total-row">
<span class="label">You'll receive</span <span class="label">You'll receive</span
><span class="to-win-value" ><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{ ><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{
totalPrice totalPrice
}}</span }}</span
> >
</div> </div>
<div class="total-row avg-price-row">
<span class="label">
Avg. Price {{ avgPriceCents }}¢
<v-icon size="14" class="info-icon">mdi-information-outline</v-icon>
</span>
</div>
</template> </template>
</div> </div>
<p v-if="orderError" class="order-error">{{ orderError }}</p> <p v-if="orderError" class="order-error">{{ orderError }}</p>
<v-btn <v-btn
v-if="activeTab === 'buy' && showDepositForBuy"
class="deposit-btn"
@click="deposit"
>
Deposit
</v-btn>
<v-btn
v-else
class="action-btn" class="action-btn"
:loading="orderLoading" :loading="orderLoading"
:disabled="orderLoading" :disabled="orderLoading"
@ -648,16 +889,18 @@
:class="{ active: selectedOption === 'yes' }" :class="{ active: selectedOption === 'yes' }"
text text
@click="handleOptionChange('yes')" @click="handleOptionChange('yes')"
>Yes {{ yesPriceCents }}¢</v-btn >{{ yesLabel }} {{ yesPriceCents }}¢</v-btn
> >
<v-btn <v-btn
class="no-btn" class="no-btn"
:class="{ active: selectedOption === 'no' }" :class="{ active: selectedOption === 'no' }"
text text
@click="handleOptionChange('no')" @click="handleOptionChange('no')"
>No {{ noPriceCents }}¢</v-btn >{{ noLabel }} {{ noPriceCents }}¢</v-btn
> >
</div> </div>
<!-- Buy: Amount Section -->
<template v-if="activeTab === 'buy'">
<div class="input-group"> <div class="input-group">
<div class="amount-header"> <div class="amount-header">
<div> <div>
@ -672,9 +915,66 @@
<v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn> <v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn> <v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn>
</div> </div>
<div v-if="amount > 0" class="total-row amount-to-win-row">
<span class="label">To win</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ toWinValue }}
</span>
</div>
</div> </div>
<v-btn class="deposit-btn" @click="deposit">Deposit</v-btn> <v-btn class="deposit-btn" @click="deposit">Deposit</v-btn>
</template> </template>
<!-- Sell: Shares + To receive + Avg. Price -->
<template v-else>
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<div class="shares-input">
<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 class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
</div>
</div>
<div class="total-section">
<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>
<div class="total-row avg-price-row">
<span class="label">
Avg. Price {{ avgPriceCents }}¢
<v-icon size="14" class="info-icon">mdi-information-outline</v-icon>
</span>
</div>
</div>
<v-btn
class="action-btn"
:loading="orderLoading"
:disabled="orderLoading"
@click="submitOrder"
>
{{ actionButtonText }}
</v-btn>
</template>
</template>
</template> </template>
<template v-else> <template v-else>
<div class="price-options hide-in-mobile-sheet"> <div class="price-options hide-in-mobile-sheet">
@ -683,14 +983,14 @@
:class="{ active: selectedOption === 'yes' }" :class="{ active: selectedOption === 'yes' }"
text text
@click="handleOptionChange('yes')" @click="handleOptionChange('yes')"
>Yes {{ yesPriceCents }}¢</v-btn >{{ yesLabel }} {{ yesPriceCents }}¢</v-btn
> >
<v-btn <v-btn
class="no-btn" class="no-btn"
:class="{ active: selectedOption === 'no' }" :class="{ active: selectedOption === 'no' }"
text text
@click="handleOptionChange('no')" @click="handleOptionChange('no')"
>No {{ noPriceCents }}¢</v-btn >{{ noLabel }} {{ noPriceCents }}¢</v-btn
> >
</div> </div>
<div class="input-group limit-price-group"> <div class="input-group limit-price-group">
@ -775,7 +1075,7 @@
<div class="total-row"> <div class="total-row">
<span class="label">To win</span <span class="label">To win</span
><span class="to-win-value" ><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> $20</span ><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span
> >
</div> </div>
</template> </template>
@ -783,7 +1083,7 @@
<div class="total-row"> <div class="total-row">
<span class="label">You'll receive</span <span class="label">You'll receive</span
><span class="to-win-value" ><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{ ><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{
totalPrice totalPrice
}}</span }}</span
> >
@ -812,7 +1112,7 @@
content-class="merge-dialog" content-class="merge-dialog"
transition="dialog-transition" transition="dialog-transition"
> >
<v-card class="merge-dialog-card" rounded="lg"> <v-card class="merge-dialog-card" rounded="lg" elevation="0">
<div class="merge-dialog-header"> <div class="merge-dialog-header">
<h3 class="merge-dialog-title">Merge shares</h3> <h3 class="merge-dialog-title">Merge shares</h3>
<v-btn <v-btn
@ -827,8 +1127,8 @@
</div> </div>
<v-card-text class="merge-dialog-body"> <v-card-text class="merge-dialog-body">
<p class="merge-dialog-desc"> <p class="merge-dialog-desc">
Merge a share of Yes and No to get 1 USDC. You can do this to save cost when trying to get Merge a share of {{ yesLabel }} and {{ noLabel }} to get 1 USDC. You can do this to save
rid of a position. cost when trying to get rid of a position.
</p> </p>
<div class="merge-amount-row"> <div class="merge-amount-row">
<label class="merge-amount-label">Amount</label> <label class="merge-amount-label">Amount</label>
@ -847,7 +1147,7 @@
<button type="button" class="merge-max-link" @click="setMergeMax">Max</button> <button type="button" class="merge-max-link" @click="setMergeMax">Max</button>
</p> </p>
<p v-if="!props.market?.marketId" class="merge-no-market"> <p v-if="!props.market?.marketId" class="merge-no-market">
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).
</p> </p>
<p v-if="mergeError" class="merge-error">{{ mergeError }}</p> <p v-if="mergeError" class="merge-error">{{ mergeError }}</p>
</v-card-text> </v-card-text>
@ -875,7 +1175,7 @@
content-class="split-dialog" content-class="split-dialog"
transition="dialog-transition" transition="dialog-transition"
> >
<v-card class="split-dialog-card" rounded="lg"> <v-card class="split-dialog-card" rounded="lg" elevation="0">
<div class="split-dialog-header"> <div class="split-dialog-header">
<h3 class="split-dialog-title">Split</h3> <h3 class="split-dialog-title">Split</h3>
<v-btn <v-btn
@ -890,8 +1190,8 @@
</div> </div>
<v-card-text class="split-dialog-body"> <v-card-text class="split-dialog-body">
<p class="split-dialog-desc"> <p class="split-dialog-desc">
Use USDC to get one share of Yes and one share of No for this market. 1 USDC 1 complete Use USDC to get one share of {{ yesLabel }} and one share of {{ noLabel }} for this
set. market. 1 USDC 1 complete set.
</p> </p>
<div class="split-amount-row"> <div class="split-amount-row">
<label class="split-amount-label">Amount (USDC)</label> <label class="split-amount-label">Amount (USDC)</label>
@ -907,7 +1207,7 @@
/> />
</div> </div>
<p v-if="!props.market?.marketId" class="split-no-market"> <p v-if="!props.market?.marketId" class="split-no-market">
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).
</p> </p>
<p v-if="splitError" class="split-error">{{ splitError }}</p> <p v-if="splitError" class="split-error">{{ splitError }}</p>
</v-card-text> </v-card-text>
@ -981,6 +1281,8 @@ export interface TradeMarketPayload {
title?: string title?: string
/** 与 outcomes/outcomePrices 顺序一致,用于下单 tokenId0=Yes 1=No */ /** 与 outcomes/outcomePrices 顺序一致,用于下单 tokenId0=Yes 1=No */
clobTokenIds?: string[] clobTokenIds?: string[]
/** 选项展示文案,如 ["Yes","No"] 或 ["Up","Down"],用于 Buy Yes/No 按钮文字 */
outcomes?: string[]
} }
const props = withDefaults( const props = withDefaults(
@ -1071,6 +1373,8 @@ defineExpose({ openMergeDialog, openSplitDialog })
const yesPriceCents = computed(() => (props.market ? Math.round(props.market.yesPrice * 100) : 19)) const yesPriceCents = computed(() => (props.market ? Math.round(props.market.yesPrice * 100) : 19))
const noPriceCents = computed(() => (props.market ? Math.round(props.market.noPrice * 100) : 82)) const noPriceCents = computed(() => (props.market ? Math.round(props.market.noPrice * 100) : 82))
const yesLabel = computed(() => props.market?.outcomes?.[0] ?? 'Yes')
const noLabel = computed(() => props.market?.outcomes?.[1] ?? 'No')
function openSheet(option: 'yes' | 'no') { function openSheet(option: 'yes' | 'no') {
handleOptionChange(option) handleOptionChange(option)
@ -1090,7 +1394,12 @@ const expirationOptions = ref(['5m', '15m', '30m', '1h', '2h', '4h', '8h', '12h'
// Market mode state // Market mode state
const isMarketMode = computed(() => limitType.value === 'Market') const isMarketMode = computed(() => limitType.value === 'Market')
const amount = ref(0) // Market mode amount 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 orderLoading = ref(false)
const orderError = ref('') const orderError = ref('')
@ -1120,8 +1429,30 @@ const totalPrice = computed(() => {
return (limitPrice.value * shares.value).toFixed(2) 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) {
const price =
selectedOption.value === 'yes'
? (props.market?.yesPrice ?? 0.5)
: (props.market?.noPrice ?? 0.5)
const sharesFromAmount = price > 0 ? amount.value / price : 0
return sharesFromAmount.toFixed(2)
}
return (shares.value * 1).toFixed(2)
})
const actionButtonText = computed(() => { const actionButtonText = computed(() => {
return `${activeTab.value} ${selectedOption.value.charAt(0).toUpperCase() + selectedOption.value.slice(1)}` const label = selectedOption.value === 'yes' ? yesLabel.value : noLabel.value
const tab = activeTab.value
const tabCapitalized = tab.charAt(0).toUpperCase() + tab.slice(1)
return `${tabCapitalized} ${label}`
}) })
function applyInitialOption(option: 'yes' | 'no') { function applyInitialOption(option: 'yes' | 'no') {
@ -1275,6 +1606,17 @@ const setMaxAmount = () => {
amount.value = balance.value amount.value = balance.value
} }
/** Buy 模式:余额是否足够(>= 所需金额且不为 0。costbalance>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 = () => { const deposit = () => {
console.log('Depositing amount:', amount.value) console.log('Depositing amount:', amount.value)
// API // API
@ -1373,11 +1715,38 @@ async function submitOrder() {
</script> </script>
<style scoped> <style scoped>
/* 扁平化:移除所有阴影 */
.trade-component,
.trade-sheet-paper,
.merge-dialog-card,
.split-dialog-card {
box-shadow: none !important;
}
.trade-component :deep(.v-btn),
.trade-component :deep(.v-btn::before),
.trade-sheet-inner :deep(.v-btn),
.trade-sheet-inner :deep(.v-btn::before),
.merge-dialog-card .v-btn,
.merge-dialog-card .v-btn::before,
.split-dialog-card .v-btn,
.split-dialog-card .v-btn::before {
box-shadow: none !important;
}
.trade-component :deep(.v-field),
.trade-sheet-inner :deep(.v-field),
.merge-dialog-card :deep(.v-field),
.split-dialog-card :deep(.v-field) {
box-shadow: none !important;
}
.trade-component { .trade-component {
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
border: 1px solid #e7e7e7;
} }
.header { .header {
@ -1410,6 +1779,7 @@ async function submitOrder() {
font-size: 14px; font-size: 14px;
text-transform: none; text-transform: none;
color: #666666; color: #666666;
box-shadow: none !important;
} }
.more-item { .more-item {
@ -1431,6 +1801,7 @@ async function submitOrder() {
border-radius: 4px; border-radius: 4px;
text-transform: none; text-transform: none;
font-size: 14px; font-size: 14px;
box-shadow: none !important;
} }
.yes-btn.active { .yes-btn.active {
@ -1445,11 +1816,12 @@ async function submitOrder() {
border-radius: 4px; border-radius: 4px;
text-transform: none; text-transform: none;
font-size: 14px; font-size: 14px;
box-shadow: none !important;
} }
.no-btn.active { .no-btn.active {
background-color: #cc0000; background-color: #f0b8b8;
color: #ffffff; color: #cc0000;
} }
.input-group { .input-group {
@ -1581,6 +1953,7 @@ async function submitOrder() {
text-transform: none; text-transform: none;
font-size: 12px; font-size: 12px;
padding: 4px; padding: 4px;
box-shadow: none !important;
} }
.matching-info { .matching-info {
@ -1591,6 +1964,17 @@ async function submitOrder() {
color: #3d8b40; 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 { .expiration-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -1609,6 +1993,10 @@ async function submitOrder() {
border-radius: 4px; border-radius: 4px;
} }
.expiration-select :deep(.v-field) {
box-shadow: none !important;
}
.total-section { .total-section {
padding: 12px; padding: 12px;
border-top: 1px solid #e0e0e0; border-top: 1px solid #e0e0e0;
@ -1638,11 +2026,12 @@ async function submitOrder() {
width: 100%; width: 100%;
background-color: #0066cc; background-color: #0066cc;
color: #ffffff; color: #ffffff;
border-radius: 0; border-radius: 6px;
padding: 16px; padding: 16px;
font-size: 15px; font-size: 15px;
font-weight: 500; font-weight: 500;
text-transform: none; text-transform: none;
box-shadow: none !important;
} }
/* Market Mode Styles */ /* Market Mode Styles */
@ -1685,17 +2074,32 @@ async function submitOrder() {
text-transform: none; text-transform: none;
font-size: 14px; font-size: 14px;
padding: 8px; padding: 8px;
box-shadow: none !important;
}
.amount-to-win-row {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #eee;
} }
.deposit-btn { .deposit-btn {
width: 100%; width: 100%;
background-color: #0066cc; background-color: #0066cc;
color: #ffffff; color: #ffffff;
border-radius: 0; border-radius: 6px;
padding: 16px; padding: 16px;
font-size: 15px; font-size: 15px;
font-weight: 500; font-weight: 500;
text-transform: none; text-transform: none;
box-shadow: none !important;
}
.deposit-btn--secondary {
margin-top: 8px;
background-color: transparent;
color: #0066cc;
border: 1px solid #0066cc;
} }
/* 移动端底部交易栏(红框样式) */ /* 移动端底部交易栏(红框样式) */
@ -1823,6 +2227,7 @@ async function submitOrder() {
.merge-dialog-card { .merge-dialog-card {
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
box-shadow: none !important;
} }
.merge-dialog-header { .merge-dialog-header {
@ -1925,12 +2330,14 @@ async function submitOrder() {
.merge-submit-btn { .merge-submit-btn {
text-transform: none; text-transform: none;
font-weight: 600; font-weight: 600;
box-shadow: none !important;
} }
/* Split dialog */ /* Split dialog */
.split-dialog-card { .split-dialog-card {
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
box-shadow: none !important;
} }
.split-dialog-header { .split-dialog-header {
display: flex; display: flex;
@ -1993,5 +2400,6 @@ async function submitOrder() {
.split-submit-btn { .split-submit-btn {
text-transform: none; text-transform: none;
font-weight: 600; font-weight: 600;
box-shadow: none !important;
} }
</style> </style>

View File

@ -1,6 +1,6 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { getUsdcBalance, formatUsdcBalance } from '@/api/user' import { getUsdcBalance, formatUsdcBalance, getUserInfo } from '@/api/user'
export interface UserInfo { export interface UserInfo {
/** 用户 IDAPI 可能返回 id 或 ID */ /** 用户 IDAPI 可能返回 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 { return {
token, token,
user, user,
@ -99,5 +123,6 @@ export const useUserStore = defineStore('user', () => {
logout, logout,
getAuthHeaders, getAuthHeaders,
fetchUsdcBalance, fetchUsdcBalance,
fetchUserInfo,
} }
}) })

View File

@ -86,7 +86,7 @@
rounded="sm" rounded="sm"
@click="openTrade(market, index, 'yes')" @click="openTrade(market, index, 'yes')"
> >
Yes {{ yesPrice(market) }} {{ yesLabel(market) }} {{ yesPrice(market) }}
</v-btn> </v-btn>
<v-btn <v-btn
class="buy-no-btn" class="buy-no-btn"
@ -95,7 +95,7 @@
rounded="sm" rounded="sm"
@click="openTrade(market, index, 'no')" @click="openTrade(market, index, 'no')"
> >
No {{ noPrice(market) }} {{ noLabel(market) }} {{ noPrice(market) }}
</v-btn> </v-btn>
</div> </div>
</div> </div>
@ -125,7 +125,7 @@
rounded="sm" rounded="sm"
@click="openSheetWithOption('yes')" @click="openSheetWithOption('yes')"
> >
Yes {{ barMarket ? yesPrice(barMarket) : '0¢' }} {{ barMarket ? yesLabel(barMarket) : 'Yes' }} {{ barMarket ? yesPrice(barMarket) : '0¢' }}
</v-btn> </v-btn>
<v-btn <v-btn
class="mobile-bar-btn mobile-bar-no" class="mobile-bar-btn mobile-bar-no"
@ -133,7 +133,7 @@
rounded="sm" rounded="sm"
@click="openSheetWithOption('no')" @click="openSheetWithOption('no')"
> >
No {{ barMarket ? noPrice(barMarket) : '0¢' }} {{ barMarket ? noLabel(barMarket) : 'No' }} {{ barMarket ? noPrice(barMarket) : '0¢' }}
</v-btn> </v-btn>
<v-menu <v-menu
v-model="mobileMenuOpen" v-model="mobileMenuOpen"
@ -239,6 +239,7 @@ const tradeMarketPayload = computed(() => {
noPrice, noPrice,
title: m.question, title: m.question,
clobTokenIds: m.clobTokenIds, clobTokenIds: m.clobTokenIds,
outcomes: m.outcomes,
} }
}) })
@ -566,6 +567,14 @@ function marketChance(market: PmEventMarketItem): number {
return Math.min(100, Math.max(0, Math.round(yesPrice * 100))) return Math.min(100, Math.max(0, Math.round(yesPrice * 100)))
} }
function yesLabel(market: PmEventMarketItem): string {
return market?.outcomes?.[0] ?? 'Yes'
}
function noLabel(market: PmEventMarketItem): string {
return market?.outcomes?.[1] ?? 'No'
}
function yesPrice(market: PmEventMarketItem): string { function yesPrice(market: PmEventMarketItem): string {
const raw = market?.outcomePrices?.[0] const raw = market?.outcomePrices?.[0]
if (raw == null) return '0¢' if (raw == null) return '0¢'
@ -734,13 +743,14 @@ watch(
} }
} }
/* 分时图卡片 */ /* 分时图卡片(扁平化) */
.chart-card.polymarket-chart { .chart-card.polymarket-chart {
margin-bottom: 24px; margin-bottom: 24px;
padding: 20px 24px 16px; padding: 20px 24px 16px;
background-color: #ffffff; background-color: #ffffff;
border: 1px solid #e7e7e7; border: 1px solid #e7e7e7;
border-radius: 12px; border-radius: 12px;
box-shadow: none;
} }
.chart-header { .chart-header {
@ -884,6 +894,7 @@ watch(
border: 1px solid #e7e7e7; border: 1px solid #e7e7e7;
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
box-shadow: none;
} }
.markets-list { .markets-list {

View File

@ -506,12 +506,14 @@ const tradeDialogMarket = ref<{
title: string title: string
marketId?: string marketId?: string
clobTokenIds?: string[] clobTokenIds?: string[]
yesLabel?: string
noLabel?: string
} | null>(null) } | null>(null)
const scrollRef = ref<HTMLElement | null>(null) const scrollRef = ref<HTMLElement | null>(null)
function onCardOpenTrade( function onCardOpenTrade(
side: 'yes' | 'no', side: 'yes' | 'no',
market?: { id: string; title: string; marketId?: string }, market?: { id: string; title: string; marketId?: string; yesLabel?: string; noLabel?: string },
) { ) {
tradeDialogSide.value = side tradeDialogSide.value = side
tradeDialogMarket.value = market ?? null tradeDialogMarket.value = market ?? null
@ -526,7 +528,11 @@ const homeTradeMarketPayload = computed(() => {
const chance = 50 const chance = 50
const yesPrice = Math.min(1, Math.max(0, chance / 100)) const yesPrice = Math.min(1, Math.max(0, chance / 100))
const noPrice = 1 - yesPrice 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<HTMLElement | null>(null) const sentinelRef = ref<HTMLElement | null>(null)

View File

@ -177,13 +177,13 @@ const connectWithWallet = async () => {
walletAddress, walletAddress,
}, },
) )
console.log('Login API response:', loginData) //
console.log('[walletLogin] 登录响应:', JSON.stringify(loginData, null, 2))
if (loginData.code === 0 && loginData.data) { if (loginData.code === 0 && loginData.data) {
userStore.setUser({ const { token, user } = loginData.data
token: loginData.data.token, console.log('[walletLogin] 存入 store 的 user:', JSON.stringify(user, null, 2))
user: loginData.data.user, userStore.setUser({ token, user })
})
userStore.fetchUsdcBalance() userStore.fetchUsdcBalance()
} }

View File

@ -130,7 +130,7 @@
rounded="sm" rounded="sm"
@click="openSheetWithOption('yes')" @click="openSheetWithOption('yes')"
> >
Yes {{ yesPriceCents }}¢ {{ yesLabel }} {{ yesPriceCents }}¢
</v-btn> </v-btn>
<v-btn <v-btn
class="mobile-bar-btn mobile-bar-no" class="mobile-bar-btn mobile-bar-no"
@ -138,7 +138,7 @@
rounded="sm" rounded="sm"
@click="openSheetWithOption('no')" @click="openSheetWithOption('no')"
> >
No {{ noPriceCents }}¢ {{ noLabel }} {{ noPriceCents }}¢
</v-btn> </v-btn>
<v-menu <v-menu
v-model="mobileMenuOpen" v-model="mobileMenuOpen"
@ -332,6 +332,7 @@ const tradeMarketPayload = computed(() => {
noPrice, noPrice,
title: m.question, title: m.question,
clobTokenIds: m.clobTokenIds, clobTokenIds: m.clobTokenIds,
outcomes: m.outcomes,
} }
} }
const qId = route.query.marketId const qId = route.query.marketId
@ -372,6 +373,8 @@ const yesPriceCents = computed(() =>
const noPriceCents = computed(() => const noPriceCents = computed(() =>
tradeMarketPayload.value ? Math.round(tradeMarketPayload.value.noPrice * 100) : 0 tradeMarketPayload.value ? Math.round(tradeMarketPayload.value.noPrice * 100) : 0
) )
const yesLabel = computed(() => currentMarket.value?.outcomes?.[0] ?? 'Yes')
const noLabel = computed(() => currentMarket.value?.outcomes?.[1] ?? 'No')
function openSheetWithOption(side: 'yes' | 'no') { function openSheetWithOption(side: 'yes' | 'no') {
tradeInitialOptionFromBar.value = side tradeInitialOptionFromBar.value = side
@ -921,13 +924,14 @@ onUnmounted(() => {
font-weight: 600; font-weight: 600;
} }
/* Polymarket 样式分时图卡片 */ /* Polymarket 样式分时图卡片(扁平化) */
.chart-card.polymarket-chart { .chart-card.polymarket-chart {
margin-top: 32px; margin-top: 32px;
padding: 20px 24px 16px; padding: 20px 24px 16px;
background-color: #ffffff; background-color: #ffffff;
border: 1px solid #e7e7e7; border: 1px solid #e7e7e7;
border-radius: 12px; border-radius: 12px;
box-shadow: none;
} }
.chart-header { .chart-header {
@ -1027,11 +1031,12 @@ onUnmounted(() => {
color: #111827; color: #111827;
} }
/* Order Book Card Styles */ /* Order Book Card Styles(扁平化) */
.order-book-card { .order-book-card {
padding: 0; padding: 0;
background-color: #ffffff; background-color: #ffffff;
border: 1px solid #e7e7e7; border: 1px solid #e7e7e7;
box-shadow: none;
} }
/* 左右布局:左侧弹性,右侧固定 */ /* 左右布局:左侧弹性,右侧固定 */
@ -1141,11 +1146,12 @@ onUnmounted(() => {
} }
} }
/* Comments / Top Holders / Activity */ /* Comments / Top Holders / Activity(扁平化) */
.activity-card { .activity-card {
margin-top: 32px; margin-top: 32px;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
overflow: hidden; overflow: hidden;
box-shadow: none;
} }
.detail-tabs { .detail-tabs {