update pay ui
This commit is contained in:
@@ -1,14 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* HierarchyView - 概念层级热力图视图
|
* HierarchyView - 概念层级热力图视图
|
||||||
*
|
*
|
||||||
* 使用 ECharts Treemap 实现热力图效果
|
* 使用 CSS Grid + Chakra UI 实现热力图效果
|
||||||
* 特性:
|
* 特性:
|
||||||
* 1. 炫酷的矩形树图/热力图展示
|
* 1. 炫酷的矩形热力图展示,涨红跌绿背景色
|
||||||
* 2. 点击 lv1 进入 lv2,点击 lv2 进入 lv3,层层钻取
|
* 2. 点击 lv1 进入 lv2,点击 lv2 进入 lv3,层层钻取
|
||||||
* 3. 集成 /hierarchy/price 接口获取实时涨跌幅
|
* 3. 集成 /hierarchy/price 接口获取实时涨跌幅
|
||||||
* 4. 支持面包屑导航返回上级
|
* 4. 每个分类有独特图标
|
||||||
|
* 5. 支持面包屑导航返回上级
|
||||||
*/
|
*/
|
||||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
VStack,
|
VStack,
|
||||||
@@ -23,8 +24,9 @@ import {
|
|||||||
useBreakpointValue,
|
useBreakpointValue,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
SimpleGrid,
|
||||||
|
keyframes,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import ReactECharts from 'echarts-for-react';
|
|
||||||
import {
|
import {
|
||||||
FaLayerGroup,
|
FaLayerGroup,
|
||||||
FaExpand,
|
FaExpand,
|
||||||
@@ -32,38 +34,124 @@ import {
|
|||||||
FaSync,
|
FaSync,
|
||||||
FaHome,
|
FaHome,
|
||||||
FaChevronRight,
|
FaChevronRight,
|
||||||
|
FaBrain,
|
||||||
|
FaMicrochip,
|
||||||
|
FaRobot,
|
||||||
|
FaMobileAlt,
|
||||||
|
FaCar,
|
||||||
|
FaBolt,
|
||||||
|
FaRocket,
|
||||||
|
FaShieldAlt,
|
||||||
|
FaGlobe,
|
||||||
|
FaIndustry,
|
||||||
|
FaShoppingCart,
|
||||||
|
FaCoins,
|
||||||
|
FaHeartbeat,
|
||||||
|
FaAtom,
|
||||||
|
FaLightbulb,
|
||||||
|
FaArrowUp,
|
||||||
|
FaArrowDown,
|
||||||
|
FaCubes,
|
||||||
|
FaServer,
|
||||||
|
FaCode,
|
||||||
|
FaMagic,
|
||||||
|
FaEye,
|
||||||
|
FaPlane,
|
||||||
|
FaSatellite,
|
||||||
|
FaBatteryFull,
|
||||||
|
FaSolarPanel,
|
||||||
|
FaWind,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
|
|
||||||
// 一级分类颜色映射
|
// 一级分类图标映射
|
||||||
const LV1_COLORS = {
|
const LV1_ICONS = {
|
||||||
'人工智能': ['#8B5CF6', '#A78BFA', '#C4B5FD'],
|
'人工智能': FaBrain,
|
||||||
'半导体': ['#3B82F6', '#60A5FA', '#93C5FD'],
|
'半导体': FaMicrochip,
|
||||||
'机器人': ['#10B981', '#34D399', '#6EE7B7'],
|
'机器人': FaRobot,
|
||||||
'消费电子': ['#EC4899', '#F472B6', '#F9A8D4'],
|
'消费电子': FaMobileAlt,
|
||||||
'智能驾驶与汽车': ['#F97316', '#FB923C', '#FDBA74'],
|
'智能驾驶与汽车': FaCar,
|
||||||
'新能源与电力': ['#22C55E', '#4ADE80', '#86EFAC'],
|
'新能源与电力': FaBolt,
|
||||||
'空天经济': ['#06B6D4', '#22D3EE', '#67E8F9'],
|
'空天经济': FaRocket,
|
||||||
'国防军工': ['#EF4444', '#F87171', '#FCA5A5'],
|
'国防军工': FaShieldAlt,
|
||||||
'政策与主题': ['#F59E0B', '#FBBF24', '#FCD34D'],
|
'政策与主题': FaGlobe,
|
||||||
'周期与材料': ['#6B7280', '#9CA3AF', '#D1D5DB'],
|
'周期与材料': FaIndustry,
|
||||||
'大消费': ['#F472B6', '#F9A8D4', '#FBCFE8'],
|
'大消费': FaShoppingCart,
|
||||||
'数字经济与金融科技': ['#6366F1', '#818CF8', '#A5B4FC'],
|
'数字经济与金融科技': FaCoins,
|
||||||
'全球宏观与贸易': ['#14B8A6', '#2DD4BF', '#5EEAD4'],
|
'全球宏观与贸易': FaGlobe,
|
||||||
'医药健康': ['#84CC16', '#A3E635', '#BEF264'],
|
'医药健康': FaHeartbeat,
|
||||||
'前沿科技': ['#A855F7', '#C084FC', '#D8B4FE'],
|
'前沿科技': FaAtom,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取涨跌幅颜色(红涨绿跌)
|
// 二级分类图标映射
|
||||||
const getChangeColor = (value) => {
|
const LV2_ICONS = {
|
||||||
if (value === null || value === undefined) return '#9CA3AF';
|
'AI基础设施': FaServer,
|
||||||
if (value > 3) return '#DC2626';
|
'AI模型与软件': FaCode,
|
||||||
if (value > 1) return '#EF4444';
|
'AI应用': FaMagic,
|
||||||
if (value > 0) return '#F87171';
|
'半导体设备': FaCubes,
|
||||||
if (value < -3) return '#15803D';
|
'半导体材料': FaAtom,
|
||||||
if (value < -1) return '#22C55E';
|
'芯片设计与制造': FaMicrochip,
|
||||||
if (value < 0) return '#4ADE80';
|
'先进封装': FaCubes,
|
||||||
return '#9CA3AF';
|
'人形机器人整机': FaRobot,
|
||||||
|
'机器人核心零部件': FaCubes,
|
||||||
|
'其他类型机器人': FaRobot,
|
||||||
|
'智能终端': FaMobileAlt,
|
||||||
|
'XR与空间计算': FaEye,
|
||||||
|
'华为产业链': FaMobileAlt,
|
||||||
|
'自动驾驶解决方案': FaCar,
|
||||||
|
'智能汽车产业链': FaCar,
|
||||||
|
'车路协同': FaCar,
|
||||||
|
'新型电池技术': FaBatteryFull,
|
||||||
|
'电力设备与电网': FaBolt,
|
||||||
|
'清洁能源': FaSolarPanel,
|
||||||
|
'低空经济': FaPlane,
|
||||||
|
'商业航天': FaSatellite,
|
||||||
|
'无人作战与信息化': FaShieldAlt,
|
||||||
|
'海军装备': FaShieldAlt,
|
||||||
|
'军贸出海': FaGlobe,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 一级分类基础颜色(用于渐变)
|
||||||
|
const LV1_BASE_COLORS = {
|
||||||
|
'人工智能': { from: '#8B5CF6', to: '#A78BFA' },
|
||||||
|
'半导体': { from: '#3B82F6', to: '#60A5FA' },
|
||||||
|
'机器人': { from: '#10B981', to: '#34D399' },
|
||||||
|
'消费电子': { from: '#EC4899', to: '#F472B6' },
|
||||||
|
'智能驾驶与汽车': { from: '#F97316', to: '#FB923C' },
|
||||||
|
'新能源与电力': { from: '#22C55E', to: '#4ADE80' },
|
||||||
|
'空天经济': { from: '#06B6D4', to: '#22D3EE' },
|
||||||
|
'国防军工': { from: '#EF4444', to: '#F87171' },
|
||||||
|
'政策与主题': { from: '#F59E0B', to: '#FBBF24' },
|
||||||
|
'周期与材料': { from: '#6B7280', to: '#9CA3AF' },
|
||||||
|
'大消费': { from: '#F472B6', to: '#F9A8D4' },
|
||||||
|
'数字经济与金融科技': { from: '#6366F1', to: '#818CF8' },
|
||||||
|
'全球宏观与贸易': { from: '#14B8A6', to: '#2DD4BF' },
|
||||||
|
'医药健康': { from: '#84CC16', to: '#A3E635' },
|
||||||
|
'前沿科技': { from: '#A855F7', to: '#C084FC' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据涨跌幅获取背景色(涨红跌绿渐变)
|
||||||
|
const getChangeBgColor = (value, baseColor = null) => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
// 无数据时使用基础色
|
||||||
|
if (baseColor) {
|
||||||
|
return `linear-gradient(135deg, ${baseColor.from} 0%, ${baseColor.to} 100%)`;
|
||||||
|
}
|
||||||
|
return 'linear-gradient(135deg, #6B7280 0%, #9CA3AF 100%)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 涨跌幅越大,颜色越深
|
||||||
|
if (value > 5) return 'linear-gradient(135deg, #991B1B 0%, #DC2626 100%)';
|
||||||
|
if (value > 3) return 'linear-gradient(135deg, #B91C1C 0%, #EF4444 100%)';
|
||||||
|
if (value > 1) return 'linear-gradient(135deg, #DC2626 0%, #F87171 100%)';
|
||||||
|
if (value > 0) return 'linear-gradient(135deg, #EF4444 0%, #FCA5A5 100%)';
|
||||||
|
if (value < -5) return 'linear-gradient(135deg, #14532D 0%, #16A34A 100%)';
|
||||||
|
if (value < -3) return 'linear-gradient(135deg, #166534 0%, #22C55E 100%)';
|
||||||
|
if (value < -1) return 'linear-gradient(135deg, #16A34A 0%, #4ADE80 100%)';
|
||||||
|
if (value < 0) return 'linear-gradient(135deg, #22C55E 0%, #86EFAC 100%)';
|
||||||
|
|
||||||
|
// 平盘
|
||||||
|
return 'linear-gradient(135deg, #6B7280 0%, #9CA3AF 100%)';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 格式化涨跌幅
|
// 格式化涨跌幅
|
||||||
@@ -73,13 +161,173 @@ const formatChangePercent = (value) => {
|
|||||||
return value > 0 ? `+${formatted}%` : value < 0 ? `-${formatted}%` : '0.00%';
|
return value > 0 ? `+${formatted}%` : value < 0 ? `-${formatted}%` : '0.00%';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取 lv1 的颜色
|
// 获取图标
|
||||||
const getLv1Color = (name, index = 0) => {
|
const getIcon = (name, level) => {
|
||||||
const colors = LV1_COLORS[name];
|
if (level === 'lv1') {
|
||||||
if (colors) return colors[Math.min(index, colors.length - 1)];
|
return LV1_ICONS[name] || FaLayerGroup;
|
||||||
// 默认颜色
|
}
|
||||||
const defaultColors = ['#8B5CF6', '#A78BFA', '#C4B5FD'];
|
if (level === 'lv2') {
|
||||||
return defaultColors[Math.min(index, defaultColors.length - 1)];
|
return LV2_ICONS[name] || FaCubes;
|
||||||
|
}
|
||||||
|
return FaLightbulb;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 脉冲动画
|
||||||
|
const pulseKeyframes = keyframes`
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.02); }
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个热力图块组件
|
||||||
|
*/
|
||||||
|
const HeatmapBlock = ({ item, onClick, size = 'normal' }) => {
|
||||||
|
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||||
|
const hasChange = item.avg_change_pct !== null && item.avg_change_pct !== undefined;
|
||||||
|
const isPositive = hasChange && item.avg_change_pct > 0;
|
||||||
|
const isNegative = hasChange && item.avg_change_pct < 0;
|
||||||
|
const isLargeChange = hasChange && Math.abs(item.avg_change_pct) > 3;
|
||||||
|
|
||||||
|
const IconComponent = getIcon(item.name, item.level);
|
||||||
|
const baseColor = item.parentLv1
|
||||||
|
? LV1_BASE_COLORS[item.parentLv1]
|
||||||
|
: LV1_BASE_COLORS[item.name];
|
||||||
|
|
||||||
|
// 根据 size 调整高度
|
||||||
|
const heightMap = {
|
||||||
|
large: { base: '140px', md: '180px' },
|
||||||
|
normal: { base: '120px', md: '150px' },
|
||||||
|
small: { base: '100px', md: '120px' },
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
bg={getChangeBgColor(item.avg_change_pct, baseColor)}
|
||||||
|
borderRadius="xl"
|
||||||
|
p={{ base: 3, md: 4 }}
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => onClick(item)}
|
||||||
|
position="relative"
|
||||||
|
overflow="hidden"
|
||||||
|
minH={heightMap[size]}
|
||||||
|
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
||||||
|
boxShadow="0 4px 15px rgba(0, 0, 0, 0.2)"
|
||||||
|
_hover={{
|
||||||
|
transform: 'translateY(-4px) scale(1.02)',
|
||||||
|
boxShadow: '0 12px 30px rgba(0, 0, 0, 0.3)',
|
||||||
|
}}
|
||||||
|
animation={isLargeChange ? `${pulseKeyframes} 2s infinite` : 'none'}
|
||||||
|
>
|
||||||
|
{/* 背景装饰 */}
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={-20}
|
||||||
|
right={-20}
|
||||||
|
width="100px"
|
||||||
|
height="100px"
|
||||||
|
borderRadius="full"
|
||||||
|
bg="whiteAlpha.100"
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
bottom={-30}
|
||||||
|
left={-30}
|
||||||
|
width="80px"
|
||||||
|
height="80px"
|
||||||
|
borderRadius="full"
|
||||||
|
bg="whiteAlpha.50"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 内容 */}
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
justify="space-between"
|
||||||
|
h="100%"
|
||||||
|
position="relative"
|
||||||
|
zIndex={1}
|
||||||
|
>
|
||||||
|
{/* 顶部:图标和名称 */}
|
||||||
|
<HStack spacing={2} align="flex-start">
|
||||||
|
<Box
|
||||||
|
p={2}
|
||||||
|
bg="whiteAlpha.200"
|
||||||
|
borderRadius="lg"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
as={IconComponent}
|
||||||
|
boxSize={{ base: 4, md: 5 }}
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<VStack align="start" spacing={0} flex={1}>
|
||||||
|
<Text
|
||||||
|
color="white"
|
||||||
|
fontWeight="bold"
|
||||||
|
fontSize={{ base: 'sm', md: 'md' }}
|
||||||
|
noOfLines={2}
|
||||||
|
textShadow="0 2px 4px rgba(0,0,0,0.3)"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
{item.concept_count && (
|
||||||
|
<Text
|
||||||
|
color="whiteAlpha.800"
|
||||||
|
fontSize="xs"
|
||||||
|
>
|
||||||
|
{item.concept_count} 个概念
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 底部:涨跌幅 */}
|
||||||
|
<Flex justify="space-between" align="flex-end">
|
||||||
|
<VStack align="start" spacing={0}>
|
||||||
|
{item.stock_count && (
|
||||||
|
<Text color="whiteAlpha.700" fontSize="xs">
|
||||||
|
{item.stock_count} 只股票
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<HStack spacing={1} align="center">
|
||||||
|
{hasChange && (
|
||||||
|
<Icon
|
||||||
|
as={isPositive ? FaArrowUp : isNegative ? FaArrowDown : null}
|
||||||
|
boxSize={3}
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
color="white"
|
||||||
|
fontWeight="bold"
|
||||||
|
fontSize={{ base: 'lg', md: '2xl' }}
|
||||||
|
textShadow="0 2px 8px rgba(0,0,0,0.4)"
|
||||||
|
>
|
||||||
|
{formatChangePercent(item.avg_change_pct)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 可点击提示 */}
|
||||||
|
{(item.children?.length > 0 || item.concepts?.length > 0) && (
|
||||||
|
<Badge
|
||||||
|
position="absolute"
|
||||||
|
top={2}
|
||||||
|
right={2}
|
||||||
|
bg="whiteAlpha.300"
|
||||||
|
color="white"
|
||||||
|
fontSize="xs"
|
||||||
|
borderRadius="full"
|
||||||
|
px={2}
|
||||||
|
>
|
||||||
|
点击展开
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,13 +347,11 @@ const HierarchyView = ({
|
|||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
// 钻取状态
|
// 钻取状态
|
||||||
const [currentLevel, setCurrentLevel] = useState('lv1'); // 'lv1' | 'lv2' | 'lv3'
|
const [currentLevel, setCurrentLevel] = useState('lv1');
|
||||||
const [currentLv1, setCurrentLv1] = useState(null);
|
const [currentLv1, setCurrentLv1] = useState(null);
|
||||||
const [currentLv2, setCurrentLv2] = useState(null);
|
const [currentLv2, setCurrentLv2] = useState(null);
|
||||||
const [breadcrumbs, setBreadcrumbs] = useState([{ label: '全部分类', level: 'root' }]);
|
const [breadcrumbs, setBreadcrumbs] = useState([{ label: '全部分类', level: 'root' }]);
|
||||||
|
|
||||||
const chartRef = useRef(null);
|
|
||||||
const containerRef = useRef(null);
|
|
||||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||||
|
|
||||||
// 获取层级结构数据
|
// 获取层级结构数据
|
||||||
@@ -151,7 +397,6 @@ const HierarchyView = ({
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// 构建映射表
|
|
||||||
const lv1Map = {};
|
const lv1Map = {};
|
||||||
const lv2Map = {};
|
const lv2Map = {};
|
||||||
const lv3Map = {};
|
const lv3Map = {};
|
||||||
@@ -197,37 +442,28 @@ const HierarchyView = ({
|
|||||||
const { lv1Map, lv2Map, lv3Map } = priceData;
|
const { lv1Map, lv2Map, lv3Map } = priceData;
|
||||||
|
|
||||||
if (currentLevel === 'lv1') {
|
if (currentLevel === 'lv1') {
|
||||||
// 显示所有 lv1
|
return hierarchy.map((lv1) => {
|
||||||
return hierarchy.map((lv1, index) => {
|
|
||||||
const price = lv1Map[lv1.name] || {};
|
const price = lv1Map[lv1.name] || {};
|
||||||
return {
|
return {
|
||||||
name: lv1.name,
|
name: lv1.name,
|
||||||
value: lv1.concept_count || 10,
|
|
||||||
id: lv1.id,
|
id: lv1.id,
|
||||||
level: 'lv1',
|
level: 'lv1',
|
||||||
concept_count: lv1.concept_count,
|
concept_count: lv1.concept_count,
|
||||||
stock_count: price.stock_count,
|
stock_count: price.stock_count,
|
||||||
avg_change_pct: price.avg_change_pct,
|
avg_change_pct: price.avg_change_pct,
|
||||||
children: lv1.children,
|
children: lv1.children,
|
||||||
itemStyle: {
|
|
||||||
color: getLv1Color(lv1.name, 0),
|
|
||||||
borderColor: '#fff',
|
|
||||||
borderWidth: 2,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentLevel === 'lv2' && currentLv1) {
|
if (currentLevel === 'lv2' && currentLv1) {
|
||||||
// 显示选中 lv1 下的 lv2
|
|
||||||
const lv1Data = hierarchy.find(h => h.name === currentLv1.name);
|
const lv1Data = hierarchy.find(h => h.name === currentLv1.name);
|
||||||
if (!lv1Data || !lv1Data.children) return [];
|
if (!lv1Data || !lv1Data.children) return [];
|
||||||
|
|
||||||
return lv1Data.children.map((lv2, index) => {
|
return lv1Data.children.map((lv2) => {
|
||||||
const price = lv2Map[lv2.name] || {};
|
const price = lv2Map[lv2.name] || {};
|
||||||
return {
|
return {
|
||||||
name: lv2.name,
|
name: lv2.name,
|
||||||
value: lv2.concept_count || 5,
|
|
||||||
id: lv2.id,
|
id: lv2.id,
|
||||||
level: 'lv2',
|
level: 'lv2',
|
||||||
parentLv1: currentLv1.name,
|
parentLv1: currentLv1.name,
|
||||||
@@ -236,30 +472,22 @@ const HierarchyView = ({
|
|||||||
avg_change_pct: price.avg_change_pct,
|
avg_change_pct: price.avg_change_pct,
|
||||||
children: lv2.children,
|
children: lv2.children,
|
||||||
concepts: lv2.concepts,
|
concepts: lv2.concepts,
|
||||||
itemStyle: {
|
|
||||||
color: getLv1Color(currentLv1.name, index % 3),
|
|
||||||
borderColor: '#fff',
|
|
||||||
borderWidth: 2,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentLevel === 'lv3' && currentLv1 && currentLv2) {
|
if (currentLevel === 'lv3' && currentLv1 && currentLv2) {
|
||||||
// 显示选中 lv2 下的 lv3 或概念
|
|
||||||
const lv1Data = hierarchy.find(h => h.name === currentLv1.name);
|
const lv1Data = hierarchy.find(h => h.name === currentLv1.name);
|
||||||
if (!lv1Data || !lv1Data.children) return [];
|
if (!lv1Data || !lv1Data.children) return [];
|
||||||
|
|
||||||
const lv2Data = lv1Data.children.find(h => h.name === currentLv2.name);
|
const lv2Data = lv1Data.children.find(h => h.name === currentLv2.name);
|
||||||
if (!lv2Data) return [];
|
if (!lv2Data) return [];
|
||||||
|
|
||||||
// 如果有 lv3 子级
|
|
||||||
if (lv2Data.children && lv2Data.children.length > 0) {
|
if (lv2Data.children && lv2Data.children.length > 0) {
|
||||||
return lv2Data.children.map((lv3, index) => {
|
return lv2Data.children.map((lv3) => {
|
||||||
const price = lv3Map[lv3.name] || {};
|
const price = lv3Map[lv3.name] || {};
|
||||||
return {
|
return {
|
||||||
name: lv3.name,
|
name: lv3.name,
|
||||||
value: lv3.concept_count || 3,
|
|
||||||
id: lv3.id,
|
id: lv3.id,
|
||||||
level: 'lv3',
|
level: 'lv3',
|
||||||
parentLv1: currentLv1.name,
|
parentLv1: currentLv1.name,
|
||||||
@@ -268,28 +496,16 @@ const HierarchyView = ({
|
|||||||
stock_count: price.stock_count,
|
stock_count: price.stock_count,
|
||||||
avg_change_pct: price.avg_change_pct,
|
avg_change_pct: price.avg_change_pct,
|
||||||
concepts: lv3.concepts,
|
concepts: lv3.concepts,
|
||||||
itemStyle: {
|
|
||||||
color: getLv1Color(currentLv1.name, index % 3),
|
|
||||||
borderColor: '#fff',
|
|
||||||
borderWidth: 2,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果直接是概念列表
|
|
||||||
if (lv2Data.concepts && lv2Data.concepts.length > 0) {
|
if (lv2Data.concepts && lv2Data.concepts.length > 0) {
|
||||||
return lv2Data.concepts.map((concept, index) => ({
|
return lv2Data.concepts.map((concept) => ({
|
||||||
name: concept,
|
name: concept,
|
||||||
value: 1,
|
|
||||||
level: 'concept',
|
level: 'concept',
|
||||||
parentLv1: currentLv1.name,
|
parentLv1: currentLv1.name,
|
||||||
parentLv2: currentLv2.name,
|
parentLv2: currentLv2.name,
|
||||||
itemStyle: {
|
|
||||||
color: getLv1Color(currentLv1.name, index % 3),
|
|
||||||
borderColor: '#fff',
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,172 +515,31 @@ const HierarchyView = ({
|
|||||||
return [];
|
return [];
|
||||||
}, [hierarchy, priceData, currentLevel, currentLv1, currentLv2]);
|
}, [hierarchy, priceData, currentLevel, currentLv1, currentLv2]);
|
||||||
|
|
||||||
// ECharts 配置
|
|
||||||
const chartOption = useMemo(() => {
|
|
||||||
if (!currentData || currentData.length === 0) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item',
|
|
||||||
formatter: (params) => {
|
|
||||||
const data = params.data || {};
|
|
||||||
let content = `<div style="font-weight:bold;font-size:14px;margin-bottom:8px">${data.name}</div>`;
|
|
||||||
|
|
||||||
if (data.avg_change_pct !== undefined && data.avg_change_pct !== null) {
|
|
||||||
const color = getChangeColor(data.avg_change_pct);
|
|
||||||
content += `<div style="margin-bottom:4px">平均涨跌: <span style="color:${color};font-weight:bold;font-size:16px">${formatChangePercent(data.avg_change_pct)}</span></div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.concept_count !== undefined) {
|
|
||||||
content += `<div style="margin-bottom:4px">概念数量: <span style="font-weight:bold">${data.concept_count}</span></div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.stock_count !== undefined) {
|
|
||||||
content += `<div style="margin-bottom:4px">成分股数: <span style="font-weight:bold">${data.stock_count}</span></div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const levelMap = { lv1: '一级分类', lv2: '二级分类', lv3: '三级分类', concept: '概念' };
|
|
||||||
if (data.level) {
|
|
||||||
content += `<div style="color:#9CA3AF;font-size:11px;margin-top:8px;padding-top:8px;border-top:1px solid #E5E7EB">${levelMap[data.level] || ''}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提示可点击
|
|
||||||
if (data.level && data.level !== 'concept' && data.children) {
|
|
||||||
content += `<div style="color:#8B5CF6;font-size:12px;margin-top:4px">👆 点击查看下级分类</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
|
||||||
},
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.98)',
|
|
||||||
borderColor: '#E5E7EB',
|
|
||||||
borderWidth: 1,
|
|
||||||
padding: [12, 16],
|
|
||||||
textStyle: {
|
|
||||||
color: '#1F2937',
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
extraCssText: 'box-shadow: 0 4px 20px rgba(0,0,0,0.15); border-radius: 8px;',
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
type: 'treemap',
|
|
||||||
data: currentData,
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
roam: false,
|
|
||||||
nodeClick: false, // 禁用默认钻取,使用自定义
|
|
||||||
breadcrumb: {
|
|
||||||
show: false, // 使用自定义面包屑
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
formatter: (params) => {
|
|
||||||
const data = params.data || {};
|
|
||||||
const name = data.name || '';
|
|
||||||
const changeStr = data.avg_change_pct !== undefined && data.avg_change_pct !== null
|
|
||||||
? formatChangePercent(data.avg_change_pct)
|
|
||||||
: '';
|
|
||||||
|
|
||||||
// 根据区块大小决定显示内容
|
|
||||||
if (params.value < 3) {
|
|
||||||
return name.length > 4 ? name.slice(0, 4) + '...' : name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changeStr) {
|
|
||||||
return `{name|${name}}\n{change|${changeStr}}`;
|
|
||||||
}
|
|
||||||
return `{name|${name}}`;
|
|
||||||
},
|
|
||||||
rich: {
|
|
||||||
name: {
|
|
||||||
fontSize: isMobile ? 12 : 14,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#fff',
|
|
||||||
textShadowColor: 'rgba(0,0,0,0.3)',
|
|
||||||
textShadowBlur: 2,
|
|
||||||
lineHeight: isMobile ? 18 : 22,
|
|
||||||
},
|
|
||||||
change: {
|
|
||||||
fontSize: isMobile ? 14 : 18,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#fff',
|
|
||||||
textShadowColor: 'rgba(0,0,0,0.5)',
|
|
||||||
textShadowBlur: 3,
|
|
||||||
lineHeight: isMobile ? 20 : 26,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
position: 'inside',
|
|
||||||
verticalAlign: 'middle',
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
upperLabel: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
borderColor: '#fff',
|
|
||||||
borderWidth: 3,
|
|
||||||
gapWidth: 3,
|
|
||||||
borderRadius: 4,
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
itemStyle: {
|
|
||||||
shadowBlur: 20,
|
|
||||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
fontSize: isMobile ? 14 : 16,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
levels: [
|
|
||||||
{
|
|
||||||
itemStyle: {
|
|
||||||
borderColor: '#fff',
|
|
||||||
borderWidth: 4,
|
|
||||||
gapWidth: 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
animationDuration: 500,
|
|
||||||
animationEasing: 'cubicOut',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}, [currentData, isMobile]);
|
|
||||||
|
|
||||||
// 处理点击事件 - 钻取
|
// 处理点击事件 - 钻取
|
||||||
const handleChartClick = useCallback((params) => {
|
const handleBlockClick = useCallback((item) => {
|
||||||
const data = params.data;
|
logger.info('HierarchyView', '热力图点击', { level: item.level, name: item.name });
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
logger.info('HierarchyView', '热力图点击', { level: data.level, name: data.name });
|
if (item.level === 'lv1' && item.children && item.children.length > 0) {
|
||||||
|
|
||||||
if (data.level === 'lv1' && data.children && data.children.length > 0) {
|
|
||||||
// 进入 lv2
|
|
||||||
setCurrentLevel('lv2');
|
setCurrentLevel('lv2');
|
||||||
setCurrentLv1(data);
|
setCurrentLv1(item);
|
||||||
setBreadcrumbs([
|
setBreadcrumbs([
|
||||||
{ label: '全部分类', level: 'root' },
|
{ label: '全部分类', level: 'root' },
|
||||||
{ label: data.name, level: 'lv1', data },
|
{ label: item.name, level: 'lv1', data: item },
|
||||||
]);
|
]);
|
||||||
} else if (data.level === 'lv2') {
|
} else if (item.level === 'lv2') {
|
||||||
// 检查是否有 lv3 或概念
|
if ((item.children && item.children.length > 0) || (item.concepts && item.concepts.length > 0)) {
|
||||||
if ((data.children && data.children.length > 0) || (data.concepts && data.concepts.length > 0)) {
|
|
||||||
setCurrentLevel('lv3');
|
setCurrentLevel('lv3');
|
||||||
setCurrentLv2(data);
|
setCurrentLv2(item);
|
||||||
setBreadcrumbs([
|
setBreadcrumbs([
|
||||||
{ label: '全部分类', level: 'root' },
|
{ label: '全部分类', level: 'root' },
|
||||||
{ label: currentLv1.name, level: 'lv1', data: currentLv1 },
|
{ label: currentLv1.name, level: 'lv1', data: currentLv1 },
|
||||||
{ label: data.name, level: 'lv2', data },
|
{ label: item.name, level: 'lv2', data: item },
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} else if (data.level === 'lv3' || data.level === 'concept') {
|
|
||||||
// 最底层,可以触发筛选或者其他操作
|
|
||||||
// 这里可以选择不做任何操作,或者提示用户
|
|
||||||
logger.info('HierarchyView', '已到达最底层', { name: data.name });
|
|
||||||
}
|
}
|
||||||
}, [currentLv1]);
|
}, [currentLv1]);
|
||||||
|
|
||||||
// 面包屑导航 - 返回上级
|
// 面包屑导航
|
||||||
const handleBreadcrumbClick = useCallback((crumb, index) => {
|
const handleBreadcrumbClick = useCallback((crumb, index) => {
|
||||||
if (crumb.level === 'root') {
|
if (crumb.level === 'root') {
|
||||||
setCurrentLevel('lv1');
|
setCurrentLevel('lv1');
|
||||||
@@ -489,22 +564,28 @@ const HierarchyView = ({
|
|||||||
setIsFullscreen(prev => !prev);
|
setIsFullscreen(prev => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 图表事件
|
|
||||||
const chartEvents = useMemo(() => ({
|
|
||||||
click: handleChartClick,
|
|
||||||
}), [handleChartClick]);
|
|
||||||
|
|
||||||
// 获取当前层级标题
|
// 获取当前层级标题
|
||||||
const getCurrentTitle = () => {
|
const getCurrentTitle = () => {
|
||||||
if (currentLevel === 'lv1') return '一级分类概览';
|
if (currentLevel === 'lv1') return '概念分类热力图';
|
||||||
if (currentLevel === 'lv2' && currentLv1) return `${currentLv1.name} - 二级分类`;
|
if (currentLevel === 'lv2' && currentLv1) return `${currentLv1.name}`;
|
||||||
if (currentLevel === 'lv3' && currentLv2) return `${currentLv2.name} - 三级分类`;
|
if (currentLevel === 'lv3' && currentLv2) return `${currentLv2.name}`;
|
||||||
return '概念分类';
|
return '概念分类';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 计算列数
|
||||||
|
const getGridColumns = () => {
|
||||||
|
if (currentLevel === 'lv1') {
|
||||||
|
return { base: 2, md: 3, lg: 4 };
|
||||||
|
}
|
||||||
|
if (currentLevel === 'lv2') {
|
||||||
|
return { base: 2, md: 3, lg: 4 };
|
||||||
|
}
|
||||||
|
return { base: 2, md: 3, lg: 4 };
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Center h="500px">
|
<Center h="400px">
|
||||||
<VStack spacing={4}>
|
<VStack spacing={4}>
|
||||||
<Spinner size="xl" color="purple.500" thickness="4px" />
|
<Spinner size="xl" color="purple.500" thickness="4px" />
|
||||||
<Text color="gray.600">正在加载概念层级...</Text>
|
<Text color="gray.600">正在加载概念层级...</Text>
|
||||||
@@ -515,7 +596,7 @@ const HierarchyView = ({
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Center h="500px">
|
<Center h="400px">
|
||||||
<VStack spacing={4}>
|
<VStack spacing={4}>
|
||||||
<Icon as={FaLayerGroup} boxSize={16} color="gray.300" />
|
<Icon as={FaLayerGroup} boxSize={16} color="gray.300" />
|
||||||
<Text color="gray.600">加载失败:{error}</Text>
|
<Text color="gray.600">加载失败:{error}</Text>
|
||||||
@@ -527,9 +608,9 @@ const HierarchyView = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chartOption) {
|
if (currentData.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Center h="500px">
|
<Center h="400px">
|
||||||
<VStack spacing={4}>
|
<VStack spacing={4}>
|
||||||
<Icon as={FaLayerGroup} boxSize={16} color="gray.300" />
|
<Icon as={FaLayerGroup} boxSize={16} color="gray.300" />
|
||||||
<Text color="gray.600">暂无层级数据</Text>
|
<Text color="gray.600">暂无层级数据</Text>
|
||||||
@@ -540,41 +621,40 @@ const HierarchyView = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
ref={containerRef}
|
|
||||||
position={isFullscreen ? 'fixed' : 'relative'}
|
position={isFullscreen ? 'fixed' : 'relative'}
|
||||||
top={isFullscreen ? 0 : 'auto'}
|
top={isFullscreen ? 0 : 'auto'}
|
||||||
left={isFullscreen ? 0 : 'auto'}
|
left={isFullscreen ? 0 : 'auto'}
|
||||||
right={isFullscreen ? 0 : 'auto'}
|
right={isFullscreen ? 0 : 'auto'}
|
||||||
bottom={isFullscreen ? 0 : 'auto'}
|
bottom={isFullscreen ? 0 : 'auto'}
|
||||||
zIndex={isFullscreen ? 1000 : 'auto'}
|
zIndex={isFullscreen ? 1000 : 'auto'}
|
||||||
bg={isFullscreen ? 'white' : 'transparent'}
|
bg={isFullscreen ? 'gray.50' : 'transparent'}
|
||||||
p={isFullscreen ? 4 : 0}
|
p={isFullscreen ? 4 : 0}
|
||||||
|
overflow={isFullscreen ? 'auto' : 'visible'}
|
||||||
>
|
>
|
||||||
{/* 工具栏 */}
|
{/* 工具栏 */}
|
||||||
<Flex
|
<Flex
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
align="center"
|
align="center"
|
||||||
mb={3}
|
mb={4}
|
||||||
flexWrap="wrap"
|
flexWrap="wrap"
|
||||||
gap={2}
|
gap={2}
|
||||||
>
|
>
|
||||||
<HStack spacing={2}>
|
<HStack spacing={3}>
|
||||||
<Icon as={FaLayerGroup} color="purple.500" boxSize={5} />
|
<Icon as={FaLayerGroup} color="purple.500" boxSize={6} />
|
||||||
<Text fontSize="lg" fontWeight="bold" color="gray.800">
|
<Text fontSize="xl" fontWeight="bold" color="gray.800">
|
||||||
{getCurrentTitle()}
|
{getCurrentTitle()}
|
||||||
</Text>
|
</Text>
|
||||||
{tradeDate && (
|
{tradeDate && (
|
||||||
<Badge colorScheme="blue" fontSize="xs">
|
<Badge colorScheme="blue" fontSize="sm" px={3} py={1} borderRadius="full">
|
||||||
{tradeDate}
|
{tradeDate}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{priceLoading && (
|
{priceLoading && (
|
||||||
<Spinner size="xs" color="purple.500" />
|
<Spinner size="sm" color="purple.500" />
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
{/* 刷新涨跌幅 */}
|
|
||||||
<Tooltip label="刷新涨跌幅" placement="top">
|
<Tooltip label="刷新涨跌幅" placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -587,7 +667,6 @@ const HierarchyView = ({
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* 全屏切换 */}
|
|
||||||
<Tooltip label={isFullscreen ? '退出全屏' : '全屏'} placement="top">
|
<Tooltip label={isFullscreen ? '退出全屏' : '全屏'} placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -604,10 +683,11 @@ const HierarchyView = ({
|
|||||||
{/* 面包屑导航 */}
|
{/* 面包屑导航 */}
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
mb={3}
|
mb={4}
|
||||||
p={2}
|
p={3}
|
||||||
bg="gray.50"
|
bg="white"
|
||||||
borderRadius="lg"
|
borderRadius="xl"
|
||||||
|
boxShadow="sm"
|
||||||
flexWrap="wrap"
|
flexWrap="wrap"
|
||||||
gap={1}
|
gap={1}
|
||||||
>
|
>
|
||||||
@@ -624,6 +704,7 @@ const HierarchyView = ({
|
|||||||
onClick={() => handleBreadcrumbClick(crumb, index)}
|
onClick={() => handleBreadcrumbClick(crumb, index)}
|
||||||
isDisabled={index === breadcrumbs.length - 1}
|
isDisabled={index === breadcrumbs.length - 1}
|
||||||
fontWeight={index === breadcrumbs.length - 1 ? 'bold' : 'medium'}
|
fontWeight={index === breadcrumbs.length - 1 ? 'bold' : 'medium'}
|
||||||
|
borderRadius="lg"
|
||||||
>
|
>
|
||||||
{crumb.label}
|
{crumb.label}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -631,71 +712,69 @@ const HierarchyView = ({
|
|||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* 操作提示 */}
|
{/* 图例说明 */}
|
||||||
<Text fontSize="xs" color="gray.500" mb={2} textAlign="center">
|
<Flex
|
||||||
👆 <Text as="span" color="purple.600" fontWeight="medium">点击色块</Text> 查看下级分类 |
|
justify="center"
|
||||||
区块大小表示概念数量 |
|
mb={4}
|
||||||
<Text as="span" color="red.500">红涨</Text>
|
gap={4}
|
||||||
<Text as="span" color="green.500">绿跌</Text>
|
flexWrap="wrap"
|
||||||
</Text>
|
fontSize="sm"
|
||||||
|
|
||||||
{/* ECharts 热力图 */}
|
|
||||||
<Box
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="gray.200"
|
|
||||||
borderRadius="xl"
|
|
||||||
overflow="hidden"
|
|
||||||
bg="white"
|
|
||||||
>
|
>
|
||||||
<ReactECharts
|
<HStack spacing={2}>
|
||||||
ref={chartRef}
|
<Box w={4} h={4} borderRadius="sm" bg="linear-gradient(135deg, #DC2626 0%, #F87171 100%)" />
|
||||||
option={chartOption}
|
<Text color="gray.600">涨</Text>
|
||||||
style={{
|
</HStack>
|
||||||
height: isFullscreen ? 'calc(100vh - 200px)' : isMobile ? '400px' : '500px',
|
<HStack spacing={2}>
|
||||||
width: '100%',
|
<Box w={4} h={4} borderRadius="sm" bg="linear-gradient(135deg, #22C55E 0%, #4ADE80 100%)" />
|
||||||
}}
|
<Text color="gray.600">跌</Text>
|
||||||
onEvents={chartEvents}
|
</HStack>
|
||||||
opts={{ renderer: 'canvas' }}
|
<HStack spacing={2}>
|
||||||
/>
|
<Box w={4} h={4} borderRadius="sm" bg="linear-gradient(135deg, #6B7280 0%, #9CA3AF 100%)" />
|
||||||
</Box>
|
<Text color="gray.600">平</Text>
|
||||||
|
</HStack>
|
||||||
|
<Text color="gray.500">|</Text>
|
||||||
|
<Text color="gray.500">点击色块查看下级分类</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 热力图网格 */}
|
||||||
|
<SimpleGrid columns={getGridColumns()} spacing={{ base: 3, md: 4 }}>
|
||||||
|
{currentData.map((item, index) => (
|
||||||
|
<HeatmapBlock
|
||||||
|
key={item.id || item.name}
|
||||||
|
item={item}
|
||||||
|
onClick={handleBlockClick}
|
||||||
|
size={currentLevel === 'lv1' ? 'large' : 'normal'}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
{/* 统计信息 */}
|
{/* 统计信息 */}
|
||||||
<Flex
|
<Flex
|
||||||
justify="center"
|
justify="center"
|
||||||
mt={3}
|
mt={6}
|
||||||
gap={3}
|
gap={3}
|
||||||
flexWrap="wrap"
|
flexWrap="wrap"
|
||||||
>
|
>
|
||||||
<Badge
|
<Badge
|
||||||
colorScheme="purple"
|
colorScheme="purple"
|
||||||
px={3}
|
px={4}
|
||||||
py={1}
|
py={2}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
fontSize="xs"
|
fontSize="sm"
|
||||||
>
|
>
|
||||||
当前显示 {currentData.length} 个{currentLevel === 'lv1' ? '一级' : currentLevel === 'lv2' ? '二级' : '三级'}分类
|
当前显示 {currentData.length} 个
|
||||||
|
{currentLevel === 'lv1' ? '一级' : currentLevel === 'lv2' ? '二级' : '三级'}分类
|
||||||
</Badge>
|
</Badge>
|
||||||
{currentLevel === 'lv1' && (
|
{currentLevel === 'lv1' && (
|
||||||
<>
|
<Badge
|
||||||
<Badge
|
colorScheme="cyan"
|
||||||
colorScheme="blue"
|
px={4}
|
||||||
px={3}
|
py={2}
|
||||||
py={1}
|
borderRadius="full"
|
||||||
borderRadius="full"
|
fontSize="sm"
|
||||||
fontSize="xs"
|
>
|
||||||
>
|
共 {hierarchy.reduce((acc, h) => acc + h.concept_count, 0)} 个概念
|
||||||
共 {hierarchy.reduce((acc, h) => acc + (h.children?.length || 0), 0)} 个二级分类
|
</Badge>
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
colorScheme="cyan"
|
|
||||||
px={3}
|
|
||||||
py={1}
|
|
||||||
borderRadius="full"
|
|
||||||
fontSize="xs"
|
|
||||||
>
|
|
||||||
共 {hierarchy.reduce((acc, h) => acc + h.concept_count, 0)} 个概念
|
|
||||||
</Badge>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user