Skip to content

Commit 8f7bc9e

Browse files
authored
Merge pull request #171 from Travisun/fix/us-tickflow-detail-panel
fix(market): TickFlow K-line periods and HK symbol canonicalization
2 parents fd4136d + 7d62dc6 commit 8f7bc9e

41 files changed

Lines changed: 1416 additions & 1976 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

client-ui/src/market/CrossMarketSnapshotDetail.tsx

Lines changed: 48 additions & 681 deletions
Large diffs are not rendered by default.

client-ui/src/market/StockDetailTab.tsx

Lines changed: 35 additions & 732 deletions
Large diffs are not rendered by default.

client-ui/src/market/TradingViewChart.tsx

Lines changed: 46 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
1+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
22
import { Spinner, Text, makeStyles, mergeClasses } from '@fluentui/react-components'
33
import { research } from '../api/client'
44
import { instrumentKey, parseInstrumentInput } from './instrument'
55
import type { InstrumentRef } from '../types/instrument'
66
import { hasApplicationCapability } from './capabilities'
77
import type { ChartPeriod, OhlcChartBar, StockChartData } from '../types/market'
88
import { ChartWorkspace } from './chartEngine'
9-
import { buildChartSeries, periodLabel } from './chartSeries'
9+
import { buildChartSeries, isLineChartView, periodLabel } from './chartSeries'
10+
import { buildChartPeriodOptions, CN_STOCK_CHART_PERIODS } from './chartPeriodOptions'
1011
import { initialFetchCount, LOAD_MORE_STEP, maxChartBars } from './chartViewConfig'
11-
import { isIntradayPeriod, isMinuteOhlcPeriod } from './chartTime'
12+
import { isLineChartPaneLabel } from './chartTime'
1213
import { chartLivePollIntervalMs, shouldPollChartLive } from './chartLiveRefresh'
1314
import CyqProfileStrip from './CyqProfileStrip'
1415
import { computeCyqPriceSpan, isCyqChartPeriod } from './cyqUtils'
@@ -18,28 +19,6 @@ import { useTheme } from '../theme/ThemeContext'
1819
import { ghostInteractive } from '../theme/mixins'
1920
import { isUnifiedChart, unifiedChartToStockChart } from './instrument-adapters'
2021

21-
const PERIODS: { id: ChartPeriod; label: string; tradingOnly?: boolean }[] = [
22-
{ id: 'intraday', label: '分时', tradingOnly: true },
23-
{ id: '1m', label: '1分' },
24-
{ id: '5m', label: '5分' },
25-
{ id: '15m', label: '15分' },
26-
{ id: '30m', label: '30分' },
27-
{ id: '60m', label: '60分' },
28-
{ id: 'daily', label: '日K' },
29-
{ id: 'weekly', label: '周K' },
30-
{ id: 'monthly', label: '月K' },
31-
]
32-
33-
const CROSS_MARKET_PERIOD_OPTIONS: { id: ChartPeriod; label: string; tradingOnly?: boolean }[] = [
34-
{ id: 'daily', label: '日K' },
35-
{ id: '5day', label: '5日' },
36-
{ id: 'weekly', label: '周K' },
37-
{ id: 'monthly', label: '月K' },
38-
{ id: 'year1', label: '1年' },
39-
{ id: 'year3', label: '3年' },
40-
{ id: 'year5', label: '5年' },
41-
]
42-
4322
const useStyles = makeStyles({
4423
root: {
4524
display: 'flex',
@@ -323,34 +302,17 @@ export default function TradingViewChart({ code, instrument, expanded = false, a
323302
&& (hasApplicationCapability(instrumentRef, 'chart_intraday')
324303
|| hasApplicationCapability(instrumentRef, 'chart_daily'))
325304
const canChart = cnEquityChart || crossMarketChart
326-
const cnIntraday = cnEquityChart
327-
&& hasApplicationCapability(instrumentRef, 'chart_intraday')
328-
const crossMarketIntraday = crossMarketChart
329-
&& hasApplicationCapability(instrumentRef, 'chart_intraday')
330-
const periodOptions = useMemo(() => {
331-
if (cnEquityChart) {
332-
if (cnIntraday) return PERIODS
333-
// CN ETF 等仅有日 K:不展示分时/分钟周期,避免整图报「不支持」
334-
return PERIODS.filter(item => item.id === 'daily' || item.id === 'weekly' || item.id === 'monthly')
335-
}
336-
if (crossMarketChart) {
337-
const options: { id: ChartPeriod; label: string; tradingOnly?: boolean }[] = []
338-
if (hasApplicationCapability(instrumentRef, 'chart_intraday')) {
339-
options.push({ id: 'intraday', label: '分时', tradingOnly: true })
340-
}
341-
options.push(...CROSS_MARKET_PERIOD_OPTIONS)
342-
return options
343-
}
344-
return PERIODS.filter(item => item.id === 'daily' || item.id === 'weekly' || item.id === 'monthly')
345-
}, [cnEquityChart, cnIntraday, crossMarketChart, instrumentRef])
305+
const periodOptions = useMemo(
306+
() => buildChartPeriodOptions(instrumentRef, { cnEquityChart, crossMarketChart }),
307+
[instrumentRef, cnEquityChart, crossMarketChart],
308+
)
346309
const { resolvedScheme } = useTheme()
347310
const maColors = useMemo(() => getMaColors(resolvedScheme), [resolvedScheme])
348311
const [period, setPeriod] = useState<ChartPeriod>('daily')
349312
const [data, setData] = useState<StockChartData | null>(null)
350313
const [loading, setLoading] = useState(false)
351314
const [refreshing, setRefreshing] = useState(false)
352315
const [error, setError] = useState('')
353-
const [intradayAvailable, setIntradayAvailable] = useState(true)
354316

355317
const mainRef = useRef<HTMLDivElement>(null)
356318
const volumeRef = useRef<HTMLDivElement>(null)
@@ -408,9 +370,7 @@ export default function TradingViewChart({ code, instrument, expanded = false, a
408370
return
409371
}
410372

411-
const useStockApi = cnEquityChart
412-
&& (isIntradayPeriod(nextPeriod) || isMinuteOhlcPeriod(nextPeriod)
413-
|| nextPeriod === 'daily' || nextPeriod === 'weekly' || nextPeriod === 'monthly')
373+
const useStockApi = cnEquityChart && CN_STOCK_CHART_PERIODS.has(nextPeriod)
414374

415375
const resp = useStockApi
416376
? await research.stockChart(
@@ -440,15 +400,6 @@ export default function TradingViewChart({ code, instrument, expanded = false, a
440400
if (!hasChart) setData(null)
441401
return
442402
}
443-
if (nextPeriod === 'intraday') {
444-
const ok = resp.data.bars.length > 0
445-
setIntradayAvailable(ok)
446-
if (!ok) {
447-
setError(resp.message || '暂无分时数据')
448-
if (!hasChart) setData(null)
449-
return
450-
}
451-
}
452403
if (opts?.append && prevBarCountRef.current > 0) {
453404
addedBarsRef.current = resp.data.bars.length - prevBarCountRef.current
454405
} else if (!opts?.append && !isLive) {
@@ -458,10 +409,10 @@ export default function TradingViewChart({ code, instrument, expanded = false, a
458409
addedBarsRef.current = 0
459410
}
460411
fetchCountRef.current = count
461-
const chartData = isUnifiedChart(resp.data)
412+
const rawChart = isUnifiedChart(resp.data)
462413
? unifiedChartToStockChart(resp.data, instrumentRef.symbol)
463414
: resp.data
464-
setData(chartData)
415+
setData({ ...rawChart, period: nextPeriod })
465416
} catch (e) {
466417
if (seq !== loadSeqRef.current || signal?.aborted) return
467418
if (e instanceof Error && e.name !== 'AbortError') {
@@ -500,20 +451,30 @@ export default function TradingViewChart({ code, instrument, expanded = false, a
500451
const handleNeedHistoryRef = useRef(handleNeedHistory)
501452
handleNeedHistoryRef.current = handleNeedHistory
502453

454+
const prevInstrumentIdentityRef = useRef(instrumentIdentity)
455+
503456
useEffect(() => {
504-
setPeriod('daily')
457+
const instrumentChanged = prevInstrumentIdentityRef.current !== instrumentIdentity
458+
prevInstrumentIdentityRef.current = instrumentIdentity
459+
460+
if (instrumentChanged) {
461+
setPeriod('daily')
462+
loadSeqRef.current += 1
463+
}
464+
465+
dataRef.current = null
466+
hasDataRef.current = false
505467
setData(null)
468+
setLoading(true)
469+
setRefreshing(false)
506470
setError('')
507-
fetchCountRef.current = initialFetchCount('daily')
508-
loadSeqRef.current += 1
509-
}, [code])
510471

511-
useEffect(() => {
512-
fetchCountRef.current = initialFetchCount(period)
472+
const loadPeriod: ChartPeriod = instrumentChanged ? 'daily' : period
473+
fetchCountRef.current = initialFetchCount(loadPeriod)
513474
const controller = new AbortController()
514-
void loadChart(period, fetchCountRef.current, controller.signal)
475+
void loadChart(loadPeriod, fetchCountRef.current, controller.signal)
515476
return () => { controller.abort() }
516-
}, [code, period, loadChart])
477+
}, [instrumentIdentity, period, loadChart])
517478

518479
useEffect(() => {
519480
const tradingDay = data?.isTradingDay
@@ -540,27 +501,9 @@ export default function TradingViewChart({ code, instrument, expanded = false, a
540501
return () => { window.clearInterval(id) }
541502
}, [period, active, data?.isTradingDay, loadChart])
542503

543-
useEffect(() => {
544-
if (!cnIntraday && !crossMarketIntraday) {
545-
setIntradayAvailable(false)
546-
if (isIntradayPeriod(period) || isMinuteOhlcPeriod(period)) setPeriod('daily')
547-
return undefined
548-
}
549-
const controller = new AbortController()
550-
const probe = cnIntraday
551-
? research.stockChart(instrumentRef, 'intraday', undefined, controller.signal)
552-
: research.instrumentChart(instrumentRef, 'intraday', 1, controller.signal)
553-
probe
554-
.then(resp => {
555-
if (controller.signal.aborted || !resp.success || !resp.data) return
556-
setIntradayAvailable(resp.data.bars.length > 0)
557-
})
558-
.catch(() => {})
559-
return () => { controller.abort() }
560-
}, [code, instrumentRef, cnIntraday, crossMarketIntraday, period])
561-
562-
useEffect(() => {
563-
if (!data || !mainRef.current || !volumeRef.current || !macdRef.current) return undefined
504+
useLayoutEffect(() => {
505+
if (!data || data.bars.length === 0 || data.period !== period) return undefined
506+
if (!mainRef.current || !volumeRef.current) return undefined
564507

565508
const workspace = workspaceRef.current
566509
const preserveRange = preserveRangeRef.current
@@ -578,23 +521,23 @@ export default function TradingViewChart({ code, instrument, expanded = false, a
578521
},
579522
series,
580523
{
581-
period: data.period,
524+
period,
582525
colorScheme: resolvedScheme,
583526
chartTimeZone: data.chartTimeZone,
584527
preserveRange,
585528
addedBars,
586-
// 经 ref 取最新回调,避免 handleNeedHistory 身份变化时 destroy/remount
587529
onNeedHistory: () => { handleNeedHistoryRef.current() },
588530
},
589531
)
532+
requestAnimationFrame(() => { workspace.resize() })
590533
setError(prev => (prev.startsWith('K线') || prev.includes('渲染') || prev.includes('时间轴') ? '' : prev))
591534
} catch (e) {
592535
workspace.destroy()
593536
setError(e instanceof Error ? e.message : '图表渲染失败')
594537
}
595538

596539
return () => { workspace.destroy() }
597-
}, [data, resolvedScheme])
540+
}, [data, period, resolvedScheme])
598541

599542
useEffect(() => {
600543
if (!active || !data) return undefined
@@ -604,16 +547,17 @@ export default function TradingViewChart({ code, instrument, expanded = false, a
604547
return () => { cancelAnimationFrame(id) }
605548
}, [active, data, expanded])
606549

550+
const paneMainLabel = isLineChartPaneLabel(period) ? '分' : 'K'
551+
const lineChartView = Boolean(data && data.period === period && isLineChartView(period, data.bars))
607552
const showMacd = Boolean(
608-
data && !isIntradayPeriod(data.period) && !isMinuteOhlcPeriod(data.period)
553+
data && !lineChartView
609554
&& data.indicators.some(row => row.macd != null),
610555
)
611-
const intraday = data ? isIntradayPeriod(data.period) : isIntradayPeriod(period)
612556

613557
useEffect(() => () => { workspaceRef.current.destroy() }, [])
614558

615-
const legendIntraday = intraday && data
616-
const legendOhlc = !intraday && data
559+
const legendLine = lineChartView
560+
const legendOhlc = !lineChartView && data
617561
const cyqLatest = data?.cyqLatest ?? null
618562
const cyqProfile = data?.cyqProfile ?? null
619563
const showCyq = Boolean(
@@ -630,9 +574,9 @@ export default function TradingViewChart({ code, instrument, expanded = false, a
630574

631575
const resetZoom = () => { workspaceRef.current.resetView() }
632576

633-
const chartLegend = (legendIntraday || legendOhlc) && (
577+
const chartLegend = (legendLine || legendOhlc) && (
634578
<div className={mergeClasses(s.legend, s.chartLegend)}>
635-
{legendIntraday && (
579+
{legendLine && (
636580
<>
637581
<span className={s.legendItem}><i className={s.dot} style={{ background: '#FF3B30' }} />价格</span>
638582
<span className={s.legendItem}><i className={s.dot} style={{ background: indicatorColors.avg }} />均价</span>
@@ -666,8 +610,7 @@ export default function TradingViewChart({ code, instrument, expanded = false, a
666610
<div className={s.toolbar}>
667611
<div className={s.periodGroup}>
668612
{periodOptions.map(item => {
669-
const disabled = (!cnEquityChart && !crossMarketChart && (isIntradayPeriod(item.id) || isMinuteOhlcPeriod(item.id)))
670-
|| (item.tradingOnly && !intradayAvailable && item.id === 'intraday')
613+
const disabled = !cnEquityChart && !crossMarketChart
671614
const activeTab = period === item.id
672615
return (
673616
<button
@@ -686,11 +629,7 @@ export default function TradingViewChart({ code, instrument, expanded = false, a
686629

687630
<div className={s.zoomRow}>
688631
<Text className={s.hint}>
689-
{intraday
690-
? (data?.sessionDate && !data.isTradingDay
691-
? `${data.sessionDate} 收盘分时 · 滚轮缩放 · 左拖查看更早`
692-
: '默认显示最新时段 · 滚轮缩放 · 左拖查看更早')
693-
: `默认显示最新 ${periodLabel(period)} · 滚轮缩放 · 左拖加载历史`}
632+
默认显示最新 {periodLabel(period)} · 滚轮缩放 · 左拖加载历史
694633
</Text>
695634
<button type="button" className={s.zoomBtn} onClick={resetZoom} disabled={!data}>
696635
最近视图
@@ -742,7 +681,7 @@ export default function TradingViewChart({ code, instrument, expanded = false, a
742681
<div className={mergeClasses(s.empty, expanded && s.emptyExpanded)}>{error}</div>
743682
)}
744683

745-
<div className={mergeClasses(s.chartArea, expanded && s.chartAreaExpanded, !data && s.paneHidden)}>
684+
<div className={mergeClasses(s.chartArea, expanded && s.chartAreaExpanded, !canChart && s.paneHidden)}>
746685
<div className={mergeClasses(s.chartFrame, expanded && s.chartFrameExpanded)}>
747686
{refreshing && (
748687
<div className={s.chartOverlay}>
@@ -752,7 +691,7 @@ export default function TradingViewChart({ code, instrument, expanded = false, a
752691

753692
<div className={mergeClasses(s.chartStack, expanded && s.chartStackExpanded)}>
754693
<div className={mergeClasses(s.paneRow, expanded && s.paneRowExpanded)}>
755-
<span className={s.paneLabel}>{intraday ? '分' : 'K'}</span>
694+
<span className={s.paneLabel}>{paneMainLabel}</span>
756695
<div className={s.paneKSplit}>
757696
<div className={mergeClasses(s.panePlot, expanded ? s.paneMainExpanded : s.paneMain)} ref={mainRef} />
758697
{showCyq && cyqProfile && cyqLatest && cyqPriceSpan && (

0 commit comments

Comments
 (0)