/** * HierarchyView - 概念层级热力图视图 * * Modern Spatial & Glassmorphism 设计风格 * 特性: * 1. 毛玻璃卡片 + 极光背景 * 2. 涨红跌绿渐变色 * 3. 支持 lv1 → lv2 → lv3 → leaf 四层钻取 * 4. 集成 /hierarchy/price 接口(含 leaf_concepts) */ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Box, VStack, HStack, Text, Badge, Icon, Spinner, Center, Flex, Button, useBreakpointValue, Tooltip, IconButton, SimpleGrid, } from '@chakra-ui/react'; import { keyframes } from '@emotion/react'; import { FaLayerGroup, FaExpand, FaCompress, FaSync, FaHome, FaChevronRight, FaBrain, FaMicrochip, FaRobot, FaMobileAlt, FaCar, FaBolt, FaRocket, FaShieldAlt, FaGlobe, FaIndustry, FaShoppingCart, FaCoins, FaHeartbeat, FaAtom, FaArrowUp, FaArrowDown, FaCubes, FaServer, FaCode, FaMagic, FaEye, FaPlane, FaSatellite, FaBatteryFull, FaSolarPanel, FaTags, FaExternalLinkAlt, } from 'react-icons/fa'; import { logger } from '../../../utils/logger'; // 一级分类图标映射 const LV1_ICONS = { '人工智能': FaBrain, '半导体': FaMicrochip, '机器人': FaRobot, '消费电子': FaMobileAlt, '智能驾驶与汽车': FaCar, '新能源与电力': FaBolt, '空天经济': FaRocket, '国防军工': FaShieldAlt, '政策与主题': FaGlobe, '周期与材料': FaIndustry, '大消费': FaShoppingCart, '数字经济与金融科技': FaCoins, '全球宏观与贸易': FaGlobe, '医药健康': FaHeartbeat, '前沿科技': FaAtom, }; // 二级分类图标映射 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 getChangeGradient = (value) => { if (value === null || value === undefined) { return 'linear-gradient(135deg, rgba(71, 85, 105, 0.6) 0%, rgba(100, 116, 139, 0.4) 100%)'; } // 涨 - 红色系 if (value > 7) return 'linear-gradient(135deg, rgba(153, 27, 27, 0.8) 0%, rgba(220, 38, 38, 0.6) 100%)'; if (value > 5) return 'linear-gradient(135deg, rgba(185, 28, 28, 0.75) 0%, rgba(239, 68, 68, 0.55) 100%)'; if (value > 3) return 'linear-gradient(135deg, rgba(220, 38, 38, 0.7) 0%, rgba(248, 113, 113, 0.5) 100%)'; if (value > 1) return 'linear-gradient(135deg, rgba(239, 68, 68, 0.65) 0%, rgba(252, 165, 165, 0.45) 100%)'; if (value > 0) return 'linear-gradient(135deg, rgba(248, 113, 113, 0.6) 0%, rgba(254, 202, 202, 0.4) 100%)'; // 跌 - 绿色系 if (value < -7) return 'linear-gradient(135deg, rgba(20, 83, 45, 0.8) 0%, rgba(22, 101, 52, 0.6) 100%)'; if (value < -5) return 'linear-gradient(135deg, rgba(22, 101, 52, 0.75) 0%, rgba(21, 128, 61, 0.55) 100%)'; if (value < -3) return 'linear-gradient(135deg, rgba(21, 128, 61, 0.7) 0%, rgba(22, 163, 74, 0.5) 100%)'; if (value < -1) return 'linear-gradient(135deg, rgba(22, 163, 74, 0.65) 0%, rgba(74, 222, 128, 0.45) 100%)'; if (value < 0) return 'linear-gradient(135deg, rgba(34, 197, 94, 0.6) 0%, rgba(134, 239, 172, 0.4) 100%)'; // 平盘 return 'linear-gradient(135deg, rgba(71, 85, 105, 0.6) 0%, rgba(100, 116, 139, 0.4) 100%)'; }; // 获取涨跌幅文字颜色 const getChangeTextColor = (value) => { if (value === null || value === undefined) return 'gray.300'; if (value > 0) return '#FCA5A5'; if (value < 0) return '#86EFAC'; return 'gray.300'; }; // 格式化涨跌幅 const formatChangePercent = (value) => { if (value === null || value === undefined) return '--'; const formatted = Math.abs(value).toFixed(2); return value > 0 ? `+${formatted}%` : value < 0 ? `-${formatted}%` : '0.00%'; }; // 获取图标 const getIcon = (name, level) => { if (level === 'lv1') return LV1_ICONS[name] || FaLayerGroup; if (level === 'lv2') return LV2_ICONS[name] || FaCubes; if (level === 'lv3') return FaCubes; return FaTags; }; // 从 API 返回的名称中提取纯名称 const extractPureName = (apiName) => { if (!apiName) return ''; return apiName.replace(/^\[(一级|二级|三级)\]\s*/, ''); }; // 呼吸动画 const breatheKeyframes = keyframes` 0%, 100% { opacity: 0.3; transform: scale(1); } 50% { opacity: 0.5; transform: scale(1.05); } `; // 浮动动画 const floatKeyframes = keyframes` 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } `; /** * 极光背景组件 */ const AuroraBackground = () => ( {/* 紫色光斑 - 左上 */} {/* 蓝色光斑 - 右下 */} {/* 青色光斑 - 中间 */} ); /** * 毛玻璃卡片组件 */ const GlassCard = ({ item, onClick, size = 'normal' }) => { 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); // 根据 size 调整高度 const heightMap = { large: { base: '150px', md: '170px' }, normal: { base: '120px', md: '140px' }, small: { base: '100px', md: '110px' }, }; // 是否可点击进入下一层 const canDrillDown = item.level !== 'concept' && (item.children?.length > 0 || item.concepts?.length > 0); const isLeafConcept = item.level === 'concept'; return ( onClick(item)} transition="all 0.4s cubic-bezier(0.4, 0, 0.2, 1)" transform="translateZ(0)" _hover={{ transform: 'translateY(-6px) scale(1.02)', }} css={isLargeChange ? { animation: `${floatKeyframes} 3s ease-in-out infinite` } : {}} > {/* 毛玻璃主体 */} {/* 高光效果 */} {/* 内容 */} {/* 顶部:图标和名称 */} {item.name} {item.concept_count > 0 && ( {item.concept_count} 个概念 )} {/* 外链图标 */} {isLeafConcept && ( )} {/* 底部:涨跌幅 */} {item.stock_count > 0 && ( {item.stock_count} 只股票 )} {hasChange && (isPositive || isNegative) && ( )} {formatChangePercent(item.avg_change_pct)} {/* 展开标识 */} {canDrillDown && ( 展开 )} ); }; /** * 主组件:层级热力图视图 */ const HierarchyView = ({ apiBaseUrl, onSelectCategory, selectedDate, }) => { const [hierarchy, setHierarchy] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [priceData, setPriceData] = useState({ lv1Map: {}, lv2Map: {}, lv3Map: {}, leafMap: {} }); const [priceLoading, setPriceLoading] = useState(false); const [tradeDate, setTradeDate] = useState(null); const [isFullscreen, setIsFullscreen] = useState(false); // 钻取状态 const [currentLevel, setCurrentLevel] = useState('lv1'); const [currentLv1, setCurrentLv1] = useState(null); const [currentLv2, setCurrentLv2] = useState(null); const [currentLv3, setCurrentLv3] = useState(null); const [breadcrumbs, setBreadcrumbs] = useState([{ label: '全部分类', level: 'root' }]); const isMobile = useBreakpointValue({ base: true, md: false }); // 获取层级结构数据 const fetchHierarchy = useCallback(async () => { setLoading(true); setError(null); try { const response = await fetch(`${apiBaseUrl}/hierarchy`); if (!response.ok) throw new Error('获取层级结构失败'); const data = await response.json(); setHierarchy(data.hierarchy || []); logger.info('HierarchyView', '层级结构加载完成', { totalLv1: data.hierarchy?.length, totalConcepts: data.total_concepts }); } catch (err) { logger.error('HierarchyView', 'fetchHierarchy', err); setError(err.message); } finally { setLoading(false); } }, [apiBaseUrl]); // 获取层级涨跌幅数据 const fetchHierarchyPrice = useCallback(async () => { setPriceLoading(true); try { let url = `${apiBaseUrl}/hierarchy/price`; if (selectedDate) { const dateStr = selectedDate.toISOString().split('T')[0]; url += `?trade_date=${dateStr}`; } const response = await fetch(url); if (!response.ok) { logger.warn('HierarchyView', '获取层级涨跌幅失败', { status: response.status }); return; } const data = await response.json(); // 构建映射表 const lv1Map = {}; const lv2Map = {}; const lv3Map = {}; const leafMap = {}; (data.lv1_concepts || []).forEach(item => { const pureName = extractPureName(item.concept_name); lv1Map[pureName] = item; }); (data.lv2_concepts || []).forEach(item => { const pureName = extractPureName(item.concept_name); lv2Map[pureName] = item; }); (data.lv3_concepts || []).forEach(item => { const pureName = extractPureName(item.concept_name); lv3Map[pureName] = item; }); // 叶子概念 - 直接用名称 (data.leaf_concepts || []).forEach(item => { leafMap[item.concept_name] = item; }); setPriceData({ lv1Map, lv2Map, lv3Map, leafMap }); setTradeDate(data.trade_date); logger.info('HierarchyView', '层级涨跌幅加载完成', { lv1Count: Object.keys(lv1Map).length, lv2Count: Object.keys(lv2Map).length, lv3Count: Object.keys(lv3Map).length, leafCount: Object.keys(leafMap).length, tradeDate: data.trade_date, }); } catch (err) { logger.warn('HierarchyView', '获取层级涨跌幅失败', { error: err.message }); } finally { setPriceLoading(false); } }, [apiBaseUrl, selectedDate]); useEffect(() => { fetchHierarchy(); }, [fetchHierarchy]); useEffect(() => { if (hierarchy.length > 0) { fetchHierarchyPrice(); } }, [hierarchy, fetchHierarchyPrice]); // 根据当前层级获取显示数据 const currentData = useMemo(() => { const { lv1Map, lv2Map, lv3Map, leafMap } = priceData; // 第一层:显示所有 lv1 if (currentLevel === 'lv1') { return hierarchy.map((lv1) => { const price = lv1Map[lv1.name] || {}; return { name: lv1.name, 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, }; }); } // 第二层:显示选中 lv1 下的 lv2 if (currentLevel === 'lv2' && currentLv1) { const lv1Data = hierarchy.find(h => h.name === currentLv1.name); if (!lv1Data || !lv1Data.children) return []; return lv1Data.children.map((lv2) => { const price = lv2Map[lv2.name] || {}; return { name: lv2.name, id: lv2.id, level: 'lv2', parentLv1: currentLv1.name, concept_count: lv2.concept_count, stock_count: price.stock_count, avg_change_pct: price.avg_change_pct, children: lv2.children, concepts: lv2.concepts, }; }); } // 第三层:显示选中 lv2 下的 lv3 if (currentLevel === 'lv3' && currentLv1 && currentLv2) { 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) => { const price = lv3Map[lv3.name] || {}; return { name: lv3.name, id: lv3.id, level: 'lv3', parentLv1: currentLv1.name, parentLv2: currentLv2.name, concept_count: lv3.concept_count, stock_count: price.stock_count, avg_change_pct: price.avg_change_pct, concepts: lv3.concepts, }; }); } // 如果 lv2 直接包含概念(没有 lv3) if (lv2Data.concepts && lv2Data.concepts.length > 0) { return lv2Data.concepts.map((conceptName) => { const price = leafMap[conceptName] || {}; return { name: conceptName, level: 'concept', parentLv1: currentLv1.name, parentLv2: currentLv2.name, stock_count: price.stock_count, avg_change_pct: price.avg_change_pct, }; }); } return []; } // 第四层:显示选中 lv3 下的具体概念 if (currentLevel === 'concept' && currentLv1 && currentLv2 && currentLv3) { 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 || !lv2Data.children) return []; const lv3Data = lv2Data.children.find(h => h.name === currentLv3.name); if (!lv3Data || !lv3Data.concepts) return []; return lv3Data.concepts.map((conceptName) => { const price = leafMap[conceptName] || {}; return { name: conceptName, level: 'concept', parentLv1: currentLv1.name, parentLv2: currentLv2.name, parentLv3: currentLv3.name, stock_count: price.stock_count, avg_change_pct: price.avg_change_pct, }; }); } return []; }, [hierarchy, priceData, currentLevel, currentLv1, currentLv2, currentLv3]); // 处理点击事件 - 钻取 const handleBlockClick = useCallback((item) => { logger.info('HierarchyView', '热力图点击', { level: item.level, name: item.name }); if (item.level === 'lv1' && item.children && item.children.length > 0) { setCurrentLevel('lv2'); setCurrentLv1(item); setBreadcrumbs([ { label: '全部分类', level: 'root' }, { label: item.name, level: 'lv1', data: item }, ]); } else if (item.level === 'lv2') { if (item.children && item.children.length > 0) { setCurrentLevel('lv3'); setCurrentLv2(item); setBreadcrumbs([ { label: '全部分类', level: 'root' }, { label: currentLv1.name, level: 'lv1', data: currentLv1 }, { label: item.name, level: 'lv2', data: item }, ]); } else if (item.concepts && item.concepts.length > 0) { setCurrentLevel('lv3'); setCurrentLv2(item); setBreadcrumbs([ { label: '全部分类', level: 'root' }, { label: currentLv1.name, level: 'lv1', data: currentLv1 }, { label: item.name, level: 'lv2', data: item }, ]); } } else if (item.level === 'lv3' && item.concepts && item.concepts.length > 0) { setCurrentLevel('concept'); setCurrentLv3(item); setBreadcrumbs([ { label: '全部分类', level: 'root' }, { label: currentLv1.name, level: 'lv1', data: currentLv1 }, { label: currentLv2.name, level: 'lv2', data: currentLv2 }, { label: item.name, level: 'lv3', data: item }, ]); } else if (item.level === 'concept') { // 跳转到概念详情页 const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(item.name)}.html`; window.open(htmlPath, '_blank'); } }, [currentLv1, currentLv2]); // 面包屑导航 const handleBreadcrumbClick = useCallback((crumb, index) => { if (crumb.level === 'root') { setCurrentLevel('lv1'); setCurrentLv1(null); setCurrentLv2(null); setCurrentLv3(null); setBreadcrumbs([{ label: '全部分类', level: 'root' }]); } else if (crumb.level === 'lv1') { setCurrentLevel('lv2'); setCurrentLv1(crumb.data); setCurrentLv2(null); setCurrentLv3(null); setBreadcrumbs(breadcrumbs.slice(0, index + 1)); } else if (crumb.level === 'lv2') { setCurrentLevel('lv3'); setCurrentLv2(crumb.data); setCurrentLv3(null); setBreadcrumbs(breadcrumbs.slice(0, index + 1)); } }, [breadcrumbs]); // 刷新 const handleRefreshPrice = useCallback(() => { fetchHierarchyPrice(); }, [fetchHierarchyPrice]); // 全屏切换 const toggleFullscreen = useCallback(() => { setIsFullscreen(prev => !prev); }, []); // 获取当前层级标题 const getCurrentTitle = () => { if (currentLevel === 'lv1') return '概念分类热力图'; if (currentLevel === 'lv2' && currentLv1) return currentLv1.name; if (currentLevel === 'lv3' && currentLv2) return currentLv2.name; if (currentLevel === 'concept' && currentLv3) return currentLv3.name; return '概念分类'; }; // 获取当前层级描述 const getLevelDesc = () => { const levelNames = { lv1: '一级分类', lv2: '二级分类', lv3: '三级分类', concept: '具体概念', }; return levelNames[currentLevel] || ''; }; // 计算列数 const getGridColumns = () => { if (currentLevel === 'concept') { return { base: 2, md: 3, lg: 4 }; } return { base: 2, md: 3, lg: 4 }; }; if (loading) { return (
正在加载概念层级...
); } if (error) { return (
加载失败:{error}
); } if (currentData.length === 0) { return (
暂无层级数据
); } return ( {/* 极光背景 - 仅全屏时显示 */} {isFullscreen && } {/* 内容层 */} {/* 面包屑导航 + 工具栏(同一行) */} {/* 左侧:面包屑导航 */} {breadcrumbs.map((crumb, index) => ( {index > 0 && ( )} ))} {/* 右侧:工具栏按钮 */} {priceLoading && ( )} } onClick={handleRefreshPrice} isLoading={priceLoading} bg="whiteAlpha.100" color="white" border="1px solid" borderColor="whiteAlpha.200" _hover={{ bg: 'whiteAlpha.200' }} aria-label="刷新涨跌幅" /> : } onClick={toggleFullscreen} bg="whiteAlpha.100" color="white" border="1px solid" borderColor="whiteAlpha.200" _hover={{ bg: 'whiteAlpha.200' }} aria-label={isFullscreen ? '退出全屏' : '全屏'} /> {/* 图例说明 */} 平/无数据 | {currentLevel !== 'concept' ? '点击色块查看下级' : '点击查看概念详情'} {/* 热力图网格 */} {currentData.map((item) => ( ))} ); }; export default HierarchyView;