Files
vf_react/src/views/Concept/components/ConceptStatsPanel.js
zdl ea627f867e feat:添加mock接口
1.  Profile 和 Settings 页面(2个文件)
  2.  EventDetail 页面(1个文件)
  3.  身份验证组件(WechatRegister.js)
  4.  Company 页面(CompanyOverview, index, FinancialPanorama, MarketDataView)
  5.  Concept 页面(ConceptTimelineModal, ConceptStatsPanel, index)
2025-10-18 08:46:56 +08:00

919 lines
39 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 {
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) {
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 },
],
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;