Files
vf_react/src/views/Concept/components/ConceptStatsPanel.js
zdl 59fdb150a9 fix(UI): 优化市值热力图和概念统计列表交互
- StockOverview: 启用热力图面包屑导航,支持返回上一级
- ConceptStatsPanel: 移除统计列表 400px 高度限制,改为自适应高度

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-15 17:21:53 +08:00

848 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 }) => (
<Box p={4}>
{isLoading ? (
<VStack spacing={3} align="stretch">
{[1, 2, 3, 4, 5].map((i) => (
<HStack key={i} justify="space-between" p={3} bg="whiteAlpha.50" borderRadius="lg">
<HStack spacing={2} flex={1}>
<Skeleton height="20px" width="20px" borderRadius="full" startColor="whiteAlpha.100" endColor="whiteAlpha.200" />
<VStack align="start" spacing={1} flex={1}>
<Skeleton height="14px" width="80%" startColor="whiteAlpha.100" endColor="whiteAlpha.200" />
<Skeleton height="12px" width="60%" startColor="whiteAlpha.100" endColor="whiteAlpha.200" />
</VStack>
</HStack>
<Skeleton height="20px" width="50px" borderRadius="md" startColor="whiteAlpha.100" endColor="whiteAlpha.200" />
</HStack>
))}
</VStack>
) : (
<VStack spacing={2} align="stretch">
{data?.map((item, index) => (
<Box key={index}>
{renderItem(item, index)}
</Box>
))}
</VStack>
)}
</Box>
);
const tabsData = [
{
label: '涨幅榜',
icon: FaArrowUp,
color: 'red',
data: statsData.hot_concepts,
renderItem: (item, index) => (
<Flex
justify="space-between"
align="center"
p={3}
borderRadius="xl"
bg={index < 3 ? 'rgba(239, 68, 68, 0.15)' : 'whiteAlpha.50'}
border="1px solid"
borderColor={index < 3 ? 'red.500' : 'whiteAlpha.100'}
_hover={{
transform: 'translateY(-1px)',
shadow: 'md',
cursor: 'pointer',
bg: index < 3 ? 'rgba(239, 68, 68, 0.25)' : 'whiteAlpha.100'
}}
transition="all 0.2s"
onClick={() => onConceptClick?.(null, item.name)}
>
<HStack spacing={3} flex={1}>
<Box position="relative">
<Badge
bg={index === 0 ? 'yellow.500' : index === 1 ? 'orange.500' : index === 2 ? 'red.500' : 'whiteAlpha.200'}
color="white"
borderRadius="full"
minW="24px"
h="24px"
textAlign="center"
fontSize="xs"
fontWeight="bold"
display="flex"
alignItems="center"
justifyContent="center"
>
{index + 1}
</Badge>
{index === 0 && (
<Icon
as={FaCrown}
position="absolute"
top="-8px"
right="-8px"
color="yellow.400"
boxSize={3}
/>
)}
</Box>
<VStack align="start" spacing={0} flex={1}>
<Text fontSize="sm" fontWeight="bold" noOfLines={1} color="white">
{item.name}
</Text>
<HStack spacing={2} fontSize="xs" color="whiteAlpha.600">
<HStack spacing={1}>
<Icon as={FaChartLine} boxSize={2.5} />
<Text>{item.stock_count}</Text>
</HStack>
<Text>·</Text>
<HStack spacing={1}>
<Icon as={FaNewspaper} boxSize={2.5} />
<Text>{item.news_count}</Text>
</HStack>
</HStack>
</VStack>
</HStack>
<Badge
bg="red.500"
color="white"
borderRadius="lg"
fontSize="xs"
fontWeight="bold"
px={2}
py={1}
>
<Icon as={FaArrowUp} boxSize={2} mr={1} />
{formatChange(item.change_pct)}
</Badge>
</Flex>
),
},
{
label: '跌幅榜',
icon: FaArrowDown,
color: 'green',
data: statsData.cold_concepts,
renderItem: (item, index) => (
<Flex
justify="space-between"
align="center"
p={3}
borderRadius="xl"
bg={index < 3 ? 'rgba(34, 197, 94, 0.15)' : 'whiteAlpha.50'}
border="1px solid"
borderColor={index < 3 ? 'green.500' : 'whiteAlpha.100'}
_hover={{
transform: 'translateY(-1px)',
shadow: 'md',
cursor: 'pointer',
bg: index < 3 ? 'rgba(34, 197, 94, 0.25)' : 'whiteAlpha.100'
}}
transition="all 0.2s"
onClick={() => onConceptClick?.(null, item.name)}
>
<HStack spacing={3} flex={1}>
<Badge
bg={index < 3 ? 'green.500' : 'whiteAlpha.200'}
color="white"
borderRadius="full"
minW="24px"
h="24px"
textAlign="center"
fontSize="xs"
fontWeight="bold"
display="flex"
alignItems="center"
justifyContent="center"
>
{index + 1}
</Badge>
<VStack align="start" spacing={0} flex={1}>
<Text fontSize="sm" fontWeight="bold" noOfLines={1} color="white">
{item.name}
</Text>
<HStack spacing={2} fontSize="xs" color="whiteAlpha.600">
<HStack spacing={1}>
<Icon as={FaChartLine} boxSize={2.5} />
<Text>{item.stock_count}</Text>
</HStack>
<Text>·</Text>
<HStack spacing={1}>
<Icon as={FaNewspaper} boxSize={2.5} />
<Text>{item.news_count}</Text>
</HStack>
</HStack>
</VStack>
</HStack>
<Badge
bg="green.500"
color="white"
borderRadius="lg"
fontSize="xs"
fontWeight="bold"
px={2}
py={1}
>
<Icon as={FaArrowDown} boxSize={2} mr={1} />
{formatChange(item.change_pct)}
</Badge>
</Flex>
),
},
{
label: '波动榜',
icon: BsLightningFill,
color: 'purple',
data: statsData.volatile_concepts,
renderItem: (item, index) => (
<Flex
justify="space-between"
align="center"
p={3}
borderRadius="xl"
bg={index < 3 ? 'rgba(168, 85, 247, 0.15)' : 'whiteAlpha.50'}
border="1px solid"
borderColor={index < 3 ? 'purple.500' : 'whiteAlpha.100'}
_hover={{
transform: 'translateY(-1px)',
shadow: 'md',
cursor: 'pointer',
bg: index < 3 ? 'rgba(168, 85, 247, 0.25)' : 'whiteAlpha.100'
}}
transition="all 0.2s"
onClick={() => onConceptClick?.(null, item.name)}
>
<HStack spacing={3} flex={1}>
<Box position="relative">
<Badge
bg={index < 3 ? 'purple.500' : 'whiteAlpha.200'}
color="white"
borderRadius="full"
minW="24px"
h="24px"
textAlign="center"
fontSize="xs"
fontWeight="bold"
display="flex"
alignItems="center"
justifyContent="center"
>
{index + 1}
</Badge>
{index === 0 && (
<Icon
as={BsLightningFill}
position="absolute"
top="-8px"
right="-8px"
color="purple.400"
boxSize={3}
/>
)}
</Box>
<VStack align="start" spacing={0} flex={1}>
<Text fontSize="sm" fontWeight="bold" noOfLines={1} color="white">
{item.name}
</Text>
<Text fontSize="xs" color="whiteAlpha.600">
均幅 {formatChange(item.avg_change)}
</Text>
</VStack>
</HStack>
<Badge
bg="purple.500"
color="white"
borderRadius="lg"
fontSize="xs"
fontWeight="bold"
px={2}
py={1}
>
<Icon as={BsLightningFill} boxSize={2} mr={1} />
{item.volatility?.toFixed(1)}%
</Badge>
</Flex>
),
},
{
label: '连涨榜',
icon: FaRocket,
color: 'cyan',
data: statsData.momentum_concepts,
renderItem: (item, index) => (
<Flex
justify="space-between"
align="center"
p={3}
borderRadius="xl"
bg={index < 3 ? 'rgba(34, 211, 238, 0.15)' : 'whiteAlpha.50'}
border="1px solid"
borderColor={index < 3 ? 'cyan.500' : 'whiteAlpha.100'}
_hover={{
transform: 'translateY(-1px)',
shadow: 'md',
cursor: 'pointer',
bg: index < 3 ? 'rgba(34, 211, 238, 0.25)' : 'whiteAlpha.100'
}}
transition="all 0.2s"
onClick={() => onConceptClick?.(null, item.name)}
>
<HStack spacing={3} flex={1}>
<Box position="relative">
<Badge
bg={index === 0 ? 'cyan.500' : index < 3 ? 'blue.500' : 'whiteAlpha.200'}
color="white"
borderRadius="full"
minW="24px"
h="24px"
textAlign="center"
fontSize="xs"
fontWeight="bold"
display="flex"
alignItems="center"
justifyContent="center"
>
{index + 1}
</Badge>
{index === 0 && (
<Icon
as={FaRocket}
position="absolute"
top="-8px"
right="-8px"
color="cyan.400"
boxSize={3}
/>
)}
</Box>
<VStack align="start" spacing={0} flex={1}>
<Text fontSize="sm" fontWeight="bold" noOfLines={1} color="white">
{item.name}
</Text>
<Text fontSize="xs" color="whiteAlpha.600">
累计 {formatChange(item.total_change)}
</Text>
</VStack>
</HStack>
<Badge
bg="cyan.500"
color="white"
borderRadius="lg"
fontSize="xs"
fontWeight="bold"
px={2}
py={1}
>
<Icon as={FaRocket} boxSize={2} mr={1} />
{item.consecutive_days}
</Badge>
</Flex>
),
},
];
return (
<Box>
{/* 顶部标题卡片 - 深色玻璃态 */}
<Box
bg="rgba(15, 23, 42, 0.8)"
backdropFilter="blur(20px)"
p={4}
borderRadius="xl"
mb={4}
position="relative"
overflow="hidden"
border="1px solid"
borderColor="whiteAlpha.100"
>
{/* 背景装饰 */}
<Box
position="absolute"
top="-20px"
right="-20px"
width="80px"
height="80px"
borderRadius="full"
bg="purple.500"
opacity={0.2}
filter="blur(20px)"
/>
<Box
position="absolute"
bottom="-10px"
left="-10px"
width="60px"
height="60px"
borderRadius="full"
bg="cyan.500"
opacity={0.15}
filter="blur(15px)"
/>
<VStack align="start" spacing={3} position="relative" w="full">
<Flex justify="space-between" align="center" w="full">
<HStack spacing={2}>
<Box p={2} bg="whiteAlpha.100" borderRadius="lg" border="1px solid" borderColor="cyan.500">
<Icon as={FaChartLine} color="cyan.400" boxSize={4} />
</Box>
<VStack align="start" spacing={0}>
<Heading size="sm" color="white" fontWeight="bold">
概念统计中心
</Heading>
<Text fontSize="xs" color="whiteAlpha.800">
{statsData.summary?.date_range ?
`统计范围: ${statsData.summary.date_range}` :
'实时追踪热门概念动态'
}
</Text>
</VStack>
</HStack>
<HStack spacing={2}>
<Button
size="xs"
bg="whiteAlpha.200"
color="white"
_hover={{ bg: 'whiteAlpha.300' }}
onClick={() => fetchStatsData(timeRange, customStartDate, customEndDate)}
isLoading={loading}
loadingText="刷新中"
borderRadius="full"
px={3}
>
🔄 刷新
</Button>
</HStack>
</Flex>
{/* 时间范围选择控件 */}
<Flex gap={2} wrap="wrap" align="center">
<Text fontSize="xs" color="whiteAlpha.900" fontWeight="medium">
📅 统计周期:
</Text>
<ButtonGroup size="xs" isAttached variant="solid">
<Button
bg={!useCustomRange && timeRange === 3 ? 'whiteAlpha.400' : 'whiteAlpha.200'}
color="white"
_hover={{ bg: 'whiteAlpha.300' }}
onClick={() => handleTimeRangeChange('3')}
borderRadius="md"
>
3
</Button>
<Button
bg={!useCustomRange && timeRange === 7 ? 'whiteAlpha.400' : 'whiteAlpha.200'}
color="white"
_hover={{ bg: 'whiteAlpha.300' }}
onClick={() => handleTimeRangeChange('7')}
borderRadius="md"
>
7
</Button>
<Button
bg={!useCustomRange && timeRange === 14 ? 'whiteAlpha.400' : 'whiteAlpha.200'}
color="white"
_hover={{ bg: 'whiteAlpha.300' }}
onClick={() => handleTimeRangeChange('14')}
borderRadius="md"
>
14
</Button>
<Button
bg={!useCustomRange && timeRange === 30 ? 'whiteAlpha.400' : 'whiteAlpha.200'}
color="white"
_hover={{ bg: 'whiteAlpha.300' }}
onClick={() => handleTimeRangeChange('30')}
borderRadius="md"
>
30
</Button>
<Button
bg={useCustomRange ? 'whiteAlpha.400' : 'whiteAlpha.200'}
color="white"
_hover={{ bg: 'whiteAlpha.300' }}
onClick={() => handleTimeRangeChange('custom')}
borderRadius="md"
>
自定义
</Button>
</ButtonGroup>
</Flex>
{/* 自定义日期范围输入 */}
{useCustomRange && (
<Flex gap={2} wrap="wrap" align="center">
<Input
type="date"
value={customStartDate}
onChange={(e) => 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)',
}
}}
/>
<Text fontSize="xs" color="whiteAlpha.800"></Text>
<Input
type="date"
value={customEndDate}
onChange={(e) => 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)',
}
}}
/>
<Button
size="xs"
bg="whiteAlpha.300"
color="white"
_hover={{ bg: 'whiteAlpha.400' }}
onClick={applyCustomRange}
borderRadius="md"
px={3}
>
应用
</Button>
</Flex>
)}
</VStack>
</Box>
{/* 主内容卡片 - 深色玻璃态 */}
<Box bg={bg} backdropFilter="blur(20px)" borderRadius="xl" border="1px" borderColor={borderColor} overflow="hidden">
<Tabs
index={activeTab}
onChange={(index) => {
const tabNames = ['涨幅榜', '跌幅榜', '波动榜', '连涨榜'];
// 🎯 追踪Tab切换
trackTabChanged(index, tabNames[index]);
setActiveTab(index);
}}
variant="unstyled"
size="sm"
>
<TabList
bg="rgba(15, 23, 42, 0.6)"
borderBottom="1px"
borderColor={borderColor}
overflowX="auto"
overflowY="hidden"
flexWrap="nowrap"
css={{
'&::-webkit-scrollbar': {
display: 'none',
},
}}
>
{tabsData.map((tab, index) => (
<Tab
key={index}
minW="fit-content"
fontSize="xs"
px={3}
py={3}
whiteSpace="nowrap"
color="whiteAlpha.700"
_selected={{
bg: `${tab.color}.500`,
color: 'white',
borderRadius: '0',
position: 'relative',
_after: {
content: '""',
position: 'absolute',
bottom: '-1px',
left: '0',
right: '0',
height: '2px',
bg: `${tab.color}.500`,
}
}}
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
transition="all 0.2s"
>
<HStack spacing={1}>
<Icon as={tab.icon} boxSize={3} />
<Text fontWeight="medium">{tab.label}</Text>
</HStack>
</Tab>
))}
</TabList>
<TabPanels>
{tabsData.map((tab, index) => (
<TabPanel key={index} px={0} py={0}>
<StatsCard
title={tab.label}
icon={tab.icon}
color={tab.color}
data={tab.data}
renderItem={tab.renderItem}
isLoading={loading}
/>
</TabPanel>
))}
</TabPanels>
</Tabs>
</Box>
</Box>
);
};
export default ConceptStatsPanel;