Files
stock/refer/frontend/src/components/StockIntradayChart.tsx
T
2026-07-04 15:59:20 +08:00

121 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Loader2 } from 'lucide-react'
import { api, type MinuteKlineRow } from '@/lib/api'
import { QK } from '@/lib/queryKeys'
import { EChartsIntraday } from '@/components/EChartsIntraday'
interface Props {
symbol: string
date: string | null
height?: number
prevClose?: number
className?: string
onPriceHover?: (price: number | null) => void
}
export function StockIntradayChart({
symbol,
date,
height = 520,
prevClose,
className,
onPriceHover,
}: Props) {
const qc = useQueryClient()
const [minuteDismissed, setMinuteDismissed] = useState(false)
const minute = useQuery({
queryKey: QK.klineMinute(symbol, date ?? ''),
queryFn: () => api.klineMinute(symbol, date ?? undefined),
enabled: !!symbol && !!date,
})
const fetchMinute = useMutation({
mutationFn: () => api.extendMinuteHistory(5, 'day'),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['kline-minute', symbol] })
qc.invalidateQueries({ queryKey: QK.dataStatus })
qc.invalidateQueries({ queryKey: QK.pipelineJobs })
setMinuteDismissed(false)
},
})
const minuteRows: MinuteKlineRow[] = useMemo(() => minute.data?.rows ?? [], [minute.data?.rows])
// source=none 表示本地无数据且 TickFlow 也拉不到 (停牌/复牌延迟/非交易日)
// 此时不弹"是否获取"询问窗, 只做静态提示, 避免误导用户去拉明知拉不到的数据
const sourceIsNone = minute.data?.source === 'none'
useEffect(() => {
setMinuteDismissed(false)
onPriceHover?.(null)
}, [date, onPriceHover])
if (!symbol || !date) return null
return (
<div className={className} style={{ height, flexShrink: 0 }}>
{minute.isLoading && <div className="text-xs text-muted py-2">分时加载中…</div>}
{!minute.isLoading && minuteRows.length === 0 && (
<>
{fetchMinute.isPending ? (
<div className="flex items-center justify-center h-full gap-2 text-xs text-accent">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
<span>正在获取最近5日分钟K</span>
</div>
) : sourceIsNone ? (
// 数据源确认无此日分钟数据 (停牌/复牌延迟等): 静态提示 + 保留重试
<div className="flex flex-col items-center justify-center h-full gap-3">
<div className="text-xs text-muted">该日暂无分钟数据(数据源未提供)</div>
<button
onClick={() => fetchMinute.mutate()}
className="px-4 py-1.5 rounded-btn bg-elevated text-secondary text-xs font-medium hover:bg-elevated/80 transition-colors duration-150"
>
重新获取
</button>
</div>
) : minuteDismissed ? (
<div className="flex flex-col items-center justify-center h-full gap-3">
<div className="text-xs text-muted">暂无分钟数据</div>
<button
onClick={() => setMinuteDismissed(false)}
className="px-4 py-1.5 rounded-btn bg-accent/90 text-base text-xs font-medium hover:bg-accent transition-colors duration-150"
>
获取分钟K
</button>
</div>
) : (
<div className="flex flex-col items-center justify-center h-full gap-4">
<div className="text-sm text-foreground">是否立即获取最近5日分钟K</div>
<div className="flex items-center gap-3">
<button
onClick={() => fetchMinute.mutate()}
className="px-4 py-1.5 rounded-btn bg-accent/90 text-base text-xs font-medium hover:bg-accent transition-colors duration-150"
>
确定
</button>
<button
onClick={() => setMinuteDismissed(true)}
className="px-4 py-1.5 rounded-btn bg-elevated text-secondary text-xs hover:bg-elevated/80 transition-colors duration-150"
>
取消
</button>
</div>
</div>
)}
</>
)}
{minuteRows.length > 0 && (
<EChartsIntraday
data={minuteRows}
height={height}
prevClose={prevClose}
date={date}
symbol={symbol}
onPriceHover={onPriceHover}
/>
)}
</div>
)
}