diff --git a/concept_api_v2.py b/concept_api_v2.py index 66ef32db..9e8ed2e9 100644 --- a/concept_api_v2.py +++ b/concept_api_v2.py @@ -755,7 +755,7 @@ async def get_hierarchy_price_early( trade_date: Optional[date] = Query(None, description="交易日期,默认最新"), lv1_filter: Optional[str] = Query(None, description="筛选特定一级分类") ): - """获取层级概念(lv1/lv2/lv3)的涨跌幅数据""" + """获取所有概念的涨跌幅数据(包含母概念lv1/lv2/lv3和叶子概念leaf)""" logger.info(f"[hierarchy/price] 请求参数: trade_date={trade_date}, lv1_filter={lv1_filter}") if not mysql_pool: @@ -768,22 +768,11 @@ async def get_hierarchy_price_early( # 获取交易日期 query_date = trade_date if query_date is None: - # 优先从母概念查最新日期 - await cursor.execute( - "SELECT MAX(trade_date) as max_date FROM concept_daily_stats WHERE concept_type IN ('lv1', 'lv2', 'lv3')" - ) + await cursor.execute("SELECT MAX(trade_date) as max_date FROM concept_daily_stats") result = await cursor.fetchone() - logger.info(f"[hierarchy/price] 查询母概念最新日期: {result}") - if result and result['max_date']: - query_date = result['max_date'] - else: - # 尝试从全部数据获取 - await cursor.execute("SELECT MAX(trade_date) as max_date FROM concept_daily_stats") - result = await cursor.fetchone() - logger.info(f"[hierarchy/price] 查询全部最新日期: {result}") - if not result or not result['max_date']: - raise HTTPException(status_code=404, detail="无涨跌幅数据") - query_date = result['max_date'] + if not result or not result['max_date']: + raise HTTPException(status_code=404, detail="无涨跌幅数据") + query_date = result['max_date'] logger.info(f"[hierarchy/price] 使用查询日期: {query_date}") @@ -802,15 +791,14 @@ async def get_hierarchy_price_early( lv1_concepts = [] lv2_concepts = [] lv3_concepts = [] + leaf_concepts = [] # 查询 lv1 if lv1_filter: await cursor.execute(base_query, (query_date, 'lv1', f"%{lv1_filter}%")) else: await cursor.execute(base_query, (query_date, 'lv1')) - lv1_rows = await cursor.fetchall() - logger.info(f"[hierarchy/price] lv1查询结果数量: {len(lv1_rows)}") - for row in lv1_rows: + for row in await cursor.fetchall(): lv1_concepts.append({ "concept_id": row['concept_id'], "concept_name": row['concept_name'], @@ -825,9 +813,7 @@ async def get_hierarchy_price_early( await cursor.execute(base_query, (query_date, 'lv2', f"%{lv1_filter}%")) else: await cursor.execute(base_query, (query_date, 'lv2')) - lv2_rows = await cursor.fetchall() - logger.info(f"[hierarchy/price] lv2查询结果数量: {len(lv2_rows)}") - for row in lv2_rows: + for row in await cursor.fetchall(): lv2_concepts.append({ "concept_id": row['concept_id'], "concept_name": row['concept_name'], @@ -842,9 +828,7 @@ async def get_hierarchy_price_early( await cursor.execute(base_query, (query_date, 'lv3', f"%{lv1_filter}%")) else: await cursor.execute(base_query, (query_date, 'lv3')) - lv3_rows = await cursor.fetchall() - logger.info(f"[hierarchy/price] lv3查询结果数量: {len(lv3_rows)}") - for row in lv3_rows: + for row in await cursor.fetchall(): lv3_concepts.append({ "concept_id": row['concept_id'], "concept_name": row['concept_name'], @@ -854,13 +838,37 @@ async def get_hierarchy_price_early( "stock_count": row['stock_count'] }) - logger.info(f"[hierarchy/price] 返回结果: lv1={len(lv1_concepts)}, lv2={len(lv2_concepts)}, lv3={len(lv3_concepts)}") + # 查询叶子概念 leaf + await cursor.execute(base_query, (query_date, 'leaf')) + for row in await cursor.fetchall(): + # 获取层级信息 + hierarchy_info = get_concept_hierarchy(row['concept_name']) + leaf_concepts.append({ + "concept_id": row['concept_id'], + "concept_name": row['concept_name'], + "concept_type": 'leaf', + "trade_date": str(row['trade_date']), + "avg_change_pct": float(row['avg_change_pct']) if row['avg_change_pct'] else None, + "stock_count": row['stock_count'], + "hierarchy": hierarchy_info + }) + + total = len(lv1_concepts) + len(lv2_concepts) + len(lv3_concepts) + len(leaf_concepts) + logger.info(f"[hierarchy/price] 返回结果: lv1={len(lv1_concepts)}, lv2={len(lv2_concepts)}, lv3={len(lv3_concepts)}, leaf={len(leaf_concepts)}") + return { "trade_date": str(query_date), "lv1_concepts": lv1_concepts, "lv2_concepts": lv2_concepts, "lv3_concepts": lv3_concepts, - "total_count": len(lv1_concepts) + len(lv2_concepts) + len(lv3_concepts) + "leaf_concepts": leaf_concepts, + "total_count": total, + "summary": { + "lv1_count": len(lv1_concepts), + "lv2_count": len(lv2_concepts), + "lv3_count": len(lv3_concepts), + "leaf_count": len(leaf_concepts) + } } except HTTPException as he: diff --git a/src/views/Concept/components/HierarchyView.js b/src/views/Concept/components/HierarchyView.js index 1342bae1..c7281814 100644 --- a/src/views/Concept/components/HierarchyView.js +++ b/src/views/Concept/components/HierarchyView.js @@ -1,13 +1,12 @@ /** * HierarchyView - 概念层级热力图视图 * - * 使用 CSS Grid + Chakra UI 实现热力图效果 + * Modern Spatial & Glassmorphism 设计风格 * 特性: - * 1. 炫酷的矩形热力图展示,涨红跌绿背景色 - * 2. 点击 lv1 进入 lv2,点击 lv2 进入 lv3,点击 lv3 进入具体概念 - * 3. 集成 /hierarchy/price 接口获取实时涨跌幅 - * 4. 每个分类有独特图标 - * 5. 支持面包屑导航返回上级 + * 1. 毛玻璃卡片 + 极光背景 + * 2. 涨红跌绿渐变色 + * 3. 支持 lv1 → lv2 → lv3 → leaf 四层钻取 + * 4. 集成 /hierarchy/price 接口(含 leaf_concepts) */ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { @@ -48,7 +47,6 @@ import { FaCoins, FaHeartbeat, FaAtom, - FaLightbulb, FaArrowUp, FaArrowDown, FaCubes, @@ -61,6 +59,7 @@ import { FaBatteryFull, FaSolarPanel, FaTags, + FaExternalLinkAlt, } from 'react-icons/fa'; import { logger } from '../../../utils/logger'; @@ -111,27 +110,36 @@ const LV2_ICONS = { '军贸出海': FaGlobe, }; -// 根据涨跌幅获取背景色(统一涨红跌绿) -const getChangeBgColor = (value) => { +// 根据涨跌幅获取背景渐变色(涨红跌绿) +const getChangeGradient = (value) => { if (value === null || value === undefined) { - // 无数据时使用灰色 - return 'linear-gradient(135deg, #4B5563 0%, #6B7280 100%)'; + 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, #7F1D1D 0%, #991B1B 100%)'; - if (value > 5) return 'linear-gradient(135deg, #991B1B 0%, #B91C1C 100%)'; - if (value > 3) return 'linear-gradient(135deg, #B91C1C 0%, #DC2626 100%)'; - if (value > 1) return 'linear-gradient(135deg, #DC2626 0%, #EF4444 100%)'; - if (value > 0) return 'linear-gradient(135deg, #EF4444 0%, #F87171 100%)'; - if (value < -7) return 'linear-gradient(135deg, #14532D 0%, #166534 100%)'; - if (value < -5) return 'linear-gradient(135deg, #166534 0%, #15803D 100%)'; - if (value < -3) return 'linear-gradient(135deg, #15803D 0%, #16A34A 100%)'; - if (value < -1) return 'linear-gradient(135deg, #16A34A 0%, #22C55E 100%)'; - if (value < 0) return 'linear-gradient(135deg, #22C55E 0%, #4ADE80 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, #4B5563 0%, #6B7280 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'; }; // 格式化涨跌幅 @@ -143,34 +151,90 @@ const formatChangePercent = (value) => { // 获取图标 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; // 具体概念用标签图标 + if (level === 'lv1') return LV1_ICONS[name] || FaLayerGroup; + if (level === 'lv2') return LV2_ICONS[name] || FaCubes; + if (level === 'lv3') return FaCubes; + return FaTags; }; -// 从 API 返回的名称中提取纯名称(去掉 [一级] [二级] [三级] 前缀) +// 从 API 返回的名称中提取纯名称 const extractPureName = (apiName) => { if (!apiName) return ''; return apiName.replace(/^\[(一级|二级|三级)\]\s*/, ''); }; -// 脉冲动画 -const pulseKeyframes = keyframes` - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.02); } +// 呼吸动画 +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 HeatmapBlock = ({ item, onClick, size = 'normal' }) => { +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; @@ -180,41 +244,64 @@ const HeatmapBlock = ({ item, onClick, size = 'normal' }) => { // 根据 size 调整高度 const heightMap = { - large: { base: '140px', md: '160px' }, - normal: { base: '110px', md: '130px' }, - small: { base: '90px', md: '100px' }, + 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)} - 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)" + transition="all 0.4s cubic-bezier(0.4, 0, 0.2, 1)" + transform="translateZ(0)" _hover={{ - transform: 'translateY(-4px) scale(1.02)', - boxShadow: '0 12px 30px rgba(0, 0, 0, 0.3)', + transform: 'translateY(-6px) scale(1.02)', }} - css={isLargeChange ? { animation: `${pulseKeyframes} 2s infinite` } : {}} + css={isLargeChange ? { animation: `${floatKeyframes} 3s ease-in-out infinite` } : {}} > - {/* 背景装饰 */} + {/* 毛玻璃主体 */} + + {/* 高光效果 */} + {/* 内容 */} @@ -222,47 +309,61 @@ const HeatmapBlock = ({ item, onClick, size = 'normal' }) => { direction="column" justify="space-between" h="100%" + p={{ base: 3, md: 4 }} position="relative" zIndex={1} > {/* 顶部:图标和名称 */} {item.name} {item.concept_count > 0 && ( - + {item.concept_count} 个概念 )} + + {/* 外链图标 */} + {isLeafConcept && ( + + )} {/* 底部:涨跌幅 */} {item.stock_count > 0 && ( - + {item.stock_count} 只股票 )} @@ -273,14 +374,15 @@ const HeatmapBlock = ({ item, onClick, size = 'normal' }) => { )} {formatChangePercent(item.avg_change_pct)} @@ -288,19 +390,23 @@ const HeatmapBlock = ({ item, onClick, size = 'normal' }) => { - {/* 可点击提示 */} + {/* 展开标识 */} {canDrillDown && ( - 点击展开 + 展开 )} @@ -318,12 +424,12 @@ const HierarchyView = ({ const [hierarchy, setHierarchy] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [priceData, setPriceData] = useState({ lv1Map: {}, lv2Map: {}, lv3Map: {} }); + const [priceData, setPriceData] = useState({ lv1Map: {}, lv2Map: {}, lv3Map: {}, leafMap: {} }); const [priceLoading, setPriceLoading] = useState(false); const [tradeDate, setTradeDate] = useState(null); const [isFullscreen, setIsFullscreen] = useState(false); - // 钻取状态 - 支持4层:lv1 -> lv2 -> lv3 -> concept + // 钻取状态 const [currentLevel, setCurrentLevel] = useState('lv1'); const [currentLv1, setCurrentLv1] = useState(null); const [currentLv2, setCurrentLv2] = useState(null); @@ -375,10 +481,11 @@ const HierarchyView = ({ const data = await response.json(); - // 构建映射表 - 使用纯名称作为 key(去掉 [一级] 等前缀) + // 构建映射表 const lv1Map = {}; const lv2Map = {}; const lv3Map = {}; + const leafMap = {}; (data.lv1_concepts || []).forEach(item => { const pureName = extractPureName(item.concept_name); @@ -392,16 +499,20 @@ const HierarchyView = ({ const pureName = extractPureName(item.concept_name); lv3Map[pureName] = item; }); + // 叶子概念 - 直接用名称 + (data.leaf_concepts || []).forEach(item => { + leafMap[item.concept_name] = item; + }); - setPriceData({ lv1Map, lv2Map, lv3Map }); + 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, - sampleLv1Keys: Object.keys(lv1Map).slice(0, 3), }); } catch (err) { logger.warn('HierarchyView', '获取层级涨跌幅失败', { error: err.message }); @@ -422,7 +533,7 @@ const HierarchyView = ({ // 根据当前层级获取显示数据 const currentData = useMemo(() => { - const { lv1Map, lv2Map, lv3Map } = priceData; + const { lv1Map, lv2Map, lv3Map, leafMap } = priceData; // 第一层:显示所有 lv1 if (currentLevel === 'lv1') { @@ -489,12 +600,17 @@ const HierarchyView = ({ // 如果 lv2 直接包含概念(没有 lv3) if (lv2Data.concepts && lv2Data.concepts.length > 0) { - return lv2Data.concepts.map((concept) => ({ - name: concept, - level: 'concept', - parentLv1: currentLv1.name, - parentLv2: currentLv2.name, - })); + 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 []; @@ -511,13 +627,18 @@ const HierarchyView = ({ const lv3Data = lv2Data.children.find(h => h.name === currentLv3.name); if (!lv3Data || !lv3Data.concepts) return []; - return lv3Data.concepts.map((concept) => ({ - name: concept, - level: 'concept', - parentLv1: currentLv1.name, - parentLv2: currentLv2.name, - parentLv3: currentLv3.name, - })); + 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 []; @@ -528,7 +649,6 @@ const HierarchyView = ({ logger.info('HierarchyView', '热力图点击', { level: item.level, name: item.name }); if (item.level === 'lv1' && item.children && item.children.length > 0) { - // 进入 lv2 setCurrentLevel('lv2'); setCurrentLv1(item); setBreadcrumbs([ @@ -536,9 +656,7 @@ const HierarchyView = ({ { label: item.name, level: 'lv1', data: item }, ]); } else if (item.level === 'lv2') { - // 检查是否有 lv3 或直接是概念 if (item.children && item.children.length > 0) { - // 进入 lv3 setCurrentLevel('lv3'); setCurrentLv2(item); setBreadcrumbs([ @@ -547,8 +665,7 @@ const HierarchyView = ({ { label: item.name, level: 'lv2', data: item }, ]); } else if (item.concepts && item.concepts.length > 0) { - // lv2 直接包含概念,进入概念层 - setCurrentLevel('lv3'); // 实际显示概念 + setCurrentLevel('lv3'); setCurrentLv2(item); setBreadcrumbs([ { label: '全部分类', level: 'root' }, @@ -557,7 +674,6 @@ const HierarchyView = ({ ]); } } else if (item.level === 'lv3' && item.concepts && item.concepts.length > 0) { - // 进入具体概念层 setCurrentLevel('concept'); setCurrentLv3(item); setBreadcrumbs([ @@ -567,7 +683,7 @@ const HierarchyView = ({ { 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'); } @@ -595,12 +711,12 @@ const HierarchyView = ({ } }, [breadcrumbs]); - // 刷新涨跌幅数据 + // 刷新 const handleRefreshPrice = useCallback(() => { fetchHierarchyPrice(); }, [fetchHierarchyPrice]); - // 切换全屏 + // 全屏切换 const toggleFullscreen = useCallback(() => { setIsFullscreen(prev => !prev); }, []); @@ -628,17 +744,18 @@ const HierarchyView = ({ // 计算列数 const getGridColumns = () => { if (currentLevel === 'concept') { - return { base: 2, md: 4, lg: 5 }; // 概念层更多列 + return { base: 2, md: 3, lg: 4 }; } return { base: 2, md: 3, lg: 4 }; }; if (loading) { return ( -
- - - 正在加载概念层级... +
+ + + + 正在加载概念层级...
); @@ -646,11 +763,18 @@ const HierarchyView = ({ if (error) { return ( -
- - - 加载失败:{error} - @@ -660,10 +784,11 @@ const HierarchyView = ({ if (currentData.length === 0) { return ( -
- - - 暂无层级数据 +
+ + + + 暂无层级数据
); @@ -677,170 +802,204 @@ const HierarchyView = ({ right={isFullscreen ? 0 : 'auto'} bottom={isFullscreen ? 0 : 'auto'} zIndex={isFullscreen ? 1000 : 'auto'} - bg={isFullscreen ? 'gray.50' : 'transparent'} - p={isFullscreen ? 4 : 0} - overflow={isFullscreen ? 'auto' : 'visible'} + bg="slate.950" + bgGradient="linear(to-br, gray.900, slate.900, gray.900)" + p={{ base: 3, md: 5 }} + borderRadius={isFullscreen ? '0' : '3xl'} + overflow={isFullscreen ? 'auto' : 'hidden'} + minH="500px" > - {/* 工具栏 */} - - - - - - {getCurrentTitle()} - - - {getLevelDesc()} · {currentData.length} 项 - - - {tradeDate && ( - - {tradeDate} - - )} - {priceLoading && ( - - )} - + {/* 极光背景 */} + - - - } - onClick={handleRefreshPrice} - isLoading={priceLoading} - variant="outline" - colorScheme="blue" - aria-label="刷新涨跌幅" - /> - - - - : } - onClick={toggleFullscreen} - variant="outline" - colorScheme="gray" - aria-label={isFullscreen ? '退出全屏' : '全屏'} - /> - - - - - {/* 面包屑导航 */} - - {breadcrumbs.map((crumb, index) => ( - - {index > 0 && ( - - )} - - - ))} - - - {/* 图例说明 */} - - - - - - - - - - - - 平/无数据 - - {currentLevel !== 'concept' && ( - <> - | - 点击色块查看下级 - - )} - {currentLevel === 'concept' && ( - <> - | - 点击查看概念详情 - - )} - - - {/* 热力图网格 */} - - {currentData.map((item) => ( - - ))} - - - {/* 统计信息 */} - - + {/* 工具栏 */} + + + + + + + + {getCurrentTitle()} + + + + {getLevelDesc()} · {currentData.length} 项 + + {tradeDate && ( + + {tradeDate} + + )} + + + {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 ? '退出全屏' : '全屏'} + /> + + + + + {/* 面包屑导航 */} + + {breadcrumbs.map((crumb, index) => ( + + {index > 0 && ( + + )} + + + ))} + + + {/* 图例说明 */} + + + + + + + + + + + + 平/无数据 + + | + + {currentLevel !== 'concept' ? '点击色块查看下级' : '点击查看概念详情'} + + + + {/* 热力图网格 */} + + {currentData.map((item) => ( + + ))} + + + {/* 统计信息 */} + - 当前显示 {currentData.length} 个{getLevelDesc()} - - {currentLevel === 'lv1' && ( - 共 {hierarchy.reduce((acc, h) => acc + h.concept_count, 0)} 个概念 + 当前显示 {currentData.length} 个{getLevelDesc()} - )} - + {currentLevel === 'lv1' && ( + + 共 {hierarchy.reduce((acc, h) => acc + h.concept_count, 0)} 个概念 + + )} + + ); };