优化:文档更新

This commit is contained in:
ivan 2026-03-15 21:05:56 +08:00
parent a29dac0146
commit fa5e0dccfe
13 changed files with 346 additions and 86 deletions

View File

@ -8,7 +8,7 @@ Event预测市场事件相关接口与类型定义对接 XTrader API
## 核心能力 ## 核心能力
- `getPmEventPublic`:分页获取公开事件列表(无需鉴权) - `getPmEventPublic`:分页获取公开事件列表(无需鉴权);请求时固定传入 **startDateMax**、**endDateMin** 为当前时间戳Unix 秒),**startDateMin**、**endDateMax** 不传
- `findPmEvent`:按 id/slug 查询事件详情(需鉴权) - `findPmEvent`:按 id/slug 查询事件详情(需鉴权)
- `mapEventItemToCard`:将 `PmEventListItem` 转为 `EventCardItem`(供 MarketCard 使用) - `mapEventItemToCard`:将 `PmEventListItem` 转为 `EventCardItem`(供 MarketCard 使用)
- 内存缓存:`getEventListCache``setEventListCache``clearEventListCache`,用于列表切换页面时复用 - 内存缓存:`getEventListCache``setEventListCache``clearEventListCache`,用于列表切换页面时复用

87
docs/api/historyRecord.md Normal file
View File

@ -0,0 +1,87 @@
# historyRecord.ts
**路径**`src/api/historyRecord.ts`
## 功能用途
历史记录接口,用于 Wallet 页「History」Tab。**Wallet 使用 GET /hr/getHistoryRecordListClient**(需鉴权、按当前用户分页);另提供 GET /hr/getHistoryRecordPublic不需要鉴权备用。
## 核心能力
- `getHistoryRecordListClient`:客户端分页获取历史记录(需鉴权,传 userId、page、pageSize
- `getHistoryRecordPublic`:分页获取历史记录(无需鉴权)
- `getHistoryRecordList`:从响应 data 中解析 list 并映射为 `HistoryDisplayItem[]`,同时返回 total
- `mapHistoryRecordToDisplayItem`:单条 `HistoryRecordItem` → 钱包展示项(与 Wallet.vue HistoryItem 一致)
## GET /hr/getHistoryRecordListClientWallet 使用)
需鉴权x-token、x-user-id。
### 请求参数Query
| 参数 | 类型 | 说明 |
|------|------|------|
| page | number | 页码 |
| pageSize | number | 每页条数 |
| userId | number | 用户 ID |
| keyword | string | 关键字 |
| slug | string | 标识 |
| title | string | 标题 |
| name | string | 名称 |
| bio | string | 简介 |
| createdAtRange | string[] | 创建时间范围 |
### 响应
`{ code, data, msg }``data``PageResult<HistoryRecordItem>`list、page、pageSize、total
## GET /hr/getHistoryRecordPublic
### 请求参数Query可选
| 参数 | 类型 | 说明 |
|------|------|------|
| page | number | 页码 |
| pageSize | number | 每页条数 |
| keyword | string | 关键字 |
| slug | string | 标识 |
| title | string | 标题 |
| name | string | 名称 |
| bio | string | 简介 |
| createdAtRange | string[] | 创建时间范围 |
### 响应
`{ code, data, msg }``data` 可能为 `PageResult<HistoryRecordItem>`list、page、pageSize、total`HistoryRecordItem[]`
### HistoryRecordItem与 doc.json polymarket.HistoryRecord 对齐)
| 字段 | 类型 | 说明 |
|------|------|------|
| ID | number | 主键 |
| title | string | 标题 |
| name | string | 名称 |
| eventSlug | string | 事件标识 |
| outcome | string | 结果Yes/No 等) |
| side | string | 方向 |
| type | string | 类型 |
| price | number | 价格 |
| size | number | 大小 |
| createdAt | string | 创建时间 |
| timestamp | number | 时间戳(秒) |
| 其他 | - | asset, bio, conditionId, icon, slug, transactionHash 等 |
## 使用方式
```typescript
import { getHistoryRecordPublic, getHistoryRecordList } from '@/api/historyRecord'
const res = await getHistoryRecordPublic({ page: 1, pageSize: 10 })
const { list, total } = getHistoryRecordList(res.data)
// list 为 HistoryDisplayItem[],可直接用于 Wallet 历史列表
```
## 扩展方式
- 若后端固定返回分页结构,可收紧 `HistoryRecordPublicResponse['data']` 类型为 `PageResult<HistoryRecordItem>`
- 展示字段market、activity、value、timeAgo 等)可在 `mapHistoryRecordToDisplayItem` 中按实际接口字段再调整

27
docs/api/jwt.md Normal file
View File

@ -0,0 +1,27 @@
# jwt.ts
**路径**`src/api/jwt.ts`
## 功能用途
JWT 相关接口,用于退出登录时将当前 token 加入黑名单,使 token 失效。
## 核心能力
- `jsonInBlacklist`POST /jwt/jsonInBlacklist需鉴权x-token、x-user-id
## POST /jwt/jsonInBlacklist
### 请求
- 方法POST
- 鉴权:需在 headers 中传 x-token、x-user-id
- Body
### 响应
`{ code, msg }`,标准 ApiResponse。
### 使用场景
退出登录时由 `useUserStore().logout()` 调用,在清空本地 token 前先请求该接口,使服务端将当前 JWT 加入黑名单。若请求失败如网络错误、401仍会执行本地登出。

View File

@ -8,8 +8,8 @@
## 核心能力 ## 核心能力
- `getPositionList`:分页获取持仓列表(需鉴权 x-token、x-user-id返回项含 `needClaim`、`market`(内嵌市场 question、outcomes、outcomePrices - `getPositionList`:分页获取持仓列表(需鉴权 x-token、x-user-id返回项含 `market`(内嵌市场 question、outcomes、outcomePrices、**closed**
- `mapPositionToDisplayItem`:将接口项转为展示结构;`market` 优先用 `market.question`,否则用 marketID`avgNow``market.outcomePrices` 时展示「AVG → NOW」格式`iconChar`/`iconClass`/`imageUrl` 用于展示图标market.image 优先) - `mapPositionToDisplayItem`:将接口项转为展示结构;`market` 优先用 `market.question`,否则用 marketID`avgNow``market.outcomePrices` 时展示「AVG → NOW」格式`iconChar`/`iconClass`/`imageUrl` 用于展示图标market.image 优先)**marketClosed** 取自 `market.closed`,用于钱包侧判断可结算/可领取
## GET /clob/position/getPositionList ## GET /clob/position/getPositionList
@ -43,8 +43,7 @@
| cost | string | 成本 | | cost | string | 成本 |
| outcome | string | 方向 | | outcome | string | 方向 |
| version | number | 版本号 | | version | number | 版本号 |
| needClaim | boolean | 是否待领取结算 | | market | ClobPositionMarket | 内嵌市场详情question、outcomes、outcomePrices、closed 等) |
| market | ClobPositionMarket | 内嵌市场详情question、outcomes、outcomePrices 等) |
| createdAt | string | 创建时间 | | createdAt | string | 创建时间 |
| updatedAt | string | 更新时间 | | updatedAt | string | 更新时间 |
@ -60,6 +59,7 @@
| outcomes | string[] | 选项(如 ["Up", "Down"] | | outcomes | string[] | 选项(如 ["Up", "Down"] |
| outcomePrices | string[] \| number[] | 各选项当前价格 | | outcomePrices | string[] \| number[] | 各选项当前价格 |
| clobTokenIds | string[] | CLOB Token ID 列表 | | clobTokenIds | string[] | CLOB Token ID 列表 |
| closed | boolean | 市场是否已关闭true 表示可结算/可领取 |
## 使用方式 ## 使用方式

69
docs/api/priceHistory.md Normal file
View File

@ -0,0 +1,69 @@
# priceHistory.ts
**路径**`src/api/priceHistory.ts`
## 功能用途
价格历史公开接口,用于 TradeDetail 页面的 Yes/No 折线图。对接 **GET /pmPriceHistory/getPmPriceHistoryPublic**(无需鉴权)。
## 核心能力
- `getPmPriceHistoryPublic`:按市场 ID 分页获取价格历史
- `priceHistoryToChartData`:将接口返回的 `list` 转为 ECharts 使用的 `[timestamp_ms, value_0_100][]`
## GET /pmPriceHistory/getPmPriceHistoryPublic
### 请求参数Query
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| market | string | 是 | 传 YES 对应的 clobTokenId即当前市场 clobTokenIds[0] |
| page | number | 否 | 页码,默认 1 |
| pageSize | number | 否 | 每页条数,默认 500 |
| interval | string | 否 | 数据间隔 |
| time | number | 否 | 时间筛选 |
| createdAtRange | string[] | 否 | 创建时间范围 |
| fidelity | number | 否 | 数据精度 |
| keyword | string | 否 | 关键字 |
| order | string | 否 | 排序 |
| sort | string | 否 | 排序字段 |
| price | number | 否 | 价格筛选 |
### 响应
`{ code, data, msg }``data``PageResult<PmPriceHistoryItem>``list``page``pageSize``total`)。
### PmPriceHistoryItemlist 单项)
| 字段 | 类型 | 说明 |
|------|------|------|
| ID | number | 主键 |
| market | string | 市场标识 |
| price | number | 价格01 小数或 0100由 priceHistoryToChartData 统一为 0100 |
| time | number | Unix 秒时间戳 |
| interval | string | 数据间隔 |
| fidelity | number | 数据精度 |
| createdAt | string | 创建时间 |
| updatedAt | string | 更新时间 |
## 使用方式
```typescript
import {
getPmPriceHistoryPublic,
priceHistoryToChartData,
type PmPriceHistoryItem,
} from '@/api/priceHistory'
const res = await getPmPriceHistoryPublic({
market: marketId,
page: 1,
pageSize: 500,
})
const chartData = priceHistoryToChartData(res.data?.list ?? [])
```
## 扩展方式
- 按时间范围1H/6H/1D 等)传 `interval``createdAtRange` 需与后端约定取值
- 若后端返回的 `price` 固定为 0100`priceHistoryToChartData` 已兼容≤1 时乘 100

View File

@ -9,7 +9,7 @@
## 核心能力 ## 核心能力
- Trade Up / Trade Down Tab - Trade Up / Trade Down Tab
- Asks、Bids 列表,带 `HorizontalProgressBar` 深度条 - Asks、Bids 列表,带 `HorizontalProgressBar` 深度条(买卖两边共用同一最大值 `maxOrderBookTotal`,取两边累计总量中的最大值,便于对比深度)
- Last price、Spread 展示 - Last price、Spread 展示
- Live / 连接中 状态展示(均通过 i18n 国际化) - Live / 连接中 状态展示(均通过 i18n 国际化)

View File

@ -12,7 +12,7 @@
- `isLoggedIn``avatarUrl`:派生状态 - `isLoggedIn``avatarUrl`:派生状态
- `balance`USDC 余额显示(如 "0.00"),支持 **UserSocket** 实时推送更新 - `balance`USDC 余额显示(如 "0.00"),支持 **UserSocket** 实时推送更新
- `setUser`:设置登录数据并持久化,登录成功后自动连接 UserSocket - `setUser`:设置登录数据并持久化,登录成功后自动连接 UserSocket
- `logout`:清空并断开 UserSocket - `logout`退出登录;先调用 **POST /jwt/jsonInBlacklist** 将当前 JWT 加入黑名单,再清空本地 token/user 并断开 UserSocket;接口失败时仍执行本地登出
- `getAuthHeaders`:返回 `{ 'x-token', 'x-user-id' }`,未登录时返回 `undefined` - `getAuthHeaders`:返回 `{ 'x-token', 'x-user-id' }`,未登录时返回 `undefined`
- `fetchUserInfo``fetchUsdcBalance`:拉取并更新用户信息与余额;`fetchUserInfo` 兼容多种 API 字段名id/ID、userName/username 等) - `fetchUserInfo``fetchUsdcBalance`:拉取并更新用户信息与余额;`fetchUserInfo` 兼容多种 API 字段名id/ID、userName/username 等)
- 内部 `parseUserId`:从 API 返回的 user 对象解析 id/ID兼容 number 与 string`setUser``fetchUserInfo` 复用 - 内部 `parseUserId`:从 API 返回的 user 对象解析 id/ID兼容 number 与 string`setUser``fetchUserInfo` 复用

View File

@ -9,7 +9,7 @@
## 核心能力 ## 核心能力
- 分时图ECharts 渲染,支持 Past、时间粒度切换**加密货币事件**可切换 YES/NO 分时图与加密货币价格走势图CoinGecko 实时数据) - 分时图ECharts 渲染,支持 Past、时间粒度切换1H/6H/1D/1W/1M/ALL**Yes/No 模式**数据来自 **GET /pmPriceHistory/getPmPriceHistoryPublic**market 传 clobTokenIds[0]),接口返回 `time`Unix 秒)、`price`01转成 `[timestamp_ms, value_0_100][]` 后缓存在 `rawChartData`**分时**为前端按当前选中范围过滤1H=最近 1 小时、6H=6 小时、1D=1 天、1W=7 天、1M=30 天、ALL=全部,切换时间范围不重复请求**加密货币事件**可切换 YES/NO 分时图与加密货币价格走势图CoinGecko 实时数据)
- 订单簿:`OrderBook` 组件,通过 **ClobSdk** 对接 CLOB WebSocket 实时数据(全量快照、增量更新、成交推送);份额接口按 6 位小数传1_000_000 = 1 share`priceSizeToRows``mergeDelta` 会将 raw 值除以 `ORDER_BOOK_SIZE_SCALE` 转为展示值 - 订单簿:`OrderBook` 组件,通过 **ClobSdk** 对接 CLOB WebSocket 实时数据(全量快照、增量更新、成交推送);份额接口按 6 位小数传1_000_000 = 1 share`priceSizeToRows``mergeDelta` 会将 raw 值除以 `ORDER_BOOK_SIZE_SCALE` 转为展示值
- 交易:`TradeComponent`,传入 `market``initialOption``positions`(持仓数据) - 交易:`TradeComponent`,传入 `market``initialOption``positions`(持仓数据)
- 持仓列表:通过 `getPositionList` 获取当前市场持仓,传递给 `TradeComponent` 用于计算可合并份额 - 持仓列表:通过 `getPositionList` 获取当前市场持仓,传递给 `TradeComponent` 用于计算可合并份额
@ -36,7 +36,7 @@
## 扩展方式 ## 扩展方式
1. **订单簿**:已通过 `sdk/clobSocket.ts` 的 ClobSdk 对接 CLOB WebSocket使用 **Yes/No token ID** 订阅 `price_size_all``price_size_delta``trade` 消息 1. **订单簿**:已通过 `sdk/clobSocket.ts` 的 ClobSdk 对接 CLOB WebSocket使用 **Yes/No token ID** 订阅 `price_size_all``price_size_delta``trade` 消息
2. **分时图**可接入 WebSocket 推送的图表数据;加密货币事件已支持 YES/NO 分时与加密货币价格图切换(`src/api/cryptoChart.ts` 2. **分时图**Yes/NO 折线图仅使用真实接口 `getPmPriceHistoryPublic`,无模拟数据与定时器;事件详情加载完成后自动请求并展示,无 marketId 或接口无数据时展示空图;加密货币事件已支持 YES/NO 分时与加密货币价格图切换(`src/api/cryptoChart.ts`
3. **Comments**:对接评论接口,替换 placeholder 3. **Comments**:对接评论接口,替换 placeholder
4. **Top Holders**:对接持仓接口 4. **Top Holders**:对接持仓接口
5. **Activity**:已对接 CLOB `trade` 消息,实时追加成交记录 5. **Activity**:已对接 CLOB `trade` 消息,实时追加成交记录

View File

@ -10,8 +10,9 @@
## 核心能力 ## 核心能力
- Portfolio 卡片余额、Deposit/Withdraw 按钮 - Portfolio 卡片余额、Deposit/Withdraw 按钮
- Profit/Loss 卡片时间范围切换、ECharts 图表 - Profit/Loss 卡片时间范围切换1D/1W/1M/ALL、ECharts 资产变化折线图;数据格式为 `[timestamp_ms, pnl][]`**暂无接口时全部显示为 0**(真实时间轴 + 数值 0有接口后在此处对接
- TabPositions、Open orders、History、Withdrawals提现记录 - TabPositions、Open orders、**History**(历史记录来自 **GET /hr/getHistoryRecordListClient**`src/api/historyRecord.ts`需鉴权、按当前用户分页、Withdrawals提现记录
- **可结算/领取**未结算项unsettledItems由持仓中有 `marketID``tokenID`**所属 market.closed=true** 的项组成,用于「领取结算」按钮;不再使用 needClaim 判断
- Withdrawals分页列表状态筛选全部/审核中/提现成功/审核不通过/提现失败),对接 GET /pmset/getPmSettlementRequestsListClient - Withdrawals分页列表状态筛选全部/审核中/提现成功/审核不通过/提现失败),对接 GET /pmset/getPmSettlementRequestsListClient
- DepositDialog、WithdrawDialog 组件 - DepositDialog、WithdrawDialog 组件
- **401 权限错误**:取消订单等接口失败时,通过 `useAuthError().formatAuthError` 统一提示「请先登录」或「权限不足」 - **401 权限错误**:取消订单等接口失败时,通过 `useAuthError().formatAuthError` 统一提示「请先登录」或「权限不足」

View File

@ -110,6 +110,7 @@ export interface PmEventListResponse {
/** /**
* GET /PmEvent/getPmEventPublic doc.json * GET /PmEvent/getPmEventPublic doc.json
* startDateMinendDateMax startDateMaxendDateMin
*/ */
export interface GetPmEventListParams { export interface GetPmEventListParams {
page?: number page?: number
@ -147,6 +148,7 @@ export async function getPmEventPublic(
archived = false, archived = false,
closed = false, closed = false,
} = params } = params
const nowTs = Math.floor(Date.now() / 1000)
const query = buildQuery({ const query = buildQuery({
page, page,
pageSize, pageSize,
@ -156,6 +158,8 @@ export async function getPmEventPublic(
active: active ? 'true' : 'false', active: active ? 'true' : 'false',
archived: archived ? 'true' : 'false', archived: archived ? 'true' : 'false',
closed: closed ? 'true' : 'false', closed: closed ? 'true' : 'false',
startDateMax: nowTs,
endDateMin: nowTs,
}) })
return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query) return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query)
} }

18
src/api/jwt.ts Normal file
View File

@ -0,0 +1,18 @@
/**
* JWT
* POST /jwt/jsonInBlacklist JWT 退
*/
import { post } from './request'
import type { ApiResponse } from './types'
/**
* JWT x-token
* POST /jwt/jsonInBlacklist
* 退使 token
*/
export async function jsonInBlacklist(
config?: { headers?: Record<string, string> },
): Promise<ApiResponse> {
return post<ApiResponse>('/jwt/jsonInBlacklist', undefined, config)
}

View File

@ -63,7 +63,9 @@
<div class="input-group shares-group"> <div class="input-group shares-group">
<div class="shares-header sell-shares-header"> <div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.amount') }}</span> <span class="label">{{ t('trade.amount') }}</span>
<span class="max-shares-inline">{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span> <span class="max-shares-inline"
>{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span
>
</div> </div>
<div class="shares-input-wrapper"> <div class="shares-input-wrapper">
<v-text-field <v-text-field
@ -94,7 +96,9 @@
<div class="input-group shares-group"> <div class="input-group shares-group">
<div class="shares-header sell-shares-header"> <div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span> <span class="label">{{ t('trade.shares') }}</span>
<span class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span> <span class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>
</div> </div>
<div class="shares-input-wrapper"> <div class="shares-input-wrapper">
<v-text-field <v-text-field
@ -154,11 +158,7 @@
<p v-if="orderError" class="order-error">{{ orderError }}</p> <p v-if="orderError" class="order-error">{{ orderError }}</p>
<!-- Action Button: Buy 余额足够显示 Buy Yes/No不足显示 {{ t('trade.deposit') }}Sell 只显示 Sell Yes/No --> <!-- Action Button: Buy 余额足够显示 Buy Yes/No不足显示 {{ t('trade.deposit') }}Sell 只显示 Sell Yes/No -->
<v-btn <v-btn v-if="activeTab === 'buy' && showDepositForBuy" class="deposit-btn" @click="deposit">
v-if="activeTab === 'buy' && showDepositForBuy"
class="deposit-btn"
@click="deposit"
>
{{ t('trade.deposit') }} {{ t('trade.deposit') }}
</v-btn> </v-btn>
<v-btn <v-btn
@ -201,7 +201,9 @@
<div class="input-group shares-group"> <div class="input-group shares-group">
<div class="shares-header sell-shares-header"> <div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.amount') }}</span> <span class="label">{{ t('trade.amount') }}</span>
<span class="max-shares-inline">{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span> <span class="max-shares-inline"
>{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span
>
</div> </div>
<div class="shares-input-wrapper"> <div class="shares-input-wrapper">
<v-text-field <v-text-field
@ -243,7 +245,9 @@
<div class="input-group shares-group"> <div class="input-group shares-group">
<div class="shares-header sell-shares-header"> <div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span> <span class="label">{{ t('trade.shares') }}</span>
<span class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span> <span class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>
</div> </div>
<div class="shares-input-wrapper"> <div class="shares-input-wrapper">
<v-text-field <v-text-field
@ -352,7 +356,9 @@
<div class="input-group shares-group"> <div class="input-group shares-group">
<div class="shares-header sell-shares-header"> <div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span> <span class="label">{{ t('trade.shares') }}</span>
<span v-if="activeTab === 'sell'" class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span> <span v-if="activeTab === 'sell'" class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>
</div> </div>
<div class="shares-input-wrapper"> <div class="shares-input-wrapper">
<v-text-field <v-text-field
@ -382,7 +388,9 @@
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn> <v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setMaxShares">{{ t('trade.max') }}</v-btn> <v-btn class="share-btn" @click="setMaxShares">{{ t('trade.max') }}</v-btn>
</div> </div>
<p v-if="sellSharesExceedsMax" class="shares-exceeds-max-hint">{{ t('trade.sharesExceedsMax', { max: maxAvailableShares }) }}</p> <p v-if="sellSharesExceedsMax" class="shares-exceeds-max-hint">
{{ t('trade.sharesExceedsMax', { max: maxAvailableShares }) }}
</p>
<div v-if="activeTab === 'buy'" class="matching-info"> <div v-if="activeTab === 'buy'" class="matching-info">
<v-icon size="14">mdi-information</v-icon> <v-icon size="14">mdi-information</v-icon>
<span>20.00 matching</span> <span>20.00 matching</span>
@ -506,7 +514,9 @@
<div class="input-group shares-group"> <div class="input-group shares-group">
<div class="shares-header sell-shares-header"> <div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.amount') }}</span> <span class="label">{{ t('trade.amount') }}</span>
<span class="max-shares-inline">{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span> <span class="max-shares-inline"
>{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span
>
</div> </div>
<div class="shares-input-wrapper"> <div class="shares-input-wrapper">
<v-text-field <v-text-field
@ -537,7 +547,9 @@
<div class="input-group shares-group"> <div class="input-group shares-group">
<div class="shares-header sell-shares-header"> <div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span> <span class="label">{{ t('trade.shares') }}</span>
<span class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span> <span class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>
</div> </div>
<div class="shares-input-wrapper"> <div class="shares-input-wrapper">
<v-text-field <v-text-field
@ -563,12 +575,14 @@
<div class="total-section"> <div class="total-section">
<template v-if="activeTab === 'buy'"> <template v-if="activeTab === 'buy'">
<div v-if="!isMarketMode" class="total-row"> <div v-if="!isMarketMode" class="total-row">
<span class="label">{{ t('trade.total') }}</span><span class="total-value">${{ totalPrice }}</span> <span class="label">{{ t('trade.total') }}</span
><span class="total-value">${{ totalPrice }}</span>
</div> </div>
<div class="total-row"> <div class="total-row">
<span class="label">{{ t('trade.toWin') }}</span <span class="label">{{ t('trade.toWin') }}</span
><span class="to-win-value" ><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span ><v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ toWinValue }}</span
> >
</div> </div>
</template> </template>
@ -576,9 +590,8 @@
<div class="total-row"> <div class="total-row">
<span class="label">{{ t('trade.youllReceive') }}</span <span class="label">{{ t('trade.youllReceive') }}</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"> <div class="total-row avg-price-row">
@ -630,7 +643,9 @@
<div class="input-group shares-group"> <div class="input-group shares-group">
<div class="shares-header sell-shares-header"> <div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.amount') }}</span> <span class="label">{{ t('trade.amount') }}</span>
<span class="max-shares-inline">{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span> <span class="max-shares-inline"
>{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span
>
</div> </div>
<div class="shares-input-wrapper"> <div class="shares-input-wrapper">
<v-text-field <v-text-field
@ -669,7 +684,9 @@
<div class="input-group shares-group"> <div class="input-group shares-group">
<div class="shares-header sell-shares-header"> <div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span> <span class="label">{{ t('trade.shares') }}</span>
<span class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span> <span class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>
</div> </div>
<div class="shares-input-wrapper"> <div class="shares-input-wrapper">
<v-text-field <v-text-field
@ -768,7 +785,9 @@
<div class="input-group shares-group"> <div class="input-group shares-group">
<div class="shares-header sell-shares-header"> <div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span> <span class="label">{{ t('trade.shares') }}</span>
<span v-if="activeTab === 'sell'" class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span> <span v-if="activeTab === 'sell'" class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>
</div> </div>
<div class="shares-input-wrapper"> <div class="shares-input-wrapper">
<v-text-field <v-text-field
@ -820,7 +839,8 @@
<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">
<span class="label">{{ t('trade.total') }}</span><span class="total-value">${{ totalPrice }}</span> <span class="label">{{ t('trade.total') }}</span
><span class="total-value">${{ totalPrice }}</span>
</div> </div>
<div class="total-row"> <div class="total-row">
<span class="label">{{ t('trade.toWin') }}</span <span class="label">{{ t('trade.toWin') }}</span
@ -936,7 +956,9 @@
<div class="input-group shares-group"> <div class="input-group shares-group">
<div class="shares-header sell-shares-header"> <div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.amount') }}</span> <span class="label">{{ t('trade.amount') }}</span>
<span class="max-shares-inline">{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span> <span class="max-shares-inline"
>{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span
>
</div> </div>
<div class="shares-input-wrapper"> <div class="shares-input-wrapper">
<v-text-field <v-text-field
@ -967,7 +989,9 @@
<div class="input-group shares-group"> <div class="input-group shares-group">
<div class="shares-header sell-shares-header"> <div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span> <span class="label">{{ t('trade.shares') }}</span>
<span class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span> <span class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>
</div> </div>
<div class="shares-input-wrapper"> <div class="shares-input-wrapper">
<v-text-field <v-text-field
@ -999,7 +1023,8 @@
<div class="total-row"> <div class="total-row">
<span class="label">{{ t('trade.toWin') }}</span <span class="label">{{ t('trade.toWin') }}</span
><span class="to-win-value" ><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span ><v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ toWinValue }}</span
> >
</div> </div>
</template> </template>
@ -1007,9 +1032,8 @@
<div class="total-row"> <div class="total-row">
<span class="label">{{ t('trade.youllReceive') }}</span <span class="label">{{ t('trade.youllReceive') }}</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"> <div class="total-row avg-price-row">
@ -1061,7 +1085,9 @@
<div class="input-group shares-group"> <div class="input-group shares-group">
<div class="shares-header sell-shares-header"> <div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.amount') }}</span> <span class="label">{{ t('trade.amount') }}</span>
<span class="max-shares-inline">{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span> <span class="max-shares-inline"
>{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span
>
</div> </div>
<div class="shares-input-wrapper"> <div class="shares-input-wrapper">
<v-text-field <v-text-field
@ -1100,7 +1126,9 @@
<div class="input-group shares-group"> <div class="input-group shares-group">
<div class="shares-header sell-shares-header"> <div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span> <span class="label">{{ t('trade.shares') }}</span>
<span class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span> <span class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>
</div> </div>
<div class="shares-input-wrapper"> <div class="shares-input-wrapper">
<v-text-field <v-text-field
@ -1199,7 +1227,9 @@
<div class="input-group shares-group"> <div class="input-group shares-group">
<div class="shares-header sell-shares-header"> <div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span> <span class="label">{{ t('trade.shares') }}</span>
<span v-if="activeTab === 'sell'" class="max-shares-inline">{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span> <span v-if="activeTab === 'sell'" class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>
</div> </div>
<div class="shares-input-wrapper"> <div class="shares-input-wrapper">
<v-text-field <v-text-field
@ -1251,12 +1281,14 @@
<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">
<span class="label">{{ t('trade.total') }}</span><span class="total-value">${{ totalPrice }}</span> <span class="label">{{ t('trade.total') }}</span
><span class="total-value">${{ totalPrice }}</span>
</div> </div>
<div class="total-row"> <div class="total-row">
<span class="label">{{ t('trade.toWin') }}</span <span class="label">{{ t('trade.toWin') }}</span
><span class="to-win-value" ><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span ><v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ toWinValue }}</span
> >
</div> </div>
</template> </template>
@ -1264,9 +1296,8 @@
<div class="total-row"> <div class="total-row">
<span class="label">{{ t('trade.youllReceive') }}</span <span class="label">{{ t('trade.youllReceive') }}</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>
@ -1324,7 +1355,9 @@
</div> </div>
<p class="merge-available"> <p class="merge-available">
{{ t('trade.mergeAvailableShares') }} {{ availableMergeShares }} {{ t('trade.mergeAvailableShares') }} {{ availableMergeShares }}
<button type="button" class="merge-max-link" @click="setMergeMax">{{ t('trade.max') }}</button> <button type="button" class="merge-max-link" @click="setMergeMax">
{{ t('trade.max') }}
</button>
</p> </p>
<p v-if="!props.market?.marketId" class="merge-no-market"> <p v-if="!props.market?.marketId" class="merge-no-market">
{{ t('trade.mergeNoMarket', { yesLabel, noLabel }) }} {{ t('trade.mergeNoMarket', { yesLabel, noLabel }) }}
@ -1498,7 +1531,13 @@ const props = withDefaults(
/** 当前市场持仓列表,用于计算可合并份额 */ /** 当前市场持仓列表,用于计算可合并份额 */
positions?: TradePositionItem[] positions?: TradePositionItem[]
}>(), }>(),
{ initialOption: undefined, initialTab: undefined, embeddedInSheet: false, market: undefined, positions: () => [] }, {
initialOption: undefined,
initialTab: undefined,
embeddedInSheet: false,
market: undefined,
positions: () => [],
},
) )
// //
@ -1575,7 +1614,7 @@ async function submitSplit() {
splitError.value = '' splitError.value = ''
try { try {
const res = await pmMarketSplit( const res = await pmMarketSplit(
{ marketID: marketId, usdcAmount: String(splitAmount.value) }, { marketID: marketId, usdcAmount: String(splitAmount.value * 1000000) },
{ headers: userStore.getAuthHeaders() }, { headers: userStore.getAuthHeaders() },
) )
if (res.code === 0 || res.code === 200) { if (res.code === 0 || res.code === 200) {
@ -1639,7 +1678,19 @@ const selectedOption = ref<'yes' | 'no'>(props.initialOption ?? 'no')
const limitPrice = ref(0.82) // 01 const limitPrice = ref(0.82) // 01
const shares = ref(20) // const shares = ref(20) //
const expirationTime = ref('5m') // const expirationTime = ref('5m') //
const EXPIRATION_VALUES = ['5m', '15m', '30m', '1h', '2h', '4h', '8h', '12h', '1d', '2d', '3d'] as const const EXPIRATION_VALUES = [
'5m',
'15m',
'30m',
'1h',
'2h',
'4h',
'8h',
'12h',
'1d',
'2d',
'3d',
] as const
const expirationOptions = computed(() => const expirationOptions = computed(() =>
EXPIRATION_VALUES.map((v) => ({ title: t(`trade.expiration.${v}`), value: v })), EXPIRATION_VALUES.map((v) => ({ title: t(`trade.expiration.${v}`), value: v })),
) )
@ -1944,7 +1995,10 @@ const adjustShares = (amount: number) => {
/** 卖出时输入份额是否超过最大可卖 */ /** 卖出时输入份额是否超过最大可卖 */
const sellSharesExceedsMax = computed( const sellSharesExceedsMax = computed(
() => activeTab.value === 'sell' && maxAvailableShares.value >= 0 && shares.value > maxAvailableShares.value, () =>
activeTab.value === 'sell' &&
maxAvailableShares.value >= 0 &&
shares.value > maxAvailableShares.value,
) )
// Sell使 // Sell使
@ -2058,12 +2112,7 @@ async function submitOrder() {
return return
} }
const uid = userStore?.user?.ID ?? 0 const uid = userStore?.user?.ID ?? 0
const userIdNum = const userIdNum = typeof uid === 'number' ? uid : uid != null ? parseInt(String(uid), 10) : 0
typeof uid === 'number'
? uid
: uid != null
? parseInt(String(uid), 10)
: 0
if (!Number.isFinite(userIdNum) || userIdNum <= 0) { if (!Number.isFinite(userIdNum) || userIdNum <= 0) {
orderError.value = t('trade.userError') orderError.value = t('trade.userError')
isNoAvailableSharesError.value = false isNoAvailableSharesError.value = false
@ -2096,9 +2145,7 @@ async function submitOrder() {
? parseExpirationTimestamp(expirationTime.value) ? parseExpirationTimestamp(expirationTime.value)
: 0 : 0
const rawSize = isMarket && activeTab.value === 'buy' const rawSize = isMarket && activeTab.value === 'buy' ? amount.value : clampShares(shares.value)
? amount.value
: clampShares(shares.value)
const sizeValue = Math.round(rawSize * 1_000_000) const sizeValue = Math.round(rawSize * 1_000_000)
orderLoading.value = true orderLoading.value = true

View File

@ -2,6 +2,7 @@ import { ref, computed } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { getUsdcBalance, formatUsdcBalance, getUserInfo } from '@/api/user' import { getUsdcBalance, formatUsdcBalance, getUserInfo } from '@/api/user'
import { getUserWsUrl, BASE_URL } from '@/api/request' import { getUserWsUrl, BASE_URL } from '@/api/request'
import { jsonInBlacklist } from '@/api/jwt'
import { UserSdk, type BalanceData, type PositionData } from '../../sdk/userSocket' import { UserSdk, type BalanceData, type PositionData } from '../../sdk/userSocket'
export interface UserInfo { export interface UserInfo {
@ -49,11 +50,7 @@ function parseUserId(raw: { id?: number | string; ID?: number } | null | undefin
} { } {
const rawId = raw?.ID ?? raw?.id const rawId = raw?.ID ?? raw?.id
const numId = const numId =
typeof rawId === 'number' typeof rawId === 'number' ? rawId : rawId != null ? parseInt(String(rawId), 10) : undefined
? rawId
: rawId != null
? parseInt(String(rawId), 10)
: undefined
return { id: rawId ?? numId, numId: Number.isFinite(numId) ? numId : undefined } return { id: rawId ?? numId, numId: Number.isFinite(numId) ? numId : undefined }
} }
@ -120,7 +117,9 @@ export const useUserStore = defineStore('user', () => {
} }
/** 订阅 position_update 推送,返回取消订阅函数 */ /** 订阅 position_update 推送,返回取消订阅函数 */
function onPositionUpdate(cb: (data: PositionData & Record<string, unknown>) => void): () => void { function onPositionUpdate(
cb: (data: PositionData & Record<string, unknown>) => void,
): () => void {
positionUpdateCallbacks.push(cb) positionUpdateCallbacks.push(cb)
return () => { return () => {
const i = positionUpdateCallbacks.indexOf(cb) const i = positionUpdateCallbacks.indexOf(cb)
@ -147,7 +146,15 @@ export const useUserStore = defineStore('user', () => {
} }
} }
function logout() { async function logout() {
const headers = getAuthHeaders()
if (headers) {
try {
await jsonInBlacklist({ headers })
} catch {
// 忽略失败,仍执行本地登出
}
}
disconnectUserSocket() disconnectUserSocket()
token.value = '' token.value = ''
user.value = null user.value = null