Compare commits
5 Commits
947ba83b2d
...
fa5e0dccfe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa5e0dccfe | ||
|
|
a29dac0146 | ||
|
|
3983105628 | ||
|
|
9dd61be92f | ||
|
|
d44e7157e8 |
@ -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
87
docs/api/historyRecord.md
Normal 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/getHistoryRecordListClient(Wallet 使用)
|
||||
|
||||
需鉴权: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
27
docs/api/jwt.md
Normal 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),仍会执行本地登出。
|
||||
@ -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
69
docs/api/priceHistory.md
Normal 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`)。
|
||||
|
||||
### PmPriceHistoryItem(list 单项)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| ID | number | 主键 |
|
||||
| market | string | 市场标识 |
|
||||
| price | number | 价格(0–1 小数或 0–100,由 priceHistoryToChartData 统一为 0–100) |
|
||||
| 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` 固定为 0–100,`priceHistoryToChartData` 已兼容(≤1 时乘 100)
|
||||
@ -9,7 +9,7 @@
|
||||
## 核心能力
|
||||
|
||||
- Trade Up / Trade Down Tab
|
||||
- Asks、Bids 列表,带 `HorizontalProgressBar` 深度条
|
||||
- Asks、Bids 列表,带 `HorizontalProgressBar` 深度条(买卖两边共用同一最大值 `maxOrderBookTotal`,取两边累计总量中的最大值,便于对比深度)
|
||||
- Last price、Spread 展示
|
||||
- Live / 连接中 状态展示(均通过 i18n 国际化)
|
||||
|
||||
|
||||
@ -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` 复用
|
||||
|
||||
@ -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`(0–1)转成 `[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` 消息,实时追加成交记录
|
||||
|
||||
@ -10,8 +10,9 @@
|
||||
## 核心能力
|
||||
|
||||
- Portfolio 卡片:余额、Deposit/Withdraw 按钮
|
||||
- Profit/Loss 卡片:时间范围切换、ECharts 图表
|
||||
- Tab:Positions、Open orders、History、Withdrawals(提现记录)
|
||||
- Profit/Loss 卡片:时间范围切换(1D/1W/1M/ALL)、ECharts 资产变化折线图;数据格式为 `[timestamp_ms, pnl][]`,**暂无接口时全部显示为 0**(真实时间轴 + 数值 0),有接口后在此处对接
|
||||
- Tab:Positions、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` 统一提示「请先登录」或「权限不足」
|
||||
|
||||
@ -110,6 +110,7 @@ export interface PmEventListResponse {
|
||||
|
||||
/**
|
||||
* GET /PmEvent/getPmEventPublic 请求参数(与 doc.json 对齐)
|
||||
* startDateMin、endDateMax 不传;startDateMax、endDateMin 传当前时间戳(秒)
|
||||
*/
|
||||
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
185
src/api/historyRecord.ts
Normal file
@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 历史记录接口:getHistoryRecordListClient(需鉴权)、getHistoryRecordPublic(无需鉴权)
|
||||
* 用于 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-token、x-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
18
src/api/jwt.ts
Normal 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)
|
||||
}
|
||||
@ -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(用于 claimPosition,API 返回 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
97
src/api/priceHistory.ts
Normal 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
|
||||
/** 价格(通常 0–1 小数,前端展示可乘 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;否则视为 0–100
|
||||
*/
|
||||
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
|
||||
}
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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,12 +575,14 @@
|
||||
<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
|
||||
><v-icon size="16" color="green">mdi-currency-usd</v-icon>
|
||||
{{ toWinValue }}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
@ -576,9 +590,8 @@
|
||||
<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,7 +839,8 @@
|
||||
<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
|
||||
@ -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
|
||||
@ -999,7 +1023,8 @@
|
||||
<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
|
||||
><v-icon size="16" color="green">mdi-currency-usd</v-icon>
|
||||
{{ toWinValue }}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
@ -1007,9 +1032,8 @@
|
||||
<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,12 +1281,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
|
||||
><v-icon size="16" color="green">mdi-currency-usd</v-icon>
|
||||
{{ toWinValue }}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
@ -1264,9 +1296,8 @@
|
||||
<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) // 内部存储 0–1,显示为美分与按钮一致
|
||||
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 })),
|
||||
)
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.ts(ChartHistoryParams / 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)
|
||||
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)
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user