update pay ui
This commit is contained in:
@@ -755,7 +755,7 @@ async def get_hierarchy_price_early(
|
|||||||
trade_date: Optional[date] = Query(None, description="交易日期,默认最新"),
|
trade_date: Optional[date] = Query(None, description="交易日期,默认最新"),
|
||||||
lv1_filter: Optional[str] = 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}")
|
logger.info(f"[hierarchy/price] 请求参数: trade_date={trade_date}, lv1_filter={lv1_filter}")
|
||||||
|
|
||||||
if not mysql_pool:
|
if not mysql_pool:
|
||||||
@@ -768,22 +768,11 @@ async def get_hierarchy_price_early(
|
|||||||
# 获取交易日期
|
# 获取交易日期
|
||||||
query_date = trade_date
|
query_date = trade_date
|
||||||
if query_date is None:
|
if query_date is None:
|
||||||
# 优先从母概念查最新日期
|
await cursor.execute("SELECT MAX(trade_date) as max_date FROM concept_daily_stats")
|
||||||
await cursor.execute(
|
|
||||||
"SELECT MAX(trade_date) as max_date FROM concept_daily_stats WHERE concept_type IN ('lv1', 'lv2', 'lv3')"
|
|
||||||
)
|
|
||||||
result = await cursor.fetchone()
|
result = await cursor.fetchone()
|
||||||
logger.info(f"[hierarchy/price] 查询母概念最新日期: {result}")
|
if not result or not result['max_date']:
|
||||||
if result and result['max_date']:
|
raise HTTPException(status_code=404, detail="无涨跌幅数据")
|
||||||
query_date = 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']
|
|
||||||
|
|
||||||
logger.info(f"[hierarchy/price] 使用查询日期: {query_date}")
|
logger.info(f"[hierarchy/price] 使用查询日期: {query_date}")
|
||||||
|
|
||||||
@@ -802,15 +791,14 @@ async def get_hierarchy_price_early(
|
|||||||
lv1_concepts = []
|
lv1_concepts = []
|
||||||
lv2_concepts = []
|
lv2_concepts = []
|
||||||
lv3_concepts = []
|
lv3_concepts = []
|
||||||
|
leaf_concepts = []
|
||||||
|
|
||||||
# 查询 lv1
|
# 查询 lv1
|
||||||
if lv1_filter:
|
if lv1_filter:
|
||||||
await cursor.execute(base_query, (query_date, 'lv1', f"%{lv1_filter}%"))
|
await cursor.execute(base_query, (query_date, 'lv1', f"%{lv1_filter}%"))
|
||||||
else:
|
else:
|
||||||
await cursor.execute(base_query, (query_date, 'lv1'))
|
await cursor.execute(base_query, (query_date, 'lv1'))
|
||||||
lv1_rows = await cursor.fetchall()
|
for row in await cursor.fetchall():
|
||||||
logger.info(f"[hierarchy/price] lv1查询结果数量: {len(lv1_rows)}")
|
|
||||||
for row in lv1_rows:
|
|
||||||
lv1_concepts.append({
|
lv1_concepts.append({
|
||||||
"concept_id": row['concept_id'],
|
"concept_id": row['concept_id'],
|
||||||
"concept_name": row['concept_name'],
|
"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}%"))
|
await cursor.execute(base_query, (query_date, 'lv2', f"%{lv1_filter}%"))
|
||||||
else:
|
else:
|
||||||
await cursor.execute(base_query, (query_date, 'lv2'))
|
await cursor.execute(base_query, (query_date, 'lv2'))
|
||||||
lv2_rows = await cursor.fetchall()
|
for row in await cursor.fetchall():
|
||||||
logger.info(f"[hierarchy/price] lv2查询结果数量: {len(lv2_rows)}")
|
|
||||||
for row in lv2_rows:
|
|
||||||
lv2_concepts.append({
|
lv2_concepts.append({
|
||||||
"concept_id": row['concept_id'],
|
"concept_id": row['concept_id'],
|
||||||
"concept_name": row['concept_name'],
|
"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}%"))
|
await cursor.execute(base_query, (query_date, 'lv3', f"%{lv1_filter}%"))
|
||||||
else:
|
else:
|
||||||
await cursor.execute(base_query, (query_date, 'lv3'))
|
await cursor.execute(base_query, (query_date, 'lv3'))
|
||||||
lv3_rows = await cursor.fetchall()
|
for row in await cursor.fetchall():
|
||||||
logger.info(f"[hierarchy/price] lv3查询结果数量: {len(lv3_rows)}")
|
|
||||||
for row in lv3_rows:
|
|
||||||
lv3_concepts.append({
|
lv3_concepts.append({
|
||||||
"concept_id": row['concept_id'],
|
"concept_id": row['concept_id'],
|
||||||
"concept_name": row['concept_name'],
|
"concept_name": row['concept_name'],
|
||||||
@@ -854,13 +838,37 @@ async def get_hierarchy_price_early(
|
|||||||
"stock_count": row['stock_count']
|
"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 {
|
return {
|
||||||
"trade_date": str(query_date),
|
"trade_date": str(query_date),
|
||||||
"lv1_concepts": lv1_concepts,
|
"lv1_concepts": lv1_concepts,
|
||||||
"lv2_concepts": lv2_concepts,
|
"lv2_concepts": lv2_concepts,
|
||||||
"lv3_concepts": lv3_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:
|
except HTTPException as he:
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* HierarchyView - 概念层级热力图视图
|
* HierarchyView - 概念层级热力图视图
|
||||||
*
|
*
|
||||||
* 使用 CSS Grid + Chakra UI 实现热力图效果
|
* Modern Spatial & Glassmorphism 设计风格
|
||||||
* 特性:
|
* 特性:
|
||||||
* 1. 炫酷的矩形热力图展示,涨红跌绿背景色
|
* 1. 毛玻璃卡片 + 极光背景
|
||||||
* 2. 点击 lv1 进入 lv2,点击 lv2 进入 lv3,点击 lv3 进入具体概念
|
* 2. 涨红跌绿渐变色
|
||||||
* 3. 集成 /hierarchy/price 接口获取实时涨跌幅
|
* 3. 支持 lv1 → lv2 → lv3 → leaf 四层钻取
|
||||||
* 4. 每个分类有独特图标
|
* 4. 集成 /hierarchy/price 接口(含 leaf_concepts)
|
||||||
* 5. 支持面包屑导航返回上级
|
|
||||||
*/
|
*/
|
||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
@@ -48,7 +47,6 @@ import {
|
|||||||
FaCoins,
|
FaCoins,
|
||||||
FaHeartbeat,
|
FaHeartbeat,
|
||||||
FaAtom,
|
FaAtom,
|
||||||
FaLightbulb,
|
|
||||||
FaArrowUp,
|
FaArrowUp,
|
||||||
FaArrowDown,
|
FaArrowDown,
|
||||||
FaCubes,
|
FaCubes,
|
||||||
@@ -61,6 +59,7 @@ import {
|
|||||||
FaBatteryFull,
|
FaBatteryFull,
|
||||||
FaSolarPanel,
|
FaSolarPanel,
|
||||||
FaTags,
|
FaTags,
|
||||||
|
FaExternalLinkAlt,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
|
|
||||||
@@ -111,27 +110,36 @@ const LV2_ICONS = {
|
|||||||
'军贸出海': FaGlobe,
|
'军贸出海': FaGlobe,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 根据涨跌幅获取背景色(统一涨红跌绿)
|
// 根据涨跌幅获取背景渐变色(涨红跌绿)
|
||||||
const getChangeBgColor = (value) => {
|
const getChangeGradient = (value) => {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
// 无数据时使用灰色
|
return 'linear-gradient(135deg, rgba(71, 85, 105, 0.6) 0%, rgba(100, 116, 139, 0.4) 100%)';
|
||||||
return 'linear-gradient(135deg, #4B5563 0%, #6B7280 100%)';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 涨跌幅越大,颜色越深 - 统一涨红跌绿
|
// 涨 - 红色系
|
||||||
if (value > 7) return 'linear-gradient(135deg, #7F1D1D 0%, #991B1B 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, #991B1B 0%, #B91C1C 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, #B91C1C 0%, #DC2626 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, #DC2626 0%, #EF4444 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, #EF4444 0%, #F87171 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, #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 < -7) return 'linear-gradient(135deg, rgba(20, 83, 45, 0.8) 0%, rgba(22, 101, 52, 0.6) 100%)';
|
||||||
if (value < -1) return 'linear-gradient(135deg, #16A34A 0%, #22C55E 100%)';
|
if (value < -5) return 'linear-gradient(135deg, rgba(22, 101, 52, 0.75) 0%, rgba(21, 128, 61, 0.55) 100%)';
|
||||||
if (value < 0) return 'linear-gradient(135deg, #22C55E 0%, #4ADE80 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) => {
|
const getIcon = (name, level) => {
|
||||||
if (level === 'lv1') {
|
if (level === 'lv1') return LV1_ICONS[name] || FaLayerGroup;
|
||||||
return LV1_ICONS[name] || FaLayerGroup;
|
if (level === 'lv2') return LV2_ICONS[name] || FaCubes;
|
||||||
}
|
if (level === 'lv3') return FaCubes;
|
||||||
if (level === 'lv2') {
|
return FaTags;
|
||||||
return LV2_ICONS[name] || FaCubes;
|
|
||||||
}
|
|
||||||
if (level === 'lv3') {
|
|
||||||
return FaCubes;
|
|
||||||
}
|
|
||||||
return FaTags; // 具体概念用标签图标
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 从 API 返回的名称中提取纯名称(去掉 [一级] [二级] [三级] 前缀)
|
// 从 API 返回的名称中提取纯名称
|
||||||
const extractPureName = (apiName) => {
|
const extractPureName = (apiName) => {
|
||||||
if (!apiName) return '';
|
if (!apiName) return '';
|
||||||
return apiName.replace(/^\[(一级|二级|三级)\]\s*/, '');
|
return apiName.replace(/^\[(一级|二级|三级)\]\s*/, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 脉冲动画
|
// 呼吸动画
|
||||||
const pulseKeyframes = keyframes`
|
const breatheKeyframes = keyframes`
|
||||||
0%, 100% { transform: scale(1); }
|
0%, 100% { opacity: 0.3; transform: scale(1); }
|
||||||
50% { transform: scale(1.02); }
|
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 = () => (
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
right={0}
|
||||||
|
bottom={0}
|
||||||
|
overflow="hidden"
|
||||||
|
pointerEvents="none"
|
||||||
|
zIndex={0}
|
||||||
|
>
|
||||||
|
{/* 紫色光斑 - 左上 */}
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top="-20%"
|
||||||
|
left="-10%"
|
||||||
|
width="500px"
|
||||||
|
height="500px"
|
||||||
|
bg="purple.600"
|
||||||
|
opacity={0.15}
|
||||||
|
borderRadius="full"
|
||||||
|
filter="blur(120px)"
|
||||||
|
css={{ animation: `${breatheKeyframes} 8s ease-in-out infinite` }}
|
||||||
|
/>
|
||||||
|
{/* 蓝色光斑 - 右下 */}
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
bottom="-20%"
|
||||||
|
right="-10%"
|
||||||
|
width="450px"
|
||||||
|
height="450px"
|
||||||
|
bg="blue.500"
|
||||||
|
opacity={0.12}
|
||||||
|
borderRadius="full"
|
||||||
|
filter="blur(100px)"
|
||||||
|
css={{ animation: `${breatheKeyframes} 10s ease-in-out infinite 2s` }}
|
||||||
|
/>
|
||||||
|
{/* 青色光斑 - 中间 */}
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top="40%"
|
||||||
|
left="30%"
|
||||||
|
width="300px"
|
||||||
|
height="300px"
|
||||||
|
bg="cyan.400"
|
||||||
|
opacity={0.08}
|
||||||
|
borderRadius="full"
|
||||||
|
filter="blur(80px)"
|
||||||
|
css={{ animation: `${breatheKeyframes} 12s ease-in-out infinite 4s` }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 毛玻璃卡片组件
|
||||||
|
*/
|
||||||
|
const GlassCard = ({ item, onClick, size = 'normal' }) => {
|
||||||
const hasChange = item.avg_change_pct !== null && item.avg_change_pct !== undefined;
|
const hasChange = item.avg_change_pct !== null && item.avg_change_pct !== undefined;
|
||||||
const isPositive = hasChange && item.avg_change_pct > 0;
|
const isPositive = hasChange && item.avg_change_pct > 0;
|
||||||
const isNegative = 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 调整高度
|
// 根据 size 调整高度
|
||||||
const heightMap = {
|
const heightMap = {
|
||||||
large: { base: '140px', md: '160px' },
|
large: { base: '150px', md: '170px' },
|
||||||
normal: { base: '110px', md: '130px' },
|
normal: { base: '120px', md: '140px' },
|
||||||
small: { base: '90px', md: '100px' },
|
small: { base: '100px', md: '110px' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// 是否可点击进入下一层
|
// 是否可点击进入下一层
|
||||||
const canDrillDown = item.level !== 'concept' && (item.children?.length > 0 || item.concepts?.length > 0);
|
const canDrillDown = item.level !== 'concept' && (item.children?.length > 0 || item.concepts?.length > 0);
|
||||||
|
const isLeafConcept = item.level === 'concept';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
bg={getChangeBgColor(item.avg_change_pct)}
|
position="relative"
|
||||||
borderRadius="xl"
|
minH={heightMap[size]}
|
||||||
p={{ base: 3, md: 4 }}
|
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
onClick={() => onClick(item)}
|
onClick={() => onClick(item)}
|
||||||
position="relative"
|
transition="all 0.4s cubic-bezier(0.4, 0, 0.2, 1)"
|
||||||
overflow="hidden"
|
transform="translateZ(0)"
|
||||||
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)"
|
|
||||||
_hover={{
|
_hover={{
|
||||||
transform: 'translateY(-4px) scale(1.02)',
|
transform: 'translateY(-6px) scale(1.02)',
|
||||||
boxShadow: '0 12px 30px rgba(0, 0, 0, 0.3)',
|
|
||||||
}}
|
}}
|
||||||
css={isLargeChange ? { animation: `${pulseKeyframes} 2s infinite` } : {}}
|
css={isLargeChange ? { animation: `${floatKeyframes} 3s ease-in-out infinite` } : {}}
|
||||||
>
|
>
|
||||||
{/* 背景装饰 */}
|
{/* 毛玻璃主体 */}
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top={-20}
|
top={0}
|
||||||
right={-20}
|
left={0}
|
||||||
width="80px"
|
right={0}
|
||||||
height="80px"
|
bottom={0}
|
||||||
borderRadius="full"
|
bg={getChangeGradient(item.avg_change_pct)}
|
||||||
bg="whiteAlpha.100"
|
backdropFilter="blur(20px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="whiteAlpha.200"
|
||||||
|
borderRadius="2xl"
|
||||||
|
overflow="hidden"
|
||||||
|
boxShadow={isLargeChange
|
||||||
|
? `0 8px 32px rgba(${isPositive ? '239, 68, 68' : '34, 197, 94'}, 0.3)`
|
||||||
|
: '0 8px 32px rgba(0, 0, 0, 0.2)'
|
||||||
|
}
|
||||||
|
_hover={{
|
||||||
|
borderColor: 'whiteAlpha.300',
|
||||||
|
boxShadow: isLargeChange
|
||||||
|
? `0 16px 48px rgba(${isPositive ? '239, 68, 68' : '34, 197, 94'}, 0.4)`
|
||||||
|
: '0 16px 48px rgba(0, 0, 0, 0.3)',
|
||||||
|
}}
|
||||||
|
transition="all 0.4s"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 高光效果 */}
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
right={0}
|
||||||
|
height="50%"
|
||||||
|
bg="linear-gradient(180deg, rgba(255,255,255,0.1) 0%, transparent 100%)"
|
||||||
|
borderRadius="2xl 2xl 0 0"
|
||||||
|
pointerEvents="none"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 内容 */}
|
{/* 内容 */}
|
||||||
@@ -222,47 +309,61 @@ const HeatmapBlock = ({ item, onClick, size = 'normal' }) => {
|
|||||||
direction="column"
|
direction="column"
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
h="100%"
|
h="100%"
|
||||||
|
p={{ base: 3, md: 4 }}
|
||||||
position="relative"
|
position="relative"
|
||||||
zIndex={1}
|
zIndex={1}
|
||||||
>
|
>
|
||||||
{/* 顶部:图标和名称 */}
|
{/* 顶部:图标和名称 */}
|
||||||
<HStack spacing={2} align="flex-start">
|
<HStack spacing={2} align="flex-start">
|
||||||
<Box
|
<Box
|
||||||
p={1.5}
|
p={2}
|
||||||
bg="whiteAlpha.200"
|
bg="whiteAlpha.150"
|
||||||
borderRadius="lg"
|
borderRadius="xl"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="whiteAlpha.100"
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
as={IconComponent}
|
as={IconComponent}
|
||||||
boxSize={{ base: 3.5, md: 4 }}
|
boxSize={{ base: 4, md: 5 }}
|
||||||
color="white"
|
color="white"
|
||||||
|
opacity={0.9}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<VStack align="start" spacing={0} flex={1}>
|
<VStack align="start" spacing={0} flex={1}>
|
||||||
<Text
|
<Text
|
||||||
color="white"
|
color="white"
|
||||||
fontWeight="bold"
|
fontWeight="semibold"
|
||||||
fontSize={{ base: 'xs', md: 'sm' }}
|
fontSize={{ base: 'sm', md: 'md' }}
|
||||||
noOfLines={2}
|
noOfLines={2}
|
||||||
textShadow="0 2px 4px rgba(0,0,0,0.3)"
|
|
||||||
lineHeight="1.3"
|
lineHeight="1.3"
|
||||||
|
letterSpacing="0.02em"
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
{item.concept_count > 0 && (
|
{item.concept_count > 0 && (
|
||||||
<Text color="whiteAlpha.800" fontSize="xs">
|
<Text color="whiteAlpha.600" fontSize="xs" mt={0.5}>
|
||||||
{item.concept_count} 个概念
|
{item.concept_count} 个概念
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
|
{/* 外链图标 */}
|
||||||
|
{isLeafConcept && (
|
||||||
|
<Icon
|
||||||
|
as={FaExternalLinkAlt}
|
||||||
|
boxSize={3}
|
||||||
|
color="whiteAlpha.500"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{/* 底部:涨跌幅 */}
|
{/* 底部:涨跌幅 */}
|
||||||
<Flex justify="space-between" align="flex-end">
|
<Flex justify="space-between" align="flex-end">
|
||||||
<VStack align="start" spacing={0}>
|
<VStack align="start" spacing={0}>
|
||||||
{item.stock_count > 0 && (
|
{item.stock_count > 0 && (
|
||||||
<Text color="whiteAlpha.700" fontSize="xs">
|
<Text color="whiteAlpha.500" fontSize="xs">
|
||||||
{item.stock_count} 只股票
|
{item.stock_count} 只股票
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -273,14 +374,15 @@ const HeatmapBlock = ({ item, onClick, size = 'normal' }) => {
|
|||||||
<Icon
|
<Icon
|
||||||
as={isPositive ? FaArrowUp : FaArrowDown}
|
as={isPositive ? FaArrowUp : FaArrowDown}
|
||||||
boxSize={3}
|
boxSize={3}
|
||||||
color="white"
|
color={getChangeTextColor(item.avg_change_pct)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Text
|
<Text
|
||||||
color="white"
|
color={getChangeTextColor(item.avg_change_pct)}
|
||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
fontSize={{ base: 'md', md: 'xl' }}
|
fontSize={{ base: 'lg', md: '2xl' }}
|
||||||
textShadow="0 2px 8px rgba(0,0,0,0.4)"
|
fontFamily="mono"
|
||||||
|
letterSpacing="-0.02em"
|
||||||
>
|
>
|
||||||
{formatChangePercent(item.avg_change_pct)}
|
{formatChangePercent(item.avg_change_pct)}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -288,19 +390,23 @@ const HeatmapBlock = ({ item, onClick, size = 'normal' }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* 可点击提示 */}
|
{/* 展开标识 */}
|
||||||
{canDrillDown && (
|
{canDrillDown && (
|
||||||
<Badge
|
<Badge
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top={2}
|
top={2}
|
||||||
right={2}
|
right={2}
|
||||||
bg="whiteAlpha.300"
|
bg="whiteAlpha.200"
|
||||||
color="white"
|
color="whiteAlpha.800"
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
px={2}
|
px={2}
|
||||||
|
py={0.5}
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="whiteAlpha.100"
|
||||||
>
|
>
|
||||||
点击展开
|
展开
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -318,12 +424,12 @@ const HierarchyView = ({
|
|||||||
const [hierarchy, setHierarchy] = useState([]);
|
const [hierarchy, setHierarchy] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
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 [priceLoading, setPriceLoading] = useState(false);
|
||||||
const [tradeDate, setTradeDate] = useState(null);
|
const [tradeDate, setTradeDate] = useState(null);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
// 钻取状态 - 支持4层:lv1 -> lv2 -> lv3 -> concept
|
// 钻取状态
|
||||||
const [currentLevel, setCurrentLevel] = useState('lv1');
|
const [currentLevel, setCurrentLevel] = useState('lv1');
|
||||||
const [currentLv1, setCurrentLv1] = useState(null);
|
const [currentLv1, setCurrentLv1] = useState(null);
|
||||||
const [currentLv2, setCurrentLv2] = useState(null);
|
const [currentLv2, setCurrentLv2] = useState(null);
|
||||||
@@ -375,10 +481,11 @@ const HierarchyView = ({
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// 构建映射表 - 使用纯名称作为 key(去掉 [一级] 等前缀)
|
// 构建映射表
|
||||||
const lv1Map = {};
|
const lv1Map = {};
|
||||||
const lv2Map = {};
|
const lv2Map = {};
|
||||||
const lv3Map = {};
|
const lv3Map = {};
|
||||||
|
const leafMap = {};
|
||||||
|
|
||||||
(data.lv1_concepts || []).forEach(item => {
|
(data.lv1_concepts || []).forEach(item => {
|
||||||
const pureName = extractPureName(item.concept_name);
|
const pureName = extractPureName(item.concept_name);
|
||||||
@@ -392,16 +499,20 @@ const HierarchyView = ({
|
|||||||
const pureName = extractPureName(item.concept_name);
|
const pureName = extractPureName(item.concept_name);
|
||||||
lv3Map[pureName] = item;
|
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);
|
setTradeDate(data.trade_date);
|
||||||
|
|
||||||
logger.info('HierarchyView', '层级涨跌幅加载完成', {
|
logger.info('HierarchyView', '层级涨跌幅加载完成', {
|
||||||
lv1Count: Object.keys(lv1Map).length,
|
lv1Count: Object.keys(lv1Map).length,
|
||||||
lv2Count: Object.keys(lv2Map).length,
|
lv2Count: Object.keys(lv2Map).length,
|
||||||
lv3Count: Object.keys(lv3Map).length,
|
lv3Count: Object.keys(lv3Map).length,
|
||||||
|
leafCount: Object.keys(leafMap).length,
|
||||||
tradeDate: data.trade_date,
|
tradeDate: data.trade_date,
|
||||||
sampleLv1Keys: Object.keys(lv1Map).slice(0, 3),
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('HierarchyView', '获取层级涨跌幅失败', { error: err.message });
|
logger.warn('HierarchyView', '获取层级涨跌幅失败', { error: err.message });
|
||||||
@@ -422,7 +533,7 @@ const HierarchyView = ({
|
|||||||
|
|
||||||
// 根据当前层级获取显示数据
|
// 根据当前层级获取显示数据
|
||||||
const currentData = useMemo(() => {
|
const currentData = useMemo(() => {
|
||||||
const { lv1Map, lv2Map, lv3Map } = priceData;
|
const { lv1Map, lv2Map, lv3Map, leafMap } = priceData;
|
||||||
|
|
||||||
// 第一层:显示所有 lv1
|
// 第一层:显示所有 lv1
|
||||||
if (currentLevel === 'lv1') {
|
if (currentLevel === 'lv1') {
|
||||||
@@ -489,12 +600,17 @@ const HierarchyView = ({
|
|||||||
|
|
||||||
// 如果 lv2 直接包含概念(没有 lv3)
|
// 如果 lv2 直接包含概念(没有 lv3)
|
||||||
if (lv2Data.concepts && lv2Data.concepts.length > 0) {
|
if (lv2Data.concepts && lv2Data.concepts.length > 0) {
|
||||||
return lv2Data.concepts.map((concept) => ({
|
return lv2Data.concepts.map((conceptName) => {
|
||||||
name: concept,
|
const price = leafMap[conceptName] || {};
|
||||||
level: 'concept',
|
return {
|
||||||
parentLv1: currentLv1.name,
|
name: conceptName,
|
||||||
parentLv2: currentLv2.name,
|
level: 'concept',
|
||||||
}));
|
parentLv1: currentLv1.name,
|
||||||
|
parentLv2: currentLv2.name,
|
||||||
|
stock_count: price.stock_count,
|
||||||
|
avg_change_pct: price.avg_change_pct,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
@@ -511,13 +627,18 @@ const HierarchyView = ({
|
|||||||
const lv3Data = lv2Data.children.find(h => h.name === currentLv3.name);
|
const lv3Data = lv2Data.children.find(h => h.name === currentLv3.name);
|
||||||
if (!lv3Data || !lv3Data.concepts) return [];
|
if (!lv3Data || !lv3Data.concepts) return [];
|
||||||
|
|
||||||
return lv3Data.concepts.map((concept) => ({
|
return lv3Data.concepts.map((conceptName) => {
|
||||||
name: concept,
|
const price = leafMap[conceptName] || {};
|
||||||
level: 'concept',
|
return {
|
||||||
parentLv1: currentLv1.name,
|
name: conceptName,
|
||||||
parentLv2: currentLv2.name,
|
level: 'concept',
|
||||||
parentLv3: currentLv3.name,
|
parentLv1: currentLv1.name,
|
||||||
}));
|
parentLv2: currentLv2.name,
|
||||||
|
parentLv3: currentLv3.name,
|
||||||
|
stock_count: price.stock_count,
|
||||||
|
avg_change_pct: price.avg_change_pct,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
@@ -528,7 +649,6 @@ const HierarchyView = ({
|
|||||||
logger.info('HierarchyView', '热力图点击', { level: item.level, name: item.name });
|
logger.info('HierarchyView', '热力图点击', { level: item.level, name: item.name });
|
||||||
|
|
||||||
if (item.level === 'lv1' && item.children && item.children.length > 0) {
|
if (item.level === 'lv1' && item.children && item.children.length > 0) {
|
||||||
// 进入 lv2
|
|
||||||
setCurrentLevel('lv2');
|
setCurrentLevel('lv2');
|
||||||
setCurrentLv1(item);
|
setCurrentLv1(item);
|
||||||
setBreadcrumbs([
|
setBreadcrumbs([
|
||||||
@@ -536,9 +656,7 @@ const HierarchyView = ({
|
|||||||
{ label: item.name, level: 'lv1', data: item },
|
{ label: item.name, level: 'lv1', data: item },
|
||||||
]);
|
]);
|
||||||
} else if (item.level === 'lv2') {
|
} else if (item.level === 'lv2') {
|
||||||
// 检查是否有 lv3 或直接是概念
|
|
||||||
if (item.children && item.children.length > 0) {
|
if (item.children && item.children.length > 0) {
|
||||||
// 进入 lv3
|
|
||||||
setCurrentLevel('lv3');
|
setCurrentLevel('lv3');
|
||||||
setCurrentLv2(item);
|
setCurrentLv2(item);
|
||||||
setBreadcrumbs([
|
setBreadcrumbs([
|
||||||
@@ -547,8 +665,7 @@ const HierarchyView = ({
|
|||||||
{ label: item.name, level: 'lv2', data: item },
|
{ label: item.name, level: 'lv2', data: item },
|
||||||
]);
|
]);
|
||||||
} else if (item.concepts && item.concepts.length > 0) {
|
} else if (item.concepts && item.concepts.length > 0) {
|
||||||
// lv2 直接包含概念,进入概念层
|
setCurrentLevel('lv3');
|
||||||
setCurrentLevel('lv3'); // 实际显示概念
|
|
||||||
setCurrentLv2(item);
|
setCurrentLv2(item);
|
||||||
setBreadcrumbs([
|
setBreadcrumbs([
|
||||||
{ label: '全部分类', level: 'root' },
|
{ label: '全部分类', level: 'root' },
|
||||||
@@ -557,7 +674,6 @@ const HierarchyView = ({
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} else if (item.level === 'lv3' && item.concepts && item.concepts.length > 0) {
|
} else if (item.level === 'lv3' && item.concepts && item.concepts.length > 0) {
|
||||||
// 进入具体概念层
|
|
||||||
setCurrentLevel('concept');
|
setCurrentLevel('concept');
|
||||||
setCurrentLv3(item);
|
setCurrentLv3(item);
|
||||||
setBreadcrumbs([
|
setBreadcrumbs([
|
||||||
@@ -567,7 +683,7 @@ const HierarchyView = ({
|
|||||||
{ label: item.name, level: 'lv3', data: item },
|
{ label: item.name, level: 'lv3', data: item },
|
||||||
]);
|
]);
|
||||||
} else if (item.level === 'concept') {
|
} else if (item.level === 'concept') {
|
||||||
// 点击具体概念,跳转到概念详情页
|
// 跳转到概念详情页
|
||||||
const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(item.name)}.html`;
|
const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(item.name)}.html`;
|
||||||
window.open(htmlPath, '_blank');
|
window.open(htmlPath, '_blank');
|
||||||
}
|
}
|
||||||
@@ -595,12 +711,12 @@ const HierarchyView = ({
|
|||||||
}
|
}
|
||||||
}, [breadcrumbs]);
|
}, [breadcrumbs]);
|
||||||
|
|
||||||
// 刷新涨跌幅数据
|
// 刷新
|
||||||
const handleRefreshPrice = useCallback(() => {
|
const handleRefreshPrice = useCallback(() => {
|
||||||
fetchHierarchyPrice();
|
fetchHierarchyPrice();
|
||||||
}, [fetchHierarchyPrice]);
|
}, [fetchHierarchyPrice]);
|
||||||
|
|
||||||
// 切换全屏
|
// 全屏切换
|
||||||
const toggleFullscreen = useCallback(() => {
|
const toggleFullscreen = useCallback(() => {
|
||||||
setIsFullscreen(prev => !prev);
|
setIsFullscreen(prev => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -628,17 +744,18 @@ const HierarchyView = ({
|
|||||||
// 计算列数
|
// 计算列数
|
||||||
const getGridColumns = () => {
|
const getGridColumns = () => {
|
||||||
if (currentLevel === 'concept') {
|
if (currentLevel === 'concept') {
|
||||||
return { base: 2, md: 4, lg: 5 }; // 概念层更多列
|
return { base: 2, md: 3, lg: 4 };
|
||||||
}
|
}
|
||||||
return { base: 2, md: 3, lg: 4 };
|
return { base: 2, md: 3, lg: 4 };
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Center h="400px">
|
<Center h="400px" position="relative">
|
||||||
<VStack spacing={4}>
|
<AuroraBackground />
|
||||||
<Spinner size="xl" color="purple.500" thickness="4px" />
|
<VStack spacing={4} position="relative" zIndex={1}>
|
||||||
<Text color="gray.600">正在加载概念层级...</Text>
|
<Spinner size="xl" color="purple.400" thickness="4px" />
|
||||||
|
<Text color="gray.400">正在加载概念层级...</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
@@ -646,11 +763,18 @@ const HierarchyView = ({
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Center h="400px">
|
<Center h="400px" position="relative">
|
||||||
<VStack spacing={4}>
|
<AuroraBackground />
|
||||||
<Icon as={FaLayerGroup} boxSize={16} color="gray.300" />
|
<VStack spacing={4} position="relative" zIndex={1}>
|
||||||
<Text color="gray.600">加载失败:{error}</Text>
|
<Icon as={FaLayerGroup} boxSize={16} color="gray.600" />
|
||||||
<Button colorScheme="purple" size="sm" onClick={fetchHierarchy}>
|
<Text color="gray.400">加载失败:{error}</Text>
|
||||||
|
<Button
|
||||||
|
colorScheme="purple"
|
||||||
|
size="sm"
|
||||||
|
onClick={fetchHierarchy}
|
||||||
|
bg="purple.500"
|
||||||
|
_hover={{ bg: 'purple.400' }}
|
||||||
|
>
|
||||||
重试
|
重试
|
||||||
</Button>
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
@@ -660,10 +784,11 @@ const HierarchyView = ({
|
|||||||
|
|
||||||
if (currentData.length === 0) {
|
if (currentData.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Center h="400px">
|
<Center h="400px" position="relative">
|
||||||
<VStack spacing={4}>
|
<AuroraBackground />
|
||||||
<Icon as={FaLayerGroup} boxSize={16} color="gray.300" />
|
<VStack spacing={4} position="relative" zIndex={1}>
|
||||||
<Text color="gray.600">暂无层级数据</Text>
|
<Icon as={FaLayerGroup} boxSize={16} color="gray.600" />
|
||||||
|
<Text color="gray.400">暂无层级数据</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
@@ -677,170 +802,204 @@ const HierarchyView = ({
|
|||||||
right={isFullscreen ? 0 : 'auto'}
|
right={isFullscreen ? 0 : 'auto'}
|
||||||
bottom={isFullscreen ? 0 : 'auto'}
|
bottom={isFullscreen ? 0 : 'auto'}
|
||||||
zIndex={isFullscreen ? 1000 : 'auto'}
|
zIndex={isFullscreen ? 1000 : 'auto'}
|
||||||
bg={isFullscreen ? 'gray.50' : 'transparent'}
|
bg="slate.950"
|
||||||
p={isFullscreen ? 4 : 0}
|
bgGradient="linear(to-br, gray.900, slate.900, gray.900)"
|
||||||
overflow={isFullscreen ? 'auto' : 'visible'}
|
p={{ base: 3, md: 5 }}
|
||||||
|
borderRadius={isFullscreen ? '0' : '3xl'}
|
||||||
|
overflow={isFullscreen ? 'auto' : 'hidden'}
|
||||||
|
minH="500px"
|
||||||
>
|
>
|
||||||
{/* 工具栏 */}
|
{/* 极光背景 */}
|
||||||
<Flex
|
<AuroraBackground />
|
||||||
justify="space-between"
|
|
||||||
align="center"
|
|
||||||
mb={4}
|
|
||||||
flexWrap="wrap"
|
|
||||||
gap={2}
|
|
||||||
>
|
|
||||||
<HStack spacing={3}>
|
|
||||||
<Icon as={FaLayerGroup} color="purple.500" boxSize={6} />
|
|
||||||
<VStack align="start" spacing={0}>
|
|
||||||
<Text fontSize="lg" fontWeight="bold" color="gray.800">
|
|
||||||
{getCurrentTitle()}
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="xs" color="gray.500">
|
|
||||||
{getLevelDesc()} · {currentData.length} 项
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
{tradeDate && (
|
|
||||||
<Badge colorScheme="blue" fontSize="xs" px={2} py={1} borderRadius="full">
|
|
||||||
{tradeDate}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{priceLoading && (
|
|
||||||
<Spinner size="sm" color="purple.500" />
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
<HStack spacing={2}>
|
{/* 内容层 */}
|
||||||
<Tooltip label="刷新涨跌幅" placement="top">
|
<Box position="relative" zIndex={1}>
|
||||||
<IconButton
|
{/* 工具栏 */}
|
||||||
size="sm"
|
<Flex
|
||||||
icon={<FaSync />}
|
justify="space-between"
|
||||||
onClick={handleRefreshPrice}
|
align="center"
|
||||||
isLoading={priceLoading}
|
mb={5}
|
||||||
variant="outline"
|
flexWrap="wrap"
|
||||||
colorScheme="blue"
|
gap={3}
|
||||||
aria-label="刷新涨跌幅"
|
>
|
||||||
/>
|
<HStack spacing={3}>
|
||||||
</Tooltip>
|
<Box
|
||||||
|
p={2}
|
||||||
<Tooltip label={isFullscreen ? '退出全屏' : '全屏'} placement="top">
|
bg="whiteAlpha.100"
|
||||||
<IconButton
|
borderRadius="xl"
|
||||||
size="sm"
|
backdropFilter="blur(10px)"
|
||||||
icon={isFullscreen ? <FaCompress /> : <FaExpand />}
|
border="1px solid"
|
||||||
onClick={toggleFullscreen}
|
borderColor="whiteAlpha.200"
|
||||||
variant="outline"
|
>
|
||||||
colorScheme="gray"
|
<Icon as={FaLayerGroup} color="purple.300" boxSize={5} />
|
||||||
aria-label={isFullscreen ? '退出全屏' : '全屏'}
|
</Box>
|
||||||
/>
|
<VStack align="start" spacing={0}>
|
||||||
</Tooltip>
|
<Text fontSize="lg" fontWeight="bold" color="white">
|
||||||
</HStack>
|
{getCurrentTitle()}
|
||||||
</Flex>
|
</Text>
|
||||||
|
<HStack spacing={2}>
|
||||||
{/* 面包屑导航 */}
|
<Text fontSize="xs" color="whiteAlpha.600">
|
||||||
<Flex
|
{getLevelDesc()} · {currentData.length} 项
|
||||||
align="center"
|
</Text>
|
||||||
mb={4}
|
{tradeDate && (
|
||||||
p={3}
|
<Badge
|
||||||
bg="white"
|
bg="blue.500"
|
||||||
borderRadius="xl"
|
color="white"
|
||||||
boxShadow="sm"
|
fontSize="xs"
|
||||||
flexWrap="wrap"
|
px={2}
|
||||||
gap={1}
|
borderRadius="full"
|
||||||
>
|
boxShadow="0 0 10px rgba(59, 130, 246, 0.5)"
|
||||||
{breadcrumbs.map((crumb, index) => (
|
>
|
||||||
<React.Fragment key={index}>
|
{tradeDate}
|
||||||
{index > 0 && (
|
</Badge>
|
||||||
<Icon as={FaChevronRight} color="gray.400" boxSize={3} mx={1} />
|
)}
|
||||||
)}
|
</HStack>
|
||||||
<Button
|
</VStack>
|
||||||
size="sm"
|
{priceLoading && (
|
||||||
variant={index === breadcrumbs.length - 1 ? 'solid' : 'ghost'}
|
<Spinner size="sm" color="purple.300" />
|
||||||
colorScheme={index === breadcrumbs.length - 1 ? 'purple' : 'gray'}
|
)}
|
||||||
leftIcon={index === 0 ? <FaHome /> : undefined}
|
</HStack>
|
||||||
onClick={() => handleBreadcrumbClick(crumb, index)}
|
|
||||||
isDisabled={index === breadcrumbs.length - 1}
|
<HStack spacing={2}>
|
||||||
fontWeight={index === breadcrumbs.length - 1 ? 'bold' : 'medium'}
|
<Tooltip label="刷新涨跌幅" placement="top">
|
||||||
borderRadius="lg"
|
<IconButton
|
||||||
>
|
size="sm"
|
||||||
{crumb.label}
|
icon={<FaSync />}
|
||||||
</Button>
|
onClick={handleRefreshPrice}
|
||||||
</React.Fragment>
|
isLoading={priceLoading}
|
||||||
))}
|
bg="whiteAlpha.100"
|
||||||
</Flex>
|
color="white"
|
||||||
|
border="1px solid"
|
||||||
{/* 图例说明 */}
|
borderColor="whiteAlpha.200"
|
||||||
<Flex
|
_hover={{ bg: 'whiteAlpha.200' }}
|
||||||
justify="center"
|
aria-label="刷新涨跌幅"
|
||||||
mb={4}
|
/>
|
||||||
gap={4}
|
</Tooltip>
|
||||||
flexWrap="wrap"
|
|
||||||
fontSize="sm"
|
<Tooltip label={isFullscreen ? '退出全屏' : '全屏'} placement="top">
|
||||||
>
|
<IconButton
|
||||||
<HStack spacing={2}>
|
size="sm"
|
||||||
<Box w={4} h={4} borderRadius="sm" bg="linear-gradient(135deg, #DC2626 0%, #EF4444 100%)" />
|
icon={isFullscreen ? <FaCompress /> : <FaExpand />}
|
||||||
<Text color="gray.600">涨</Text>
|
onClick={toggleFullscreen}
|
||||||
</HStack>
|
bg="whiteAlpha.100"
|
||||||
<HStack spacing={2}>
|
color="white"
|
||||||
<Box w={4} h={4} borderRadius="sm" bg="linear-gradient(135deg, #16A34A 0%, #22C55E 100%)" />
|
border="1px solid"
|
||||||
<Text color="gray.600">跌</Text>
|
borderColor="whiteAlpha.200"
|
||||||
</HStack>
|
_hover={{ bg: 'whiteAlpha.200' }}
|
||||||
<HStack spacing={2}>
|
aria-label={isFullscreen ? '退出全屏' : '全屏'}
|
||||||
<Box w={4} h={4} borderRadius="sm" bg="linear-gradient(135deg, #4B5563 0%, #6B7280 100%)" />
|
/>
|
||||||
<Text color="gray.600">平/无数据</Text>
|
</Tooltip>
|
||||||
</HStack>
|
</HStack>
|
||||||
{currentLevel !== 'concept' && (
|
</Flex>
|
||||||
<>
|
|
||||||
<Text color="gray.400">|</Text>
|
{/* 面包屑导航 */}
|
||||||
<Text color="gray.500">点击色块查看下级</Text>
|
<Flex
|
||||||
</>
|
align="center"
|
||||||
)}
|
mb={5}
|
||||||
{currentLevel === 'concept' && (
|
p={3}
|
||||||
<>
|
bg="whiteAlpha.50"
|
||||||
<Text color="gray.400">|</Text>
|
backdropFilter="blur(20px)"
|
||||||
<Text color="gray.500">点击查看概念详情</Text>
|
borderRadius="2xl"
|
||||||
</>
|
border="1px solid"
|
||||||
)}
|
borderColor="whiteAlpha.100"
|
||||||
</Flex>
|
flexWrap="wrap"
|
||||||
|
gap={1}
|
||||||
{/* 热力图网格 */}
|
>
|
||||||
<SimpleGrid columns={getGridColumns()} spacing={{ base: 3, md: 4 }}>
|
{breadcrumbs.map((crumb, index) => (
|
||||||
{currentData.map((item) => (
|
<React.Fragment key={index}>
|
||||||
<HeatmapBlock
|
{index > 0 && (
|
||||||
key={item.id || item.name}
|
<Icon as={FaChevronRight} color="whiteAlpha.400" boxSize={3} mx={1} />
|
||||||
item={item}
|
)}
|
||||||
onClick={handleBlockClick}
|
<Button
|
||||||
size={currentLevel === 'lv1' ? 'large' : currentLevel === 'concept' ? 'small' : 'normal'}
|
size="sm"
|
||||||
/>
|
variant="ghost"
|
||||||
))}
|
bg={index === breadcrumbs.length - 1 ? 'purple.500' : 'transparent'}
|
||||||
</SimpleGrid>
|
color={index === breadcrumbs.length - 1 ? 'white' : 'whiteAlpha.700'}
|
||||||
|
leftIcon={index === 0 ? <FaHome /> : undefined}
|
||||||
{/* 统计信息 */}
|
onClick={() => handleBreadcrumbClick(crumb, index)}
|
||||||
<Flex
|
isDisabled={index === breadcrumbs.length - 1}
|
||||||
justify="center"
|
fontWeight={index === breadcrumbs.length - 1 ? 'bold' : 'medium'}
|
||||||
mt={6}
|
borderRadius="xl"
|
||||||
gap={3}
|
_hover={index !== breadcrumbs.length - 1 ? { bg: 'whiteAlpha.100' } : {}}
|
||||||
flexWrap="wrap"
|
boxShadow={index === breadcrumbs.length - 1 ? '0 0 20px rgba(139, 92, 246, 0.5)' : 'none'}
|
||||||
>
|
>
|
||||||
<Badge
|
{crumb.label}
|
||||||
colorScheme="purple"
|
</Button>
|
||||||
px={4}
|
</React.Fragment>
|
||||||
py={2}
|
))}
|
||||||
borderRadius="full"
|
</Flex>
|
||||||
fontSize="sm"
|
|
||||||
|
{/* 图例说明 */}
|
||||||
|
<Flex
|
||||||
|
justify="center"
|
||||||
|
mb={5}
|
||||||
|
gap={4}
|
||||||
|
flexWrap="wrap"
|
||||||
|
fontSize="xs"
|
||||||
|
>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Box w={3} h={3} borderRadius="sm" bg="linear-gradient(135deg, rgba(239, 68, 68, 0.7) 0%, rgba(248, 113, 113, 0.5) 100%)" />
|
||||||
|
<Text color="whiteAlpha.600">涨</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Box w={3} h={3} borderRadius="sm" bg="linear-gradient(135deg, rgba(22, 163, 74, 0.7) 0%, rgba(74, 222, 128, 0.5) 100%)" />
|
||||||
|
<Text color="whiteAlpha.600">跌</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Box w={3} h={3} borderRadius="sm" bg="linear-gradient(135deg, rgba(71, 85, 105, 0.6) 0%, rgba(100, 116, 139, 0.4) 100%)" />
|
||||||
|
<Text color="whiteAlpha.600">平/无数据</Text>
|
||||||
|
</HStack>
|
||||||
|
<Text color="whiteAlpha.300">|</Text>
|
||||||
|
<Text color="whiteAlpha.500">
|
||||||
|
{currentLevel !== 'concept' ? '点击色块查看下级' : '点击查看概念详情'}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 热力图网格 */}
|
||||||
|
<SimpleGrid columns={getGridColumns()} spacing={{ base: 3, md: 4 }}>
|
||||||
|
{currentData.map((item) => (
|
||||||
|
<GlassCard
|
||||||
|
key={item.id || item.name}
|
||||||
|
item={item}
|
||||||
|
onClick={handleBlockClick}
|
||||||
|
size={currentLevel === 'lv1' ? 'large' : currentLevel === 'concept' ? 'small' : 'normal'}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
{/* 统计信息 */}
|
||||||
|
<Flex
|
||||||
|
justify="center"
|
||||||
|
mt={6}
|
||||||
|
gap={3}
|
||||||
|
flexWrap="wrap"
|
||||||
>
|
>
|
||||||
当前显示 {currentData.length} 个{getLevelDesc()}
|
|
||||||
</Badge>
|
|
||||||
{currentLevel === 'lv1' && (
|
|
||||||
<Badge
|
<Badge
|
||||||
colorScheme="cyan"
|
bg="purple.500"
|
||||||
|
color="white"
|
||||||
px={4}
|
px={4}
|
||||||
py={2}
|
py={2}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
|
boxShadow="0 0 20px rgba(139, 92, 246, 0.4)"
|
||||||
>
|
>
|
||||||
共 {hierarchy.reduce((acc, h) => acc + h.concept_count, 0)} 个概念
|
当前显示 {currentData.length} 个{getLevelDesc()}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
{currentLevel === 'lv1' && (
|
||||||
</Flex>
|
<Badge
|
||||||
|
bg="cyan.500"
|
||||||
|
color="white"
|
||||||
|
px={4}
|
||||||
|
py={2}
|
||||||
|
borderRadius="full"
|
||||||
|
fontSize="sm"
|
||||||
|
boxShadow="0 0 20px rgba(6, 182, 212, 0.4)"
|
||||||
|
>
|
||||||
|
共 {hierarchy.reduce((acc, h) => acc + h.concept_count, 0)} 个概念
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user