/** * TreemapView - 概念层级矩形树图(玻璃态深空风格) * * 设计理念: * - 半透明玻璃态数据终端,漂浮在深空中 * - 极光背景 + 毛玻璃卡片 * - 光影深度、弥散背景光、极致圆角 * * 特性: * 1. 默认显示3层(一级 → 二级 → 三级),面积按概念数量 * 2. 智能文字颜色:浅色背景用深色字,深色背景用浅色字 * 3. 点击矩形钻取进入,支持返回 * 4. 涨红跌绿颜色映射 */ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import ReactECharts from 'echarts-for-react'; import { Box, VStack, HStack, Text, Spinner, Center, Icon, Flex, Button, IconButton, Tooltip, Badge, useBreakpointValue, } from '@chakra-ui/react'; import { keyframes } from '@emotion/react'; import { FaLayerGroup, FaSync, FaExpand, FaCompress, FaHome, FaArrowUp, FaArrowDown, FaCircle, FaTh, FaChevronRight, FaArrowLeft, } from 'react-icons/fa'; import { logger } from '../../../utils/logger'; // 极光动画 - 黑金色主题 const auroraAnimation = keyframes` 0%, 100% { background-position: 0% 50%; filter: hue-rotate(0deg); } 25% { background-position: 50% 100%; filter: hue-rotate(10deg); } 50% { background-position: 100% 50%; filter: hue-rotate(0deg); } 75% { background-position: 50% 0%; filter: hue-rotate(-10deg); } `; // 光晕脉冲动画 const glowPulse = keyframes` 0%, 100% { opacity: 0.3; transform: scale(1); } 50% { opacity: 0.6; transform: scale(1.05); } `; // 一级分类颜色映射(基础色 - 半透明玻璃态) const LV1_COLORS = { '人工智能': '#8B5CF6', '半导体': '#3B82F6', '机器人': '#10B981', '消费电子': '#F59E0B', '智能驾驶与汽车': '#EF4444', '新能源与电力': '#06B6D4', '空天经济': '#6366F1', '国防军工': '#EC4899', '政策与主题': '#14B8A6', '周期与材料': '#F97316', '大消费': '#A855F7', '数字经济与金融科技': '#22D3EE', '全球宏观与贸易': '#84CC16', '医药健康': '#E879F9', '前沿科技': '#38BDF8', }; // 根据涨跌幅获取颜色(涨红跌绿 - 玻璃态半透明) const getChangeColor = (value, baseColor = '#64748B') => { if (value === null || value === undefined) return baseColor; // 涨 - 红色系(调整透明度使其更柔和) if (value > 7) return 'rgba(220, 38, 38, 0.85)'; // 深红 if (value > 5) return 'rgba(239, 68, 68, 0.8)'; if (value > 3) return 'rgba(248, 113, 113, 0.75)'; if (value > 1) return 'rgba(252, 165, 165, 0.7)'; if (value > 0) return 'rgba(254, 202, 202, 0.65)'; // 跌 - 绿色系 if (value < -7) return 'rgba(21, 128, 61, 0.85)'; // 深绿 if (value < -5) return 'rgba(22, 163, 74, 0.8)'; if (value < -3) return 'rgba(34, 197, 94, 0.75)'; if (value < -1) return 'rgba(74, 222, 128, 0.7)'; if (value < 0) return 'rgba(134, 239, 172, 0.65)'; return baseColor; }; // 判断颜色是否为浅色(用于决定文字颜色) const isLightColor = (color) => { if (!color) return false; // 处理 rgba 格式 const rgbaMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (rgbaMatch) { const [, r, g, b] = rgbaMatch.map(Number); // 使用相对亮度公式 const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance > 0.55; } // 处理 hex 格式 const hex = color.replace('#', ''); if (hex.length === 6) { const r = parseInt(hex.substr(0, 2), 16); const g = parseInt(hex.substr(2, 2), 16); const b = parseInt(hex.substr(4, 2), 16); const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance > 0.55; } return false; }; // 获取文字颜色(根据背景色自动适配) const getTextColor = (bgColor, isTitle = false) => { if (isLightColor(bgColor)) { return isTitle ? '#1E293B' : '#334155'; } return isTitle ? '#FFFFFF' : '#E2E8F0'; }; // 获取文字阴影(根据背景色自动适配) const getTextShadow = (bgColor) => { if (isLightColor(bgColor)) { return 'rgba(255,255,255,0.8)'; } return 'rgba(0,0,0,0.8)'; }; // 从 API 返回的名称中提取纯名称 const extractPureName = (apiName) => { if (!apiName) return ''; return apiName.replace(/^\[(一级|二级|三级)\]\s*/, ''); }; // 格式化涨跌幅 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 ForceGraphView = ({ 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 [isFullscreen, setIsFullscreen] = useState(false); // 钻取状态:记录当前查看的路径 const [drillPath, setDrillPath] = useState(null); const chartRef = useRef(); const containerRef = useRef(); 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('TreemapView', '层级结构加载完成', { totalLv1: data.hierarchy?.length, totalConcepts: data.total_concepts }); } catch (err) { logger.error('TreemapView', '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('TreemapView', '获取层级涨跌幅失败', { 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 }); } catch (err) { logger.warn('TreemapView', '获取层级涨跌幅失败', { error: err.message }); } finally { setPriceLoading(false); } }, [apiBaseUrl, selectedDate]); useEffect(() => { fetchHierarchy(); }, [fetchHierarchy]); useEffect(() => { if (hierarchy.length > 0) { fetchHierarchyPrice(); } }, [hierarchy, fetchHierarchyPrice]); // 递归计算概念数量 const getConceptCount = useCallback((node) => { if (!node) return 1; // 如果是叶子节点(概念),返回1 if (node.concepts && !node.children) { return node.concepts.length || 1; } // 如果有直接概念 let count = node.concepts?.length || 0; // 递归计算子节点 if (node.children) { node.children.forEach(child => { count += getConceptCount(child); }); } return count || 1; }, []); // 根据钻取路径构建 Treemap 数据(使用概念数量作为面积) const treemapData = useMemo(() => { const { lv1Map, lv2Map, lv3Map, leafMap } = priceData; // 根视图:显示所有一级,每个一级下显示二级和三级(不显示概念) if (!drillPath) { return hierarchy.map((lv1) => { const lv1BaseColor = LV1_COLORS[lv1.name] || '#8B5CF6'; const lv1Price = lv1Map[lv1.name] || {}; const bgColor = getChangeColor(lv1Price.avg_change_pct, lv1BaseColor); const lv1Node = { name: lv1.name, value: lv1.concept_count || getConceptCount(lv1), itemStyle: { color: bgColor, borderColor: 'rgba(255, 255, 255, 0.1)', borderWidth: 2, borderRadius: 12, }, data: { level: 'lv1', changePct: lv1Price.avg_change_pct, stockCount: lv1Price.stock_count, conceptCount: lv1.concept_count, baseColor: lv1BaseColor, bgColor: bgColor, }, children: [], }; // 二级 if (lv1.children) { lv1.children.forEach((lv2) => { const lv2Price = lv2Map[lv2.name] || {}; const lv2BgColor = getChangeColor(lv2Price.avg_change_pct, lv1BaseColor); const lv2ConceptCount = lv2.concept_count || getConceptCount(lv2); const lv2Node = { name: lv2.name, value: lv2ConceptCount, itemStyle: { color: lv2BgColor, borderColor: 'rgba(255, 255, 255, 0.08)', borderWidth: 1, borderRadius: 8, }, data: { level: 'lv2', parentLv1: lv1.name, changePct: lv2Price.avg_change_pct, stockCount: lv2Price.stock_count, conceptCount: lv2ConceptCount, baseColor: lv1BaseColor, bgColor: lv2BgColor, }, children: [], }; // 三级(不显示概念) if (lv2.children) { lv2.children.forEach((lv3) => { const lv3Price = lv3Map[lv3.name] || {}; const lv3BgColor = getChangeColor(lv3Price.avg_change_pct, lv1BaseColor); const lv3ConceptCount = lv3.concepts?.length || 1; lv2Node.children.push({ name: lv3.name, value: lv3ConceptCount, itemStyle: { color: lv3BgColor, borderColor: 'rgba(255, 255, 255, 0.05)', borderWidth: 1, borderRadius: 6, }, data: { level: 'lv3', parentLv1: lv1.name, parentLv2: lv2.name, changePct: lv3Price.avg_change_pct, stockCount: lv3Price.stock_count, conceptCount: lv3ConceptCount, baseColor: lv1BaseColor, bgColor: lv3BgColor, hasChildren: lv3.concepts && lv3.concepts.length > 0, }, }); }); } lv1Node.children.push(lv2Node); }); } return lv1Node; }); } // 钻取到某个一级分类 if (drillPath.lv1 && !drillPath.lv2) { const lv1 = hierarchy.find(h => h.name === drillPath.lv1); if (!lv1) return []; const lv1BaseColor = LV1_COLORS[lv1.name] || '#8B5CF6'; return (lv1.children || []).map((lv2) => { const lv2Price = lv2Map[lv2.name] || {}; const lv2BgColor = getChangeColor(lv2Price.avg_change_pct, lv1BaseColor); const lv2ConceptCount = lv2.concept_count || getConceptCount(lv2); const lv2Node = { name: lv2.name, value: lv2ConceptCount, itemStyle: { color: lv2BgColor, borderColor: 'rgba(255, 255, 255, 0.1)', borderWidth: 2, borderRadius: 12, }, data: { level: 'lv2', parentLv1: lv1.name, changePct: lv2Price.avg_change_pct, stockCount: lv2Price.stock_count, conceptCount: lv2ConceptCount, baseColor: lv1BaseColor, bgColor: lv2BgColor, }, children: [], }; // 三级 if (lv2.children) { lv2.children.forEach((lv3) => { const lv3Price = lv3Map[lv3.name] || {}; const lv3BgColor = getChangeColor(lv3Price.avg_change_pct, lv1BaseColor); const lv3ConceptCount = lv3.concepts?.length || 1; const lv3Node = { name: lv3.name, value: lv3ConceptCount, itemStyle: { color: lv3BgColor, borderColor: 'rgba(255, 255, 255, 0.08)', borderWidth: 1, borderRadius: 8, }, data: { level: 'lv3', parentLv1: lv1.name, parentLv2: lv2.name, changePct: lv3Price.avg_change_pct, stockCount: lv3Price.stock_count, conceptCount: lv3ConceptCount, baseColor: lv1BaseColor, bgColor: lv3BgColor, }, children: [], }; // 概念 if (lv3.concepts) { lv3.concepts.forEach((conceptName) => { const conceptPrice = leafMap[conceptName] || {}; const conceptBgColor = getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor); lv3Node.children.push({ name: conceptName, value: 1, itemStyle: { color: conceptBgColor, borderColor: 'rgba(255, 255, 255, 0.05)', borderWidth: 1, borderRadius: 6, }, data: { level: 'concept', parentLv1: lv1.name, parentLv2: lv2.name, parentLv3: lv3.name, changePct: conceptPrice.avg_change_pct, stockCount: conceptPrice.stock_count, baseColor: lv1BaseColor, bgColor: conceptBgColor, }, }); }); } lv2Node.children.push(lv3Node); }); } // lv2 直接包含的概念 if (lv2.concepts) { lv2.concepts.forEach((conceptName) => { const conceptPrice = leafMap[conceptName] || {}; const conceptBgColor = getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor); lv2Node.children.push({ name: conceptName, value: 1, itemStyle: { color: conceptBgColor, borderColor: 'rgba(255, 255, 255, 0.05)', borderWidth: 1, borderRadius: 6, }, data: { level: 'concept', parentLv1: lv1.name, parentLv2: lv2.name, changePct: conceptPrice.avg_change_pct, stockCount: conceptPrice.stock_count, baseColor: lv1BaseColor, bgColor: conceptBgColor, }, }); }); } return lv2Node; }); } // 钻取到某个二级分类 if (drillPath.lv1 && drillPath.lv2 && !drillPath.lv3) { const lv1 = hierarchy.find(h => h.name === drillPath.lv1); if (!lv1) return []; const lv2 = lv1.children?.find(c => c.name === drillPath.lv2); if (!lv2) return []; const lv1BaseColor = LV1_COLORS[lv1.name] || '#8B5CF6'; const result = []; // 三级 if (lv2.children) { lv2.children.forEach((lv3) => { const lv3Price = lv3Map[lv3.name] || {}; const lv3BgColor = getChangeColor(lv3Price.avg_change_pct, lv1BaseColor); const lv3ConceptCount = lv3.concepts?.length || 1; const lv3Node = { name: lv3.name, value: lv3ConceptCount, itemStyle: { color: lv3BgColor, borderColor: 'rgba(255, 255, 255, 0.1)', borderWidth: 2, borderRadius: 12, }, data: { level: 'lv3', parentLv1: lv1.name, parentLv2: lv2.name, changePct: lv3Price.avg_change_pct, stockCount: lv3Price.stock_count, conceptCount: lv3ConceptCount, baseColor: lv1BaseColor, bgColor: lv3BgColor, }, children: [], }; // 概念 if (lv3.concepts) { lv3.concepts.forEach((conceptName) => { const conceptPrice = leafMap[conceptName] || {}; const conceptBgColor = getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor); lv3Node.children.push({ name: conceptName, value: 1, itemStyle: { color: conceptBgColor, borderColor: 'rgba(255, 255, 255, 0.05)', borderWidth: 1, borderRadius: 8, }, data: { level: 'concept', parentLv1: lv1.name, parentLv2: lv2.name, parentLv3: lv3.name, changePct: conceptPrice.avg_change_pct, stockCount: conceptPrice.stock_count, baseColor: lv1BaseColor, bgColor: conceptBgColor, }, }); }); } result.push(lv3Node); }); } // lv2 直接包含的概念 if (lv2.concepts) { lv2.concepts.forEach((conceptName) => { const conceptPrice = leafMap[conceptName] || {}; const conceptBgColor = getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor); result.push({ name: conceptName, value: 1, itemStyle: { color: conceptBgColor, borderColor: 'rgba(255, 255, 255, 0.1)', borderWidth: 2, borderRadius: 12, }, data: { level: 'concept', parentLv1: lv1.name, parentLv2: lv2.name, changePct: conceptPrice.avg_change_pct, stockCount: conceptPrice.stock_count, baseColor: lv1BaseColor, bgColor: conceptBgColor, }, }); }); } return result; } // 钻取到某个三级分类 if (drillPath.lv1 && drillPath.lv2 && drillPath.lv3) { const lv1 = hierarchy.find(h => h.name === drillPath.lv1); if (!lv1) return []; const lv2 = lv1.children?.find(c => c.name === drillPath.lv2); if (!lv2) return []; const lv3 = lv2.children?.find(c => c.name === drillPath.lv3); if (!lv3) return []; const lv1BaseColor = LV1_COLORS[lv1.name] || '#8B5CF6'; return (lv3.concepts || []).map((conceptName) => { const conceptPrice = leafMap[conceptName] || {}; const conceptBgColor = getChangeColor(conceptPrice.avg_change_pct, lv1BaseColor); return { name: conceptName, value: 1, itemStyle: { color: conceptBgColor, borderColor: 'rgba(255, 255, 255, 0.1)', borderWidth: 2, borderRadius: 12, }, data: { level: 'concept', parentLv1: lv1.name, parentLv2: lv2.name, parentLv3: lv3.name, changePct: conceptPrice.avg_change_pct, stockCount: conceptPrice.stock_count, baseColor: lv1BaseColor, bgColor: conceptBgColor, }, }; }); } return []; }, [hierarchy, priceData, drillPath, getConceptCount]); // ECharts 配置 - 玻璃态深空风格 const chartOption = useMemo(() => { // 玻璃态层级配置 const levels = [ { // 根节点配置 itemStyle: { borderColor: 'transparent', borderWidth: 0, gapWidth: 4, }, }, { // 第一层 - 主要分类 itemStyle: { borderColor: 'rgba(255, 255, 255, 0.15)', borderWidth: 3, gapWidth: 4, borderRadius: 16, shadowBlur: 20, shadowColor: 'rgba(139, 92, 246, 0.3)', }, upperLabel: { show: true, height: 32, fontSize: 14, fontWeight: 'bold', padding: [0, 8], formatter: (params) => { const data = params.data?.data || {}; const changePct = data.changePct; const changeStr = changePct !== undefined && changePct !== null ? ` ${formatChangePercent(changePct)}` : ''; return `${params.name}${changeStr}`; }, rich: { name: { fontSize: 14, fontWeight: 'bold', } } }, color: (params) => { const bgColor = params.data?.data?.bgColor; return getTextColor(bgColor, true); }, }, { // 第二层 itemStyle: { borderColor: 'rgba(255, 255, 255, 0.1)', borderWidth: 2, gapWidth: 3, borderRadius: 12, shadowBlur: 10, shadowColor: 'rgba(139, 92, 246, 0.2)', }, upperLabel: { show: true, height: 26, fontSize: 12, fontWeight: 'bold', padding: [0, 6], 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: 'rgba(255, 255, 255, 0.08)', borderWidth: 1, gapWidth: 2, borderRadius: 8, }, label: { show: true, position: 'insideTopLeft', fontSize: 11, padding: [4, 6], formatter: (params) => { const data = params.data?.data || {}; const bgColor = data.bgColor; const changePct = data.changePct; if (changePct !== undefined && changePct !== null) { return `{name|${params.name}}\n{change|${formatChangePercent(changePct)}}`; } return params.name; }, rich: { name: { fontSize: 11, fontWeight: 'bold', lineHeight: 16, }, change: { fontSize: 10, lineHeight: 14, } } }, }, { // 第四层(概念层) itemStyle: { borderColor: 'rgba(255, 255, 255, 0.05)', borderWidth: 1, gapWidth: 2, borderRadius: 6, }, label: { show: true, position: 'insideTopLeft', fontSize: 10, padding: [3, 5], formatter: (params) => { const data = params.data?.data || {}; const changePct = data.changePct; if (changePct !== undefined && changePct !== null) { return `{name|${params.name}}\n{change|${formatChangePercent(changePct)}}`; } return params.name; }, rich: { name: { fontSize: 10, fontWeight: 'bold', lineHeight: 14, }, change: { fontSize: 9, lineHeight: 12, } } }, }, ]; return { backgroundColor: 'transparent', tooltip: { trigger: 'item', backgroundColor: 'rgba(15, 23, 42, 0.9)', borderColor: 'rgba(139, 92, 246, 0.4)', borderWidth: 1, borderRadius: 16, padding: [14, 18], extraCssText: 'backdrop-filter: blur(20px); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);', textStyle: { color: '#E2E8F0', fontSize: 13, }, formatter: (params) => { const data = params.data?.data || {}; const levelMap = { 'lv1': '一级分类', 'lv2': '二级分类', 'lv3': '三级分类', 'concept': '概念', }; const changePct = data.changePct; const changeColor = changePct > 0 ? '#F87171' : changePct < 0 ? '#4ADE80' : '#94A3B8'; const changeIcon = changePct > 0 ? '▲' : changePct < 0 ? '▼' : '●'; return `
${levelMap[data.level] || '分类'}
${params.name}
${changePct !== undefined && changePct !== null ? `
${changeIcon} ${formatChangePercent(changePct)}
` : ''}
${data.stockCount ? `📊 ${data.stockCount} 只股票` : ''} ${data.conceptCount ? `📁 ${data.conceptCount} 个概念` : ''}
${data.level === 'concept' ? `
🔗 点击查看概念详情
` : data.level !== 'concept' ? `
📂 点击进入查看子分类
` : ''}
`; }, }, series: [ { type: 'treemap', data: treemapData, left: 8, top: 60, right: 8, bottom: 50, roam: false, nodeClick: false, breadcrumb: { show: false, }, levels: levels, label: { show: true, formatter: '{b}', }, // 动态设置文字颜色 labelLayout: (params) => { return {}; }, itemStyle: { borderRadius: 8, }, emphasis: { itemStyle: { shadowBlur: 30, shadowColor: 'rgba(139, 92, 246, 0.5)', borderColor: 'rgba(255, 255, 255, 0.3)', }, }, animation: true, animationDuration: 600, animationEasing: 'cubicOut', }, ], // 使用 visualMap 来控制文字颜色 visualMap: { show: false, type: 'continuous', min: -10, max: 10, dimension: 'value', inRange: { // 这里可以添加颜色映射 }, }, }; }, [treemapData]); // 图表事件 const onChartEvents = useMemo(() => ({ click: (params) => { const data = params.data?.data || {}; if (data.level === 'concept') { // 跳转到概念详情页 const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(params.name)}.html`; window.open(htmlPath, '_blank'); return; } // 钻取进入 if (data.level === 'lv1') { setDrillPath({ lv1: params.name }); } else if (data.level === 'lv2') { setDrillPath({ lv1: data.parentLv1, lv2: params.name }); } else if (data.level === 'lv3') { setDrillPath({ lv1: data.parentLv1, lv2: data.parentLv2, lv3: params.name }); } logger.info('TreemapView', '钻取', { level: data.level, name: params.name }); }, }), []); // 返回上一层 const handleGoBack = useCallback(() => { if (!drillPath) return; if (drillPath.lv3) { setDrillPath({ lv1: drillPath.lv1, lv2: drillPath.lv2 }); } else if (drillPath.lv2) { setDrillPath({ lv1: drillPath.lv1 }); } else if (drillPath.lv1) { setDrillPath(null); } }, [drillPath]); // 返回根视图 const handleGoHome = useCallback(() => { setDrillPath(null); }, []); // 刷新数据 const handleRefresh = useCallback(() => { fetchHierarchyPrice(); }, [fetchHierarchyPrice]); // 全屏切换 const toggleFullscreen = useCallback(() => { setIsFullscreen(prev => !prev); }, []); // 获取面包屑 const breadcrumbItems = useMemo(() => { const items = [{ label: '全部分类', path: null }]; if (drillPath?.lv1) { items.push({ label: drillPath.lv1, path: { lv1: drillPath.lv1 } }); } if (drillPath?.lv2) { items.push({ label: drillPath.lv2, path: { lv1: drillPath.lv1, lv2: drillPath.lv2 } }); } if (drillPath?.lv3) { items.push({ label: drillPath.lv3, path: { lv1: drillPath.lv1, lv2: drillPath.lv2, lv3: drillPath.lv3 } }); } return items; }, [drillPath]); // 获取容器高度 const containerHeight = useMemo(() => { if (isFullscreen) return '100vh'; return isMobile ? '500px' : '700px'; }, [isFullscreen, isMobile]); if (loading) { return (
正在构建矩形树图...
); } if (error) { return (
加载失败:{error}
); } return ( {/* 极光背景层 */} {/* 弥散光晕层 */} {/* 顶部工具栏 - 毛玻璃风格 */} {/* 左侧标题和面包屑 */} {/* 返回按钮 */} {drillPath && ( } onClick={handleGoBack} bg="rgba(255, 255, 255, 0.1)" backdropFilter="blur(20px)" color="white" border="1px solid" borderColor="whiteAlpha.200" borderRadius="full" _hover={{ bg: 'rgba(139, 92, 246, 0.4)', borderColor: 'purple.400', transform: 'scale(1.05)', }} transition="all 0.2s" aria-label="返回" /> )} 概念矩形树图 {/* 面包屑导航 */} {breadcrumbItems.map((item, index) => ( {index > 0 && ( )} { if (index < breadcrumbItems.length - 1) { setDrillPath(item.path); } }} > {item.label} ))} {/* 右侧控制按钮 */} {priceLoading && } {drillPath && ( } onClick={handleGoHome} bg="rgba(255, 255, 255, 0.1)" backdropFilter="blur(20px)" color="white" border="1px solid" borderColor="whiteAlpha.200" borderRadius="full" _hover={{ bg: 'rgba(139, 92, 246, 0.4)', borderColor: 'purple.400', transform: 'scale(1.05)', }} transition="all 0.2s" aria-label="返回全部" /> )} } onClick={handleRefresh} isLoading={priceLoading} bg="rgba(255, 255, 255, 0.1)" backdropFilter="blur(20px)" color="white" border="1px solid" borderColor="whiteAlpha.200" borderRadius="full" _hover={{ bg: 'rgba(255, 255, 255, 0.2)', borderColor: 'whiteAlpha.300', transform: 'scale(1.05)', }} transition="all 0.2s" aria-label="刷新" /> : } onClick={toggleFullscreen} bg="rgba(255, 255, 255, 0.1)" backdropFilter="blur(20px)" color="white" border="1px solid" borderColor="whiteAlpha.200" borderRadius="full" _hover={{ bg: 'rgba(255, 255, 255, 0.2)', borderColor: 'whiteAlpha.300', transform: 'scale(1.05)', }} transition="all 0.2s" aria-label={isFullscreen ? '退出全屏' : '全屏'} /> {/* 底部图例 - 毛玻璃风格 */} {/* 操作提示 */} 点击分类进入 · 点击概念查看详情 {/* ECharts 图表 */} ); }; export default ForceGraphView;