update pay ui
This commit is contained in:
170
src/views/Concept/components/BreadcrumbNav.js
Normal file
170
src/views/Concept/components/BreadcrumbNav.js
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* BreadcrumbNav - 层级筛选面包屑导航
|
||||
*
|
||||
* 功能:
|
||||
* 1. 显示当前选中的层级路径
|
||||
* 2. 支持点击返回上级或清除筛选
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
HStack,
|
||||
Text,
|
||||
Icon,
|
||||
Badge,
|
||||
IconButton,
|
||||
Flex,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { ChevronRightIcon, CloseIcon } from '@chakra-ui/icons';
|
||||
import { FaLayerGroup, FaFilter, FaTimes, FaHome } from 'react-icons/fa';
|
||||
|
||||
const BreadcrumbNav = ({
|
||||
filter,
|
||||
onClearFilter,
|
||||
onNavigate,
|
||||
}) => {
|
||||
// 如果没有筛选条件,不显示
|
||||
if (!filter || (!filter.lv1 && !filter.lv2 && !filter.lv3)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const breadcrumbs = [];
|
||||
|
||||
// 构建面包屑路径
|
||||
if (filter.lv1) {
|
||||
breadcrumbs.push({
|
||||
label: filter.lv1,
|
||||
level: 'lv1',
|
||||
onClick: () => onNavigate({ lv1: filter.lv1, lv2: null, lv3: null }),
|
||||
});
|
||||
}
|
||||
|
||||
if (filter.lv2) {
|
||||
breadcrumbs.push({
|
||||
label: filter.lv2,
|
||||
level: 'lv2',
|
||||
onClick: () => onNavigate({ lv1: filter.lv1, lv2: filter.lv2, lv3: null }),
|
||||
});
|
||||
}
|
||||
|
||||
if (filter.lv3) {
|
||||
breadcrumbs.push({
|
||||
label: filter.lv3,
|
||||
level: 'lv3',
|
||||
onClick: null, // 最后一级不可点击
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
bg="linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%)"
|
||||
borderRadius="xl"
|
||||
p={{ base: 3, md: 4 }}
|
||||
mb={4}
|
||||
border="1px solid"
|
||||
borderColor="purple.200"
|
||||
>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
flexWrap="wrap"
|
||||
gap={2}
|
||||
>
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
{/* 筛选图标 */}
|
||||
<HStack
|
||||
spacing={2}
|
||||
bg="purple.500"
|
||||
color="white"
|
||||
px={3}
|
||||
py={1}
|
||||
borderRadius="full"
|
||||
>
|
||||
<Icon as={FaFilter} boxSize={3} />
|
||||
<Text fontSize="sm" fontWeight="bold">
|
||||
层级筛选
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 首页入口 */}
|
||||
<Tooltip label="返回全部概念" placement="top">
|
||||
<Badge
|
||||
colorScheme="gray"
|
||||
px={3}
|
||||
py={1}
|
||||
borderRadius="full"
|
||||
cursor="pointer"
|
||||
onClick={onClearFilter}
|
||||
_hover={{
|
||||
bg: 'gray.200',
|
||||
}}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
>
|
||||
<Icon as={FaHome} boxSize={3} />
|
||||
全部
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
|
||||
{/* 面包屑路径 */}
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
<React.Fragment key={crumb.level}>
|
||||
<Icon as={ChevronRightIcon} color="gray.400" boxSize={4} />
|
||||
<Badge
|
||||
colorScheme={
|
||||
crumb.level === 'lv1' ? 'purple' :
|
||||
crumb.level === 'lv2' ? 'blue' : 'cyan'
|
||||
}
|
||||
px={3}
|
||||
py={1}
|
||||
borderRadius="full"
|
||||
cursor={crumb.onClick ? 'pointer' : 'default'}
|
||||
onClick={crumb.onClick}
|
||||
_hover={crumb.onClick ? {
|
||||
opacity: 0.8,
|
||||
transform: 'scale(1.05)',
|
||||
} : {}}
|
||||
transition="all 0.2s"
|
||||
fontWeight={index === breadcrumbs.length - 1 ? 'bold' : 'medium'}
|
||||
>
|
||||
{crumb.label}
|
||||
</Badge>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</HStack>
|
||||
|
||||
{/* 清除筛选按钮 */}
|
||||
<Tooltip label="清除筛选" placement="top">
|
||||
<IconButton
|
||||
size="sm"
|
||||
icon={<FaTimes />}
|
||||
colorScheme="red"
|
||||
variant="ghost"
|
||||
borderRadius="full"
|
||||
onClick={onClearFilter}
|
||||
aria-label="清除筛选"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
|
||||
{/* 筛选提示 */}
|
||||
<Text fontSize="xs" color="gray.500" mt={2}>
|
||||
当前显示「{breadcrumbs.map(b => b.label).join(' > ')}」分类下的概念,
|
||||
<Text
|
||||
as="span"
|
||||
color="purple.600"
|
||||
cursor="pointer"
|
||||
fontWeight="medium"
|
||||
onClick={onClearFilter}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
>
|
||||
点击清除筛选
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default BreadcrumbNav;
|
||||
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;
|
||||
@@ -81,11 +81,13 @@ import {
|
||||
useBreakpointValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { SearchIcon, ViewIcon, CalendarIcon, ExternalLinkIcon, StarIcon, ChevronDownIcon, InfoIcon, CloseIcon, ChevronRightIcon } from '@chakra-ui/icons';
|
||||
import { FaThLarge, FaList, FaTags, FaChartLine, FaRobot, FaTable, FaHistory, FaBrain, FaLightbulb, FaRocket, FaShieldAlt, FaCalendarAlt, FaArrowUp, FaArrowDown, FaNewspaper, FaFileAlt, FaExpand, FaCompress, FaClock, FaLock } from 'react-icons/fa';
|
||||
import { FaThLarge, FaList, FaTags, FaChartLine, FaRobot, FaTable, FaHistory, FaBrain, FaLightbulb, FaRocket, FaShieldAlt, FaCalendarAlt, FaArrowUp, FaArrowDown, FaNewspaper, FaFileAlt, FaExpand, FaCompress, FaClock, FaLock, FaSitemap, FaLayerGroup } from 'react-icons/fa';
|
||||
import { BsGraphUp, BsLightningFill } from 'react-icons/bs';
|
||||
import { keyframes } from '@emotion/react';
|
||||
import ConceptTimelineModal from './ConceptTimelineModal';
|
||||
import ConceptStatsPanel from './components/ConceptStatsPanel';
|
||||
import HierarchyView from './components/HierarchyView';
|
||||
import BreadcrumbNav from './components/BreadcrumbNav';
|
||||
import ConceptStocksModal from '@components/ConceptStocksModal';
|
||||
import TradeDatePicker from '@components/TradeDatePicker';
|
||||
// 导航栏已由 MainLayout 提供,无需在此导入
|
||||
@@ -161,7 +163,10 @@ const ConceptCenter = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalConcepts, setTotalConcepts] = useState(0);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [viewMode, setViewMode] = useState('grid');
|
||||
const [viewMode, setViewMode] = useState('list'); // 默认列表视图
|
||||
|
||||
// 层级筛选状态
|
||||
const [hierarchyFilter, setHierarchyFilter] = useState({ lv1: null, lv2: null, lv3: null });
|
||||
|
||||
// 日期相关状态
|
||||
const [selectedDate, setSelectedDate] = useState(null);
|
||||
@@ -253,7 +258,11 @@ const ConceptCenter = () => {
|
||||
sort: searchParams.get('sort') || defaultSort,
|
||||
page: parseInt(searchParams.get('page') || '1', 10),
|
||||
date: searchParams.get('date') || null,
|
||||
size: 12
|
||||
size: 12,
|
||||
// 层级筛选参数
|
||||
lv1: searchParams.get('lv1') || null,
|
||||
lv2: searchParams.get('lv2') || null,
|
||||
lv3: searchParams.get('lv3') || null,
|
||||
};
|
||||
}, [searchParams]);
|
||||
|
||||
@@ -271,7 +280,7 @@ const ConceptCenter = () => {
|
||||
}, [searchParams, setSearchParams]);
|
||||
|
||||
// 获取概念数据
|
||||
const fetchConcepts = useCallback(async (query = '', page = 1, date = selectedDate, customSortBy = null) => {
|
||||
const fetchConcepts = useCallback(async (query = '', page = 1, date = selectedDate, customSortBy = null, filter = hierarchyFilter) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const sortToUse = customSortBy !== null ? customSortBy : sortBy;
|
||||
@@ -287,6 +296,14 @@ const ConceptCenter = () => {
|
||||
requestBody.trade_date = date.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
// 添加层级筛选参数
|
||||
if (filter?.lv1) {
|
||||
requestBody.filter_lv1 = filter.lv1;
|
||||
}
|
||||
if (filter?.lv2) {
|
||||
requestBody.filter_lv2 = filter.lv2;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/search`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -308,24 +325,75 @@ const ConceptCenter = () => {
|
||||
setSelectedDate(new Date(data.price_date));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('ConceptCenter', 'fetchConcepts', error, { query, page, date: date?.toISOString(), sortToUse });
|
||||
logger.error('ConceptCenter', 'fetchConcepts', error, { query, page, date: date?.toISOString(), sortToUse, filter });
|
||||
|
||||
// ❌ 移除获取数据失败toast
|
||||
// toast({ title: '获取数据失败', description: error.message, status: 'error', duration: 3000, isClosable: true });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [pageSize, sortBy]);
|
||||
}, [pageSize, sortBy, hierarchyFilter]);
|
||||
|
||||
// 清除搜索
|
||||
const handleClearSearch = () => {
|
||||
setSearchQuery('');
|
||||
setSortBy('change_pct');
|
||||
setCurrentPage(1);
|
||||
updateUrlParams({ q: '', page: 1, sort: 'change_pct' });
|
||||
fetchConcepts('', 1, selectedDate, 'change_pct');
|
||||
setHierarchyFilter({ lv1: null, lv2: null, lv3: null });
|
||||
updateUrlParams({ q: '', page: 1, sort: 'change_pct', lv1: '', lv2: '', lv3: '' });
|
||||
fetchConcepts('', 1, selectedDate, 'change_pct', { lv1: null, lv2: null, lv3: null });
|
||||
};
|
||||
|
||||
// 处理层级筛选选择(从 HierarchyView 点击分类)
|
||||
const handleHierarchySelect = useCallback((filter) => {
|
||||
logger.info('ConceptCenter', '层级筛选选择', filter);
|
||||
|
||||
setHierarchyFilter(filter);
|
||||
setCurrentPage(1);
|
||||
setViewMode('list'); // 切换到列表视图
|
||||
|
||||
// 更新 URL 参数
|
||||
updateUrlParams({
|
||||
lv1: filter.lv1 || '',
|
||||
lv2: filter.lv2 || '',
|
||||
lv3: filter.lv3 || '',
|
||||
page: 1
|
||||
});
|
||||
|
||||
// 重新获取数据
|
||||
fetchConcepts(searchQuery, 1, selectedDate, sortBy, filter);
|
||||
|
||||
// 显示提示
|
||||
toast({
|
||||
title: '已应用筛选',
|
||||
description: `正在显示「${[filter.lv1, filter.lv2, filter.lv3].filter(Boolean).join(' > ')}」分类下的概念`,
|
||||
status: 'info',
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
});
|
||||
}, [searchQuery, selectedDate, sortBy, updateUrlParams, fetchConcepts, toast]);
|
||||
|
||||
// 清除层级筛选
|
||||
const handleClearHierarchyFilter = useCallback(() => {
|
||||
setHierarchyFilter({ lv1: null, lv2: null, lv3: null });
|
||||
setCurrentPage(1);
|
||||
updateUrlParams({ lv1: '', lv2: '', lv3: '', page: 1 });
|
||||
fetchConcepts(searchQuery, 1, selectedDate, sortBy, { lv1: null, lv2: null, lv3: null });
|
||||
}, [searchQuery, selectedDate, sortBy, updateUrlParams, fetchConcepts]);
|
||||
|
||||
// 导航到特定层级
|
||||
const handleNavigateHierarchy = useCallback((filter) => {
|
||||
setHierarchyFilter(filter);
|
||||
setCurrentPage(1);
|
||||
updateUrlParams({
|
||||
lv1: filter.lv1 || '',
|
||||
lv2: filter.lv2 || '',
|
||||
lv3: filter.lv3 || '',
|
||||
page: 1
|
||||
});
|
||||
fetchConcepts(searchQuery, 1, selectedDate, sortBy, filter);
|
||||
}, [searchQuery, selectedDate, sortBy, updateUrlParams, fetchConcepts]);
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1);
|
||||
@@ -556,12 +624,20 @@ const ConceptCenter = () => {
|
||||
setSortBy(filters.sort);
|
||||
setCurrentPage(filters.page);
|
||||
|
||||
// 恢复层级筛选状态
|
||||
const hierarchyFilterFromUrl = {
|
||||
lv1: filters.lv1,
|
||||
lv2: filters.lv2,
|
||||
lv3: filters.lv3,
|
||||
};
|
||||
setHierarchyFilter(hierarchyFilterFromUrl);
|
||||
|
||||
const dateToUse = filters.date ? new Date(filters.date) : latestDate;
|
||||
if (dateToUse) {
|
||||
setSelectedDate(dateToUse);
|
||||
fetchConcepts(filters.q, filters.page, dateToUse, filters.sort);
|
||||
fetchConcepts(filters.q, filters.page, dateToUse, filters.sort, hierarchyFilterFromUrl);
|
||||
} else {
|
||||
fetchConcepts(filters.q, filters.page, null, filters.sort);
|
||||
fetchConcepts(filters.q, filters.page, null, filters.sort, hierarchyFilterFromUrl);
|
||||
}
|
||||
};
|
||||
init();
|
||||
@@ -1431,46 +1507,76 @@ const ConceptCenter = () => {
|
||||
</HStack>
|
||||
|
||||
<ButtonGroup size="sm" isAttached variant="outline">
|
||||
<IconButton
|
||||
icon={<FaThLarge />}
|
||||
onClick={() => {
|
||||
if (viewMode !== 'grid') {
|
||||
trackViewModeChanged('grid', viewMode);
|
||||
setViewMode('grid');
|
||||
}
|
||||
}}
|
||||
bg={viewMode === 'grid' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'transparent'}
|
||||
color={viewMode === 'grid' ? 'white' : 'purple.500'}
|
||||
borderColor="purple.500"
|
||||
_hover={{
|
||||
bg: viewMode === 'grid' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'purple.50',
|
||||
boxShadow: viewMode === 'grid' ? '0 0 10px rgba(139, 92, 246, 0.3)' : 'none',
|
||||
}}
|
||||
aria-label="网格视图"
|
||||
/>
|
||||
<IconButton
|
||||
icon={<FaList />}
|
||||
onClick={() => {
|
||||
if (viewMode !== 'list') {
|
||||
trackViewModeChanged('list', viewMode);
|
||||
setViewMode('list');
|
||||
}
|
||||
}}
|
||||
bg={viewMode === 'list' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'transparent'}
|
||||
color={viewMode === 'list' ? 'white' : 'purple.500'}
|
||||
borderColor="purple.500"
|
||||
_hover={{
|
||||
bg: viewMode === 'list' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'purple.50',
|
||||
boxShadow: viewMode === 'list' ? '0 0 10px rgba(139, 92, 246, 0.3)' : 'none',
|
||||
}}
|
||||
aria-label="列表视图"
|
||||
/>
|
||||
<Tooltip label="层级图" placement="top">
|
||||
<IconButton
|
||||
icon={<FaSitemap />}
|
||||
onClick={() => {
|
||||
if (viewMode !== 'hierarchy') {
|
||||
trackViewModeChanged('hierarchy', viewMode);
|
||||
setViewMode('hierarchy');
|
||||
}
|
||||
}}
|
||||
bg={viewMode === 'hierarchy' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'transparent'}
|
||||
color={viewMode === 'hierarchy' ? 'white' : 'purple.500'}
|
||||
borderColor="purple.500"
|
||||
_hover={{
|
||||
bg: viewMode === 'hierarchy' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'purple.50',
|
||||
boxShadow: viewMode === 'hierarchy' ? '0 0 10px rgba(139, 92, 246, 0.3)' : 'none',
|
||||
}}
|
||||
aria-label="层级图"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label="网格视图" placement="top">
|
||||
<IconButton
|
||||
icon={<FaThLarge />}
|
||||
onClick={() => {
|
||||
if (viewMode !== 'grid') {
|
||||
trackViewModeChanged('grid', viewMode);
|
||||
setViewMode('grid');
|
||||
}
|
||||
}}
|
||||
bg={viewMode === 'grid' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'transparent'}
|
||||
color={viewMode === 'grid' ? 'white' : 'purple.500'}
|
||||
borderColor="purple.500"
|
||||
_hover={{
|
||||
bg: viewMode === 'grid' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'purple.50',
|
||||
boxShadow: viewMode === 'grid' ? '0 0 10px rgba(139, 92, 246, 0.3)' : 'none',
|
||||
}}
|
||||
aria-label="网格视图"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label="列表视图" placement="top">
|
||||
<IconButton
|
||||
icon={<FaList />}
|
||||
onClick={() => {
|
||||
if (viewMode !== 'list') {
|
||||
trackViewModeChanged('list', viewMode);
|
||||
setViewMode('list');
|
||||
}
|
||||
}}
|
||||
bg={viewMode === 'list' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'transparent'}
|
||||
color={viewMode === 'list' ? 'white' : 'purple.500'}
|
||||
borderColor="purple.500"
|
||||
_hover={{
|
||||
bg: viewMode === 'list' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'purple.50',
|
||||
boxShadow: viewMode === 'list' ? '0 0 10px rgba(139, 92, 246, 0.3)' : 'none',
|
||||
}}
|
||||
aria-label="列表视图"
|
||||
/>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{selectedDate && (
|
||||
{/* 面包屑导航 - 显示当前层级筛选 */}
|
||||
<BreadcrumbNav
|
||||
filter={hierarchyFilter}
|
||||
onClearFilter={handleClearHierarchyFilter}
|
||||
onNavigate={handleNavigateHierarchy}
|
||||
/>
|
||||
|
||||
{selectedDate && viewMode !== 'hierarchy' && (
|
||||
<Box mb={4} p={3} bg="blue.50" borderRadius="md" borderLeft="4px solid" borderColor="blue.500">
|
||||
<HStack>
|
||||
<Icon as={InfoIcon} color="blue.500" />
|
||||
@@ -1482,7 +1588,14 @@ const ConceptCenter = () => {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
{/* 层级图视图 */}
|
||||
{viewMode === 'hierarchy' ? (
|
||||
<HierarchyView
|
||||
apiBaseUrl={API_BASE_URL}
|
||||
onSelectCategory={handleHierarchySelect}
|
||||
selectedDate={selectedDate}
|
||||
/>
|
||||
) : loading ? (
|
||||
<SimpleGrid columns={{ base: 2, md: 2, lg: 3 }} spacing={{ base: 3, md: 6 }}>
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<SkeletonCard key={i} />
|
||||
@@ -1590,17 +1703,32 @@ const ConceptCenter = () => {
|
||||
</HStack>
|
||||
</Center>
|
||||
</>
|
||||
) : (
|
||||
) : viewMode !== 'hierarchy' ? (
|
||||
<Center h="400px">
|
||||
<VStack spacing={6}>
|
||||
<Icon as={FaTags} boxSize={20} color="gray.300" />
|
||||
<VStack spacing={2}>
|
||||
<Text fontSize="xl" color="gray.600" fontWeight="medium">暂无概念数据</Text>
|
||||
<Text color="gray.500">请尝试其他搜索关键词或选择其他日期</Text>
|
||||
<Text color="gray.500">
|
||||
{hierarchyFilter?.lv1
|
||||
? `「${[hierarchyFilter.lv1, hierarchyFilter.lv2, hierarchyFilter.lv3].filter(Boolean).join(' > ')}」分类下暂无数据`
|
||||
: '请尝试其他搜索关键词或选择其他日期'
|
||||
}
|
||||
</Text>
|
||||
{hierarchyFilter?.lv1 && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="purple"
|
||||
variant="outline"
|
||||
onClick={handleClearHierarchyFilter}
|
||||
>
|
||||
清除筛选
|
||||
</Button>
|
||||
)}
|
||||
</VStack>
|
||||
</VStack>
|
||||
</Center>
|
||||
)}
|
||||
) : null}
|
||||
</Box>
|
||||
|
||||
{/* 右侧统计面板 */}
|
||||
|
||||
Reference in New Issue
Block a user