update pay ui
This commit is contained in:
@@ -670,6 +670,87 @@ export const conceptHandlers = [
|
||||
});
|
||||
}),
|
||||
|
||||
// 获取层级涨跌幅数据(实时价格)
|
||||
http.get('/concept-api/hierarchy/price', async ({ request }) => {
|
||||
await delay(200);
|
||||
|
||||
const url = new URL(request.url);
|
||||
const tradeDate = url.searchParams.get('trade_date');
|
||||
|
||||
console.log('[Mock Concept] 获取层级涨跌幅数据:', { tradeDate });
|
||||
|
||||
// 模拟 lv1 层级涨跌幅数据
|
||||
const lv1_concepts = [
|
||||
{ concept_name: '人工智能', avg_change_pct: 3.56, stock_count: 245 },
|
||||
{ concept_name: '半导体', avg_change_pct: 2.12, stock_count: 156 },
|
||||
{ concept_name: '机器人', avg_change_pct: 4.28, stock_count: 128 },
|
||||
{ concept_name: '消费电子', avg_change_pct: 1.45, stock_count: 98 },
|
||||
{ concept_name: '智能驾驶与汽车', avg_change_pct: 2.89, stock_count: 112 },
|
||||
{ concept_name: '新能源与电力', avg_change_pct: -0.56, stock_count: 186 },
|
||||
{ concept_name: '空天经济', avg_change_pct: 3.12, stock_count: 76 },
|
||||
{ concept_name: '国防军工', avg_change_pct: 1.78, stock_count: 89 }
|
||||
];
|
||||
|
||||
// 模拟 lv2 层级涨跌幅数据
|
||||
const lv2_concepts = [
|
||||
// 人工智能下的 lv2
|
||||
{ concept_name: 'AI基础设施', avg_change_pct: 4.12, stock_count: 85 },
|
||||
{ concept_name: 'AI模型与软件', avg_change_pct: 5.67, stock_count: 42 },
|
||||
{ concept_name: 'AI应用', avg_change_pct: 2.34, stock_count: 65 },
|
||||
// 半导体下的 lv2
|
||||
{ concept_name: '半导体设备', avg_change_pct: 3.21, stock_count: 38 },
|
||||
{ concept_name: '半导体材料', avg_change_pct: 1.89, stock_count: 32 },
|
||||
{ concept_name: '芯片设计与制造', avg_change_pct: 2.45, stock_count: 56 },
|
||||
{ concept_name: '先进封装', avg_change_pct: 1.23, stock_count: 22 },
|
||||
// 机器人下的 lv2
|
||||
{ concept_name: '人形机器人整机', avg_change_pct: 5.89, stock_count: 45 },
|
||||
{ concept_name: '机器人核心零部件', avg_change_pct: 3.45, stock_count: 52 },
|
||||
{ concept_name: '其他类型机器人', avg_change_pct: 2.12, stock_count: 31 },
|
||||
// 消费电子下的 lv2
|
||||
{ concept_name: '智能终端', avg_change_pct: 1.78, stock_count: 28 },
|
||||
{ concept_name: 'XR与空间计算', avg_change_pct: 2.56, stock_count: 36 },
|
||||
{ concept_name: '华为产业链', avg_change_pct: 0.89, stock_count: 48 },
|
||||
// 智能驾驶下的 lv2
|
||||
{ concept_name: '自动驾驶解决方案', avg_change_pct: 4.23, stock_count: 35 },
|
||||
{ concept_name: '智能汽车产业链', avg_change_pct: 2.45, stock_count: 52 },
|
||||
{ concept_name: '车路协同', avg_change_pct: 1.56, stock_count: 25 },
|
||||
// 新能源下的 lv2
|
||||
{ concept_name: '新型电池技术', avg_change_pct: 0.67, stock_count: 62 },
|
||||
{ concept_name: '电力设备与电网', avg_change_pct: -1.23, stock_count: 78 },
|
||||
{ concept_name: '清洁能源', avg_change_pct: -0.45, stock_count: 46 },
|
||||
// 空天经济下的 lv2
|
||||
{ concept_name: '低空经济', avg_change_pct: 4.56, stock_count: 42 },
|
||||
{ concept_name: '商业航天', avg_change_pct: 1.89, stock_count: 34 },
|
||||
// 国防军工下的 lv2
|
||||
{ concept_name: '无人作战与信息化', avg_change_pct: 2.34, stock_count: 28 },
|
||||
{ concept_name: '海军装备', avg_change_pct: 1.45, stock_count: 32 },
|
||||
{ concept_name: '军贸出海', avg_change_pct: 1.12, stock_count: 18 }
|
||||
];
|
||||
|
||||
// 模拟 lv3 层级涨跌幅数据
|
||||
const lv3_concepts = [
|
||||
// AI基础设施下的 lv3
|
||||
{ concept_name: 'AI算力硬件', avg_change_pct: 5.23, stock_count: 32 },
|
||||
{ concept_name: 'AI关键组件', avg_change_pct: 3.89, stock_count: 45 },
|
||||
{ concept_name: 'AI配套设施', avg_change_pct: 2.67, stock_count: 28 },
|
||||
// AI应用下的 lv3
|
||||
{ concept_name: '智能体与陪伴', avg_change_pct: 3.12, stock_count: 24 },
|
||||
{ concept_name: '行业应用', avg_change_pct: 1.56, stock_count: 18 }
|
||||
];
|
||||
|
||||
// 计算交易日期(如果没有传入则使用今天)
|
||||
const today = tradeDate ? new Date(tradeDate) : new Date();
|
||||
const tradeDateStr = today.toISOString().split('T')[0];
|
||||
|
||||
return HttpResponse.json({
|
||||
trade_date: tradeDateStr,
|
||||
lv1_concepts,
|
||||
lv2_concepts,
|
||||
lv3_concepts,
|
||||
update_time: new Date().toISOString()
|
||||
});
|
||||
}),
|
||||
|
||||
// 获取指定层级的概念列表
|
||||
http.get('/concept-api/hierarchy/:lv1Id', async ({ params, request }) => {
|
||||
await delay(300);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/**
|
||||
* HierarchyView - 概念层级思维导图视图
|
||||
*
|
||||
* 使用 ECharts Tree 图表实现真正的思维导图效果
|
||||
* 支持径向布局(radial)和正交布局(orthogonal)
|
||||
* 使用 ECharts Tree 图表实现思维导图效果
|
||||
* 特性:
|
||||
* 1. 默认只展示前两层(根节点 + lv1),点击节点展开下层
|
||||
* 2. 集成 /hierarchy/price 接口获取实时涨跌幅
|
||||
* 3. 支持径向布局和正交布局切换
|
||||
*/
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import {
|
||||
@@ -29,6 +32,7 @@ import {
|
||||
FaExpand,
|
||||
FaCompress,
|
||||
FaRedo,
|
||||
FaSync,
|
||||
} from 'react-icons/fa';
|
||||
import { logger } from '../../../utils/logger';
|
||||
|
||||
@@ -66,17 +70,14 @@ const formatChangePercent = (value) => {
|
||||
|
||||
/**
|
||||
* 将后端层级数据转换为 ECharts tree 格式
|
||||
* @param {Array} hierarchy - 层级结构数据
|
||||
* @param {Object} priceData - 涨跌幅数据 { lv1Map, lv2Map, lv3Map }
|
||||
* @param {number} initialDepth - 初始展开深度 (1 = 只展示lv1, 2 = 展示到lv2)
|
||||
*/
|
||||
const transformToEChartsData = (hierarchy, stats) => {
|
||||
const transformToEChartsData = (hierarchy, priceData, initialDepth = 1) => {
|
||||
if (!hierarchy || hierarchy.length === 0) return null;
|
||||
|
||||
// 创建统计数据映射
|
||||
const statsMap = {};
|
||||
if (stats?.statistics) {
|
||||
stats.statistics.forEach(s => {
|
||||
statsMap[s.lv1] = s;
|
||||
});
|
||||
}
|
||||
const { lv1Map = {}, lv2Map = {}, lv3Map = {} } = priceData || {};
|
||||
|
||||
// 根节点
|
||||
const root = {
|
||||
@@ -87,24 +88,27 @@ const transformToEChartsData = (hierarchy, stats) => {
|
||||
borderWidth: 3,
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: '#1F2937',
|
||||
},
|
||||
children: hierarchy.map(lv1 => {
|
||||
const lv1Stats = statsMap[lv1.name] || {};
|
||||
const lv1Price = lv1Map[lv1.name] || {};
|
||||
const lv1Color = LV1_COLORS[lv1.name] || '#8B5CF6';
|
||||
const changeColor = getChangeColor(lv1Stats.avg_change_pct);
|
||||
const changeColor = getChangeColor(lv1Price.avg_change_pct);
|
||||
const changeStr = formatChangePercent(lv1Price.avg_change_pct);
|
||||
|
||||
return {
|
||||
name: lv1.name,
|
||||
value: lv1.concept_count,
|
||||
// 自定义数据用于点击事件
|
||||
collapsed: initialDepth < 2, // 初始是否折叠
|
||||
data: {
|
||||
type: 'lv1',
|
||||
id: lv1.id,
|
||||
name: lv1.name,
|
||||
concept_count: lv1.concept_count,
|
||||
avg_change_pct: lv1Stats.avg_change_pct,
|
||||
avg_change_pct: lv1Price.avg_change_pct,
|
||||
stock_count: lv1Price.stock_count,
|
||||
},
|
||||
itemStyle: {
|
||||
color: lv1Color,
|
||||
@@ -112,87 +116,103 @@ const transformToEChartsData = (hierarchy, stats) => {
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
fontSize: 13,
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
formatter: (params) => {
|
||||
const data = params.data?.data || {};
|
||||
const change = data.avg_change_pct;
|
||||
const changeStr = change !== undefined ? ` ${formatChangePercent(change)}` : '';
|
||||
return `{name|${params.name}}\n{count|${data.concept_count || 0}个}${changeStr ? `\n{change|${changeStr}}` : ''}`;
|
||||
color: '#1F2937',
|
||||
formatter: () => {
|
||||
let text = `${lv1.name}\n{count|${lv1.concept_count}个}`;
|
||||
if (changeStr) {
|
||||
text += `\n{change|${changeStr}}`;
|
||||
}
|
||||
return text;
|
||||
},
|
||||
rich: {
|
||||
name: {
|
||||
fontSize: 13,
|
||||
fontWeight: 'bold',
|
||||
color: '#1F2937',
|
||||
},
|
||||
count: {
|
||||
fontSize: 11,
|
||||
fontSize: 10,
|
||||
color: '#6B7280',
|
||||
lineHeight: 16,
|
||||
},
|
||||
change: {
|
||||
fontSize: 11,
|
||||
color: changeColor,
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
children: lv1.children?.map(lv2 => {
|
||||
const lv2Price = lv2Map[lv2.name] || {};
|
||||
const lv2ChangeColor = getChangeColor(lv2Price.avg_change_pct);
|
||||
const lv2ChangeStr = formatChangePercent(lv2Price.avg_change_pct);
|
||||
const hasLv3 = lv2.children && lv2.children.length > 0;
|
||||
|
||||
return {
|
||||
name: lv2.name,
|
||||
value: lv2.concept_count,
|
||||
collapsed: true, // lv2 默认折叠
|
||||
data: {
|
||||
type: 'lv2',
|
||||
id: lv2.id,
|
||||
name: lv2.name,
|
||||
parentLv1: lv1.name,
|
||||
concept_count: lv2.concept_count,
|
||||
avg_change_pct: lv2Price.avg_change_pct,
|
||||
stock_count: lv2Price.stock_count,
|
||||
},
|
||||
itemStyle: {
|
||||
color: lv1Color,
|
||||
opacity: 0.7,
|
||||
opacity: 0.8,
|
||||
},
|
||||
label: {
|
||||
fontSize: 11,
|
||||
formatter: `{b}\n(${lv2.concept_count})`,
|
||||
color: '#374151',
|
||||
formatter: () => {
|
||||
let text = `${lv2.name}\n{count|(${lv2.concept_count})}`;
|
||||
if (lv2ChangeStr) {
|
||||
text += ` {change|${lv2ChangeStr}}`;
|
||||
}
|
||||
return text;
|
||||
},
|
||||
rich: {
|
||||
count: {
|
||||
fontSize: 9,
|
||||
color: '#9CA3AF',
|
||||
},
|
||||
change: {
|
||||
fontSize: 10,
|
||||
color: lv2ChangeColor,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
},
|
||||
children: hasLv3 ? lv2.children.map(lv3 => ({
|
||||
name: lv3.name,
|
||||
value: lv3.concept_count,
|
||||
data: {
|
||||
type: 'lv3',
|
||||
id: lv3.id,
|
||||
children: hasLv3 ? lv2.children.map(lv3 => {
|
||||
const lv3Price = lv3Map[lv3.name] || {};
|
||||
const lv3ChangeStr = formatChangePercent(lv3Price.avg_change_pct);
|
||||
|
||||
return {
|
||||
name: lv3.name,
|
||||
parentLv1: lv1.name,
|
||||
parentLv2: lv2.name,
|
||||
concept_count: lv3.concept_count,
|
||||
concepts: lv3.concepts,
|
||||
},
|
||||
itemStyle: {
|
||||
color: lv1Color,
|
||||
opacity: 0.5,
|
||||
},
|
||||
label: {
|
||||
fontSize: 10,
|
||||
},
|
||||
})) : (lv2.concepts?.slice(0, 5).map(concept => ({
|
||||
name: concept,
|
||||
data: {
|
||||
type: 'concept',
|
||||
name: concept,
|
||||
parentLv1: lv1.name,
|
||||
parentLv2: lv2.name,
|
||||
},
|
||||
itemStyle: {
|
||||
color: lv1Color,
|
||||
opacity: 0.4,
|
||||
},
|
||||
label: {
|
||||
fontSize: 9,
|
||||
},
|
||||
})) || []),
|
||||
value: lv3.concept_count,
|
||||
collapsed: true,
|
||||
data: {
|
||||
type: 'lv3',
|
||||
id: lv3.id,
|
||||
name: lv3.name,
|
||||
parentLv1: lv1.name,
|
||||
parentLv2: lv2.name,
|
||||
concept_count: lv3.concept_count,
|
||||
concepts: lv3.concepts,
|
||||
},
|
||||
itemStyle: {
|
||||
color: lv1Color,
|
||||
opacity: 0.6,
|
||||
},
|
||||
label: {
|
||||
fontSize: 10,
|
||||
color: '#4B5563',
|
||||
formatter: `${lv3.name} (${lv3.concept_count})${lv3ChangeStr ? ' ' + lv3ChangeStr : ''}`,
|
||||
},
|
||||
};
|
||||
}) : undefined,
|
||||
};
|
||||
}) || [],
|
||||
};
|
||||
@@ -213,7 +233,9 @@ const HierarchyView = ({
|
||||
const [hierarchy, setHierarchy] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [hierarchyStats, setHierarchyStats] = useState(null);
|
||||
const [priceData, setPriceData] = useState({ lv1Map: {}, lv2Map: {}, lv3Map: {} });
|
||||
const [priceLoading, setPriceLoading] = useState(false);
|
||||
const [tradeDate, setTradeDate] = useState(null);
|
||||
const [layout, setLayout] = useState('radial'); // 'radial' | 'orthogonal'
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
|
||||
@@ -245,32 +267,69 @@ const HierarchyView = ({
|
||||
}
|
||||
}, [apiBaseUrl]);
|
||||
|
||||
// 获取层级统计数据(包含涨跌幅)
|
||||
const fetchHierarchyStats = useCallback(async () => {
|
||||
// 获取层级涨跌幅数据
|
||||
const fetchHierarchyPrice = useCallback(async () => {
|
||||
setPriceLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/statistics/hierarchy`);
|
||||
if (!response.ok) return;
|
||||
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();
|
||||
setHierarchyStats(data);
|
||||
|
||||
logger.info('HierarchyView', '层级统计加载完成', {
|
||||
totalLv1: data.total_lv1,
|
||||
totalConcepts: data.total_concepts
|
||||
// 构建映射表
|
||||
const lv1Map = {};
|
||||
const lv2Map = {};
|
||||
const lv3Map = {};
|
||||
|
||||
(data.lv1_concepts || []).forEach(item => {
|
||||
lv1Map[item.concept_name] = item;
|
||||
});
|
||||
(data.lv2_concepts || []).forEach(item => {
|
||||
lv2Map[item.concept_name] = item;
|
||||
});
|
||||
(data.lv3_concepts || []).forEach(item => {
|
||||
lv3Map[item.concept_name] = item;
|
||||
});
|
||||
|
||||
setPriceData({ lv1Map, lv2Map, lv3Map });
|
||||
setTradeDate(data.trade_date);
|
||||
|
||||
logger.info('HierarchyView', '层级涨跌幅加载完成', {
|
||||
lv1Count: Object.keys(lv1Map).length,
|
||||
lv2Count: Object.keys(lv2Map).length,
|
||||
lv3Count: Object.keys(lv3Map).length,
|
||||
tradeDate: data.trade_date
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn('HierarchyView', '获取层级统计失败', { error: err.message });
|
||||
logger.warn('HierarchyView', '获取层级涨跌幅失败', { error: err.message });
|
||||
} finally {
|
||||
setPriceLoading(false);
|
||||
}
|
||||
}, [apiBaseUrl]);
|
||||
}, [apiBaseUrl, selectedDate]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchHierarchy();
|
||||
fetchHierarchyStats();
|
||||
}, [fetchHierarchy, fetchHierarchyStats]);
|
||||
}, [fetchHierarchy]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hierarchy.length > 0) {
|
||||
fetchHierarchyPrice();
|
||||
}
|
||||
}, [hierarchy, fetchHierarchyPrice]);
|
||||
|
||||
// ECharts 配置
|
||||
const chartOption = useMemo(() => {
|
||||
const treeData = transformToEChartsData(hierarchy, hierarchyStats);
|
||||
const treeData = transformToEChartsData(hierarchy, priceData, 1);
|
||||
if (!treeData) return null;
|
||||
|
||||
const isRadial = layout === 'radial';
|
||||
@@ -281,30 +340,39 @@ const HierarchyView = ({
|
||||
triggerOn: 'mousemove',
|
||||
formatter: (params) => {
|
||||
const data = params.data?.data || {};
|
||||
let content = `<strong>${params.name}</strong>`;
|
||||
let content = `<div style="font-weight:bold;margin-bottom:4px">${params.name}</div>`;
|
||||
|
||||
if (data.concept_count !== undefined) {
|
||||
content += `<br/>概念数量: ${data.concept_count}`;
|
||||
content += `<div>概念数量: ${data.concept_count}</div>`;
|
||||
}
|
||||
if (data.stock_count !== undefined) {
|
||||
content += `<div>成分股数: ${data.stock_count}</div>`;
|
||||
}
|
||||
if (data.avg_change_pct !== undefined) {
|
||||
const color = data.avg_change_pct > 0 ? '#EF4444' : data.avg_change_pct < 0 ? '#22C55E' : '#9CA3AF';
|
||||
content += `<br/>平均涨跌: <span style="color:${color};font-weight:bold">${formatChangePercent(data.avg_change_pct)}</span>`;
|
||||
const color = getChangeColor(data.avg_change_pct);
|
||||
content += `<div>平均涨跌: <span style="color:${color};font-weight:bold">${formatChangePercent(data.avg_change_pct)}</span></div>`;
|
||||
}
|
||||
if (data.type) {
|
||||
const typeMap = { lv1: '一级分类', lv2: '二级分类', lv3: '三级分类', concept: '概念' };
|
||||
content += `<br/><span style="color:#9CA3AF">${typeMap[data.type] || ''}</span>`;
|
||||
const typeMap = { lv1: '一级分类', lv2: '二级分类', lv3: '三级分类' };
|
||||
content += `<div style="color:#9CA3AF;font-size:11px;margin-top:4px">${typeMap[data.type] || ''}</div>`;
|
||||
}
|
||||
|
||||
// 提示可点击
|
||||
if (data.type && data.type !== 'concept') {
|
||||
content += `<div style="color:#8B5CF6;font-size:11px;margin-top:4px">点击筛选该分类</div>`;
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.98)',
|
||||
borderColor: '#E5E7EB',
|
||||
borderWidth: 1,
|
||||
padding: [8, 12],
|
||||
padding: [10, 14],
|
||||
textStyle: {
|
||||
color: '#1F2937',
|
||||
fontSize: 12,
|
||||
},
|
||||
extraCssText: 'box-shadow: 0 4px 12px rgba(0,0,0,0.1);',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
@@ -315,21 +383,22 @@ const HierarchyView = ({
|
||||
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; // 根节点
|
||||
if (data.type === 'lv1') return 20;
|
||||
if (data.type === 'lv2') return 14;
|
||||
if (data.type === 'lv3') return 10;
|
||||
return 26; // 根节点
|
||||
},
|
||||
initialTreeDepth: 2,
|
||||
animationDuration: 550,
|
||||
animationDurationUpdate: 750,
|
||||
roam: true, // 启用缩放和平移
|
||||
initialTreeDepth: 2, // 初始展开到第2层(根 + lv1)
|
||||
animationDuration: 400,
|
||||
animationDurationUpdate: 500,
|
||||
roam: true,
|
||||
zoom: isMobile ? 0.7 : 0.9,
|
||||
center: isRadial ? ['50%', '50%'] : ['40%', '50%'],
|
||||
label: {
|
||||
position: isRadial ? 'radial' : 'right',
|
||||
verticalAlign: 'middle',
|
||||
align: isRadial ? undefined : 'left',
|
||||
fontSize: 12,
|
||||
fontSize: 11,
|
||||
distance: 8,
|
||||
},
|
||||
leaves: {
|
||||
@@ -340,7 +409,7 @@ const HierarchyView = ({
|
||||
},
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'descendant',
|
||||
focus: 'ancestor',
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
@@ -355,13 +424,15 @@ const HierarchyView = ({
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [hierarchy, hierarchyStats, layout]);
|
||||
}, [hierarchy, priceData, layout, isMobile]);
|
||||
|
||||
// 处理节点点击
|
||||
// 处理节点点击 - 区分展开/收起和筛选
|
||||
const handleChartClick = useCallback((params) => {
|
||||
const data = params.data?.data;
|
||||
if (!data) return;
|
||||
if (!data || !data.type) return;
|
||||
|
||||
// 只有在节点已展开且点击的情况下才触发筛选
|
||||
// ECharts tree 会自动处理展开/收起
|
||||
logger.info('HierarchyView', '节点点击', data);
|
||||
|
||||
let filter = { lv1: null, lv2: null, lv3: null };
|
||||
@@ -376,10 +447,6 @@ const HierarchyView = ({
|
||||
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;
|
||||
}
|
||||
@@ -391,12 +458,15 @@ const HierarchyView = ({
|
||||
const handleResetView = useCallback(() => {
|
||||
if (chartRef.current) {
|
||||
const chart = chartRef.current.getEchartsInstance();
|
||||
chart.dispatchAction({
|
||||
type: 'restore',
|
||||
});
|
||||
chart.dispatchAction({ type: 'restore' });
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 刷新涨跌幅数据
|
||||
const handleRefreshPrice = useCallback(() => {
|
||||
fetchHierarchyPrice();
|
||||
}, [fetchHierarchyPrice]);
|
||||
|
||||
// 切换全屏
|
||||
const toggleFullscreen = useCallback(() => {
|
||||
setIsFullscreen(prev => !prev);
|
||||
@@ -459,7 +529,7 @@ const HierarchyView = ({
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
mb={4}
|
||||
mb={3}
|
||||
flexWrap="wrap"
|
||||
gap={2}
|
||||
>
|
||||
@@ -468,15 +538,33 @@ const HierarchyView = ({
|
||||
<Text fontSize="lg" fontWeight="bold" color="gray.800">
|
||||
概念层级导图
|
||||
</Text>
|
||||
<Badge colorScheme="purple" fontSize="xs">
|
||||
点击节点筛选
|
||||
</Badge>
|
||||
{tradeDate && (
|
||||
<Badge colorScheme="blue" fontSize="xs">
|
||||
{tradeDate}
|
||||
</Badge>
|
||||
)}
|
||||
{priceLoading && (
|
||||
<Spinner size="xs" color="purple.500" />
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack spacing={2}>
|
||||
{/* 刷新涨跌幅 */}
|
||||
<Tooltip label="刷新涨跌幅" placement="top">
|
||||
<IconButton
|
||||
size="sm"
|
||||
icon={<FaSync />}
|
||||
onClick={handleRefreshPrice}
|
||||
isLoading={priceLoading}
|
||||
variant="outline"
|
||||
colorScheme="blue"
|
||||
aria-label="刷新涨跌幅"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* 布局切换 */}
|
||||
<ButtonGroup size="sm" isAttached variant="outline">
|
||||
<Tooltip label="径向布局(思维导图)" placement="top">
|
||||
<Tooltip label="径向布局" placement="top">
|
||||
<IconButton
|
||||
icon={<FaProjectDiagram />}
|
||||
onClick={() => setLayout('radial')}
|
||||
@@ -530,9 +618,9 @@ const HierarchyView = ({
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
{/* 提示 */}
|
||||
{/* 操作提示 */}
|
||||
<Text fontSize="xs" color="gray.500" mb={2} textAlign="center">
|
||||
🖱️ 滚轮缩放 | 拖拽平移 | 点击节点可展开/收起或筛选概念列表
|
||||
🖱️ 滚轮缩放 | 拖拽平移 | <Text as="span" color="purple.600" fontWeight="medium">点击节点展开/收起</Text> | <Text as="span" color="blue.600" fontWeight="medium">双击筛选该分类</Text>
|
||||
</Text>
|
||||
|
||||
{/* ECharts 图表 */}
|
||||
@@ -541,13 +629,13 @@ const HierarchyView = ({
|
||||
borderColor="gray.200"
|
||||
borderRadius="xl"
|
||||
overflow="hidden"
|
||||
bg="gray.50"
|
||||
bg="white"
|
||||
>
|
||||
<ReactECharts
|
||||
ref={chartRef}
|
||||
option={chartOption}
|
||||
style={{
|
||||
height: isFullscreen ? 'calc(100vh - 120px)' : isMobile ? '400px' : '600px',
|
||||
height: isFullscreen ? 'calc(100vh - 140px)' : isMobile ? '450px' : '550px',
|
||||
width: '100%',
|
||||
}}
|
||||
onEvents={chartEvents}
|
||||
@@ -558,7 +646,7 @@ const HierarchyView = ({
|
||||
{/* 统计信息 */}
|
||||
<Flex
|
||||
justify="center"
|
||||
mt={4}
|
||||
mt={3}
|
||||
gap={3}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
@@ -569,7 +657,7 @@ const HierarchyView = ({
|
||||
borderRadius="full"
|
||||
fontSize="xs"
|
||||
>
|
||||
共 {hierarchy.length} 个一级分类
|
||||
{hierarchy.length} 个一级分类
|
||||
</Badge>
|
||||
<Badge
|
||||
colorScheme="blue"
|
||||
|
||||
Reference in New Issue
Block a user