import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { getApiBase } from '@utils/apiConfig'; import { Box, Container, Heading, Text, Input, InputGroup, InputLeftElement, InputRightElement, IconButton, Button, SimpleGrid, Card, CardBody, CardHeader, VStack, HStack, Badge, Stat, StatLabel, StatNumber, StatHelpText, StatArrow, Flex, Spacer, Icon, useToast, Spinner, Center, Divider, List, ListItem, Tooltip, Menu, MenuButton, MenuList, MenuItem, useDisclosure, Image, Fade, Collapse, Stack, Progress, Tag, TagLabel, Skeleton, SkeletonText, } from '@chakra-ui/react'; import { SearchIcon, CloseIcon, ArrowForwardIcon, TrendingUpIcon, InfoIcon, ChevronRightIcon, CalendarIcon } from '@chakra-ui/icons'; import { FaChartLine, FaFire, FaRocket, FaBrain, FaCalendarAlt, FaChevronRight, FaArrowUp, FaArrowDown, FaChartBar, FaTag, FaLayerGroup, FaBolt } from 'react-icons/fa'; import ConceptStocksModal from '@components/ConceptStocksModal'; import TradeDatePicker from '@components/TradeDatePicker'; import HotspotOverview from './components/HotspotOverview'; import FlexScreen from './components/FlexScreen'; import { BsGraphUp, BsLightningFill } from 'react-icons/bs'; import * as echarts from 'echarts'; import { logger } from '../../utils/logger'; import tradingDays from '../../data/tradingDays.json'; import { useStockOverviewEvents } from './hooks/useStockOverviewEvents'; // 交易日 Set,用于快速查找 const tradingDaysSet = new Set(tradingDays); // Navigation bar now provided by MainLayout // import HomeNavbar from '../../components/Navbars/HomeNavbar'; const StockOverview = () => { const navigate = useNavigate(); const toast = useToast(); const colorMode = 'dark'; // 固定为 dark 深色模式 const heatmapRef = useRef(null); const heatmapChart = useRef(null); // 🎯 事件追踪 Hook const { trackMarketStatsViewed, trackSearchInitiated, trackStockSearched, trackSearchResultClicked, trackConceptClicked, trackConceptStockClicked, trackHeatmapStockClicked, trackStockDetailViewed, trackConceptDetailViewed, trackDateChanged, } = useStockOverviewEvents({ navigate }); // 状态管理 const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [showResults, setShowResults] = useState(false); const [topConcepts, setTopConcepts] = useState([]); const [heatmapData, setHeatmapData] = useState([]); const [loadingConcepts, setLoadingConcepts] = useState(true); const [loadingHeatmap, setLoadingHeatmap] = useState(true); const [selectedDate, setSelectedDate] = useState(null); const [marketStats, setMarketStats] = useState(null); const [availableDates, setAvailableDates] = useState([]); // 个股列表弹窗状态 const [isStockModalOpen, setIsStockModalOpen] = useState(false); const [selectedConcept, setSelectedConcept] = useState(null); // 深色主题配色 - 参考概念中心 const bgColor = '#0a0a0f'; // 深色背景 const cardBg = 'rgba(255, 255, 255, 0.03)'; // 玻璃态卡片背景 const borderColor = 'rgba(255, 255, 255, 0.08)'; // 边框 const hoverBg = 'rgba(255, 255, 255, 0.06)'; // 悬停背景 const searchBg = 'rgba(255, 255, 255, 0.15)'; // 搜索框背景(调亮) const textColor = 'rgba(255, 255, 255, 0.95)'; // 主文字 const subTextColor = 'rgba(255, 255, 255, 0.6)'; // 次要文字 const goldColor = '#8b5cf6'; // 使用紫色作为强调色 const accentColor = '#8b5cf6'; // 紫色强调 const heroBg = 'linear(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%)'; // 深色极光背景 // 打开个股列表弹窗 const handleViewStocks = useCallback((e, concept) => { e.stopPropagation(); setSelectedConcept(concept); setIsStockModalOpen(true); }, []); // 防抖搜索 const debounceSearch = useCallback( (() => { let timeoutId; return (query) => { clearTimeout(timeoutId); if (!query.trim()) { setSearchResults([]); setShowResults(false); return; } timeoutId = setTimeout(() => { searchStocks(query); }, 300); }; })(), [] ); // 搜索股票 const searchStocks = async (query) => { setIsSearching(true); try { logger.debug('StockOverview', '开始搜索股票', { query }); const response = await fetch(`${getApiBase()}/api/stocks/search?q=${encodeURIComponent(query)}&limit=10`); const data = await response.json(); logger.debug('StockOverview', 'API返回数据', { status: response.status, resultCount: data.data?.length || 0 }); if (data.success) { const results = data.data || []; setSearchResults(results); setShowResults(true); // 🎯 追踪搜索查询 trackStockSearched(query, results.length); } else { logger.warn('StockOverview', '搜索失败', data.error || '请稍后重试', { query }); // ❌ 移除搜索失败 toast(非关键操作) // 🎯 追踪搜索无结果 trackStockSearched(query, 0); } } catch (error) { logger.error('StockOverview', 'searchStocks', error, { query }); // ❌ 移除搜索失败 toast(非关键操作) } finally { setIsSearching(false); } }; // 获取每日涨幅靠前的概念 const fetchTopConcepts = async (date = null) => { setLoadingConcepts(true); try { const url = date ? `${getApiBase()}/api/concepts/daily-top?limit=6&date=${date}` : `${getApiBase()}/api/concepts/daily-top?limit=6`; const response = await fetch(url); const data = await response.json(); if (data.success) { setTopConcepts(data.data); // 使用概念接口的日期作为统一数据源(数据最新) setSelectedDate(new Date(data.trade_date)); // 基于交易日历生成可选日期列表 if (data.trade_date && tradingDays.length > 0) { // 找到当前日期或最近的交易日 let targetDate = data.trade_date; if (!tradingDaysSet.has(data.trade_date)) { for (let i = tradingDays.length - 1; i >= 0; i--) { if (tradingDays[i] <= data.trade_date) { targetDate = tradingDays[i]; break; } } } const idx = tradingDays.indexOf(targetDate); if (idx !== -1) { const startIdx = Math.max(0, idx - 19); const dates = tradingDays.slice(startIdx, idx + 1).reverse(); setAvailableDates(dates); } } logger.debug('StockOverview', '热门概念加载成功', { count: data.data?.length || 0, date: data.trade_date }); } } catch (error) { logger.error('StockOverview', 'fetchTopConcepts', error, { date }); } finally { setLoadingConcepts(false); } }; // 获取热力图数据 const fetchHeatmapData = async (date = null) => { setLoadingHeatmap(true); try { const url = date ? `${getApiBase()}/api/market/heatmap?limit=500&date=${date}` : `${getApiBase()}/api/market/heatmap?limit=500`; const response = await fetch(url); const data = await response.json(); if (data.success) { setHeatmapData(data.data); // 保存统计数据 if (data.statistics) { setMarketStats(prevStats => ({ ...(prevStats || {}), rising_count: data.statistics.rising_count, falling_count: data.statistics.falling_count })); } // 日期由 fetchTopConcepts 统一设置,这里不再设置 logger.debug('StockOverview', '热力图数据加载成功', { count: data.data?.length || 0, date: data.trade_date }); // 延迟渲染热力图,确保DOM已经准备好 setTimeout(() => renderHeatmap(data.data), 100); } } catch (error) { logger.error('StockOverview', 'fetchHeatmapData', error, { date }); } finally { setLoadingHeatmap(false); } }; // 获取市场统计数据 const fetchMarketStats = async (date = null) => { try { const url = date ? `${getApiBase()}/api/market/statistics?date=${date}` : `${getApiBase()}/api/market/statistics`; const response = await fetch(url); const data = await response.json(); if (data.success) { // 使用函数式更新,只更新 summary 数据,不覆盖 heatmap 接口设置的 rising_count/falling_count setMarketStats(prevStats => { const newStats = { ...(prevStats || {}), // 先保留所有现有字段(包括 rising_count/falling_count) ...data.summary, // 然后覆盖 summary 字段 date: data.trade_date }; return newStats; }); const newStats = { ...data.summary, date: data.trade_date }; // 日期和可选日期列表由 fetchTopConcepts 统一设置,这里不再设置 logger.debug('StockOverview', '市场统计数据加载成功', { date: data.trade_date }); // 🎯 追踪市场统计数据查看 trackMarketStatsViewed(newStats); } } catch (error) { logger.error('StockOverview', 'fetchMarketStats', error, { date }); // ❌ 移除统计数据加载失败 toast(非关键操作) } }; // 渲染热力图 const renderHeatmap = useCallback((data) => { if (!heatmapRef.current || !data || !data.length) return; try { // 初始化或获取ECharts实例 if (!heatmapChart.current) { heatmapChart.current = echarts.init(heatmapRef.current, colorMode === 'dark' ? 'dark' : null); } // 按市值分组 const groupedData = {}; data.forEach(item => { const capRange = getMarketCapRange(item.market_cap); if (!groupedData[capRange]) { groupedData[capRange] = []; } groupedData[capRange].push(item); }); // 构建树图数据 - 修复格式问题 const treeData = Object.entries(groupedData).map(([range, stocks]) => ({ name: range, children: stocks.map(stock => { const change = stock.change_percent || 0; let color = colorMode === 'dark' ? '#333333' : '#9ca3af'; // 默认灰色 if (change > 0) { const intensity = Math.min(change / 10, 1); if (colorMode === 'dark') { // 夜间模式:红色带金色调 color = `rgba(255, 77, 77, ${0.4 + intensity * 0.6})`; } else { color = `rgba(239, 68, 68, ${0.3 + intensity * 0.7})`; } } else if (change < 0) { const intensity = Math.min(Math.abs(change) / 10, 1); if (colorMode === 'dark') { // 夜间模式:绿色带暗色调 color = `rgba(34, 197, 94, ${0.3 + intensity * 0.5})`; } else { color = `rgba(34, 197, 94, ${0.3 + intensity * 0.7})`; } } return { name: stock.stock_name, value: Math.abs(stock.market_cap), change: stock.change_percent, code: stock.stock_code, amount: stock.amount, industry: stock.industry, province: stock.province, itemStyle: { color: color } }; }) })); const option = { backgroundColor: colorMode === 'dark' ? '#0a0a0a' : 'transparent', tooltip: { backgroundColor: colorMode === 'dark' ? '#1a1a1a' : 'white', borderColor: colorMode === 'dark' ? goldColor : '#ccc', borderWidth: colorMode === 'dark' ? 2 : 1, textStyle: { color: colorMode === 'dark' ? 'white' : '#333' }, formatter: function(info) { const data = info.data; const isDark = colorMode === 'dark'; // 如果是父节点(市值分组) if (data.children) { return `
${data.name}
包含 ${data.children.length} 只股票
总市值: ${data.children.reduce((sum, item) => sum + item.value, 0).toFixed(2)} 亿元
`; } // 个股详情 return `
${data.name}
代码: ${data.code || '-'}
涨跌幅: ${data.change > 0 ? '+' : ''}${data.change?.toFixed(2) || 0}%
市值: ${data.value?.toFixed(2) || 0} 亿元
成交额: ${data.amount?.toFixed(2) || 0} 亿元
行业: ${data.industry || '未知'}
地区: ${data.province || '未知'}
`; } }, series: [{ name: 'A股市场', type: 'treemap', data: treeData, leafDepth: 1, roam: false, breadcrumb: { show: true, top: 10, left: 10, itemStyle: { color: colorMode === 'dark' ? '#1a1a2e' : '#f0f0f0', borderColor: colorMode === 'dark' ? goldColor : '#ccc', borderWidth: 1, shadowBlur: colorMode === 'dark' ? 5 : 0, shadowColor: colorMode === 'dark' ? `${goldColor}40` : 'transparent', textStyle: { color: colorMode === 'dark' ? goldColor : '#333' } }, emphasis: { itemStyle: { color: colorMode === 'dark' ? goldColor : '#e0e0e0', textStyle: { color: colorMode === 'dark' ? '#0a0a0a' : '#333' } } } }, levels: [ { itemStyle: { borderColor: colorMode === 'dark' ? '#1a1a1a' : '#fff', borderWidth: 3, gapWidth: 3 } }, { itemStyle: { borderColor: colorMode === 'dark' ? '#0a0a0a' : '#fff', borderWidth: 1, gapWidth: 1 } } ], itemStyle: { borderColor: colorMode === 'dark' ? '#0a0a0a' : '#fff', borderWidth: 1 }, label: { show: true, formatter: function(params) { const data = params.data; // 父节点(市值分组)显示名称 if (data.children) { return params.name; } // 子节点(个股)根据市值大小决定是否显示 return data.value > 5 ? data.name : ''; }, fontSize: 12, color: function(params) { if (colorMode === 'dark') { // 夜间模式:根据背景色调整文字颜色 const change = params.data.change || 0; if (Math.abs(change) > 5) { return 'white'; } return '#ccc'; } return '#333'; } } }] }; // 设置配置项 heatmapChart.current.setOption(option); // 先移除之前的点击事件,避免重复绑定 heatmapChart.current.off('click'); // 添加点击事件 heatmapChart.current.on('click', function(params) { // 只有点击个股(有code的节点)才跳转 if (params.data && params.data.code && !params.data.children) { const stock = { code: params.data.code, name: params.data.name, change_percent: params.data.change }; const marketCapRange = getMarketCapRange(params.data.value); // 🎯 追踪热力图股票点击 trackHeatmapStockClicked(stock, marketCapRange); navigate(`/company?scode=${params.data.code}`); } }); } catch (error) { logger.error('StockOverview', 'renderHeatmap', error, { dataLength: data?.length || 0 }); // ❌ 移除热力图渲染失败 toast(非关键操作) } }, [colorMode, goldColor, navigate, trackHeatmapStockClicked]); // ✅ 添加追踪函数依赖 // 获取市值区间 const getMarketCapRange = (cap) => { if (cap >= 1000) return '超大盘股(>1000亿)'; if (cap >= 500) return '大盘股(500-1000亿)'; if (cap >= 100) return '中盘股(100-500亿)'; if (cap >= 50) return '小盘股(50-100亿)'; return '微盘股(<50亿)'; }; // 处理搜索输入 const handleSearchChange = (e) => { const value = e.target.value; setSearchQuery(value); // 🎯 追踪搜索开始(首次输入时) if (value && !searchQuery) { trackSearchInitiated(); } debounceSearch(value); }; // 清空搜索 const handleClearSearch = () => { setSearchQuery(''); setSearchResults([]); setShowResults(false); }; // 选择股票 const handleSelectStock = (stock, index = 0) => { // 🎯 追踪搜索结果点击 trackSearchResultClicked(stock, index); navigate(`/company?scode=${stock.stock_code}`); handleClearSearch(); }; // 查看概念详情(模仿概念中心:打开对应HTML页) const handleConceptClick = (concept, rank = 0) => { // 🎯 追踪概念点击 trackConceptClicked(concept, rank); const htmlPath = `/htmls/${concept.concept_name}.html`; window.open(htmlPath, '_blank'); }; // 格式化涨跌幅 const formatChangePercent = (value) => { if (value === null || value === undefined) return '0.00%'; const formatted = value.toFixed(2); return formatted > 0 ? `+${formatted}%` : `${formatted}%`; }; // 获取涨跌幅颜色 const getChangeColor = (value) => { if (value === null || value === undefined) return 'gray'; return value > 0 ? 'red' : value < 0 ? 'green' : 'gray'; }; // 组件挂载时获取数据 useEffect(() => { fetchTopConcepts(); fetchHeatmapData(); fetchMarketStats(); // 监听窗口大小变化,重新渲染热力图 const handleResize = () => { if (heatmapChart.current) { heatmapChart.current.resize(); } }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); if (heatmapChart.current) { heatmapChart.current.dispose(); } }; }, []); // 监听colorMode和heatmapData变化,重新渲染热力图 useEffect(() => { if (heatmapData.length > 0) { // 如果已有实例,先销毁再重新创建 if (heatmapChart.current) { heatmapChart.current.dispose(); heatmapChart.current = null; } renderHeatmap(heatmapData); } }, [heatmapData, colorMode, renderHeatmap]); // 概念卡片骨架屏 const ConceptSkeleton = () => ( ); return ( {/* 全局极光背景 */} {/* Hero Section */} {/* 背景装饰 */} 个股中心 实时追踪市场动态,洞察投资机会 {/* 搜索框 */} {searchQuery && ( } variant="ghost" onClick={handleClearSearch} aria-label="清空搜索" color={colorMode === 'dark' ? goldColor : 'gray.600'} _hover={{ bg: colorMode === 'dark' ? 'whiteAlpha.100' : 'gray.100' }} /> )} {/* 搜索结果下拉 */} {isSearching ? (
) : searchResults.length > 0 ? ( {searchResults.map((stock, index) => ( handleSelectStock(stock, index)} borderBottomWidth={index < searchResults.length - 1 ? "1px" : "0"} borderColor={borderColor} > {stock.stock_name} {stock.stock_code} {stock.pinyin_abbr && ( ({stock.pinyin_abbr.toUpperCase()}) )} {stock.exchange && ( {stock.exchange} )} ))} ) : (
未找到相关股票
)}
{/* 统计数据 - 使用市场统计API数据 */} A股总市值 {marketStats ? `${(marketStats.total_market_cap / 10000).toFixed(1)}万亿` : '-' } 今日成交额 {marketStats ? `${(marketStats.total_amount / 10000).toFixed(1)}万亿` : '-' } 上涨家数 {marketStats && marketStats.rising_count !== undefined && marketStats.rising_count !== null ? marketStats.rising_count.toLocaleString() : '-' } 下跌家数 {marketStats && marketStats.falling_count !== undefined && marketStats.falling_count !== null ? marketStats.falling_count.toLocaleString() : '-' }
{/* 主内容区 */} {/* 日期选择器 */} { const dateStr = date.toISOString().split('T')[0]; const previousDateStr = selectedDate ? selectedDate.toISOString().split('T')[0] : null; trackDateChanged(dateStr, previousDateStr); setSelectedDate(date); fetchHeatmapData(dateStr); fetchMarketStats(dateStr); fetchTopConcepts(dateStr); }} latestTradeDate={null} minDate={tradingDays.length > 0 ? new Date(tradingDays[0]) : undefined} maxDate={tradingDays.length > 0 ? new Date(tradingDays[tradingDays.length - 1]) : undefined} label="交易日期" isDarkMode={true} /> {selectedDate && ( 当前显示 {selectedDate.toISOString().split('T')[0]} 的市场数据 )} {/* 热点概览 - 大盘走势 + 概念异动 */} {/* 只在 selectedDate 确定后渲染,避免 null → 日期 的双重请求 */} {selectedDate ? ( ) : (
加载热点概览数据...
)}
{/* 灵活屏 - 实时行情监控 */} {/* 今日热门概念 */} 今日热门概念 {loadingConcepts ? ( {[...Array(6)].map((_, i) => ( ))} ) : ( {topConcepts.map((concept, index) => ( handleConceptClick(concept, index)} position="relative" overflow="hidden" > {/* 排名标签 */} TOP {index + 1} {/* 涨跌幅标签 */} 0 ? '#ff4d4d' : '#22c55e' : 'transparent'} > 0 ? FaArrowUp : FaArrowDown} boxSize={3} /> {formatChangePercent(concept.change_percent)} {/* 概念名称 */} {concept.concept_name} {/* 层级信息 */} {concept.hierarchy && ( {[concept.hierarchy.lv1, concept.hierarchy.lv2, concept.hierarchy.lv3] .filter(Boolean) .join(' > ')} )} {/* 描述 */} {concept.description || '暂无描述'} {/* 标签 */} {concept.tags && concept.tags.length > 0 && ( {concept.tags.slice(0, 4).map((tag, idx) => ( {tag} ))} {concept.tags.length > 4 && ( +{concept.tags.length - 4} )} )} {/* 爆发日期 */} {concept.outbreak_dates && concept.outbreak_dates.length > 0 && ( 近期爆发: {concept.outbreak_dates.slice(0, 2).join(', ')} {concept.outbreak_dates.length > 2 && ` 等${concept.outbreak_dates.length}次`} )} {/* 相关股票 */} handleViewStocks(e, concept)} _hover={{ bg: hoverBg }} p={2} borderRadius="md" transition="background 0.2s" > 包含 {concept.stock_count} 只个股 {concept.stocks && concept.stocks.length > 0 && ( {concept.stocks.slice(0, 5).map((stock, idx) => ( {stock.stock_name || stock.name} ))} {concept.stocks.length > 5 && ( +{concept.stocks.length - 5} )} )} ))} )} {/* 市值热力图 */} 市值热力图 {loadingHeatmap ? (
正在加载热力图数据...
) : ( {/* 图例说明 */} 上涨 平盘 下跌 {/* 热力图容器 */} )}
{/* 个股列表弹窗 */} setIsStockModalOpen(false)} concept={selectedConcept} />
); }; export default StockOverview;