diff --git a/src/views/Concept/components/HierarchyView.js b/src/views/Concept/components/HierarchyView.js
index 511be394..bcfffde5 100644
--- a/src/views/Concept/components/HierarchyView.js
+++ b/src/views/Concept/components/HierarchyView.js
@@ -1,12 +1,10 @@
/**
* HierarchyView - 概念层级思维导图视图
*
- * 功能:
- * 1. 思维导图式展示概念层级结构(lv1 → lv2 → lv3 → concepts)
- * 2. 显示各层级的涨跌幅数据和概念数量
- * 3. 点击分类后切换到列表视图显示该分类下的概念
+ * 使用 ECharts Tree 图表实现真正的思维导图效果
+ * 支持径向布局(radial)和正交布局(orthogonal)
*/
-import React, { useState, useEffect, useCallback, useMemo } from 'react';
+import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import {
Box,
VStack,
@@ -17,416 +15,191 @@ import {
Spinner,
Center,
Flex,
- Collapse,
+ ButtonGroup,
+ Button,
useBreakpointValue,
Tooltip,
- Tag,
- TagLabel,
- Wrap,
- WrapItem,
+ IconButton,
} from '@chakra-ui/react';
-import {
- ChevronRightIcon,
- ChevronDownIcon,
-} from '@chakra-ui/icons';
+import ReactECharts from 'echarts-for-react';
import {
FaLayerGroup,
- FaArrowUp,
- FaArrowDown,
- FaTags,
- FaChartLine,
- FaBrain,
- FaMicrochip,
- FaRobot,
- FaMobileAlt,
- FaCar,
- FaBolt,
- FaPlane,
- FaShieldAlt,
- FaLandmark,
- FaFlask,
- FaShoppingCart,
- FaCoins,
- FaGlobe,
- FaHeartbeat,
- FaAtom,
+ FaSitemap,
+ FaProjectDiagram,
+ FaExpand,
+ FaCompress,
+ FaRedo,
} from 'react-icons/fa';
-import { keyframes } from '@emotion/react';
import { logger } from '../../../utils/logger';
-// 脉冲动画
-const pulseAnimation = keyframes`
- 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.4); }
- 70% { transform: scale(1.02); box-shadow: 0 0 0 10px rgba(139, 92, 246, 0); }
- 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(139, 92, 246, 0); }
-`;
-
-// 连接线动画
-const flowAnimation = keyframes`
- 0% { stroke-dashoffset: 20; }
- 100% { stroke-dashoffset: 0; }
-`;
-
-// 一级分类图标映射
-const LV1_ICONS = {
- '人工智能': FaBrain,
- '半导体': FaMicrochip,
- '机器人': FaRobot,
- '消费电子': FaMobileAlt,
- '智能驾驶与汽车': FaCar,
- '新能源与电力': FaBolt,
- '空天经济': FaPlane,
- '国防军工': FaShieldAlt,
- '政策与主题': FaLandmark,
- '周期与材料': FaFlask,
- '大消费': FaShoppingCart,
- '数字经济与金融科技': FaCoins,
- '全球宏观与贸易': FaGlobe,
- '医药健康': FaHeartbeat,
- '前沿科技': FaAtom,
-};
-
// 一级分类颜色映射
const LV1_COLORS = {
- '人工智能': { bg: 'purple', gradient: 'linear(135deg, #667eea 0%, #764ba2 100%)' },
- '半导体': { bg: 'blue', gradient: 'linear(135deg, #4facfe 0%, #00f2fe 100%)' },
- '机器人': { bg: 'cyan', gradient: 'linear(135deg, #43e97b 0%, #38f9d7 100%)' },
- '消费电子': { bg: 'pink', gradient: 'linear(135deg, #fa709a 0%, #fee140 100%)' },
- '智能驾驶与汽车': { bg: 'orange', gradient: 'linear(135deg, #f093fb 0%, #f5576c 100%)' },
- '新能源与电力': { bg: 'green', gradient: 'linear(135deg, #11998e 0%, #38ef7d 100%)' },
- '空天经济': { bg: 'teal', gradient: 'linear(135deg, #667eea 0%, #764ba2 100%)' },
- '国防军工': { bg: 'red', gradient: 'linear(135deg, #eb3349 0%, #f45c43 100%)' },
- '政策与主题': { bg: 'yellow', gradient: 'linear(135deg, #f6d365 0%, #fda085 100%)' },
- '周期与材料': { bg: 'gray', gradient: 'linear(135deg, #bdc3c7 0%, #2c3e50 100%)' },
- '大消费': { bg: 'pink', gradient: 'linear(135deg, #ff758c 0%, #ff7eb3 100%)' },
- '数字经济与金融科技': { bg: 'blue', gradient: 'linear(135deg, #4776e6 0%, #8e54e9 100%)' },
- '全球宏观与贸易': { bg: 'teal', gradient: 'linear(135deg, #00cdac 0%, #8ddad5 100%)' },
- '医药健康': { bg: 'green', gradient: 'linear(135deg, #56ab2f 0%, #a8e063 100%)' },
- '前沿科技': { bg: 'purple', gradient: 'linear(135deg, #a18cd1 0%, #fbc2eb 100%)' },
+ '人工智能': '#8B5CF6',
+ '半导体': '#3B82F6',
+ '机器人': '#10B981',
+ '消费电子': '#EC4899',
+ '智能驾驶与汽车': '#F97316',
+ '新能源与电力': '#22C55E',
+ '空天经济': '#06B6D4',
+ '国防军工': '#EF4444',
+ '政策与主题': '#F59E0B',
+ '周期与材料': '#6B7280',
+ '大消费': '#F472B6',
+ '数字经济与金融科技': '#6366F1',
+ '全球宏观与贸易': '#14B8A6',
+ '医药健康': '#84CC16',
+ '前沿科技': '#A855F7',
};
// 获取涨跌幅颜色
const getChangeColor = (value) => {
- if (value === null || value === undefined) return 'gray';
- return value > 0 ? 'red' : value < 0 ? 'green' : 'gray';
+ if (value === null || value === undefined) return '#9CA3AF';
+ return value > 0 ? '#EF4444' : value < 0 ? '#22C55E' : '#9CA3AF';
};
// 格式化涨跌幅
const formatChangePercent = (value) => {
- if (value === null || value === undefined) return null;
- const formatted = value.toFixed(2);
- return value > 0 ? `+${formatted}%` : `${formatted}%`;
+ if (value === null || value === undefined) return '';
+ const formatted = Math.abs(value).toFixed(2);
+ return value > 0 ? `+${formatted}%` : value < 0 ? `-${formatted}%` : '0.00%';
};
/**
- * 一级分类卡片组件
+ * 将后端层级数据转换为 ECharts tree 格式
*/
-const Lv1Card = ({
- item,
- isExpanded,
- onToggle,
- onSelectCategory,
- stats
-}) => {
- const IconComponent = LV1_ICONS[item.name] || FaLayerGroup;
- const colorConfig = LV1_COLORS[item.name] || { bg: 'purple', gradient: 'linear(135deg, #667eea 0%, #764ba2 100%)' };
- const isMobile = useBreakpointValue({ base: true, md: false });
+const transformToEChartsData = (hierarchy, stats) => {
+ if (!hierarchy || hierarchy.length === 0) return null;
- // 从统计数据中获取涨跌幅
- const avgChange = stats?.avg_change_pct;
- const changeColor = getChangeColor(avgChange);
+ // 创建统计数据映射
+ const statsMap = {};
+ if (stats?.statistics) {
+ stats.statistics.forEach(s => {
+ statsMap[s.lv1] = s;
+ });
+ }
- return (
-
概念数量: ${data.concept_count}`;
+ }
+ if (data.avg_change_pct !== undefined) {
+ const color = data.avg_change_pct > 0 ? '#EF4444' : data.avg_change_pct < 0 ? '#22C55E' : '#9CA3AF';
+ content += `
平均涨跌: ${formatChangePercent(data.avg_change_pct)}`;
+ }
+ if (data.type) {
+ const typeMap = { lv1: '一级分类', lv2: '二级分类', lv3: '三级分类', concept: '概念' };
+ content += `
${typeMap[data.type] || ''}`;
+ }
+
+ return content;
+ },
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
+ borderColor: '#E5E7EB',
+ borderWidth: 1,
+ padding: [8, 12],
+ textStyle: {
+ color: '#1F2937',
+ fontSize: 12,
+ },
+ },
+ series: [
+ {
+ type: 'tree',
+ data: [treeData],
+ layout: isRadial ? 'radial' : 'orthogonal',
+ orient: isRadial ? undefined : 'LR',
+ symbol: 'circle',
+ symbolSize: (value, params) => {
+ const data = params.data?.data || {};
+ if (data.type === 'lv1') return 24;
+ if (data.type === 'lv2') return 18;
+ if (data.type === 'lv3') return 14;
+ if (data.type === 'concept') return 10;
+ return 30; // 根节点
+ },
+ initialTreeDepth: 2,
+ animationDuration: 550,
+ animationDurationUpdate: 750,
+ roam: true, // 启用缩放和平移
+ label: {
+ position: isRadial ? 'radial' : 'right',
+ verticalAlign: 'middle',
+ align: isRadial ? undefined : 'left',
+ fontSize: 12,
+ distance: 8,
+ },
+ leaves: {
+ label: {
+ position: isRadial ? 'radial' : 'right',
+ verticalAlign: 'middle',
+ align: isRadial ? undefined : 'left',
+ },
+ },
+ emphasis: {
+ focus: 'descendant',
+ itemStyle: {
+ shadowBlur: 10,
+ shadowColor: 'rgba(0, 0, 0, 0.3)',
+ },
+ },
+ expandAndCollapse: true,
+ lineStyle: {
+ color: '#CBD5E1',
+ width: 1.5,
+ curveness: 0.5,
+ },
+ },
+ ],
+ };
+ }, [hierarchy, hierarchyStats, layout]);
+
+ // 处理节点点击
+ const handleChartClick = useCallback((params) => {
+ const data = params.data?.data;
+ if (!data) return;
+
+ logger.info('HierarchyView', '节点点击', data);
+
+ let filter = { lv1: null, lv2: null, lv3: null };
+
+ switch (data.type) {
+ case 'lv1':
+ filter = { lv1: data.name, lv2: null, lv3: null };
+ break;
+ case 'lv2':
+ filter = { lv1: data.parentLv1, lv2: data.name, lv3: null };
+ break;
+ case 'lv3':
+ filter = { lv1: data.parentLv1, lv2: data.parentLv2, lv3: data.name };
+ break;
+ case 'concept':
+ // 点击具体概念,筛选到其所属的 lv2
+ filter = { lv1: data.parentLv1, lv2: data.parentLv2, lv3: null };
+ break;
+ default:
+ return;
+ }
+
+ onSelectCategory && onSelectCategory(filter);
}, [onSelectCategory]);
+ // 重置图表视图
+ const handleResetView = useCallback(() => {
+ if (chartRef.current) {
+ const chart = chartRef.current.getEchartsInstance();
+ chart.dispatchAction({
+ type: 'restore',
+ });
+ }
+ }, []);
+
+ // 切换全屏
+ const toggleFullscreen = useCallback(() => {
+ setIsFullscreen(prev => !prev);
+ }, []);
+
+ // 图表事件
+ const chartEvents = useMemo(() => ({
+ click: handleChartClick,
+ }), [handleChartClick]);
+
if (loading) {
return (
-