Compare commits
No commits in common. "e7a33c96382d11c5209df43cd1c3b5a8adc70ad0" and "fa5e0dccfe0ea32d09898a714ff5d09215217205" have entirely different histories.
e7a33c9638
...
fa5e0dccfe
3
.env
3
.env
@ -1,8 +1,7 @@
|
|||||||
# API 基础地址,不设置时默认 https://api.xtrader.vip
|
# API 基础地址,不设置时默认 https://api.xtrader.vip
|
||||||
# 连接测试服务器 192.168.3.21:8888 时复制本文件为 .env 或 .env.local 并取消下一行注释:
|
# 连接测试服务器 192.168.3.21:8888 时复制本文件为 .env 或 .env.local 并取消下一行注释:
|
||||||
# VITE_API_BASE_URL=http://192.168.3.14:8888
|
VITE_API_BASE_URL=http://localhost:8888
|
||||||
|
|
||||||
# VITE_USE_MOCK_DATA=false # 全部关闭 mock
|
|
||||||
# SSH 部署(npm run deploy),可选覆盖
|
# SSH 部署(npm run deploy),可选覆盖
|
||||||
# DEPLOY_HOST=38.246.250.238
|
# DEPLOY_HOST=38.246.250.238
|
||||||
# DEPLOY_USER=root
|
# DEPLOY_USER=root
|
||||||
|
|||||||
@ -59,16 +59,17 @@
|
|||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| ID | number | 主键 |
|
| ID | number | 主键 |
|
||||||
| title | string | 标题(如「充值资金」) |
|
| title | string | 标题 |
|
||||||
| type | string | 类型(如 recharge) |
|
| name | string | 名称 |
|
||||||
| **usdcSize** | number | **金额(USDC)**,用于充值等 |
|
| eventSlug | string | 事件标识 |
|
||||||
| **icon** | string | **图标路径**(如 uploads/file/btc.png),会转为完整 URL 展示 |
|
| outcome | string | 结果(Yes/No 等) |
|
||||||
| **UpdatedAt** | string | **更新时间**,用于 timeAgo 展示 |
|
| side | string | 方向 |
|
||||||
|
| type | string | 类型 |
|
||||||
| price | number | 价格 |
|
| price | number | 价格 |
|
||||||
| size | number | 大小 |
|
| size | number | 大小 |
|
||||||
| outcome | string | 结果 |
|
| createdAt | string | 创建时间 |
|
||||||
| timestamp | number | 时间戳(秒或毫秒) |
|
| timestamp | number | 时间戳(秒) |
|
||||||
| 其他 | - | asset, bio, conditionId, slug, transactionHash 等 |
|
| 其他 | - | asset, bio, conditionId, icon, slug, transactionHash 等 |
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,6 @@
|
|||||||
## 核心能力
|
## 核心能力
|
||||||
|
|
||||||
- `getPmPriceHistoryPublic`:按市场 ID 分页获取价格历史
|
- `getPmPriceHistoryPublic`:按市场 ID 分页获取价格历史
|
||||||
- `getTimeRangeSeconds`:根据分时范围计算 `startTs`、`endTs`(Unix 秒);`endTs` 始终为当前时间;1H/6H/1D/1W/1M 的 `startTs` 为当前时间往前对应时长;ALL 的 `startTs` 为事件开始时间、`endTs` 为当前时间
|
|
||||||
- `priceHistoryToChartData`:将接口返回的 `list` 转为 ECharts 使用的 `[timestamp_ms, value_0_100][]`
|
- `priceHistoryToChartData`:将接口返回的 `list` 转为 ECharts 使用的 `[timestamp_ms, value_0_100][]`
|
||||||
|
|
||||||
## GET /pmPriceHistory/getPmPriceHistoryPublic
|
## GET /pmPriceHistory/getPmPriceHistoryPublic
|
||||||
@ -21,8 +20,6 @@
|
|||||||
| market | string | 是 | 传 YES 对应的 clobTokenId(即当前市场 clobTokenIds[0]) |
|
| market | string | 是 | 传 YES 对应的 clobTokenId(即当前市场 clobTokenIds[0]) |
|
||||||
| page | number | 否 | 页码,默认 1 |
|
| page | number | 否 | 页码,默认 1 |
|
||||||
| pageSize | number | 否 | 每页条数,默认 500 |
|
| pageSize | number | 否 | 每页条数,默认 500 |
|
||||||
| startTs | number | 否 | 时间范围起始(Unix 秒) |
|
|
||||||
| endTs | number | 否 | 时间范围结束(Unix 秒) |
|
|
||||||
| interval | string | 否 | 数据间隔 |
|
| interval | string | 否 | 数据间隔 |
|
||||||
| time | number | 否 | 时间筛选 |
|
| time | number | 否 | 时间筛选 |
|
||||||
| createdAtRange | string[] | 否 | 创建时间范围 |
|
| createdAtRange | string[] | 否 | 创建时间范围 |
|
||||||
@ -55,22 +52,18 @@
|
|||||||
import {
|
import {
|
||||||
getPmPriceHistoryPublic,
|
getPmPriceHistoryPublic,
|
||||||
priceHistoryToChartData,
|
priceHistoryToChartData,
|
||||||
getTimeRangeSeconds,
|
|
||||||
type PmPriceHistoryItem,
|
type PmPriceHistoryItem,
|
||||||
} from '@/api/priceHistory'
|
} from '@/api/priceHistory'
|
||||||
|
|
||||||
const timeRange = getTimeRangeSeconds('1D') // { startTs, endTs }
|
|
||||||
// ALL 时传 eventDates:getTimeRangeSeconds('ALL', { startDate: ev.startDate, endDate: ev.endDate })
|
|
||||||
const res = await getPmPriceHistoryPublic({
|
const res = await getPmPriceHistoryPublic({
|
||||||
market: marketId,
|
market: marketId,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 500,
|
pageSize: 500,
|
||||||
...(timeRange && { startTs: timeRange.startTs, endTs: timeRange.endTs }),
|
|
||||||
})
|
})
|
||||||
const chartData = priceHistoryToChartData(res.data?.list ?? [])
|
const chartData = priceHistoryToChartData(res.data?.list ?? [])
|
||||||
```
|
```
|
||||||
|
|
||||||
## 扩展方式
|
## 扩展方式
|
||||||
|
|
||||||
- 时间戳规则:`endTs` 始终为当前时间;1H/6H/1D/1W/1M 的 `startTs` 为当前时间往前对应时长;ALL 的 `startTs` 为事件 `startDate`,`endTs` 为当前时间
|
- 按时间范围(1H/6H/1D 等)传 `interval` 或 `createdAtRange` 需与后端约定取值
|
||||||
- 若后端返回的 `price` 固定为 0–100,`priceHistoryToChartData` 已兼容(≤1 时乘 100)
|
- 若后端返回的 `price` 固定为 0–100,`priceHistoryToChartData` 已兼容(≤1 时乘 100)
|
||||||
|
|||||||
@ -7,17 +7,12 @@
|
|||||||
|
|
||||||
事件下的市场列表页,展示某个 Event 的多个 Market(如 NFL 多支队伍),支持选择并跳转交易详情。
|
事件下的市场列表页,展示某个 Event 的多个 Market(如 NFL 多支队伍),支持选择并跳转交易详情。
|
||||||
|
|
||||||
- **多市场折线图**:按市场数量依次调用 `getPmPriceHistoryPublic`,每个市场使用 `clobTokenIds[0]`(YES token)作为 `market` 参数,展示多条分时曲线
|
|
||||||
- **时间范围**:1H / 6H / 1D / 1W / 1M / ALL,与 TradeDetail 一致
|
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
- 从首页或详情进入,路由 `/event/123/markets`
|
- 从首页或详情进入,路由 `/event/123/markets`
|
||||||
- 路由参数 `id` 为 Event ID
|
- 路由参数 `id` 为 Event ID
|
||||||
- 分时图数据来源:`src/api/priceHistory.ts` 的 `getPmPriceHistoryPublic`、`priceHistoryToChartData`
|
|
||||||
|
|
||||||
## 扩展方式
|
## 扩展方式
|
||||||
|
|
||||||
- 增加市场筛选、排序
|
- 增加市场筛选、排序
|
||||||
- 与 TradeDetail 联动,支持从市场列表直接进入指定 market 的交易
|
- 与 TradeDetail 联动,支持从市场列表直接进入指定 market 的交易
|
||||||
- 可抽取 `getTimeRangeMs`、`filterChartDataByRange` 为共享 util,与 TradeDetail 复用
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { buildQuery, get } from './request'
|
import { buildQuery, get } from './request'
|
||||||
import { BASE_URL } from './request'
|
|
||||||
import type { PageResult } from './types'
|
import type { PageResult } from './types'
|
||||||
|
|
||||||
/** 单条历史记录(与 doc.json definitions["polymarket.HistoryRecord"] 对齐) */
|
/** 单条历史记录(与 doc.json definitions["polymarket.HistoryRecord"] 对齐) */
|
||||||
@ -14,7 +13,6 @@ export interface HistoryRecordItem {
|
|||||||
bio?: string
|
bio?: string
|
||||||
conditionId?: string
|
conditionId?: string
|
||||||
createdAt?: string
|
createdAt?: string
|
||||||
CreatedAt?: string
|
|
||||||
eventSlug?: string
|
eventSlug?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
name?: string
|
name?: string
|
||||||
@ -33,9 +31,6 @@ export interface HistoryRecordItem {
|
|||||||
transactionHash?: string
|
transactionHash?: string
|
||||||
type?: string
|
type?: string
|
||||||
updatedAt?: string
|
updatedAt?: string
|
||||||
UpdatedAt?: string
|
|
||||||
/** 金额(USDC),用于充值等类型 */
|
|
||||||
usdcSize?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GET /hr/getHistoryRecordPublic 请求参数 */
|
/** GET /hr/getHistoryRecordPublic 请求参数 */
|
||||||
@ -134,15 +129,11 @@ export interface HistoryDisplayItem {
|
|||||||
shares?: string
|
shares?: string
|
||||||
iconChar?: string
|
iconChar?: string
|
||||||
iconClass?: string
|
iconClass?: string
|
||||||
/** 图标 URL(来自 record.icon,用于展示) */
|
|
||||||
imageUrl?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTimeAgo(dateStr: string | undefined, timestamp?: number): string {
|
function formatTimeAgo(createdAt: string | undefined, timestamp?: number): string {
|
||||||
let ms = 0
|
const ms = createdAt ? new Date(createdAt).getTime() : (timestamp != null ? timestamp * 1000 : 0)
|
||||||
if (dateStr) ms = new Date(dateStr).getTime()
|
if (!ms) return ''
|
||||||
else if (timestamp != null) ms = timestamp < 1e12 ? timestamp * 1000 : timestamp
|
|
||||||
if (!ms || !Number.isFinite(ms)) return ''
|
|
||||||
const diff = Date.now() - ms
|
const diff = Date.now() - ms
|
||||||
if (diff < 60000) return 'Just now'
|
if (diff < 60000) return 'Just now'
|
||||||
if (diff < 3600000) return `${Math.floor(diff / 60000)} minutes ago`
|
if (diff < 3600000) return `${Math.floor(diff / 60000)} minutes ago`
|
||||||
@ -151,18 +142,8 @@ function formatTimeAgo(dateStr: string | undefined, timestamp?: number): string
|
|||||||
return new Date(ms).toLocaleDateString()
|
return new Date(ms).toLocaleDateString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将相对路径转为完整 URL */
|
|
||||||
function toFullIconUrl(icon: string | undefined): string | undefined {
|
|
||||||
if (!icon?.trim()) return undefined
|
|
||||||
const s = icon.trim()
|
|
||||||
if (s.startsWith('http://') || s.startsWith('https://')) return s
|
|
||||||
const base = BASE_URL?.replace(/\/$/, '') ?? ''
|
|
||||||
return `${base}/${s.replace(/^\//, '')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 HistoryRecordItem 映射为钱包 History 展示项
|
* 将 HistoryRecordItem 映射为钱包 History 展示项
|
||||||
* 金额用 usdcSize,图标用 icon,日期用 UpdatedAt
|
|
||||||
*/
|
*/
|
||||||
export function mapHistoryRecordToDisplayItem(record: HistoryRecordItem): HistoryDisplayItem {
|
export function mapHistoryRecordToDisplayItem(record: HistoryRecordItem): HistoryDisplayItem {
|
||||||
const id = String(record.ID ?? '')
|
const id = String(record.ID ?? '')
|
||||||
@ -171,18 +152,13 @@ export function mapHistoryRecordToDisplayItem(record: HistoryRecordItem): Histor
|
|||||||
const side = outcome === 'No' || outcome === 'Down' ? 'No' : 'Yes'
|
const side = outcome === 'No' || outcome === 'Down' ? 'No' : 'Yes'
|
||||||
const typeLabel = record.type ?? 'Trade'
|
const typeLabel = record.type ?? 'Trade'
|
||||||
const activity = `${typeLabel} ${outcome}`.trim()
|
const activity = `${typeLabel} ${outcome}`.trim()
|
||||||
const usdcSize = record.usdcSize ?? 0
|
|
||||||
const price = record.price ?? 0
|
const price = record.price ?? 0
|
||||||
const size = record.size ?? 0
|
const size = record.size ?? 0
|
||||||
const valueUsd = usdcSize !== 0 ? usdcSize : price * size
|
const valueUsd = price * size
|
||||||
const value = `$${Math.abs(valueUsd).toFixed(2)}`
|
const value = `$${Math.abs(valueUsd).toFixed(2)}`
|
||||||
const priceCents = Math.round(price * 100)
|
const priceCents = Math.round(price * 100)
|
||||||
const activityDetail = size > 0 ? `Sold ${Math.floor(size)} ${outcome} at ${priceCents}¢` : value
|
const activityDetail = size > 0 ? `Sold ${Math.floor(size)} ${outcome} at ${priceCents}¢` : value
|
||||||
const timeAgo = formatTimeAgo(
|
const timeAgo = formatTimeAgo(record.createdAt, record.timestamp)
|
||||||
record.UpdatedAt ?? record.updatedAt ?? record.CreatedAt ?? record.createdAt,
|
|
||||||
record.timestamp,
|
|
||||||
)
|
|
||||||
const imageUrl = toFullIconUrl(record.icon)
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
market,
|
market,
|
||||||
@ -195,7 +171,6 @@ export function mapHistoryRecordToDisplayItem(record: HistoryRecordItem): Histor
|
|||||||
timeAgo,
|
timeAgo,
|
||||||
avgPrice: priceCents ? `${priceCents}¢` : undefined,
|
avgPrice: priceCents ? `${priceCents}¢` : undefined,
|
||||||
shares: size > 0 ? String(Math.floor(size)) : undefined,
|
shares: size > 0 ? String(Math.floor(size)) : undefined,
|
||||||
imageUrl,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,10 +28,6 @@ export interface GetPmPriceHistoryPublicParams {
|
|||||||
market: string
|
market: string
|
||||||
page?: number
|
page?: number
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
/** 时间范围:起始时间戳(Unix 秒) */
|
|
||||||
startTs?: number
|
|
||||||
/** 时间范围:结束时间戳(Unix 秒) */
|
|
||||||
endTs?: number
|
|
||||||
/** 数据间隔 */
|
/** 数据间隔 */
|
||||||
interval?: string
|
interval?: string
|
||||||
/** 时间筛选 */
|
/** 时间筛选 */
|
||||||
@ -60,13 +56,11 @@ export async function getPmPriceHistoryPublic(
|
|||||||
params: GetPmPriceHistoryPublicParams,
|
params: GetPmPriceHistoryPublicParams,
|
||||||
config?: { headers?: Record<string, string> },
|
config?: { headers?: Record<string, string> },
|
||||||
): Promise<PmPriceHistoryPublicResponse> {
|
): Promise<PmPriceHistoryPublicResponse> {
|
||||||
const { market, page = 1, pageSize = 500, startTs, endTs, interval, time, createdAtRange, fidelity, keyword, order, sort, price } = params
|
const { market, page = 1, pageSize = 500, interval, time, createdAtRange, fidelity, keyword, order, sort, price } = params
|
||||||
const query = buildQuery({
|
const query = buildQuery({
|
||||||
market,
|
market,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
startTs,
|
|
||||||
endTs,
|
|
||||||
interval,
|
interval,
|
||||||
time,
|
time,
|
||||||
createdAtRange,
|
createdAtRange,
|
||||||
@ -82,34 +76,6 @@ export async function getPmPriceHistoryPublic(
|
|||||||
/** 图表单点格式 [timestamp_ms, value_0_100] */
|
/** 图表单点格式 [timestamp_ms, value_0_100] */
|
||||||
export type ChartDataPoint = [number, number]
|
export type ChartDataPoint = [number, number]
|
||||||
|
|
||||||
/**
|
|
||||||
* 分时范围对应的 Unix 秒时间戳
|
|
||||||
* 规则:endTs 始终为当前时间;1H/6H/1D/1W/1M 的 startTs 为当前时间往前对应时长;ALL 的 startTs 为事件开始时间、endTs 为当前时间
|
|
||||||
*/
|
|
||||||
export function getTimeRangeSeconds(
|
|
||||||
range: string,
|
|
||||||
eventDates?: { startDate?: string; endDate?: string },
|
|
||||||
): { startTs: number; endTs: number } | null {
|
|
||||||
const nowSec = Math.floor(Date.now() / 1000)
|
|
||||||
const H = 60 * 60
|
|
||||||
const D = 24 * H
|
|
||||||
switch (range) {
|
|
||||||
case '1H': return { startTs: nowSec - 1 * H, endTs: nowSec }
|
|
||||||
case '6H': return { startTs: nowSec - 6 * H, endTs: nowSec }
|
|
||||||
case '1D': return { startTs: nowSec - 1 * D, endTs: nowSec }
|
|
||||||
case '1W': return { startTs: nowSec - 7 * D, endTs: nowSec }
|
|
||||||
case '1M': return { startTs: nowSec - 30 * D, endTs: nowSec }
|
|
||||||
case 'ALL':
|
|
||||||
if (eventDates?.startDate) {
|
|
||||||
const startTs = Math.floor(new Date(eventDates.startDate).getTime() / 1000)
|
|
||||||
if (Number.isFinite(startTs))
|
|
||||||
return { startTs, endTs: nowSec }
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
default: return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将接口返回的 list 转为 ECharts 折线图数据
|
* 将接口返回的 list 转为 ECharts 折线图数据
|
||||||
* - time 转为毫秒时间戳
|
* - time 转为毫秒时间戳
|
||||||
|
|||||||
@ -296,7 +296,7 @@ async function checkBalance(isFirst = false) {
|
|||||||
for (const addr of contractsToCheck) {
|
for (const addr of contractsToCheck) {
|
||||||
try {
|
try {
|
||||||
const contract = new ethers.Contract(addr, ['function balanceOf(address) view returns (uint256)'], provider)
|
const contract = new ethers.Contract(addr, ['function balanceOf(address) view returns (uint256)'], provider)
|
||||||
const bal = contract.balanceOf ? await contract.balanceOf(targetAddr) : BigInt(0)
|
const bal = await contract.balanceOf(targetAddr)
|
||||||
totalBalance += bal
|
totalBalance += bal
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`Failed to check balance for token ${addr}`, err)
|
console.warn(`Failed to check balance for token ${addr}`, err)
|
||||||
|
|||||||
@ -1579,7 +1579,7 @@ async function submitMerge() {
|
|||||||
mergeError.value = ''
|
mergeError.value = ''
|
||||||
try {
|
try {
|
||||||
const res = await pmMarketMerge(
|
const res = await pmMarketMerge(
|
||||||
{ marketID: marketId, amount: (mergeAmount.value * 1000000).toFixed(0) },
|
{ marketID: marketId, amount: String(mergeAmount.value) },
|
||||||
{ headers: userStore.getAuthHeaders() },
|
{ headers: userStore.getAuthHeaders() },
|
||||||
)
|
)
|
||||||
if (res.code === 0 || res.code === 200) {
|
if (res.code === 0 || res.code === 200) {
|
||||||
@ -1614,7 +1614,7 @@ async function submitSplit() {
|
|||||||
splitError.value = ''
|
splitError.value = ''
|
||||||
try {
|
try {
|
||||||
const res = await pmMarketSplit(
|
const res = await pmMarketSplit(
|
||||||
{ marketID: marketId, usdcAmount: (splitAmount.value * 1000000).toFixed(0) },
|
{ marketID: marketId, usdcAmount: String(splitAmount.value * 1000000) },
|
||||||
{ headers: userStore.getAuthHeaders() },
|
{ headers: userStore.getAuthHeaders() },
|
||||||
)
|
)
|
||||||
if (res.code === 0 || res.code === 200) {
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
|||||||
@ -36,9 +36,6 @@
|
|||||||
<p class="chart-legend-hint">{{ markets.length }} 个市场</p>
|
<p class="chart-legend-hint">{{ markets.length }} 个市场</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper">
|
<div class="chart-wrapper">
|
||||||
<div v-if="chartLoading" class="chart-loading-overlay">
|
|
||||||
<v-progress-circular indeterminate color="primary" size="32" />
|
|
||||||
</div>
|
|
||||||
<div ref="chartContainerRef" class="chart-container"></div>
|
<div ref="chartContainerRef" class="chart-container"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-footer">
|
<div class="chart-footer">
|
||||||
@ -221,12 +218,10 @@ import TradeComponent from '../components/TradeComponent.vue'
|
|||||||
import {
|
import {
|
||||||
findPmEvent,
|
findPmEvent,
|
||||||
getMarketId,
|
getMarketId,
|
||||||
getClobTokenId,
|
|
||||||
type FindPmEventParams,
|
type FindPmEventParams,
|
||||||
type PmEventListItem,
|
type PmEventListItem,
|
||||||
type PmEventMarketItem,
|
type PmEventMarketItem,
|
||||||
} from '../api/event'
|
} from '../api/event'
|
||||||
import { getPmPriceHistoryPublic, priceHistoryToChartData, getTimeRangeSeconds } from '../api/priceHistory'
|
|
||||||
import { getMockEventById } from '../api/mockData'
|
import { getMockEventById } from '../api/mockData'
|
||||||
import { USE_MOCK_EVENT } from '../config/mock'
|
import { USE_MOCK_EVENT } from '../config/mock'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
@ -341,13 +336,13 @@ const timeRanges = [
|
|||||||
{ label: '1M', value: '1M' },
|
{ label: '1M', value: '1M' },
|
||||||
{ label: 'ALL', value: 'ALL' },
|
{ label: 'ALL', value: 'ALL' },
|
||||||
]
|
]
|
||||||
const selectedTimeRange = ref('1D')
|
const selectedTimeRange = ref('ALL')
|
||||||
const chartContainerRef = ref<HTMLElement | null>(null)
|
const chartContainerRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
type ChartSeriesItem = { name: string; data: [number, number][] }
|
type ChartSeriesItem = { name: string; data: [number, number][] }
|
||||||
const chartData = ref<ChartSeriesItem[]>([])
|
const chartData = ref<ChartSeriesItem[]>([])
|
||||||
const chartLoading = ref(false)
|
|
||||||
let chartInstance: ECharts | null = null
|
let chartInstance: ECharts | null = null
|
||||||
|
let dynamicInterval: number | undefined
|
||||||
|
|
||||||
const LINE_COLORS = [
|
const LINE_COLORS = [
|
||||||
'#2563eb',
|
'#2563eb',
|
||||||
@ -361,38 +356,51 @@ const LINE_COLORS = [
|
|||||||
]
|
]
|
||||||
const MOBILE_BREAKPOINT = 600
|
const MOBILE_BREAKPOINT = 600
|
||||||
|
|
||||||
/** 按市场依次请求 getPmPriceHistoryPublic,market 传 clobTokenIds[0](YES token) */
|
function getStepAndCount(range: string): { stepMs: number; count: number } {
|
||||||
async function loadChartFromApi(): Promise<ChartSeriesItem[]> {
|
switch (range) {
|
||||||
const list = markets.value
|
case '1H':
|
||||||
|
return { stepMs: 60 * 1000, count: 60 }
|
||||||
|
case '6H':
|
||||||
|
return { stepMs: 10 * 60 * 1000, count: 36 }
|
||||||
|
case '1D':
|
||||||
|
return { stepMs: 60 * 60 * 1000, count: 24 }
|
||||||
|
case '1W':
|
||||||
|
return { stepMs: 24 * 60 * 60 * 1000, count: 7 }
|
||||||
|
case '1M':
|
||||||
|
case 'ALL':
|
||||||
|
return { stepMs: 24 * 60 * 60 * 1000, count: 30 }
|
||||||
|
default:
|
||||||
|
return { stepMs: 60 * 60 * 1000, count: 24 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDataForMarket(baseChance: number, range: string): [number, number][] {
|
||||||
|
const now = Date.now()
|
||||||
|
const data: [number, number][] = []
|
||||||
|
const { stepMs, count } = getStepAndCount(range)
|
||||||
|
let value = baseChance + (Math.random() - 0.5) * 10
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateAllData(): ChartSeriesItem[] {
|
||||||
const range = selectedTimeRange.value
|
const range = selectedTimeRange.value
|
||||||
const results: ChartSeriesItem[] = []
|
const list = markets.value
|
||||||
for (let i = 0; i < list.length; i++) {
|
return list.map((market, i) => {
|
||||||
const market = list[i]
|
const chance = marketChance(market)
|
||||||
if (!market) continue
|
|
||||||
const yesTokenId = getClobTokenId(market, 0)
|
|
||||||
const base = (market.question || 'Market').slice(0, 32)
|
const base = (market.question || 'Market').slice(0, 32)
|
||||||
const baseName = base + (base.length >= 32 ? '…' : '')
|
const baseName = base + (base.length >= 32 ? '…' : '')
|
||||||
|
// 确保图例 name 唯一,否则 ECharts 会合并导致左右切换箭头不显示
|
||||||
const name = list.length > 1 ? `${baseName} (${i + 1}/${list.length})` : baseName
|
const name = list.length > 1 ? `${baseName} (${i + 1}/${list.length})` : baseName
|
||||||
if (!yesTokenId) {
|
return {
|
||||||
results.push({ name, data: [] })
|
name,
|
||||||
continue
|
data: generateDataForMarket(chance || 20, range),
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const ev = eventDetail.value
|
|
||||||
const timeRange = getTimeRangeSeconds(range, ev ? { startDate: ev.startDate, endDate: ev.endDate } : undefined)
|
|
||||||
const res = await getPmPriceHistoryPublic({
|
|
||||||
market: yesTokenId,
|
|
||||||
page: 1,
|
|
||||||
pageSize: 500,
|
|
||||||
...(timeRange && { startTs: timeRange.startTs, endTs: timeRange.endTs }),
|
|
||||||
})
|
})
|
||||||
const points = priceHistoryToChartData(res.data?.list ?? [])
|
|
||||||
results.push({ name, data: points })
|
|
||||||
} catch {
|
|
||||||
results.push({ name, data: [] })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOption(seriesArr: ChartSeriesItem[], containerWidth?: number) {
|
function buildOption(seriesArr: ChartSeriesItem[], containerWidth?: number) {
|
||||||
@ -489,29 +497,28 @@ function buildOption(seriesArr: ChartSeriesItem[], containerWidth?: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initChart() {
|
function initChart() {
|
||||||
if (!chartContainerRef.value || markets.value.length === 0) return
|
if (!chartContainerRef.value || markets.value.length === 0) return
|
||||||
chartLoading.value = true
|
chartData.value = generateAllData()
|
||||||
try {
|
|
||||||
chartData.value = await loadChartFromApi()
|
|
||||||
chartInstance = echarts.init(chartContainerRef.value)
|
chartInstance = echarts.init(chartContainerRef.value)
|
||||||
const w = chartContainerRef.value.clientWidth
|
const w = chartContainerRef.value.clientWidth
|
||||||
chartInstance.setOption(buildOption(chartData.value, w))
|
chartInstance.setOption(buildOption(chartData.value, w))
|
||||||
} finally {
|
|
||||||
chartLoading.value = false
|
// 数据打印,便于分析
|
||||||
}
|
console.log('[EventMarkets] 数据分析:', {
|
||||||
|
marketsCount: markets.value.length,
|
||||||
|
chartSeriesCount: chartData.value.length,
|
||||||
|
containerWidth: w,
|
||||||
|
legendData: chartData.value.map((s) => ({ name: s.name, nameLen: s.name.length, points: s.data.length })),
|
||||||
|
eventDetail: eventDetail.value ? { title: eventDetail.value.title, id: eventDetail.value.ID } : null,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateChartData() {
|
function updateChartData() {
|
||||||
chartLoading.value = true
|
chartData.value = generateAllData()
|
||||||
try {
|
|
||||||
chartData.value = await loadChartFromApi()
|
|
||||||
const w = chartContainerRef.value?.clientWidth
|
const w = chartContainerRef.value?.clientWidth
|
||||||
if (chartInstance)
|
if (chartInstance)
|
||||||
chartInstance.setOption(buildOption(chartData.value, w), { replaceMerge: ['series'] })
|
chartInstance.setOption(buildOption(chartData.value, w), { replaceMerge: ['series'] })
|
||||||
} finally {
|
|
||||||
chartLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectTimeRange(range: string) {
|
function selectTimeRange(range: string) {
|
||||||
@ -519,6 +526,35 @@ function selectTimeRange(range: string) {
|
|||||||
updateChartData()
|
updateChartData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMaxPoints(range: string): number {
|
||||||
|
return getStepAndCount(range).count + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDynamicUpdate() {
|
||||||
|
dynamicInterval = window.setInterval(() => {
|
||||||
|
const nextData = chartData.value.map((s) => {
|
||||||
|
const list = [...s.data]
|
||||||
|
const last = list[list.length - 1]
|
||||||
|
if (!last) return s
|
||||||
|
const nextVal = Math.max(10, Math.min(90, last[1] + (Math.random() - 0.5) * 4))
|
||||||
|
list.push([Date.now(), Math.round(nextVal * 10) / 10])
|
||||||
|
const max = getMaxPoints(selectedTimeRange.value)
|
||||||
|
return { name: s.name, data: list.slice(-max) }
|
||||||
|
})
|
||||||
|
chartData.value = nextData
|
||||||
|
const w = chartContainerRef.value?.clientWidth
|
||||||
|
if (chartInstance)
|
||||||
|
chartInstance.setOption(buildOption(chartData.value, w), { replaceMerge: ['series'] })
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDynamicUpdate() {
|
||||||
|
if (dynamicInterval) {
|
||||||
|
clearInterval(dynamicInterval)
|
||||||
|
dynamicInterval = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (!chartInstance || !chartContainerRef.value) return
|
if (!chartInstance || !chartContainerRef.value) return
|
||||||
chartInstance.resize()
|
chartInstance.resize()
|
||||||
@ -716,6 +752,7 @@ watch(tradeSheetOpen, (open) => {
|
|||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
stopDynamicUpdate()
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
chartInstance?.dispose()
|
chartInstance?.dispose()
|
||||||
chartInstance = null
|
chartInstance = null
|
||||||
@ -727,7 +764,10 @@ watch(
|
|||||||
() => markets.value.length,
|
() => markets.value.length,
|
||||||
(len) => {
|
(len) => {
|
||||||
if (len > 0) {
|
if (len > 0) {
|
||||||
nextTick(() => initChart())
|
nextTick(() => {
|
||||||
|
initChart()
|
||||||
|
if (dynamicInterval == null) startDynamicUpdate()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -875,21 +915,10 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chart-wrapper {
|
.chart-wrapper {
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-loading-overlay {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 320px;
|
height: 320px;
|
||||||
|
|||||||
@ -401,7 +401,6 @@ import { cancelOrder as apiCancelOrder } from '../api/order'
|
|||||||
import type { ChartDataPoint, ChartTimeRange } from '../api/chart'
|
import type { ChartDataPoint, ChartTimeRange } from '../api/chart'
|
||||||
import {
|
import {
|
||||||
getPmPriceHistoryPublic,
|
getPmPriceHistoryPublic,
|
||||||
getTimeRangeSeconds,
|
|
||||||
priceHistoryToChartData,
|
priceHistoryToChartData,
|
||||||
} from '../api/priceHistory'
|
} from '../api/priceHistory'
|
||||||
import {
|
import {
|
||||||
@ -1232,7 +1231,7 @@ function formatTimeAgo(ts: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 时间粒度
|
// 时间粒度
|
||||||
const selectedTimeRange = ref('1D')
|
const selectedTimeRange = ref('ALL')
|
||||||
const timeRanges = [
|
const timeRanges = [
|
||||||
{ label: '1H', value: '1H' },
|
{ label: '1H', value: '1H' },
|
||||||
{ label: '6H', value: '6H' },
|
{ label: '6H', value: '6H' },
|
||||||
@ -1250,6 +1249,40 @@ const cryptoChartLoading = ref(false)
|
|||||||
const chartYesNoLoading = ref(false)
|
const chartYesNoLoading = ref(false)
|
||||||
let chartInstance: ECharts | null = null
|
let chartInstance: ECharts | null = null
|
||||||
|
|
||||||
|
/** 分时范围对应的毫秒数,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 currentChance = computed(() => {
|
||||||
const ev = eventDetail.value
|
const ev = eventDetail.value
|
||||||
const market = ev?.markets?.[0]
|
const market = ev?.markets?.[0]
|
||||||
@ -1495,14 +1528,11 @@ function initChart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 从 GET /pmPriceHistory/getPmPriceHistoryPublic 拉取价格历史,market 传 YES 对应的 clobTokenId */
|
/** 从 GET /pmPriceHistory/getPmPriceHistoryPublic 拉取价格历史,market 传 YES 对应的 clobTokenId */
|
||||||
async function loadChartFromApi(marketParam: string, range: string): Promise<ChartDataPoint[]> {
|
async function loadChartFromApi(marketParam: string): Promise<ChartDataPoint[]> {
|
||||||
const ev = eventDetail.value
|
|
||||||
const timeRange = getTimeRangeSeconds(range, ev ? { startDate: ev.startDate, endDate: ev.endDate } : undefined)
|
|
||||||
const res = await getPmPriceHistoryPublic({
|
const res = await getPmPriceHistoryPublic({
|
||||||
market: marketParam,
|
market: marketParam,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 500,
|
pageSize: 500,
|
||||||
...(timeRange && { startTs: timeRange.startTs, endTs: timeRange.endTs }),
|
|
||||||
})
|
})
|
||||||
const list = res.data?.list ?? []
|
const list = res.data?.list ?? []
|
||||||
return priceHistoryToChartData(list)
|
return priceHistoryToChartData(list)
|
||||||
@ -1562,9 +1592,9 @@ async function updateChartData() {
|
|||||||
try {
|
try {
|
||||||
// 价格历史接口的 market 传 clobTokenIds[0](YES 对应 token ID)
|
// 价格历史接口的 market 传 clobTokenIds[0](YES 对应 token ID)
|
||||||
const yesTokenId = clobTokenIds.value[0]
|
const yesTokenId = clobTokenIds.value[0]
|
||||||
const points = yesTokenId ? await loadChartFromApi(yesTokenId, selectedTimeRange.value) : []
|
const points = yesTokenId ? await loadChartFromApi(yesTokenId) : []
|
||||||
rawChartData.value = points
|
rawChartData.value = points
|
||||||
data.value = points
|
data.value = filterChartDataByRange(points, selectedTimeRange.value)
|
||||||
if (chartInstance)
|
if (chartInstance)
|
||||||
chartInstance.setOption(buildOption(data.value, w), { replaceMerge: ['series'] })
|
chartInstance.setOption(buildOption(data.value, w), { replaceMerge: ['series'] })
|
||||||
} finally {
|
} finally {
|
||||||
@ -1577,7 +1607,16 @@ function selectTimeRange(range: string) {
|
|||||||
selectedTimeRange.value = range
|
selectedTimeRange.value = range
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(selectedTimeRange, () => updateChartData())
|
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'] })
|
||||||
|
} else {
|
||||||
|
updateChartData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// CLOB:当有 market 且存在 clobTokenIds 时连接(使用 Yes/No token ID)
|
// CLOB:当有 market 且存在 clobTokenIds 时连接(使用 Yes/No token ID)
|
||||||
const clobTokenIds = computed(() => {
|
const clobTokenIds = computed(() => {
|
||||||
|
|||||||
@ -471,8 +471,7 @@
|
|||||||
>
|
>
|
||||||
<div class="history-mobile-row">
|
<div class="history-mobile-row">
|
||||||
<div class="history-mobile-icon" :class="h.iconClass">
|
<div class="history-mobile-icon" :class="h.iconClass">
|
||||||
<img v-if="h.imageUrl" :src="h.imageUrl" alt="" class="position-icon-img" />
|
<span class="position-icon-char">{{ h.iconChar }}</span>
|
||||||
<span v-else class="position-icon-char">{{ h.iconChar || '•' }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="history-mobile-main">
|
<div class="history-mobile-main">
|
||||||
<div class="history-mobile-title">{{ h.market }}</div>
|
<div class="history-mobile-title">{{ h.market }}</div>
|
||||||
@ -921,8 +920,6 @@ interface HistoryItem {
|
|||||||
shares?: string
|
shares?: string
|
||||||
iconChar?: string
|
iconChar?: string
|
||||||
iconClass?: string
|
iconClass?: string
|
||||||
/** 图标 URL(来自 record.icon) */
|
|
||||||
imageUrl?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const positions = ref<Position[]>(
|
const positions = ref<Position[]>(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user