import React, { useState, useEffect } from 'react'; import { logger } from '../../../utils/logger'; import { getApiBase } from '../../../utils/apiConfig'; import { useConceptStatsEvents } from '../hooks/useConceptStatsEvents'; import { Box, SimpleGrid, Card, CardBody, CardHeader, Heading, Text, VStack, HStack, Badge, Icon, Skeleton, SkeletonText, Tooltip, Button, ButtonGroup, Input, useColorModeValue, Divider, Flex, Avatar, StatLabel, StatNumber, Stat, StatHelpText, StatArrow, Tabs, TabList, TabPanels, Tab, TabPanel, useToast, } from '@chakra-ui/react'; import { FaArrowUp, FaArrowDown, FaChartLine, FaNewspaper, FaRocket, FaCrown, } from 'react-icons/fa'; import { BsLightningFill, BsGraphUp, BsGraphDown } from 'react-icons/bs'; const ConceptStatsPanel = ({ apiBaseUrl, onConceptClick }) => { // 获取正确的API基础URL const conceptApiBaseUrl = process.env.NODE_ENV === 'production' ? `${getApiBase()}/concept-api` : 'http://111.198.58.126:16801'; // 🎯 PostHog 事件追踪 const { trackTabChanged, trackTimeRangeChanged, trackCustomDateRangeSet, trackRankItemClicked, trackDataRefreshed, } = useConceptStatsEvents(); const [statsData, setStatsData] = useState({}); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState(0); const [timeRange, setTimeRange] = useState(7); // 默认7天 const [customStartDate, setCustomStartDate] = useState(''); const [customEndDate, setCustomEndDate] = useState(''); const [useCustomRange, setUseCustomRange] = useState(false); const toast = useToast(); // 深色主题颜色(固定使用深色模式) const bg = 'rgba(15, 23, 42, 0.8)'; const cardBg = 'rgba(15, 23, 42, 0.6)'; const borderColor = 'whiteAlpha.100'; // 获取统计数据 const fetchStatsData = async (days = timeRange, startDate = null, endDate = null) => { setLoading(true); try { // 构建查询参数 let queryParams = 'min_stock_count=3'; if (useCustomRange && startDate && endDate) { queryParams += `&start_date=${startDate}&end_date=${endDate}`; } else { queryParams += `&days=${days}`; } let response; let result; try { // 优先尝试concept-api路由(通过nginx代理) response = await fetch(`${conceptApiBaseUrl}/statistics?${queryParams}`); if (response.ok) { result = await response.json(); if (result.success && result.data) { setStatsData(result.data); return; // 成功获取数据,直接返回 } else { throw new Error(result.note || 'Concept API返回错误'); } } else { throw new Error(`Concept API错误: ${response.status}`); } } catch (conceptApiError) { logger.warn('ConceptStatsPanel', 'concept-api路由失败,尝试直接访问', { error: conceptApiError.message, days, startDate, endDate }); // 备用方案:直接访问concept_api服务(开发环境回退) try { response = await fetch(`http://111.198.58.126:16801/statistics?${queryParams}`); if (response.ok) { result = await response.json(); if (result.success && result.data) { setStatsData(result.data); logger.info('ConceptStatsPanel', '统计数据加载成功(备用API)', { dataKeys: Object.keys(result.data) }); return; } else { throw new Error(result.note || '直接API返回错误'); } } else { throw new Error(`直接API错误: ${response.status}`); } } catch (directError) { logger.error('ConceptStatsPanel', '所有API都失败', directError, { days, startDate, endDate }); throw new Error('无法访问概念统计API'); } } } catch (error) { logger.error('ConceptStatsPanel', 'fetchStatsData', error, { days, startDate, endDate }); // ❌ 移除获取统计数据失败toast // toast({ title: '获取统计数据失败', description: '正在使用默认数据,请稍后刷新重试', status: 'warning', duration: 3000, isClosable: true }); // 使用简化的默认数据作为最后的fallback setStatsData({ hot_concepts: [ { name: '小米大模型', change_pct: 18.76, stock_count: 12, news_count: 35 }, { name: '人工智能', change_pct: 15.67, stock_count: 45, news_count: 23 }, { name: '新能源汽车', change_pct: 12.34, stock_count: 38, news_count: 18 }, { name: '芯片概念', change_pct: 9.87, stock_count: 52, news_count: 31 }, { name: '5G通信', change_pct: 8.45, stock_count: 29, news_count: 15 }, ], cold_concepts: [ { name: '房地产', change_pct: -8.76, stock_count: 33, news_count: 12 }, { name: '煤炭开采', change_pct: -6.54, stock_count: 25, news_count: 8 }, { name: '传统零售', change_pct: -5.43, stock_count: 19, news_count: 6 }, { name: '钢铁冶炼', change_pct: -4.21, stock_count: 28, news_count: 9 }, { name: '纺织服装', change_pct: -3.98, stock_count: 15, news_count: 4 }, ], volatile_concepts: [ { name: '区块链', volatility: 23.45, avg_change: 3.21, max_change: 12.34 }, { name: '元宇宙', volatility: 21.87, avg_change: 2.98, max_change: 11.76 }, { name: '虚拟现实', volatility: 19.65, avg_change: -1.23, max_change: 9.87 }, { name: '游戏概念', volatility: 18.32, avg_change: 4.56, max_change: 10.45 }, { name: '在线教育', volatility: 17.89, avg_change: -2.11, max_change: 8.76 }, ], momentum_concepts: [ { name: '数字经济', consecutive_days: 5, total_change: 18.76, avg_daily: 3.75 }, { name: '云计算', consecutive_days: 4, total_change: 14.32, avg_daily: 3.58 }, { name: '物联网', consecutive_days: 4, total_change: 12.89, avg_daily: 3.22 }, { name: '大数据', consecutive_days: 3, total_change: 11.45, avg_daily: 3.82 }, { name: '工业互联网', consecutive_days: 3, total_change: 9.87, avg_daily: 3.29 }, ], }); } finally { setLoading(false); } }; // 处理时间范围变化 const handleTimeRangeChange = (newRange) => { if (newRange === 'custom') { setUseCustomRange(true); // 默认设置最近7天的自定义范围 const today = new Date(); const weekAgo = new Date(today); weekAgo.setDate(today.getDate() - 6); setCustomEndDate(today.toISOString().split('T')[0]); setCustomStartDate(weekAgo.toISOString().split('T')[0]); // 🎯 追踪切换到自定义范围 trackTimeRangeChanged(0, true); } else { setUseCustomRange(false); const days = parseInt(newRange); setTimeRange(days); // 🎯 追踪时间范围变化 trackTimeRangeChanged(days, false); fetchStatsData(days); } }; // 应用自定义日期范围 const applyCustomRange = () => { if (customStartDate && customEndDate) { if (new Date(customStartDate) > new Date(customEndDate)) { toast({ title: '日期选择错误', description: '开始日期不能晚于结束日期', status: 'error', duration: 3000, }); return; } // 🎯 追踪自定义日期范围设置 trackCustomDateRangeSet(customStartDate, customEndDate); fetchStatsData(null, customStartDate, customEndDate); } }; useEffect(() => { fetchStatsData(); }, []); // 当自定义范围状态改变时重新获取数据 useEffect(() => { if (useCustomRange && customStartDate && customEndDate) { fetchStatsData(null, customStartDate, customEndDate); } }, [useCustomRange]); // 格式化涨跌幅 const formatChange = (value) => { if (!value) return '0.00%'; const formatted = Math.abs(value).toFixed(2); return value > 0 ? `+${formatted}%` : `-${formatted}%`; }; // 获取涨跌幅颜色 const getChangeColor = (value) => { if (value > 0) return 'red.500'; if (value < 0) return 'green.500'; return 'gray.500'; }; // 统计卡片组件 - 深色主题版 const StatsCard = ({ title, icon, color, data, renderItem, isLoading }) => ( {isLoading ? ( {[1, 2, 3, 4, 5].map((i) => ( ))} ) : ( {data?.map((item, index) => ( {renderItem(item, index)} ))} )} ); const tabsData = [ { label: '涨幅榜', icon: FaArrowUp, color: 'red', data: statsData.hot_concepts, renderItem: (item, index) => ( onConceptClick?.(null, item.name)} > {index + 1} {index === 0 && ( )} {item.name} {item.stock_count}股 · {item.news_count}讯 {formatChange(item.change_pct)} ), }, { label: '跌幅榜', icon: FaArrowDown, color: 'green', data: statsData.cold_concepts, renderItem: (item, index) => ( onConceptClick?.(null, item.name)} > {index + 1} {item.name} {item.stock_count}股 · {item.news_count}讯 {formatChange(item.change_pct)} ), }, { label: '波动榜', icon: BsLightningFill, color: 'purple', data: statsData.volatile_concepts, renderItem: (item, index) => ( onConceptClick?.(null, item.name)} > {index + 1} {index === 0 && ( )} {item.name} 均幅 {formatChange(item.avg_change)} {item.volatility?.toFixed(1)}% ), }, { label: '连涨榜', icon: FaRocket, color: 'cyan', data: statsData.momentum_concepts, renderItem: (item, index) => ( onConceptClick?.(null, item.name)} > {index + 1} {index === 0 && ( )} {item.name} 累计 {formatChange(item.total_change)} {item.consecutive_days}天 ), }, ]; return ( {/* 顶部标题卡片 - 深色玻璃态 */} {/* 背景装饰 */} 概念统计中心 {statsData.summary?.date_range ? `统计范围: ${statsData.summary.date_range}` : '实时追踪热门概念动态' } {/* 时间范围选择控件 */} 📅 统计周期: {/* 自定义日期范围输入 */} {useCustomRange && ( setCustomStartDate(e.target.value)} size="xs" bg="whiteAlpha.200" color="white" border="1px solid" borderColor="whiteAlpha.300" _hover={{ borderColor: 'whiteAlpha.400' }} _focus={{ borderColor: 'whiteAlpha.500', boxShadow: 'none' }} w="auto" maxW="130px" sx={{ '&::-webkit-calendar-picker-indicator': { filter: 'invert(1)', } }} /> setCustomEndDate(e.target.value)} size="xs" bg="whiteAlpha.200" color="white" border="1px solid" borderColor="whiteAlpha.300" _hover={{ borderColor: 'whiteAlpha.400' }} _focus={{ borderColor: 'whiteAlpha.500', boxShadow: 'none' }} w="auto" maxW="130px" sx={{ '&::-webkit-calendar-picker-indicator': { filter: 'invert(1)', } }} /> )} {/* 主内容卡片 - 深色玻璃态 */} { const tabNames = ['涨幅榜', '跌幅榜', '波动榜', '连涨榜']; // 🎯 追踪Tab切换 trackTabChanged(index, tabNames[index]); setActiveTab(index); }} variant="unstyled" size="sm" > {tabsData.map((tab, index) => ( {tab.label} ))} {tabsData.map((tab, index) => ( ))} ); }; export default ConceptStatsPanel;