update pay ui
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* SunburstView - 概念层级旭日图(支持钻取)
|
||||
* TreemapView - 概念层级矩形树图(支持钻取)
|
||||
*
|
||||
* 特性:
|
||||
* 1. 默认显示3层(一级 → 二级 → 三级)
|
||||
* 2. 点击扇区钻取进入,显示该分类下的子级
|
||||
* 2. 点击矩形钻取进入,显示该分类下的子级
|
||||
* 3. 支持返回上级
|
||||
* 4. 涨红跌绿颜色映射
|
||||
*/
|
||||
@@ -23,9 +23,6 @@ import {
|
||||
Tooltip,
|
||||
Badge,
|
||||
useBreakpointValue,
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
FaLayerGroup,
|
||||
@@ -36,12 +33,10 @@ import {
|
||||
FaArrowUp,
|
||||
FaArrowDown,
|
||||
FaCircle,
|
||||
FaChartPie,
|
||||
FaUndo,
|
||||
FaTh,
|
||||
FaChevronRight,
|
||||
FaArrowLeft,
|
||||
} from 'react-icons/fa';
|
||||
import { ChevronRightIcon } from '@chakra-ui/icons';
|
||||
import { logger } from '../../../utils/logger';
|
||||
|
||||
// 一级分类颜色映射(基础色)
|
||||
@@ -72,14 +67,14 @@ const getChangeColor = (value, baseColor = '#64748B') => {
|
||||
if (value > 5) return '#EF4444';
|
||||
if (value > 3) return '#F87171';
|
||||
if (value > 1) return '#FCA5A5';
|
||||
if (value > 0) return '#FED7D7';
|
||||
if (value > 0) return '#FECACA';
|
||||
|
||||
// 跌 - 绿色系
|
||||
if (value < -7) return '#15803D';
|
||||
if (value < -5) return '#16A34A';
|
||||
if (value < -3) return '#22C55E';
|
||||
if (value < -1) return '#4ADE80';
|
||||
if (value < 0) return '#BBF7D0';
|
||||
if (value < 0) return '#86EFAC';
|
||||
|
||||
return baseColor;
|
||||
};
|
||||
@@ -111,13 +106,8 @@ const ForceGraphView = ({
|
||||
const [priceData, setPriceData] = useState({ lv1Map: {}, lv2Map: {}, lv3Map: {}, leafMap: {} });
|
||||
const [priceLoading, setPriceLoading] = useState(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [hoveredItem, setHoveredItem] = useState(null);
|
||||
|
||||
// 钻取状态:记录当前查看的路径
|
||||
// null = 根视图(显示所有一级)
|
||||
// { lv1: '人工智能' } = 查看人工智能下的二级和三级
|
||||
// { lv1: '人工智能', lv2: 'AI基础设施' } = 查看AI基础设施下的三级和概念
|
||||
// { lv1: '人工智能', lv2: 'AI基础设施', lv3: 'AI芯片' } = 查看AI芯片下的概念
|
||||
const [drillPath, setDrillPath] = useState(null);
|
||||
|
||||
const chartRef = useRef();
|
||||
@@ -137,12 +127,12 @@ const ForceGraphView = ({
|
||||
const data = await response.json();
|
||||
setHierarchy(data.hierarchy || []);
|
||||
|
||||
logger.info('SunburstView', '层级结构加载完成', {
|
||||
logger.info('TreemapView', '层级结构加载完成', {
|
||||
totalLv1: data.hierarchy?.length,
|
||||
totalConcepts: data.total_concepts
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('SunburstView', 'fetchHierarchy', err);
|
||||
logger.error('TreemapView', 'fetchHierarchy', err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -162,7 +152,7 @@ const ForceGraphView = ({
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
logger.warn('SunburstView', '获取层级涨跌幅失败', { status: response.status });
|
||||
logger.warn('TreemapView', '获取层级涨跌幅失败', { status: response.status });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -191,7 +181,7 @@ const ForceGraphView = ({
|
||||
|
||||
setPriceData({ lv1Map, lv2Map, lv3Map, leafMap });
|
||||
} catch (err) {
|
||||
logger.warn('SunburstView', '获取层级涨跌幅失败', { error: err.message });
|
||||
logger.warn('TreemapView', '获取层级涨跌幅失败', { error: err.message });
|
||||
} finally {
|
||||
setPriceLoading(false);
|
||||
}
|
||||
@@ -207,8 +197,8 @@ const ForceGraphView = ({
|
||||
}
|
||||
}, [hierarchy, fetchHierarchyPrice]);
|
||||
|
||||
// 根据钻取路径构建旭日图数据
|
||||
const sunburstData = useMemo(() => {
|
||||
// 根据钻取路径构建 Treemap 数据
|
||||
const treemapData = useMemo(() => {
|
||||
const { lv1Map, lv2Map, lv3Map, leafMap } = priceData;
|
||||
|
||||
// 根视图:显示所有一级,每个一级下显示二级和三级(不显示概念)
|
||||
@@ -222,6 +212,8 @@ const ForceGraphView = ({
|
||||
value: lv1Price.stock_count || lv1.concept_count * 10 || 100,
|
||||
itemStyle: {
|
||||
color: getChangeColor(lv1Price.avg_change_pct, lv1BaseColor),
|
||||
borderColor: '#1E293B',
|
||||
borderWidth: 2,
|
||||
},
|
||||
data: {
|
||||
level: 'lv1',
|
||||
@@ -243,6 +235,8 @@ const ForceGraphView = ({
|
||||
value: lv2Price.stock_count || lv2.concept_count * 5 || 50,
|
||||
itemStyle: {
|
||||
color: getChangeColor(lv2Price.avg_change_pct, lv1BaseColor),
|
||||
borderColor: '#334155',
|
||||
borderWidth: 1,
|
||||
},
|
||||
data: {
|
||||
level: 'lv2',
|
||||
@@ -265,6 +259,8 @@ const ForceGraphView = ({
|
||||
value: lv3Price.stock_count || 30,
|
||||
itemStyle: {
|
||||
color: getChangeColor(lv3Price.avg_change_pct, lv1BaseColor),
|
||||
borderColor: '#475569',
|
||||
borderWidth: 1,
|
||||
},
|
||||
data: {
|
||||
level: 'lv3',
|
||||
@@ -288,7 +284,7 @@ const ForceGraphView = ({
|
||||
});
|
||||
}
|
||||
|
||||
// 钻取到某个一级分类:显示该一级下的二级、三级、概念
|
||||
// 钻取到某个一级分类
|
||||
if (drillPath.lv1 && !drillPath.lv2) {
|
||||
const lv1 = hierarchy.find(h => h.name === drillPath.lv1);
|
||||
if (!lv1) return [];
|
||||
@@ -303,6 +299,8 @@ const ForceGraphView = ({
|
||||
value: lv2Price.stock_count || lv2.concept_count * 5 || 50,
|
||||
itemStyle: {
|
||||
color: getChangeColor(lv2Price.avg_change_pct, lv1BaseColor),
|
||||
borderColor: '#1E293B',
|
||||
borderWidth: 2,
|
||||
},
|
||||
data: {
|
||||
level: 'lv2',
|
||||
@@ -325,6 +323,8 @@ const ForceGraphView = ({
|
||||
value: lv3Price.stock_count || 30,
|
||||
itemStyle: {
|
||||
color: getChangeColor(lv3Price.avg_change_pct, lv1BaseColor),
|
||||
borderColor: '#334155',
|
||||
borderWidth: 1,
|
||||
},
|
||||
data: {
|
||||
level: 'lv3',
|
||||
@@ -346,6 +346,8 @@ const ForceGraphView = ({
|
||||
value: conceptPrice.stock_count || 10,
|
||||
itemStyle: {
|
||||
color: getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor),
|
||||
borderColor: '#475569',
|
||||
borderWidth: 1,
|
||||
},
|
||||
data: {
|
||||
level: 'concept',
|
||||
@@ -373,6 +375,8 @@ const ForceGraphView = ({
|
||||
value: conceptPrice.stock_count || 10,
|
||||
itemStyle: {
|
||||
color: getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor),
|
||||
borderColor: '#475569',
|
||||
borderWidth: 1,
|
||||
},
|
||||
data: {
|
||||
level: 'concept',
|
||||
@@ -390,7 +394,7 @@ const ForceGraphView = ({
|
||||
});
|
||||
}
|
||||
|
||||
// 钻取到某个二级分类:显示该二级下的三级和概念
|
||||
// 钻取到某个二级分类
|
||||
if (drillPath.lv1 && drillPath.lv2 && !drillPath.lv3) {
|
||||
const lv1 = hierarchy.find(h => h.name === drillPath.lv1);
|
||||
if (!lv1) return [];
|
||||
@@ -412,6 +416,8 @@ const ForceGraphView = ({
|
||||
value: lv3Price.stock_count || 30,
|
||||
itemStyle: {
|
||||
color: getChangeColor(lv3Price.avg_change_pct, lv1BaseColor),
|
||||
borderColor: '#1E293B',
|
||||
borderWidth: 2,
|
||||
},
|
||||
data: {
|
||||
level: 'lv3',
|
||||
@@ -433,6 +439,8 @@ const ForceGraphView = ({
|
||||
value: conceptPrice.stock_count || 10,
|
||||
itemStyle: {
|
||||
color: getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor),
|
||||
borderColor: '#334155',
|
||||
borderWidth: 1,
|
||||
},
|
||||
data: {
|
||||
level: 'concept',
|
||||
@@ -460,6 +468,8 @@ const ForceGraphView = ({
|
||||
value: conceptPrice.stock_count || 10,
|
||||
itemStyle: {
|
||||
color: getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor),
|
||||
borderColor: '#1E293B',
|
||||
borderWidth: 2,
|
||||
},
|
||||
data: {
|
||||
level: 'concept',
|
||||
@@ -476,7 +486,7 @@ const ForceGraphView = ({
|
||||
return result;
|
||||
}
|
||||
|
||||
// 钻取到某个三级分类:显示该三级下的概念
|
||||
// 钻取到某个三级分类
|
||||
if (drillPath.lv1 && drillPath.lv2 && drillPath.lv3) {
|
||||
const lv1 = hierarchy.find(h => h.name === drillPath.lv1);
|
||||
if (!lv1) return [];
|
||||
@@ -496,6 +506,8 @@ const ForceGraphView = ({
|
||||
value: conceptPrice.stock_count || 10,
|
||||
itemStyle: {
|
||||
color: getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor),
|
||||
borderColor: '#1E293B',
|
||||
borderWidth: 2,
|
||||
},
|
||||
data: {
|
||||
level: 'concept',
|
||||
@@ -515,54 +527,124 @@ const ForceGraphView = ({
|
||||
|
||||
// 获取当前显示的层级数
|
||||
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; // 三级钻取:概念
|
||||
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 配置
|
||||
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,
|
||||
// 根据层级深度设置不同的 levels 配置
|
||||
const levels = [
|
||||
{
|
||||
// 根节点配置
|
||||
itemStyle: {
|
||||
borderWidth: index === 1 ? 3 : index === 2 ? 2 : 1,
|
||||
borderColor: '#0F172A',
|
||||
borderRadius: 4,
|
||||
borderWidth: 0,
|
||||
gapWidth: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
// 第一层
|
||||
itemStyle: {
|
||||
borderColor: '#1E293B',
|
||||
borderWidth: 3,
|
||||
gapWidth: 2,
|
||||
},
|
||||
upperLabel: {
|
||||
show: true,
|
||||
height: 30,
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
textShadowColor: 'rgba(0,0,0,0.8)',
|
||||
textShadowBlur: 4,
|
||||
formatter: (params) => {
|
||||
const data = params.data?.data || {};
|
||||
const changePct = data.changePct;
|
||||
const changeStr = changePct !== undefined && changePct !== null
|
||||
? ` ${formatChangePercent(changePct)}`
|
||||
: '';
|
||||
return `${params.name}${changeStr}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// 第二层
|
||||
itemStyle: {
|
||||
borderColor: '#334155',
|
||||
borderWidth: 2,
|
||||
gapWidth: 1,
|
||||
},
|
||||
upperLabel: {
|
||||
show: true,
|
||||
height: 24,
|
||||
color: '#E2E8F0',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
textShadowColor: 'rgba(0,0,0,0.6)',
|
||||
textShadowBlur: 3,
|
||||
formatter: (params) => {
|
||||
const data = params.data?.data || {};
|
||||
const changePct = data.changePct;
|
||||
const changeStr = changePct !== undefined && changePct !== null
|
||||
? ` ${formatChangePercent(changePct)}`
|
||||
: '';
|
||||
return `${params.name}${changeStr}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// 第三层
|
||||
itemStyle: {
|
||||
borderColor: '#475569',
|
||||
borderWidth: 1,
|
||||
gapWidth: 1,
|
||||
},
|
||||
label: {
|
||||
...level.label,
|
||||
color: '#FFFFFF',
|
||||
textShadowColor: '#000',
|
||||
textShadowBlur: 4,
|
||||
show: true,
|
||||
position: 'insideTopLeft',
|
||||
color: '#F1F5F9',
|
||||
fontSize: 11,
|
||||
textShadowColor: 'rgba(0,0,0,0.5)',
|
||||
textShadowBlur: 2,
|
||||
formatter: (params) => {
|
||||
const data = params.data?.data || {};
|
||||
const changePct = data.changePct;
|
||||
if (changePct !== undefined && changePct !== null) {
|
||||
return `${params.name}\n${formatChangePercent(changePct)}`;
|
||||
}
|
||||
return params.name;
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
{
|
||||
// 第四层(概念层,只在钻取时显示)
|
||||
itemStyle: {
|
||||
borderColor: '#64748B',
|
||||
borderWidth: 1,
|
||||
gapWidth: 1,
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'insideTopLeft',
|
||||
color: '#F8FAFC',
|
||||
fontSize: 10,
|
||||
textShadowColor: 'rgba(0,0,0,0.4)',
|
||||
textShadowBlur: 2,
|
||||
formatter: (params) => {
|
||||
const data = params.data?.data || {};
|
||||
const changePct = data.changePct;
|
||||
if (changePct !== undefined && changePct !== null) {
|
||||
return `${params.name}\n${formatChangePercent(changePct)}`;
|
||||
}
|
||||
return params.name;
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
@@ -591,17 +673,17 @@ const ForceGraphView = ({
|
||||
const changeIcon = changePct > 0 ? '▲' : changePct < 0 ? '▼' : '●';
|
||||
|
||||
return `
|
||||
<div style="min-width: 160px;">
|
||||
<div style="min-width: 180px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||||
<span style="
|
||||
background: ${data.baseColor || '#8B5CF6'};
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
">${levelMap[data.level] || '分类'}</span>
|
||||
</div>
|
||||
<div style="font-size: 16px; font-weight: bold; margin-bottom: 8px; color: #FFFFFF;">
|
||||
<div style="font-size: 16px; font-weight: bold; margin-bottom: 10px; color: #FFFFFF;">
|
||||
${params.name}
|
||||
</div>
|
||||
${changePct !== undefined && changePct !== null ? `
|
||||
@@ -610,12 +692,12 @@ const ForceGraphView = ({
|
||||
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;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 10px;
|
||||
">
|
||||
<span style="color: ${changeColor}; font-size: 12px;">${changeIcon}</span>
|
||||
<span style="color: ${changeColor}; font-weight: bold; font-size: 15px; font-family: monospace;">
|
||||
<span style="color: ${changeColor}; font-size: 14px;">${changeIcon}</span>
|
||||
<span style="color: ${changeColor}; font-weight: bold; font-size: 18px; font-family: monospace;">
|
||||
${formatChangePercent(changePct)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -625,43 +707,52 @@ const ForceGraphView = ({
|
||||
${data.conceptCount ? ` · ${data.conceptCount} 个概念` : ''}
|
||||
</div>
|
||||
${data.level === 'concept' ? `
|
||||
<div style="color: #A78BFA; font-size: 11px; margin-top: 6px;">
|
||||
点击查看详情 →
|
||||
<div style="color: #A78BFA; font-size: 11px; margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
🔗 点击查看概念详情
|
||||
</div>
|
||||
` : `
|
||||
<div style="color: #64748B; font-size: 11px; margin-top: 6px;">
|
||||
点击进入查看详情
|
||||
` : data.level !== 'concept' ? `
|
||||
<div style="color: #64748B; font-size: 11px; margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
📂 点击进入查看子分类
|
||||
</div>
|
||||
`}
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'sunburst',
|
||||
data: sunburstData,
|
||||
radius: ['15%', '90%'],
|
||||
center: ['50%', '50%'],
|
||||
sort: 'desc',
|
||||
emphasis: {
|
||||
focus: 'ancestor',
|
||||
itemStyle: {
|
||||
shadowBlur: 20,
|
||||
shadowColor: 'rgba(139, 92, 246, 0.5)',
|
||||
},
|
||||
type: 'treemap',
|
||||
data: treemapData,
|
||||
left: 0,
|
||||
top: 60,
|
||||
right: 0,
|
||||
bottom: 50,
|
||||
roam: false,
|
||||
nodeClick: false, // 禁用内置的点击行为,我们自己处理
|
||||
breadcrumb: {
|
||||
show: false, // 隐藏内置面包屑,使用自定义的
|
||||
},
|
||||
levels: levels,
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}',
|
||||
},
|
||||
levels: styledLevels,
|
||||
itemStyle: {
|
||||
borderRadius: 4,
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 20,
|
||||
shadowColor: 'rgba(139, 92, 246, 0.6)',
|
||||
},
|
||||
},
|
||||
animation: true,
|
||||
animationDuration: 600,
|
||||
animationDuration: 500,
|
||||
animationEasing: 'cubicOut',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [sunburstData, currentLevels]);
|
||||
}, [treemapData, currentLevels]);
|
||||
|
||||
// 图表事件
|
||||
const onChartEvents = useMemo(() => ({
|
||||
@@ -684,18 +775,9 @@ const ForceGraphView = ({
|
||||
setDrillPath({ lv1: data.parentLv1, lv2: data.parentLv2, lv3: params.name });
|
||||
}
|
||||
|
||||
logger.info('SunburstView', '钻取', { level: data.level, name: params.name, path: drillPath });
|
||||
logger.info('TreemapView', '钻取', { level: data.level, name: params.name });
|
||||
},
|
||||
mouseover: (params) => {
|
||||
setHoveredItem({
|
||||
name: params.name,
|
||||
data: params.data?.data,
|
||||
});
|
||||
},
|
||||
mouseout: () => {
|
||||
setHoveredItem(null);
|
||||
},
|
||||
}), [drillPath]);
|
||||
}), []);
|
||||
|
||||
// 返回上一层
|
||||
const handleGoBack = useCallback(() => {
|
||||
@@ -753,7 +835,7 @@ const ForceGraphView = ({
|
||||
<Center h="500px" bg="rgba(15, 23, 42, 0.8)" borderRadius="2xl">
|
||||
<VStack spacing={4}>
|
||||
<Spinner size="xl" color="purple.400" thickness="4px" />
|
||||
<Text color="gray.400">正在构建旭日图...</Text>
|
||||
<Text color="gray.400">正在构建矩形树图...</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
@@ -789,18 +871,6 @@ const ForceGraphView = ({
|
||||
borderColor="whiteAlpha.200"
|
||||
h={containerHeight}
|
||||
>
|
||||
{/* 背景装饰 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
opacity={0.15}
|
||||
bgImage="radial-gradient(circle at 50% 50%, rgba(139, 92, 246, 0.3) 0%, transparent 60%)"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
|
||||
{/* 顶部工具栏 */}
|
||||
<Flex
|
||||
position="absolute"
|
||||
@@ -813,7 +883,7 @@ const ForceGraphView = ({
|
||||
pointerEvents="none"
|
||||
>
|
||||
{/* 左侧标题和面包屑 */}
|
||||
<VStack align="start" spacing={3} pointerEvents="auto">
|
||||
<VStack align="start" spacing={2} pointerEvents="auto">
|
||||
<HStack spacing={3}>
|
||||
{/* 返回按钮 */}
|
||||
{drillPath && (
|
||||
@@ -842,115 +912,46 @@ const ForceGraphView = ({
|
||||
borderColor="whiteAlpha.200"
|
||||
boxShadow="0 4px 20px rgba(0, 0, 0, 0.3)"
|
||||
>
|
||||
<Icon as={FaChartPie} color="purple.300" />
|
||||
<Icon as={FaTh} color="purple.300" />
|
||||
<Text color="white" fontWeight="bold" fontSize="sm">
|
||||
概念旭日图
|
||||
概念矩形树图
|
||||
</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{/* 面包屑导航 */}
|
||||
<HStack
|
||||
bg="rgba(0, 0, 0, 0.7)"
|
||||
backdropFilter="blur(12px)"
|
||||
px={4}
|
||||
py={2}
|
||||
borderRadius="full"
|
||||
border="1px solid"
|
||||
borderColor="whiteAlpha.200"
|
||||
spacing={2}
|
||||
>
|
||||
{breadcrumbItems.map((item, index) => (
|
||||
<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>
|
||||
|
||||
{/* 悬停信息 */}
|
||||
{hoveredItem && hoveredItem.data?.level && (
|
||||
<Box
|
||||
bg="rgba(0, 0, 0, 0.85)"
|
||||
{/* 面包屑导航 */}
|
||||
<HStack
|
||||
bg="rgba(0, 0, 0, 0.7)"
|
||||
backdropFilter="blur(12px)"
|
||||
px={4}
|
||||
py={3}
|
||||
borderRadius="xl"
|
||||
py={2}
|
||||
borderRadius="full"
|
||||
border="1px solid"
|
||||
borderColor={hoveredItem.data?.baseColor || 'purple.500'}
|
||||
boxShadow="0 4px 20px rgba(0, 0, 0, 0.4)"
|
||||
maxW="280px"
|
||||
borderColor="whiteAlpha.200"
|
||||
spacing={2}
|
||||
>
|
||||
<VStack align="start" spacing={2}>
|
||||
<HStack spacing={2}>
|
||||
<Badge
|
||||
bg={hoveredItem.data?.baseColor || 'purple.500'}
|
||||
color="white"
|
||||
borderRadius="full"
|
||||
px={2}
|
||||
fontSize="xs"
|
||||
>
|
||||
{hoveredItem.data?.level === 'lv1' ? '一级分类' :
|
||||
hoveredItem.data?.level === 'lv2' ? '二级分类' :
|
||||
hoveredItem.data?.level === 'lv3' ? '三级分类' : '概念'}
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
<Text color="white" fontWeight="bold" fontSize="md">
|
||||
{hoveredItem.name}
|
||||
</Text>
|
||||
|
||||
{hoveredItem.data?.changePct !== undefined && hoveredItem.data?.changePct !== null && (
|
||||
<HStack
|
||||
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={hoveredItem.data.changePct > 0 ? 'red.400' : hoveredItem.data.changePct < 0 ? 'green.400' : 'gray.500'}
|
||||
>
|
||||
<Icon
|
||||
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}
|
||||
/>
|
||||
<Text
|
||||
color={hoveredItem.data.changePct > 0 ? 'red.300' : hoveredItem.data.changePct < 0 ? 'green.300' : 'gray.300'}
|
||||
fontWeight="bold"
|
||||
fontSize="lg"
|
||||
fontFamily="mono"
|
||||
>
|
||||
{formatChangePercent(hoveredItem.data.changePct)}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
<HStack spacing={4} color="whiteAlpha.700" fontSize="xs">
|
||||
{hoveredItem.data?.stockCount && (
|
||||
<Text>{hoveredItem.data.stockCount} 只股票</Text>
|
||||
)}
|
||||
{hoveredItem.data?.conceptCount && (
|
||||
<Text>{hoveredItem.data.conceptCount} 个概念</Text>
|
||||
{breadcrumbItems.map((item, index) => (
|
||||
<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>
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
))}
|
||||
</HStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
|
||||
{/* 右侧控制按钮 */}
|
||||
@@ -1024,7 +1025,7 @@ const ForceGraphView = ({
|
||||
borderColor="whiteAlpha.200"
|
||||
spacing={2}
|
||||
>
|
||||
<Box w={3} h={3} borderRadius="full" bg="#EF4444" boxShadow="0 0 8px rgba(239, 68, 68, 0.5)" />
|
||||
<Box w={3} h={3} borderRadius="sm" bg="#EF4444" />
|
||||
<Text color="whiteAlpha.800" fontSize="xs">涨</Text>
|
||||
</HStack>
|
||||
<HStack
|
||||
@@ -1037,7 +1038,7 @@ const ForceGraphView = ({
|
||||
borderColor="whiteAlpha.200"
|
||||
spacing={2}
|
||||
>
|
||||
<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="sm" bg="#22C55E" />
|
||||
<Text color="whiteAlpha.800" fontSize="xs">跌</Text>
|
||||
</HStack>
|
||||
</Flex>
|
||||
@@ -1060,7 +1061,7 @@ const ForceGraphView = ({
|
||||
borderColor="whiteAlpha.200"
|
||||
>
|
||||
<Text color="whiteAlpha.600" fontSize="xs">
|
||||
点击扇区进入 · 点击概念查看详情
|
||||
点击分类进入 · 点击概念查看详情
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user