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