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,
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user