fix(ForceGraphView): 标签显示优化

- 第一层 upperLabel 添加 overflow: break,超长标题折行
- 添加 getConceptLabel 辅助函数
- 所有概念节点添加 label 配置,确保详情按钮始终显示
- 支持外部 drillPath 控制(externalDrillPath, onDrillPathChange)
- hideNavigation 模式支持(供 ChartContainer 使用)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2026-01-08 19:03:59 +08:00
parent 5ae05eebd8
commit 582cc073a0

View File

@@ -26,20 +26,13 @@ import {
Button, Button,
IconButton, IconButton,
Tooltip, Tooltip,
Badge,
useBreakpointValue, useBreakpointValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { keyframes } from '@emotion/react'; import { keyframes } from '@emotion/react';
import { import {
Layers, Layers,
RefreshCw,
Maximize2, Maximize2,
Minimize2, Minimize2,
Home,
ArrowUp,
ArrowDown,
Circle,
Grid3x3,
ChevronRight, ChevronRight,
ArrowLeft, ArrowLeft,
} from 'lucide-react'; } from 'lucide-react';
@@ -175,6 +168,11 @@ const ForceGraphView = ({
apiBaseUrl, apiBaseUrl,
onSelectCategory, onSelectCategory,
selectedDate, selectedDate,
// 外部控制的 drillPath可选用于状态共享
externalDrillPath,
onDrillPathChange,
// 是否隐藏导航和背景(由外部 ChartContainer 提供时设为 true
hideNavigation = false,
}) => { }) => {
const [hierarchy, setHierarchy] = useState([]); const [hierarchy, setHierarchy] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -183,8 +181,10 @@ const ForceGraphView = ({
const [priceLoading, setPriceLoading] = useState(false); const [priceLoading, setPriceLoading] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
// 钻取状态:记录当前查看的路径 // 钻取状态:支持受控和非受控模式
const [drillPath, setDrillPath] = useState(null); const [internalDrillPath, setInternalDrillPath] = useState(null);
const drillPath = externalDrillPath !== undefined ? externalDrillPath : internalDrillPath;
const setDrillPath = onDrillPathChange || setInternalDrillPath;
const chartRef = useRef(); const chartRef = useRef();
const containerRef = useRef(); const containerRef = useRef();
@@ -295,6 +295,40 @@ const ForceGraphView = ({
return count || 1; return count || 1;
}, []); }, []);
// 生成概念节点的 label 配置(确保详情按钮始终显示)
const getConceptLabel = useCallback((conceptName, changePct) => {
const changeStr = changePct !== undefined && changePct !== null
? ` {change|${formatChangePercent(changePct)}}`
: '';
return {
show: true,
position: 'insideTopLeft',
fontSize: 11,
padding: [4, 6],
overflow: 'break',
formatter: `{name|${conceptName}}${changeStr}\n{btn|详情 >}`,
rich: {
name: {
fontSize: 11,
fontWeight: 'bold',
lineHeight: 16,
},
change: {
fontSize: 10,
lineHeight: 16,
},
btn: {
fontSize: 9,
color: '#A78BFA',
backgroundColor: 'rgba(139, 92, 246, 0.3)',
borderRadius: 3,
padding: [2, 6],
lineHeight: 18,
}
}
};
}, []);
// 根据钻取路径构建 Treemap 数据(使用概念数量作为面积) // 根据钻取路径构建 Treemap 数据(使用概念数量作为面积)
const treemapData = useMemo(() => { const treemapData = useMemo(() => {
const { lv1Map, lv2Map, lv3Map, leafMap } = priceData; const { lv1Map, lv2Map, lv3Map, leafMap } = priceData;
@@ -322,6 +356,7 @@ const ForceGraphView = ({
conceptCount: lv1.concept_count, conceptCount: lv1.concept_count,
baseColor: lv1BaseColor, baseColor: lv1BaseColor,
bgColor: bgColor, bgColor: bgColor,
topStocks: lv1Price.top_stocks,
}, },
children: [], children: [],
}; };
@@ -350,6 +385,7 @@ const ForceGraphView = ({
conceptCount: lv2ConceptCount, conceptCount: lv2ConceptCount,
baseColor: lv1BaseColor, baseColor: lv1BaseColor,
bgColor: lv2BgColor, bgColor: lv2BgColor,
topStocks: lv2Price.top_stocks,
}, },
children: [], children: [],
}; };
@@ -379,6 +415,7 @@ const ForceGraphView = ({
conceptCount: lv3ConceptCount, conceptCount: lv3ConceptCount,
baseColor: lv1BaseColor, baseColor: lv1BaseColor,
bgColor: lv3BgColor, bgColor: lv3BgColor,
topStocks: lv3Price.top_stocks,
hasChildren: lv3.concepts && lv3.concepts.length > 0, hasChildren: lv3.concepts && lv3.concepts.length > 0,
}, },
}); });
@@ -469,6 +506,7 @@ const ForceGraphView = ({
borderWidth: 1, borderWidth: 1,
borderRadius: 6, borderRadius: 6,
}, },
label: getConceptLabel(conceptName, conceptPrice.avg_change_pct),
data: { data: {
level: 'concept', level: 'concept',
parentLv1: lv1.name, parentLv1: lv1.name,
@@ -501,6 +539,7 @@ const ForceGraphView = ({
borderWidth: 1, borderWidth: 1,
borderRadius: 6, borderRadius: 6,
}, },
label: getConceptLabel(conceptName, conceptPrice.avg_change_pct),
data: { data: {
level: 'concept', level: 'concept',
parentLv1: lv1.name, parentLv1: lv1.name,
@@ -573,6 +612,7 @@ const ForceGraphView = ({
borderWidth: 1, borderWidth: 1,
borderRadius: 8, borderRadius: 8,
}, },
label: getConceptLabel(conceptName, conceptPrice.avg_change_pct),
data: { data: {
level: 'concept', level: 'concept',
parentLv1: lv1.name, parentLv1: lv1.name,
@@ -605,6 +645,7 @@ const ForceGraphView = ({
borderWidth: 2, borderWidth: 2,
borderRadius: 12, borderRadius: 12,
}, },
label: getConceptLabel(conceptName, conceptPrice.avg_change_pct),
data: { data: {
level: 'concept', level: 'concept',
parentLv1: lv1.name, parentLv1: lv1.name,
@@ -646,6 +687,7 @@ const ForceGraphView = ({
borderWidth: 2, borderWidth: 2,
borderRadius: 12, borderRadius: 12,
}, },
label: getConceptLabel(conceptName, conceptPrice.avg_change_pct),
data: { data: {
level: 'concept', level: 'concept',
parentLv1: lv1.name, parentLv1: lv1.name,
@@ -661,7 +703,7 @@ const ForceGraphView = ({
} }
return []; return [];
}, [hierarchy, priceData, drillPath, getConceptCount]); }, [hierarchy, priceData, drillPath, getConceptCount, getConceptLabel]);
// ECharts 配置 - 玻璃态深空风格 // ECharts 配置 - 玻璃态深空风格
const chartOption = useMemo(() => { const chartOption = useMemo(() => {
@@ -687,22 +729,29 @@ const ForceGraphView = ({
}, },
upperLabel: { upperLabel: {
show: true, show: true,
height: 32, height: 36,
fontSize: 14, fontSize: 14,
fontWeight: 'bold', fontWeight: 'bold',
padding: [0, 8], padding: [8, 8, 0, 8],
overflow: 'break',
formatter: (params) => { formatter: (params) => {
const data = params.data?.data || {}; const data = params.data?.data || {};
const changePct = data.changePct; const changePct = data.changePct;
const changeStr = changePct !== undefined && changePct !== null const changeStr = changePct !== undefined && changePct !== null
? ` ${formatChangePercent(changePct)}` ? ` ${formatChangePercent(changePct)}`
: ''; : '';
return `${params.name}${changeStr}`; return `${params.name}${changeStr} {icon|⤢}`;
}, },
rich: { rich: {
name: { name: {
fontSize: 14, fontSize: 14,
fontWeight: 'bold', fontWeight: 'bold',
},
icon: {
fontSize: 20,
color: 'rgba(255, 255, 255, 0.7)',
verticalAlign: 'middle',
padding: [0, 0, 0, 8],
} }
} }
}, },
@@ -723,18 +772,26 @@ const ForceGraphView = ({
}, },
upperLabel: { upperLabel: {
show: true, show: true,
height: 26, height: 30,
fontSize: 12, fontSize: 12,
fontWeight: 'bold', fontWeight: 'bold',
padding: [0, 6], padding: [6, 6, 0, 6],
formatter: (params) => { formatter: (params) => {
const data = params.data?.data || {}; const data = params.data?.data || {};
const changePct = data.changePct; const changePct = data.changePct;
const changeStr = changePct !== undefined && changePct !== null const changeStr = changePct !== undefined && changePct !== null
? ` ${formatChangePercent(changePct)}` ? ` ${formatChangePercent(changePct)}`
: ''; : '';
return `${params.name}${changeStr}`; return `${params.name}${changeStr} {icon|⤢}`;
}, },
rich: {
icon: {
fontSize: 16,
color: 'rgba(255, 255, 255, 0.6)',
verticalAlign: 'middle',
padding: [0, 0, 0, 6],
}
}
}, },
}, },
{ {
@@ -750,9 +807,9 @@ const ForceGraphView = ({
position: 'insideTopLeft', position: 'insideTopLeft',
fontSize: 11, fontSize: 11,
padding: [4, 6], padding: [4, 6],
overflow: 'break',
formatter: (params) => { formatter: (params) => {
const data = params.data?.data || {}; const data = params.data?.data || {};
const bgColor = data.bgColor;
const changePct = data.changePct; const changePct = data.changePct;
if (changePct !== undefined && changePct !== null) { if (changePct !== undefined && changePct !== null) {
return `{name|${params.name}}\n{change|${formatChangePercent(changePct)}}`; return `{name|${params.name}}\n{change|${formatChangePercent(changePct)}}`;
@@ -785,23 +842,32 @@ const ForceGraphView = ({
position: 'insideTopLeft', position: 'insideTopLeft',
fontSize: 10, fontSize: 10,
padding: [3, 5], padding: [3, 5],
overflow: 'break',
formatter: (params) => { formatter: (params) => {
const data = params.data?.data || {}; const data = params.data?.data || {};
const changePct = data.changePct; const changePct = data.changePct;
if (changePct !== undefined && changePct !== null) { if (changePct !== undefined && changePct !== null) {
return `{name|${params.name}}\n{change|${formatChangePercent(changePct)}}`; return `{name|${params.name}} {change|${formatChangePercent(changePct)}}\n{btn|详情 >}`;
} }
return params.name; return `{name|${params.name}}\n{btn|详情 >}`;
}, },
rich: { rich: {
name: { name: {
fontSize: 10, fontSize: 10,
fontWeight: 'bold', fontWeight: 'bold',
lineHeight: 14, lineHeight: 16,
}, },
change: { change: {
fontSize: 9, fontSize: 9,
lineHeight: 12, lineHeight: 16,
},
btn: {
fontSize: 9,
color: '#A78BFA',
backgroundColor: 'rgba(139, 92, 246, 0.3)',
borderRadius: 3,
padding: [2, 6],
lineHeight: 18,
} }
} }
}, },
@@ -824,61 +890,30 @@ const ForceGraphView = ({
}, },
formatter: (params) => { formatter: (params) => {
const data = params.data?.data || {}; const data = params.data?.data || {};
const levelMap = {
'lv1': '一级分类',
'lv2': '二级分类',
'lv3': '三级分类',
'concept': '概念',
};
const changePct = data.changePct; const changePct = data.changePct;
const changeColor = changePct > 0 ? '#F87171' : changePct < 0 ? '#4ADE80' : '#94A3B8'; const changeColor = changePct > 0 ? '#F87171' : changePct < 0 ? '#4ADE80' : '#94A3B8';
const changeIcon = changePct > 0 ? '▲' : changePct < 0 ? '▼' : '●'; const changeIcon = changePct > 0 ? '▲' : changePct < 0 ? '▼' : '●';
const topStocksStr = data.topStocks?.slice(0, 3).join('、') || '';
return ` return `
<div style="min-width: 200px;"> <div style="min-width: 180px;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;"> <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
<span style="
background: linear-gradient(135deg, ${data.baseColor || '#8B5CF6'}, ${data.baseColor || '#8B5CF6'}88);
color: white;
padding: 3px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
box-shadow: 0 2px 8px ${data.baseColor || '#8B5CF6'}44;
">${levelMap[data.level] || '分类'}</span>
</div>
<div style="font-size: 17px; font-weight: bold; margin-bottom: 12px; color: #FFFFFF; text-shadow: 0 2px 4px rgba(0,0,0,0.3);">
${params.name}
</div>
${changePct !== undefined && changePct !== null ? ` ${changePct !== undefined && changePct !== null ? `
<div style=" <span style="color: ${changeColor}; font-size: 12px;">${changeIcon}</span>
display: inline-flex; <span style="color: ${changeColor}; font-weight: bold; font-size: 16px; font-family: 'SF Mono', monospace;">
align-items: center;
gap: 8px;
background: ${changePct > 0 ? 'rgba(248, 113, 113, 0.15)' : changePct < 0 ? 'rgba(74, 222, 128, 0.15)' : 'rgba(148, 163, 184, 0.15)'};
padding: 8px 14px;
border-radius: 12px;
margin-bottom: 12px;
border: 1px solid ${changePct > 0 ? 'rgba(248, 113, 113, 0.3)' : changePct < 0 ? 'rgba(74, 222, 128, 0.3)' : 'rgba(148, 163, 184, 0.3)'};
">
<span style="color: ${changeColor}; font-size: 14px;">${changeIcon}</span>
<span style="color: ${changeColor}; font-weight: bold; font-size: 20px; font-family: 'SF Mono', monospace;">
${formatChangePercent(changePct)} ${formatChangePercent(changePct)}
</span> </span>
</div>
` : ''} ` : ''}
<div style="color: #94A3B8; font-size: 12px; display: flex; gap: 12px;"> <span style="font-size: 15px; font-weight: bold; color: #FFFFFF;">
${data.stockCount ? `<span>📊 ${data.stockCount} 只股票</span>` : ''} ${params.name}
${data.conceptCount ? `<span>📁 ${data.conceptCount} 个概念</span>` : ''} </span>
</div> </div>
${data.level === 'concept' ? ` <div style="color: #94A3B8; font-size: 12px; margin-bottom: 6px;">
<div style="color: #A78BFA; font-size: 11px; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.1);"> ${data.stockCount ? `${data.stockCount} 只股票` : ''}${data.stockCount && data.conceptCount ? ' · ' : ''}${data.conceptCount ? `${data.conceptCount} 个概念` : ''}
🔗 点击查看概念详情
</div> </div>
` : data.level !== 'concept' ? ` ${topStocksStr ? `
<div style="color: #64748B; font-size: 11px; margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.1);"> <div style="color: #A78BFA; font-size: 11px;">
📂 点击进入查看子分类 热门: ${topStocksStr}
</div> </div>
` : ''} ` : ''}
</div> </div>
@@ -974,16 +1009,6 @@ const ForceGraphView = ({
} }
}, [drillPath]); }, [drillPath]);
// 返回根视图
const handleGoHome = useCallback(() => {
setDrillPath(null);
}, []);
// 刷新数据
const handleRefresh = useCallback(() => {
fetchHierarchyPrice();
}, [fetchHierarchyPrice]);
// 全屏切换 // 全屏切换
const toggleFullscreen = useCallback(() => { const toggleFullscreen = useCallback(() => {
setIsFullscreen(prev => !prev); setIsFullscreen(prev => !prev);
@@ -1070,159 +1095,47 @@ const ForceGraphView = ({
); );
} }
// 数据为空时显示紧凑提示
if (!loading && !error && hierarchy.length === 0) {
return ( return (
<VStack spacing={3} align="stretch" w="100%"> <Center
{/* 外部工具栏 - 在蓝紫色背景外面 */} h="200px"
<HStack spacing={3} px={2} justify="space-between" align="center"> bg="rgba(15, 23, 42, 0.6)"
<HStack spacing={3}> borderRadius="3xl"
{/* 返回按钮 */} backdropFilter={GLASS_BLUR.lg}
{drillPath && (
<Tooltip label="返回上一层" placement="bottom">
<IconButton
size="sm"
icon={<ArrowLeft />}
onClick={handleGoBack}
bg="whiteAlpha.100"
color="white"
border="1px solid"
borderColor="whiteAlpha.200"
borderRadius="full"
_hover={{
bg: 'purple.500',
borderColor: 'purple.400',
transform: 'scale(1.05)',
}}
transition="all 0.2s"
aria-label="返回"
/>
</Tooltip>
)}
<HStack
bg="whiteAlpha.100"
px={4}
py={2}
borderRadius="full"
border="1px solid"
borderColor="whiteAlpha.200"
>
<Icon as={Grid3x3} color="purple.300" />
<Text color="white" fontWeight="bold" fontSize="sm">
概念矩形树图
</Text>
</HStack>
{/* 面包屑导航 */}
<HStack
bg="whiteAlpha.50"
px={4}
py={2}
borderRadius="full"
border="1px solid" border="1px solid"
borderColor="whiteAlpha.100" borderColor="whiteAlpha.100"
spacing={2}
> >
{breadcrumbItems.map((item, index) => ( <VStack spacing={3}>
<HStack key={index} spacing={2}> <Icon as={Layers} boxSize={12} color="whiteAlpha.300" />
{index > 0 && ( <Text color="whiteAlpha.600" fontSize="sm">暂无概念层级数据</Text>
<Icon as={ChevronRight} color="whiteAlpha.400" boxSize={3} /> </VStack>
)} </Center>
<Text );
color={index === breadcrumbItems.length - 1 ? 'purple.300' : 'whiteAlpha.700'}
fontSize="sm"
fontWeight={index === breadcrumbItems.length - 1 ? 'bold' : 'normal'}
cursor={index < breadcrumbItems.length - 1 ? 'pointer' : 'default'}
_hover={index < breadcrumbItems.length - 1 ? { color: 'white' } : {}}
transition="color 0.2s"
onClick={() => {
if (index < breadcrumbItems.length - 1) {
setDrillPath(item.path);
} }
}}
>
{item.label}
</Text>
</HStack>
))}
</HStack>
</HStack>
{/* 右侧控制按钮 */} // 当 hideNavigation 为 true 时,只渲染纯图表内容
<HStack spacing={2}> if (hideNavigation) {
{priceLoading && <Spinner size="sm" color="purple.300" />} return (
<ReactECharts
{drillPath && ( ref={chartRef}
<Tooltip label="返回全部" placement="left"> option={chartOption}
<IconButton style={{ height: '100%', width: '100%' }}
size="sm" onEvents={onChartEvents}
icon={<Home />} opts={{ renderer: 'canvas' }}
onClick={handleGoHome}
bg="whiteAlpha.100"
color="white"
border="1px solid"
borderColor="whiteAlpha.200"
borderRadius="full"
_hover={{
bg: 'purple.500',
borderColor: 'purple.400',
transform: 'scale(1.05)',
}}
transition="all 0.2s"
aria-label="返回全部"
/> />
</Tooltip> );
)} }
<Tooltip label="刷新数据" placement="left"> return (
<IconButton <VStack spacing={0} align="stretch" w="100%">
size="sm" {/* 图表容器 */}
icon={<RefreshCw />}
onClick={handleRefresh}
isLoading={priceLoading}
bg="whiteAlpha.100"
color="white"
border="1px solid"
borderColor="whiteAlpha.200"
borderRadius="full"
_hover={{
bg: 'whiteAlpha.200',
borderColor: 'whiteAlpha.300',
transform: 'scale(1.05)',
}}
transition="all 0.2s"
aria-label="刷新"
/>
</Tooltip>
<Tooltip label={isFullscreen ? '退出全屏' : '全屏'} placement="left">
<IconButton
size="sm"
icon={isFullscreen ? <Minimize2 /> : <Maximize2 />}
onClick={toggleFullscreen}
bg="whiteAlpha.100"
color="white"
border="1px solid"
borderColor="whiteAlpha.200"
borderRadius="full"
_hover={{
bg: 'whiteAlpha.200',
borderColor: 'whiteAlpha.300',
transform: 'scale(1.05)',
}}
transition="all 0.2s"
aria-label={isFullscreen ? '退出全屏' : '全屏'}
/>
</Tooltip>
</HStack>
</HStack>
{/* 蓝紫色背景容器 */}
<Box <Box
ref={containerRef} ref={containerRef}
position={isFullscreen ? 'fixed' : 'relative'} position={isFullscreen ? 'fixed' : 'relative'}
top={isFullscreen ? '60px' : 'auto'} top={isFullscreen ? '60px' : 'auto'}
left={isFullscreen ? 0 : 'auto'} left={isFullscreen ? 0 : 'auto'}
right={isFullscreen ? 0 : 'auto'} right={isFullscreen ? '72px' : 'auto'}
bottom={isFullscreen ? 0 : 'auto'} bottom={isFullscreen ? 0 : 'auto'}
zIndex={isFullscreen ? 1000 : 'auto'} zIndex={isFullscreen ? 1000 : 'auto'}
borderRadius={isFullscreen ? '0' : '3xl'} borderRadius={isFullscreen ? '0' : '3xl'}
@@ -1279,73 +1192,95 @@ const ForceGraphView = ({
animation={`${glowPulse} 6s ease-in-out infinite 2s`} animation={`${glowPulse} 6s ease-in-out infinite 2s`}
/> />
{/* 底部图例 - 毛玻璃风格 */} {/* 左上角面包屑导航 */}
<Flex <HStack
position="absolute" position="absolute"
bottom={4} top={4}
left={4} left={4}
zIndex={10} zIndex={10}
gap={2} spacing={2}
flexWrap="wrap"
pointerEvents="none"
> >
{drillPath && (
<Tooltip label="返回上一层">
<IconButton
icon={<ArrowLeft size={16} />}
size="sm"
variant="ghost"
onClick={handleGoBack}
bg="rgba(255, 255, 255, 0.08)"
backdropFilter={GLASS_BLUR.lg}
border="1px solid"
borderColor="whiteAlpha.100"
color="whiteAlpha.800"
borderRadius="full"
_hover={{
bg: 'rgba(255, 255, 255, 0.15)',
transform: 'scale(1.05)',
}}
transition="all 0.2s"
/>
</Tooltip>
)}
<HStack <HStack
bg="rgba(255, 255, 255, 0.08)" bg="rgba(255, 255, 255, 0.08)"
backdropFilter={GLASS_BLUR.lg} backdropFilter={GLASS_BLUR.lg}
px={4} px={3}
py={2} py={1.5}
borderRadius="full" borderRadius="full"
border="1px solid" border="1px solid"
borderColor="whiteAlpha.100" borderColor="whiteAlpha.100"
spacing={3} spacing={1}
boxShadow="0 4px 16px rgba(0, 0, 0, 0.2)" boxShadow="0 4px 16px rgba(0, 0, 0, 0.2)"
> >
<HStack spacing={2}> {breadcrumbItems.map((item, index) => (
<Box <React.Fragment key={index}>
w={3} {index > 0 && (
h={3} <Icon as={ChevronRight} boxSize={3} color="whiteAlpha.400" />
borderRadius="full" )}
bg="linear-gradient(135deg, #EF4444, #DC2626)" <Text
boxShadow="0 0 8px rgba(239, 68, 68, 0.5)" fontSize="xs"
/> color={index === breadcrumbItems.length - 1 ? 'purple.300' : 'whiteAlpha.700'}
<Text color="whiteAlpha.800" fontSize="xs"></Text> fontWeight={index === breadcrumbItems.length - 1 ? 'bold' : 'normal'}
</HStack> cursor={index < breadcrumbItems.length - 1 ? 'pointer' : 'default'}
<Box w="1px" h={4} bg="whiteAlpha.200" /> _hover={index < breadcrumbItems.length - 1 ? { color: 'purple.300' } : {}}
<HStack spacing={2}> onClick={() => {
<Box if (index < breadcrumbItems.length - 1) {
w={3} setDrillPath(item.path);
h={3} }
borderRadius="full" }}
bg="linear-gradient(135deg, #22C55E, #16A34A)" transition="color 0.2s"
boxShadow="0 0 8px rgba(34, 197, 94, 0.5)" >
/> {item.label}
<Text color="whiteAlpha.800" fontSize="xs"></Text> </Text>
</React.Fragment>
))}
</HStack> </HStack>
</HStack> </HStack>
</Flex>
{/* 操作提示 */} {/* 右上角放大按钮 */}
<Box <Tooltip label={isFullscreen ? '退出全屏' : '全屏查看'}>
<IconButton
position="absolute" position="absolute"
bottom={4} top={4}
right={4} right={4}
zIndex={10} zIndex={10}
pointerEvents="none" icon={isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
> size="sm"
<HStack variant="ghost"
bg="rgba(255, 255, 255, 0.06)" onClick={toggleFullscreen}
bg="rgba(255, 255, 255, 0.08)"
backdropFilter={GLASS_BLUR.lg} backdropFilter={GLASS_BLUR.lg}
px={4}
py={2}
borderRadius="full"
border="1px solid" border="1px solid"
borderColor="whiteAlpha.100" borderColor="whiteAlpha.100"
> color="whiteAlpha.800"
<Text color="whiteAlpha.500" fontSize="xs"> borderRadius="full"
点击分类进入 · 点击概念查看详情 _hover={{
</Text> bg: 'rgba(255, 255, 255, 0.15)',
</HStack> transform: 'scale(1.05)',
</Box> }}
transition="all 0.2s"
/>
</Tooltip>
{/* ECharts 图表 */} {/* ECharts 图表 */}
<ReactECharts <ReactECharts