Compare commits

..

5 Commits

Author SHA1 Message Date
ivan
fa5e0dccfe 优化:文档更新 2026-03-15 21:08:51 +08:00
ivan
a29dac0146 优化:订单薄最大值逻辑更新 2026-03-15 21:06:37 +08:00
ivan
3983105628 新增:折线图数据对接 2026-03-15 21:06:37 +08:00
ivan
9dd61be92f 优化:历史数据接口对接 2026-03-15 21:06:37 +08:00
ivan
d44e7157e8 新增:历史数据接口对接,资产变化 2026-03-15 21:06:37 +08:00
19 changed files with 733 additions and 221 deletions

View File

@ -8,7 +8,7 @@ Event预测市场事件相关接口与类型定义对接 XTrader API
## 核心能力
- `getPmEventPublic`:分页获取公开事件列表(无需鉴权)
- `getPmEventPublic`:分页获取公开事件列表(无需鉴权);请求时固定传入 **startDateMax**、**endDateMin** 为当前时间戳Unix 秒),**startDateMin**、**endDateMax** 不传
- `findPmEvent`:按 id/slug 查询事件详情(需鉴权)
- `mapEventItemToCard`:将 `PmEventListItem` 转为 `EventCardItem`(供 MarketCard 使用)
- 内存缓存:`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
- `mapPositionToDisplayItem`:将接口项转为展示结构;`market` 优先用 `market.question`,否则用 marketID`avgNow``market.outcomePrices` 时展示「AVG → NOW」格式`iconChar`/`iconClass`/`imageUrl` 用于展示图标market.image 优先)
- `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 优先)**marketClosed** 取自 `market.closed`,用于钱包侧判断可结算/可领取
## GET /clob/position/getPositionList
@ -43,8 +43,7 @@
| cost | string | 成本 |
| outcome | string | 方向 |
| version | number | 版本号 |
| needClaim | boolean | 是否待领取结算 |
| market | ClobPositionMarket | 内嵌市场详情question、outcomes、outcomePrices 等) |
| market | ClobPositionMarket | 内嵌市场详情question、outcomes、outcomePrices、closed 等) |
| createdAt | string | 创建时间 |
| updatedAt | string | 更新时间 |
@ -60,6 +59,7 @@
| outcomes | string[] | 选项(如 ["Up", "Down"] |
| outcomePrices | string[] \| number[] | 各选项当前价格 |
| 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
- Asks、Bids 列表,带 `HorizontalProgressBar` 深度条
- Asks、Bids 列表,带 `HorizontalProgressBar` 深度条(买卖两边共用同一最大值 `maxOrderBookTotal`,取两边累计总量中的最大值,便于对比深度)
- Last price、Spread 展示
- Live / 连接中 状态展示(均通过 i18n 国际化)

View File

@ -12,7 +12,7 @@
- `isLoggedIn``avatarUrl`:派生状态
- `balance`USDC 余额显示(如 "0.00"),支持 **UserSocket** 实时推送更新
- `setUser`:设置登录数据并持久化,登录成功后自动连接 UserSocket
- `logout`:清空并断开 UserSocket
- `logout`退出登录;先调用 **POST /jwt/jsonInBlacklist** 将当前 JWT 加入黑名单,再清空本地 token/user 并断开 UserSocket;接口失败时仍执行本地登出
- `getAuthHeaders`:返回 `{ 'x-token', 'x-user-id' }`,未登录时返回 `undefined`
- `fetchUserInfo``fetchUsdcBalance`:拉取并更新用户信息与余额;`fetchUserInfo` 兼容多种 API 字段名id/ID、userName/username 等)
- 内部 `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` 转为展示值
- 交易:`TradeComponent`,传入 `market``initialOption``positions`(持仓数据)
- 持仓列表:通过 `getPositionList` 获取当前市场持仓,传递给 `TradeComponent` 用于计算可合并份额
@ -36,7 +36,7 @@
## 扩展方式
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
4. **Top Holders**:对接持仓接口
5. **Activity**:已对接 CLOB `trade` 消息,实时追加成交记录

View File

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

View File

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

185
src/api/historyRecord.ts Normal file
View File

@ -0,0 +1,185 @@
/**
* getHistoryRecordListClientgetHistoryRecordPublic
* Wallet.vue Tab
*/
import { buildQuery, get } from './request'
import type { PageResult } from './types'
/** 单条历史记录(与 doc.json definitions["polymarket.HistoryRecord"] 对齐) */
export interface HistoryRecordItem {
ID?: number
asset?: string
bio?: string
conditionId?: string
createdAt?: string
eventSlug?: string
icon?: string
name?: string
outcome?: string
outcomeIndex?: number
price?: number
profileImage?: string
profileImageOptimized?: string
proxyWallet?: string
pseudonym?: string
side?: string
size?: number
slug?: string
timestamp?: number
title?: string
transactionHash?: string
type?: string
updatedAt?: string
}
/** GET /hr/getHistoryRecordPublic 请求参数 */
export interface GetHistoryRecordPublicParams {
page?: number
pageSize?: number
keyword?: string
slug?: string
title?: string
name?: string
bio?: string
createdAtRange?: string[]
}
/** GET /hr/getHistoryRecordListClient 请求参数(客户端分页,需鉴权) */
export interface GetHistoryRecordListClientParams {
page?: number
pageSize?: number
userId?: number
keyword?: string
slug?: string
title?: string
name?: string
bio?: string
createdAtRange?: string[]
}
/** 响应 data 可能为分页或数组 */
export interface HistoryRecordPublicResponse {
code: number
data?: PageResult<HistoryRecordItem> | HistoryRecordItem[]
msg: string
}
/** getHistoryRecordListClient 响应 data 为 PageResult */
export interface HistoryRecordListClientResponse {
code: number
data?: PageResult<HistoryRecordItem>
msg: string
}
/**
* x-tokenx-user-id
* GET /hr/getHistoryRecordListClient
*/
export async function getHistoryRecordListClient(
params: GetHistoryRecordListClientParams = {},
config?: { headers?: Record<string, string> },
): Promise<HistoryRecordListClientResponse> {
const query = buildQuery({
page: params.page,
pageSize: params.pageSize,
userId: params.userId,
keyword: params.keyword,
slug: params.slug,
title: params.title,
name: params.name,
bio: params.bio,
createdAtRange: params.createdAtRange,
})
return get<HistoryRecordListClientResponse>('/hr/getHistoryRecordListClient', query, config)
}
/**
*
* GET /hr/getHistoryRecordPublic
*/
export async function getHistoryRecordPublic(
params: GetHistoryRecordPublicParams = {},
): Promise<HistoryRecordPublicResponse> {
const query = buildQuery({
page: params.page,
pageSize: params.pageSize,
keyword: params.keyword,
slug: params.slug,
title: params.title,
name: params.name,
bio: params.bio,
createdAtRange: params.createdAtRange,
})
return get<HistoryRecordPublicResponse>('/hr/getHistoryRecordPublic', query)
}
/** 钱包 History 展示项(与 Wallet.vue HistoryItem / order.HistoryDisplayItem 一致) */
export interface HistoryDisplayItem {
id: string
market: string
side: 'Yes' | 'No'
activity: string
value: string
activityDetail?: string
profitLoss?: string
profitLossNegative?: boolean
timeAgo?: string
avgPrice?: string
shares?: string
iconChar?: string
iconClass?: string
}
function formatTimeAgo(createdAt: string | undefined, timestamp?: number): string {
const ms = createdAt ? new Date(createdAt).getTime() : (timestamp != null ? timestamp * 1000 : 0)
if (!ms) return ''
const diff = Date.now() - ms
if (diff < 60000) return 'Just now'
if (diff < 3600000) return `${Math.floor(diff / 60000)} minutes ago`
if (diff < 86400000) return `${Math.floor(diff / 3600000)} hours ago`
if (diff < 604800000) return `${Math.floor(diff / 86400000)} days ago`
return new Date(ms).toLocaleDateString()
}
/**
* HistoryRecordItem History
*/
export function mapHistoryRecordToDisplayItem(record: HistoryRecordItem): HistoryDisplayItem {
const id = String(record.ID ?? '')
const market = record.title ?? record.name ?? record.eventSlug ?? ''
const outcome = (record.outcome ?? record.side ?? 'Yes').toString()
const side = outcome === 'No' || outcome === 'Down' ? 'No' : 'Yes'
const typeLabel = record.type ?? 'Trade'
const activity = `${typeLabel} ${outcome}`.trim()
const price = record.price ?? 0
const size = record.size ?? 0
const valueUsd = price * size
const value = `$${Math.abs(valueUsd).toFixed(2)}`
const priceCents = Math.round(price * 100)
const activityDetail = size > 0 ? `Sold ${Math.floor(size)} ${outcome} at ${priceCents}¢` : value
const timeAgo = formatTimeAgo(record.createdAt, record.timestamp)
return {
id,
market,
side,
activity,
value,
activityDetail,
profitLoss: value,
profitLossNegative: valueUsd < 0,
timeAgo,
avgPrice: priceCents ? `${priceCents}¢` : undefined,
shares: size > 0 ? String(Math.floor(size)) : undefined,
}
}
/** 从响应中取出 list 并映射为展示项,同时返回 total数组时为 length */
export function getHistoryRecordList(
data: HistoryRecordPublicResponse['data'],
): { list: HistoryDisplayItem[]; total: number } {
if (!data) return { list: [], total: 0 }
const list = Array.isArray(data) ? data : (data as PageResult<HistoryRecordItem>).list ?? []
const total = Array.isArray(data) ? data.length : (data as PageResult<HistoryRecordItem>).total ?? list.length
return { list: list.map(mapHistoryRecordToDisplayItem), total }
}

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

@ -16,6 +16,8 @@ export interface ClobPositionMarket {
outcomes?: string[]
outcomePrices?: string[] | number[]
clobTokenIds?: string[]
/** 市场是否已关闭closed=true 表示可结算/可领取 */
closed?: boolean
[key: string]: unknown
}
@ -162,8 +164,8 @@ export interface PositionDisplayItem {
marketID?: string
/** Token ID用于 claimPositionAPI 返回 tokenId */
tokenID?: string
/** 是否待领取/未结算needClaim 为 true 时显示领取按钮) */
needClaim?: boolean
/** 所属市场是否已关闭market.closed=true 表示可结算/可领取 */
marketClosed?: boolean
}
/**
@ -230,6 +232,7 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
const marketID = String(pos.marketID ?? pos.market?.ID ?? '')
const tokenID = pos.tokenId ?? (pos as { tokenID?: string }).tokenID ?? ''
const imageUrl = (pos.market?.image ?? pos.market?.icon) as string | undefined
const marketClosed = pos.market?.closed === true
return {
id,
@ -252,6 +255,6 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
availableSharesNum: availableNum >= 0 ? availableNum : undefined,
marketID: marketID || undefined,
tokenID: tokenID || undefined,
needClaim: pos.needClaim,
marketClosed,
}
}

97
src/api/priceHistory.ts Normal file
View File

@ -0,0 +1,97 @@
/**
* GET /pmPriceHistory/getPmPriceHistoryPublic
* TradeDetail.vue 线
*/
import { buildQuery, get } from './request'
import type { ApiResponse } from './types'
import type { PageResult } from './types'
/** 单条价格历史(与 doc.json definitions["polymarket.PmPriceHistory"] 对齐) */
export interface PmPriceHistoryItem {
ID?: number
createdAt?: string
fidelity?: number
interval?: string
/** 市场标识 */
market?: string
/** 价格(通常 01 小数,前端展示可乘 100 为百分比) */
price?: number
/** 时间戳Unix 秒) */
time?: number
updatedAt?: string
}
/** GET /pmPriceHistory/getPmPriceHistoryPublic 请求参数 */
export interface GetPmPriceHistoryPublicParams {
/** 市场标识(必填,用于筛选) */
market: string
page?: number
pageSize?: number
/** 数据间隔 */
interval?: string
/** 时间筛选 */
time?: number
/** 创建时间范围 */
createdAtRange?: string[]
fidelity?: number
keyword?: string
order?: string
sort?: string
price?: number
}
/** 响应 data 为 PageResult<PmPriceHistoryItem> */
export interface PmPriceHistoryPublicResponse {
code: number
data?: PageResult<PmPriceHistoryItem>
msg: string
}
/**
*
* GET /pmPriceHistory/getPmPriceHistoryPublic
*/
export async function getPmPriceHistoryPublic(
params: GetPmPriceHistoryPublicParams,
config?: { headers?: Record<string, string> },
): Promise<PmPriceHistoryPublicResponse> {
const { market, page = 1, pageSize = 500, interval, time, createdAtRange, fidelity, keyword, order, sort, price } = params
const query = buildQuery({
market,
page,
pageSize,
interval,
time,
createdAtRange,
fidelity,
keyword,
order,
sort,
price,
})
return get<PmPriceHistoryPublicResponse>('/pmPriceHistory/getPmPriceHistoryPublic', query, config)
}
/** 图表单点格式 [timestamp_ms, value_0_100] */
export type ChartDataPoint = [number, number]
/**
* list ECharts 线
* - time
* - price 1 100 0100
*/
export function priceHistoryToChartData(list: PmPriceHistoryItem[]): ChartDataPoint[] {
if (!list?.length) return []
const out: ChartDataPoint[] = []
for (const item of list) {
const t = item.time
const p = item.price
if (t == null || p == null || !Number.isFinite(Number(t))) continue
const tsMs = Number(t) < 1e12 ? Number(t) * 1000 : Number(t)
const value = Number(p) <= 1 ? Number(p) * 100 : Number(p)
out.push([tsMs, value])
}
out.sort((a, b) => a[0] - b[0])
return out
}

View File

@ -37,7 +37,7 @@
<div v-for="(ask, index) in asksWithCumulativeTotal" :key="index" class="order-item">
<div class="order-progress">
<HorizontalProgressBar
:max="maxAsksTotal"
:max="maxOrderBookTotal"
:value="ask.total"
:fillStyle="{ backgroundColor: '#e89595' }"
:trackStyle="{ backgroundColor: 'transparent' }"
@ -57,7 +57,7 @@
>
<div class="order-progress">
<HorizontalProgressBar
:max="maxBidsTotal"
:max="maxOrderBookTotal"
:value="bid.total"
:fillStyle="{ backgroundColor: '#7acc7a' }"
:trackStyle="{ backgroundColor: 'transparent' }"
@ -284,17 +284,12 @@ const bidsWithCumulativeTotal = computed(() => {
// Reverse to display in descending order
})
// Calculate max total from cumulative totals
const maxAsksTotal = computed(() => {
// Shared max total from both asks and bids cumulative totals (for progress bar scale)
const maxOrderBookTotal = computed(() => {
const askTotals = asksWithCumulativeTotal.value.map((item) => item.cumulativeTotal)
const allTotals = [...askTotals]
return Math.max(...allTotals)
})
const maxBidsTotal = computed(() => {
const bidTotals = bidsWithCumulativeTotal.value.map((item) => item.cumulativeTotal)
const allTotals = [...bidTotals]
return Math.max(...allTotals)
const allTotals = [...askTotals, ...bidTotals]
return allTotals.length ? Math.max(...allTotals) : 0
})
</script>

View File

@ -63,7 +63,9 @@
<div class="input-group shares-group">
<div class="shares-header sell-shares-header">
<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 class="shares-input-wrapper">
<v-text-field
@ -94,7 +96,9 @@
<div class="input-group shares-group">
<div class="shares-header sell-shares-header">
<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 class="shares-input-wrapper">
<v-text-field
@ -154,11 +158,7 @@
<p v-if="orderError" class="order-error">{{ orderError }}</p>
<!-- Action Button: Buy 余额足够显示 Buy Yes/No不足显示 {{ t('trade.deposit') }}Sell 只显示 Sell Yes/No -->
<v-btn
v-if="activeTab === 'buy' && showDepositForBuy"
class="deposit-btn"
@click="deposit"
>
<v-btn v-if="activeTab === 'buy' && showDepositForBuy" class="deposit-btn" @click="deposit">
{{ t('trade.deposit') }}
</v-btn>
<v-btn
@ -201,7 +201,9 @@
<div class="input-group shares-group">
<div class="shares-header sell-shares-header">
<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 class="shares-input-wrapper">
<v-text-field
@ -243,7 +245,9 @@
<div class="input-group shares-group">
<div class="shares-header sell-shares-header">
<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 class="shares-input-wrapper">
<v-text-field
@ -352,7 +356,9 @@
<div class="input-group shares-group">
<div class="shares-header sell-shares-header">
<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 class="shares-input-wrapper">
<v-text-field
@ -382,7 +388,9 @@
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setMaxShares">{{ t('trade.max') }}</v-btn>
</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">
<v-icon size="14">mdi-information</v-icon>
<span>20.00 matching</span>
@ -506,7 +514,9 @@
<div class="input-group shares-group">
<div class="shares-header sell-shares-header">
<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 class="shares-input-wrapper">
<v-text-field
@ -537,7 +547,9 @@
<div class="input-group shares-group">
<div class="shares-header sell-shares-header">
<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 class="shares-input-wrapper">
<v-text-field
@ -563,22 +575,23 @@
<div class="total-section">
<template v-if="activeTab === 'buy'">
<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 class="total-row">
<span class="label">{{ t('trade.toWin') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span
>
<div class="total-row">
<span class="label">{{ t('trade.toWin') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ toWinValue }}</span
>
</div>
</template>
<template v-else>
<div class="total-row">
<span class="label">{{ t('trade.youllReceive') }}</span
><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 class="total-row avg-price-row">
@ -630,7 +643,9 @@
<div class="input-group shares-group">
<div class="shares-header sell-shares-header">
<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 class="shares-input-wrapper">
<v-text-field
@ -669,7 +684,9 @@
<div class="input-group shares-group">
<div class="shares-header sell-shares-header">
<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 class="shares-input-wrapper">
<v-text-field
@ -768,7 +785,9 @@
<div class="input-group shares-group">
<div class="shares-header sell-shares-header">
<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 class="shares-input-wrapper">
<v-text-field
@ -820,13 +839,14 @@
<div class="total-section">
<template v-if="activeTab === 'buy'">
<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 class="total-row">
<span class="label">{{ t('trade.toWin') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span
>
<div class="total-row">
<span class="label">{{ t('trade.toWin') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span
>
</div>
</template>
<template v-else>
@ -936,7 +956,9 @@
<div class="input-group shares-group">
<div class="shares-header sell-shares-header">
<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 class="shares-input-wrapper">
<v-text-field
@ -967,7 +989,9 @@
<div class="input-group shares-group">
<div class="shares-header sell-shares-header">
<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 class="shares-input-wrapper">
<v-text-field
@ -996,20 +1020,20 @@
<span class="label">Total</span
><span class="total-value">${{ totalPrice }}</span>
</div>
<div class="total-row">
<span class="label">{{ t('trade.toWin') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span
>
<div class="total-row">
<span class="label">{{ t('trade.toWin') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ toWinValue }}</span
>
</div>
</template>
<template v-else>
<div class="total-row">
<span class="label">{{ t('trade.youllReceive') }}</span
><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 class="total-row avg-price-row">
@ -1061,7 +1085,9 @@
<div class="input-group shares-group">
<div class="shares-header sell-shares-header">
<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 class="shares-input-wrapper">
<v-text-field
@ -1100,7 +1126,9 @@
<div class="input-group shares-group">
<div class="shares-header sell-shares-header">
<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 class="shares-input-wrapper">
<v-text-field
@ -1199,7 +1227,9 @@
<div class="input-group shares-group">
<div class="shares-header sell-shares-header">
<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 class="shares-input-wrapper">
<v-text-field
@ -1251,22 +1281,23 @@
<div class="total-section">
<template v-if="activeTab === 'buy'">
<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 class="total-row">
<span class="label">{{ t('trade.toWin') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span
>
<div class="total-row">
<span class="label">{{ t('trade.toWin') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ toWinValue }}</span
>
</div>
</template>
<template v-else>
<div class="total-row">
<span class="label">{{ t('trade.youllReceive') }}</span
><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>
</template>
@ -1324,7 +1355,9 @@
</div>
<p class="merge-available">
{{ 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 v-if="!props.market?.marketId" class="merge-no-market">
{{ t('trade.mergeNoMarket', { yesLabel, noLabel }) }}
@ -1498,7 +1531,13 @@ const props = withDefaults(
/** 当前市场持仓列表,用于计算可合并份额 */
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 = ''
try {
const res = await pmMarketSplit(
{ marketID: marketId, usdcAmount: String(splitAmount.value) },
{ marketID: marketId, usdcAmount: String(splitAmount.value * 1000000) },
{ headers: userStore.getAuthHeaders() },
)
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 shares = ref(20) //
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(() =>
EXPIRATION_VALUES.map((v) => ({ title: t(`trade.expiration.${v}`), value: v })),
)
@ -1856,7 +1907,7 @@ const handleOptionChange = (option: 'yes' | 'no') => {
const noP = props.market?.noPrice ?? 0.82
limitPrice.value = clampLimitPrice(option === 'yes' ? yesP : noP)
emit('optionChange', option)
// Set max shares when option changes in sell mode
if (activeTab.value === 'sell') {
setMaxShares()
@ -1944,7 +1995,10 @@ const adjustShares = (amount: number) => {
/** 卖出时输入份额是否超过最大可卖 */
const sellSharesExceedsMax = computed(
() => activeTab.value === 'sell' && maxAvailableShares.value >= 0 && shares.value > maxAvailableShares.value,
() =>
activeTab.value === 'sell' &&
maxAvailableShares.value >= 0 &&
shares.value > maxAvailableShares.value,
)
// Sell使
@ -2058,12 +2112,7 @@ async function submitOrder() {
return
}
const uid = userStore?.user?.ID ?? 0
const userIdNum =
typeof uid === 'number'
? uid
: uid != null
? parseInt(String(uid), 10)
: 0
const userIdNum = typeof uid === 'number' ? uid : uid != null ? parseInt(String(uid), 10) : 0
if (!Number.isFinite(userIdNum) || userIdNum <= 0) {
orderError.value = t('trade.userError')
isNoAvailableSharesError.value = false
@ -2096,9 +2145,7 @@ async function submitOrder() {
? parseExpirationTimestamp(expirationTime.value)
: 0
const rawSize = isMarket && activeTab.value === 'buy'
? amount.value
: clampShares(shares.value)
const rawSize = isMarket && activeTab.value === 'buy' ? amount.value : clampShares(shares.value)
const sizeValue = Math.round(rawSize * 1_000_000)
orderLoading.value = true

View File

@ -2,6 +2,7 @@ import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import { getUsdcBalance, formatUsdcBalance, getUserInfo } from '@/api/user'
import { getUserWsUrl, BASE_URL } from '@/api/request'
import { jsonInBlacklist } from '@/api/jwt'
import { UserSdk, type BalanceData, type PositionData } from '../../sdk/userSocket'
export interface UserInfo {
@ -49,11 +50,7 @@ function parseUserId(raw: { id?: number | string; ID?: number } | null | undefin
} {
const rawId = raw?.ID ?? raw?.id
const numId =
typeof rawId === 'number'
? rawId
: rawId != null
? parseInt(String(rawId), 10)
: undefined
typeof rawId === 'number' ? rawId : rawId != null ? parseInt(String(rawId), 10) : undefined
return { id: rawId ?? numId, numId: Number.isFinite(numId) ? numId : undefined }
}
@ -120,7 +117,9 @@ export const useUserStore = defineStore('user', () => {
}
/** 订阅 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)
return () => {
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()
token.value = ''
user.value = null

View File

@ -53,7 +53,7 @@
<!-- 图表区域 -->
<div class="chart-wrapper">
<div v-if="cryptoChartLoading" class="chart-loading-overlay">
<div v-if="cryptoChartLoading || chartYesNoLoading" class="chart-loading-overlay">
<v-progress-circular indeterminate color="primary" size="32" />
</div>
<div ref="chartContainerRef" class="chart-container"></div>
@ -398,12 +398,11 @@ import {
type OpenOrderDisplayItem,
} from '../api/order'
import { cancelOrder as apiCancelOrder } from '../api/order'
import type { ChartDataPoint, ChartTimeRange } from '../api/chart'
import {
normalizeChartData,
fetchChartHistory,
type ChartDataPoint,
type ChartTimeRange,
} from '../api/chart'
getPmPriceHistoryPublic,
priceHistoryToChartData,
} from '../api/priceHistory'
import {
isCryptoEvent as checkIsCryptoEvent,
inferCryptoSymbol,
@ -1242,52 +1241,47 @@ const timeRanges = [
{ label: 'ALL', value: 'ALL' },
]
// [(ms), (0-100)][] src/api/chart.tsChartHistoryParams / ChartHistoryItem / normalizeChartData
function generateData(range: string): ChartDataPoint[] {
const now = Date.now()
const data: [number, number][] = []
let stepMs: number
let count: number
switch (range) {
case '1H':
stepMs = 60 * 1000
count = 60
break
case '6H':
stepMs = 10 * 60 * 1000
count = 36
break
case '1D':
stepMs = 60 * 60 * 1000
count = 24
break
case '1W':
stepMs = 24 * 60 * 60 * 1000
count = 7
break
case '1M':
case 'ALL':
stepMs = 24 * 60 * 60 * 1000
count = 30
break
default:
stepMs = 60 * 60 * 1000
count = 24
}
let value = 15 + Math.random() * 25
for (let i = count; i >= 0; i--) {
const t = now - i * stepMs
value = Math.max(10, Math.min(90, value + (Math.random() - 0.5) * 6))
data.push([t, Math.round(value * 10) / 10])
}
return data
}
const chartContainerRef = ref<HTMLElement | null>(null)
const data = ref<[number, number][]>([])
/** Yes/No 折线图接口返回的完整数据time 已转 ms用于分时筛选 */
const rawChartData = ref<ChartDataPoint[]>([])
const cryptoChartLoading = ref(false)
const chartYesNoLoading = ref(false)
let chartInstance: ECharts | null = null
let dynamicInterval: number | undefined
/** 分时范围对应的毫秒数ALL 返回 null 表示不截断 */
function getTimeRangeMs(range: string): number | null {
const H = 60 * 60 * 1000
const D = 24 * H
switch (range) {
case '1H':
return 1 * H
case '6H':
return 6 * H
case '1D':
return 1 * D
case '1W':
return 7 * D
case '1M':
return 30 * D
case 'ALL':
default:
return null
}
}
/** 按分时范围过滤 [timestamp_ms, value][],保留区间 [now - rangeMs, now] 内的点 */
function filterChartDataByRange(
points: ChartDataPoint[],
range: string,
): ChartDataPoint[] {
if (!points.length) return []
const rangeMs = getTimeRangeMs(range)
if (rangeMs == null) return points
const nowMs = Date.now()
const cutoffMs = nowMs - rangeMs
return points.filter(([ts]) => ts >= cutoffMs)
}
const currentChance = computed(() => {
const ev = eventDetail.value
@ -1523,7 +1517,7 @@ function buildOptionForCrypto(
function initChart() {
if (!chartContainerRef.value) return
data.value = generateData(selectedTimeRange.value)
data.value = []
chartInstance = echarts.init(chartContainerRef.value)
const w = chartContainerRef.value.clientWidth
if (chartMode.value === 'crypto') {
@ -1533,12 +1527,15 @@ function initChart() {
}
}
/** 从接口拉取图表数据(接入时在 updateChartData 中调用并赋给 data.value */
async function loadChartFromApi(marketId: string): Promise<ChartDataPoint[]> {
const res = await fetchChartHistory(
{ marketID: marketId, range: selectedTimeRange.value as ChartTimeRange }
)
return normalizeChartData(res.data ?? [])
/** 从 GET /pmPriceHistory/getPmPriceHistoryPublic 拉取价格历史market 传 YES 对应的 clobTokenId */
async function loadChartFromApi(marketParam: string): Promise<ChartDataPoint[]> {
const res = await getPmPriceHistoryPublic({
market: marketParam,
page: 1,
pageSize: 500,
})
const list = res.data?.list ?? []
return priceHistoryToChartData(list)
}
const MINUTE_MS = 60 * 1000
@ -1566,7 +1563,6 @@ function applyCryptoRealtimePoint(point: [number, number]) {
)
}
// 使 updateChartData await loadChartFromApi(marketId) setOption generateData
async function updateChartData() {
const w = chartContainerRef.value?.clientWidth
if (chartMode.value === 'crypto') {
@ -1592,60 +1588,35 @@ async function updateChartData() {
} else {
cryptoWsUnsubscribe?.()
cryptoWsUnsubscribe = null
data.value = generateData(selectedTimeRange.value)
if (chartInstance)
chartInstance.setOption(buildOption(data.value, w), { replaceMerge: ['series'] })
chartYesNoLoading.value = true
try {
// market clobTokenIds[0]YES token ID
const yesTokenId = clobTokenIds.value[0]
const points = yesTokenId ? await loadChartFromApi(yesTokenId) : []
rawChartData.value = points
data.value = filterChartDataByRange(points, selectedTimeRange.value)
if (chartInstance)
chartInstance.setOption(buildOption(data.value, w), { replaceMerge: ['series'] })
} finally {
chartYesNoLoading.value = false
}
}
}
function selectTimeRange(range: string) {
selectedTimeRange.value = range
updateChartData()
}
function getMaxPoints(range: string): number {
switch (range) {
case '1H':
return 60
case '6H':
return 36
case '1D':
return 24
case '1W':
return 7
case '1M':
case 'ALL':
return 30
default:
return 24
}
}
function startDynamicUpdate() {
dynamicInterval = window.setInterval(() => {
if (chartMode.value === 'crypto') return
const list = [...data.value]
const last = list[list.length - 1]
if (!last) return
const nextVal = Math.max(10, Math.min(90, last[1] + (Math.random() - 0.5) * 4))
const nextT = Date.now()
list.push([nextT, Math.round(nextVal * 10) / 10])
const max = getMaxPoints(selectedTimeRange.value)
data.value = list.slice(-max)
watch(selectedTimeRange, (range) => {
if (chartMode.value === 'yesno') {
data.value = filterChartDataByRange(rawChartData.value, range)
const w = chartContainerRef.value?.clientWidth
if (chartInstance)
chartInstance.setOption(buildOption(data.value, w), { replaceMerge: ['series'] })
}, 3000)
}
function stopDynamicUpdate() {
if (dynamicInterval) {
clearInterval(dynamicInterval)
dynamicInterval = undefined
} else {
updateChartData()
}
}
watch(selectedTimeRange, () => updateChartData())
})
// CLOB market clobTokenIds 使 Yes/No token ID
const clobTokenIds = computed(() => {
@ -1711,15 +1682,13 @@ const unsubscribePositionUpdate = userStore.onPositionUpdate((data) => {
})
onMounted(() => {
loadEventDetail()
loadEventDetail().then(() => updateChartData())
initChart()
startDynamicUpdate()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
unsubscribePositionUpdate()
stopDynamicUpdate()
cryptoWsUnsubscribe?.()
cryptoWsUnsubscribe = null
window.removeEventListener('resize', handleResize)

View File

@ -735,7 +735,8 @@ import { useUserStore } from '../stores/user'
import { useLocaleStore } from '../stores/locale'
import { useAuthError } from '../composables/useAuthError'
import { cancelOrder as apiCancelOrder } from '../api/order'
import { getOrderList, mapOrderToHistoryItem, mapOrderToOpenOrderItem, OrderStatus } from '../api/order'
import { getOrderList, mapOrderToOpenOrderItem, OrderStatus } from '../api/order'
import { getHistoryRecordListClient, getHistoryRecordList } from '../api/historyRecord'
import { getPositionList, mapPositionToDisplayItem, claimPosition } from '../api/position'
import {
getSettlementRequestsListClient,
@ -780,7 +781,7 @@ const withdrawStatusOptions = computed(() => [
const currentPositionList = computed(() =>
USE_MOCK_WALLET ? positions.value : positionList.value,
)
/** 未结算项:从持仓列表中筛出可领取的(有 marketID+tokenID;若后端有 needClaim 则仅 needClaim 为 true */
/** 未结算项:从持仓列表中筛出可领取的(有 marketID+tokenID,且所属市场已关闭 market.closed=true */
const unsettledItems = computed(() => {
const list = currentPositionList.value
return list
@ -788,7 +789,7 @@ const unsettledItems = computed(() => {
(p) =>
p.marketID &&
p.tokenID &&
(p.needClaim === undefined || p.needClaim === true),
p.marketClosed === true,
)
.map((p) => {
const amount = parseFloat(String(p.value).replace(/[^0-9.-]/g, '')) || 0
@ -872,8 +873,8 @@ interface Position {
marketID?: string
/** Token ID从持仓列表来用于领取结算 */
tokenID?: string
/** 是否待领取/未结算(后端可选,无则按有 marketID+tokenID 视为可领取) */
needClaim?: boolean
/** 所属市场是否已关闭marketClosed=true 表示可结算/可领取 */
marketClosed?: boolean
}
/** 从 avgNow "72¢ → 0.5¢" 解析出 [avg, now] */
@ -1030,6 +1031,7 @@ const historyList = ref<HistoryItem[]>([])
const historyTotal = ref(0)
const historyLoading = ref(false)
/** 历史记录来自 GET /hr/getHistoryRecordListClient需鉴权按当前用户分页 */
async function loadHistoryOrders() {
if (USE_MOCK_WALLET) return
const headers = userStore.getAuthHeaders()
@ -1047,18 +1049,18 @@ async function loadHistoryOrders() {
}
historyLoading.value = true
try {
const res = await getOrderList(
const res = await getHistoryRecordListClient(
{
page: page.value,
pageSize: itemsPerPage.value,
userID,
userId: userID,
},
{ headers },
)
if (res.code === 0 || res.code === 200) {
const list = res.data?.list ?? []
historyList.value = list.map(mapOrderToHistoryItem)
historyTotal.value = res.data?.total ?? 0
const { list, total } = getHistoryRecordList(res.data)
historyList.value = list
historyTotal.value = total
} else {
historyList.value = []
historyTotal.value = 0
@ -1346,8 +1348,11 @@ function shareHistory(id: string) {
const plChartRef = ref<HTMLElement | null>(null)
let plChartInstance: ECharts | null = null
/** 根据时间范围生成盈亏折线数据 [timestamp, pnl] */
function generatePlData(range: string): [number, number][] {
/**
* 资产变化折线图数据 [timestamp_ms, pnl]
* 暂无接口时返回真实格式的时间序列数值均为 0有接口后改为从 API 拉取并在此处做分时过滤
*/
function getPlChartData(range: string): [number, number][] {
const now = Date.now()
let stepMs: number
let count: number
@ -1371,11 +1376,9 @@ function generatePlData(range: string): [number, number][] {
break
}
const data: [number, number][] = []
let pnl = 0
for (let i = count; i >= 0; i--) {
const t = now - i * stepMs
pnl += (Math.random() - 0.48) * 20
data.push([t, Math.round(pnl * 100) / 100])
data.push([t, 0])
}
return data
}
@ -1437,18 +1440,18 @@ function buildPlChartOption(chartData: [number, number][]) {
const plChartData = ref<[number, number][]>([])
function updatePlChart() {
plChartData.value = generatePlData(plRange.value)
plChartData.value = getPlChartData(plRange.value)
const last = plChartData.value[plChartData.value.length - 1]
if (last) profitLoss.value = last[1].toFixed(2)
if (last != null) profitLoss.value = last[1].toFixed(2)
if (plChartInstance)
plChartInstance.setOption(buildPlChartOption(plChartData.value), { replaceMerge: ['series'] })
}
function initPlChart() {
if (!plChartRef.value) return
plChartData.value = generatePlData(plRange.value)
plChartData.value = getPlChartData(plRange.value)
const last = plChartData.value[plChartData.value.length - 1]
if (last) profitLoss.value = last[1].toFixed(2)
if (last != null) profitLoss.value = last[1].toFixed(2)
plChartInstance = echarts.init(plChartRef.value)
plChartInstance.setOption(buildPlChartOption(plChartData.value))
}