import { useState } from 'react' import { useQuery } from '@tanstack/react-query' import { Sparkles, LineChart, History as HistoryIcon, Loader2, ExternalLink, Bell } from 'lucide-react' import { PageHeader } from '@/components/PageHeader' import { EmptyState } from '@/components/EmptyState' import { StockFinancialSearch } from '@/components/financials/StockFinancialSearch' import { StockPreviewDialog } from '@/components/StockPreviewDialog' import { LastStockChip } from '@/components/LastStockChip' import { AnalysisKChart, type PriceLevel, type LevelType } from '@/components/stock-analysis/AnalysisKChart' import { api } from '@/lib/api' import { useLastStock } from '@/lib/useLastStock' import { QK } from '@/lib/queryKeys' import { toast } from '@/components/Toast' import { startAnalysis, findTodayReport, useHistoryReports, deleteReport, openHistoryReport, } from '@/lib/stockAnalysisStore' /** * 个股分析页 —— 日 K + 关键价位(压力/支撑/密集区/枢轴/前高前低)+ AI 四维分析。 * * 与财务分析页的区别: * - 以【行情 + 关键价位】为视觉主体(专用日 K 图表,不复用个股对话框图表) * - AI 分析输出买卖区间 / 操作建议(非财务质量评级) * - 报告胶囊用蓝色系,与财务分析(紫色)并存 */ export function StockAnalysis() { const [symbol, setSymbol] = useState('') const [name, setName] = useState('') const [checking, setChecking] = useState(false) const [confirmReport, setConfirmReport] = useState<{ id: string; created_at: string; focus: string } | null>(null) const [showHistory, setShowHistory] = useState(false) const [previewSymbol, setPreviewSymbol] = useState(null) const { last: lastStock, remember: rememberStock } = useLastStock('stock-analysis') const onSelect = (sym: string, nm: string) => { setSymbol(sym) setName(nm) setShowHistory(false) setConfirmReport(null) rememberStock(sym, nm) } const handleAnalyze = async () => { if (!symbol || checking) return setChecking(true) try { // 当日已分析过 → 二次确认(查看今日报告 / 重新分析) const today = await findTodayReport(symbol) if (today) { setConfirmReport({ id: today.id, created_at: today.created_at, focus: today.focus }) } else { await doAnalysis() } } catch { await doAnalysis() } finally { setChecking(false) } } const doAnalysis = async () => { const r = await startAnalysis(symbol, name) if (r.error) toast(r.error, 'error') } return ( <> Beta } subtitle="日 K · 关键价位 · AI 四维分析(技术 / 基本面 / 财务 / 消息面)" right={
{symbol && ( )}
} />
{/* 搜索栏 */}
{symbol && ( <> )}
{/* 主体 */} {!symbol ? ( ) : showHistory ? ( ) : ( )}
{/* 二次确认:已有历史报告 */} {confirmReport && ( { openHistoryReport(confirmReport.id); setConfirmReport(null) }} onRedo={async () => { setConfirmReport(null); await doAnalysis() }} onClose={() => setConfirmReport(null)} /> )} {/* 个股日 K 详情对话框(点击名称/代码打开) */} setPreviewSymbol(null)} /> ) } // ===== 分析看板:日 K + 关键价位 ===== function StockAnalysisBoard({ symbol }: { symbol: string }) { const kline = useQuery({ queryKey: ['kline', symbol, ''], queryFn: () => api.klineDaily(symbol, 250), enabled: !!symbol, staleTime: 60_000, }) const levelsQ = useQuery({ queryKey: QK.stockLevels(symbol), queryFn: () => api.stockAnalysisLevels(symbol, 250), enabled: !!symbol, staleTime: 60_000, }) if (kline.isLoading) { return
} const rows = kline.data?.rows ?? [] if (rows.length === 0) { return } const levels = (levelsQ.data?.levels ?? {}) as Record // 涨跌色:最后一根 K 线收 vs 前一根收(无前日则按开收判断) const last = rows[rows.length - 1] const prev = rows[rows.length - 2] const curClose = levelsQ.data?.close const isUp = prev ? (last.close >= prev.close) : (last.close >= last.open) return (
关键价位分析
{rows.length} 个交易日 · 当前价 {curClose?.toFixed(2) ?? '—'}
) } // ===== 历史报告列表 ===== function HistoryList({ symbol }: { symbol: string }) { const { reports, loaded } = useHistoryReports() const mine = reports.filter(r => r.symbol === symbol) if (!loaded) { return
} if (mine.length === 0) { return } return (
{mine.map(r => (
))}
) } // ===== 二次确认弹窗 ===== function ConfirmModal({ report, onView, onRedo, onClose }: { report: { id: string; created_at: string; focus: string } onView: () => void onRedo: () => void onClose: () => void }) { return (
e.stopPropagation()} >
该个股已有分析报告

最近一次报告生成于 {fmtRelative(report.created_at)}

{report.focus &&

关注点: {report.focus}

}

可直接查看历史,或重新生成一份新报告。

) } function fmtRelative(iso: string): string { try { const t = new Date(iso).getTime() const diff = Date.now() - t if (diff < 60_000) return '刚刚' if (diff < 3600_000) return `${Math.floor(diff / 60_000)} 分钟前` if (diff < 86400_000) return `${Math.floor(diff / 3600_000)} 小时前` if (diff < 7 * 86400_000) return `${Math.floor(diff / 86400_000)} 天前` return new Date(iso).toLocaleDateString('zh-CN') } catch { return iso } }