优化:加密货币图表显示规则

This commit is contained in:
ivan 2026-04-01 22:43:05 +08:00
parent 1542790e4a
commit 9ec66ff163
2 changed files with 107 additions and 58 deletions

View File

@ -7,6 +7,17 @@
/** 图表数据点:[时间戳(ms), 价格(USD)] */ /** 图表数据点:[时间戳(ms), 价格(USD)] */
export type CryptoChartPoint = [number, number] 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 等) */ /** Binance 交易对映射symbol -> BTCUSDT 等) */
export const BINANCE_SYMBOLS: Record<string, string> = { export const BINANCE_SYMBOLS: Record<string, string> = {
btc: 'BTCUSDT', btc: 'BTCUSDT',
@ -125,9 +136,20 @@ const RANGE_TO_LIMIT: Record<string, number> = {
ALL: 1000, 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: { export function inferCryptoSymbol(event: {
ticker?: string ticker?: string
@ -135,6 +157,9 @@ export function inferCryptoSymbol(event: {
series?: { slug?: string; ticker?: string }[] series?: { slug?: string; ticker?: string }[]
markets?: { question?: string }[] markets?: { question?: string }[]
}): string | null { }): string | null {
const fromTickerSeg = parseCryptoSymbolFromTicker(event.ticker)
if (fromTickerSeg) return fromTickerSeg
const t = (event.ticker ?? '').toLowerCase().trim() const t = (event.ticker ?? '').toLowerCase().trim()
if (t && COINGECKO_COIN_IDS[t]) return t if (t && COINGECKO_COIN_IDS[t]) return t

View File

@ -27,8 +27,9 @@
</h1> </h1>
<p v-if="detailError" class="chart-error">{{ detailError }}</p> <p v-if="detailError" class="chart-error">{{ detailError }}</p>
<div class="chart-controls-row"> <div class="chart-controls-row">
<v-btn class="date-pill" size="small" rounded="pill">{{ resolutionDate }}</v-btn>
<v-btn-group <v-btn-group
v-if="isCryptoEvent" v-if="cryptoSymbol"
variant="outlined" variant="outlined"
density="compact" density="compact"
divided divided
@ -46,15 +47,13 @@
<v-btn <v-btn
:class="{ active: chartMode === 'crypto' }" :class="{ active: chartMode === 'crypto' }"
size="small" size="small"
icon class="chart-mode-crypto-btn"
:aria-label="t('chart.cryptoPrice')" :aria-label="t('chart.cryptoPrice')"
@click="setChartMode('crypto')" @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>
</v-btn-group> </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>
<div class="chart-chance"> <div class="chart-chance">
<template v-if="chartMode === 'crypto' && cryptoSymbol"> <template v-if="chartMode === 'crypto' && cryptoSymbol">
@ -369,6 +368,7 @@ import {
LineType, LineType,
LastPriceAnimationMode, LastPriceAnimationMode,
TickMarkType, TickMarkType,
type BarPrice,
type IChartApi, type IChartApi,
type ISeriesApi, type ISeriesApi,
type Time, type Time,
@ -408,8 +408,8 @@ import {
priceHistoryToChartData, priceHistoryToChartData,
} from '../api/priceHistory' } from '../api/priceHistory'
import { import {
isCryptoEvent as checkIsCryptoEvent, parseCryptoSymbolFromTicker,
inferCryptoSymbol, formatCryptoChartPrice,
fetchCryptoChart, fetchCryptoChart,
subscribeCryptoRealtime, subscribeCryptoRealtime,
alignCryptoChartTimeMs, alignCryptoChartTimeMs,
@ -559,10 +559,16 @@ const isResolutionSourceUrl = computed(() => {
}) })
/** 是否为加密货币类型事件(可切换 YES/NO 分时 vs 加密货币价格图) */ /** 是否为加密货币类型事件(可切换 YES/NO 分时 vs 加密货币价格图) */
const isCryptoEvent = computed(() => checkIsCryptoEvent(eventDetail.value)) /** 从 event.ticker 首段解析的加密货币符号(如 bnb-updown-… → bnb无映射则不展示切换器 */
const cryptoSymbol = computed(() => parseCryptoSymbolFromTicker(eventDetail.value?.ticker))
/** 从事件推断的加密货币符号,如 btc、eth */ watch(cryptoSymbol, (sym) => {
const cryptoSymbol = computed(() => inferCryptoSymbol(eventDetail.value ?? {}) ?? null) if (!sym && chartMode.value === 'crypto') {
chartMode.value = 'yesno'
if (selectedTimeRange.value === '30S') selectedTimeRange.value = '1D'
void updateChartData()
}
})
/** 图表模式yesno=YES/NO 分时crypto=加密货币价格 */ /** 图表模式yesno=YES/NO 分时crypto=加密货币价格 */
const chartMode = ref<'yesno' | 'crypto'>('yesno') const chartMode = ref<'yesno' | 'crypto'>('yesno')
@ -572,10 +578,7 @@ const currentCryptoPrice = computed(() => {
const d = data.value const d = data.value
const last = d.length > 0 ? d[d.length - 1] : undefined const last = d.length > 0 ? d[d.length - 1] : undefined
if (last != null && chartMode.value === 'crypto') { if (last != null && chartMode.value === 'crypto') {
const p = last[1] return formatCryptoChartPrice(last[1])
return Number.isFinite(p)
? p.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
: '—'
} }
return '—' return '—'
}) })
@ -1246,7 +1249,13 @@ function ensureChartSeries() {
lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate, lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate,
crosshairMarkerVisible: true, crosshairMarkerVisible: true,
lastValueVisible: 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, priceScaleId: CHART_OVERLAY_PRICE_SCALE_ID,
}) })
chartInstance.priceScale(CHART_OVERLAY_PRICE_SCALE_ID).applyOptions({ chartInstance.priceScale(CHART_OVERLAY_PRICE_SCALE_ID).applyOptions({
@ -1437,7 +1446,8 @@ function applyCryptoRealtimePoint(point: [number, number]) {
async function updateChartData() { async function updateChartData() {
if (chartMode.value === 'crypto') { if (chartMode.value === 'crypto') {
const sym = cryptoSymbol.value ?? 'btc' const sym = cryptoSymbol.value
if (sym) {
const gen = ++cryptoLoadGeneration const gen = ++cryptoLoadGeneration
cryptoWsUnsubscribe?.() cryptoWsUnsubscribe?.()
cryptoWsUnsubscribe = null cryptoWsUnsubscribe = null
@ -1460,7 +1470,11 @@ async function updateChartData() {
cryptoChartLoading.value = false cryptoChartLoading.value = false
} }
} }
} else { return
}
chartMode.value = 'yesno'
if (selectedTimeRange.value === '30S') selectedTimeRange.value = '1D'
}
cryptoLoadGeneration++ cryptoLoadGeneration++
cryptoWsUnsubscribe?.() cryptoWsUnsubscribe?.()
cryptoWsUnsubscribe = null cryptoWsUnsubscribe = null
@ -1475,7 +1489,6 @@ async function updateChartData() {
} finally { } finally {
chartYesNoLoading.value = false chartYesNoLoading.value = false
} }
}
} }
function selectTimeRange(range: string) { function selectTimeRange(range: string) {
@ -1798,12 +1811,6 @@ onUnmounted(() => {
margin-bottom: 8px; margin-bottom: 8px;
} }
.past-btn {
font-size: 13px;
color: #6b7280;
text-transform: none;
}
.date-pill { .date-pill {
background-color: #111827 !important; background-color: #111827 !important;
color: #fff !important; color: #fff !important;
@ -1834,11 +1841,28 @@ onUnmounted(() => {
z-index: 2; z-index: 2;
} }
.chart-mode-toggle {
height: 26px;
margin-left: auto;
}
.chart-mode-toggle .v-btn.active { .chart-mode-toggle .v-btn.active {
background: rgb(var(--v-theme-primary)); background: rgb(var(--v-theme-primary));
color: rgb(var(--v-theme-on-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 { .chart-container {
width: 100%; width: 100%;
height: 320px; height: 320px;