From 1cdfb732f6f1a9ed7daad9e310f5938a9fc327e2 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Fri, 5 Dec 2025 17:59:19 +0800 Subject: [PATCH] update pay ui --- .../Concept/components/ForceGraphView.js | 767 +++++++++--------- 1 file changed, 393 insertions(+), 374 deletions(-) diff --git a/src/views/Concept/components/ForceGraphView.js b/src/views/Concept/components/ForceGraphView.js index c35cbb0c..72774167 100644 --- a/src/views/Concept/components/ForceGraphView.js +++ b/src/views/Concept/components/ForceGraphView.js @@ -1,20 +1,15 @@ /** - * ForceGraphView - 概念层级关系图(使用 reagraph) + * SunburstView - 概念层级旭日图 * * 特性: - * 1. 清晰的层级布局(Radial/Hierarchical) - * 2. 节点大小根据股票数量动态调整 - * 3. 涨红跌绿颜色映射 - * 4. 悬停显示涨跌幅详情 - * 5. 点击节点可聚焦或跳转 + * 1. 同心圆环展示层级关系,从内到外:根 → 一级 → 二级 → 三级 → 概念 + * 2. 涨红跌绿颜色映射 + * 3. 点击扇区可钻取到子层级 + * 4. 悬停显示详细信息 + * 5. 支持返回上级 */ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import { - GraphCanvas, - useSelection, - lightTheme, - darkTheme, -} from 'reagraph'; +import ReactECharts from 'echarts-for-react'; import { Box, VStack, @@ -29,29 +24,22 @@ import { Tooltip, Badge, useBreakpointValue, - Select, - FormControl, - FormLabel, - Collapse, - useDisclosure, - Divider, } from '@chakra-ui/react'; import { FaLayerGroup, FaSync, FaExpand, FaCompress, - FaCog, FaHome, - FaChevronUp, FaArrowUp, FaArrowDown, - FaProjectDiagram, FaCircle, + FaChartPie, + FaUndo, } from 'react-icons/fa'; import { logger } from '../../../utils/logger'; -// 一级分类颜色映射 +// 一级分类颜色映射(基础色) const LV1_COLORS = { '人工智能': '#8B5CF6', '半导体': '#3B82F6', @@ -74,33 +62,23 @@ const LV1_COLORS = { const getChangeColor = (value, baseColor = '#64748B') => { if (value === null || value === undefined) return baseColor; - // 涨 - 红色系(更鲜艳) + // 涨 - 红色系 if (value > 7) return '#DC2626'; if (value > 5) return '#EF4444'; if (value > 3) return '#F87171'; if (value > 1) return '#FCA5A5'; - if (value > 0) return '#FECACA'; + if (value > 0) return '#FED7D7'; // 跌 - 绿色系 if (value < -7) return '#15803D'; if (value < -5) return '#16A34A'; if (value < -3) return '#22C55E'; if (value < -1) return '#4ADE80'; - if (value < 0) return '#86EFAC'; + if (value < 0) return '#BBF7D0'; return baseColor; // 平盘 }; -// 根据涨跌幅获取发光颜色 -const getGlowColor = (value) => { - if (value === null || value === undefined) return 'rgba(100, 116, 139, 0.5)'; - if (value > 3) return 'rgba(239, 68, 68, 0.6)'; - if (value > 0) return 'rgba(252, 165, 165, 0.5)'; - if (value < -3) return 'rgba(34, 197, 94, 0.6)'; - if (value < 0) return 'rgba(134, 239, 172, 0.5)'; - return 'rgba(100, 116, 139, 0.5)'; -}; - // 从 API 返回的名称中提取纯名称 const extractPureName = (apiName) => { if (!apiName) return ''; @@ -114,50 +92,6 @@ const formatChangePercent = (value) => { return value > 0 ? `+${formatted}%` : value < 0 ? `-${formatted}%` : '0.00%'; }; -/** - * 自定义深色主题 - */ -const customDarkTheme = { - ...darkTheme, - canvas: { - background: 'transparent', - }, - node: { - ...darkTheme.node, - fill: '#8B5CF6', - activeFill: '#A78BFA', - label: { - color: '#E2E8F0', - stroke: '#0F172A', - activeColor: '#FFFFFF', - }, - }, - edge: { - ...darkTheme.edge, - fill: '#475569', - activeFill: '#8B5CF6', - }, - ring: { - ...darkTheme.ring, - fill: '#8B5CF6', - activeFill: '#A78BFA', - }, - arrow: { - ...darkTheme.arrow, - fill: '#475569', - activeFill: '#8B5CF6', - }, - cluster: { - ...darkTheme.cluster, - stroke: '#475569', - fill: 'rgba(139, 92, 246, 0.1)', - label: { - ...darkTheme.cluster?.label, - color: '#94A3B8', - }, - }, -}; - /** * 主组件 */ @@ -172,13 +106,11 @@ const ForceGraphView = ({ const [priceData, setPriceData] = useState({ lv1Map: {}, lv2Map: {}, lv3Map: {}, leafMap: {} }); const [priceLoading, setPriceLoading] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); - const [layoutType, setLayoutType] = useState('radialOut2d'); // 'radialOut2d', 'treeTd2d', 'treeLr2d', 'forceDirected2d' - const [displayLevel, setDisplayLevel] = useState('lv2'); // 'lv1', 'lv2', 'lv3', 'all' - const [selectedNode, setSelectedNode] = useState(null); + const [hoveredItem, setHoveredItem] = useState(null); + const [drillPath, setDrillPath] = useState([]); // 钻取路径 - const graphRef = useRef(); + const chartRef = useRef(); const containerRef = useRef(); - const { isOpen: isSettingsOpen, onToggle: onSettingsToggle } = useDisclosure({ defaultIsOpen: false }); const isMobile = useBreakpointValue({ base: true, md: false }); @@ -194,12 +126,12 @@ const ForceGraphView = ({ const data = await response.json(); setHierarchy(data.hierarchy || []); - logger.info('ForceGraphView', '层级结构加载完成', { + logger.info('SunburstView', '层级结构加载完成', { totalLv1: data.hierarchy?.length, totalConcepts: data.total_concepts }); } catch (err) { - logger.error('ForceGraphView', 'fetchHierarchy', err); + logger.error('SunburstView', 'fetchHierarchy', err); setError(err.message); } finally { setLoading(false); @@ -219,7 +151,7 @@ const ForceGraphView = ({ const response = await fetch(url); if (!response.ok) { - logger.warn('ForceGraphView', '获取层级涨跌幅失败', { status: response.status }); + logger.warn('SunburstView', '获取层级涨跌幅失败', { status: response.status }); return; } @@ -248,12 +180,12 @@ const ForceGraphView = ({ setPriceData({ lv1Map, lv2Map, lv3Map, leafMap }); - logger.info('ForceGraphView', '层级涨跌幅加载完成', { + logger.info('SunburstView', '层级涨跌幅加载完成', { lv1Count: Object.keys(lv1Map).length, lv2Count: Object.keys(lv2Map).length, }); } catch (err) { - logger.warn('ForceGraphView', '获取层级涨跌幅失败', { error: err.message }); + logger.warn('SunburstView', '获取层级涨跌幅失败', { error: err.message }); } finally { setPriceLoading(false); } @@ -269,31 +201,20 @@ const ForceGraphView = ({ } }, [hierarchy, fetchHierarchyPrice]); - // 构建图数据 - const graphData = useMemo(() => { - const nodes = []; - const edges = []; + // 构建旭日图数据 + const sunburstData = useMemo(() => { const { lv1Map, lv2Map, lv3Map, leafMap } = priceData; - // 根节点 - nodes.push({ - id: 'root', - label: '概念中心', - data: { level: 'root', changePct: null }, - fill: '#8B5CF6', - size: 60, - }); - - hierarchy.forEach((lv1) => { - const lv1Id = `lv1_${lv1.name}`; - const lv1Price = lv1Map[lv1.name] || {}; + const buildChildren = (lv1) => { const lv1BaseColor = LV1_COLORS[lv1.name] || '#8B5CF6'; - const lv1Color = getChangeColor(lv1Price.avg_change_pct, lv1BaseColor); + const lv1Price = lv1Map[lv1.name] || {}; - // 一级节点 - nodes.push({ - id: lv1Id, - label: lv1.name, + const lv1Node = { + name: lv1.name, + value: lv1Price.stock_count || lv1.concept_count * 10 || 100, + itemStyle: { + color: getChangeColor(lv1Price.avg_change_pct, lv1BaseColor), + }, data: { level: 'lv1', changePct: lv1Price.avg_change_pct, @@ -301,27 +222,20 @@ const ForceGraphView = ({ conceptCount: lv1.concept_count, baseColor: lv1BaseColor, }, - fill: lv1Color, - size: Math.max(35, Math.min(55, 35 + (lv1Price.stock_count || 0) / 50)), - }); + children: [], + }; - edges.push({ - id: `root-${lv1Id}`, - source: 'root', - target: lv1Id, - size: 2, - }); - - // 二级节点 - if (lv1.children && (displayLevel === 'lv2' || displayLevel === 'lv3' || displayLevel === 'all')) { + // 二级 + if (lv1.children) { lv1.children.forEach((lv2) => { - const lv2Id = `lv2_${lv1.name}_${lv2.name}`; const lv2Price = lv2Map[lv2.name] || {}; - const lv2Color = getChangeColor(lv2Price.avg_change_pct, lv1BaseColor); - nodes.push({ - id: lv2Id, - label: lv2.name, + const lv2Node = { + name: lv2.name, + value: lv2Price.stock_count || lv2.concept_count * 5 || 50, + itemStyle: { + color: getChangeColor(lv2Price.avg_change_pct, lv1BaseColor), + }, data: { level: 'lv2', parentLv1: lv1.name, @@ -330,27 +244,20 @@ const ForceGraphView = ({ conceptCount: lv2.concept_count, baseColor: lv1BaseColor, }, - fill: lv2Color, - size: Math.max(25, Math.min(40, 25 + (lv2Price.stock_count || 0) / 80)), - }); + children: [], + }; - edges.push({ - id: `${lv1Id}-${lv2Id}`, - source: lv1Id, - target: lv2Id, - size: 1.5, - }); - - // 三级节点 - if (lv2.children && (displayLevel === 'lv3' || displayLevel === 'all')) { + // 三级 + if (lv2.children) { lv2.children.forEach((lv3) => { - const lv3Id = `lv3_${lv1.name}_${lv2.name}_${lv3.name}`; const lv3Price = lv3Map[lv3.name] || {}; - const lv3Color = getChangeColor(lv3Price.avg_change_pct, lv1BaseColor); - nodes.push({ - id: lv3Id, - label: lv3.name, + const lv3Node = { + name: lv3.name, + value: lv3Price.stock_count || 30, + itemStyle: { + color: getChangeColor(lv3Price.avg_change_pct, lv1BaseColor), + }, data: { level: 'lv3', parentLv1: lv1.name, @@ -359,104 +266,264 @@ const ForceGraphView = ({ stockCount: lv3Price.stock_count, baseColor: lv1BaseColor, }, - fill: lv3Color, - size: Math.max(18, Math.min(30, 18 + (lv3Price.stock_count || 0) / 100)), - }); - - edges.push({ - id: `${lv2Id}-${lv3Id}`, - source: lv2Id, - target: lv3Id, - size: 1, - }); + children: [], + }; // 叶子概念 - if (displayLevel === 'all' && lv3.concepts) { - lv3.concepts.slice(0, 5).forEach((conceptName) => { // 限制每个lv3只显示5个概念 - const conceptId = `concept_${conceptName}`; + if (lv3.concepts) { + lv3.concepts.forEach((conceptName) => { const conceptPrice = leafMap[conceptName] || {}; - const conceptColor = getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor); - - // 避免重复添加节点 - if (!nodes.find(n => n.id === conceptId)) { - nodes.push({ - id: conceptId, - label: conceptName, - data: { - level: 'concept', - parentLv1: lv1.name, - parentLv2: lv2.name, - parentLv3: lv3.name, - changePct: conceptPrice.avg_change_pct, - stockCount: conceptPrice.stock_count, - baseColor: lv1BaseColor, - }, - fill: conceptColor, - size: 12, - }); - } - - edges.push({ - id: `${lv3Id}-${conceptId}`, - source: lv3Id, - target: conceptId, - size: 0.5, + lv3Node.children.push({ + name: conceptName, + value: conceptPrice.stock_count || 10, + itemStyle: { + color: getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor), + }, + data: { + level: 'concept', + parentLv1: lv1.name, + parentLv2: lv2.name, + parentLv3: lv3.name, + changePct: conceptPrice.avg_change_pct, + stockCount: conceptPrice.stock_count, + baseColor: lv1BaseColor, + }, }); }); } + + lv2Node.children.push(lv3Node); }); } // lv2 直接包含的概念 - if (displayLevel === 'all' && lv2.concepts) { - lv2.concepts.slice(0, 5).forEach((conceptName) => { - const conceptId = `concept_${conceptName}`; + if (lv2.concepts) { + lv2.concepts.forEach((conceptName) => { const conceptPrice = leafMap[conceptName] || {}; - const conceptColor = getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor); - - if (!nodes.find(n => n.id === conceptId)) { - nodes.push({ - id: conceptId, - label: conceptName, - data: { - level: 'concept', - parentLv1: lv1.name, - parentLv2: lv2.name, - changePct: conceptPrice.avg_change_pct, - stockCount: conceptPrice.stock_count, - baseColor: lv1BaseColor, - }, - fill: conceptColor, - size: 12, - }); - } - - edges.push({ - id: `${lv2Id}-${conceptId}`, - source: lv2Id, - target: conceptId, - size: 0.5, + lv2Node.children.push({ + name: conceptName, + value: conceptPrice.stock_count || 10, + itemStyle: { + color: getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor), + }, + data: { + level: 'concept', + parentLv1: lv1.name, + parentLv2: lv2.name, + changePct: conceptPrice.avg_change_pct, + stockCount: conceptPrice.stock_count, + baseColor: lv1BaseColor, + }, }); }); } + + lv1Node.children.push(lv2Node); }); } - }); - return { nodes, edges }; - }, [hierarchy, priceData, displayLevel]); + return lv1Node; + }; - // 节点点击处理 - const handleNodeClick = useCallback((node) => { - setSelectedNode(node); + return hierarchy.map(buildChildren); + }, [hierarchy, priceData]); - if (node.data?.level === 'concept') { - const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(node.label)}.html`; - window.open(htmlPath, '_blank'); - } + // ECharts 配置 + const chartOption = useMemo(() => { + return { + backgroundColor: 'transparent', + tooltip: { + trigger: 'item', + backgroundColor: 'rgba(15, 23, 42, 0.95)', + borderColor: 'rgba(139, 92, 246, 0.5)', + borderWidth: 1, + borderRadius: 12, + padding: [12, 16], + textStyle: { + color: '#E2E8F0', + fontSize: 13, + }, + formatter: (params) => { + const data = params.data?.data || {}; + const levelMap = { + 'lv1': '一级分类', + 'lv2': '二级分类', + 'lv3': '三级分类', + 'concept': '概念', + }; - logger.info('ForceGraphView', '节点点击', { level: node.data?.level, name: node.label }); - }, []); + const changePct = data.changePct; + const changeColor = changePct > 0 ? '#F87171' : changePct < 0 ? '#4ADE80' : '#94A3B8'; + const changeIcon = changePct > 0 ? '▲' : changePct < 0 ? '▼' : '●'; + + return ` +
+
+ ${levelMap[data.level] || '分类'} +
+
+ ${params.name} +
+ ${changePct !== undefined && changePct !== null ? ` +
+ ${changeIcon} + + ${formatChangePercent(changePct)} + +
+ ` : ''} +
+ ${data.stockCount ? `${data.stockCount} 只股票` : ''} + ${data.conceptCount ? ` · ${data.conceptCount} 个概念` : ''} +
+ ${data.level === 'concept' ? ` +
+ 点击查看详情 → +
+ ` : ` +
+ 点击钻取到下级 +
+ `} +
+ `; + }, + }, + series: [ + { + type: 'sunburst', + data: sunburstData, + radius: ['12%', '95%'], + center: ['50%', '50%'], + sort: 'desc', + emphasis: { + focus: 'ancestor', + itemStyle: { + shadowBlur: 20, + shadowColor: 'rgba(139, 92, 246, 0.5)', + }, + }, + levels: [ + {}, + // 一级 - 最内圈 + { + r0: '12%', + r: '35%', + itemStyle: { + borderWidth: 3, + borderColor: '#0F172A', + borderRadius: 4, + }, + label: { + show: true, + rotate: 'tangential', + fontSize: 12, + fontWeight: 'bold', + color: '#FFFFFF', + textShadowColor: '#000', + textShadowBlur: 4, + }, + }, + // 二级 + { + r0: '35%', + r: '58%', + itemStyle: { + borderWidth: 2, + borderColor: '#0F172A', + borderRadius: 3, + }, + label: { + show: true, + rotate: 'tangential', + fontSize: 10, + color: '#F1F5F9', + textShadowColor: '#000', + textShadowBlur: 3, + }, + }, + // 三级 + { + r0: '58%', + r: '78%', + itemStyle: { + borderWidth: 1, + borderColor: '#0F172A', + borderRadius: 2, + }, + label: { + show: true, + rotate: 'radial', + fontSize: 9, + color: '#E2E8F0', + align: 'center', + }, + }, + // 概念 - 最外圈 + { + r0: '78%', + r: '95%', + itemStyle: { + borderWidth: 1, + borderColor: '#1E293B', + borderRadius: 1, + }, + label: { + show: false, // 概念层太多,隐藏标签 + position: 'outside', + fontSize: 8, + color: '#94A3B8', + }, + }, + ], + itemStyle: { + borderRadius: 4, + }, + animation: true, + animationDuration: 800, + animationEasing: 'cubicOut', + }, + ], + }; + }, [sunburstData]); + + // 图表事件 + const onChartEvents = useMemo(() => ({ + click: (params) => { + const data = params.data?.data || {}; + + if (data.level === 'concept') { + // 跳转到概念详情页 + const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(params.name)}.html`; + window.open(htmlPath, '_blank'); + } + + logger.info('SunburstView', '点击', { level: data.level, name: params.name }); + }, + mouseover: (params) => { + setHoveredItem({ + name: params.name, + data: params.data?.data, + }); + }, + mouseout: () => { + setHoveredItem(null); + }, + }), []); // 刷新数据 const handleRefresh = useCallback(() => { @@ -479,7 +546,7 @@ const ForceGraphView = ({
- 正在构建概念关系图... + 正在构建旭日图...
); @@ -522,9 +589,8 @@ const ForceGraphView = ({ left={0} right={0} bottom={0} - opacity={0.1} - bgImage="radial-gradient(circle at 20% 30%, rgba(139, 92, 246, 0.3) 0%, transparent 50%), - radial-gradient(circle at 80% 70%, rgba(59, 130, 246, 0.3) 0%, transparent 50%)" + opacity={0.15} + bgImage="radial-gradient(circle at 50% 50%, rgba(139, 92, 246, 0.3) 0%, transparent 60%)" pointerEvents="none" /> @@ -539,7 +605,7 @@ const ForceGraphView = ({ zIndex={10} pointerEvents="none" > - {/* 左侧标题和信息 */} + {/* 左侧标题 */} - + - 概念关系图 + 概念旭日图 - {graphData.nodes.length} 节点 + {hierarchy.length} 板块 - {/* 选中节点信息卡片 */} - {selectedNode && selectedNode.data?.level !== 'root' && ( + {/* 悬停信息 */} + {hoveredItem && hoveredItem.data?.level !== 'root' && ( - {selectedNode.data?.level === 'lv1' ? '一级分类' : - selectedNode.data?.level === 'lv2' ? '二级分类' : - selectedNode.data?.level === 'lv3' ? '三级分类' : '概念'} + {hoveredItem.data?.level === 'lv1' ? '一级分类' : + hoveredItem.data?.level === 'lv2' ? '二级分类' : + hoveredItem.data?.level === 'lv3' ? '三级分类' : '概念'} - {selectedNode.label} + {hoveredItem.name} - {/* 涨跌幅显示 */} - {selectedNode.data?.changePct !== undefined && selectedNode.data?.changePct !== null && ( + {hoveredItem.data?.changePct !== undefined && hoveredItem.data?.changePct !== null && ( 0 ? 'rgba(239, 68, 68, 0.2)' : selectedNode.data.changePct < 0 ? 'rgba(34, 197, 94, 0.2)' : 'rgba(100, 116, 139, 0.2)'} + bg={hoveredItem.data.changePct > 0 ? 'rgba(248, 113, 113, 0.2)' : hoveredItem.data.changePct < 0 ? 'rgba(74, 222, 128, 0.2)' : 'rgba(148, 163, 184, 0.2)'} px={3} py={1} borderRadius="full" border="1px solid" - borderColor={selectedNode.data.changePct > 0 ? 'red.400' : selectedNode.data.changePct < 0 ? 'green.400' : 'gray.500'} + borderColor={hoveredItem.data.changePct > 0 ? 'red.400' : hoveredItem.data.changePct < 0 ? 'green.400' : 'gray.500'} > 0 ? FaArrowUp : selectedNode.data.changePct < 0 ? FaArrowDown : FaCircle} - color={selectedNode.data.changePct > 0 ? 'red.400' : selectedNode.data.changePct < 0 ? 'green.400' : 'gray.400'} + as={hoveredItem.data.changePct > 0 ? FaArrowUp : hoveredItem.data.changePct < 0 ? FaArrowDown : FaCircle} + color={hoveredItem.data.changePct > 0 ? 'red.400' : hoveredItem.data.changePct < 0 ? 'green.400' : 'gray.400'} boxSize={3} /> 0 ? 'red.300' : selectedNode.data.changePct < 0 ? 'green.300' : 'gray.300'} + color={hoveredItem.data.changePct > 0 ? 'red.300' : hoveredItem.data.changePct < 0 ? 'green.300' : 'gray.300'} fontWeight="bold" fontSize="lg" fontFamily="mono" > - {formatChangePercent(selectedNode.data.changePct)} + {formatChangePercent(hoveredItem.data.changePct)} )} - {selectedNode.data?.stockCount && ( - {selectedNode.data.stockCount} 只股票 + {hoveredItem.data?.stockCount && ( + {hoveredItem.data.stockCount} 只股票 )} - {selectedNode.data?.conceptCount && ( - {selectedNode.data.conceptCount} 个概念 + {hoveredItem.data?.conceptCount && ( + {hoveredItem.data.conceptCount} 个概念 )} - - {selectedNode.data?.level === 'concept' && ( - - 点击查看详情 → - - )} )} {/* 右侧控制按钮 */} - - - {priceLoading && } + + {priceLoading && } - - } - onClick={handleRefresh} - isLoading={priceLoading} - bg="rgba(0, 0, 0, 0.7)" - color="white" - border="1px solid" - borderColor="whiteAlpha.200" - _hover={{ bg: 'whiteAlpha.200', borderColor: 'purple.400' }} - aria-label="刷新" - /> - - - - : } - onClick={onSettingsToggle} - bg={isSettingsOpen ? 'purple.500' : 'rgba(0, 0, 0, 0.7)'} - color="white" - border="1px solid" - borderColor={isSettingsOpen ? 'purple.400' : 'whiteAlpha.200'} - _hover={{ bg: isSettingsOpen ? 'purple.400' : 'whiteAlpha.200' }} - aria-label="设置" - /> - - - - : } - onClick={toggleFullscreen} - bg="rgba(0, 0, 0, 0.7)" - color="white" - border="1px solid" - borderColor="whiteAlpha.200" - _hover={{ bg: 'whiteAlpha.200', borderColor: 'purple.400' }} - aria-label={isFullscreen ? '退出全屏' : '全屏'} - /> - - - - {/* 设置面板 */} - - + } + onClick={handleRefresh} + isLoading={priceLoading} + bg="rgba(0, 0, 0, 0.7)" + color="white" border="1px solid" borderColor="whiteAlpha.200" - w="220px" - boxShadow="0 4px 20px rgba(0, 0, 0, 0.4)" - > - - - - 布局方式 - - - + _hover={{ bg: 'whiteAlpha.200', borderColor: 'purple.400' }} + aria-label="刷新" + /> + - - - 显示层级 - - - - - - - + + : } + onClick={toggleFullscreen} + bg="rgba(0, 0, 0, 0.7)" + color="white" + border="1px solid" + borderColor="whiteAlpha.200" + _hover={{ bg: 'whiteAlpha.200', borderColor: 'purple.400' }} + aria-label={isFullscreen ? '退出全屏' : '全屏'} + /> + + {/* 底部图例 */} @@ -821,25 +806,59 @@ const ForceGraphView = ({ borderColor="whiteAlpha.200" > - 滚轮缩放 · 拖拽平移 · 点击节点查看详情 + 悬停查看详情 · 点击概念跳转 - {/* 图表 */} - + + + + 一级 + + + + + 二级 + + + + + 三级 + + + + + 概念 + + + + + {/* ECharts 图表 */} + );