update pay ui
This commit is contained in:
709
src/views/Concept/components/HierarchyView.js
Normal file
709
src/views/Concept/components/HierarchyView.js
Normal file
@@ -0,0 +1,709 @@
|
||||
/**
|
||||
* HierarchyView - 概念层级思维导图视图
|
||||
*
|
||||
* 功能:
|
||||
* 1. 思维导图式展示概念层级结构(lv1 → lv2 → lv3 → concepts)
|
||||
* 2. 显示各层级的涨跌幅数据和概念数量
|
||||
* 3. 点击分类后切换到列表视图显示该分类下的概念
|
||||
*/
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Badge,
|
||||
Icon,
|
||||
Spinner,
|
||||
Center,
|
||||
Flex,
|
||||
Collapse,
|
||||
useBreakpointValue,
|
||||
Tooltip,
|
||||
Tag,
|
||||
TagLabel,
|
||||
Wrap,
|
||||
WrapItem,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
ChevronDownIcon,
|
||||
} from '@chakra-ui/icons';
|
||||
import {
|
||||
FaLayerGroup,
|
||||
FaArrowUp,
|
||||
FaArrowDown,
|
||||
FaTags,
|
||||
FaChartLine,
|
||||
FaBrain,
|
||||
FaMicrochip,
|
||||
FaRobot,
|
||||
FaMobileAlt,
|
||||
FaCar,
|
||||
FaBolt,
|
||||
FaPlane,
|
||||
FaShieldAlt,
|
||||
FaLandmark,
|
||||
FaFlask,
|
||||
FaShoppingCart,
|
||||
FaCoins,
|
||||
FaGlobe,
|
||||
FaHeartbeat,
|
||||
FaAtom,
|
||||
} from 'react-icons/fa';
|
||||
import { keyframes } from '@emotion/react';
|
||||
import { logger } from '../../../utils/logger';
|
||||
|
||||
// 脉冲动画
|
||||
const pulseAnimation = keyframes`
|
||||
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.4); }
|
||||
70% { transform: scale(1.02); box-shadow: 0 0 0 10px rgba(139, 92, 246, 0); }
|
||||
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(139, 92, 246, 0); }
|
||||
`;
|
||||
|
||||
// 连接线动画
|
||||
const flowAnimation = keyframes`
|
||||
0% { stroke-dashoffset: 20; }
|
||||
100% { stroke-dashoffset: 0; }
|
||||
`;
|
||||
|
||||
// 一级分类图标映射
|
||||
const LV1_ICONS = {
|
||||
'人工智能': FaBrain,
|
||||
'半导体': FaMicrochip,
|
||||
'机器人': FaRobot,
|
||||
'消费电子': FaMobileAlt,
|
||||
'智能驾驶与汽车': FaCar,
|
||||
'新能源与电力': FaBolt,
|
||||
'空天经济': FaPlane,
|
||||
'国防军工': FaShieldAlt,
|
||||
'政策与主题': FaLandmark,
|
||||
'周期与材料': FaFlask,
|
||||
'大消费': FaShoppingCart,
|
||||
'数字经济与金融科技': FaCoins,
|
||||
'全球宏观与贸易': FaGlobe,
|
||||
'医药健康': FaHeartbeat,
|
||||
'前沿科技': FaAtom,
|
||||
};
|
||||
|
||||
// 一级分类颜色映射
|
||||
const LV1_COLORS = {
|
||||
'人工智能': { bg: 'purple', gradient: 'linear(135deg, #667eea 0%, #764ba2 100%)' },
|
||||
'半导体': { bg: 'blue', gradient: 'linear(135deg, #4facfe 0%, #00f2fe 100%)' },
|
||||
'机器人': { bg: 'cyan', gradient: 'linear(135deg, #43e97b 0%, #38f9d7 100%)' },
|
||||
'消费电子': { bg: 'pink', gradient: 'linear(135deg, #fa709a 0%, #fee140 100%)' },
|
||||
'智能驾驶与汽车': { bg: 'orange', gradient: 'linear(135deg, #f093fb 0%, #f5576c 100%)' },
|
||||
'新能源与电力': { bg: 'green', gradient: 'linear(135deg, #11998e 0%, #38ef7d 100%)' },
|
||||
'空天经济': { bg: 'teal', gradient: 'linear(135deg, #667eea 0%, #764ba2 100%)' },
|
||||
'国防军工': { bg: 'red', gradient: 'linear(135deg, #eb3349 0%, #f45c43 100%)' },
|
||||
'政策与主题': { bg: 'yellow', gradient: 'linear(135deg, #f6d365 0%, #fda085 100%)' },
|
||||
'周期与材料': { bg: 'gray', gradient: 'linear(135deg, #bdc3c7 0%, #2c3e50 100%)' },
|
||||
'大消费': { bg: 'pink', gradient: 'linear(135deg, #ff758c 0%, #ff7eb3 100%)' },
|
||||
'数字经济与金融科技': { bg: 'blue', gradient: 'linear(135deg, #4776e6 0%, #8e54e9 100%)' },
|
||||
'全球宏观与贸易': { bg: 'teal', gradient: 'linear(135deg, #00cdac 0%, #8ddad5 100%)' },
|
||||
'医药健康': { bg: 'green', gradient: 'linear(135deg, #56ab2f 0%, #a8e063 100%)' },
|
||||
'前沿科技': { bg: 'purple', gradient: 'linear(135deg, #a18cd1 0%, #fbc2eb 100%)' },
|
||||
};
|
||||
|
||||
// 获取涨跌幅颜色
|
||||
const getChangeColor = (value) => {
|
||||
if (value === null || value === undefined) return 'gray';
|
||||
return value > 0 ? 'red' : value < 0 ? 'green' : 'gray';
|
||||
};
|
||||
|
||||
// 格式化涨跌幅
|
||||
const formatChangePercent = (value) => {
|
||||
if (value === null || value === undefined) return null;
|
||||
const formatted = value.toFixed(2);
|
||||
return value > 0 ? `+${formatted}%` : `${formatted}%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 一级分类卡片组件
|
||||
*/
|
||||
const Lv1Card = ({
|
||||
item,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
onSelectCategory,
|
||||
stats
|
||||
}) => {
|
||||
const IconComponent = LV1_ICONS[item.name] || FaLayerGroup;
|
||||
const colorConfig = LV1_COLORS[item.name] || { bg: 'purple', gradient: 'linear(135deg, #667eea 0%, #764ba2 100%)' };
|
||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||
|
||||
// 从统计数据中获取涨跌幅
|
||||
const avgChange = stats?.avg_change_pct;
|
||||
const changeColor = getChangeColor(avgChange);
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
cursor="pointer"
|
||||
onClick={onToggle}
|
||||
transition="all 0.3s"
|
||||
_hover={{
|
||||
transform: 'scale(1.02)',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
bgGradient={colorConfig.gradient}
|
||||
borderRadius="2xl"
|
||||
p={{ base: 4, md: 6 }}
|
||||
color="white"
|
||||
boxShadow="0 10px 40px rgba(0, 0, 0, 0.2)"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
minW={{ base: '160px', md: '220px' }}
|
||||
animation={isExpanded ? `${pulseAnimation} 2s infinite` : 'none'}
|
||||
>
|
||||
{/* 背景装饰 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="-20%"
|
||||
right="-20%"
|
||||
width="60%"
|
||||
height="60%"
|
||||
borderRadius="full"
|
||||
bg="whiteAlpha.200"
|
||||
filter="blur(20px)"
|
||||
/>
|
||||
|
||||
<VStack spacing={2} align="center">
|
||||
<Icon as={IconComponent} boxSize={{ base: 8, md: 10 }} />
|
||||
<Text
|
||||
fontSize={{ base: 'md', md: 'lg' }}
|
||||
fontWeight="bold"
|
||||
textAlign="center"
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
|
||||
<HStack spacing={3}>
|
||||
<Badge
|
||||
bg="whiteAlpha.300"
|
||||
color="white"
|
||||
px={2}
|
||||
py={1}
|
||||
borderRadius="full"
|
||||
fontSize="xs"
|
||||
>
|
||||
{item.concept_count} 概念
|
||||
</Badge>
|
||||
|
||||
{avgChange !== null && avgChange !== undefined && (
|
||||
<Badge
|
||||
bg={changeColor === 'red' ? 'red.500' : changeColor === 'green' ? 'green.500' : 'gray.500'}
|
||||
color="white"
|
||||
px={2}
|
||||
py={1}
|
||||
borderRadius="full"
|
||||
fontSize="xs"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
>
|
||||
<Icon
|
||||
as={avgChange > 0 ? FaArrowUp : FaArrowDown}
|
||||
boxSize={2}
|
||||
/>
|
||||
{formatChangePercent(avgChange)}
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<Icon
|
||||
as={isExpanded ? ChevronDownIcon : ChevronRightIcon}
|
||||
boxSize={5}
|
||||
mt={1}
|
||||
/>
|
||||
</VStack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 二级分类卡片组件
|
||||
*/
|
||||
const Lv2Card = ({
|
||||
item,
|
||||
parentName,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
onSelectCategory,
|
||||
stats,
|
||||
colorConfig
|
||||
}) => {
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const avgChange = stats?.avg_change_pct;
|
||||
const changeColor = getChangeColor(avgChange);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box
|
||||
bg="white"
|
||||
borderRadius="xl"
|
||||
p={{ base: 3, md: 4 }}
|
||||
boxShadow="0 4px 20px rgba(0, 0, 0, 0.08)"
|
||||
border="2px solid"
|
||||
borderColor={`${colorConfig.bg}.200`}
|
||||
cursor="pointer"
|
||||
onClick={() => hasChildren ? onToggle() : onSelectCategory(parentName, item.name, null)}
|
||||
transition="all 0.2s"
|
||||
_hover={{
|
||||
borderColor: `${colorConfig.bg}.400`,
|
||||
transform: 'translateX(4px)',
|
||||
boxShadow: '0 6px 25px rgba(0, 0, 0, 0.12)',
|
||||
}}
|
||||
>
|
||||
<Flex align="center" justify="space-between">
|
||||
<HStack spacing={3}>
|
||||
<Box
|
||||
w={2}
|
||||
h={8}
|
||||
borderRadius="full"
|
||||
bgGradient={colorConfig.gradient}
|
||||
/>
|
||||
<VStack align="start" spacing={0}>
|
||||
<Text fontWeight="bold" color="gray.800" fontSize={{ base: 'sm', md: 'md' }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<HStack spacing={2}>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{item.concept_count} 概念
|
||||
</Text>
|
||||
{avgChange !== null && avgChange !== undefined && (
|
||||
<Badge
|
||||
colorScheme={changeColor}
|
||||
size="sm"
|
||||
fontSize="xs"
|
||||
>
|
||||
{formatChangePercent(avgChange)}
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
</HStack>
|
||||
|
||||
{hasChildren ? (
|
||||
<Icon
|
||||
as={isExpanded ? ChevronDownIcon : ChevronRightIcon}
|
||||
color={`${colorConfig.bg}.500`}
|
||||
boxSize={5}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
as={FaTags}
|
||||
color={`${colorConfig.bg}.400`}
|
||||
boxSize={4}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* 三级分类展开 */}
|
||||
{hasChildren && (
|
||||
<Collapse in={isExpanded} animateOpacity>
|
||||
<VStack
|
||||
spacing={2}
|
||||
pl={{ base: 4, md: 6 }}
|
||||
mt={2}
|
||||
align="stretch"
|
||||
>
|
||||
{item.children.map((lv3Item) => (
|
||||
<Lv3Card
|
||||
key={lv3Item.id}
|
||||
item={lv3Item}
|
||||
parentLv1={parentName}
|
||||
parentLv2={item.name}
|
||||
onSelectCategory={onSelectCategory}
|
||||
colorConfig={colorConfig}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</Collapse>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 三级分类卡片组件
|
||||
*/
|
||||
const Lv3Card = ({
|
||||
item,
|
||||
parentLv1,
|
||||
parentLv2,
|
||||
onSelectCategory,
|
||||
colorConfig
|
||||
}) => {
|
||||
return (
|
||||
<Box
|
||||
bg={`${colorConfig.bg}.50`}
|
||||
borderRadius="lg"
|
||||
p={{ base: 2, md: 3 }}
|
||||
border="1px solid"
|
||||
borderColor={`${colorConfig.bg}.100`}
|
||||
cursor="pointer"
|
||||
onClick={() => onSelectCategory(parentLv1, parentLv2, item.name)}
|
||||
transition="all 0.2s"
|
||||
_hover={{
|
||||
bg: `${colorConfig.bg}.100`,
|
||||
transform: 'translateX(4px)',
|
||||
}}
|
||||
>
|
||||
<Flex align="center" justify="space-between">
|
||||
<HStack spacing={2}>
|
||||
<Icon as={FaChartLine} color={`${colorConfig.bg}.500`} boxSize={3} />
|
||||
<Text fontSize="sm" fontWeight="medium" color="gray.700">
|
||||
{item.name}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Badge
|
||||
colorScheme={colorConfig.bg}
|
||||
size="sm"
|
||||
fontSize="xs"
|
||||
>
|
||||
{item.concept_count}
|
||||
</Badge>
|
||||
</Flex>
|
||||
|
||||
{/* 概念标签预览 */}
|
||||
{item.concepts && item.concepts.length > 0 && (
|
||||
<Wrap spacing={1} mt={2}>
|
||||
{item.concepts.slice(0, 5).map((concept, idx) => (
|
||||
<WrapItem key={idx}>
|
||||
<Tag
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
colorScheme={colorConfig.bg}
|
||||
borderRadius="full"
|
||||
>
|
||||
<TagLabel fontSize="xs">{concept}</TagLabel>
|
||||
</Tag>
|
||||
</WrapItem>
|
||||
))}
|
||||
{item.concepts.length > 5 && (
|
||||
<WrapItem>
|
||||
<Text fontSize="xs" color={`${colorConfig.bg}.600`}>
|
||||
+{item.concepts.length - 5}
|
||||
</Text>
|
||||
</WrapItem>
|
||||
)}
|
||||
</Wrap>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 思维导图连接线 SVG
|
||||
*/
|
||||
const ConnectionLine = ({ from, to, isActive }) => {
|
||||
return (
|
||||
<svg
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
}}
|
||||
>
|
||||
<line
|
||||
x1={from.x}
|
||||
y1={from.y}
|
||||
x2={to.x}
|
||||
y2={to.y}
|
||||
stroke={isActive ? '#8B5CF6' : '#E2E8F0'}
|
||||
strokeWidth={isActive ? 3 : 2}
|
||||
strokeDasharray={isActive ? '0' : '5,5'}
|
||||
style={{
|
||||
animation: isActive ? `${flowAnimation} 0.5s linear` : 'none',
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 主组件:层级视图
|
||||
*/
|
||||
const HierarchyView = ({
|
||||
apiBaseUrl,
|
||||
onSelectCategory,
|
||||
selectedDate,
|
||||
}) => {
|
||||
const [hierarchy, setHierarchy] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [expandedLv1, setExpandedLv1] = useState(null);
|
||||
const [expandedLv2, setExpandedLv2] = useState({});
|
||||
const [hierarchyStats, setHierarchyStats] = useState(null);
|
||||
|
||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||
|
||||
// 获取层级结构数据
|
||||
const fetchHierarchy = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/hierarchy`);
|
||||
if (!response.ok) throw new Error('获取层级结构失败');
|
||||
|
||||
const data = await response.json();
|
||||
setHierarchy(data.hierarchy || []);
|
||||
|
||||
logger.info('HierarchyView', '层级结构加载完成', {
|
||||
totalLv1: data.hierarchy?.length,
|
||||
totalConcepts: data.total_concepts
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('HierarchyView', 'fetchHierarchy', err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [apiBaseUrl]);
|
||||
|
||||
// 获取层级统计数据(包含涨跌幅)
|
||||
const fetchHierarchyStats = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/statistics/hierarchy`);
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
setHierarchyStats(data);
|
||||
|
||||
logger.info('HierarchyView', '层级统计加载完成', {
|
||||
totalLv1: data.total_lv1,
|
||||
totalConcepts: data.total_concepts
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn('HierarchyView', '获取层级统计失败', { error: err.message });
|
||||
}
|
||||
}, [apiBaseUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchHierarchy();
|
||||
fetchHierarchyStats();
|
||||
}, [fetchHierarchy, fetchHierarchyStats]);
|
||||
|
||||
// 获取某个 lv1 的统计数据
|
||||
const getLv1Stats = useCallback((lv1Name) => {
|
||||
if (!hierarchyStats?.statistics) return null;
|
||||
return hierarchyStats.statistics.find(s => s.lv1 === lv1Name);
|
||||
}, [hierarchyStats]);
|
||||
|
||||
// 切换一级分类展开状态
|
||||
const toggleLv1 = useCallback((lv1Id) => {
|
||||
setExpandedLv1(prev => prev === lv1Id ? null : lv1Id);
|
||||
setExpandedLv2({});
|
||||
}, []);
|
||||
|
||||
// 切换二级分类展开状态
|
||||
const toggleLv2 = useCallback((lv2Id) => {
|
||||
setExpandedLv2(prev => ({
|
||||
...prev,
|
||||
[lv2Id]: !prev[lv2Id]
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// 处理分类选择
|
||||
const handleSelectCategory = useCallback((lv1, lv2, lv3) => {
|
||||
logger.info('HierarchyView', '选择分类', { lv1, lv2, lv3 });
|
||||
onSelectCategory && onSelectCategory({ lv1, lv2, lv3 });
|
||||
}, [onSelectCategory]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Center h="400px">
|
||||
<VStack spacing={4}>
|
||||
<Spinner size="xl" color="purple.500" thickness="4px" />
|
||||
<Text color="gray.600">正在加载概念层级...</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Center h="400px">
|
||||
<VStack spacing={4}>
|
||||
<Icon as={FaLayerGroup} boxSize={16} color="gray.300" />
|
||||
<Text color="gray.600">加载失败:{error}</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
{/* 标题 */}
|
||||
<VStack spacing={2} mb={6} textAlign="center">
|
||||
<HStack spacing={2}>
|
||||
<Icon as={FaLayerGroup} color="purple.500" boxSize={6} />
|
||||
<Text fontSize="xl" fontWeight="bold" color="gray.800">
|
||||
概念层级导航
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
点击分类展开查看,点击具体类目筛选概念列表
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
{/* 思维导图布局 */}
|
||||
<Box
|
||||
position="relative"
|
||||
overflowX="auto"
|
||||
pb={4}
|
||||
css={{
|
||||
'&::-webkit-scrollbar': {
|
||||
height: '8px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: '#f1f1f1',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: '#c1c1c1',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* 一级分类网格 - 居中展示 */}
|
||||
<Flex
|
||||
wrap="wrap"
|
||||
justify="center"
|
||||
gap={{ base: 3, md: 4 }}
|
||||
mb={6}
|
||||
>
|
||||
{hierarchy.map((lv1Item) => (
|
||||
<Lv1Card
|
||||
key={lv1Item.id}
|
||||
item={lv1Item}
|
||||
isExpanded={expandedLv1 === lv1Item.id}
|
||||
onToggle={() => toggleLv1(lv1Item.id)}
|
||||
onSelectCategory={handleSelectCategory}
|
||||
stats={getLv1Stats(lv1Item.name)}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
{/* 展开的二级分类 */}
|
||||
{expandedLv1 && (
|
||||
<Box
|
||||
bg="gray.50"
|
||||
borderRadius="2xl"
|
||||
p={{ base: 4, md: 6 }}
|
||||
border="2px dashed"
|
||||
borderColor="purple.200"
|
||||
animation={`${pulseAnimation} 0.5s ease-out`}
|
||||
>
|
||||
{(() => {
|
||||
const lv1Item = hierarchy.find(h => h.id === expandedLv1);
|
||||
if (!lv1Item) return null;
|
||||
|
||||
const colorConfig = LV1_COLORS[lv1Item.name] || { bg: 'purple', gradient: 'linear(135deg, #667eea 0%, #764ba2 100%)' };
|
||||
|
||||
return (
|
||||
<VStack spacing={4} align="stretch">
|
||||
{/* 展开分类的标题 */}
|
||||
<HStack spacing={3} mb={2}>
|
||||
<Box
|
||||
w={4}
|
||||
h={4}
|
||||
borderRadius="full"
|
||||
bgGradient={colorConfig.gradient}
|
||||
/>
|
||||
<Text fontSize="lg" fontWeight="bold" color="gray.700">
|
||||
{lv1Item.name}
|
||||
</Text>
|
||||
<Badge colorScheme={colorConfig.bg}>
|
||||
{lv1Item.children?.length || 0} 个子分类
|
||||
</Badge>
|
||||
|
||||
{/* 点击筛选该一级分类 */}
|
||||
<Badge
|
||||
colorScheme="purple"
|
||||
cursor="pointer"
|
||||
onClick={() => handleSelectCategory(lv1Item.name, null, null)}
|
||||
_hover={{ opacity: 0.8 }}
|
||||
>
|
||||
筛选全部 →
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
{/* 二级分类列表 */}
|
||||
<Flex
|
||||
wrap="wrap"
|
||||
gap={3}
|
||||
>
|
||||
{lv1Item.children?.map((lv2Item) => (
|
||||
<Box
|
||||
key={lv2Item.id}
|
||||
flex={{ base: '1 1 100%', md: '1 1 calc(50% - 12px)', lg: '1 1 calc(33.333% - 12px)' }}
|
||||
minW={{ base: '100%', md: '280px' }}
|
||||
>
|
||||
<Lv2Card
|
||||
item={lv2Item}
|
||||
parentName={lv1Item.name}
|
||||
isExpanded={expandedLv2[lv2Item.id]}
|
||||
onToggle={() => toggleLv2(lv2Item.id)}
|
||||
onSelectCategory={handleSelectCategory}
|
||||
colorConfig={colorConfig}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
</VStack>
|
||||
);
|
||||
})()}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<Flex
|
||||
justify="center"
|
||||
mt={6}
|
||||
gap={4}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<Badge
|
||||
colorScheme="purple"
|
||||
px={4}
|
||||
py={2}
|
||||
borderRadius="full"
|
||||
fontSize="sm"
|
||||
>
|
||||
共 {hierarchy.length} 个一级分类
|
||||
</Badge>
|
||||
<Badge
|
||||
colorScheme="blue"
|
||||
px={4}
|
||||
py={2}
|
||||
borderRadius="full"
|
||||
fontSize="sm"
|
||||
>
|
||||
{hierarchy.reduce((acc, h) => acc + (h.children?.length || 0), 0)} 个二级分类
|
||||
</Badge>
|
||||
<Badge
|
||||
colorScheme="cyan"
|
||||
px={4}
|
||||
py={2}
|
||||
borderRadius="full"
|
||||
fontSize="sm"
|
||||
>
|
||||
{hierarchy.reduce((acc, h) => acc + h.concept_count, 0)} 个概念
|
||||
</Badge>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default HierarchyView;
|
||||
Reference in New Issue
Block a user