Initial commit

This commit is contained in:
2025-10-11 11:55:25 +08:00
parent 467dad8449
commit 8107dee8d3
2879 changed files with 610575 additions and 0 deletions

View File

@@ -0,0 +1,921 @@
import React, { useState, useEffect } from 'react';
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,
FaFire,
FaChartLine,
FaNewspaper,
FaRocket,
FaBolt,
FaEye,
FaCrown,
FaThermometerHalf,
} 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'
? '/concept-api'
: 'http://111.198.58.126:16801';
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 = useColorModeValue('white', 'gray.800');
const cardBg = useColorModeValue('gray.50', 'gray.700');
const borderColor = useColorModeValue('gray.200', 'gray.600');
// 获取统计数据
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) {
console.warn('concept-api路由失败尝试直接访问:', conceptApiError.message);
// 备用方案直接访问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);
return;
} else {
throw new Error(result.note || '直接API返回错误');
}
} else {
throw new Error(`直接API错误: ${response.status}`);
}
} catch (directError) {
console.error('所有API都失败:', directError);
throw new Error('无法访问概念统计API');
}
}
} catch (error) {
console.error('获取统计数据失败:', error);
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 },
],
active_concepts: [
{ name: '人工智能', news_count: 89, report_count: 15, total_mentions: 104 },
{ name: '芯片概念', news_count: 76, report_count: 12, total_mentions: 88 },
{ name: '新能源汽车', news_count: 65, report_count: 18, total_mentions: 83 },
{ name: '生物医药', news_count: 54, report_count: 9, total_mentions: 63 },
{ name: '量子科技', news_count: 41, report_count: 7, total_mentions: 48 },
],
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]);
} else {
setUseCustomRange(false);
setTimeRange(parseInt(newRange));
fetchStatsData(parseInt(newRange));
}
};
// 应用自定义日期范围
const applyCustomRange = () => {
if (customStartDate && customEndDate) {
if (new Date(customStartDate) > new Date(customEndDate)) {
toast({
title: '日期选择错误',
description: '开始日期不能晚于结束日期',
status: 'error',
duration: 3000,
});
return;
}
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="gray.50" borderRadius="lg">
<HStack spacing={2} flex={1}>
<Skeleton height="20px" width="20px" borderRadius="full" />
<VStack align="start" spacing={1} flex={1}>
<Skeleton height="14px" width="80%" />
<Skeleton height="12px" width="60%" />
</VStack>
</HStack>
<Skeleton height="20px" width="50px" borderRadius="md" />
</HStack>
))}
</VStack>
) : (
<VStack spacing={2} align="stretch" maxH="400px" overflowY="auto"
css={{
'&::-webkit-scrollbar': {
width: '4px',
},
'&::-webkit-scrollbar-track': {
background: '#f1f1f1',
borderRadius: '10px',
},
'&::-webkit-scrollbar-thumb': {
background: '#c1c1c1',
borderRadius: '10px',
},
'&::-webkit-scrollbar-thumb:hover': {
background: '#a8a8a8',
},
}}
>
{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 ? 'red.50' : 'gray.50'}
border="1px solid"
borderColor={index < 3 ? 'red.100' : 'gray.200'}
_hover={{
transform: 'translateY(-1px)',
shadow: 'md',
cursor: 'pointer',
bg: index < 3 ? 'red.100' : 'gray.100'
}}
transition="all 0.2s"
onClick={() => onConceptClick?.(null, item.name)}
>
<HStack spacing={3} flex={1}>
<Box position="relative">
<Badge
colorScheme={index === 0 ? 'yellow' : index === 1 ? 'orange' : index === 2 ? 'red' : 'gray'}
variant="solid"
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.500"
boxSize={3}
/>
)}
</Box>
<VStack align="start" spacing={0} flex={1}>
<Text fontSize="sm" fontWeight="bold" noOfLines={1} color="gray.800">
{item.name}
</Text>
<HStack spacing={2} fontSize="xs" color="gray.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
colorScheme="red"
variant="solid"
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 ? 'green.50' : 'gray.50'}
border="1px solid"
borderColor={index < 3 ? 'green.100' : 'gray.200'}
_hover={{
transform: 'translateY(-1px)',
shadow: 'md',
cursor: 'pointer',
bg: index < 3 ? 'green.100' : 'gray.100'
}}
transition="all 0.2s"
onClick={() => onConceptClick?.(null, item.name)}
>
<HStack spacing={3} flex={1}>
<Badge
colorScheme={index < 3 ? 'green' : 'gray'}
variant="solid"
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="gray.800">
{item.name}
</Text>
<HStack spacing={2} fontSize="xs" color="gray.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
colorScheme="green"
variant="solid"
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: FaFire,
color: 'orange',
data: statsData.active_concepts,
renderItem: (item, index) => (
<Flex
justify="space-between"
align="center"
p={3}
borderRadius="xl"
bg={index < 3 ? 'orange.50' : 'gray.50'}
border="1px solid"
borderColor={index < 3 ? 'orange.100' : 'gray.200'}
_hover={{
transform: 'translateY(-1px)',
shadow: 'md',
cursor: 'pointer',
bg: index < 3 ? 'orange.100' : 'gray.100'
}}
transition="all 0.2s"
onClick={() => onConceptClick?.(null, item.name)}
>
<HStack spacing={3} flex={1}>
<Box position="relative">
<Badge
colorScheme={index === 0 ? 'orange' : index < 3 ? 'yellow' : 'gray'}
variant="solid"
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={FaFire}
position="absolute"
top="-8px"
right="-8px"
color="orange.500"
boxSize={3}
/>
)}
</Box>
<VStack align="start" spacing={0} flex={1}>
<Text fontSize="sm" fontWeight="bold" noOfLines={1} color="gray.800">
{item.name}
</Text>
<HStack spacing={2} fontSize="xs" color="gray.600">
<HStack spacing={1}>
<Icon as={FaNewspaper} boxSize={2.5} />
<Text>{item.news_count}</Text>
</HStack>
<Text>·</Text>
<HStack spacing={1}>
<Icon as={FaEye} boxSize={2.5} />
<Text>{item.report_count}</Text>
</HStack>
</HStack>
</VStack>
</HStack>
<Badge
colorScheme="orange"
variant="solid"
borderRadius="lg"
fontSize="xs"
fontWeight="bold"
px={2}
py={1}
>
<Icon as={FaFire} boxSize={2} mr={1} />
{item.total_mentions}
</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 ? 'purple.50' : 'gray.50'}
border="1px solid"
borderColor={index < 3 ? 'purple.100' : 'gray.200'}
_hover={{
transform: 'translateY(-1px)',
shadow: 'md',
cursor: 'pointer',
bg: index < 3 ? 'purple.100' : 'gray.100'
}}
transition="all 0.2s"
onClick={() => onConceptClick?.(null, item.name)}
>
<HStack spacing={3} flex={1}>
<Box position="relative">
<Badge
colorScheme={index < 3 ? 'purple' : 'gray'}
variant="solid"
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.500"
boxSize={3}
/>
)}
</Box>
<VStack align="start" spacing={0} flex={1}>
<Text fontSize="sm" fontWeight="bold" noOfLines={1} color="gray.800">
{item.name}
</Text>
<Text fontSize="xs" color="gray.600">
均幅 {formatChange(item.avg_change)}
</Text>
</VStack>
</HStack>
<Badge
colorScheme="purple"
variant="solid"
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 ? 'cyan.50' : 'gray.50'}
border="1px solid"
borderColor={index < 3 ? 'cyan.100' : 'gray.200'}
_hover={{
transform: 'translateY(-1px)',
shadow: 'md',
cursor: 'pointer',
bg: index < 3 ? 'cyan.100' : 'gray.100'
}}
transition="all 0.2s"
onClick={() => onConceptClick?.(null, item.name)}
>
<HStack spacing={3} flex={1}>
<Box position="relative">
<Badge
colorScheme={index === 0 ? 'cyan' : index < 3 ? 'blue' : 'gray'}
variant="solid"
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.500"
boxSize={3}
/>
)}
</Box>
<VStack align="start" spacing={0} flex={1}>
<Text fontSize="sm" fontWeight="bold" noOfLines={1} color="gray.800">
{item.name}
</Text>
<Text fontSize="xs" color="gray.600">
累计 {formatChange(item.total_change)}
</Text>
</VStack>
</HStack>
<Badge
colorScheme="cyan"
variant="solid"
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="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
p={4}
borderRadius="xl"
mb={4}
position="relative"
overflow="hidden"
>
{/* 背景装饰 */}
<Box
position="absolute"
top="-20px"
right="-20px"
width="80px"
height="80px"
borderRadius="full"
bg="whiteAlpha.200"
filter="blur(10px)"
/>
<Box
position="absolute"
bottom="-10px"
left="-10px"
width="60px"
height="60px"
borderRadius="full"
bg="whiteAlpha.100"
filter="blur(8px)"
/>
<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.200" borderRadius="lg">
<Icon as={FaChartLine} color="white" 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} borderRadius="xl" border="1px" borderColor={borderColor} shadow="sm" overflow="hidden">
<Tabs index={activeTab} onChange={setActiveTab} variant="unstyled" size="sm">
<TabList
bg="gray.50"
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"
_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: `${tab.color}.50` }}
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;