@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
22import { Spinner , Text , makeStyles , mergeClasses } from '@fluentui/react-components'
33import { EditRegular } from '@fluentui/react-icons'
44import { research } from '../api/client'
5+ import { quoteDtoToCrossMarket } from './instrument-adapters'
56import type { CryptoSnapshotData , UsSnapshotData , WatchlistItem } from '../types/market'
67import type { InstrumentRef } from '../types/instrument'
78import {
@@ -25,6 +26,7 @@ import { hasApplicationCapability } from './capabilities'
2526import { isWatchlistItemWithinQuoteGrace } from './watchlistQuotes'
2627import TradingViewChart from './TradingViewChart'
2728import { DETAIL_PANEL_CHART_MAX_HEIGHT_PX } from './chartViewConfig'
29+ import { mergeSnapshotPreserveQuote } from './detailSnapshotUtils'
2830import { opptrixTokens , opptrixCssVars } from '../theme/tokens'
2931import { ghostInteractive } from '../theme/mixins'
3032import { listRowKey } from '../utils/listRowKey'
@@ -269,25 +271,33 @@ function MiniKline({
269271 )
270272}
271273
272- async function loadSnapshot ( ref : InstrumentRef ) : Promise < EquityDetail | CryptoSnapshotData > {
274+ async function loadSnapshot (
275+ ref : InstrumentRef ,
276+ opts ?: { fresh ?: boolean } ,
277+ ) : Promise < EquityDetail | CryptoSnapshotData > {
273278 if ( ! hasApplicationCapability ( ref , 'snapshot' ) ) {
274279 throw new Error ( '该标的暂不支持快照' )
275280 }
276- const resp = await research . instrumentSnapshot ( ref )
281+ const resp = await research . instrumentSnapshot ( ref , { fresh : opts ?. fresh } )
277282 if ( ! resp . success || ! resp . data || typeof resp . data !== 'object' ) {
278283 throw new Error ( resp . message || SNAPSHOT_LOAD_ERROR_COPY )
279284 }
280- return resp . data as EquityDetail | CryptoSnapshotData
281- }
282-
283- /** 刷新成功但新 quote 为空时保留上一份,避免摘要闪没 */
284- function mergeSnapshotPreserveQuote (
285- prev : EquityDetail | CryptoSnapshotData | null ,
286- next : EquityDetail | CryptoSnapshotData ,
287- ) : EquityDetail | CryptoSnapshotData {
288- if ( next . quote != null ) return next
289- if ( prev ?. quote == null ) return next
290- return { ...next , quote : prev . quote }
285+ const data = resp . data as EquityDetail | CryptoSnapshotData
286+ if ( ! data . quote && ( ref . market === 'US' || ref . market === 'HK' ) ) {
287+ try {
288+ const quoteResp = await research . instrumentQuote ( ref , { fresh : true } )
289+ const q = quoteResp . success && quoteResp . data ?. quote
290+ ? quoteResp . data . quote
291+ : null
292+ if ( q ) {
293+ return {
294+ ...data ,
295+ quote : quoteDtoToCrossMarket ( q ) ,
296+ }
297+ }
298+ } catch { /* ignore */ }
299+ }
300+ return data
291301}
292302
293303function detailFootnote ( ref : InstrumentRef , quote : { quoteSession ?: string ; sessionLabel ?: string } | null ) : string {
@@ -346,34 +356,43 @@ export default function CrossMarketSnapshotDetail({
346356 const isCrypto = ref ?. market === 'CRYPTO'
347357 const isEquity = ref ?. market === 'US' || ref ?. market === 'HK'
348358
349- const [ snapshot , setSnapshot ] = useState < EquityDetail | CryptoSnapshotData | null > ( null )
359+ const [ snapshotByKey , setSnapshotByKey ] = useState < Record < string , EquityDetail | CryptoSnapshotData > > ( { } )
350360 const [ fetching , setFetching ] = useState ( false )
351361 const [ error , setError ] = useState < string | null > ( null )
352362 const [ , setGraceTick ] = useState ( 0 )
353363 const loadSeqRef = useRef ( 0 )
354- const snapshotRef = useRef < EquityDetail | CryptoSnapshotData | null > ( null )
355364 const initialRetryRef = useRef ( 0 )
356- snapshotRef . current = snapshot
365+ const freshLoadedRef = useRef < Set < string > > ( new Set ( ) )
366+ const snapshotByKeyRef = useRef ( snapshotByKey )
367+ snapshotByKeyRef . current = snapshotByKey
368+ const snapshot = snapshotByKey [ instrumentIdentity ] ?? null
357369
358- const load = useCallback ( async ( ) => {
370+ const load = useCallback ( async ( opts ?: { fresh ?: boolean } ) => {
359371 if ( ! ref ) return
360372 const seq = ++ loadSeqRef . current
361373 setFetching ( true )
362374 let scheduleRetry = false
363375 try {
364- const data = await loadSnapshot ( ref )
376+ const data = await loadSnapshot ( ref , { fresh : opts ?. fresh } )
365377 if ( seq !== loadSeqRef . current ) return
366- setSnapshot ( prev => mergeSnapshotPreserveQuote ( prev , data ) )
378+ setSnapshotByKey ( prev => {
379+ const prior = prev [ instrumentIdentity ] ?? null
380+ return {
381+ ...prev ,
382+ [ instrumentIdentity ] : mergeSnapshotPreserveQuote ( prior , data ) ,
383+ }
384+ } )
367385 setError ( null )
368386 initialRetryRef . current = 0
387+ if ( opts ?. fresh ) freshLoadedRef . current . add ( instrumentIdentity )
369388 } catch ( e ) {
370389 if ( seq !== loadSeqRef . current ) return
371- const hadQuote = snapshotRef . current ?. quote != null
390+ const hadQuote = snapshotByKeyRef . current [ instrumentIdentity ] ?. quote != null
372391 if ( ! hadQuote && initialRetryRef . current < SNAPSHOT_INITIAL_MAX_RETRIES ) {
373392 initialRetryRef . current += 1
374393 scheduleRetry = true
375394 window . setTimeout ( ( ) => {
376- void load ( )
395+ void load ( { fresh : true } )
377396 } , SNAPSHOT_INITIAL_RETRY_MS * initialRetryRef . current )
378397 return
379398 }
@@ -383,22 +402,22 @@ export default function CrossMarketSnapshotDetail({
383402 setFetching ( false )
384403 }
385404 }
386- } , [ ref ] )
405+ } , [ ref , instrumentIdentity ] )
387406
388407 useEffect ( ( ) => {
389- setSnapshot ( null )
390- setError ( null )
391- initialRetryRef . current = 0
392408 loadSeqRef . current += 1
409+ initialRetryRef . current = 0
410+ setError ( null )
393411 } , [ instrumentIdentity ] )
394412
395413 useEffect ( ( ) => {
396414 if ( ! ref ) return undefined
397- void load ( )
415+ const fresh = ! freshLoadedRef . current . has ( instrumentIdentity )
416+ void load ( { fresh } )
398417 const ms = isCrypto ? 30_000 : 90_000
399- const timer = window . setInterval ( ( ) => { void load ( ) } , ms )
418+ const timer = window . setInterval ( ( ) => { void load ( { fresh : false } ) } , ms )
400419 return ( ) => window . clearInterval ( timer )
401- } , [ load , isCrypto , instrumentIdentity ] )
420+ } , [ load , isCrypto , instrumentIdentity , ref ] )
402421
403422 useEffect ( ( ) => {
404423 if ( ! isWatchlistItemWithinQuoteGrace ( stock ) ) return undefined
0 commit comments