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 `