优化:加密货币图表显示规则
This commit is contained in:
parent
1542790e4a
commit
9ec66ff163
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user