优化:折线图的显示宽度优化
This commit is contained in:
parent
062e370bea
commit
fef3d86fc2
@ -278,11 +278,21 @@ interface BinanceAggTradeMsg {
|
|||||||
|
|
||||||
const CRYPTO_UPDATE_THROTTLE_MS = 80
|
const CRYPTO_UPDATE_THROTTLE_MS = 80
|
||||||
|
|
||||||
|
/** 加密货币分时折线图 X 轴 / 补点统一时间步长(ms),与图表 timeScale 一致 */
|
||||||
|
export const CRYPTO_CHART_TIME_STEP_MS = 100
|
||||||
|
|
||||||
|
/** 将毫秒时间戳对齐到 {@link CRYPTO_CHART_TIME_STEP_MS} 网格(向下取整,单调递增友好) */
|
||||||
|
export function alignCryptoChartTimeMs(tsMs: number): number {
|
||||||
|
const step = CRYPTO_CHART_TIME_STEP_MS
|
||||||
|
return Math.floor(tsMs / step) * step
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订阅 Binance 归集交易 WebSocket(aggTrade),实现更丝滑的实时走势
|
* 订阅 Binance 归集交易 WebSocket(aggTrade),实现更丝滑的实时走势
|
||||||
* 参考:https://developers.binance.com/docs/zh-CN/binance-spot-api-docs/web-socket-streams#归集交易
|
* 参考:https://developers.binance.com/docs/zh-CN/binance-spot-api-docs/web-socket-streams#归集交易
|
||||||
* - aggTrade 实时推送每笔成交,比 kline_1m(约 1s 一次)更细
|
* - aggTrade 实时推送每笔成交,比 kline_1m(约 1s 一次)更细
|
||||||
* - 内部节流避免频繁 setOption 造成卡顿
|
* - 内部节流避免频繁 setOption 造成卡顿
|
||||||
|
* - 每 {@link CRYPTO_CHART_TIME_STEP_MS}ms 若本周期未收到真实 aggTrade,则用最新成交价补一点(时间戳对齐到步长网格)
|
||||||
*/
|
*/
|
||||||
export function subscribeCryptoRealtime(
|
export function subscribeCryptoRealtime(
|
||||||
symbol: string,
|
symbol: string,
|
||||||
@ -294,14 +304,23 @@ export function subscribeCryptoRealtime(
|
|||||||
const stream = `${binanceSymbol.toLowerCase()}@aggTrade`
|
const stream = `${binanceSymbol.toLowerCase()}@aggTrade`
|
||||||
const ws = new WebSocket(`${BINANCE_WS}/${stream}`)
|
const ws = new WebSocket(`${BINANCE_WS}/${stream}`)
|
||||||
|
|
||||||
|
let disposed = false
|
||||||
let lastUpdateTs = 0
|
let lastUpdateTs = 0
|
||||||
let pendingPoint: CryptoChartPoint | null = null
|
let pendingPoint: CryptoChartPoint | null = null
|
||||||
let rafId: number | null = null
|
let rafId: number | null = null
|
||||||
let throttleTimer: ReturnType<typeof setTimeout> | null = null
|
let throttleTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
/** 本 hold 周期内是否收到过真实 aggTrade(用于持价补点) */
|
||||||
|
let hadAggTradeSinceLastHoldTick = false
|
||||||
|
/** 最近一笔有效成交价,供无推送时重复输出 */
|
||||||
|
let lastTradedPrice: number | null = null
|
||||||
|
|
||||||
const flush = () => {
|
const flush = () => {
|
||||||
rafId = null
|
rafId = null
|
||||||
throttleTimer = null
|
throttleTimer = null
|
||||||
|
if (disposed) {
|
||||||
|
pendingPoint = null
|
||||||
|
return
|
||||||
|
}
|
||||||
if (pendingPoint) {
|
if (pendingPoint) {
|
||||||
onPoint(pendingPoint)
|
onPoint(pendingPoint)
|
||||||
pendingPoint = null
|
pendingPoint = null
|
||||||
@ -323,7 +342,16 @@ export function subscribeCryptoRealtime(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const holdTimer = setInterval(() => {
|
||||||
|
if (disposed) return
|
||||||
|
if (!hadAggTradeSinceLastHoldTick && lastTradedPrice != null) {
|
||||||
|
onPoint([alignCryptoChartTimeMs(Date.now()), lastTradedPrice])
|
||||||
|
}
|
||||||
|
hadAggTradeSinceLastHoldTick = false
|
||||||
|
}, CRYPTO_CHART_TIME_STEP_MS)
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
|
if (disposed) return
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse((event as MessageEvent).data) as BinanceAggTradeMsg & { e?: string }
|
const msg = JSON.parse((event as MessageEvent).data) as BinanceAggTradeMsg & { e?: string }
|
||||||
if (msg.e === 'ping') {
|
if (msg.e === 'ping') {
|
||||||
@ -335,9 +363,11 @@ export function subscribeCryptoRealtime(
|
|||||||
if (p == null || T == null) return
|
if (p == null || T == null) return
|
||||||
const price = parseFloat(p)
|
const price = parseFloat(p)
|
||||||
if (!Number.isFinite(price)) return
|
if (!Number.isFinite(price)) return
|
||||||
const ts = T < 1e12 ? T * 1000 : T
|
const rawTs = T < 1e12 ? T * 1000 : T
|
||||||
const point: CryptoChartPoint = [ts, price]
|
const point: CryptoChartPoint = [alignCryptoChartTimeMs(rawTs), price]
|
||||||
|
|
||||||
|
hadAggTradeSinceLastHoldTick = true
|
||||||
|
lastTradedPrice = price
|
||||||
pendingPoint = point
|
pendingPoint = point
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (now - lastUpdateTs >= CRYPTO_UPDATE_THROTTLE_MS) {
|
if (now - lastUpdateTs >= CRYPTO_UPDATE_THROTTLE_MS) {
|
||||||
@ -352,6 +382,9 @@ export function subscribeCryptoRealtime(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
disposed = true
|
||||||
|
pendingPoint = null
|
||||||
|
clearInterval(holdTimer)
|
||||||
if (rafId) cancelAnimationFrame(rafId)
|
if (rafId) cancelAnimationFrame(rafId)
|
||||||
if (throttleTimer) clearTimeout(throttleTimer)
|
if (throttleTimer) clearTimeout(throttleTimer)
|
||||||
ws.close()
|
ws.close()
|
||||||
|
|||||||
160
src/composables/inPanePriceScalePrimitive.ts
Normal file
160
src/composables/inPanePriceScalePrimitive.ts
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
/**
|
||||||
|
* 在主图区域内绘制 Y 轴刻度标签(不占用右侧独立价轴条带)。
|
||||||
|
* 思路来自 lightweight-charts 官方 plugin-examples/overlay-price-scale。
|
||||||
|
*/
|
||||||
|
import type { CanvasRenderingTarget2D } from 'fancy-canvas'
|
||||||
|
import type {
|
||||||
|
BarPrice,
|
||||||
|
IPrimitivePaneRenderer,
|
||||||
|
IPrimitivePaneView,
|
||||||
|
ISeriesPrimitive,
|
||||||
|
IPriceFormatter,
|
||||||
|
SeriesAttachedParameter,
|
||||||
|
Time,
|
||||||
|
} from 'lightweight-charts'
|
||||||
|
|
||||||
|
export interface InPanePriceScaleOptions {
|
||||||
|
textColor: string
|
||||||
|
side: 'left' | 'right'
|
||||||
|
/** 相邻刻度最小间距(像素) */
|
||||||
|
tickSpacingPx: number
|
||||||
|
fontSizePx: number
|
||||||
|
/** 与绘图区边缘的水平间距 */
|
||||||
|
sideMarginPx: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOptions: InPanePriceScaleOptions = {
|
||||||
|
textColor: 'rgba(107, 114, 128, 0.65)',
|
||||||
|
side: 'right',
|
||||||
|
tickSpacingPx: 44,
|
||||||
|
fontSizePx: 9,
|
||||||
|
sideMarginPx: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Label {
|
||||||
|
label: string
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RendererData {
|
||||||
|
priceFormatter: IPriceFormatter
|
||||||
|
coordinateToPrice: (y: number) => BarPrice | null
|
||||||
|
options: InPanePriceScaleOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
function barPriceToNumber(p: BarPrice | null): number | null {
|
||||||
|
if (p == null) return null
|
||||||
|
const n = typeof p === 'number' ? p : Number(p)
|
||||||
|
return Number.isFinite(n) ? n : null
|
||||||
|
}
|
||||||
|
|
||||||
|
class InPanePriceScaleRenderer implements IPrimitivePaneRenderer {
|
||||||
|
private _data: RendererData | null = null
|
||||||
|
|
||||||
|
update(data: RendererData) {
|
||||||
|
this._data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(target: CanvasRenderingTarget2D) {
|
||||||
|
target.useMediaCoordinateSpace((scope) => {
|
||||||
|
if (!this._data) return
|
||||||
|
const { options } = this._data
|
||||||
|
const labels = this._labelsForHeight(scope.mediaSize.height, this._data)
|
||||||
|
if (labels.length === 0) return
|
||||||
|
|
||||||
|
const ctx = scope.context
|
||||||
|
ctx.font = `${options.fontSizePx}px inherit`
|
||||||
|
const isLeft = options.side === 'left'
|
||||||
|
|
||||||
|
ctx.textAlign = isLeft ? 'left' : 'right'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
|
||||||
|
for (const item of labels) {
|
||||||
|
const textX = isLeft
|
||||||
|
? options.sideMarginPx
|
||||||
|
: scope.mediaSize.width - options.sideMarginPx
|
||||||
|
ctx.fillStyle = options.textColor
|
||||||
|
ctx.fillText(item.label, textX, item.y)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private _labelsForHeight(height: number, data: RendererData): Label[] {
|
||||||
|
const { coordinateToPrice, priceFormatter, options } = data
|
||||||
|
const spacing = options.tickSpacingPx
|
||||||
|
const half = Math.max(4, Math.round(spacing / 4))
|
||||||
|
const ys: number[] = []
|
||||||
|
for (let y = half; y <= height - half; y += spacing) {
|
||||||
|
ys.push(y)
|
||||||
|
}
|
||||||
|
const out: Label[] = []
|
||||||
|
for (const y of ys) {
|
||||||
|
const price = barPriceToNumber(coordinateToPrice(y))
|
||||||
|
if (price == null) continue
|
||||||
|
out.push({ label: priceFormatter.format(price), y })
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InPanePriceScalePaneView implements IPrimitivePaneView {
|
||||||
|
private readonly _renderer: InPanePriceScaleRenderer
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._renderer = new InPanePriceScaleRenderer()
|
||||||
|
}
|
||||||
|
|
||||||
|
zOrder(): 'top' {
|
||||||
|
return 'top'
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer(): IPrimitivePaneRenderer {
|
||||||
|
return this._renderer
|
||||||
|
}
|
||||||
|
|
||||||
|
update(data: RendererData) {
|
||||||
|
this._renderer.update(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InPanePriceScalePrimitive implements ISeriesPrimitive<Time> {
|
||||||
|
private readonly _paneViews: InPanePriceScalePaneView[]
|
||||||
|
private _options: InPanePriceScaleOptions
|
||||||
|
private _series: SeriesAttachedParameter<Time>['series'] | null = null
|
||||||
|
private _requestUpdate: (() => void) | undefined
|
||||||
|
|
||||||
|
constructor(options?: Partial<InPanePriceScaleOptions>) {
|
||||||
|
this._options = { ...defaultOptions, ...options }
|
||||||
|
this._paneViews = [new InPanePriceScalePaneView()]
|
||||||
|
}
|
||||||
|
|
||||||
|
applyOptions(options: Partial<InPanePriceScaleOptions>) {
|
||||||
|
this._options = { ...this._options, ...options }
|
||||||
|
this._requestUpdate?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
attached(param: SeriesAttachedParameter<Time>): void {
|
||||||
|
this._series = param.series
|
||||||
|
this._requestUpdate = param.requestUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
detached(): void {
|
||||||
|
this._series = null
|
||||||
|
this._requestUpdate = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAllViews(): void {
|
||||||
|
const s = this._series
|
||||||
|
if (!s) return
|
||||||
|
const data: RendererData = {
|
||||||
|
priceFormatter: s.priceFormatter(),
|
||||||
|
coordinateToPrice: (y) => s.coordinateToPrice(y),
|
||||||
|
options: this._options,
|
||||||
|
}
|
||||||
|
this._paneViews[0]!.update(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
paneViews(): readonly IPrimitivePaneView[] {
|
||||||
|
return this._paneViews
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -30,10 +30,14 @@
|
|||||||
{{ categoryText }}
|
{{ categoryText }}
|
||||||
</p>
|
</p>
|
||||||
<div class="chart-controls-row">
|
<div class="chart-controls-row">
|
||||||
<v-btn variant="text" size="small" class="past-btn">{{ t('eventMarkets.past') }}</v-btn>
|
<v-btn variant="text" size="small" class="past-btn">{{
|
||||||
|
t('eventMarkets.past')
|
||||||
|
}}</v-btn>
|
||||||
<v-btn class="date-pill" size="small" rounded="pill">{{ resolutionDate }}</v-btn>
|
<v-btn class="date-pill" size="small" rounded="pill">{{ resolutionDate }}</v-btn>
|
||||||
</div>
|
</div>
|
||||||
<p class="chart-legend-hint">{{ t('eventMarkets.marketsCount', { n: markets.length }) }}</p>
|
<p class="chart-legend-hint">
|
||||||
|
{{ t('eventMarkets.marketsCount', { n: markets.length }) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper">
|
<div class="chart-wrapper">
|
||||||
<div v-if="chartLoading" class="chart-loading-overlay">
|
<div v-if="chartLoading" class="chart-loading-overlay">
|
||||||
@ -101,7 +105,9 @@
|
|||||||
@click="goToTradeDetail(market)"
|
@click="goToTradeDetail(market)"
|
||||||
>
|
>
|
||||||
<div class="market-row-main">
|
<div class="market-row-main">
|
||||||
<span class="market-question">{{ market.question || t('eventMarkets.marketPlaceholder') }}</span>
|
<span class="market-question">{{
|
||||||
|
market.question || t('eventMarkets.marketPlaceholder')
|
||||||
|
}}</span>
|
||||||
<span class="market-chance">{{ marketChance(market) }}%</span>
|
<span class="market-chance">{{ marketChance(market) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="market-row-vol">{{ formatVolume(market.volume) }}</div>
|
<div class="market-row-vol">{{ formatVolume(market.volume) }}</div>
|
||||||
@ -152,7 +158,8 @@
|
|||||||
rounded="sm"
|
rounded="sm"
|
||||||
@click="openSheetWithOption('yes')"
|
@click="openSheetWithOption('yes')"
|
||||||
>
|
>
|
||||||
{{ barMarket ? yesLabel(barMarket) : t('eventMarkets.yes') }} {{ barMarket ? yesPrice(barMarket) : t('eventMarkets.priceZero') }}
|
{{ barMarket ? yesLabel(barMarket) : t('eventMarkets.yes') }}
|
||||||
|
{{ barMarket ? yesPrice(barMarket) : t('eventMarkets.priceZero') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
class="mobile-bar-btn mobile-bar-no"
|
class="mobile-bar-btn mobile-bar-no"
|
||||||
@ -160,7 +167,8 @@
|
|||||||
rounded="sm"
|
rounded="sm"
|
||||||
@click="openSheetWithOption('no')"
|
@click="openSheetWithOption('no')"
|
||||||
>
|
>
|
||||||
{{ barMarket ? noLabel(barMarket) : t('eventMarkets.no') }} {{ barMarket ? noPrice(barMarket) : t('eventMarkets.priceZero') }}
|
{{ barMarket ? noLabel(barMarket) : t('eventMarkets.no') }}
|
||||||
|
{{ barMarket ? noPrice(barMarket) : t('eventMarkets.priceZero') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-menu
|
<v-menu
|
||||||
v-model="mobileMenuOpen"
|
v-model="mobileMenuOpen"
|
||||||
@ -215,7 +223,14 @@ defineOptions({ name: 'EventMarkets' })
|
|||||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { createChart, LineSeries, LineType, LastPriceAnimationMode, type IChartApi, type ISeriesApi } from 'lightweight-charts'
|
import {
|
||||||
|
createChart,
|
||||||
|
LineSeries,
|
||||||
|
LineType,
|
||||||
|
LastPriceAnimationMode,
|
||||||
|
type IChartApi,
|
||||||
|
type ISeriesApi,
|
||||||
|
} from 'lightweight-charts'
|
||||||
import { toLwcData } from '../composables/useLightweightChart'
|
import { toLwcData } from '../composables/useLightweightChart'
|
||||||
import TradeComponent from '../components/TradeComponent.vue'
|
import TradeComponent from '../components/TradeComponent.vue'
|
||||||
import {
|
import {
|
||||||
@ -226,7 +241,11 @@ import {
|
|||||||
type PmEventListItem,
|
type PmEventListItem,
|
||||||
type PmEventMarketItem,
|
type PmEventMarketItem,
|
||||||
} from '../api/event'
|
} from '../api/event'
|
||||||
import { getPmPriceHistoryPublic, priceHistoryToChartData, getTimeRangeSeconds } from '../api/priceHistory'
|
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'
|
||||||
@ -391,7 +410,10 @@ async function loadChartFromApi(): Promise<ChartSeriesItem[]> {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const ev = eventDetail.value
|
const ev = eventDetail.value
|
||||||
const timeRange = getTimeRangeSeconds(range, ev ? { startDate: ev.startDate, endDate: ev.endDate } : undefined)
|
const timeRange = getTimeRangeSeconds(
|
||||||
|
range,
|
||||||
|
ev ? { startDate: ev.startDate, endDate: ev.endDate } : undefined,
|
||||||
|
)
|
||||||
const res = await getPmPriceHistoryPublic({
|
const res = await getPmPriceHistoryPublic({
|
||||||
market: yesTokenId,
|
market: yesTokenId,
|
||||||
page: 1,
|
page: 1,
|
||||||
@ -416,7 +438,7 @@ function setChartSeries(seriesArr: ChartSeriesItem[]) {
|
|||||||
const series = chartInstance!.addSeries(LineSeries, {
|
const series = chartInstance!.addSeries(LineSeries, {
|
||||||
color,
|
color,
|
||||||
lineWidth: 2,
|
lineWidth: 2,
|
||||||
lineType: LineType.Simple,
|
lineType: LineType.Curved,
|
||||||
lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate,
|
lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate,
|
||||||
crosshairMarkerVisible: true,
|
crosshairMarkerVisible: true,
|
||||||
lastValueVisible: true,
|
lastValueVisible: true,
|
||||||
@ -630,7 +652,9 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
let tradeSheetUnmountTimer: ReturnType<typeof setTimeout> | undefined
|
let tradeSheetUnmountTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
let tradeSheetMountTimer: ReturnType<typeof setTimeout> | undefined
|
let tradeSheetMountTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
watch(tradeSheetOpen, (open) => {
|
watch(
|
||||||
|
tradeSheetOpen,
|
||||||
|
(open) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
if (tradeSheetUnmountTimer) {
|
if (tradeSheetUnmountTimer) {
|
||||||
clearTimeout(tradeSheetUnmountTimer)
|
clearTimeout(tradeSheetUnmountTimer)
|
||||||
@ -662,7 +686,9 @@ watch(tradeSheetOpen, (open) => {
|
|||||||
tradeSheetUnmountTimer = undefined
|
tradeSheetUnmountTimer = undefined
|
||||||
}, 350)
|
}, 350)
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
|
|||||||
@ -3,7 +3,12 @@
|
|||||||
<v-pull-to-refresh class="trade-detail-pull-refresh" @load="onRefresh">
|
<v-pull-to-refresh class="trade-detail-pull-refresh" @load="onRefresh">
|
||||||
<div class="trade-detail-pull-refresh-inner">
|
<div class="trade-detail-pull-refresh-inner">
|
||||||
<!-- findPmEvent 请求中:仅显示 loading -->
|
<!-- findPmEvent 请求中:仅显示 loading -->
|
||||||
<v-card v-if="detailLoading && !eventDetail" class="trade-detail-loading-card" elevation="0" rounded="lg">
|
<v-card
|
||||||
|
v-if="detailLoading && !eventDetail"
|
||||||
|
class="trade-detail-loading-card"
|
||||||
|
elevation="0"
|
||||||
|
rounded="lg"
|
||||||
|
>
|
||||||
<div class="trade-detail-loading-placeholder">
|
<div class="trade-detail-loading-placeholder">
|
||||||
<v-progress-circular indeterminate color="primary" size="48" />
|
<v-progress-circular indeterminate color="primary" size="48" />
|
||||||
<p>{{ t('common.loading') }}</p>
|
<p>{{ t('common.loading') }}</p>
|
||||||
@ -90,7 +95,11 @@
|
|||||||
|
|
||||||
<!-- 持仓 / 限价(订单簿上方) -->
|
<!-- 持仓 / 限价(订单簿上方) -->
|
||||||
<v-card class="positions-orders-card" elevation="0" rounded="lg">
|
<v-card class="positions-orders-card" elevation="0" rounded="lg">
|
||||||
<v-tabs v-model="positionsOrdersTab" class="positions-orders-tabs" density="comfortable">
|
<v-tabs
|
||||||
|
v-model="positionsOrdersTab"
|
||||||
|
class="positions-orders-tabs"
|
||||||
|
density="comfortable"
|
||||||
|
>
|
||||||
<v-tab value="positions">{{ t('activity.myPositions') }}</v-tab>
|
<v-tab value="positions">{{ t('activity.myPositions') }}</v-tab>
|
||||||
<v-tab value="orders">{{ t('activity.openOrders') }}</v-tab>
|
<v-tab value="orders">{{ t('activity.openOrders') }}</v-tab>
|
||||||
</v-tabs>
|
</v-tabs>
|
||||||
@ -104,7 +113,11 @@
|
|||||||
{{ t('activity.noPositionsInMarket') }}
|
{{ t('activity.noPositionsInMarket') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="positions-list">
|
<div v-else class="positions-list">
|
||||||
<div v-for="pos in marketPositionsFiltered" :key="pos.id" class="position-row-item">
|
<div
|
||||||
|
v-for="pos in marketPositionsFiltered"
|
||||||
|
:key="pos.id"
|
||||||
|
class="position-row-item"
|
||||||
|
>
|
||||||
<div class="position-row-header">
|
<div class="position-row-header">
|
||||||
<div class="position-row-icon" :class="pos.iconClass">
|
<div class="position-row-icon" :class="pos.iconClass">
|
||||||
<img
|
<img
|
||||||
@ -113,7 +126,9 @@
|
|||||||
alt=""
|
alt=""
|
||||||
class="position-row-icon-img"
|
class="position-row-icon-img"
|
||||||
/>
|
/>
|
||||||
<span v-else class="position-row-icon-char">{{ pos.iconChar || '•' }}</span>
|
<span v-else class="position-row-icon-char">{{
|
||||||
|
pos.iconChar || '•'
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="position-row-title">{{ pos.market }}</span>
|
<span class="position-row-title">{{ pos.market }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -135,7 +150,9 @@
|
|||||||
size="small"
|
size="small"
|
||||||
color="primary"
|
color="primary"
|
||||||
class="position-sell-btn"
|
class="position-sell-btn"
|
||||||
:disabled="!(pos.availableSharesNum != null && pos.availableSharesNum > 0)"
|
:disabled="
|
||||||
|
!(pos.availableSharesNum != null && pos.availableSharesNum > 0)
|
||||||
|
"
|
||||||
@click="openSellFromPosition(pos)"
|
@click="openSellFromPosition(pos)"
|
||||||
>
|
>
|
||||||
{{ t('trade.sell') }}
|
{{ t('trade.sell') }}
|
||||||
@ -156,7 +173,9 @@
|
|||||||
<div v-else class="orders-list">
|
<div v-else class="orders-list">
|
||||||
<div v-for="ord in marketOpenOrders" :key="ord.id" class="order-row-item">
|
<div v-for="ord in marketOpenOrders" :key="ord.id" class="order-row-item">
|
||||||
<div class="order-row-main">
|
<div class="order-row-main">
|
||||||
<span :class="['order-side-pill', ord.side === 'Yes' ? 'side-yes' : 'side-no']">
|
<span
|
||||||
|
:class="['order-side-pill', ord.side === 'Yes' ? 'side-yes' : 'side-no']"
|
||||||
|
>
|
||||||
{{ ord.actionLabel || `Buy ${ord.outcome}` }}
|
{{ ord.actionLabel || `Buy ${ord.outcome}` }}
|
||||||
</span>
|
</span>
|
||||||
<span class="order-price">{{ ord.price }}</span>
|
<span class="order-price">{{ ord.price }}</span>
|
||||||
@ -207,7 +226,10 @@
|
|||||||
<!-- Comments / Top Holders / Activity(与左侧图表、订单簿同宽) -->
|
<!-- Comments / Top Holders / Activity(与左侧图表、订单簿同宽) -->
|
||||||
<v-card class="activity-card" elevation="0" rounded="lg">
|
<v-card class="activity-card" elevation="0" rounded="lg">
|
||||||
<div class="rules-pane">
|
<div class="rules-pane">
|
||||||
<div v-if="!eventDetail?.description && !eventDetail?.resolutionSource" class="placeholder-pane">
|
<div
|
||||||
|
v-if="!eventDetail?.description && !eventDetail?.resolutionSource"
|
||||||
|
class="placeholder-pane"
|
||||||
|
>
|
||||||
{{ t('activity.rulesEmpty') }}
|
{{ t('activity.rulesEmpty') }}
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@ -346,10 +368,13 @@ import {
|
|||||||
LineSeries,
|
LineSeries,
|
||||||
LineType,
|
LineType,
|
||||||
LastPriceAnimationMode,
|
LastPriceAnimationMode,
|
||||||
|
TickMarkType,
|
||||||
type IChartApi,
|
type IChartApi,
|
||||||
type ISeriesApi,
|
type ISeriesApi,
|
||||||
|
type Time,
|
||||||
} from 'lightweight-charts'
|
} from 'lightweight-charts'
|
||||||
import { toLwcData, toLwcPoint } from '../composables/useLightweightChart'
|
import { toLwcData, toLwcPoint } from '../composables/useLightweightChart'
|
||||||
|
import { InPanePriceScalePrimitive } from '../composables/inPanePriceScalePrimitive'
|
||||||
import OrderBook from '../components/OrderBook.vue'
|
import OrderBook from '../components/OrderBook.vue'
|
||||||
import TradeComponent, { type TradePositionItem } from '../components/TradeComponent.vue'
|
import TradeComponent, { type TradePositionItem } from '../components/TradeComponent.vue'
|
||||||
import {
|
import {
|
||||||
@ -387,6 +412,8 @@ import {
|
|||||||
inferCryptoSymbol,
|
inferCryptoSymbol,
|
||||||
fetchCryptoChart,
|
fetchCryptoChart,
|
||||||
subscribeCryptoRealtime,
|
subscribeCryptoRealtime,
|
||||||
|
alignCryptoChartTimeMs,
|
||||||
|
CRYPTO_CHART_TIME_STEP_MS,
|
||||||
} from '../api/cryptoChart'
|
} from '../api/cryptoChart'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@ -502,11 +529,9 @@ async function loadEventDetail() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onRefresh({ done }: { done: () => void }) {
|
function onRefresh({ done }: { done: () => void }) {
|
||||||
Promise.all([
|
Promise.all([loadEventDetail(), loadMarketPositions(), loadMarketOpenOrders()]).finally(() =>
|
||||||
loadEventDetail(),
|
done(),
|
||||||
loadMarketPositions(),
|
)
|
||||||
loadMarketOpenOrders(),
|
|
||||||
]).finally(() => done())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标题、成交量、到期日:优先接口详情,其次卡片 query,最后占位
|
// 标题、成交量、到期日:优先接口详情,其次卡片 query,最后占位
|
||||||
@ -1174,6 +1199,8 @@ const cryptoChartLoading = ref(false)
|
|||||||
const chartYesNoLoading = ref(false)
|
const chartYesNoLoading = ref(false)
|
||||||
let chartInstance: IChartApi | null = null
|
let chartInstance: IChartApi | null = null
|
||||||
let chartSeries: ISeriesApi<'Line'> | null = null
|
let chartSeries: ISeriesApi<'Line'> | null = null
|
||||||
|
/** 主图内自绘 Y 轴刻度(不占用右侧价轴条带) */
|
||||||
|
let inPanePriceScale: InPanePriceScalePrimitive | null = null
|
||||||
|
|
||||||
const currentChance = computed(() => {
|
const currentChance = computed(() => {
|
||||||
const ev = eventDetail.value
|
const ev = eventDetail.value
|
||||||
@ -1195,11 +1222,18 @@ const currentChance = computed(() => {
|
|||||||
|
|
||||||
const lineColor = '#2563eb'
|
const lineColor = '#2563eb'
|
||||||
|
|
||||||
const MOBILE_BREAKPOINT = 600
|
/** 叠加价格轴 ID:曲线绑定在 overlay scale,主图占满宽;Y 轴数字由 InPanePriceScalePrimitive 画在图内 */
|
||||||
|
const CHART_OVERLAY_PRICE_SCALE_ID = 'overlay-price'
|
||||||
|
/** 右侧留白(px):fitContent 时压缩可用宽度,线头略离开物理右缘,lastPrice 水波能露出一半且不依赖超大 rightOffset(bars) */
|
||||||
|
const CHART_RIPPLE_RIGHT_PAD_PX = 28
|
||||||
|
|
||||||
function ensureChartSeries() {
|
function ensureChartSeries() {
|
||||||
if (!chartInstance || !chartContainerRef.value) return
|
if (!chartInstance || !chartContainerRef.value) return
|
||||||
if (chartSeries) {
|
if (chartSeries) {
|
||||||
|
if (inPanePriceScale) {
|
||||||
|
chartSeries.detachPrimitive(inPanePriceScale)
|
||||||
|
inPanePriceScale = null
|
||||||
|
}
|
||||||
chartInstance.removeSeries(chartSeries)
|
chartInstance.removeSeries(chartSeries)
|
||||||
chartSeries = null
|
chartSeries = null
|
||||||
}
|
}
|
||||||
@ -1207,12 +1241,26 @@ function ensureChartSeries() {
|
|||||||
chartSeries = chartInstance.addSeries(LineSeries, {
|
chartSeries = chartInstance.addSeries(LineSeries, {
|
||||||
color: lineColor,
|
color: lineColor,
|
||||||
lineWidth: 2,
|
lineWidth: 2,
|
||||||
lineType: LineType.Simple,
|
lineType: LineType.Curved,
|
||||||
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: 'price', precision: 2 } : { type: 'percent', precision: 1 },
|
||||||
|
priceScaleId: CHART_OVERLAY_PRICE_SCALE_ID,
|
||||||
})
|
})
|
||||||
|
chartInstance.priceScale(CHART_OVERLAY_PRICE_SCALE_ID).applyOptions({
|
||||||
|
borderVisible: false,
|
||||||
|
scaleMargins: { top: 0.1, bottom: 0.1 },
|
||||||
|
})
|
||||||
|
inPanePriceScale = new InPanePriceScalePrimitive({
|
||||||
|
textColor: 'rgba(107, 114, 128, 0.6)',
|
||||||
|
side: 'right',
|
||||||
|
tickSpacingPx: isCrypto ? 40 : 48,
|
||||||
|
fontSizePx: 9,
|
||||||
|
sideMarginPx: 6,
|
||||||
|
})
|
||||||
|
chartSeries.attachPrimitive(inPanePriceScale)
|
||||||
|
applyCryptoChartTimeScaleOptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
function setChartData(chartData: [number, number][]) {
|
function setChartData(chartData: [number, number][]) {
|
||||||
@ -1226,7 +1274,7 @@ function initChart() {
|
|||||||
const el = chartContainerRef.value
|
const el = chartContainerRef.value
|
||||||
chartInstance = createChart(el, {
|
chartInstance = createChart(el, {
|
||||||
width: el.clientWidth,
|
width: el.clientWidth,
|
||||||
height: 320,
|
height: el.clientHeight || 320,
|
||||||
layout: {
|
layout: {
|
||||||
background: { color: '#ffffff' },
|
background: { color: '#ffffff' },
|
||||||
textColor: '#6b7280',
|
textColor: '#6b7280',
|
||||||
@ -1235,8 +1283,19 @@ function initChart() {
|
|||||||
attributionLogo: false,
|
attributionLogo: false,
|
||||||
},
|
},
|
||||||
grid: { vertLines: { color: '#f3f4f6' }, horzLines: { color: '#f3f4f6' } },
|
grid: { vertLines: { color: '#f3f4f6' }, horzLines: { color: '#f3f4f6' } },
|
||||||
rightPriceScale: { borderColor: '#e5e7eb', scaleMargins: { top: 0.1, bottom: 0.1 } },
|
leftPriceScale: { visible: false },
|
||||||
timeScale: { borderColor: '#e5e7eb', timeVisible: true, secondsVisible: false },
|
// 关闭右侧价轴条带;Y 轴数值由 InPanePriceScalePrimitive 绘在主图内
|
||||||
|
rightPriceScale: { visible: false },
|
||||||
|
overlayPriceScales: {
|
||||||
|
borderVisible: false,
|
||||||
|
scaleMargins: { top: 0.1, bottom: 0.1 },
|
||||||
|
},
|
||||||
|
timeScale: {
|
||||||
|
borderColor: '#e5e7eb',
|
||||||
|
timeVisible: true,
|
||||||
|
secondsVisible: false,
|
||||||
|
rightOffsetPixels: CHART_RIPPLE_RIGHT_PAD_PX,
|
||||||
|
},
|
||||||
handleScroll: false,
|
handleScroll: false,
|
||||||
handleScale: false,
|
handleScale: false,
|
||||||
crosshair: { vertLine: { labelVisible: false }, horzLine: { labelVisible: true } },
|
crosshair: { vertLine: { labelVisible: false }, horzLine: { labelVisible: true } },
|
||||||
@ -1273,17 +1332,72 @@ async function loadChartFromApi(marketParam: string, range: string): Promise<Cha
|
|||||||
|
|
||||||
const MINUTE_MS = 60 * 1000
|
const MINUTE_MS = 60 * 1000
|
||||||
let cryptoWsUnsubscribe: (() => void) | null = null
|
let cryptoWsUnsubscribe: (() => void) | null = null
|
||||||
|
/** 防止重叠的 updateChartData:旧请求若在晚请求已 subscribe 之后才结束,会再挂一层 WS/hold 定时器,插值越来越快 */
|
||||||
|
let cryptoLoadGeneration = 0
|
||||||
|
let cryptoScrollToRtRaf = 0
|
||||||
|
|
||||||
|
/** 每帧最多跟一次盘,避免插值过密时线头跑出视口(shiftVisibleRangeOnNewBar 对 update 新点偶发不跟) */
|
||||||
|
function scheduleCryptoScrollToRealtime() {
|
||||||
|
if (!chartInstance || chartMode.value !== 'crypto') return
|
||||||
|
if (cryptoScrollToRtRaf) return
|
||||||
|
cryptoScrollToRtRaf = requestAnimationFrame(() => {
|
||||||
|
cryptoScrollToRtRaf = 0
|
||||||
|
chartInstance?.timeScale().scrollToRealTime()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加密货币细粒度推送:显示到秒+毫秒;新点推入时视窗右移并每帧 scrollToRealTime 咬住线头 */
|
||||||
|
function applyCryptoChartTimeScaleOptions() {
|
||||||
|
if (!chartInstance) return
|
||||||
|
if (chartMode.value === 'crypto') {
|
||||||
|
chartInstance.applyOptions({
|
||||||
|
timeScale: {
|
||||||
|
rightOffsetPixels: CHART_RIPPLE_RIGHT_PAD_PX,
|
||||||
|
secondsVisible: true,
|
||||||
|
shiftVisibleRangeOnNewBar: true,
|
||||||
|
tickMarkFormatter: (time: Time, tickMarkType: TickMarkType) => {
|
||||||
|
if (typeof time !== 'number') return null
|
||||||
|
if (tickMarkType !== TickMarkType.TimeWithSeconds) return null
|
||||||
|
const d = new Date(time * 1000)
|
||||||
|
const pad2 = (n: number) => String(n).padStart(2, '0')
|
||||||
|
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${String(d.getMilliseconds()).padStart(3, '0')}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
chartInstance.applyOptions({
|
||||||
|
timeScale: {
|
||||||
|
rightOffsetPixels: CHART_RIPPLE_RIGHT_PAD_PX,
|
||||||
|
secondsVisible: false,
|
||||||
|
shiftVisibleRangeOnNewBar: true,
|
||||||
|
tickMarkFormatter: () => null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyCryptoRealtimePoint(point: [number, number]) {
|
function applyCryptoRealtimePoint(point: [number, number]) {
|
||||||
|
if (chartMode.value !== 'crypto') return
|
||||||
|
|
||||||
const list = [...data.value]
|
const list = [...data.value]
|
||||||
const [ts, price] = point
|
let ts = alignCryptoChartTimeMs(point[0])
|
||||||
|
const price = point[1]
|
||||||
|
const prevLastT = list.length > 0 ? list[list.length - 1]![0] : null
|
||||||
|
// 本地补点用 now,成交用 Binance T;T 可能略早于上一根 → update() 报 Cannot update oldest data
|
||||||
|
if (prevLastT != null && ts < prevLastT) {
|
||||||
|
ts = alignCryptoChartTimeMs(prevLastT + CRYPTO_CHART_TIME_STEP_MS)
|
||||||
|
}
|
||||||
const range = selectedTimeRange.value
|
const range = selectedTimeRange.value
|
||||||
let useIncrementalUpdate = false
|
let useIncrementalUpdate = false
|
||||||
let updatePoint: [number, number] | null = null
|
let updatePoint: [number, number] | null = null
|
||||||
|
|
||||||
if (range === '30S') {
|
if (range === '30S') {
|
||||||
const cutoff = Date.now() - 30 * 1000
|
const cutoff = Date.now() - 30 * 1000
|
||||||
|
if (prevLastT != null && ts === prevLastT) {
|
||||||
|
list[list.length - 1] = [ts, price]
|
||||||
|
} else {
|
||||||
list.push([ts, price])
|
list.push([ts, price])
|
||||||
|
}
|
||||||
const filtered = list.filter(([t]) => t >= cutoff).sort((a, b) => a[0] - b[0])
|
const filtered = list.filter(([t]) => t >= cutoff).sort((a, b) => a[0] - b[0])
|
||||||
data.value = filtered
|
data.value = filtered
|
||||||
// 仅当未过滤掉任何点时可用 update 实现平滑动画
|
// 仅当未过滤掉任何点时可用 update 实现平滑动画
|
||||||
@ -1293,7 +1407,8 @@ function applyCryptoRealtimePoint(point: [number, number]) {
|
|||||||
const last = list[list.length - 1]
|
const last = list[list.length - 1]
|
||||||
const minuteStart = Math.floor(ts / MINUTE_MS) * MINUTE_MS
|
const minuteStart = Math.floor(ts / MINUTE_MS) * MINUTE_MS
|
||||||
const sameMinute = last && Math.floor(last[0] / MINUTE_MS) === Math.floor(ts / MINUTE_MS)
|
const sameMinute = last && Math.floor(last[0] / MINUTE_MS) === Math.floor(ts / MINUTE_MS)
|
||||||
const baseMax = { '1H': 60, '6H': 360, '1D': 1440, '1W': 1680, '1M': 43200, ALL: 10080 }[range] ?? 60
|
const baseMax =
|
||||||
|
{ '1H': 60, '6H': 360, '1D': 1440, '1W': 1680, '1M': 43200, ALL: 10080 }[range] ?? 60
|
||||||
const extra = Math.min(200, Math.round(baseMax * 0.1))
|
const extra = Math.min(200, Math.round(baseMax * 0.1))
|
||||||
const max = baseMax + extra
|
const max = baseMax + extra
|
||||||
if (sameMinute) {
|
if (sameMinute) {
|
||||||
@ -1316,11 +1431,13 @@ function applyCryptoRealtimePoint(point: [number, number]) {
|
|||||||
} else {
|
} else {
|
||||||
setChartData(data.value)
|
setChartData(data.value)
|
||||||
}
|
}
|
||||||
|
scheduleCryptoScrollToRealtime()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateChartData() {
|
async function updateChartData() {
|
||||||
if (chartMode.value === 'crypto') {
|
if (chartMode.value === 'crypto') {
|
||||||
const sym = cryptoSymbol.value ?? 'btc'
|
const sym = cryptoSymbol.value ?? 'btc'
|
||||||
|
const gen = ++cryptoLoadGeneration
|
||||||
cryptoWsUnsubscribe?.()
|
cryptoWsUnsubscribe?.()
|
||||||
cryptoWsUnsubscribe = null
|
cryptoWsUnsubscribe = null
|
||||||
cryptoChartLoading.value = true
|
cryptoChartLoading.value = true
|
||||||
@ -1329,14 +1446,23 @@ async function updateChartData() {
|
|||||||
symbol: sym,
|
symbol: sym,
|
||||||
range: selectedTimeRange.value,
|
range: selectedTimeRange.value,
|
||||||
})
|
})
|
||||||
|
if (gen !== cryptoLoadGeneration || chartMode.value !== 'crypto') {
|
||||||
|
return
|
||||||
|
}
|
||||||
data.value = (res.data ?? []) as [number, number][]
|
data.value = (res.data ?? []) as [number, number][]
|
||||||
ensureChartSeries()
|
ensureChartSeries()
|
||||||
setChartData(data.value)
|
setChartData(data.value)
|
||||||
|
nextTick(() => scheduleCryptoScrollToRealtime())
|
||||||
|
cryptoWsUnsubscribe?.()
|
||||||
|
cryptoWsUnsubscribe = null
|
||||||
cryptoWsUnsubscribe = subscribeCryptoRealtime(sym, applyCryptoRealtimePoint)
|
cryptoWsUnsubscribe = subscribeCryptoRealtime(sym, applyCryptoRealtimePoint)
|
||||||
} finally {
|
} finally {
|
||||||
|
if (gen === cryptoLoadGeneration) {
|
||||||
cryptoChartLoading.value = false
|
cryptoChartLoading.value = false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
cryptoLoadGeneration++
|
||||||
cryptoWsUnsubscribe?.()
|
cryptoWsUnsubscribe?.()
|
||||||
cryptoWsUnsubscribe = null
|
cryptoWsUnsubscribe = null
|
||||||
chartYesNoLoading.value = true
|
chartYesNoLoading.value = true
|
||||||
@ -1388,7 +1514,8 @@ watch(
|
|||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (!chartInstance || !chartContainerRef.value) return
|
if (!chartInstance || !chartContainerRef.value) return
|
||||||
chartInstance.resize(chartContainerRef.value.clientWidth, 320)
|
const el = chartContainerRef.value
|
||||||
|
chartInstance.resize(el.clientWidth, el.clientHeight || 320)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -1443,10 +1570,15 @@ onUnmounted(() => {
|
|||||||
unsubscribePositionUpdate()
|
unsubscribePositionUpdate()
|
||||||
cryptoWsUnsubscribe?.()
|
cryptoWsUnsubscribe?.()
|
||||||
cryptoWsUnsubscribe = null
|
cryptoWsUnsubscribe = null
|
||||||
|
if (cryptoScrollToRtRaf) {
|
||||||
|
cancelAnimationFrame(cryptoScrollToRtRaf)
|
||||||
|
cryptoScrollToRtRaf = 0
|
||||||
|
}
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
chartInstance?.remove()
|
chartInstance?.remove()
|
||||||
chartInstance = null
|
chartInstance = null
|
||||||
chartSeries = null
|
chartSeries = null
|
||||||
|
inPanePriceScale = null
|
||||||
disconnectClob()
|
disconnectClob()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -164,12 +164,7 @@
|
|||||||
>
|
>
|
||||||
<div class="design-order-top">
|
<div class="design-order-top">
|
||||||
<div class="order-mobile-icon" :class="ord.iconClass">
|
<div class="order-mobile-icon" :class="ord.iconClass">
|
||||||
<img
|
<img v-if="ord.imageUrl" :src="ord.imageUrl" alt="" class="position-icon-img" />
|
||||||
v-if="ord.imageUrl"
|
|
||||||
:src="ord.imageUrl"
|
|
||||||
alt=""
|
|
||||||
class="position-icon-img"
|
|
||||||
/>
|
|
||||||
<span v-else class="position-icon-char">{{ ord.iconChar || '•' }}</span>
|
<span v-else class="position-icon-char">{{ ord.iconChar || '•' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-mobile-main design-order-title-col">
|
<div class="order-mobile-main design-order-title-col">
|
||||||
@ -182,9 +177,11 @@
|
|||||||
<span class="order-side-pill" :class="getOrderSideClass(ord)">{{
|
<span class="order-side-pill" :class="getOrderSideClass(ord)">{{
|
||||||
getOrderActionLabel(ord)
|
getOrderActionLabel(ord)
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="order-outcome-pill" :class="ord.side === 'Yes' ? 'outcome-yes' : 'outcome-no'">{{
|
<span
|
||||||
ord.side
|
class="order-outcome-pill"
|
||||||
}}</span>
|
:class="ord.side === 'Yes' ? 'outcome-yes' : 'outcome-no'"
|
||||||
|
>{{ ord.side }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<v-btn
|
<v-btn
|
||||||
@ -267,9 +264,8 @@
|
|||||||
{{ getHistoryTagLabel(h) }}
|
{{ getHistoryTagLabel(h) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="design-history-meta"
|
<span class="design-history-meta"
|
||||||
>{{ t('wallet.priceLabel') }}: {{ h.avgPrice || '—' }} · {{ t('wallet.sharesLabel') }}: {{
|
>{{ t('wallet.priceLabel') }}: {{ h.avgPrice || '—' }} ·
|
||||||
h.shares || '—'
|
{{ t('wallet.sharesLabel') }}: {{ h.shares || '—' }}</span
|
||||||
}}</span
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -280,7 +276,11 @@
|
|||||||
<span v-else class="position-icon-char">{{ getFundingIconText(h) }}</span>
|
<span v-else class="position-icon-char">{{ getFundingIconText(h) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="history-mobile-main">
|
<div class="history-mobile-main">
|
||||||
<MarqueeTitle :text="getFundingTitle(h)" title-class="history-mobile-title" fast />
|
<MarqueeTitle
|
||||||
|
:text="getFundingTitle(h)"
|
||||||
|
title-class="history-mobile-title"
|
||||||
|
fast
|
||||||
|
/>
|
||||||
<div class="history-mobile-activity">{{ h.timeAgo || '—' }}</div>
|
<div class="history-mobile-activity">{{ h.timeAgo || '—' }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -441,7 +441,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="sell-dialog-title">
|
<h3 class="sell-dialog-title">
|
||||||
{{ t('wallet.sellDialogTitle', { outcome: sellPositionItem.sellOutcome || t('wallet.position') }) }}
|
{{
|
||||||
|
t('wallet.sellDialogTitle', {
|
||||||
|
outcome: sellPositionItem.sellOutcome || t('wallet.position'),
|
||||||
|
})
|
||||||
|
}}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="sell-dialog-market">{{ sellPositionItem.market }}</p>
|
<p class="sell-dialog-market">{{ sellPositionItem.market }}</p>
|
||||||
<div class="sell-receive-box">
|
<div class="sell-receive-box">
|
||||||
@ -456,7 +460,9 @@
|
|||||||
<v-btn color="success" variant="flat" block class="sell-redeem-btn" @click="redeemSell">
|
<v-btn color="success" variant="flat" block class="sell-redeem-btn" @click="redeemSell">
|
||||||
{{ t('wallet.sellDialogRedeem') }}
|
{{ t('wallet.sellDialogRedeem') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<a href="#" class="sell-edit-link" @click.prevent="editSellOrder">{{ t('wallet.sellDialogEditOrder') }}</a>
|
<a href="#" class="sell-edit-link" @click.prevent="editSellOrder">{{
|
||||||
|
t('wallet.sellDialogEditOrder')
|
||||||
|
}}</a>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
@ -687,10 +693,7 @@ interface HistoryItem {
|
|||||||
detailMarketId?: string
|
detailMarketId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function canOpenTradeDetail(opts: {
|
function canOpenTradeDetail(opts: { tradeEventId?: string; tradeEventSlug?: string }): boolean {
|
||||||
tradeEventId?: string
|
|
||||||
tradeEventSlug?: string
|
|
||||||
}): boolean {
|
|
||||||
return !!(opts.tradeEventId?.trim() || opts.tradeEventSlug?.trim())
|
return !!(opts.tradeEventId?.trim() || opts.tradeEventSlug?.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -792,7 +795,9 @@ function isFundingHistory(h: HistoryItem): boolean {
|
|||||||
|
|
||||||
function getFundingTitle(h: HistoryItem): string {
|
function getFundingTitle(h: HistoryItem): string {
|
||||||
if (h.market?.trim()) return h.market
|
if (h.market?.trim()) return h.market
|
||||||
return isWithdrawalHistory(h) ? t('wallet.btcWithdrawHistoryLabel') : t('wallet.usdtDepositHistoryLabel')
|
return isWithdrawalHistory(h)
|
||||||
|
? t('wallet.btcWithdrawHistoryLabel')
|
||||||
|
: t('wallet.usdtDepositHistoryLabel')
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFundingIconText(h: HistoryItem): string {
|
function getFundingIconText(h: HistoryItem): string {
|
||||||
@ -1062,8 +1067,10 @@ function getWithdrawAmountText(w: SettlementRequestClientItem): string {
|
|||||||
function getWithdrawFeeText(w: SettlementRequestClientItem): string {
|
function getWithdrawFeeText(w: SettlementRequestClientItem): string {
|
||||||
const feeRaw = Number(
|
const feeRaw = Number(
|
||||||
(w as { fee?: string | number; gasFee?: string | number; serviceFee?: string | number }).fee ??
|
(w as { fee?: string | number; gasFee?: string | number; serviceFee?: string | number }).fee ??
|
||||||
(w as { fee?: string | number; gasFee?: string | number; serviceFee?: string | number }).gasFee ??
|
(w as { fee?: string | number; gasFee?: string | number; serviceFee?: string | number })
|
||||||
(w as { fee?: string | number; gasFee?: string | number; serviceFee?: string | number }).serviceFee ??
|
.gasFee ??
|
||||||
|
(w as { fee?: string | number; gasFee?: string | number; serviceFee?: string | number })
|
||||||
|
.serviceFee ??
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
const feeText = Number.isFinite(feeRaw)
|
const feeText = Number.isFinite(feeRaw)
|
||||||
@ -1375,7 +1382,7 @@ function initPlChart() {
|
|||||||
bottomColor: color + '08',
|
bottomColor: color + '08',
|
||||||
lineColor: color,
|
lineColor: color,
|
||||||
lineWidth: 2,
|
lineWidth: 2,
|
||||||
lineType: LineType.Simple,
|
lineType: LineType.Curved,
|
||||||
lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate,
|
lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate,
|
||||||
priceFormat: { type: 'price', precision: 2 },
|
priceFormat: { type: 'price', precision: 2 },
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user