diff --git a/src/views/Concept/components/HierarchyView.js b/src/views/Concept/components/HierarchyView.js index 21b65489..7700f1eb 100644 --- a/src/views/Concept/components/HierarchyView.js +++ b/src/views/Concept/components/HierarchyView.js @@ -1,14 +1,15 @@ /** * HierarchyView - 概念层级热力图视图 * - * 使用 ECharts Treemap 实现热力图效果 + * 使用 CSS Grid + Chakra UI 实现热力图效果 * 特性: - * 1. 炫酷的矩形树图/热力图展示 + * 1. 炫酷的矩形热力图展示,涨红跌绿背景色 * 2. 点击 lv1 进入 lv2,点击 lv2 进入 lv3,层层钻取 * 3. 集成 /hierarchy/price 接口获取实时涨跌幅 - * 4. 支持面包屑导航返回上级 + * 4. 每个分类有独特图标 + * 5. 支持面包屑导航返回上级 */ -import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Box, VStack, @@ -23,8 +24,9 @@ import { useBreakpointValue, Tooltip, IconButton, + SimpleGrid, + keyframes, } from '@chakra-ui/react'; -import ReactECharts from 'echarts-for-react'; import { FaLayerGroup, FaExpand, @@ -32,38 +34,124 @@ import { FaSync, FaHome, FaChevronRight, + FaBrain, + FaMicrochip, + FaRobot, + FaMobileAlt, + FaCar, + FaBolt, + FaRocket, + FaShieldAlt, + FaGlobe, + FaIndustry, + FaShoppingCart, + FaCoins, + FaHeartbeat, + FaAtom, + FaLightbulb, + FaArrowUp, + FaArrowDown, + FaCubes, + FaServer, + FaCode, + FaMagic, + FaEye, + FaPlane, + FaSatellite, + FaBatteryFull, + FaSolarPanel, + FaWind, } from 'react-icons/fa'; import { logger } from '../../../utils/logger'; -// 一级分类颜色映射 -const LV1_COLORS = { - '人工智能': ['#8B5CF6', '#A78BFA', '#C4B5FD'], - '半导体': ['#3B82F6', '#60A5FA', '#93C5FD'], - '机器人': ['#10B981', '#34D399', '#6EE7B7'], - '消费电子': ['#EC4899', '#F472B6', '#F9A8D4'], - '智能驾驶与汽车': ['#F97316', '#FB923C', '#FDBA74'], - '新能源与电力': ['#22C55E', '#4ADE80', '#86EFAC'], - '空天经济': ['#06B6D4', '#22D3EE', '#67E8F9'], - '国防军工': ['#EF4444', '#F87171', '#FCA5A5'], - '政策与主题': ['#F59E0B', '#FBBF24', '#FCD34D'], - '周期与材料': ['#6B7280', '#9CA3AF', '#D1D5DB'], - '大消费': ['#F472B6', '#F9A8D4', '#FBCFE8'], - '数字经济与金融科技': ['#6366F1', '#818CF8', '#A5B4FC'], - '全球宏观与贸易': ['#14B8A6', '#2DD4BF', '#5EEAD4'], - '医药健康': ['#84CC16', '#A3E635', '#BEF264'], - '前沿科技': ['#A855F7', '#C084FC', '#D8B4FE'], +// 一级分类图标映射 +const LV1_ICONS = { + '人工智能': FaBrain, + '半导体': FaMicrochip, + '机器人': FaRobot, + '消费电子': FaMobileAlt, + '智能驾驶与汽车': FaCar, + '新能源与电力': FaBolt, + '空天经济': FaRocket, + '国防军工': FaShieldAlt, + '政策与主题': FaGlobe, + '周期与材料': FaIndustry, + '大消费': FaShoppingCart, + '数字经济与金融科技': FaCoins, + '全球宏观与贸易': FaGlobe, + '医药健康': FaHeartbeat, + '前沿科技': FaAtom, }; -// 获取涨跌幅颜色(红涨绿跌) -const getChangeColor = (value) => { - if (value === null || value === undefined) return '#9CA3AF'; - if (value > 3) return '#DC2626'; - if (value > 1) return '#EF4444'; - if (value > 0) return '#F87171'; - if (value < -3) return '#15803D'; - if (value < -1) return '#22C55E'; - if (value < 0) return '#4ADE80'; - return '#9CA3AF'; +// 二级分类图标映射 +const LV2_ICONS = { + 'AI基础设施': FaServer, + 'AI模型与软件': FaCode, + 'AI应用': FaMagic, + '半导体设备': FaCubes, + '半导体材料': FaAtom, + '芯片设计与制造': FaMicrochip, + '先进封装': FaCubes, + '人形机器人整机': FaRobot, + '机器人核心零部件': FaCubes, + '其他类型机器人': FaRobot, + '智能终端': FaMobileAlt, + 'XR与空间计算': FaEye, + '华为产业链': FaMobileAlt, + '自动驾驶解决方案': FaCar, + '智能汽车产业链': FaCar, + '车路协同': FaCar, + '新型电池技术': FaBatteryFull, + '电力设备与电网': FaBolt, + '清洁能源': FaSolarPanel, + '低空经济': FaPlane, + '商业航天': FaSatellite, + '无人作战与信息化': FaShieldAlt, + '海军装备': FaShieldAlt, + '军贸出海': FaGlobe, +}; + +// 一级分类基础颜色(用于渐变) +const LV1_BASE_COLORS = { + '人工智能': { from: '#8B5CF6', to: '#A78BFA' }, + '半导体': { from: '#3B82F6', to: '#60A5FA' }, + '机器人': { from: '#10B981', to: '#34D399' }, + '消费电子': { from: '#EC4899', to: '#F472B6' }, + '智能驾驶与汽车': { from: '#F97316', to: '#FB923C' }, + '新能源与电力': { from: '#22C55E', to: '#4ADE80' }, + '空天经济': { from: '#06B6D4', to: '#22D3EE' }, + '国防军工': { from: '#EF4444', to: '#F87171' }, + '政策与主题': { from: '#F59E0B', to: '#FBBF24' }, + '周期与材料': { from: '#6B7280', to: '#9CA3AF' }, + '大消费': { from: '#F472B6', to: '#F9A8D4' }, + '数字经济与金融科技': { from: '#6366F1', to: '#818CF8' }, + '全球宏观与贸易': { from: '#14B8A6', to: '#2DD4BF' }, + '医药健康': { from: '#84CC16', to: '#A3E635' }, + '前沿科技': { from: '#A855F7', to: '#C084FC' }, +}; + +// 根据涨跌幅获取背景色(涨红跌绿渐变) +const getChangeBgColor = (value, baseColor = null) => { + if (value === null || value === undefined) { + // 无数据时使用基础色 + if (baseColor) { + return `linear-gradient(135deg, ${baseColor.from} 0%, ${baseColor.to} 100%)`; + } + return 'linear-gradient(135deg, #6B7280 0%, #9CA3AF 100%)'; + } + + // 涨跌幅越大,颜色越深 + if (value > 5) return 'linear-gradient(135deg, #991B1B 0%, #DC2626 100%)'; + if (value > 3) return 'linear-gradient(135deg, #B91C1C 0%, #EF4444 100%)'; + if (value > 1) return 'linear-gradient(135deg, #DC2626 0%, #F87171 100%)'; + if (value > 0) return 'linear-gradient(135deg, #EF4444 0%, #FCA5A5 100%)'; + if (value < -5) return 'linear-gradient(135deg, #14532D 0%, #16A34A 100%)'; + if (value < -3) return 'linear-gradient(135deg, #166534 0%, #22C55E 100%)'; + if (value < -1) return 'linear-gradient(135deg, #16A34A 0%, #4ADE80 100%)'; + if (value < 0) return 'linear-gradient(135deg, #22C55E 0%, #86EFAC 100%)'; + + // 平盘 + return 'linear-gradient(135deg, #6B7280 0%, #9CA3AF 100%)'; }; // 格式化涨跌幅 @@ -73,13 +161,173 @@ const formatChangePercent = (value) => { return value > 0 ? `+${formatted}%` : value < 0 ? `-${formatted}%` : '0.00%'; }; -// 获取 lv1 的颜色 -const getLv1Color = (name, index = 0) => { - const colors = LV1_COLORS[name]; - if (colors) return colors[Math.min(index, colors.length - 1)]; - // 默认颜色 - const defaultColors = ['#8B5CF6', '#A78BFA', '#C4B5FD']; - return defaultColors[Math.min(index, defaultColors.length - 1)]; +// 获取图标 +const getIcon = (name, level) => { + if (level === 'lv1') { + return LV1_ICONS[name] || FaLayerGroup; + } + if (level === 'lv2') { + return LV2_ICONS[name] || FaCubes; + } + return FaLightbulb; +}; + +// 脉冲动画 +const pulseKeyframes = keyframes` + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.02); } +`; + +/** + * 单个热力图块组件 + */ +const HeatmapBlock = ({ item, onClick, size = 'normal' }) => { + const isMobile = useBreakpointValue({ base: true, md: false }); + const hasChange = item.avg_change_pct !== null && item.avg_change_pct !== undefined; + const isPositive = hasChange && item.avg_change_pct > 0; + const isNegative = hasChange && item.avg_change_pct < 0; + const isLargeChange = hasChange && Math.abs(item.avg_change_pct) > 3; + + const IconComponent = getIcon(item.name, item.level); + const baseColor = item.parentLv1 + ? LV1_BASE_COLORS[item.parentLv1] + : LV1_BASE_COLORS[item.name]; + + // 根据 size 调整高度 + const heightMap = { + large: { base: '140px', md: '180px' }, + normal: { base: '120px', md: '150px' }, + small: { base: '100px', md: '120px' }, + }; + + return ( + onClick(item)} + position="relative" + overflow="hidden" + minH={heightMap[size]} + transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)" + boxShadow="0 4px 15px rgba(0, 0, 0, 0.2)" + _hover={{ + transform: 'translateY(-4px) scale(1.02)', + boxShadow: '0 12px 30px rgba(0, 0, 0, 0.3)', + }} + animation={isLargeChange ? `${pulseKeyframes} 2s infinite` : 'none'} + > + {/* 背景装饰 */} + + + + {/* 内容 */} + + {/* 顶部:图标和名称 */} + + + + + + + {item.name} + + {item.concept_count && ( + + {item.concept_count} 个概念 + + )} + + + + {/* 底部:涨跌幅 */} + + + {item.stock_count && ( + + {item.stock_count} 只股票 + + )} + + + + {hasChange && ( + + )} + + {formatChangePercent(item.avg_change_pct)} + + + + + + {/* 可点击提示 */} + {(item.children?.length > 0 || item.concepts?.length > 0) && ( + + 点击展开 + + )} + + ); }; /** @@ -99,13 +347,11 @@ const HierarchyView = ({ const [isFullscreen, setIsFullscreen] = useState(false); // 钻取状态 - const [currentLevel, setCurrentLevel] = useState('lv1'); // 'lv1' | 'lv2' | 'lv3' + const [currentLevel, setCurrentLevel] = useState('lv1'); const [currentLv1, setCurrentLv1] = useState(null); const [currentLv2, setCurrentLv2] = useState(null); const [breadcrumbs, setBreadcrumbs] = useState([{ label: '全部分类', level: 'root' }]); - const chartRef = useRef(null); - const containerRef = useRef(null); const isMobile = useBreakpointValue({ base: true, md: false }); // 获取层级结构数据 @@ -151,7 +397,6 @@ const HierarchyView = ({ const data = await response.json(); - // 构建映射表 const lv1Map = {}; const lv2Map = {}; const lv3Map = {}; @@ -197,37 +442,28 @@ const HierarchyView = ({ const { lv1Map, lv2Map, lv3Map } = priceData; if (currentLevel === 'lv1') { - // 显示所有 lv1 - return hierarchy.map((lv1, index) => { + return hierarchy.map((lv1) => { const price = lv1Map[lv1.name] || {}; return { name: lv1.name, - value: lv1.concept_count || 10, id: lv1.id, level: 'lv1', concept_count: lv1.concept_count, stock_count: price.stock_count, avg_change_pct: price.avg_change_pct, children: lv1.children, - itemStyle: { - color: getLv1Color(lv1.name, 0), - borderColor: '#fff', - borderWidth: 2, - }, }; }); } if (currentLevel === 'lv2' && currentLv1) { - // 显示选中 lv1 下的 lv2 const lv1Data = hierarchy.find(h => h.name === currentLv1.name); if (!lv1Data || !lv1Data.children) return []; - return lv1Data.children.map((lv2, index) => { + return lv1Data.children.map((lv2) => { const price = lv2Map[lv2.name] || {}; return { name: lv2.name, - value: lv2.concept_count || 5, id: lv2.id, level: 'lv2', parentLv1: currentLv1.name, @@ -236,30 +472,22 @@ const HierarchyView = ({ avg_change_pct: price.avg_change_pct, children: lv2.children, concepts: lv2.concepts, - itemStyle: { - color: getLv1Color(currentLv1.name, index % 3), - borderColor: '#fff', - borderWidth: 2, - }, }; }); } if (currentLevel === 'lv3' && currentLv1 && currentLv2) { - // 显示选中 lv2 下的 lv3 或概念 const lv1Data = hierarchy.find(h => h.name === currentLv1.name); if (!lv1Data || !lv1Data.children) return []; const lv2Data = lv1Data.children.find(h => h.name === currentLv2.name); if (!lv2Data) return []; - // 如果有 lv3 子级 if (lv2Data.children && lv2Data.children.length > 0) { - return lv2Data.children.map((lv3, index) => { + return lv2Data.children.map((lv3) => { const price = lv3Map[lv3.name] || {}; return { name: lv3.name, - value: lv3.concept_count || 3, id: lv3.id, level: 'lv3', parentLv1: currentLv1.name, @@ -268,28 +496,16 @@ const HierarchyView = ({ stock_count: price.stock_count, avg_change_pct: price.avg_change_pct, concepts: lv3.concepts, - itemStyle: { - color: getLv1Color(currentLv1.name, index % 3), - borderColor: '#fff', - borderWidth: 2, - }, }; }); } - // 如果直接是概念列表 if (lv2Data.concepts && lv2Data.concepts.length > 0) { - return lv2Data.concepts.map((concept, index) => ({ + return lv2Data.concepts.map((concept) => ({ name: concept, - value: 1, level: 'concept', parentLv1: currentLv1.name, parentLv2: currentLv2.name, - itemStyle: { - color: getLv1Color(currentLv1.name, index % 3), - borderColor: '#fff', - borderWidth: 1, - }, })); } @@ -299,172 +515,31 @@ const HierarchyView = ({ return []; }, [hierarchy, priceData, currentLevel, currentLv1, currentLv2]); - // ECharts 配置 - const chartOption = useMemo(() => { - if (!currentData || currentData.length === 0) return null; - - return { - tooltip: { - trigger: 'item', - formatter: (params) => { - const data = params.data || {}; - let content = `
${data.name}
`; - - if (data.avg_change_pct !== undefined && data.avg_change_pct !== null) { - const color = getChangeColor(data.avg_change_pct); - content += `
平均涨跌: ${formatChangePercent(data.avg_change_pct)}
`; - } - - if (data.concept_count !== undefined) { - content += `
概念数量: ${data.concept_count}
`; - } - - if (data.stock_count !== undefined) { - content += `
成分股数: ${data.stock_count}
`; - } - - const levelMap = { lv1: '一级分类', lv2: '二级分类', lv3: '三级分类', concept: '概念' }; - if (data.level) { - content += `
${levelMap[data.level] || ''}
`; - } - - // 提示可点击 - if (data.level && data.level !== 'concept' && data.children) { - content += `
👆 点击查看下级分类
`; - } - - return content; - }, - backgroundColor: 'rgba(255, 255, 255, 0.98)', - borderColor: '#E5E7EB', - borderWidth: 1, - padding: [12, 16], - textStyle: { - color: '#1F2937', - fontSize: 12, - }, - extraCssText: 'box-shadow: 0 4px 20px rgba(0,0,0,0.15); border-radius: 8px;', - }, - series: [ - { - type: 'treemap', - data: currentData, - width: '100%', - height: '100%', - roam: false, - nodeClick: false, // 禁用默认钻取,使用自定义 - breadcrumb: { - show: false, // 使用自定义面包屑 - }, - label: { - show: true, - formatter: (params) => { - const data = params.data || {}; - const name = data.name || ''; - const changeStr = data.avg_change_pct !== undefined && data.avg_change_pct !== null - ? formatChangePercent(data.avg_change_pct) - : ''; - - // 根据区块大小决定显示内容 - if (params.value < 3) { - return name.length > 4 ? name.slice(0, 4) + '...' : name; - } - - if (changeStr) { - return `{name|${name}}\n{change|${changeStr}}`; - } - return `{name|${name}}`; - }, - rich: { - name: { - fontSize: isMobile ? 12 : 14, - fontWeight: 'bold', - color: '#fff', - textShadowColor: 'rgba(0,0,0,0.3)', - textShadowBlur: 2, - lineHeight: isMobile ? 18 : 22, - }, - change: { - fontSize: isMobile ? 14 : 18, - fontWeight: 'bold', - color: '#fff', - textShadowColor: 'rgba(0,0,0,0.5)', - textShadowBlur: 3, - lineHeight: isMobile ? 20 : 26, - }, - }, - position: 'inside', - verticalAlign: 'middle', - align: 'center', - }, - upperLabel: { - show: false, - }, - itemStyle: { - borderColor: '#fff', - borderWidth: 3, - gapWidth: 3, - borderRadius: 4, - }, - emphasis: { - itemStyle: { - shadowBlur: 20, - shadowColor: 'rgba(0, 0, 0, 0.3)', - }, - label: { - fontSize: isMobile ? 14 : 16, - }, - }, - levels: [ - { - itemStyle: { - borderColor: '#fff', - borderWidth: 4, - gapWidth: 4, - }, - }, - ], - animationDuration: 500, - animationEasing: 'cubicOut', - }, - ], - }; - }, [currentData, isMobile]); - // 处理点击事件 - 钻取 - const handleChartClick = useCallback((params) => { - const data = params.data; - if (!data) return; + const handleBlockClick = useCallback((item) => { + logger.info('HierarchyView', '热力图点击', { level: item.level, name: item.name }); - logger.info('HierarchyView', '热力图点击', { level: data.level, name: data.name }); - - if (data.level === 'lv1' && data.children && data.children.length > 0) { - // 进入 lv2 + if (item.level === 'lv1' && item.children && item.children.length > 0) { setCurrentLevel('lv2'); - setCurrentLv1(data); + setCurrentLv1(item); setBreadcrumbs([ { label: '全部分类', level: 'root' }, - { label: data.name, level: 'lv1', data }, + { label: item.name, level: 'lv1', data: item }, ]); - } else if (data.level === 'lv2') { - // 检查是否有 lv3 或概念 - if ((data.children && data.children.length > 0) || (data.concepts && data.concepts.length > 0)) { + } else if (item.level === 'lv2') { + if ((item.children && item.children.length > 0) || (item.concepts && item.concepts.length > 0)) { setCurrentLevel('lv3'); - setCurrentLv2(data); + setCurrentLv2(item); setBreadcrumbs([ { label: '全部分类', level: 'root' }, { label: currentLv1.name, level: 'lv1', data: currentLv1 }, - { label: data.name, level: 'lv2', data }, + { label: item.name, level: 'lv2', data: item }, ]); } - } else if (data.level === 'lv3' || data.level === 'concept') { - // 最底层,可以触发筛选或者其他操作 - // 这里可以选择不做任何操作,或者提示用户 - logger.info('HierarchyView', '已到达最底层', { name: data.name }); } }, [currentLv1]); - // 面包屑导航 - 返回上级 + // 面包屑导航 const handleBreadcrumbClick = useCallback((crumb, index) => { if (crumb.level === 'root') { setCurrentLevel('lv1'); @@ -489,22 +564,28 @@ const HierarchyView = ({ setIsFullscreen(prev => !prev); }, []); - // 图表事件 - const chartEvents = useMemo(() => ({ - click: handleChartClick, - }), [handleChartClick]); - // 获取当前层级标题 const getCurrentTitle = () => { - if (currentLevel === 'lv1') return '一级分类概览'; - if (currentLevel === 'lv2' && currentLv1) return `${currentLv1.name} - 二级分类`; - if (currentLevel === 'lv3' && currentLv2) return `${currentLv2.name} - 三级分类`; + if (currentLevel === 'lv1') return '概念分类热力图'; + if (currentLevel === 'lv2' && currentLv1) return `${currentLv1.name}`; + if (currentLevel === 'lv3' && currentLv2) return `${currentLv2.name}`; return '概念分类'; }; + // 计算列数 + const getGridColumns = () => { + if (currentLevel === 'lv1') { + return { base: 2, md: 3, lg: 4 }; + } + if (currentLevel === 'lv2') { + return { base: 2, md: 3, lg: 4 }; + } + return { base: 2, md: 3, lg: 4 }; + }; + if (loading) { return ( -
+
正在加载概念层级... @@ -515,7 +596,7 @@ const HierarchyView = ({ if (error) { return ( -
+
加载失败:{error} @@ -527,9 +608,9 @@ const HierarchyView = ({ ); } - if (!chartOption) { + if (currentData.length === 0) { return ( -
+
暂无层级数据 @@ -540,41 +621,40 @@ const HierarchyView = ({ return ( {/* 工具栏 */} - - - + + + {getCurrentTitle()} {tradeDate && ( - + {tradeDate} )} {priceLoading && ( - + )} - {/* 刷新涨跌幅 */} - {/* 全屏切换 */} @@ -624,6 +704,7 @@ const HierarchyView = ({ onClick={() => handleBreadcrumbClick(crumb, index)} isDisabled={index === breadcrumbs.length - 1} fontWeight={index === breadcrumbs.length - 1 ? 'bold' : 'medium'} + borderRadius="lg" > {crumb.label} @@ -631,71 +712,69 @@ const HierarchyView = ({ ))} - {/* 操作提示 */} - - 👆 点击色块 查看下级分类 | - 区块大小表示概念数量 | - 红涨 - 绿跌 - - - {/* ECharts 热力图 */} - - - + + + + + + + + + + + + + | + 点击色块查看下级分类 + + + {/* 热力图网格 */} + + {currentData.map((item, index) => ( + + ))} + {/* 统计信息 */} - 当前显示 {currentData.length} 个{currentLevel === 'lv1' ? '一级' : currentLevel === 'lv2' ? '二级' : '三级'}分类 + 当前显示 {currentData.length} 个 + {currentLevel === 'lv1' ? '一级' : currentLevel === 'lv2' ? '二级' : '三级'}分类 {currentLevel === 'lv1' && ( - <> - - 共 {hierarchy.reduce((acc, h) => acc + (h.children?.length || 0), 0)} 个二级分类 - - - 共 {hierarchy.reduce((acc, h) => acc + h.concept_count, 0)} 个概念 - - + + 共 {hierarchy.reduce((acc, h) => acc + h.concept_count, 0)} 个概念 + )}