update pay ui
This commit is contained in:
@@ -749,6 +749,130 @@ async def get_hierarchy():
|
|||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# 注意:/hierarchy/price 必须在 /hierarchy/{lv1_id} 之前定义,否则会被错误匹配
|
||||||
|
@app.get("/hierarchy/price", tags=["Hierarchy"])
|
||||||
|
async def get_hierarchy_price_early(
|
||||||
|
trade_date: Optional[date] = Query(None, description="交易日期,默认最新"),
|
||||||
|
lv1_filter: Optional[str] = Query(None, description="筛选特定一级分类")
|
||||||
|
):
|
||||||
|
"""获取层级概念(lv1/lv2/lv3)的涨跌幅数据"""
|
||||||
|
logger.info(f"[hierarchy/price] 请求参数: trade_date={trade_date}, lv1_filter={lv1_filter}")
|
||||||
|
|
||||||
|
if not mysql_pool:
|
||||||
|
logger.error("[hierarchy/price] MySQL连接池不可用")
|
||||||
|
raise HTTPException(status_code=503, detail="MySQL连接不可用")
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with mysql_pool.acquire() as conn:
|
||||||
|
async with conn.cursor(aiomysql.DictCursor) as cursor:
|
||||||
|
# 获取交易日期
|
||||||
|
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')"
|
||||||
|
)
|
||||||
|
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']
|
||||||
|
|
||||||
|
logger.info(f"[hierarchy/price] 使用查询日期: {query_date}")
|
||||||
|
|
||||||
|
# 构建查询
|
||||||
|
base_query = """
|
||||||
|
SELECT concept_id, concept_name, concept_type, trade_date, avg_change_pct, stock_count
|
||||||
|
FROM concept_daily_stats
|
||||||
|
WHERE trade_date = %s AND concept_type = %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
if lv1_filter:
|
||||||
|
base_query += " AND concept_name LIKE %s"
|
||||||
|
|
||||||
|
base_query += " ORDER BY avg_change_pct DESC"
|
||||||
|
|
||||||
|
lv1_concepts = []
|
||||||
|
lv2_concepts = []
|
||||||
|
lv3_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:
|
||||||
|
lv1_concepts.append({
|
||||||
|
"concept_id": row['concept_id'],
|
||||||
|
"concept_name": row['concept_name'],
|
||||||
|
"concept_type": 'lv1',
|
||||||
|
"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']
|
||||||
|
})
|
||||||
|
|
||||||
|
# 查询 lv2
|
||||||
|
if lv1_filter:
|
||||||
|
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:
|
||||||
|
lv2_concepts.append({
|
||||||
|
"concept_id": row['concept_id'],
|
||||||
|
"concept_name": row['concept_name'],
|
||||||
|
"concept_type": 'lv2',
|
||||||
|
"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']
|
||||||
|
})
|
||||||
|
|
||||||
|
# 查询 lv3
|
||||||
|
if lv1_filter:
|
||||||
|
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:
|
||||||
|
lv3_concepts.append({
|
||||||
|
"concept_id": row['concept_id'],
|
||||||
|
"concept_name": row['concept_name'],
|
||||||
|
"concept_type": 'lv3',
|
||||||
|
"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']
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"[hierarchy/price] 返回结果: lv1={len(lv1_concepts)}, lv2={len(lv2_concepts)}, lv3={len(lv3_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)
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException as he:
|
||||||
|
logger.error(f"[hierarchy/price] HTTPException: {he.status_code} - {he.detail}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[hierarchy/price] 获取层级涨跌幅失败: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@app.get("/hierarchy/{lv1_id}", tags=["Hierarchy"])
|
@app.get("/hierarchy/{lv1_id}", tags=["Hierarchy"])
|
||||||
async def get_hierarchy_level(
|
async def get_hierarchy_level(
|
||||||
lv1_id: str,
|
lv1_id: str,
|
||||||
@@ -1494,12 +1618,33 @@ class HierarchyPriceResponse(BaseModel):
|
|||||||
total_count: int
|
total_count: int
|
||||||
|
|
||||||
|
|
||||||
@app.get("/hierarchy/price", response_model=HierarchyPriceResponse, tags=["Hierarchy"])
|
class ConceptPriceItem(BaseModel):
|
||||||
async def get_hierarchy_price(
|
"""概念价格项"""
|
||||||
|
concept_id: str
|
||||||
|
concept_name: str
|
||||||
|
concept_type: str # leaf/lv1/lv2/lv3
|
||||||
|
trade_date: date
|
||||||
|
avg_change_pct: Optional[float] = None
|
||||||
|
stock_count: Optional[int] = None
|
||||||
|
hierarchy: Optional[HierarchyInfo] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ConceptPriceListResponse(BaseModel):
|
||||||
|
"""概念价格列表响应"""
|
||||||
|
trade_date: date
|
||||||
|
total: int
|
||||||
|
concepts: List[ConceptPriceItem]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/price/list", response_model=ConceptPriceListResponse, tags=["Price"])
|
||||||
|
async def get_concept_price_list(
|
||||||
trade_date: Optional[date] = Query(None, description="交易日期,默认最新"),
|
trade_date: Optional[date] = Query(None, description="交易日期,默认最新"),
|
||||||
lv1_filter: Optional[str] = Query(None, description="筛选特定一级分类")
|
concept_type: Optional[str] = Query(None, description="概念类型: leaf/lv1/lv2/lv3,默认全部"),
|
||||||
|
sort_by: str = Query("change_desc", description="排序: change_desc(涨幅降序), change_asc(涨幅升序), stock_count(股票数)"),
|
||||||
|
limit: int = Query(100, ge=1, le=1000, description="返回数量"),
|
||||||
|
offset: int = Query(0, ge=0, description="偏移量")
|
||||||
):
|
):
|
||||||
"""获取层级概念(lv1/lv2/lv3)的涨跌幅数据"""
|
"""批量获取概念涨跌幅列表"""
|
||||||
if not mysql_pool:
|
if not mysql_pool:
|
||||||
raise HTTPException(status_code=503, detail="MySQL连接不可用")
|
raise HTTPException(status_code=503, detail="MySQL连接不可用")
|
||||||
|
|
||||||
@@ -1508,87 +1653,74 @@ async def get_hierarchy_price(
|
|||||||
async with conn.cursor(aiomysql.DictCursor) as cursor:
|
async with conn.cursor(aiomysql.DictCursor) as cursor:
|
||||||
# 获取交易日期
|
# 获取交易日期
|
||||||
if trade_date is None:
|
if trade_date is None:
|
||||||
await cursor.execute(
|
await cursor.execute("SELECT MAX(trade_date) as max_date FROM concept_daily_stats")
|
||||||
"SELECT MAX(trade_date) as max_date FROM concept_daily_stats WHERE concept_type != 'leaf'"
|
|
||||||
)
|
|
||||||
result = await cursor.fetchone()
|
result = await cursor.fetchone()
|
||||||
if not result or not result['max_date']:
|
if not result or not result['max_date']:
|
||||||
raise HTTPException(status_code=404, detail="无母概念涨跌幅数据")
|
raise HTTPException(status_code=404, detail="无涨跌幅数据")
|
||||||
trade_date = result['max_date']
|
trade_date = result['max_date']
|
||||||
|
|
||||||
# 构建查询
|
# 构建查询
|
||||||
base_query = """
|
where_conditions = ["trade_date = %s"]
|
||||||
|
params = [trade_date]
|
||||||
|
|
||||||
|
if concept_type and concept_type in ['leaf', 'lv1', 'lv2', 'lv3']:
|
||||||
|
where_conditions.append("concept_type = %s")
|
||||||
|
params.append(concept_type)
|
||||||
|
|
||||||
|
# 排序
|
||||||
|
order_clause = "avg_change_pct DESC"
|
||||||
|
if sort_by == "change_asc":
|
||||||
|
order_clause = "avg_change_pct ASC"
|
||||||
|
elif sort_by == "stock_count":
|
||||||
|
order_clause = "stock_count DESC"
|
||||||
|
|
||||||
|
# 获取总数
|
||||||
|
count_query = f"SELECT COUNT(*) as cnt FROM concept_daily_stats WHERE {' AND '.join(where_conditions)}"
|
||||||
|
await cursor.execute(count_query, params)
|
||||||
|
total = (await cursor.fetchone())['cnt']
|
||||||
|
|
||||||
|
# 获取数据
|
||||||
|
query = f"""
|
||||||
SELECT concept_id, concept_name, concept_type, trade_date, avg_change_pct, stock_count
|
SELECT concept_id, concept_name, concept_type, trade_date, avg_change_pct, stock_count
|
||||||
FROM concept_daily_stats
|
FROM concept_daily_stats
|
||||||
WHERE trade_date = %s AND concept_type = %s
|
WHERE {' AND '.join(where_conditions)}
|
||||||
|
ORDER BY {order_clause}
|
||||||
|
LIMIT %s OFFSET %s
|
||||||
"""
|
"""
|
||||||
|
params.extend([limit, offset])
|
||||||
|
await cursor.execute(query, params)
|
||||||
|
rows = await cursor.fetchall()
|
||||||
|
|
||||||
if lv1_filter:
|
concepts = []
|
||||||
base_query += " AND concept_name LIKE %s"
|
for row in rows:
|
||||||
|
concept_name = row['concept_name']
|
||||||
|
# 获取层级信息(仅对叶子概念)
|
||||||
|
hierarchy = None
|
||||||
|
if row['concept_type'] == 'leaf':
|
||||||
|
hierarchy_info = get_concept_hierarchy(concept_name)
|
||||||
|
if hierarchy_info:
|
||||||
|
hierarchy = HierarchyInfo(**hierarchy_info)
|
||||||
|
|
||||||
base_query += " ORDER BY avg_change_pct DESC"
|
concepts.append(ConceptPriceItem(
|
||||||
|
|
||||||
lv1_concepts = []
|
|
||||||
lv2_concepts = []
|
|
||||||
lv3_concepts = []
|
|
||||||
|
|
||||||
# 查询 lv1
|
|
||||||
if lv1_filter:
|
|
||||||
await cursor.execute(base_query, (trade_date, 'lv1', f"%{lv1_filter}%"))
|
|
||||||
else:
|
|
||||||
await cursor.execute(base_query, (trade_date, 'lv1'))
|
|
||||||
for row in await cursor.fetchall():
|
|
||||||
lv1_concepts.append(HierarchyPriceItem(
|
|
||||||
concept_id=row['concept_id'],
|
concept_id=row['concept_id'],
|
||||||
concept_name=row['concept_name'],
|
concept_name=concept_name,
|
||||||
concept_type='lv1',
|
concept_type=row['concept_type'],
|
||||||
trade_date=row['trade_date'],
|
trade_date=row['trade_date'],
|
||||||
avg_change_pct=float(row['avg_change_pct']) if row['avg_change_pct'] else None,
|
avg_change_pct=float(row['avg_change_pct']) if row['avg_change_pct'] else None,
|
||||||
stock_count=row['stock_count']
|
stock_count=row['stock_count'],
|
||||||
|
hierarchy=hierarchy
|
||||||
))
|
))
|
||||||
|
|
||||||
# 查询 lv2
|
return ConceptPriceListResponse(
|
||||||
if lv1_filter:
|
|
||||||
await cursor.execute(base_query, (trade_date, 'lv2', f"%{lv1_filter}%"))
|
|
||||||
else:
|
|
||||||
await cursor.execute(base_query, (trade_date, 'lv2'))
|
|
||||||
for row in await cursor.fetchall():
|
|
||||||
lv2_concepts.append(HierarchyPriceItem(
|
|
||||||
concept_id=row['concept_id'],
|
|
||||||
concept_name=row['concept_name'],
|
|
||||||
concept_type='lv2',
|
|
||||||
trade_date=row['trade_date'],
|
|
||||||
avg_change_pct=float(row['avg_change_pct']) if row['avg_change_pct'] else None,
|
|
||||||
stock_count=row['stock_count']
|
|
||||||
))
|
|
||||||
|
|
||||||
# 查询 lv3
|
|
||||||
if lv1_filter:
|
|
||||||
await cursor.execute(base_query, (trade_date, 'lv3', f"%{lv1_filter}%"))
|
|
||||||
else:
|
|
||||||
await cursor.execute(base_query, (trade_date, 'lv3'))
|
|
||||||
for row in await cursor.fetchall():
|
|
||||||
lv3_concepts.append(HierarchyPriceItem(
|
|
||||||
concept_id=row['concept_id'],
|
|
||||||
concept_name=row['concept_name'],
|
|
||||||
concept_type='lv3',
|
|
||||||
trade_date=row['trade_date'],
|
|
||||||
avg_change_pct=float(row['avg_change_pct']) if row['avg_change_pct'] else None,
|
|
||||||
stock_count=row['stock_count']
|
|
||||||
))
|
|
||||||
|
|
||||||
return HierarchyPriceResponse(
|
|
||||||
trade_date=trade_date,
|
trade_date=trade_date,
|
||||||
lv1_concepts=lv1_concepts,
|
total=total,
|
||||||
lv2_concepts=lv2_concepts,
|
concepts=concepts
|
||||||
lv3_concepts=lv3_concepts,
|
|
||||||
total_count=len(lv1_concepts) + len(lv2_concepts) + len(lv3_concepts)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取层级涨跌幅失败: {e}")
|
logger.error(f"获取概念涨跌幅列表失败: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 使用 CSS Grid + Chakra UI 实现热力图效果
|
* 使用 CSS Grid + Chakra UI 实现热力图效果
|
||||||
* 特性:
|
* 特性:
|
||||||
* 1. 炫酷的矩形热力图展示,涨红跌绿背景色
|
* 1. 炫酷的矩形热力图展示,涨红跌绿背景色
|
||||||
* 2. 点击 lv1 进入 lv2,点击 lv2 进入 lv3,层层钻取
|
* 2. 点击 lv1 进入 lv2,点击 lv2 进入 lv3,点击 lv3 进入具体概念
|
||||||
* 3. 集成 /hierarchy/price 接口获取实时涨跌幅
|
* 3. 集成 /hierarchy/price 接口获取实时涨跌幅
|
||||||
* 4. 每个分类有独特图标
|
* 4. 每个分类有独特图标
|
||||||
* 5. 支持面包屑导航返回上级
|
* 5. 支持面包屑导航返回上级
|
||||||
@@ -60,7 +60,7 @@ import {
|
|||||||
FaSatellite,
|
FaSatellite,
|
||||||
FaBatteryFull,
|
FaBatteryFull,
|
||||||
FaSolarPanel,
|
FaSolarPanel,
|
||||||
FaWind,
|
FaTags,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
|
|
||||||
@@ -111,47 +111,27 @@ const LV2_ICONS = {
|
|||||||
'军贸出海': FaGlobe,
|
'军贸出海': FaGlobe,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 一级分类基础颜色(用于渐变)
|
// 根据涨跌幅获取背景色(统一涨红跌绿)
|
||||||
const LV1_BASE_COLORS = {
|
const getChangeBgColor = (value) => {
|
||||||
'人工智能': { from: '#8B5CF6', to: '#A78BFA' },
|
|
||||||
'半导体': { from: '#3B82F6', to: '#60A5FA' },
|
|
||||||
'机器人': { from: '#10B981', to: '#34D399' },
|
|
||||||
'消费电子': { from: '#EC4899', to: '#F472B6' },
|
|
||||||
'智能驾驶与汽车': { from: '#F97316', to: '#FB923C' },
|
|
||||||
'新能源与电力': { from: '#22C55E', to: '#4ADE80' },
|
|
||||||
'空天经济': { from: '#06B6D4', to: '#22D3EE' },
|
|
||||||
'国防军工': { from: '#EF4444', to: '#F87171' },
|
|
||||||
'政策与主题': { from: '#F59E0B', to: '#FBBF24' },
|
|
||||||
'周期与材料': { from: '#6B7280', to: '#9CA3AF' },
|
|
||||||
'大消费': { from: '#F472B6', to: '#F9A8D4' },
|
|
||||||
'数字经济与金融科技': { from: '#6366F1', to: '#818CF8' },
|
|
||||||
'全球宏观与贸易': { from: '#14B8A6', to: '#2DD4BF' },
|
|
||||||
'医药健康': { from: '#84CC16', to: '#A3E635' },
|
|
||||||
'前沿科技': { from: '#A855F7', to: '#C084FC' },
|
|
||||||
};
|
|
||||||
|
|
||||||
// 根据涨跌幅获取背景色(涨红跌绿渐变)
|
|
||||||
const getChangeBgColor = (value, baseColor = null) => {
|
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
// 无数据时使用基础色
|
// 无数据时使用灰色
|
||||||
if (baseColor) {
|
return 'linear-gradient(135deg, #4B5563 0%, #6B7280 100%)';
|
||||||
return `linear-gradient(135deg, ${baseColor.from} 0%, ${baseColor.to} 100%)`;
|
|
||||||
}
|
|
||||||
return 'linear-gradient(135deg, #6B7280 0%, #9CA3AF 100%)';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 涨跌幅越大,颜色越深
|
// 涨跌幅越大,颜色越深 - 统一涨红跌绿
|
||||||
if (value > 5) return 'linear-gradient(135deg, #991B1B 0%, #DC2626 100%)';
|
if (value > 7) return 'linear-gradient(135deg, #7F1D1D 0%, #991B1B 100%)';
|
||||||
if (value > 3) return 'linear-gradient(135deg, #B91C1C 0%, #EF4444 100%)';
|
if (value > 5) return 'linear-gradient(135deg, #991B1B 0%, #B91C1C 100%)';
|
||||||
if (value > 1) return 'linear-gradient(135deg, #DC2626 0%, #F87171 100%)';
|
if (value > 3) return 'linear-gradient(135deg, #B91C1C 0%, #DC2626 100%)';
|
||||||
if (value > 0) return 'linear-gradient(135deg, #EF4444 0%, #FCA5A5 100%)';
|
if (value > 1) return 'linear-gradient(135deg, #DC2626 0%, #EF4444 100%)';
|
||||||
if (value < -5) return 'linear-gradient(135deg, #14532D 0%, #16A34A 100%)';
|
if (value > 0) return 'linear-gradient(135deg, #EF4444 0%, #F87171 100%)';
|
||||||
if (value < -3) return 'linear-gradient(135deg, #166534 0%, #22C55E 100%)';
|
if (value < -7) return 'linear-gradient(135deg, #14532D 0%, #166534 100%)';
|
||||||
if (value < -1) return 'linear-gradient(135deg, #16A34A 0%, #4ADE80 100%)';
|
if (value < -5) return 'linear-gradient(135deg, #166534 0%, #15803D 100%)';
|
||||||
if (value < 0) return 'linear-gradient(135deg, #22C55E 0%, #86EFAC 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%)';
|
||||||
|
|
||||||
// 平盘
|
// 平盘
|
||||||
return 'linear-gradient(135deg, #6B7280 0%, #9CA3AF 100%)';
|
return 'linear-gradient(135deg, #4B5563 0%, #6B7280 100%)';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 格式化涨跌幅
|
// 格式化涨跌幅
|
||||||
@@ -169,7 +149,16 @@ const getIcon = (name, level) => {
|
|||||||
if (level === 'lv2') {
|
if (level === 'lv2') {
|
||||||
return LV2_ICONS[name] || FaCubes;
|
return LV2_ICONS[name] || FaCubes;
|
||||||
}
|
}
|
||||||
return FaLightbulb;
|
if (level === 'lv3') {
|
||||||
|
return FaCubes;
|
||||||
|
}
|
||||||
|
return FaTags; // 具体概念用标签图标
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从 API 返回的名称中提取纯名称(去掉 [一级] [二级] [三级] 前缀)
|
||||||
|
const extractPureName = (apiName) => {
|
||||||
|
if (!apiName) return '';
|
||||||
|
return apiName.replace(/^\[(一级|二级|三级)\]\s*/, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 脉冲动画
|
// 脉冲动画
|
||||||
@@ -182,27 +171,26 @@ const pulseKeyframes = keyframes`
|
|||||||
* 单个热力图块组件
|
* 单个热力图块组件
|
||||||
*/
|
*/
|
||||||
const HeatmapBlock = ({ item, onClick, size = 'normal' }) => {
|
const HeatmapBlock = ({ item, onClick, size = 'normal' }) => {
|
||||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
|
||||||
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;
|
||||||
const isLargeChange = hasChange && Math.abs(item.avg_change_pct) > 3;
|
const isLargeChange = hasChange && Math.abs(item.avg_change_pct) > 3;
|
||||||
|
|
||||||
const IconComponent = getIcon(item.name, item.level);
|
const IconComponent = getIcon(item.name, item.level);
|
||||||
const baseColor = item.parentLv1
|
|
||||||
? LV1_BASE_COLORS[item.parentLv1]
|
|
||||||
: LV1_BASE_COLORS[item.name];
|
|
||||||
|
|
||||||
// 根据 size 调整高度
|
// 根据 size 调整高度
|
||||||
const heightMap = {
|
const heightMap = {
|
||||||
large: { base: '140px', md: '180px' },
|
large: { base: '140px', md: '160px' },
|
||||||
normal: { base: '120px', md: '150px' },
|
normal: { base: '110px', md: '130px' },
|
||||||
small: { base: '100px', md: '120px' },
|
small: { base: '90px', md: '100px' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 是否可点击进入下一层
|
||||||
|
const canDrillDown = item.level !== 'concept' && (item.children?.length > 0 || item.concepts?.length > 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
bg={getChangeBgColor(item.avg_change_pct, baseColor)}
|
bg={getChangeBgColor(item.avg_change_pct)}
|
||||||
borderRadius="xl"
|
borderRadius="xl"
|
||||||
p={{ base: 3, md: 4 }}
|
p={{ base: 3, md: 4 }}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
@@ -216,26 +204,17 @@ const HeatmapBlock = ({ item, onClick, size = 'normal' }) => {
|
|||||||
transform: 'translateY(-4px) scale(1.02)',
|
transform: 'translateY(-4px) scale(1.02)',
|
||||||
boxShadow: '0 12px 30px rgba(0, 0, 0, 0.3)',
|
boxShadow: '0 12px 30px rgba(0, 0, 0, 0.3)',
|
||||||
}}
|
}}
|
||||||
animation={isLargeChange ? `${pulseKeyframes} 2s infinite` : 'none'}
|
css={isLargeChange ? { animation: `${pulseKeyframes} 2s infinite` } : {}}
|
||||||
>
|
>
|
||||||
{/* 背景装饰 */}
|
{/* 背景装饰 */}
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top={-20}
|
top={-20}
|
||||||
right={-20}
|
right={-20}
|
||||||
width="100px"
|
|
||||||
height="100px"
|
|
||||||
borderRadius="full"
|
|
||||||
bg="whiteAlpha.100"
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
bottom={-30}
|
|
||||||
left={-30}
|
|
||||||
width="80px"
|
width="80px"
|
||||||
height="80px"
|
height="80px"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
bg="whiteAlpha.50"
|
bg="whiteAlpha.100"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 内容 */}
|
{/* 内容 */}
|
||||||
@@ -249,14 +228,14 @@ const HeatmapBlock = ({ item, onClick, size = 'normal' }) => {
|
|||||||
{/* 顶部:图标和名称 */}
|
{/* 顶部:图标和名称 */}
|
||||||
<HStack spacing={2} align="flex-start">
|
<HStack spacing={2} align="flex-start">
|
||||||
<Box
|
<Box
|
||||||
p={2}
|
p={1.5}
|
||||||
bg="whiteAlpha.200"
|
bg="whiteAlpha.200"
|
||||||
borderRadius="lg"
|
borderRadius="lg"
|
||||||
backdropFilter="blur(10px)"
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
as={IconComponent}
|
as={IconComponent}
|
||||||
boxSize={{ base: 4, md: 5 }}
|
boxSize={{ base: 3.5, md: 4 }}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -264,17 +243,15 @@ const HeatmapBlock = ({ item, onClick, size = 'normal' }) => {
|
|||||||
<Text
|
<Text
|
||||||
color="white"
|
color="white"
|
||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
fontSize={{ base: 'sm', md: 'md' }}
|
fontSize={{ base: 'xs', md: 'sm' }}
|
||||||
noOfLines={2}
|
noOfLines={2}
|
||||||
textShadow="0 2px 4px rgba(0,0,0,0.3)"
|
textShadow="0 2px 4px rgba(0,0,0,0.3)"
|
||||||
|
lineHeight="1.3"
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
{item.concept_count && (
|
{item.concept_count > 0 && (
|
||||||
<Text
|
<Text color="whiteAlpha.800" fontSize="xs">
|
||||||
color="whiteAlpha.800"
|
|
||||||
fontSize="xs"
|
|
||||||
>
|
|
||||||
{item.concept_count} 个概念
|
{item.concept_count} 个概念
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -284,7 +261,7 @@ const HeatmapBlock = ({ item, onClick, size = 'normal' }) => {
|
|||||||
{/* 底部:涨跌幅 */}
|
{/* 底部:涨跌幅 */}
|
||||||
<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 && (
|
{item.stock_count > 0 && (
|
||||||
<Text color="whiteAlpha.700" fontSize="xs">
|
<Text color="whiteAlpha.700" fontSize="xs">
|
||||||
{item.stock_count} 只股票
|
{item.stock_count} 只股票
|
||||||
</Text>
|
</Text>
|
||||||
@@ -292,9 +269,9 @@ const HeatmapBlock = ({ item, onClick, size = 'normal' }) => {
|
|||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
<HStack spacing={1} align="center">
|
<HStack spacing={1} align="center">
|
||||||
{hasChange && (
|
{hasChange && (isPositive || isNegative) && (
|
||||||
<Icon
|
<Icon
|
||||||
as={isPositive ? FaArrowUp : isNegative ? FaArrowDown : null}
|
as={isPositive ? FaArrowUp : FaArrowDown}
|
||||||
boxSize={3}
|
boxSize={3}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
@@ -302,7 +279,7 @@ const HeatmapBlock = ({ item, onClick, size = 'normal' }) => {
|
|||||||
<Text
|
<Text
|
||||||
color="white"
|
color="white"
|
||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
fontSize={{ base: 'lg', md: '2xl' }}
|
fontSize={{ base: 'md', md: 'xl' }}
|
||||||
textShadow="0 2px 8px rgba(0,0,0,0.4)"
|
textShadow="0 2px 8px rgba(0,0,0,0.4)"
|
||||||
>
|
>
|
||||||
{formatChangePercent(item.avg_change_pct)}
|
{formatChangePercent(item.avg_change_pct)}
|
||||||
@@ -312,7 +289,7 @@ const HeatmapBlock = ({ item, onClick, size = 'normal' }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* 可点击提示 */}
|
{/* 可点击提示 */}
|
||||||
{(item.children?.length > 0 || item.concepts?.length > 0) && (
|
{canDrillDown && (
|
||||||
<Badge
|
<Badge
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top={2}
|
top={2}
|
||||||
@@ -346,10 +323,11 @@ const HierarchyView = ({
|
|||||||
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);
|
||||||
|
const [currentLv3, setCurrentLv3] = useState(null);
|
||||||
const [breadcrumbs, setBreadcrumbs] = useState([{ label: '全部分类', level: 'root' }]);
|
const [breadcrumbs, setBreadcrumbs] = useState([{ label: '全部分类', level: 'root' }]);
|
||||||
|
|
||||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||||
@@ -397,18 +375,22 @@ 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 = {};
|
||||||
|
|
||||||
(data.lv1_concepts || []).forEach(item => {
|
(data.lv1_concepts || []).forEach(item => {
|
||||||
lv1Map[item.concept_name] = item;
|
const pureName = extractPureName(item.concept_name);
|
||||||
|
lv1Map[pureName] = item;
|
||||||
});
|
});
|
||||||
(data.lv2_concepts || []).forEach(item => {
|
(data.lv2_concepts || []).forEach(item => {
|
||||||
lv2Map[item.concept_name] = item;
|
const pureName = extractPureName(item.concept_name);
|
||||||
|
lv2Map[pureName] = item;
|
||||||
});
|
});
|
||||||
(data.lv3_concepts || []).forEach(item => {
|
(data.lv3_concepts || []).forEach(item => {
|
||||||
lv3Map[item.concept_name] = item;
|
const pureName = extractPureName(item.concept_name);
|
||||||
|
lv3Map[pureName] = item;
|
||||||
});
|
});
|
||||||
|
|
||||||
setPriceData({ lv1Map, lv2Map, lv3Map });
|
setPriceData({ lv1Map, lv2Map, lv3Map });
|
||||||
@@ -418,7 +400,8 @@ const 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,
|
||||||
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 });
|
||||||
@@ -441,6 +424,7 @@ const HierarchyView = ({
|
|||||||
const currentData = useMemo(() => {
|
const currentData = useMemo(() => {
|
||||||
const { lv1Map, lv2Map, lv3Map } = priceData;
|
const { lv1Map, lv2Map, lv3Map } = priceData;
|
||||||
|
|
||||||
|
// 第一层:显示所有 lv1
|
||||||
if (currentLevel === 'lv1') {
|
if (currentLevel === 'lv1') {
|
||||||
return hierarchy.map((lv1) => {
|
return hierarchy.map((lv1) => {
|
||||||
const price = lv1Map[lv1.name] || {};
|
const price = lv1Map[lv1.name] || {};
|
||||||
@@ -456,6 +440,7 @@ const HierarchyView = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 第二层:显示选中 lv1 下的 lv2
|
||||||
if (currentLevel === 'lv2' && currentLv1) {
|
if (currentLevel === 'lv2' && currentLv1) {
|
||||||
const lv1Data = hierarchy.find(h => h.name === currentLv1.name);
|
const lv1Data = hierarchy.find(h => h.name === currentLv1.name);
|
||||||
if (!lv1Data || !lv1Data.children) return [];
|
if (!lv1Data || !lv1Data.children) return [];
|
||||||
@@ -476,6 +461,7 @@ const HierarchyView = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 第三层:显示选中 lv2 下的 lv3
|
||||||
if (currentLevel === 'lv3' && currentLv1 && currentLv2) {
|
if (currentLevel === 'lv3' && currentLv1 && currentLv2) {
|
||||||
const lv1Data = hierarchy.find(h => h.name === currentLv1.name);
|
const lv1Data = hierarchy.find(h => h.name === currentLv1.name);
|
||||||
if (!lv1Data || !lv1Data.children) return [];
|
if (!lv1Data || !lv1Data.children) return [];
|
||||||
@@ -483,6 +469,7 @@ const HierarchyView = ({
|
|||||||
const lv2Data = lv1Data.children.find(h => h.name === currentLv2.name);
|
const lv2Data = lv1Data.children.find(h => h.name === currentLv2.name);
|
||||||
if (!lv2Data) return [];
|
if (!lv2Data) return [];
|
||||||
|
|
||||||
|
// 如果有 lv3 子级
|
||||||
if (lv2Data.children && lv2Data.children.length > 0) {
|
if (lv2Data.children && lv2Data.children.length > 0) {
|
||||||
return lv2Data.children.map((lv3) => {
|
return lv2Data.children.map((lv3) => {
|
||||||
const price = lv3Map[lv3.name] || {};
|
const price = lv3Map[lv3.name] || {};
|
||||||
@@ -500,6 +487,7 @@ const HierarchyView = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果 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((concept) => ({
|
||||||
name: concept,
|
name: concept,
|
||||||
@@ -512,14 +500,35 @@ const HierarchyView = ({
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 第四层:显示选中 lv3 下的具体概念
|
||||||
|
if (currentLevel === 'concept' && currentLv1 && currentLv2 && currentLv3) {
|
||||||
|
const lv1Data = hierarchy.find(h => h.name === currentLv1.name);
|
||||||
|
if (!lv1Data || !lv1Data.children) return [];
|
||||||
|
|
||||||
|
const lv2Data = lv1Data.children.find(h => h.name === currentLv2.name);
|
||||||
|
if (!lv2Data || !lv2Data.children) return [];
|
||||||
|
|
||||||
|
const lv3Data = lv2Data.children.find(h => h.name === currentLv3.name);
|
||||||
|
if (!lv3Data || !lv3Data.concepts) return [];
|
||||||
|
|
||||||
|
return lv3Data.concepts.map((concept) => ({
|
||||||
|
name: concept,
|
||||||
|
level: 'concept',
|
||||||
|
parentLv1: currentLv1.name,
|
||||||
|
parentLv2: currentLv2.name,
|
||||||
|
parentLv3: currentLv3.name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}, [hierarchy, priceData, currentLevel, currentLv1, currentLv2]);
|
}, [hierarchy, priceData, currentLevel, currentLv1, currentLv2, currentLv3]);
|
||||||
|
|
||||||
// 处理点击事件 - 钻取
|
// 处理点击事件 - 钻取
|
||||||
const handleBlockClick = useCallback((item) => {
|
const handleBlockClick = useCallback((item) => {
|
||||||
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([
|
||||||
@@ -527,7 +536,9 @@ 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') {
|
||||||
if ((item.children && item.children.length > 0) || (item.concepts && item.concepts.length > 0)) {
|
// 检查是否有 lv3 或直接是概念
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
// 进入 lv3
|
||||||
setCurrentLevel('lv3');
|
setCurrentLevel('lv3');
|
||||||
setCurrentLv2(item);
|
setCurrentLv2(item);
|
||||||
setBreadcrumbs([
|
setBreadcrumbs([
|
||||||
@@ -535,9 +546,32 @@ const HierarchyView = ({
|
|||||||
{ label: currentLv1.name, level: 'lv1', data: currentLv1 },
|
{ label: currentLv1.name, level: 'lv1', data: currentLv1 },
|
||||||
{ label: item.name, level: 'lv2', data: item },
|
{ label: item.name, level: 'lv2', data: item },
|
||||||
]);
|
]);
|
||||||
|
} else if (item.concepts && item.concepts.length > 0) {
|
||||||
|
// lv2 直接包含概念,进入概念层
|
||||||
|
setCurrentLevel('lv3'); // 实际显示概念
|
||||||
|
setCurrentLv2(item);
|
||||||
|
setBreadcrumbs([
|
||||||
|
{ label: '全部分类', level: 'root' },
|
||||||
|
{ label: currentLv1.name, level: 'lv1', data: currentLv1 },
|
||||||
|
{ label: item.name, level: 'lv2', data: item },
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
} else if (item.level === 'lv3' && item.concepts && item.concepts.length > 0) {
|
||||||
|
// 进入具体概念层
|
||||||
|
setCurrentLevel('concept');
|
||||||
|
setCurrentLv3(item);
|
||||||
|
setBreadcrumbs([
|
||||||
|
{ label: '全部分类', level: 'root' },
|
||||||
|
{ label: currentLv1.name, level: 'lv1', data: currentLv1 },
|
||||||
|
{ label: currentLv2.name, level: 'lv2', data: currentLv2 },
|
||||||
|
{ label: item.name, level: 'lv3', data: item },
|
||||||
|
]);
|
||||||
|
} else if (item.level === 'concept') {
|
||||||
|
// 点击具体概念,跳转到概念详情页
|
||||||
|
const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(item.name)}.html`;
|
||||||
|
window.open(htmlPath, '_blank');
|
||||||
}
|
}
|
||||||
}, [currentLv1]);
|
}, [currentLv1, currentLv2]);
|
||||||
|
|
||||||
// 面包屑导航
|
// 面包屑导航
|
||||||
const handleBreadcrumbClick = useCallback((crumb, index) => {
|
const handleBreadcrumbClick = useCallback((crumb, index) => {
|
||||||
@@ -545,11 +579,18 @@ const HierarchyView = ({
|
|||||||
setCurrentLevel('lv1');
|
setCurrentLevel('lv1');
|
||||||
setCurrentLv1(null);
|
setCurrentLv1(null);
|
||||||
setCurrentLv2(null);
|
setCurrentLv2(null);
|
||||||
|
setCurrentLv3(null);
|
||||||
setBreadcrumbs([{ label: '全部分类', level: 'root' }]);
|
setBreadcrumbs([{ label: '全部分类', level: 'root' }]);
|
||||||
} else if (crumb.level === 'lv1') {
|
} else if (crumb.level === 'lv1') {
|
||||||
setCurrentLevel('lv2');
|
setCurrentLevel('lv2');
|
||||||
setCurrentLv1(crumb.data);
|
setCurrentLv1(crumb.data);
|
||||||
setCurrentLv2(null);
|
setCurrentLv2(null);
|
||||||
|
setCurrentLv3(null);
|
||||||
|
setBreadcrumbs(breadcrumbs.slice(0, index + 1));
|
||||||
|
} else if (crumb.level === 'lv2') {
|
||||||
|
setCurrentLevel('lv3');
|
||||||
|
setCurrentLv2(crumb.data);
|
||||||
|
setCurrentLv3(null);
|
||||||
setBreadcrumbs(breadcrumbs.slice(0, index + 1));
|
setBreadcrumbs(breadcrumbs.slice(0, index + 1));
|
||||||
}
|
}
|
||||||
}, [breadcrumbs]);
|
}, [breadcrumbs]);
|
||||||
@@ -567,18 +608,27 @@ const HierarchyView = ({
|
|||||||
// 获取当前层级标题
|
// 获取当前层级标题
|
||||||
const getCurrentTitle = () => {
|
const getCurrentTitle = () => {
|
||||||
if (currentLevel === 'lv1') return '概念分类热力图';
|
if (currentLevel === 'lv1') return '概念分类热力图';
|
||||||
if (currentLevel === 'lv2' && currentLv1) return `${currentLv1.name}`;
|
if (currentLevel === 'lv2' && currentLv1) return currentLv1.name;
|
||||||
if (currentLevel === 'lv3' && currentLv2) return `${currentLv2.name}`;
|
if (currentLevel === 'lv3' && currentLv2) return currentLv2.name;
|
||||||
|
if (currentLevel === 'concept' && currentLv3) return currentLv3.name;
|
||||||
return '概念分类';
|
return '概念分类';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取当前层级描述
|
||||||
|
const getLevelDesc = () => {
|
||||||
|
const levelNames = {
|
||||||
|
lv1: '一级分类',
|
||||||
|
lv2: '二级分类',
|
||||||
|
lv3: '三级分类',
|
||||||
|
concept: '具体概念',
|
||||||
|
};
|
||||||
|
return levelNames[currentLevel] || '';
|
||||||
|
};
|
||||||
|
|
||||||
// 计算列数
|
// 计算列数
|
||||||
const getGridColumns = () => {
|
const getGridColumns = () => {
|
||||||
if (currentLevel === 'lv1') {
|
if (currentLevel === 'concept') {
|
||||||
return { base: 2, md: 3, lg: 4 };
|
return { base: 2, md: 4, lg: 5 }; // 概念层更多列
|
||||||
}
|
|
||||||
if (currentLevel === 'lv2') {
|
|
||||||
return { base: 2, md: 3, lg: 4 };
|
|
||||||
}
|
}
|
||||||
return { base: 2, md: 3, lg: 4 };
|
return { base: 2, md: 3, lg: 4 };
|
||||||
};
|
};
|
||||||
@@ -641,11 +691,16 @@ const HierarchyView = ({
|
|||||||
>
|
>
|
||||||
<HStack spacing={3}>
|
<HStack spacing={3}>
|
||||||
<Icon as={FaLayerGroup} color="purple.500" boxSize={6} />
|
<Icon as={FaLayerGroup} color="purple.500" boxSize={6} />
|
||||||
<Text fontSize="xl" fontWeight="bold" color="gray.800">
|
<VStack align="start" spacing={0}>
|
||||||
|
<Text fontSize="lg" fontWeight="bold" color="gray.800">
|
||||||
{getCurrentTitle()}
|
{getCurrentTitle()}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
{getLevelDesc()} · {currentData.length} 项
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
{tradeDate && (
|
{tradeDate && (
|
||||||
<Badge colorScheme="blue" fontSize="sm" px={3} py={1} borderRadius="full">
|
<Badge colorScheme="blue" fontSize="xs" px={2} py={1} borderRadius="full">
|
||||||
{tradeDate}
|
{tradeDate}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -721,29 +776,39 @@ const HierarchyView = ({
|
|||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
>
|
>
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
<Box w={4} h={4} borderRadius="sm" bg="linear-gradient(135deg, #DC2626 0%, #F87171 100%)" />
|
<Box w={4} h={4} borderRadius="sm" bg="linear-gradient(135deg, #DC2626 0%, #EF4444 100%)" />
|
||||||
<Text color="gray.600">涨</Text>
|
<Text color="gray.600">涨</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
<Box w={4} h={4} borderRadius="sm" bg="linear-gradient(135deg, #22C55E 0%, #4ADE80 100%)" />
|
<Box w={4} h={4} borderRadius="sm" bg="linear-gradient(135deg, #16A34A 0%, #22C55E 100%)" />
|
||||||
<Text color="gray.600">跌</Text>
|
<Text color="gray.600">跌</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
<Box w={4} h={4} borderRadius="sm" bg="linear-gradient(135deg, #6B7280 0%, #9CA3AF 100%)" />
|
<Box w={4} h={4} borderRadius="sm" bg="linear-gradient(135deg, #4B5563 0%, #6B7280 100%)" />
|
||||||
<Text color="gray.600">平</Text>
|
<Text color="gray.600">平/无数据</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Text color="gray.500">|</Text>
|
{currentLevel !== 'concept' && (
|
||||||
<Text color="gray.500">点击色块查看下级分类</Text>
|
<>
|
||||||
|
<Text color="gray.400">|</Text>
|
||||||
|
<Text color="gray.500">点击色块查看下级</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{currentLevel === 'concept' && (
|
||||||
|
<>
|
||||||
|
<Text color="gray.400">|</Text>
|
||||||
|
<Text color="gray.500">点击查看概念详情</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* 热力图网格 */}
|
{/* 热力图网格 */}
|
||||||
<SimpleGrid columns={getGridColumns()} spacing={{ base: 3, md: 4 }}>
|
<SimpleGrid columns={getGridColumns()} spacing={{ base: 3, md: 4 }}>
|
||||||
{currentData.map((item, index) => (
|
{currentData.map((item) => (
|
||||||
<HeatmapBlock
|
<HeatmapBlock
|
||||||
key={item.id || item.name}
|
key={item.id || item.name}
|
||||||
item={item}
|
item={item}
|
||||||
onClick={handleBlockClick}
|
onClick={handleBlockClick}
|
||||||
size={currentLevel === 'lv1' ? 'large' : 'normal'}
|
size={currentLevel === 'lv1' ? 'large' : currentLevel === 'concept' ? 'small' : 'normal'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
@@ -762,8 +827,7 @@ const HierarchyView = ({
|
|||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
>
|
>
|
||||||
当前显示 {currentData.length} 个
|
当前显示 {currentData.length} 个{getLevelDesc()}
|
||||||
{currentLevel === 'lv1' ? '一级' : currentLevel === 'lv2' ? '二级' : '三级'}分类
|
|
||||||
</Badge>
|
</Badge>
|
||||||
{currentLevel === 'lv1' && (
|
{currentLevel === 'lv1' && (
|
||||||
<Badge
|
<Badge
|
||||||
|
|||||||
Reference in New Issue
Block a user