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 `