update pay ui
This commit is contained in:
@@ -1,20 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* ForceGraphView - 概念层级关系图(使用 reagraph)
|
* SunburstView - 概念层级旭日图
|
||||||
*
|
*
|
||||||
* 特性:
|
* 特性:
|
||||||
* 1. 清晰的层级布局(Radial/Hierarchical)
|
* 1. 同心圆环展示层级关系,从内到外:根 → 一级 → 二级 → 三级 → 概念
|
||||||
* 2. 节点大小根据股票数量动态调整
|
* 2. 涨红跌绿颜色映射
|
||||||
* 3. 涨红跌绿颜色映射
|
* 3. 点击扇区可钻取到子层级
|
||||||
* 4. 悬停显示涨跌幅详情
|
* 4. 悬停显示详细信息
|
||||||
* 5. 点击节点可聚焦或跳转
|
* 5. 支持返回上级
|
||||||
*/
|
*/
|
||||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||||
import {
|
import ReactECharts from 'echarts-for-react';
|
||||||
GraphCanvas,
|
|
||||||
useSelection,
|
|
||||||
lightTheme,
|
|
||||||
darkTheme,
|
|
||||||
} from 'reagraph';
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
VStack,
|
VStack,
|
||||||
@@ -29,29 +24,22 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Badge,
|
Badge,
|
||||||
useBreakpointValue,
|
useBreakpointValue,
|
||||||
Select,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
Collapse,
|
|
||||||
useDisclosure,
|
|
||||||
Divider,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import {
|
import {
|
||||||
FaLayerGroup,
|
FaLayerGroup,
|
||||||
FaSync,
|
FaSync,
|
||||||
FaExpand,
|
FaExpand,
|
||||||
FaCompress,
|
FaCompress,
|
||||||
FaCog,
|
|
||||||
FaHome,
|
FaHome,
|
||||||
FaChevronUp,
|
|
||||||
FaArrowUp,
|
FaArrowUp,
|
||||||
FaArrowDown,
|
FaArrowDown,
|
||||||
FaProjectDiagram,
|
|
||||||
FaCircle,
|
FaCircle,
|
||||||
|
FaChartPie,
|
||||||
|
FaUndo,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
|
|
||||||
// 一级分类颜色映射
|
// 一级分类颜色映射(基础色)
|
||||||
const LV1_COLORS = {
|
const LV1_COLORS = {
|
||||||
'人工智能': '#8B5CF6',
|
'人工智能': '#8B5CF6',
|
||||||
'半导体': '#3B82F6',
|
'半导体': '#3B82F6',
|
||||||
@@ -74,33 +62,23 @@ const LV1_COLORS = {
|
|||||||
const getChangeColor = (value, baseColor = '#64748B') => {
|
const getChangeColor = (value, baseColor = '#64748B') => {
|
||||||
if (value === null || value === undefined) return baseColor;
|
if (value === null || value === undefined) return baseColor;
|
||||||
|
|
||||||
// 涨 - 红色系(更鲜艳)
|
// 涨 - 红色系
|
||||||
if (value > 7) return '#DC2626';
|
if (value > 7) return '#DC2626';
|
||||||
if (value > 5) return '#EF4444';
|
if (value > 5) return '#EF4444';
|
||||||
if (value > 3) return '#F87171';
|
if (value > 3) return '#F87171';
|
||||||
if (value > 1) return '#FCA5A5';
|
if (value > 1) return '#FCA5A5';
|
||||||
if (value > 0) return '#FECACA';
|
if (value > 0) return '#FED7D7';
|
||||||
|
|
||||||
// 跌 - 绿色系
|
// 跌 - 绿色系
|
||||||
if (value < -7) return '#15803D';
|
if (value < -7) return '#15803D';
|
||||||
if (value < -5) return '#16A34A';
|
if (value < -5) return '#16A34A';
|
||||||
if (value < -3) return '#22C55E';
|
if (value < -3) return '#22C55E';
|
||||||
if (value < -1) return '#4ADE80';
|
if (value < -1) return '#4ADE80';
|
||||||
if (value < 0) return '#86EFAC';
|
if (value < 0) return '#BBF7D0';
|
||||||
|
|
||||||
return baseColor; // 平盘
|
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 返回的名称中提取纯名称
|
// 从 API 返回的名称中提取纯名称
|
||||||
const extractPureName = (apiName) => {
|
const extractPureName = (apiName) => {
|
||||||
if (!apiName) return '';
|
if (!apiName) return '';
|
||||||
@@ -114,50 +92,6 @@ const formatChangePercent = (value) => {
|
|||||||
return value > 0 ? `+${formatted}%` : value < 0 ? `-${formatted}%` : '0.00%';
|
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 [priceData, setPriceData] = useState({ lv1Map: {}, lv2Map: {}, lv3Map: {}, leafMap: {} });
|
||||||
const [priceLoading, setPriceLoading] = useState(false);
|
const [priceLoading, setPriceLoading] = useState(false);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const [layoutType, setLayoutType] = useState('radialOut2d'); // 'radialOut2d', 'treeTd2d', 'treeLr2d', 'forceDirected2d'
|
const [hoveredItem, setHoveredItem] = useState(null);
|
||||||
const [displayLevel, setDisplayLevel] = useState('lv2'); // 'lv1', 'lv2', 'lv3', 'all'
|
const [drillPath, setDrillPath] = useState([]); // 钻取路径
|
||||||
const [selectedNode, setSelectedNode] = useState(null);
|
|
||||||
|
|
||||||
const graphRef = useRef();
|
const chartRef = useRef();
|
||||||
const containerRef = useRef();
|
const containerRef = useRef();
|
||||||
const { isOpen: isSettingsOpen, onToggle: onSettingsToggle } = useDisclosure({ defaultIsOpen: false });
|
|
||||||
|
|
||||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||||
|
|
||||||
@@ -194,12 +126,12 @@ const ForceGraphView = ({
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setHierarchy(data.hierarchy || []);
|
setHierarchy(data.hierarchy || []);
|
||||||
|
|
||||||
logger.info('ForceGraphView', '层级结构加载完成', {
|
logger.info('SunburstView', '层级结构加载完成', {
|
||||||
totalLv1: data.hierarchy?.length,
|
totalLv1: data.hierarchy?.length,
|
||||||
totalConcepts: data.total_concepts
|
totalConcepts: data.total_concepts
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('ForceGraphView', 'fetchHierarchy', err);
|
logger.error('SunburstView', 'fetchHierarchy', err);
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -219,7 +151,7 @@ const ForceGraphView = ({
|
|||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
logger.warn('ForceGraphView', '获取层级涨跌幅失败', { status: response.status });
|
logger.warn('SunburstView', '获取层级涨跌幅失败', { status: response.status });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,12 +180,12 @@ const ForceGraphView = ({
|
|||||||
|
|
||||||
setPriceData({ lv1Map, lv2Map, lv3Map, leafMap });
|
setPriceData({ lv1Map, lv2Map, lv3Map, leafMap });
|
||||||
|
|
||||||
logger.info('ForceGraphView', '层级涨跌幅加载完成', {
|
logger.info('SunburstView', '层级涨跌幅加载完成', {
|
||||||
lv1Count: Object.keys(lv1Map).length,
|
lv1Count: Object.keys(lv1Map).length,
|
||||||
lv2Count: Object.keys(lv2Map).length,
|
lv2Count: Object.keys(lv2Map).length,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('ForceGraphView', '获取层级涨跌幅失败', { error: err.message });
|
logger.warn('SunburstView', '获取层级涨跌幅失败', { error: err.message });
|
||||||
} finally {
|
} finally {
|
||||||
setPriceLoading(false);
|
setPriceLoading(false);
|
||||||
}
|
}
|
||||||
@@ -269,31 +201,20 @@ const ForceGraphView = ({
|
|||||||
}
|
}
|
||||||
}, [hierarchy, fetchHierarchyPrice]);
|
}, [hierarchy, fetchHierarchyPrice]);
|
||||||
|
|
||||||
// 构建图数据
|
// 构建旭日图数据
|
||||||
const graphData = useMemo(() => {
|
const sunburstData = useMemo(() => {
|
||||||
const nodes = [];
|
|
||||||
const edges = [];
|
|
||||||
const { lv1Map, lv2Map, lv3Map, leafMap } = priceData;
|
const { lv1Map, lv2Map, lv3Map, leafMap } = priceData;
|
||||||
|
|
||||||
// 根节点
|
const buildChildren = (lv1) => {
|
||||||
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 lv1BaseColor = LV1_COLORS[lv1.name] || '#8B5CF6';
|
const lv1BaseColor = LV1_COLORS[lv1.name] || '#8B5CF6';
|
||||||
const lv1Color = getChangeColor(lv1Price.avg_change_pct, lv1BaseColor);
|
const lv1Price = lv1Map[lv1.name] || {};
|
||||||
|
|
||||||
// 一级节点
|
const lv1Node = {
|
||||||
nodes.push({
|
name: lv1.name,
|
||||||
id: lv1Id,
|
value: lv1Price.stock_count || lv1.concept_count * 10 || 100,
|
||||||
label: lv1.name,
|
itemStyle: {
|
||||||
|
color: getChangeColor(lv1Price.avg_change_pct, lv1BaseColor),
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
level: 'lv1',
|
level: 'lv1',
|
||||||
changePct: lv1Price.avg_change_pct,
|
changePct: lv1Price.avg_change_pct,
|
||||||
@@ -301,27 +222,20 @@ const ForceGraphView = ({
|
|||||||
conceptCount: lv1.concept_count,
|
conceptCount: lv1.concept_count,
|
||||||
baseColor: lv1BaseColor,
|
baseColor: lv1BaseColor,
|
||||||
},
|
},
|
||||||
fill: lv1Color,
|
children: [],
|
||||||
size: Math.max(35, Math.min(55, 35 + (lv1Price.stock_count || 0) / 50)),
|
};
|
||||||
});
|
|
||||||
|
|
||||||
edges.push({
|
// 二级
|
||||||
id: `root-${lv1Id}`,
|
if (lv1.children) {
|
||||||
source: 'root',
|
|
||||||
target: lv1Id,
|
|
||||||
size: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 二级节点
|
|
||||||
if (lv1.children && (displayLevel === 'lv2' || displayLevel === 'lv3' || displayLevel === 'all')) {
|
|
||||||
lv1.children.forEach((lv2) => {
|
lv1.children.forEach((lv2) => {
|
||||||
const lv2Id = `lv2_${lv1.name}_${lv2.name}`;
|
|
||||||
const lv2Price = lv2Map[lv2.name] || {};
|
const lv2Price = lv2Map[lv2.name] || {};
|
||||||
const lv2Color = getChangeColor(lv2Price.avg_change_pct, lv1BaseColor);
|
|
||||||
|
|
||||||
nodes.push({
|
const lv2Node = {
|
||||||
id: lv2Id,
|
name: lv2.name,
|
||||||
label: lv2.name,
|
value: lv2Price.stock_count || lv2.concept_count * 5 || 50,
|
||||||
|
itemStyle: {
|
||||||
|
color: getChangeColor(lv2Price.avg_change_pct, lv1BaseColor),
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
level: 'lv2',
|
level: 'lv2',
|
||||||
parentLv1: lv1.name,
|
parentLv1: lv1.name,
|
||||||
@@ -330,27 +244,20 @@ const ForceGraphView = ({
|
|||||||
conceptCount: lv2.concept_count,
|
conceptCount: lv2.concept_count,
|
||||||
baseColor: lv1BaseColor,
|
baseColor: lv1BaseColor,
|
||||||
},
|
},
|
||||||
fill: lv2Color,
|
children: [],
|
||||||
size: Math.max(25, Math.min(40, 25 + (lv2Price.stock_count || 0) / 80)),
|
};
|
||||||
});
|
|
||||||
|
|
||||||
edges.push({
|
// 三级
|
||||||
id: `${lv1Id}-${lv2Id}`,
|
if (lv2.children) {
|
||||||
source: lv1Id,
|
|
||||||
target: lv2Id,
|
|
||||||
size: 1.5,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 三级节点
|
|
||||||
if (lv2.children && (displayLevel === 'lv3' || displayLevel === 'all')) {
|
|
||||||
lv2.children.forEach((lv3) => {
|
lv2.children.forEach((lv3) => {
|
||||||
const lv3Id = `lv3_${lv1.name}_${lv2.name}_${lv3.name}`;
|
|
||||||
const lv3Price = lv3Map[lv3.name] || {};
|
const lv3Price = lv3Map[lv3.name] || {};
|
||||||
const lv3Color = getChangeColor(lv3Price.avg_change_pct, lv1BaseColor);
|
|
||||||
|
|
||||||
nodes.push({
|
const lv3Node = {
|
||||||
id: lv3Id,
|
name: lv3.name,
|
||||||
label: lv3.name,
|
value: lv3Price.stock_count || 30,
|
||||||
|
itemStyle: {
|
||||||
|
color: getChangeColor(lv3Price.avg_change_pct, lv1BaseColor),
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
level: 'lv3',
|
level: 'lv3',
|
||||||
parentLv1: lv1.name,
|
parentLv1: lv1.name,
|
||||||
@@ -359,29 +266,19 @@ const ForceGraphView = ({
|
|||||||
stockCount: lv3Price.stock_count,
|
stockCount: lv3Price.stock_count,
|
||||||
baseColor: lv1BaseColor,
|
baseColor: lv1BaseColor,
|
||||||
},
|
},
|
||||||
fill: lv3Color,
|
children: [],
|
||||||
size: Math.max(18, Math.min(30, 18 + (lv3Price.stock_count || 0) / 100)),
|
};
|
||||||
});
|
|
||||||
|
|
||||||
edges.push({
|
|
||||||
id: `${lv2Id}-${lv3Id}`,
|
|
||||||
source: lv2Id,
|
|
||||||
target: lv3Id,
|
|
||||||
size: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 叶子概念
|
// 叶子概念
|
||||||
if (displayLevel === 'all' && lv3.concepts) {
|
if (lv3.concepts) {
|
||||||
lv3.concepts.slice(0, 5).forEach((conceptName) => { // 限制每个lv3只显示5个概念
|
lv3.concepts.forEach((conceptName) => {
|
||||||
const conceptId = `concept_${conceptName}`;
|
|
||||||
const conceptPrice = leafMap[conceptName] || {};
|
const conceptPrice = leafMap[conceptName] || {};
|
||||||
const conceptColor = getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor);
|
lv3Node.children.push({
|
||||||
|
name: conceptName,
|
||||||
// 避免重复添加节点
|
value: conceptPrice.stock_count || 10,
|
||||||
if (!nodes.find(n => n.id === conceptId)) {
|
itemStyle: {
|
||||||
nodes.push({
|
color: getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor),
|
||||||
id: conceptId,
|
},
|
||||||
label: conceptName,
|
|
||||||
data: {
|
data: {
|
||||||
level: 'concept',
|
level: 'concept',
|
||||||
parentLv1: lv1.name,
|
parentLv1: lv1.name,
|
||||||
@@ -391,33 +288,24 @@ const ForceGraphView = ({
|
|||||||
stockCount: conceptPrice.stock_count,
|
stockCount: conceptPrice.stock_count,
|
||||||
baseColor: lv1BaseColor,
|
baseColor: lv1BaseColor,
|
||||||
},
|
},
|
||||||
fill: conceptColor,
|
});
|
||||||
size: 12,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
edges.push({
|
lv2Node.children.push(lv3Node);
|
||||||
id: `${lv3Id}-${conceptId}`,
|
|
||||||
source: lv3Id,
|
|
||||||
target: conceptId,
|
|
||||||
size: 0.5,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// lv2 直接包含的概念
|
// lv2 直接包含的概念
|
||||||
if (displayLevel === 'all' && lv2.concepts) {
|
if (lv2.concepts) {
|
||||||
lv2.concepts.slice(0, 5).forEach((conceptName) => {
|
lv2.concepts.forEach((conceptName) => {
|
||||||
const conceptId = `concept_${conceptName}`;
|
|
||||||
const conceptPrice = leafMap[conceptName] || {};
|
const conceptPrice = leafMap[conceptName] || {};
|
||||||
const conceptColor = getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor);
|
lv2Node.children.push({
|
||||||
|
name: conceptName,
|
||||||
if (!nodes.find(n => n.id === conceptId)) {
|
value: conceptPrice.stock_count || 10,
|
||||||
nodes.push({
|
itemStyle: {
|
||||||
id: conceptId,
|
color: getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor),
|
||||||
label: conceptName,
|
},
|
||||||
data: {
|
data: {
|
||||||
level: 'concept',
|
level: 'concept',
|
||||||
parentLv1: lv1.name,
|
parentLv1: lv1.name,
|
||||||
@@ -426,37 +314,216 @@ const ForceGraphView = ({
|
|||||||
stockCount: conceptPrice.stock_count,
|
stockCount: conceptPrice.stock_count,
|
||||||
baseColor: lv1BaseColor,
|
baseColor: lv1BaseColor,
|
||||||
},
|
},
|
||||||
fill: conceptColor,
|
|
||||||
size: 12,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
edges.push({
|
|
||||||
id: `${lv2Id}-${conceptId}`,
|
|
||||||
source: lv2Id,
|
|
||||||
target: conceptId,
|
|
||||||
size: 0.5,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lv1Node.children.push(lv2Node);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return { nodes, edges };
|
return lv1Node;
|
||||||
}, [hierarchy, priceData, displayLevel]);
|
};
|
||||||
|
|
||||||
// 节点点击处理
|
return hierarchy.map(buildChildren);
|
||||||
const handleNodeClick = useCallback((node) => {
|
}, [hierarchy, priceData]);
|
||||||
setSelectedNode(node);
|
|
||||||
|
|
||||||
if (node.data?.level === 'concept') {
|
// ECharts 配置
|
||||||
const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(node.label)}.html`;
|
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': '概念',
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePct = data.changePct;
|
||||||
|
const changeColor = changePct > 0 ? '#F87171' : changePct < 0 ? '#4ADE80' : '#94A3B8';
|
||||||
|
const changeIcon = changePct > 0 ? '▲' : changePct < 0 ? '▼' : '●';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="min-width: 160px;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||||||
|
<span style="
|
||||||
|
background: ${data.baseColor || '#8B5CF6'};
|
||||||
|
color: white;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
">${levelMap[data.level] || '分类'}</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 16px; font-weight: bold; margin-bottom: 8px; color: #FFFFFF;">
|
||||||
|
${params.name}
|
||||||
|
</div>
|
||||||
|
${changePct !== undefined && changePct !== null ? `
|
||||||
|
<div style="
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: ${changePct > 0 ? 'rgba(248, 113, 113, 0.2)' : changePct < 0 ? 'rgba(74, 222, 128, 0.2)' : 'rgba(148, 163, 184, 0.2)'};
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
">
|
||||||
|
<span style="color: ${changeColor}; font-size: 12px;">${changeIcon}</span>
|
||||||
|
<span style="color: ${changeColor}; font-weight: bold; font-size: 15px; font-family: monospace;">
|
||||||
|
${formatChangePercent(changePct)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div style="color: #94A3B8; font-size: 12px;">
|
||||||
|
${data.stockCount ? `${data.stockCount} 只股票` : ''}
|
||||||
|
${data.conceptCount ? ` · ${data.conceptCount} 个概念` : ''}
|
||||||
|
</div>
|
||||||
|
${data.level === 'concept' ? `
|
||||||
|
<div style="color: #A78BFA; font-size: 11px; margin-top: 6px;">
|
||||||
|
点击查看详情 →
|
||||||
|
</div>
|
||||||
|
` : `
|
||||||
|
<div style="color: #64748B; font-size: 11px; margin-top: 6px;">
|
||||||
|
点击钻取到下级
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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');
|
window.open(htmlPath, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('ForceGraphView', '节点点击', { level: node.data?.level, name: node.label });
|
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(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
@@ -479,7 +546,7 @@ const ForceGraphView = ({
|
|||||||
<Center h="500px" bg="rgba(15, 23, 42, 0.8)" borderRadius="2xl">
|
<Center h="500px" bg="rgba(15, 23, 42, 0.8)" borderRadius="2xl">
|
||||||
<VStack spacing={4}>
|
<VStack spacing={4}>
|
||||||
<Spinner size="xl" color="purple.400" thickness="4px" />
|
<Spinner size="xl" color="purple.400" thickness="4px" />
|
||||||
<Text color="gray.400">正在构建概念关系图...</Text>
|
<Text color="gray.400">正在构建旭日图...</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
@@ -522,9 +589,8 @@ const ForceGraphView = ({
|
|||||||
left={0}
|
left={0}
|
||||||
right={0}
|
right={0}
|
||||||
bottom={0}
|
bottom={0}
|
||||||
opacity={0.1}
|
opacity={0.15}
|
||||||
bgImage="radial-gradient(circle at 20% 30%, rgba(139, 92, 246, 0.3) 0%, transparent 50%),
|
bgImage="radial-gradient(circle at 50% 50%, rgba(139, 92, 246, 0.3) 0%, transparent 60%)"
|
||||||
radial-gradient(circle at 80% 70%, rgba(59, 130, 246, 0.3) 0%, transparent 50%)"
|
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -539,7 +605,7 @@ const ForceGraphView = ({
|
|||||||
zIndex={10}
|
zIndex={10}
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
>
|
>
|
||||||
{/* 左侧标题和信息 */}
|
{/* 左侧标题 */}
|
||||||
<VStack align="start" spacing={3} pointerEvents="auto">
|
<VStack align="start" spacing={3} pointerEvents="auto">
|
||||||
<HStack
|
<HStack
|
||||||
bg="rgba(0, 0, 0, 0.7)"
|
bg="rgba(0, 0, 0, 0.7)"
|
||||||
@@ -551,9 +617,9 @@ const ForceGraphView = ({
|
|||||||
borderColor="whiteAlpha.200"
|
borderColor="whiteAlpha.200"
|
||||||
boxShadow="0 4px 20px rgba(0, 0, 0, 0.3)"
|
boxShadow="0 4px 20px rgba(0, 0, 0, 0.3)"
|
||||||
>
|
>
|
||||||
<Icon as={FaProjectDiagram} color="purple.300" />
|
<Icon as={FaChartPie} color="purple.300" />
|
||||||
<Text color="white" fontWeight="bold" fontSize="sm">
|
<Text color="white" fontWeight="bold" fontSize="sm">
|
||||||
概念关系图
|
概念旭日图
|
||||||
</Text>
|
</Text>
|
||||||
<Badge
|
<Badge
|
||||||
bg="purple.500"
|
bg="purple.500"
|
||||||
@@ -562,12 +628,12 @@ const ForceGraphView = ({
|
|||||||
px={2}
|
px={2}
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
>
|
>
|
||||||
{graphData.nodes.length} 节点
|
{hierarchy.length} 板块
|
||||||
</Badge>
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{/* 选中节点信息卡片 */}
|
{/* 悬停信息 */}
|
||||||
{selectedNode && selectedNode.data?.level !== 'root' && (
|
{hoveredItem && hoveredItem.data?.level !== 'root' && (
|
||||||
<Box
|
<Box
|
||||||
bg="rgba(0, 0, 0, 0.85)"
|
bg="rgba(0, 0, 0, 0.85)"
|
||||||
backdropFilter="blur(12px)"
|
backdropFilter="blur(12px)"
|
||||||
@@ -575,77 +641,69 @@ const ForceGraphView = ({
|
|||||||
py={3}
|
py={3}
|
||||||
borderRadius="xl"
|
borderRadius="xl"
|
||||||
border="1px solid"
|
border="1px solid"
|
||||||
borderColor={selectedNode.fill || 'purple.500'}
|
borderColor={hoveredItem.data?.baseColor || 'purple.500'}
|
||||||
boxShadow={`0 4px 20px ${getGlowColor(selectedNode.data?.changePct)}`}
|
boxShadow="0 4px 20px rgba(0, 0, 0, 0.4)"
|
||||||
maxW="280px"
|
maxW="280px"
|
||||||
>
|
>
|
||||||
<VStack align="start" spacing={2}>
|
<VStack align="start" spacing={2}>
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
<Badge
|
<Badge
|
||||||
bg={selectedNode.data?.baseColor || 'purple.500'}
|
bg={hoveredItem.data?.baseColor || 'purple.500'}
|
||||||
color="white"
|
color="white"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
px={2}
|
px={2}
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
>
|
>
|
||||||
{selectedNode.data?.level === 'lv1' ? '一级分类' :
|
{hoveredItem.data?.level === 'lv1' ? '一级分类' :
|
||||||
selectedNode.data?.level === 'lv2' ? '二级分类' :
|
hoveredItem.data?.level === 'lv2' ? '二级分类' :
|
||||||
selectedNode.data?.level === 'lv3' ? '三级分类' : '概念'}
|
hoveredItem.data?.level === 'lv3' ? '三级分类' : '概念'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<Text color="white" fontWeight="bold" fontSize="md">
|
<Text color="white" fontWeight="bold" fontSize="md">
|
||||||
{selectedNode.label}
|
{hoveredItem.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* 涨跌幅显示 */}
|
{hoveredItem.data?.changePct !== undefined && hoveredItem.data?.changePct !== null && (
|
||||||
{selectedNode.data?.changePct !== undefined && selectedNode.data?.changePct !== null && (
|
|
||||||
<HStack
|
<HStack
|
||||||
bg={selectedNode.data.changePct > 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}
|
px={3}
|
||||||
py={1}
|
py={1}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
border="1px solid"
|
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'}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
as={selectedNode.data.changePct > 0 ? FaArrowUp : selectedNode.data.changePct < 0 ? FaArrowDown : FaCircle}
|
as={hoveredItem.data.changePct > 0 ? FaArrowUp : hoveredItem.data.changePct < 0 ? FaArrowDown : FaCircle}
|
||||||
color={selectedNode.data.changePct > 0 ? 'red.400' : selectedNode.data.changePct < 0 ? 'green.400' : 'gray.400'}
|
color={hoveredItem.data.changePct > 0 ? 'red.400' : hoveredItem.data.changePct < 0 ? 'green.400' : 'gray.400'}
|
||||||
boxSize={3}
|
boxSize={3}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
color={selectedNode.data.changePct > 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"
|
fontWeight="bold"
|
||||||
fontSize="lg"
|
fontSize="lg"
|
||||||
fontFamily="mono"
|
fontFamily="mono"
|
||||||
>
|
>
|
||||||
{formatChangePercent(selectedNode.data.changePct)}
|
{formatChangePercent(hoveredItem.data.changePct)}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<HStack spacing={4} color="whiteAlpha.700" fontSize="xs">
|
<HStack spacing={4} color="whiteAlpha.700" fontSize="xs">
|
||||||
{selectedNode.data?.stockCount && (
|
{hoveredItem.data?.stockCount && (
|
||||||
<Text>{selectedNode.data.stockCount} 只股票</Text>
|
<Text>{hoveredItem.data.stockCount} 只股票</Text>
|
||||||
)}
|
)}
|
||||||
{selectedNode.data?.conceptCount && (
|
{hoveredItem.data?.conceptCount && (
|
||||||
<Text>{selectedNode.data.conceptCount} 个概念</Text>
|
<Text>{hoveredItem.data.conceptCount} 个概念</Text>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{selectedNode.data?.level === 'concept' && (
|
|
||||||
<Text color="purple.300" fontSize="xs">
|
|
||||||
点击查看详情 →
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</VStack>
|
</VStack>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
{/* 右侧控制按钮 */}
|
{/* 右侧控制按钮 */}
|
||||||
<VStack spacing={2} pointerEvents="auto">
|
<HStack spacing={2} pointerEvents="auto">
|
||||||
<HStack spacing={2}>
|
|
||||||
{priceLoading && <Spinner size="sm" color="purple.300" />}
|
{priceLoading && <Spinner size="sm" color="purple.300" />}
|
||||||
|
|
||||||
<Tooltip label="刷新数据" placement="left">
|
<Tooltip label="刷新数据" placement="left">
|
||||||
@@ -663,20 +721,6 @@ const ForceGraphView = ({
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip label="设置" placement="left">
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
icon={isSettingsOpen ? <FaChevronUp /> : <FaCog />}
|
|
||||||
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="设置"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label={isFullscreen ? '退出全屏' : '全屏'} placement="left">
|
<Tooltip label={isFullscreen ? '退出全屏' : '全屏'} placement="left">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -691,65 +735,6 @@ const ForceGraphView = ({
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{/* 设置面板 */}
|
|
||||||
<Collapse in={isSettingsOpen}>
|
|
||||||
<Box
|
|
||||||
bg="rgba(0, 0, 0, 0.85)"
|
|
||||||
backdropFilter="blur(12px)"
|
|
||||||
p={4}
|
|
||||||
borderRadius="xl"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="whiteAlpha.200"
|
|
||||||
w="220px"
|
|
||||||
boxShadow="0 4px 20px rgba(0, 0, 0, 0.4)"
|
|
||||||
>
|
|
||||||
<VStack spacing={4} align="stretch">
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel color="whiteAlpha.800" fontSize="xs" mb={1}>
|
|
||||||
布局方式
|
|
||||||
</FormLabel>
|
|
||||||
<Select
|
|
||||||
value={layoutType}
|
|
||||||
onChange={(e) => setLayoutType(e.target.value)}
|
|
||||||
size="sm"
|
|
||||||
bg="whiteAlpha.100"
|
|
||||||
color="white"
|
|
||||||
borderColor="whiteAlpha.300"
|
|
||||||
_hover={{ borderColor: 'purple.400' }}
|
|
||||||
sx={{ option: { bg: '#1a1a2e', color: 'white' } }}
|
|
||||||
>
|
|
||||||
<option value="radialOut2d">放射状</option>
|
|
||||||
<option value="treeTd2d">树形(上下)</option>
|
|
||||||
<option value="treeLr2d">树形(左右)</option>
|
|
||||||
<option value="forceDirected2d">力导向</option>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel color="whiteAlpha.800" fontSize="xs" mb={1}>
|
|
||||||
显示层级
|
|
||||||
</FormLabel>
|
|
||||||
<Select
|
|
||||||
value={displayLevel}
|
|
||||||
onChange={(e) => setDisplayLevel(e.target.value)}
|
|
||||||
size="sm"
|
|
||||||
bg="whiteAlpha.100"
|
|
||||||
color="white"
|
|
||||||
borderColor="whiteAlpha.300"
|
|
||||||
_hover={{ borderColor: 'purple.400' }}
|
|
||||||
sx={{ option: { bg: '#1a1a2e', color: 'white' } }}
|
|
||||||
>
|
|
||||||
<option value="lv1">一级分类</option>
|
|
||||||
<option value="lv2">到二级</option>
|
|
||||||
<option value="lv3">到三级</option>
|
|
||||||
<option value="all">全部(含概念)</option>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
</Collapse>
|
|
||||||
</VStack>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* 底部图例 */}
|
{/* 底部图例 */}
|
||||||
@@ -821,25 +806,59 @@ const ForceGraphView = ({
|
|||||||
borderColor="whiteAlpha.200"
|
borderColor="whiteAlpha.200"
|
||||||
>
|
>
|
||||||
<Text color="whiteAlpha.600" fontSize="xs">
|
<Text color="whiteAlpha.600" fontSize="xs">
|
||||||
滚轮缩放 · 拖拽平移 · 点击节点查看详情
|
悬停查看详情 · 点击概念跳转
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 图表 */}
|
{/* 层级说明 */}
|
||||||
<GraphCanvas
|
<Box
|
||||||
ref={graphRef}
|
position="absolute"
|
||||||
nodes={graphData.nodes}
|
bottom={4}
|
||||||
edges={graphData.edges}
|
left="50%"
|
||||||
theme={customDarkTheme}
|
transform="translateX(-50%)"
|
||||||
layoutType={layoutType}
|
zIndex={10}
|
||||||
labelType="all"
|
pointerEvents="none"
|
||||||
edgeArrowPosition="none"
|
>
|
||||||
draggable
|
<HStack
|
||||||
onNodeClick={handleNodeClick}
|
bg="rgba(0, 0, 0, 0.7)"
|
||||||
contextMenu={null}
|
backdropFilter="blur(10px)"
|
||||||
cameraMode="pan"
|
px={4}
|
||||||
sizingType="centrality"
|
py={2}
|
||||||
|
borderRadius="full"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="whiteAlpha.200"
|
||||||
|
spacing={3}
|
||||||
|
>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box w={2} h={2} borderRadius="full" bg="purple.400" />
|
||||||
|
<Text color="whiteAlpha.700" fontSize="xs">一级</Text>
|
||||||
|
</HStack>
|
||||||
|
<Text color="whiteAlpha.400">→</Text>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box w={2} h={2} borderRadius="full" bg="blue.400" />
|
||||||
|
<Text color="whiteAlpha.700" fontSize="xs">二级</Text>
|
||||||
|
</HStack>
|
||||||
|
<Text color="whiteAlpha.400">→</Text>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box w={2} h={2} borderRadius="full" bg="cyan.400" />
|
||||||
|
<Text color="whiteAlpha.700" fontSize="xs">三级</Text>
|
||||||
|
</HStack>
|
||||||
|
<Text color="whiteAlpha.400">→</Text>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Box w={2} h={2} borderRadius="full" bg="gray.400" />
|
||||||
|
<Text color="whiteAlpha.700" fontSize="xs">概念</Text>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* ECharts 图表 */}
|
||||||
|
<ReactECharts
|
||||||
|
ref={chartRef}
|
||||||
|
option={chartOption}
|
||||||
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
onEvents={onChartEvents}
|
||||||
|
opts={{ renderer: 'canvas' }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user