import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; 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, useColorModeValue, useColorMode, useToast, Spinner, Center, Divider, List, ListItem, Tooltip, Menu, MenuButton, MenuList, MenuItem, useDisclosure, Image, Fade, ScaleFade, Collapse, Stack, Progress, Tag, TagLabel, Skeleton, SkeletonText, Popover, PopoverTrigger, PopoverContent, PopoverBody, } from '@chakra-ui/react'; import { SearchIcon, CloseIcon, ArrowForwardIcon, TrendingUpIcon, InfoIcon, ChevronRightIcon, MoonIcon, SunIcon, CalendarIcon } from '@chakra-ui/icons'; import { FaChartLine, FaFire, FaRocket, FaBrain, FaCalendarAlt, FaChevronRight, FaArrowUp, FaArrowDown, FaChartBar } from 'react-icons/fa'; import { BsGraphUp, BsLightningFill } from 'react-icons/bs'; import { keyframes } from '@emotion/react'; import * as echarts from 'echarts'; import { logger } from '../../utils/logger'; import { useStockOverviewEvents } from './hooks/useStockOverviewEvents'; // Navigation bar now provided by MainLayout // import HomeNavbar from '../../components/Navbars/HomeNavbar'; // 动画定义 const pulseAnimation = keyframes` 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } `; const floatAnimation = keyframes` 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-10px); } `; const StockOverview = () => { const navigate = useNavigate(); const toast = useToast(); const { colorMode, toggleColorMode } = useColorMode(); 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 [isCalendarOpen, setIsCalendarOpen] = useState(false); // 专业的颜色主题 const bgColor = useColorModeValue('white', '#0a0a0a'); const cardBg = useColorModeValue('white', '#1a1a1a'); const borderColor = useColorModeValue('gray.200', '#333333'); const hoverBg = useColorModeValue('gray.50', '#2a2a2a'); const searchBg = useColorModeValue('white', '#1a1a1a'); const textColor = useColorModeValue('gray.800', 'white'); const subTextColor = useColorModeValue('gray.600', 'gray.400'); const goldColor = useColorModeValue('#FFD700', '#FFC107'); const accentColor = useColorModeValue('purple.600', goldColor); const heroBg = useColorModeValue('linear(to-br, purple.600, pink.500)', 'linear(to-br, #0a0a0a, #1a1a1a)'); // 防抖搜索 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(`/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 ? `/api/concepts/daily-top?limit=6&date=${date}` : '/api/concepts/daily-top?limit=6'; const response = await fetch(url); const data = await response.json(); if (data.success) { setTopConcepts(data.data); if (!selectedDate) setSelectedDate(data.trade_date); 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 ? `/api/market/heatmap?limit=500&date=${date}` : '/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 })); } if (!selectedDate) setSelectedDate(data.trade_date); 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 ? `/api/market/statistics?date=${date}` : '/api/market/statistics'; const response = await fetch(url); const data = await response.json(); if (data.success) { const newStats = { ...data.summary, // 保留之前从 heatmap 接口获取的上涨/下跌家数 rising_count: prevStats?.rising_count, falling_count: prevStats?.falling_count, date: data.trade_date }; setMarketStats(newStats); setAvailableDates(data.available_dates || []); if (!selectedDate) setSelectedDate(data.trade_date); logger.debug('StockOverview', '市场统计数据加载成功', { date: data.trade_date, availableDatesCount: data.available_dates?.length || 0 }); // 🎯 追踪市场统计数据查看 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: false }, 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 handleDateChange = (date) => { const previousDate = selectedDate; // 🎯 追踪日期变化 trackDateChanged(date, previousDate); setSelectedDate(date); setIsCalendarOpen(false); // 重新获取数据 fetchHeatmapData(date); fetchMarketStats(date); fetchTopConcepts(date); }; // 格式化涨跌幅 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 ( {/* 导航栏已由 MainLayout 提供 */} {/* Hero Section */} {/* 背景装饰 */} {/* 日夜模式切换按钮 */} : } onClick={toggleColorMode} size="lg" bg={colorMode === 'dark' ? '#1a1a1a' : 'white'} color={colorMode === 'dark' ? goldColor : 'purple.600'} border="2px solid" borderColor={colorMode === 'dark' ? goldColor : 'purple.200'} _hover={{ bg: colorMode === 'dark' ? '#2a2a2a' : 'purple.50', transform: 'scale(1.1)' }} transition="all 0.3s" /> 个股中心 实时追踪市场动态,洞察投资机会 {/* 搜索框 */} {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() : '-' }
{/* 主内容区 */} {/* 日期选择器 */} setIsCalendarOpen(false)}> 选择交易日期 {availableDates.length > 0 ? ( {availableDates.map((date) => ( ))} ) : ( 暂无可用日期 )} {selectedDate && ( 当前显示 {selectedDate} 的市场数据 )} {/* 今日热门概念 */} 今日热门概念 {loadingConcepts ? ( {[...Array(6)].map((_, i) => ( ))} ) : ( {topConcepts.map((concept, index) => ( handleConceptClick(concept, index)} position="relative" overflow="hidden" > {/* 排名标签 */} TOP {index + 1} {/* 涨跌幅标签 */} 5 ? `${pulseAnimation} 2s infinite` : 'none'} border={colorMode === 'dark' ? '1px solid' : 'none'} borderColor={colorMode === 'dark' ? concept.change_percent > 0 ? '#ff4d4d' : '#22c55e' : 'transparent'} > 0 ? FaArrowUp : FaArrowDown} boxSize={3} /> {formatChangePercent(concept.change_percent)} {concept.concept_name} {concept.description || '暂无描述'} 包含 {concept.stock_count} 只个股 {concept.stocks && concept.stocks.length > 0 && ( {concept.stocks.map((stock, idx) => ( { e.stopPropagation(); // 🎯 追踪概念下的股票标签点击 trackConceptStockClicked({ code: stock.stock_code, name: stock.stock_name }, concept.concept_name); navigate(`/company?scode=${stock.stock_code}`); }} > {stock.stock_name} ))} )} ))} )} {/* 市值热力图 */} 市值热力图 {loadingHeatmap ? (
正在加载热力图数据...
) : ( {/* 图例说明 */} 上涨 平盘 下跌 {/* 热力图容器 */} )}
); }; export default StockOverview;