1- import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
1+ import { useCallback , useEffect , useLayoutEffect , useMemo , useRef , useState } from 'react'
22import { Spinner , Text , makeStyles , mergeClasses } from '@fluentui/react-components'
33import { research } from '../api/client'
44import { instrumentKey , parseInstrumentInput } from './instrument'
55import type { InstrumentRef } from '../types/instrument'
66import { hasApplicationCapability } from './capabilities'
77import type { ChartPeriod , OhlcChartBar , StockChartData } from '../types/market'
88import { 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'
1011import { initialFetchCount , LOAD_MORE_STEP , maxChartBars } from './chartViewConfig'
11- import { isIntradayPeriod , isMinuteOhlcPeriod } from './chartTime'
12+ import { isLineChartPaneLabel } from './chartTime'
1213import { chartLivePollIntervalMs , shouldPollChartLive } from './chartLiveRefresh'
1314import CyqProfileStrip from './CyqProfileStrip'
1415import { computeCyqPriceSpan , isCyqChartPeriod } from './cyqUtils'
@@ -18,28 +19,6 @@ import { useTheme } from '../theme/ThemeContext'
1819import { ghostInteractive } from '../theme/mixins'
1920import { 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-
4322const 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