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 = () => (
+