优化:历史数据更新,折线图接口对接更新(参数变更)

This commit is contained in:
ivan 2026-03-16 15:29:28 +08:00
parent fa5e0dccfe
commit acd3e41f36
10 changed files with 168 additions and 163 deletions

View File

@ -59,17 +59,16 @@
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
|------|------|------| |------|------|------|
| ID | number | 主键 | | ID | number | 主键 |
| title | string | 标题 | | title | string | 标题(如「充值资金」) |
| name | string | 名称 | | type | string | 类型(如 recharge |
| eventSlug | string | 事件标识 | | **usdcSize** | number | **金额USDC**,用于充值等 |
| outcome | string | 结果Yes/No 等) | | **icon** | string | **图标路径**(如 uploads/file/btc.png会转为完整 URL 展示 |
| side | string | 方向 | | **UpdatedAt** | string | **更新时间**,用于 timeAgo 展示 |
| type | string | 类型 |
| price | number | 价格 | | price | number | 价格 |
| size | number | 大小 | | size | number | 大小 |
| createdAt | string | 创建时间 | | outcome | string | 结果 |
| timestamp | number | 时间戳(秒) | | timestamp | number | 时间戳(秒或毫秒 |
| 其他 | - | asset, bio, conditionId, icon, slug, transactionHash 等 | | 其他 | - | asset, bio, conditionId, slug, transactionHash 等 |
## 使用方式 ## 使用方式

View File

@ -9,6 +9,7 @@
## 核心能力 ## 核心能力
- `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
@ -20,6 +21,8 @@
| 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[] | 否 | 创建时间范围 |
@ -52,18 +55,22 @@
import { import {
getPmPriceHistoryPublic, getPmPriceHistoryPublic,
priceHistoryToChartData, priceHistoryToChartData,
getTimeRangeSeconds,
type PmPriceHistoryItem, type PmPriceHistoryItem,
} from '@/api/priceHistory' } from '@/api/priceHistory'
const timeRange = getTimeRangeSeconds('1D') // { startTs, endTs }
// ALL 时传 eventDatesgetTimeRangeSeconds('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 ?? [])
``` ```
## 扩展方式 ## 扩展方式
- 按时间范围1H/6H/1D 等)传 `interval``createdAtRange` 需与后端约定取值 - 时间戳规则:`endTs` 始终为当前时间1H/6H/1D/1W/1M 的 `startTs` 为当前时间往前对应时长ALL 的 `startTs` 为事件 `startDate``endTs` 为当前时间
- 若后端返回的 `price` 固定为 0100`priceHistoryToChartData` 已兼容≤1 时乘 100 - 若后端返回的 `price` 固定为 0100`priceHistoryToChartData` 已兼容≤1 时乘 100

View File

@ -7,12 +7,17 @@
事件下的市场列表页,展示某个 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 复用

View File

@ -4,6 +4,7 @@
*/ */
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"] 对齐) */
@ -13,6 +14,7 @@ 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
@ -31,6 +33,9 @@ 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 请求参数 */
@ -129,11 +134,15 @@ export interface HistoryDisplayItem {
shares?: string shares?: string
iconChar?: string iconChar?: string
iconClass?: string iconClass?: string
/** 图标 URL来自 record.icon用于展示 */
imageUrl?: string
} }
function formatTimeAgo(createdAt: string | undefined, timestamp?: number): string { function formatTimeAgo(dateStr: string | undefined, timestamp?: number): string {
const ms = createdAt ? new Date(createdAt).getTime() : (timestamp != null ? timestamp * 1000 : 0) let ms = 0
if (!ms) return '' if (dateStr) ms = new Date(dateStr).getTime()
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`
@ -142,8 +151,18 @@ function formatTimeAgo(createdAt: string | undefined, timestamp?: number): strin
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 ?? '')
@ -152,13 +171,18 @@ 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 = price * size const valueUsd = usdcSize !== 0 ? usdcSize : 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(record.createdAt, record.timestamp) const timeAgo = formatTimeAgo(
record.UpdatedAt ?? record.updatedAt ?? record.CreatedAt ?? record.createdAt,
record.timestamp,
)
const imageUrl = toFullIconUrl(record.icon)
return { return {
id, id,
market, market,
@ -171,6 +195,7 @@ 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,
} }
} }

View File

@ -28,6 +28,10 @@ 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
/** 时间筛选 */ /** 时间筛选 */
@ -56,11 +60,13 @@ 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, interval, time, createdAtRange, fidelity, keyword, order, sort, price } = params const { market, page = 1, pageSize = 500, startTs, endTs, 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,
@ -76,6 +82,34 @@ 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

View File

@ -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 = await contract.balanceOf(targetAddr) const bal = contract.balanceOf ? await contract.balanceOf(targetAddr) : BigInt(0)
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)

View File

@ -1579,7 +1579,7 @@ async function submitMerge() {
mergeError.value = '' mergeError.value = ''
try { try {
const res = await pmMarketMerge( const res = await pmMarketMerge(
{ marketID: marketId, amount: String(mergeAmount.value) }, { marketID: marketId, amount: (mergeAmount.value * 1000000).toFixed(0) },
{ 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: String(splitAmount.value * 1000000) }, { marketID: marketId, usdcAmount: (splitAmount.value * 1000000).toFixed(0) },
{ headers: userStore.getAuthHeaders() }, { headers: userStore.getAuthHeaders() },
) )
if (res.code === 0 || res.code === 200) { if (res.code === 0 || res.code === 200) {

View File

@ -36,6 +36,9 @@
<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">
@ -218,10 +221,12 @@ 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'
@ -336,13 +341,13 @@ const timeRanges = [
{ label: '1M', value: '1M' }, { label: '1M', value: '1M' },
{ label: 'ALL', value: 'ALL' }, { label: 'ALL', value: 'ALL' },
] ]
const selectedTimeRange = ref('ALL') const selectedTimeRange = ref('1D')
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',
@ -356,51 +361,38 @@ const LINE_COLORS = [
] ]
const MOBILE_BREAKPOINT = 600 const MOBILE_BREAKPOINT = 600
function getStepAndCount(range: string): { stepMs: number; count: number } { /** 按市场依次请求 getPmPriceHistoryPublicmarket 传 clobTokenIds[0]YES token */
switch (range) { async function loadChartFromApi(): Promise<ChartSeriesItem[]> {
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 list = markets.value const list = markets.value
return list.map((market, i) => { const range = selectedTimeRange.value
const chance = marketChance(market) const results: ChartSeriesItem[] = []
for (let i = 0; i < list.length; i++) {
const market = list[i]
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
return { if (!yesTokenId) {
name, results.push({ name, data: [] })
data: generateDataForMarket(chance || 20, range), continue
} }
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) {
@ -497,28 +489,29 @@ function buildOption(seriesArr: ChartSeriesItem[], containerWidth?: number) {
} }
} }
function initChart() { async function initChart() {
if (!chartContainerRef.value || markets.value.length === 0) return if (!chartContainerRef.value || markets.value.length === 0) return
chartData.value = generateAllData() chartLoading.value = true
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,
})
} }
function updateChartData() { async function updateChartData() {
chartData.value = generateAllData() chartLoading.value = true
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) {
@ -526,35 +519,6 @@ 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()
@ -752,7 +716,6 @@ 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
@ -764,10 +727,7 @@ watch(
() => markets.value.length, () => markets.value.length,
(len) => { (len) => {
if (len > 0) { if (len > 0) {
nextTick(() => { nextTick(() => initChart())
initChart()
if (dynamicInterval == null) startDynamicUpdate()
})
} }
}, },
) )
@ -915,10 +875,21 @@ 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;

View File

@ -401,6 +401,7 @@ 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 {
@ -1231,7 +1232,7 @@ function formatTimeAgo(ts: number): string {
} }
// //
const selectedTimeRange = ref('ALL') const selectedTimeRange = ref('1D')
const timeRanges = [ const timeRanges = [
{ label: '1H', value: '1H' }, { label: '1H', value: '1H' },
{ label: '6H', value: '6H' }, { label: '6H', value: '6H' },
@ -1249,40 +1250,6 @@ 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]
@ -1528,11 +1495,14 @@ function initChart() {
} }
/** 从 GET /pmPriceHistory/getPmPriceHistoryPublic 拉取价格历史market 传 YES 对应的 clobTokenId */ /** 从 GET /pmPriceHistory/getPmPriceHistoryPublic 拉取价格历史market 传 YES 对应的 clobTokenId */
async function loadChartFromApi(marketParam: string): Promise<ChartDataPoint[]> { async function loadChartFromApi(marketParam: string, range: 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)
@ -1592,9 +1562,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) : [] const points = yesTokenId ? await loadChartFromApi(yesTokenId, selectedTimeRange.value) : []
rawChartData.value = points rawChartData.value = points
data.value = filterChartDataByRange(points, selectedTimeRange.value) data.value = points
if (chartInstance) if (chartInstance)
chartInstance.setOption(buildOption(data.value, w), { replaceMerge: ['series'] }) chartInstance.setOption(buildOption(data.value, w), { replaceMerge: ['series'] })
} finally { } finally {
@ -1607,16 +1577,7 @@ function selectTimeRange(range: string) {
selectedTimeRange.value = range selectedTimeRange.value = range
} }
watch(selectedTimeRange, (range) => { watch(selectedTimeRange, () => updateChartData())
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(() => {

View File

@ -471,7 +471,8 @@
> >
<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">
<span class="position-icon-char">{{ h.iconChar }}</span> <img v-if="h.imageUrl" :src="h.imageUrl" alt="" class="position-icon-img" />
<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>
@ -920,6 +921,8 @@ 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[]>(