update pay ui
This commit is contained in:
@@ -1,12 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* SunburstView - 概念层级旭日图
|
* SunburstView - 概念层级旭日图(支持钻取)
|
||||||
*
|
*
|
||||||
* 特性:
|
* 特性:
|
||||||
* 1. 同心圆环展示层级关系,从内到外:根 → 一级 → 二级 → 三级 → 概念
|
* 1. 默认显示3层(一级 → 二级 → 三级)
|
||||||
* 2. 涨红跌绿颜色映射
|
* 2. 点击扇区钻取进入,显示该分类下的子级
|
||||||
* 3. 点击扇区可钻取到子层级
|
* 3. 支持返回上级
|
||||||
* 4. 悬停显示详细信息
|
* 4. 涨红跌绿颜色映射
|
||||||
* 5. 支持返回上级
|
|
||||||
*/
|
*/
|
||||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||||
import ReactECharts from 'echarts-for-react';
|
import ReactECharts from 'echarts-for-react';
|
||||||
@@ -24,6 +23,9 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Badge,
|
Badge,
|
||||||
useBreakpointValue,
|
useBreakpointValue,
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import {
|
import {
|
||||||
FaLayerGroup,
|
FaLayerGroup,
|
||||||
@@ -36,7 +38,10 @@ import {
|
|||||||
FaCircle,
|
FaCircle,
|
||||||
FaChartPie,
|
FaChartPie,
|
||||||
FaUndo,
|
FaUndo,
|
||||||
|
FaChevronRight,
|
||||||
|
FaArrowLeft,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
|
import { ChevronRightIcon } from '@chakra-ui/icons';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
|
|
||||||
// 一级分类颜色映射(基础色)
|
// 一级分类颜色映射(基础色)
|
||||||
@@ -76,7 +81,7 @@ const getChangeColor = (value, baseColor = '#64748B') => {
|
|||||||
if (value < -1) return '#4ADE80';
|
if (value < -1) return '#4ADE80';
|
||||||
if (value < 0) return '#BBF7D0';
|
if (value < 0) return '#BBF7D0';
|
||||||
|
|
||||||
return baseColor; // 平盘
|
return baseColor;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 从 API 返回的名称中提取纯名称
|
// 从 API 返回的名称中提取纯名称
|
||||||
@@ -107,7 +112,13 @@ const ForceGraphView = ({
|
|||||||
const [priceLoading, setPriceLoading] = useState(false);
|
const [priceLoading, setPriceLoading] = useState(false);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const [hoveredItem, setHoveredItem] = useState(null);
|
const [hoveredItem, setHoveredItem] = useState(null);
|
||||||
const [drillPath, setDrillPath] = useState([]); // 钻取路径
|
|
||||||
|
// 钻取状态:记录当前查看的路径
|
||||||
|
// null = 根视图(显示所有一级)
|
||||||
|
// { lv1: '人工智能' } = 查看人工智能下的二级和三级
|
||||||
|
// { lv1: '人工智能', lv2: 'AI基础设施' } = 查看AI基础设施下的三级和概念
|
||||||
|
// { lv1: '人工智能', lv2: 'AI基础设施', lv3: 'AI芯片' } = 查看AI芯片下的概念
|
||||||
|
const [drillPath, setDrillPath] = useState(null);
|
||||||
|
|
||||||
const chartRef = useRef();
|
const chartRef = useRef();
|
||||||
const containerRef = useRef();
|
const containerRef = useRef();
|
||||||
@@ -179,11 +190,6 @@ const ForceGraphView = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setPriceData({ lv1Map, lv2Map, lv3Map, leafMap });
|
setPriceData({ lv1Map, lv2Map, lv3Map, leafMap });
|
||||||
|
|
||||||
logger.info('SunburstView', '层级涨跌幅加载完成', {
|
|
||||||
lv1Count: Object.keys(lv1Map).length,
|
|
||||||
lv2Count: Object.keys(lv2Map).length,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('SunburstView', '获取层级涨跌幅失败', { error: err.message });
|
logger.warn('SunburstView', '获取层级涨跌幅失败', { error: err.message });
|
||||||
} finally {
|
} finally {
|
||||||
@@ -201,11 +207,13 @@ const ForceGraphView = ({
|
|||||||
}
|
}
|
||||||
}, [hierarchy, fetchHierarchyPrice]);
|
}, [hierarchy, fetchHierarchyPrice]);
|
||||||
|
|
||||||
// 构建旭日图数据
|
// 根据钻取路径构建旭日图数据
|
||||||
const sunburstData = useMemo(() => {
|
const sunburstData = useMemo(() => {
|
||||||
const { lv1Map, lv2Map, lv3Map, leafMap } = priceData;
|
const { lv1Map, lv2Map, lv3Map, leafMap } = priceData;
|
||||||
|
|
||||||
const buildChildren = (lv1) => {
|
// 根视图:显示所有一级,每个一级下显示二级和三级(不显示概念)
|
||||||
|
if (!drillPath) {
|
||||||
|
return hierarchy.map((lv1) => {
|
||||||
const lv1BaseColor = LV1_COLORS[lv1.name] || '#8B5CF6';
|
const lv1BaseColor = LV1_COLORS[lv1.name] || '#8B5CF6';
|
||||||
const lv1Price = lv1Map[lv1.name] || {};
|
const lv1Price = lv1Map[lv1.name] || {};
|
||||||
|
|
||||||
@@ -247,6 +255,66 @@ const ForceGraphView = ({
|
|||||||
children: [],
|
children: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 三级(不显示概念)
|
||||||
|
if (lv2.children) {
|
||||||
|
lv2.children.forEach((lv3) => {
|
||||||
|
const lv3Price = lv3Map[lv3.name] || {};
|
||||||
|
|
||||||
|
lv2Node.children.push({
|
||||||
|
name: lv3.name,
|
||||||
|
value: lv3Price.stock_count || 30,
|
||||||
|
itemStyle: {
|
||||||
|
color: getChangeColor(lv3Price.avg_change_pct, lv1BaseColor),
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
level: 'lv3',
|
||||||
|
parentLv1: lv1.name,
|
||||||
|
parentLv2: lv2.name,
|
||||||
|
changePct: lv3Price.avg_change_pct,
|
||||||
|
stockCount: lv3Price.stock_count,
|
||||||
|
conceptCount: lv3.concepts?.length || 0,
|
||||||
|
baseColor: lv1BaseColor,
|
||||||
|
hasChildren: lv3.concepts && lv3.concepts.length > 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
lv1Node.children.push(lv2Node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return lv1Node;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 钻取到某个一级分类:显示该一级下的二级、三级、概念
|
||||||
|
if (drillPath.lv1 && !drillPath.lv2) {
|
||||||
|
const lv1 = hierarchy.find(h => h.name === drillPath.lv1);
|
||||||
|
if (!lv1) return [];
|
||||||
|
|
||||||
|
const lv1BaseColor = LV1_COLORS[lv1.name] || '#8B5CF6';
|
||||||
|
|
||||||
|
return (lv1.children || []).map((lv2) => {
|
||||||
|
const lv2Price = lv2Map[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,
|
||||||
|
changePct: lv2Price.avg_change_pct,
|
||||||
|
stockCount: lv2Price.stock_count,
|
||||||
|
conceptCount: lv2.concept_count,
|
||||||
|
baseColor: lv1BaseColor,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
|
||||||
// 三级
|
// 三级
|
||||||
if (lv2.children) {
|
if (lv2.children) {
|
||||||
lv2.children.forEach((lv3) => {
|
lv2.children.forEach((lv3) => {
|
||||||
@@ -269,7 +337,7 @@ const ForceGraphView = ({
|
|||||||
children: [],
|
children: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 叶子概念
|
// 概念
|
||||||
if (lv3.concepts) {
|
if (lv3.concepts) {
|
||||||
lv3.concepts.forEach((conceptName) => {
|
lv3.concepts.forEach((conceptName) => {
|
||||||
const conceptPrice = leafMap[conceptName] || {};
|
const conceptPrice = leafMap[conceptName] || {};
|
||||||
@@ -318,18 +386,184 @@ const ForceGraphView = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
lv1Node.children.push(lv2Node);
|
return lv2Node;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return lv1Node;
|
// 钻取到某个二级分类:显示该二级下的三级和概念
|
||||||
|
if (drillPath.lv1 && drillPath.lv2 && !drillPath.lv3) {
|
||||||
|
const lv1 = hierarchy.find(h => h.name === drillPath.lv1);
|
||||||
|
if (!lv1) return [];
|
||||||
|
|
||||||
|
const lv2 = lv1.children?.find(c => c.name === drillPath.lv2);
|
||||||
|
if (!lv2) return [];
|
||||||
|
|
||||||
|
const lv1BaseColor = LV1_COLORS[lv1.name] || '#8B5CF6';
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
// 三级
|
||||||
|
if (lv2.children) {
|
||||||
|
lv2.children.forEach((lv3) => {
|
||||||
|
const lv3Price = lv3Map[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,
|
||||||
|
parentLv2: lv2.name,
|
||||||
|
changePct: lv3Price.avg_change_pct,
|
||||||
|
stockCount: lv3Price.stock_count,
|
||||||
|
baseColor: lv1BaseColor,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return hierarchy.map(buildChildren);
|
// 概念
|
||||||
}, [hierarchy, priceData]);
|
if (lv3.concepts) {
|
||||||
|
lv3.concepts.forEach((conceptName) => {
|
||||||
|
const conceptPrice = leafMap[conceptName] || {};
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(lv3Node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// lv2 直接包含的概念
|
||||||
|
if (lv2.concepts) {
|
||||||
|
lv2.concepts.forEach((conceptName) => {
|
||||||
|
const conceptPrice = leafMap[conceptName] || {};
|
||||||
|
result.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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 钻取到某个三级分类:显示该三级下的概念
|
||||||
|
if (drillPath.lv1 && drillPath.lv2 && drillPath.lv3) {
|
||||||
|
const lv1 = hierarchy.find(h => h.name === drillPath.lv1);
|
||||||
|
if (!lv1) return [];
|
||||||
|
|
||||||
|
const lv2 = lv1.children?.find(c => c.name === drillPath.lv2);
|
||||||
|
if (!lv2) return [];
|
||||||
|
|
||||||
|
const lv3 = lv2.children?.find(c => c.name === drillPath.lv3);
|
||||||
|
if (!lv3) return [];
|
||||||
|
|
||||||
|
const lv1BaseColor = LV1_COLORS[lv1.name] || '#8B5CF6';
|
||||||
|
|
||||||
|
return (lv3.concepts || []).map((conceptName) => {
|
||||||
|
const conceptPrice = leafMap[conceptName] || {};
|
||||||
|
return {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}, [hierarchy, priceData, drillPath]);
|
||||||
|
|
||||||
|
// 获取当前显示的层级数
|
||||||
|
const currentLevels = useMemo(() => {
|
||||||
|
if (!drillPath) return 3; // 根视图:一级、二级、三级
|
||||||
|
if (drillPath.lv1 && !drillPath.lv2) return 3; // 一级钻取:二级、三级、概念
|
||||||
|
if (drillPath.lv1 && drillPath.lv2 && !drillPath.lv3) return 2; // 二级钻取:三级、概念
|
||||||
|
if (drillPath.lv1 && drillPath.lv2 && drillPath.lv3) return 1; // 三级钻取:概念
|
||||||
|
return 3;
|
||||||
|
}, [drillPath]);
|
||||||
|
|
||||||
// ECharts 配置
|
// ECharts 配置
|
||||||
const chartOption = useMemo(() => {
|
const chartOption = useMemo(() => {
|
||||||
|
// 根据当前层级数调整半径
|
||||||
|
const levelConfigs = {
|
||||||
|
1: [
|
||||||
|
{},
|
||||||
|
{ r0: '15%', r: '90%', label: { show: true, rotate: 'tangential', fontSize: 12, fontWeight: 'bold' } },
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
{},
|
||||||
|
{ r0: '15%', r: '50%', label: { show: true, rotate: 'tangential', fontSize: 12, fontWeight: 'bold' } },
|
||||||
|
{ r0: '50%', r: '90%', label: { show: true, rotate: 'radial', fontSize: 10 } },
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
{},
|
||||||
|
{ r0: '15%', r: '40%', label: { show: true, rotate: 'tangential', fontSize: 12, fontWeight: 'bold' } },
|
||||||
|
{ r0: '40%', r: '65%', label: { show: true, rotate: 'tangential', fontSize: 10 } },
|
||||||
|
{ r0: '65%', r: '90%', label: { show: true, rotate: 'radial', fontSize: 9 } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const levels = levelConfigs[currentLevels] || levelConfigs[3];
|
||||||
|
|
||||||
|
// 为每个层级添加样式
|
||||||
|
const styledLevels = levels.map((level, index) => {
|
||||||
|
if (index === 0) return level;
|
||||||
|
return {
|
||||||
|
...level,
|
||||||
|
itemStyle: {
|
||||||
|
borderWidth: index === 1 ? 3 : index === 2 ? 2 : 1,
|
||||||
|
borderColor: '#0F172A',
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
...level.label,
|
||||||
|
color: '#FFFFFF',
|
||||||
|
textShadowColor: '#000',
|
||||||
|
textShadowBlur: 4,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@@ -396,7 +630,7 @@ const ForceGraphView = ({
|
|||||||
</div>
|
</div>
|
||||||
` : `
|
` : `
|
||||||
<div style="color: #64748B; font-size: 11px; margin-top: 6px;">
|
<div style="color: #64748B; font-size: 11px; margin-top: 6px;">
|
||||||
点击钻取到下级
|
点击进入查看详情
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
@@ -407,7 +641,7 @@ const ForceGraphView = ({
|
|||||||
{
|
{
|
||||||
type: 'sunburst',
|
type: 'sunburst',
|
||||||
data: sunburstData,
|
data: sunburstData,
|
||||||
radius: ['12%', '95%'],
|
radius: ['15%', '90%'],
|
||||||
center: ['50%', '50%'],
|
center: ['50%', '50%'],
|
||||||
sort: 'desc',
|
sort: 'desc',
|
||||||
emphasis: {
|
emphasis: {
|
||||||
@@ -417,89 +651,17 @@ const ForceGraphView = ({
|
|||||||
shadowColor: 'rgba(139, 92, 246, 0.5)',
|
shadowColor: 'rgba(139, 92, 246, 0.5)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
levels: [
|
levels: styledLevels,
|
||||||
{},
|
|
||||||
// 一级 - 最内圈
|
|
||||||
{
|
|
||||||
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: {
|
itemStyle: {
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
animation: true,
|
animation: true,
|
||||||
animationDuration: 800,
|
animationDuration: 600,
|
||||||
animationEasing: 'cubicOut',
|
animationEasing: 'cubicOut',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}, [sunburstData]);
|
}, [sunburstData, currentLevels]);
|
||||||
|
|
||||||
// 图表事件
|
// 图表事件
|
||||||
const onChartEvents = useMemo(() => ({
|
const onChartEvents = useMemo(() => ({
|
||||||
@@ -510,9 +672,19 @@ const ForceGraphView = ({
|
|||||||
// 跳转到概念详情页
|
// 跳转到概念详情页
|
||||||
const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(params.name)}.html`;
|
const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(params.name)}.html`;
|
||||||
window.open(htmlPath, '_blank');
|
window.open(htmlPath, '_blank');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('SunburstView', '点击', { level: data.level, name: params.name });
|
// 钻取进入
|
||||||
|
if (data.level === 'lv1') {
|
||||||
|
setDrillPath({ lv1: params.name });
|
||||||
|
} else if (data.level === 'lv2') {
|
||||||
|
setDrillPath({ lv1: data.parentLv1, lv2: params.name });
|
||||||
|
} else if (data.level === 'lv3') {
|
||||||
|
setDrillPath({ lv1: data.parentLv1, lv2: data.parentLv2, lv3: params.name });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('SunburstView', '钻取', { level: data.level, name: params.name, path: drillPath });
|
||||||
},
|
},
|
||||||
mouseover: (params) => {
|
mouseover: (params) => {
|
||||||
setHoveredItem({
|
setHoveredItem({
|
||||||
@@ -523,7 +695,25 @@ const ForceGraphView = ({
|
|||||||
mouseout: () => {
|
mouseout: () => {
|
||||||
setHoveredItem(null);
|
setHoveredItem(null);
|
||||||
},
|
},
|
||||||
}), []);
|
}), [drillPath]);
|
||||||
|
|
||||||
|
// 返回上一层
|
||||||
|
const handleGoBack = useCallback(() => {
|
||||||
|
if (!drillPath) return;
|
||||||
|
|
||||||
|
if (drillPath.lv3) {
|
||||||
|
setDrillPath({ lv1: drillPath.lv1, lv2: drillPath.lv2 });
|
||||||
|
} else if (drillPath.lv2) {
|
||||||
|
setDrillPath({ lv1: drillPath.lv1 });
|
||||||
|
} else if (drillPath.lv1) {
|
||||||
|
setDrillPath(null);
|
||||||
|
}
|
||||||
|
}, [drillPath]);
|
||||||
|
|
||||||
|
// 返回根视图
|
||||||
|
const handleGoHome = useCallback(() => {
|
||||||
|
setDrillPath(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 刷新数据
|
// 刷新数据
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
@@ -535,6 +725,23 @@ const ForceGraphView = ({
|
|||||||
setIsFullscreen(prev => !prev);
|
setIsFullscreen(prev => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 获取面包屑
|
||||||
|
const breadcrumbItems = useMemo(() => {
|
||||||
|
const items = [{ label: '全部分类', path: null }];
|
||||||
|
|
||||||
|
if (drillPath?.lv1) {
|
||||||
|
items.push({ label: drillPath.lv1, path: { lv1: drillPath.lv1 } });
|
||||||
|
}
|
||||||
|
if (drillPath?.lv2) {
|
||||||
|
items.push({ label: drillPath.lv2, path: { lv1: drillPath.lv1, lv2: drillPath.lv2 } });
|
||||||
|
}
|
||||||
|
if (drillPath?.lv3) {
|
||||||
|
items.push({ label: drillPath.lv3, path: { lv1: drillPath.lv1, lv2: drillPath.lv2, lv3: drillPath.lv3 } });
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}, [drillPath]);
|
||||||
|
|
||||||
// 获取容器高度
|
// 获取容器高度
|
||||||
const containerHeight = useMemo(() => {
|
const containerHeight = useMemo(() => {
|
||||||
if (isFullscreen) return '100vh';
|
if (isFullscreen) return '100vh';
|
||||||
@@ -605,8 +812,26 @@ 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 spacing={3}>
|
||||||
|
{/* 返回按钮 */}
|
||||||
|
{drillPath && (
|
||||||
|
<Tooltip label="返回上一层" placement="bottom">
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
icon={<FaArrowLeft />}
|
||||||
|
onClick={handleGoBack}
|
||||||
|
bg="rgba(0, 0, 0, 0.7)"
|
||||||
|
color="white"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="whiteAlpha.200"
|
||||||
|
_hover={{ bg: 'purple.500', borderColor: 'purple.400' }}
|
||||||
|
aria-label="返回"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
<HStack
|
<HStack
|
||||||
bg="rgba(0, 0, 0, 0.7)"
|
bg="rgba(0, 0, 0, 0.7)"
|
||||||
backdropFilter="blur(12px)"
|
backdropFilter="blur(12px)"
|
||||||
@@ -621,19 +846,45 @@ const ForceGraphView = ({
|
|||||||
<Text color="white" fontWeight="bold" fontSize="sm">
|
<Text color="white" fontWeight="bold" fontSize="sm">
|
||||||
概念旭日图
|
概念旭日图
|
||||||
</Text>
|
</Text>
|
||||||
<Badge
|
</HStack>
|
||||||
bg="purple.500"
|
</HStack>
|
||||||
color="white"
|
|
||||||
|
{/* 面包屑导航 */}
|
||||||
|
<HStack
|
||||||
|
bg="rgba(0, 0, 0, 0.7)"
|
||||||
|
backdropFilter="blur(12px)"
|
||||||
|
px={4}
|
||||||
|
py={2}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
px={2}
|
border="1px solid"
|
||||||
fontSize="xs"
|
borderColor="whiteAlpha.200"
|
||||||
|
spacing={2}
|
||||||
>
|
>
|
||||||
{hierarchy.length} 板块
|
{breadcrumbItems.map((item, index) => (
|
||||||
</Badge>
|
<HStack key={index} spacing={2}>
|
||||||
|
{index > 0 && (
|
||||||
|
<Icon as={FaChevronRight} 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' } : {}}
|
||||||
|
onClick={() => {
|
||||||
|
if (index < breadcrumbItems.length - 1) {
|
||||||
|
setDrillPath(item.path);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
))}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{/* 悬停信息 */}
|
{/* 悬停信息 */}
|
||||||
{hoveredItem && hoveredItem.data?.level !== 'root' && (
|
{hoveredItem && hoveredItem.data?.level && (
|
||||||
<Box
|
<Box
|
||||||
bg="rgba(0, 0, 0, 0.85)"
|
bg="rgba(0, 0, 0, 0.85)"
|
||||||
backdropFilter="blur(12px)"
|
backdropFilter="blur(12px)"
|
||||||
@@ -706,6 +957,22 @@ const ForceGraphView = ({
|
|||||||
<HStack spacing={2} pointerEvents="auto">
|
<HStack spacing={2} pointerEvents="auto">
|
||||||
{priceLoading && <Spinner size="sm" color="purple.300" />}
|
{priceLoading && <Spinner size="sm" color="purple.300" />}
|
||||||
|
|
||||||
|
{drillPath && (
|
||||||
|
<Tooltip label="返回全部" placement="left">
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
icon={<FaHome />}
|
||||||
|
onClick={handleGoHome}
|
||||||
|
bg="rgba(0, 0, 0, 0.7)"
|
||||||
|
color="white"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="whiteAlpha.200"
|
||||||
|
_hover={{ bg: 'purple.500', borderColor: 'purple.400' }}
|
||||||
|
aria-label="返回全部"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
<Tooltip label="刷新数据" placement="left">
|
<Tooltip label="刷新数据" placement="left">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -773,19 +1040,6 @@ const ForceGraphView = ({
|
|||||||
<Box w={3} h={3} borderRadius="full" bg="#22C55E" boxShadow="0 0 8px rgba(34, 197, 94, 0.5)" />
|
<Box w={3} h={3} borderRadius="full" bg="#22C55E" boxShadow="0 0 8px rgba(34, 197, 94, 0.5)" />
|
||||||
<Text color="whiteAlpha.800" fontSize="xs">跌</Text>
|
<Text color="whiteAlpha.800" fontSize="xs">跌</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack
|
|
||||||
bg="rgba(0, 0, 0, 0.7)"
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
px={3}
|
|
||||||
py={2}
|
|
||||||
borderRadius="full"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="whiteAlpha.200"
|
|
||||||
spacing={2}
|
|
||||||
>
|
|
||||||
<Box w={3} h={3} borderRadius="full" bg="#64748B" />
|
|
||||||
<Text color="whiteAlpha.800" fontSize="xs">平/无数据</Text>
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* 操作提示 */}
|
{/* 操作提示 */}
|
||||||
@@ -806,52 +1060,11 @@ 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>
|
||||||
|
|
||||||
{/* 层级说明 */}
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
bottom={4}
|
|
||||||
left="50%"
|
|
||||||
transform="translateX(-50%)"
|
|
||||||
zIndex={10}
|
|
||||||
pointerEvents="none"
|
|
||||||
>
|
|
||||||
<HStack
|
|
||||||
bg="rgba(0, 0, 0, 0.7)"
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
px={4}
|
|
||||||
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 图表 */}
|
{/* ECharts 图表 */}
|
||||||
<ReactECharts
|
<ReactECharts
|
||||||
ref={chartRef}
|
ref={chartRef}
|
||||||
|
|||||||
Reference in New Issue
Block a user