优化:加密货币图表显示规则
This commit is contained in:
parent
1542790e4a
commit
9ec66ff163
@ -7,6 +7,17 @@
|
||||
/** 图表数据点:[时间戳(ms), 价格(USD)] */
|
||||
export type CryptoChartPoint = [number, number]
|
||||
|
||||
/** 折线图 / 行情区小数位数上限 */
|
||||
const CRYPTO_CHART_PRICE_MAX_DECIMALS = 10
|
||||
|
||||
/**
|
||||
* 加密货币价格展示:最多 {@link CRYPTO_CHART_PRICE_MAX_DECIMALS} 位小数,并去掉小数末尾多余的 0(如 1.20000000 → 1.2)
|
||||
*/
|
||||
export function formatCryptoChartPrice(price: number): string {
|
||||
if (!Number.isFinite(price)) return '—'
|
||||
return price.toFixed(CRYPTO_CHART_PRICE_MAX_DECIMALS).replace(/\.?0+$/, '')
|
||||
}
|
||||
|
||||
/** Binance 交易对映射(symbol -> BTCUSDT 等) */
|
||||
export const BINANCE_SYMBOLS: Record<string, string> = {
|
||||
btc: 'BTCUSDT',
|
||||
@ -125,9 +136,20 @@ const RANGE_TO_LIMIT: Record<string, number> = {
|
||||
ALL: 1000,
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Polymarket 风格 ticker 解析币种:取首个 "-" 段,且须在 {@link COINGECKO_COIN_IDS} 中有映射。
|
||||
* 例:`bnb-updown-15m-1775056500` → `bnb`
|
||||
*/
|
||||
export function parseCryptoSymbolFromTicker(ticker: string | undefined | null): string | null {
|
||||
if (ticker == null || typeof ticker !== 'string') return null
|
||||
const first = ticker.split('-')[0]?.trim().toLowerCase() ?? ''
|
||||
if (!first || !COINGECKO_COIN_IDS[first]) return null
|
||||
return first
|
||||
}
|
||||
|
||||
/**
|
||||
* 从事件信息推断加密货币符号
|
||||
* 优先:ticker -> tags.slug -> series.slug -> 市场 question 中的币种名
|
||||
* 优先:ticker 首段(见 {@link parseCryptoSymbolFromTicker})-> 整段 ticker -> tags.slug -> series -> question
|
||||
*/
|
||||
export function inferCryptoSymbol(event: {
|
||||
ticker?: string
|
||||
@ -135,6 +157,9 @@ export function inferCryptoSymbol(event: {
|
||||
series?: { slug?: string; ticker?: string }[]
|
||||
markets?: { question?: string }[]
|
||||
}): string | null {
|
||||
const fromTickerSeg = parseCryptoSymbolFromTicker(event.ticker)
|
||||
if (fromTickerSeg) return fromTickerSeg
|
||||
|
||||
const t = (event.ticker ?? '').toLowerCase().trim()
|
||||
if (t && COINGECKO_COIN_IDS[t]) return t
|
||||
|
||||
|
||||
@ -27,8 +27,9 @@
|
||||
</h1>
|
||||
<p v-if="detailError" class="chart-error">{{ detailError }}</p>
|
||||
<div class="chart-controls-row">
|
||||
<v-btn class="date-pill" size="small" rounded="pill">{{ resolutionDate }}</v-btn>
|
||||
<v-btn-group
|
||||
v-if="isCryptoEvent"
|
||||
v-if="cryptoSymbol"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
divided
|
||||
@ -46,15 +47,13 @@
|
||||
<v-btn
|
||||
:class="{ active: chartMode === 'crypto' }"
|
||||
size="small"
|
||||
icon
|
||||
class="chart-mode-crypto-btn"
|
||||
:aria-label="t('chart.cryptoPrice')"
|
||||
@click="setChartMode('crypto')"
|
||||
>
|
||||
<v-icon size="20">mdi-currency-btc</v-icon>
|
||||
<span class="chart-crypto-ticker-label">{{ cryptoSymbol.toUpperCase() }}</span>
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
<v-btn variant="text" size="small" class="past-btn">Past ▾</v-btn>
|
||||
<v-btn class="date-pill" size="small" rounded="pill">{{ resolutionDate }}</v-btn>
|
||||
</div>
|
||||
<div class="chart-chance">
|
||||
<template v-if="chartMode === 'crypto' && cryptoSymbol">
|
||||
@ -369,6 +368,7 @@ import {
|
||||
LineType,
|
||||
LastPriceAnimationMode,
|
||||
TickMarkType,
|
||||
type BarPrice,
|
||||
type IChartApi,
|
||||
type ISeriesApi,
|
||||
type Time,
|
||||
@ -408,8 +408,8 @@ import {
|
||||
priceHistoryToChartData,
|
||||
} from '../api/priceHistory'
|
||||
import {
|
||||
isCryptoEvent as checkIsCryptoEvent,
|
||||
inferCryptoSymbol,
|
||||
parseCryptoSymbolFromTicker,
|
||||
formatCryptoChartPrice,
|
||||
fetchCryptoChart,
|
||||
subscribeCryptoRealtime,
|
||||
alignCryptoChartTimeMs,
|
||||
@ -559,10 +559,16 @@ const isResolutionSourceUrl = computed(() => {
|
||||
})
|
||||
|
||||
/** 是否为加密货币类型事件(可切换 YES/NO 分时 vs 加密货币价格图) */
|
||||
const isCryptoEvent = computed(() => checkIsCryptoEvent(eventDetail.value))
|
||||
/** 从 event.ticker 首段解析的加密货币符号(如 bnb-updown-… → bnb);无映射则不展示切换器 */
|
||||
const cryptoSymbol = computed(() => parseCryptoSymbolFromTicker(eventDetail.value?.ticker))
|
||||
|
||||
/** 从事件推断的加密货币符号,如 btc、eth */
|
||||
const cryptoSymbol = computed(() => inferCryptoSymbol(eventDetail.value ?? {}) ?? null)
|
||||
watch(cryptoSymbol, (sym) => {
|
||||
if (!sym && chartMode.value === 'crypto') {
|
||||
chartMode.value = 'yesno'
|
||||
if (selectedTimeRange.value === '30S') selectedTimeRange.value = '1D'
|
||||
void updateChartData()
|
||||
}
|
||||
})
|
||||
|
||||
/** 图表模式:yesno=YES/NO 分时,crypto=加密货币价格 */
|
||||
const chartMode = ref<'yesno' | 'crypto'>('yesno')
|
||||
@ -572,10 +578,7 @@ const currentCryptoPrice = computed(() => {
|
||||
const d = data.value
|
||||
const last = d.length > 0 ? d[d.length - 1] : undefined
|
||||
if (last != null && chartMode.value === 'crypto') {
|
||||
const p = last[1]
|
||||
return Number.isFinite(p)
|
||||
? p.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
: '—'
|
||||
return formatCryptoChartPrice(last[1])
|
||||
}
|
||||
return '—'
|
||||
})
|
||||
@ -1246,7 +1249,13 @@ function ensureChartSeries() {
|
||||
lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate,
|
||||
crosshairMarkerVisible: true,
|
||||
lastValueVisible: true,
|
||||
priceFormat: isCrypto ? { type: 'price', precision: 2 } : { type: 'percent', precision: 1 },
|
||||
priceFormat: isCrypto
|
||||
? {
|
||||
type: 'custom',
|
||||
minMove: 1e-10,
|
||||
formatter: (priceValue: BarPrice) => formatCryptoChartPrice(priceValue as number),
|
||||
}
|
||||
: { type: 'percent', precision: 1 },
|
||||
priceScaleId: CHART_OVERLAY_PRICE_SCALE_ID,
|
||||
})
|
||||
chartInstance.priceScale(CHART_OVERLAY_PRICE_SCALE_ID).applyOptions({
|
||||
@ -1437,44 +1446,48 @@ function applyCryptoRealtimePoint(point: [number, number]) {
|
||||
|
||||
async function updateChartData() {
|
||||
if (chartMode.value === 'crypto') {
|
||||
const sym = cryptoSymbol.value ?? 'btc'
|
||||
const gen = ++cryptoLoadGeneration
|
||||
cryptoWsUnsubscribe?.()
|
||||
cryptoWsUnsubscribe = null
|
||||
cryptoChartLoading.value = true
|
||||
try {
|
||||
const res = await fetchCryptoChart({
|
||||
symbol: sym,
|
||||
range: selectedTimeRange.value,
|
||||
})
|
||||
if (gen !== cryptoLoadGeneration || chartMode.value !== 'crypto') {
|
||||
return
|
||||
}
|
||||
data.value = (res.data ?? []) as [number, number][]
|
||||
ensureChartSeries()
|
||||
setChartData(data.value)
|
||||
nextTick(() => scheduleCryptoScrollToRealtime())
|
||||
cryptoWsUnsubscribe = subscribeCryptoRealtime(sym, applyCryptoRealtimePoint)
|
||||
} finally {
|
||||
if (gen === cryptoLoadGeneration) {
|
||||
cryptoChartLoading.value = false
|
||||
const sym = cryptoSymbol.value
|
||||
if (sym) {
|
||||
const gen = ++cryptoLoadGeneration
|
||||
cryptoWsUnsubscribe?.()
|
||||
cryptoWsUnsubscribe = null
|
||||
cryptoChartLoading.value = true
|
||||
try {
|
||||
const res = await fetchCryptoChart({
|
||||
symbol: sym,
|
||||
range: selectedTimeRange.value,
|
||||
})
|
||||
if (gen !== cryptoLoadGeneration || chartMode.value !== 'crypto') {
|
||||
return
|
||||
}
|
||||
data.value = (res.data ?? []) as [number, number][]
|
||||
ensureChartSeries()
|
||||
setChartData(data.value)
|
||||
nextTick(() => scheduleCryptoScrollToRealtime())
|
||||
cryptoWsUnsubscribe = subscribeCryptoRealtime(sym, applyCryptoRealtimePoint)
|
||||
} finally {
|
||||
if (gen === cryptoLoadGeneration) {
|
||||
cryptoChartLoading.value = false
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
cryptoLoadGeneration++
|
||||
cryptoWsUnsubscribe?.()
|
||||
cryptoWsUnsubscribe = null
|
||||
chartYesNoLoading.value = true
|
||||
try {
|
||||
const yesTokenId = clobTokenIds.value[0]
|
||||
const points = yesTokenId ? await loadChartFromApi(yesTokenId, selectedTimeRange.value) : []
|
||||
rawChartData.value = points
|
||||
data.value = points
|
||||
ensureChartSeries()
|
||||
setChartData(data.value)
|
||||
} finally {
|
||||
chartYesNoLoading.value = false
|
||||
}
|
||||
chartMode.value = 'yesno'
|
||||
if (selectedTimeRange.value === '30S') selectedTimeRange.value = '1D'
|
||||
}
|
||||
cryptoLoadGeneration++
|
||||
cryptoWsUnsubscribe?.()
|
||||
cryptoWsUnsubscribe = null
|
||||
chartYesNoLoading.value = true
|
||||
try {
|
||||
const yesTokenId = clobTokenIds.value[0]
|
||||
const points = yesTokenId ? await loadChartFromApi(yesTokenId, selectedTimeRange.value) : []
|
||||
rawChartData.value = points
|
||||
data.value = points
|
||||
ensureChartSeries()
|
||||
setChartData(data.value)
|
||||
} finally {
|
||||
chartYesNoLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -1798,12 +1811,6 @@ onUnmounted(() => {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.past-btn {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.date-pill {
|
||||
background-color: #111827 !important;
|
||||
color: #fff !important;
|
||||
@ -1834,11 +1841,28 @@ onUnmounted(() => {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.chart-mode-toggle {
|
||||
height: 26px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.chart-mode-toggle .v-btn.active {
|
||||
background: rgb(var(--v-theme-primary));
|
||||
color: rgb(var(--v-theme-on-primary));
|
||||
}
|
||||
|
||||
.chart-mode-crypto-btn {
|
||||
min-width: 44px;
|
||||
padding: 0 10px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.chart-crypto-ticker-label {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user