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;