From 98e975e755bde11a26215bad7a1f68a9b125da47 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Fri, 5 Dec 2025 13:29:18 +0800 Subject: [PATCH] update pay ui --- concept_api_v2.py | 1667 +++++++++++++++++ concept_hierarchy_v3.json | 1275 +++++++++++++ max.docx | Bin 0 -> 31217 bytes pro.docx | Bin 0 -> 30873 bytes src/mocks/handlers/concept.js | 179 ++ src/views/Concept/components/BreadcrumbNav.js | 170 ++ src/views/Concept/components/HierarchyView.js | 709 +++++++ src/views/Concept/index.js | 226 ++- 8 files changed, 4177 insertions(+), 49 deletions(-) create mode 100644 concept_api_v2.py create mode 100644 concept_hierarchy_v3.json create mode 100644 max.docx create mode 100644 pro.docx create mode 100644 src/views/Concept/components/BreadcrumbNav.js create mode 100644 src/views/Concept/components/HierarchyView.js diff --git a/concept_api_v2.py b/concept_api_v2.py new file mode 100644 index 00000000..896f5d5e --- /dev/null +++ b/concept_api_v2.py @@ -0,0 +1,1667 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +概念搜索API V2 - 适配 concept_library_v3 索引 +新特性: +1. 支持层级结构(lv1/lv2/lv3) +2. 返回 tags, outbreak_dates, insight 等新字段 +3. 新增层级浏览接口 +""" +import json +import openai +from typing import List, Dict, Optional, Union, Any +from fastapi import FastAPI, HTTPException, Query +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field +from elasticsearch import Elasticsearch +from datetime import datetime, date +import logging +import re +from contextlib import asynccontextmanager +import aiomysql +import os + +# 配置日志 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# ==================== 配置 ==================== + +# ES配置 +ES_HOST = 'http://127.0.0.1:9200' +INDEX_NAME = 'concept_library_v3' + +# Embedding配置 +OPENAI_BASE_URL = "http://127.0.0.1:8000/v1" +OPENAI_API_KEY = "dummy" +EMBEDDING_MODEL = "qwen3-embedding-8b" + +# 层级结构文件 +HIERARCHY_FILE = 'concept_hierarchy_v3.json' + +# MySQL配置 +MYSQL_CONFIG = { + 'host': '192.168.1.8', + 'port': 3306, + 'user': 'root', + 'password': 'Zzl5588161!', + 'db': 'stock', + 'charset': 'utf8mb4', + 'autocommit': True, + 'minsize': 1, + 'maxsize': 10 +} + +# ==================== 全局变量 ==================== + +es_client = None +openai_client = None +mysql_pool = None + +# 层级结构相关 +hierarchy_data = {} # 原始层级数据 +concept_to_hierarchy = {} # 概念名称 -> 层级信息的映射 + + +def load_hierarchy(): + """加载层级结构并建立概念到层级的映射""" + global hierarchy_data, concept_to_hierarchy + + hierarchy_path = os.path.join(os.path.dirname(__file__), HIERARCHY_FILE) + if not os.path.exists(hierarchy_path): + logger.warning(f"层级文件不存在: {hierarchy_path}") + return + + try: + with open(hierarchy_path, 'r', encoding='utf-8') as f: + hierarchy_data = json.load(f) + + # 遍历层级结构,建立概念到层级的映射 + for lv1 in hierarchy_data.get('hierarchy', []): + lv1_name = lv1.get('lv1', '') + lv1_id = lv1.get('lv1_id', '') + + for child in lv1.get('children', []): + lv2_name = child.get('lv2', '') + lv2_id = child.get('lv2_id', '') + + # 检查是否有 lv3 子级 + if 'children' in child: + for lv3_child in child.get('children', []): + lv3_name = lv3_child.get('lv3', '') + lv3_id = lv3_child.get('lv3_id', '') + + # lv3 级别的概念 + for concept in lv3_child.get('concepts', []): + concept_to_hierarchy[concept] = { + 'lv1': lv1_name, + 'lv1_id': lv1_id, + 'lv2': lv2_name, + 'lv2_id': lv2_id, + 'lv3': lv3_name, + 'lv3_id': lv3_id + } + else: + # lv2 级别直接有概念(没有 lv3) + for concept in child.get('concepts', []): + concept_to_hierarchy[concept] = { + 'lv1': lv1_name, + 'lv1_id': lv1_id, + 'lv2': lv2_name, + 'lv2_id': lv2_id, + 'lv3': None, + 'lv3_id': None + } + + logger.info(f"加载层级结构完成,共 {len(concept_to_hierarchy)} 个概念有层级信息") + + except Exception as e: + logger.error(f"加载层级结构失败: {e}") + + +def get_concept_hierarchy(concept_name: str) -> Optional[Dict]: + """获取概念的层级信息""" + return concept_to_hierarchy.get(concept_name) + + +# ==================== 生命周期管理 ==================== + +@asynccontextmanager +async def lifespan(app: FastAPI): + global es_client, openai_client, mysql_pool + + # 加载层级结构 + load_hierarchy() + + # 初始化ES客户端 + es_client = Elasticsearch( + [ES_HOST], + timeout=30, + max_retries=3, + retry_on_timeout=True + ) + logger.info(f"Connected to Elasticsearch at {ES_HOST}, index: {INDEX_NAME}") + + # 初始化OpenAI客户端 + openai_client = openai.OpenAI( + api_key=OPENAI_API_KEY, + base_url=OPENAI_BASE_URL, + timeout=60, + ) + logger.info(f"Initialized OpenAI client") + + # 初始化MySQL连接池 + try: + mysql_pool = await aiomysql.create_pool(**MYSQL_CONFIG) + logger.info(f"Connected to MySQL at {MYSQL_CONFIG['host']}") + except Exception as e: + logger.error(f"Failed to connect to MySQL: {e}") + + yield + + # 清理资源 + if es_client: + es_client.close() + if mysql_pool: + mysql_pool.close() + await mysql_pool.wait_closed() + logger.info("Cleanup completed") + + +# ==================== FastAPI 应用 ==================== + +app = FastAPI( + title="概念搜索API V2", + description="支持层级结构的概念库搜索API,适配 concept_library_v3 索引", + version="2.0.0", + lifespan=lifespan +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# ==================== 数据模型 ==================== + +class HierarchyInfo(BaseModel): + """层级信息""" + lv1: Optional[str] = Field(None, description="一级分类") + lv1_id: Optional[str] = None + lv2: Optional[str] = Field(None, description="二级分类") + lv2_id: Optional[str] = None + lv3: Optional[str] = Field(None, description="三级分类") + lv3_id: Optional[str] = None + + +class StockInfo(BaseModel): + """股票信息""" + name: str = Field(..., description="股票名称") + code: Optional[str] = Field(None, description="股票代码") + + +class ConceptPriceInfo(BaseModel): + """概念涨跌幅信息""" + trade_date: date + avg_change_pct: Optional[float] = Field(None, description="平均涨跌幅(%)") + + +class SearchRequest(BaseModel): + """搜索请求""" + query: str = Field(..., description="搜索查询文本") + size: int = Field(10, ge=1, le=100, description="每页返回结果数量") + page: int = Field(1, ge=1, description="页码") + search_size: int = Field(100, ge=10, le=1000, description="搜索数量") + semantic_weight: Optional[float] = Field(None, ge=0.0, le=1.0, description="语义搜索权重") + filter_stocks: Optional[List[str]] = Field(None, description="过滤特定股票") + filter_lv1: Optional[str] = Field(None, description="过滤一级分类") + filter_lv2: Optional[str] = Field(None, description="过滤二级分类") + trade_date: Optional[date] = Field(None, description="交易日期") + sort_by: str = Field("_score", description="排序: _score, change_pct, stock_count, outbreak_date") + use_knn: bool = Field(True, description="是否使用KNN搜索") + + +class ConceptResult(BaseModel): + """概念搜索结果""" + concept_id: str + concept: str + description: Optional[str] = None + tags: Optional[List[str]] = Field(None, description="标签") + outbreak_dates: Optional[List[str]] = Field(None, description="爆发日期") + stocks: List[StockInfo] = Field(default_factory=list) + stock_count: int = 0 + hierarchy: Optional[HierarchyInfo] = Field(None, description="层级信息") + score: float = 0.0 + match_type: str = "keyword" + highlights: Optional[Dict[str, List[str]]] = None + price_info: Optional[ConceptPriceInfo] = None + + +class SearchResponse(BaseModel): + """搜索响应""" + total: int + took_ms: int + results: List[ConceptResult] + search_info: Dict[str, Any] + price_date: Optional[date] = None + page: int = 1 + total_pages: int = 1 + + +class HierarchyLevel(BaseModel): + """层级节点""" + id: str + name: str + concept_count: int = 0 + children: Optional[List['HierarchyLevel']] = None + concepts: Optional[List[str]] = None + + +class HierarchyResponse(BaseModel): + """层级结构响应""" + hierarchy: List[HierarchyLevel] + total_concepts: int + + +class ConceptDetailResponse(BaseModel): + """概念详情响应""" + concept_id: str + concept: str + description: Optional[str] = None + insight: Optional[str] = None + tags: Optional[List[str]] = None + outbreak_dates: Optional[List[str]] = None + stocks: List[Dict[str, Any]] = Field(default_factory=list) + stock_count: int = 0 + hierarchy: Optional[HierarchyInfo] = None + folders: Optional[List[str]] = None + created_at: Optional[str] = None + price_info: Optional[ConceptPriceInfo] = None + + +# ==================== 辅助函数 ==================== + +def generate_embedding(text: str) -> List[float]: + """生成文本向量""" + try: + if not text or len(text.strip()) == 0: + return [] + + text = text[:8000] if len(text) > 8000 else text + + if not openai_client: + return [] + + response = openai_client.embeddings.create( + model=EMBEDDING_MODEL, + input=[text] + ) + return response.data[0].embedding + + except Exception as e: + logger.warning(f"Embedding生成失败: {e}") + return [] + + +def calculate_semantic_weight(query: str) -> float: + """根据查询长度动态计算语义权重""" + query_length = len(query) + + if query_length < 10: + return 0.3 + elif query_length < 50: + return 0.5 + elif query_length < 200: + return 0.6 + else: + return 0.7 + + +async def get_concept_price_data(concept_ids: List[str], trade_date: Optional[date] = None) -> Dict[str, ConceptPriceInfo]: + """获取概念的涨跌幅数据""" + if not mysql_pool or not concept_ids: + return {} + + try: + async with mysql_pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cursor: + placeholders = ','.join(['%s'] * len(concept_ids)) + + if trade_date: + query = f""" + SELECT concept_id, trade_date, avg_change_pct + FROM concept_daily_stats + WHERE concept_id IN ({placeholders}) AND trade_date = %s + """ + await cursor.execute(query, (*concept_ids, trade_date)) + else: + query = f""" + SELECT cds.concept_id, cds.trade_date, cds.avg_change_pct + FROM concept_daily_stats cds + INNER JOIN ( + SELECT concept_id, MAX(trade_date) as max_date + FROM concept_daily_stats + WHERE concept_id IN ({placeholders}) + GROUP BY concept_id + ) latest ON cds.concept_id = latest.concept_id + AND cds.trade_date = latest.max_date + """ + await cursor.execute(query, concept_ids) + + rows = await cursor.fetchall() + + result = {} + for row in rows: + result[row['concept_id']] = ConceptPriceInfo( + trade_date=row['trade_date'], + avg_change_pct=float(row['avg_change_pct']) if row['avg_change_pct'] is not None else None + ) + return result + + except Exception as e: + logger.error(f"获取涨跌幅数据失败: {e}") + return {} + + +def build_keyword_query(query: str, stock_filters: List[str] = None) -> Dict: + """构建关键词查询 - 适配 v3 索引""" + must_queries = [] + + if query.strip(): + must_queries.append({ + "multi_match": { + "query": query, + "fields": [ + "concept^3", + "description^2", + "tags.text^2", + "tags^1.5" + ], + "type": "best_fields", + "analyzer": "ik_smart" + } + }) + + if stock_filters: + stock_query = { + "nested": { + "path": "stocks", + "query": { + "bool": { + "should": [ + {"terms": {"stocks.name": stock_filters}}, + {"terms": {"stocks.code": stock_filters}} + ] + } + } + } + } + must_queries.append(stock_query) + + return { + "bool": { + "must": must_queries if must_queries else [{"match_all": {}}] + } + } + + +def parse_stocks_from_es(source: Dict) -> tuple: + """从ES文档解析股票信息""" + # nested stocks 字段(只有 name, code) + stocks_nested = source.get('stocks', []) + stocks_list = [] + + for s in stocks_nested[:30]: # 限制返回数量 + stocks_list.append(StockInfo( + name=s.get('name', ''), + code=s.get('code') + )) + + # 完整股票信息从 stocks_json 解析 + stocks_full = [] + stocks_json = source.get('stocks_json', '') + if stocks_json: + try: + stocks_full = json.loads(stocks_json) + except: + pass + + return stocks_list, stocks_full + + +# ==================== API 端点 ==================== + +@app.get("/", tags=["Health"]) +async def root(): + """健康检查""" + return { + "status": "healthy", + "service": "概念搜索API V2", + "version": "2.0.0", + "index": INDEX_NAME, + "hierarchy_concepts": len(concept_to_hierarchy) + } + + +@app.post("/search", response_model=SearchResponse, tags=["Search"]) +async def search_concepts(request: SearchRequest): + """ + 搜索概念 - 支持语义搜索和层级过滤 + """ + start_time = datetime.now() + + try: + # 计算语义权重 + if request.semantic_weight is not None: + semantic_weight = request.semantic_weight + else: + semantic_weight = calculate_semantic_weight(request.query) + + # 生成embedding + embedding = [] + if semantic_weight > 0: + embedding = generate_embedding(request.query) + if not embedding: + semantic_weight = 0 + + # 确定搜索数量 + effective_search_size = request.search_size + if request.sort_by in ["change_pct", "outbreak_date"]: + effective_search_size = min(500, request.search_size * 5) + + # 构建查询 + search_body = {} + match_type = "keyword" + + if semantic_weight == 0: + # 纯关键词搜索 + search_body = { + "query": build_keyword_query(request.query, request.filter_stocks), + "size": effective_search_size + } + match_type = "keyword" + + elif request.use_knn and embedding: + # KNN + 关键词混合搜索 + keyword_weight = 1.0 - semantic_weight + + search_body = { + "knn": { + "field": "description_embedding", + "query_vector": embedding, + "k": effective_search_size, + "num_candidates": min(effective_search_size * 2, 10000), + "boost": semantic_weight + }, + "query": { + "bool": { + "must": [build_keyword_query(request.query, request.filter_stocks)], + "boost": keyword_weight + } + }, + "size": effective_search_size + } + match_type = "hybrid_knn" + + else: + # 传统混合搜索 + search_body = { + "query": build_keyword_query(request.query, request.filter_stocks), + "size": effective_search_size + } + match_type = "keyword" + + # 添加高亮和源过滤 + search_body.update({ + "highlight": { + "fields": { + "concept": {}, + "description": {"fragment_size": 150}, + "tags": {} + } + }, + "_source": { + "excludes": ["description_embedding", "insight"] + }, + "track_total_hits": True + }) + + # 执行搜索 + es_response = es_client.search( + index=INDEX_NAME, + body=search_body, + timeout="30s" + ) + + # 处理结果 + all_results = [] + concept_ids = [] + + for hit in es_response['hits']['hits']: + source = hit['_source'] + concept_name = source.get('concept', '') + concept_id = source.get('concept_id', '') + concept_ids.append(concept_id) + + # 解析股票 + stocks_list, _ = parse_stocks_from_es(source) + + # 获取层级信息 + hierarchy_info = get_concept_hierarchy(concept_name) + hierarchy = None + if hierarchy_info: + hierarchy = HierarchyInfo(**hierarchy_info) + + # 层级过滤 + if request.filter_lv1 and (not hierarchy_info or hierarchy_info.get('lv1') != request.filter_lv1): + continue + if request.filter_lv2 and (not hierarchy_info or hierarchy_info.get('lv2') != request.filter_lv2): + continue + + # 高亮信息 + highlights = hit.get('highlight', {}) + + result = ConceptResult( + concept_id=concept_id, + concept=concept_name, + description=source.get('description'), + tags=source.get('tags', []), + outbreak_dates=source.get('outbreak_dates', []), + stocks=stocks_list, + stock_count=len(source.get('stocks', [])), + hierarchy=hierarchy, + score=hit['_score'] or 0, + match_type=match_type, + highlights=highlights, + price_info=None + ) + all_results.append(result) + + # 获取涨跌幅数据 + price_data = {} + actual_price_date = None + + if concept_ids: + price_data = await get_concept_price_data(concept_ids, request.trade_date) + if price_data: + actual_price_date = next(iter(price_data.values())).trade_date + + for result in all_results: + result.price_info = price_data.get(result.concept_id) + + # 排序 + if request.sort_by == "change_pct": + all_results.sort( + key=lambda x: ( + x.price_info.avg_change_pct + if x.price_info and x.price_info.avg_change_pct is not None + else -999 + ), + reverse=True + ) + elif request.sort_by == "stock_count": + all_results.sort(key=lambda x: x.stock_count, reverse=True) + elif request.sort_by == "outbreak_date": + all_results.sort( + key=lambda x: ( + x.outbreak_dates[0] if x.outbreak_dates else '1900-01-01' + ), + reverse=True + ) + + # 分页 + total_results = len(all_results) + total_pages = max(1, (total_results + request.size - 1) // request.size) + current_page = min(request.page, total_pages) + + start_idx = (current_page - 1) * request.size + end_idx = start_idx + request.size + page_results = all_results[start_idx:end_idx] + + took_ms = int((datetime.now() - start_time).total_seconds() * 1000) + + return SearchResponse( + total=es_response['hits']['total']['value'], + took_ms=took_ms, + results=page_results, + search_info={ + "query": request.query, + "semantic_weight": semantic_weight, + "match_type": match_type, + "filter_lv1": request.filter_lv1, + "filter_lv2": request.filter_lv2, + "sort_by": request.sort_by, + "has_embedding": bool(embedding) + }, + price_date=actual_price_date, + page=current_page, + total_pages=total_pages + ) + + except Exception as e: + logger.error(f"搜索失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/concept/{concept_id}", response_model=ConceptDetailResponse, tags=["Concepts"]) +async def get_concept_detail( + concept_id: str, + trade_date: Optional[date] = Query(None, description="交易日期") +): + """获取概念详情""" + try: + result = es_client.get(index=INDEX_NAME, id=concept_id) + source = result['_source'] + + concept_name = source.get('concept', '') + + # 解析完整股票信息 + _, stocks_full = parse_stocks_from_es(source) + + # 获取层级信息 + hierarchy_info = get_concept_hierarchy(concept_name) + hierarchy = HierarchyInfo(**hierarchy_info) if hierarchy_info else None + + # 获取涨跌幅 + price_data = await get_concept_price_data([concept_id], trade_date) + price_info = price_data.get(concept_id) + + return ConceptDetailResponse( + concept_id=concept_id, + concept=concept_name, + description=source.get('description'), + insight=source.get('insight'), + tags=source.get('tags', []), + outbreak_dates=source.get('outbreak_dates', []), + stocks=stocks_full, + stock_count=len(stocks_full), + hierarchy=hierarchy, + folders=source.get('folders', []), + created_at=source.get('created_at'), + price_info=price_info + ) + + except Exception as e: + if "NotFoundError" in str(type(e)): + raise HTTPException(status_code=404, detail="概念不存在") + logger.error(f"获取概念详情失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/hierarchy", response_model=HierarchyResponse, tags=["Hierarchy"]) +async def get_hierarchy(): + """获取完整层级结构""" + try: + result = [] + + for lv1 in hierarchy_data.get('hierarchy', []): + lv1_node = HierarchyLevel( + id=lv1.get('lv1_id', ''), + name=lv1.get('lv1', ''), + concept_count=0, + children=[] + ) + + for child in lv1.get('children', []): + lv2_node = HierarchyLevel( + id=child.get('lv2_id', ''), + name=child.get('lv2', ''), + concept_count=0, + children=[] + ) + + if 'children' in child: + # 有 lv3 + for lv3_child in child.get('children', []): + concepts = lv3_child.get('concepts', []) + lv3_node = HierarchyLevel( + id=lv3_child.get('lv3_id', ''), + name=lv3_child.get('lv3', ''), + concept_count=len(concepts), + concepts=concepts + ) + lv2_node.children.append(lv3_node) + lv2_node.concept_count += len(concepts) + else: + # lv2 直接有概念 + concepts = child.get('concepts', []) + lv2_node.concept_count = len(concepts) + lv2_node.concepts = concepts + + lv1_node.children.append(lv2_node) + lv1_node.concept_count += lv2_node.concept_count + + result.append(lv1_node) + + total_concepts = sum(lv1.concept_count for lv1 in result) + + return HierarchyResponse( + hierarchy=result, + total_concepts=total_concepts + ) + + except Exception as e: + logger.error(f"获取层级结构失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/hierarchy/{lv1_id}", tags=["Hierarchy"]) +async def get_hierarchy_level( + lv1_id: str, + lv2_id: Optional[str] = Query(None, description="二级分类ID") +): + """获取指定层级的概念列表""" + try: + for lv1 in hierarchy_data.get('hierarchy', []): + if lv1.get('lv1_id') == lv1_id: + if lv2_id: + # 返回指定 lv2 下的概念 + for child in lv1.get('children', []): + if child.get('lv2_id') == lv2_id: + concepts = [] + if 'children' in child: + for lv3_child in child.get('children', []): + concepts.extend(lv3_child.get('concepts', [])) + else: + concepts = child.get('concepts', []) + + return { + "lv1": lv1.get('lv1'), + "lv1_id": lv1_id, + "lv2": child.get('lv2'), + "lv2_id": lv2_id, + "concepts": concepts, + "concept_count": len(concepts) + } + raise HTTPException(status_code=404, detail=f"未找到二级分类: {lv2_id}") + else: + # 返回 lv1 下所有概念 + concepts = [] + for child in lv1.get('children', []): + if 'children' in child: + for lv3_child in child.get('children', []): + concepts.extend(lv3_child.get('concepts', [])) + else: + concepts.extend(child.get('concepts', [])) + + return { + "lv1": lv1.get('lv1'), + "lv1_id": lv1_id, + "concepts": concepts, + "concept_count": len(concepts), + "children": lv1.get('children', []) + } + + raise HTTPException(status_code=404, detail=f"未找到一级分类: {lv1_id}") + + except HTTPException: + raise + except Exception as e: + logger.error(f"获取层级详情失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/stock/{stock_code}/concepts", tags=["Stocks"]) +async def get_stock_concepts( + stock_code: str, + size: int = Query(50, ge=1, le=200), + trade_date: Optional[date] = Query(None) +): + """根据股票代码查询相关概念""" + try: + query = { + "nested": { + "path": "stocks", + "query": { + "bool": { + "should": [ + {"term": {"stocks.code": stock_code}}, + {"term": {"stocks.name": stock_code}} + ] + } + } + } + } + + search_body = { + "query": query, + "size": size, + "_source": { + "excludes": ["description_embedding", "insight", "stocks_json"] + } + } + + es_response = es_client.search(index=INDEX_NAME, body=search_body) + + concept_ids = [] + concepts = [] + + for hit in es_response['hits']['hits']: + source = hit['_source'] + concept_name = source.get('concept', '') + concept_id = source.get('concept_id', '') + concept_ids.append(concept_id) + + # 获取层级 + hierarchy_info = get_concept_hierarchy(concept_name) + + concepts.append({ + "concept_id": concept_id, + "concept": concept_name, + "description": source.get('description'), + "tags": source.get('tags', []), + "stock_count": len(source.get('stocks', [])), + "hierarchy": hierarchy_info, + "outbreak_dates": source.get('outbreak_dates', []) + }) + + # 获取涨跌幅 + price_data = await get_concept_price_data(concept_ids, trade_date) + for concept in concepts: + concept['price_info'] = price_data.get(concept['concept_id']) + + return { + "stock_code": stock_code, + "total": es_response['hits']['total']['value'], + "concepts": concepts + } + + except Exception as e: + logger.error(f"查询股票概念失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/statistics/hierarchy", tags=["Statistics"]) +async def get_hierarchy_statistics(): + """获取层级统计信息""" + try: + stats = [] + + for lv1 in hierarchy_data.get('hierarchy', []): + lv1_stats = { + "lv1": lv1.get('lv1'), + "lv1_id": lv1.get('lv1_id'), + "lv2_count": len(lv1.get('children', [])), + "concept_count": 0, + "children": [] + } + + for child in lv1.get('children', []): + lv2_concept_count = 0 + if 'children' in child: + for lv3_child in child.get('children', []): + lv2_concept_count += len(lv3_child.get('concepts', [])) + else: + lv2_concept_count = len(child.get('concepts', [])) + + lv1_stats['children'].append({ + "lv2": child.get('lv2'), + "lv2_id": child.get('lv2_id'), + "concept_count": lv2_concept_count + }) + lv1_stats['concept_count'] += lv2_concept_count + + stats.append(lv1_stats) + + # 按概念数量排序 + stats.sort(key=lambda x: x['concept_count'], reverse=True) + + return { + "total_lv1": len(stats), + "total_concepts": sum(s['concept_count'] for s in stats), + "statistics": stats + } + + except Exception as e: + logger.error(f"获取层级统计失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +# ==================== 股票搜索相关 ==================== + +class StockSearchResult(BaseModel): + """股票搜索结果""" + stock_code: str + stock_name: str + concept_count: int + + +class StockSearchResponse(BaseModel): + """股票搜索响应""" + total: int + stocks: List[StockSearchResult] + + +@app.get("/stock/search", response_model=StockSearchResponse, tags=["Stocks"]) +async def search_stocks( + keyword: str = Query(..., description="股票关键词(代码或名称)"), + size: int = Query(20, ge=1, le=100, description="返回数量") +): + """搜索股票名称或代码""" + try: + # v3 索引中股票字段是 stocks.name 和 stocks.code + search_body = { + "query": { + "nested": { + "path": "stocks", + "query": { + "bool": { + "should": [ + {"wildcard": {"stocks.code": f"*{keyword}*"}}, + {"match": {"stocks.name": keyword}} + ] + } + } + } + }, + "size": 0, + "aggs": { + "unique_stocks": { + "nested": { + "path": "stocks" + }, + "aggs": { + "stock_codes": { + "terms": { + "field": "stocks.code", + "size": size * 2 + }, + "aggs": { + "stock_names": { + "terms": { + "field": "stocks.name", + "size": 1 + } + } + } + } + } + } + } + } + + es_response = es_client.search(index=INDEX_NAME, body=search_body) + + stocks = [] + buckets = es_response['aggregations']['unique_stocks']['stock_codes']['buckets'] + + for bucket in buckets: + stock_code = bucket['key'] + concept_count = bucket['doc_count'] + + name_buckets = bucket['stock_names']['buckets'] + stock_name = name_buckets[0]['key'] if name_buckets else stock_code + + if keyword in stock_code or keyword in stock_name: + stocks.append(StockSearchResult( + stock_code=stock_code, + stock_name=stock_name, + concept_count=concept_count + )) + + stocks.sort(key=lambda x: x.concept_count, reverse=True) + + return StockSearchResponse( + total=len(stocks), + stocks=stocks[:size] + ) + + except Exception as e: + logger.error(f"搜索股票失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +# ==================== 价格相关接口 ==================== + +class PriceTimeSeriesItem(BaseModel): + """价格时间序列数据点""" + trade_date: date + avg_change_pct: Optional[float] = Field(None, description="平均涨跌幅(%)") + stock_count: Optional[int] = Field(None, description="当日股票数量") + + +class PriceTimeSeriesResponse(BaseModel): + """价格时间序列响应""" + concept_id: str + concept_name: str + start_date: date + end_date: date + data_points: int + timeseries: List[PriceTimeSeriesItem] + + +async def get_latest_trade_date() -> Optional[date]: + """获取最新的交易日期""" + if not mysql_pool: + return None + + try: + async with mysql_pool.acquire() as conn: + async with conn.cursor() as cursor: + query = "SELECT MAX(trade_date) as latest_date FROM concept_daily_stats" + await cursor.execute(query) + result = await cursor.fetchone() + return result[0] if result and result[0] else None + except Exception as e: + logger.error(f"获取最新交易日期失败: {e}") + return None + + +@app.get("/price/latest", tags=["Price"]) +async def get_latest_price_date(): + """获取最新的涨跌幅数据日期""" + try: + latest_date = await get_latest_trade_date() + return { + "latest_trade_date": latest_date, + "has_data": latest_date is not None + } + except Exception as e: + logger.error(f"获取最新价格日期失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/concept/{concept_id}/price-timeseries", response_model=PriceTimeSeriesResponse, tags=["Price"]) +async def get_concept_price_timeseries( + concept_id: str, + start_date: date = Query(..., description="开始日期,格式:YYYY-MM-DD"), + end_date: date = Query(..., description="结束日期,格式:YYYY-MM-DD") +): + """获取概念在指定日期范围内的涨跌幅时间序列数据""" + if not mysql_pool: + logger.warning(f"[PriceTimeseries] MySQL 连接不可用") + return PriceTimeSeriesResponse( + concept_id=concept_id, + concept_name=concept_id, + start_date=start_date, + end_date=end_date, + data_points=0, + timeseries=[] + ) + + if start_date > end_date: + raise HTTPException(status_code=400, detail="开始日期不能晚于结束日期") + + try: + async with mysql_pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cursor: + query = """ + SELECT trade_date, concept_name, avg_change_pct, stock_count + FROM concept_daily_stats + WHERE concept_id = %s + AND trade_date >= %s + AND trade_date <= %s + ORDER BY trade_date ASC + """ + + await cursor.execute(query, (concept_id, start_date, end_date)) + rows = await cursor.fetchall() + + if not rows: + raise HTTPException( + status_code=404, + detail=f"未找到概念ID {concept_id} 在 {start_date} 至 {end_date} 期间的数据" + ) + + timeseries = [] + concept_name = "" + + for row in rows: + if not concept_name and row.get('concept_name'): + concept_name = row['concept_name'] + + item = PriceTimeSeriesItem( + trade_date=row['trade_date'], + avg_change_pct=float(row['avg_change_pct']) if row['avg_change_pct'] is not None else None, + stock_count=row.get('stock_count') + ) + timeseries.append(item) + + return PriceTimeSeriesResponse( + concept_id=concept_id, + concept_name=concept_name or concept_id, + start_date=start_date, + end_date=end_date, + data_points=len(timeseries), + timeseries=timeseries + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"获取价格时间序列失败: {e}") + raise HTTPException(status_code=500, detail=f"获取时间序列数据失败: {str(e)}") + + +# ==================== 统计接口 ==================== + +class ConceptStatItem(BaseModel): + """概念统计项""" + name: str + concept_id: Optional[str] = None + change_pct: Optional[float] = None + stock_count: Optional[int] = None + news_count: Optional[int] = None + report_count: Optional[int] = None + total_mentions: Optional[int] = None + volatility: Optional[float] = None + avg_change: Optional[float] = None + max_change: Optional[float] = None + consecutive_days: Optional[int] = None + total_change: Optional[float] = None + avg_daily: Optional[float] = None + hierarchy: Optional[HierarchyInfo] = None + + +class ConceptStatistics(BaseModel): + """概念统计数据""" + hot_concepts: List[ConceptStatItem] + cold_concepts: List[ConceptStatItem] + active_concepts: List[ConceptStatItem] + volatile_concepts: List[ConceptStatItem] + momentum_concepts: List[ConceptStatItem] + summary: Dict[str, Any] + + +class ConceptStatisticsResponse(BaseModel): + """概念统计响应""" + success: bool + data: ConceptStatistics + params: Dict[str, Any] + note: Optional[str] = None + + +def get_fallback_statistics(start_date, end_date) -> ConceptStatistics: + """返回示例统计数据""" + return ConceptStatistics( + hot_concepts=[ + ConceptStatItem(name="人工智能", change_pct=8.76, stock_count=45, news_count=12), + ConceptStatItem(name="机器人", change_pct=7.54, stock_count=38, news_count=10), + ConceptStatItem(name="半导体", change_pct=6.43, stock_count=52, news_count=15), + ConceptStatItem(name="低空经济", change_pct=5.21, stock_count=28, news_count=6), + ConceptStatItem(name="量子科技", change_pct=4.98, stock_count=15, news_count=9), + ], + cold_concepts=[ + ConceptStatItem(name="房地产", change_pct=-5.76, stock_count=33, news_count=5), + ConceptStatItem(name="煤炭", change_pct=-4.32, stock_count=25, news_count=3), + ConceptStatItem(name="钢铁", change_pct=-3.21, stock_count=28, news_count=4), + ConceptStatItem(name="传统零售", change_pct=-2.98, stock_count=19, news_count=2), + ConceptStatItem(name="纺织服装", change_pct=-2.45, stock_count=15, news_count=2), + ], + active_concepts=[ + ConceptStatItem(name="人工智能", news_count=45, report_count=15, total_mentions=60), + ConceptStatItem(name="半导体", news_count=42, report_count=12, total_mentions=54), + ConceptStatItem(name="机器人", news_count=38, report_count=8, total_mentions=46), + ConceptStatItem(name="新能源", news_count=28, report_count=6, total_mentions=34), + ConceptStatItem(name="量子科技", news_count=25, report_count=5, total_mentions=30), + ], + volatile_concepts=[ + ConceptStatItem(name="数字货币", volatility=25.6, avg_change=2.1, max_change=15.2), + ConceptStatItem(name="元宇宙", volatility=23.8, avg_change=1.8, max_change=13.9), + ConceptStatItem(name="虚拟现实", volatility=21.2, avg_change=-0.5, max_change=10.1), + ConceptStatItem(name="游戏", volatility=19.7, avg_change=3.2, max_change=12.8), + ConceptStatItem(name="短剧", volatility=18.3, avg_change=-1.1, max_change=8.1), + ], + momentum_concepts=[ + ConceptStatItem(name="AI应用", consecutive_days=6, total_change=19.2, avg_daily=3.2), + ConceptStatItem(name="算力", consecutive_days=5, total_change=16.8, avg_daily=3.36), + ConceptStatItem(name="光通信", consecutive_days=4, total_change=13.1, avg_daily=3.28), + ConceptStatItem(name="存储", consecutive_days=4, total_change=12.4, avg_daily=3.1), + ConceptStatItem(name="PCB", consecutive_days=3, total_change=9.6, avg_daily=3.2), + ], + summary={ + 'total_concepts': 500, + 'positive_count': 320, + 'negative_count': 180, + 'avg_change': 1.8, + 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'date_range': f"{start_date} 至 {end_date}", + 'days': (end_date - start_date).days + 1 if start_date and end_date else 7, + 'start_date': str(start_date) if start_date else '', + 'end_date': str(end_date) if end_date else '' + } + ) + + +@app.get("/statistics", response_model=ConceptStatisticsResponse, tags=["Statistics"]) +async def get_concept_statistics( + days: Optional[int] = Query(None, ge=1, le=90, description="统计天数范围"), + start_date: Optional[date] = Query(None, description="开始日期"), + end_date: Optional[date] = Query(None, description="结束日期"), + min_stock_count: int = Query(3, ge=1, description="最少股票数量过滤"), + concept_type: Optional[str] = Query(None, description="概念类型过滤: leaf/lv1/lv2/lv3,默认为leaf") +): + """获取概念板块统计数据 - 涨幅榜、跌幅榜、活跃榜、波动榜、连涨榜""" + from datetime import timedelta + + # 计算日期范围 + if days is not None and (start_date is not None or end_date is not None): + raise HTTPException(status_code=400, detail="days参数与start_date/end_date参数不能同时使用") + + if start_date is not None and end_date is not None: + if start_date > end_date: + raise HTTPException(status_code=400, detail="开始日期不能晚于结束日期") + if (end_date - start_date).days > 90: + raise HTTPException(status_code=400, detail="日期范围不能超过90天") + elif days is not None: + end_date = datetime.now().date() + start_date = end_date - timedelta(days=days) + elif start_date is not None: + end_date = datetime.now().date() + elif end_date is not None: + start_date = end_date - timedelta(days=7) + else: + end_date = datetime.now().date() + start_date = end_date - timedelta(days=7) + + if not mysql_pool: + logger.warning("[Statistics] MySQL 连接不可用,使用示例数据") + return ConceptStatisticsResponse( + success=True, + data=get_fallback_statistics(start_date, end_date), + params={ + 'days': (end_date - start_date).days + 1, + 'min_stock_count': min_stock_count, + 'start_date': str(start_date), + 'end_date': str(end_date) + }, + note="MySQL 连接不可用,使用示例数据" + ) + + # 默认只查询叶子概念 + type_filter = concept_type if concept_type in ['leaf', 'lv1', 'lv2', 'lv3'] else 'leaf' + + try: + async with mysql_pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cursor: + + # 1. 涨幅榜 + hot_query = """ + SELECT concept_id, concept_name, + AVG(avg_change_pct) as avg_change_pct, + AVG(stock_count) as avg_stock_count, + COUNT(*) as trading_days + FROM concept_daily_stats + WHERE trade_date >= %s AND trade_date <= %s + AND avg_change_pct IS NOT NULL AND stock_count >= %s + AND concept_type = %s + GROUP BY concept_id, concept_name + HAVING COUNT(*) >= 2 + ORDER BY AVG(avg_change_pct) DESC + LIMIT 10 + """ + await cursor.execute(hot_query, (start_date, end_date, min_stock_count, type_filter)) + hot_rows = await cursor.fetchall() + + # 2. 跌幅榜 + cold_query = """ + SELECT concept_id, concept_name, + AVG(avg_change_pct) as avg_change_pct, + AVG(stock_count) as avg_stock_count, + COUNT(*) as trading_days + FROM concept_daily_stats + WHERE trade_date >= %s AND trade_date <= %s + AND avg_change_pct IS NOT NULL AND stock_count >= %s + AND concept_type = %s + GROUP BY concept_id, concept_name + HAVING COUNT(*) >= 2 + ORDER BY AVG(avg_change_pct) ASC + LIMIT 10 + """ + await cursor.execute(cold_query, (start_date, end_date, min_stock_count, type_filter)) + cold_rows = await cursor.fetchall() + + # 3. 活跃榜 + active_query = """ + SELECT concept_id, concept_name, + COUNT(*) as trading_days, + AVG(stock_count) as avg_stock_count + FROM concept_daily_stats + WHERE trade_date >= %s AND trade_date <= %s AND stock_count >= %s + AND concept_type = %s + GROUP BY concept_id, concept_name + ORDER BY COUNT(*) DESC, AVG(stock_count) DESC + LIMIT 10 + """ + await cursor.execute(active_query, (start_date, end_date, min_stock_count, type_filter)) + active_rows = await cursor.fetchall() + + # 4. 波动榜 + volatile_query = """ + SELECT concept_id, concept_name, + STDDEV(avg_change_pct) as volatility, + AVG(avg_change_pct) as avg_change_pct, + MAX(avg_change_pct) as max_change_pct + FROM concept_daily_stats + WHERE trade_date >= %s AND trade_date <= %s + AND avg_change_pct IS NOT NULL AND stock_count >= %s + AND concept_type = %s + GROUP BY concept_id, concept_name + HAVING COUNT(*) >= 3 AND STDDEV(avg_change_pct) IS NOT NULL + ORDER BY STDDEV(avg_change_pct) DESC + LIMIT 10 + """ + await cursor.execute(volatile_query, (start_date, end_date, min_stock_count, type_filter)) + volatile_rows = await cursor.fetchall() + + # 5. 连涨榜 + momentum_query = """ + SELECT concept_id, concept_name, + COUNT(*) as positive_days, + SUM(avg_change_pct) as total_change, + AVG(avg_change_pct) as avg_change_pct + FROM concept_daily_stats + WHERE trade_date >= %s AND trade_date <= %s + AND avg_change_pct > 0 AND stock_count >= %s + AND concept_type = %s + GROUP BY concept_id, concept_name + HAVING COUNT(*) >= 2 + ORDER BY COUNT(*) DESC, AVG(avg_change_pct) DESC + LIMIT 10 + """ + await cursor.execute(momentum_query, (start_date, end_date, min_stock_count, type_filter)) + momentum_rows = await cursor.fetchall() + + # 6. 总体统计 + total_query = """ + SELECT COUNT(DISTINCT concept_id) as total_concepts, + COUNT(DISTINCT CASE WHEN avg_change_pct > 0 THEN concept_id END) as positive_concepts, + COUNT(DISTINCT CASE WHEN avg_change_pct < 0 THEN concept_id END) as negative_concepts, + AVG(avg_change_pct) as overall_avg_change + FROM concept_daily_stats + WHERE trade_date >= %s AND trade_date <= %s AND avg_change_pct IS NOT NULL + AND concept_type = %s + """ + await cursor.execute(total_query, (start_date, end_date, type_filter)) + total_row = await cursor.fetchone() + + # 构建响应 + def build_items(rows, item_type): + items = [] + for row in rows: + concept_name = row['concept_name'] + hierarchy_info = get_concept_hierarchy(concept_name) + + item = ConceptStatItem( + name=concept_name, + concept_id=row.get('concept_id'), + hierarchy=HierarchyInfo(**hierarchy_info) if hierarchy_info else None + ) + + if item_type in ['hot', 'cold']: + item.change_pct = round(float(row['avg_change_pct']), 2) if row['avg_change_pct'] else 0.0 + item.stock_count = int(row['avg_stock_count']) if row['avg_stock_count'] else 0 + item.news_count = int(row['trading_days']) if row['trading_days'] else 0 + + elif item_type == 'active': + item.news_count = int(row['trading_days']) if row['trading_days'] else 0 + item.stock_count = int(row['avg_stock_count']) if row['avg_stock_count'] else 0 + item.report_count = max(1, int(row['trading_days'] * 0.3)) if row['trading_days'] else 1 + item.total_mentions = item.news_count + item.report_count + + elif item_type == 'volatile': + item.volatility = round(float(row['volatility']), 2) if row['volatility'] else 0.0 + item.avg_change = round(float(row['avg_change_pct']), 2) if row['avg_change_pct'] else 0.0 + item.max_change = round(float(row['max_change_pct']), 2) if row['max_change_pct'] else 0.0 + + elif item_type == 'momentum': + item.consecutive_days = int(row['positive_days']) if row['positive_days'] else 0 + item.total_change = round(float(row['total_change']), 2) if row['total_change'] else 0.0 + item.avg_daily = round(float(row['avg_change_pct']), 2) if row['avg_change_pct'] else 0.0 + + items.append(item) + return items + + statistics = ConceptStatistics( + hot_concepts=build_items(hot_rows, 'hot'), + cold_concepts=build_items(cold_rows, 'cold'), + active_concepts=build_items(active_rows, 'active'), + volatile_concepts=build_items(volatile_rows, 'volatile'), + momentum_concepts=build_items(momentum_rows, 'momentum'), + summary={ + 'total_concepts': int(total_row['total_concepts']) if total_row['total_concepts'] else 0, + 'positive_count': int(total_row['positive_concepts']) if total_row['positive_concepts'] else 0, + 'negative_count': int(total_row['negative_concepts']) if total_row['negative_concepts'] else 0, + 'avg_change': round(float(total_row['overall_avg_change']), 2) if total_row['overall_avg_change'] else 0.0, + 'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'date_range': f"{start_date} 至 {end_date}", + 'days': (end_date - start_date).days + 1, + 'start_date': str(start_date), + 'end_date': str(end_date) + } + ) + + # 补充空数据 + if not statistics.hot_concepts or not statistics.cold_concepts: + fallback = get_fallback_statistics(start_date, end_date) + if not statistics.hot_concepts: + statistics.hot_concepts = fallback.hot_concepts + if not statistics.cold_concepts: + statistics.cold_concepts = fallback.cold_concepts + + return ConceptStatisticsResponse( + success=True, + data=statistics, + params={ + 'days': (end_date - start_date).days + 1, + 'min_stock_count': min_stock_count, + 'start_date': str(start_date), + 'end_date': str(end_date), + 'concept_type': type_filter + } + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"获取概念统计失败: {e}") + return ConceptStatisticsResponse( + success=True, + data=get_fallback_statistics(start_date, end_date), + params={ + 'days': (end_date - start_date).days + 1, + 'min_stock_count': min_stock_count, + 'start_date': str(start_date), + 'end_date': str(end_date), + 'concept_type': type_filter + }, + note=f"使用示例数据,原因: {str(e)}" + ) + + +# ==================== 层级概念涨跌幅接口 ==================== + +class HierarchyPriceItem(BaseModel): + """层级概念价格项""" + concept_id: str + concept_name: str + concept_type: str # lv1/lv2/lv3 + trade_date: date + avg_change_pct: Optional[float] = None + stock_count: Optional[int] = None + + +class HierarchyPriceResponse(BaseModel): + """层级概念价格响应""" + trade_date: date + lv1_concepts: List[HierarchyPriceItem] + lv2_concepts: List[HierarchyPriceItem] + lv3_concepts: List[HierarchyPriceItem] + total_count: int + + +@app.get("/hierarchy/price", response_model=HierarchyPriceResponse, tags=["Hierarchy"]) +async def get_hierarchy_price( + trade_date: Optional[date] = Query(None, description="交易日期,默认最新"), + lv1_filter: Optional[str] = Query(None, description="筛选特定一级分类") +): + """获取层级概念(lv1/lv2/lv3)的涨跌幅数据""" + if not mysql_pool: + raise HTTPException(status_code=503, detail="MySQL连接不可用") + + try: + async with mysql_pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cursor: + # 获取交易日期 + if trade_date is None: + await cursor.execute( + "SELECT MAX(trade_date) as max_date FROM concept_daily_stats WHERE concept_type != 'leaf'" + ) + result = await cursor.fetchone() + if not result or not result['max_date']: + raise HTTPException(status_code=404, detail="无母概念涨跌幅数据") + trade_date = result['max_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, (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_name=row['concept_name'], + concept_type='lv1', + 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'] + )) + + # 查询 lv2 + 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, + 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: + raise + except Exception as e: + logger.error(f"获取层级涨跌幅失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/hierarchy/{hierarchy_id}/price-timeseries", tags=["Hierarchy"]) +async def get_hierarchy_price_timeseries( + hierarchy_id: str, + start_date: date = Query(..., description="开始日期"), + end_date: date = Query(..., description="结束日期") +): + """获取层级概念(lv1/lv2/lv3)的价格时间序列""" + if not mysql_pool: + raise HTTPException(status_code=503, detail="MySQL连接不可用") + + if start_date > end_date: + raise HTTPException(status_code=400, detail="开始日期不能晚于结束日期") + + try: + async with mysql_pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cursor: + query = """ + SELECT concept_id, concept_name, concept_type, trade_date, avg_change_pct, stock_count + FROM concept_daily_stats + WHERE concept_id = %s + AND trade_date >= %s AND trade_date <= %s + ORDER BY trade_date ASC + """ + await cursor.execute(query, (hierarchy_id, start_date, end_date)) + rows = await cursor.fetchall() + + if not rows: + raise HTTPException(status_code=404, detail=f"未找到层级概念 {hierarchy_id} 的数据") + + timeseries = [] + concept_name = "" + concept_type = "" + + for row in rows: + if not concept_name: + concept_name = row['concept_name'] + concept_type = row['concept_type'] + + timeseries.append({ + '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 { + 'concept_id': hierarchy_id, + 'concept_name': concept_name, + 'concept_type': concept_type, + 'start_date': start_date, + 'end_date': end_date, + 'data_points': len(timeseries), + 'timeseries': timeseries + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"获取层级价格时间序列失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +# ==================== 主函数 ==================== + +if __name__ == "__main__": + import uvicorn + + uvicorn.run( + "concept_api_v2:app", + host="0.0.0.0", + port=6801, + reload=True, + log_level="info" + ) diff --git a/concept_hierarchy_v3.json b/concept_hierarchy_v3.json new file mode 100644 index 00000000..a7eb2200 --- /dev/null +++ b/concept_hierarchy_v3.json @@ -0,0 +1,1275 @@ +{ + "hierarchy": [ + { + "lv1": "人工智能", + "lv1_id": "lv1_1", + "children": [ + { + "lv2": "AI基础设施", + "lv2_id": "lv2_1_1", + "children": [ + { + "lv3": "AI算力硬件", + "lv3_id": "lv3_1_1_1", + "concepts": [ + "AI一体机", + "AI算力芯片", + "AI芯片", + "DeepSeek智算一体机", + "GPU概念股", + "TPU芯片", + "一体机核心标的弹性测算", + "中昊芯英概念股", + "国产算力芯片", + "国产GPU", + "服务器", + "昇腾推理一体机", + "摩尔线程", + "摩尔线程IPO", + "沐曦集成", + "阿里AI芯片" + ] + }, + { + "lv3": "AI关键组件", + "lv3_id": "lv3_1_1_2", + "concepts": [ + "AI PCB", + "AI PCB英伟达M9", + "AI服务器钽电容", + "HBM", + "OCS光电路交换机", + "PCB", + "PCB设备及耗材", + "SRAM存储", + "光纤", + "光通信CPO", + "光通信", + "光芯片", + "光纤列阵单元FAU", + "博通交换机", + "改良型半加成工艺mSAP", + "服务器零部件", + "硅光技术", + "空芯光纤", + "薄膜铌酸锂", + "存储", + "存储芯片", + "存储芯片产业", + "铜互连", + "铜连接", + "钽电容", + "忆阻器", + "利基型存储DDR4" + ] + }, + { + "lv3": "AI配套设施", + "lv3_id": "lv3_1_1_3", + "concepts": [ + "AIDC供配电设备弹性", + "数据中心", + "数据中心液冷", + "数据中心电力设备", + "微泵液冷", + "微通道水冷板", + "液冷", + "液冷数据中心", + "液态金属散热", + "电池备份单元", + "电磁屏蔽", + "超级电容器", + "柴油发电机", + "钻石散热", + "英伟达电源方案" + ] + }, + { + "lv3": "算力网络与服务", + "lv3_id": "lv3_1_1_4", + "concepts": [ + "中国星际之门芜湖", + "上海算力", + "四川算力", + "大厂算力订单", + "字节算力", + "星际之门概念", + "杭州算力大会", + "核心城市智算算力", + "毫秒用算", + "算力" + ] + } + ] + }, + { + "lv2": "AI模型与软件", + "lv2_id": "lv2_1_2", + "concepts": [ + "DeepSeek FP8", + "DeepSeek", + "DeepSeek、国产算力", + "KIMI", + "MOE模型", + "Minimax", + "Nano Banana", + "SORA概念", + "国产大模型", + "文生视频", + "秘塔AI", + "阶跃星辰", + "马斯克Grok3大模型" + ] + }, + { + "lv2": "AI应用", + "lv2_id": "lv2_1_3", + "children": [ + { + "lv3": "智能体与陪伴", + "lv3_id": "lv3_1_3_1", + "concepts": [ + "AI伴侣", + "AI成人陪伴", + "AI应用陪伴智能体", + "AI智能体", + "AI应用智能体", + "AI智能体AI应用", + "AI语音助手", + "AI陪伴", + "Manus", + "字节AI陪伴", + "开发智能体" + ] + }, + { + "lv3": "行业应用", + "lv3_id": "lv3_1_3_2", + "concepts": [ + "AI4S", + "AI应用AI语料", + "AI编程", + "低代码", + "内容审核概念", + "物理AI" + ] + } + ] + }, + { + "lv2": "AI生态系统", + "lv2_id": "lv2_1_4", + "children": [ + { + "lv3": "通用生态", + "lv3_id": "lv3_1_4_1", + "concepts": [ + "云计算各厂商云", + "微软Azure云平台", + "甲骨文概念股", + "英伟达代理", + "英伟达概念", + "英伟达H20", + "超威半导体AMD", + "谷歌", + "谷歌概念" + ] + }, + { + "lv3": "阿里生态", + "lv3_id": "lv3_1_4_2", + "concepts": [ + "通义千问阿里云", + "阿里云", + "阿里云通义千问", + "阿里AI千问、灵光", + "阿里“千问”项目", + "阿里AI来听" + ] + }, + { + "lv3": "腾讯生态", + "lv3_id": "lv3_1_4_3", + "concepts": [ + "腾讯元宝", + "腾讯大模型", + "腾讯混元大模型", + "腾讯云及大模型合作公司" + ] + }, + { + "lv3": "字节跳动生态", + "lv3_id": "lv3_1_4_4", + "concepts": [ + "努比亚手机", + "华为抖音支付", + "字节概念豆包AI手机", + "字节豆包概念股", + "抖音概念", + "豆包大模型" + ] + } + ] + }, + { + "lv2": "AI综合与趋势", + "lv2_id": "lv2_1_5", + "concepts": [ + "AI-细分延伸更新", + "AI合集" + ] + } + ] + }, + { + "lv1": "半导体", + "lv1_id": "lv1_2", + "children": [ + { + "lv2": "半导体设备", + "lv2_id": "lv2_2_1", + "concepts": [ + "EDA", + "上海微电子", + "光刻机", + "光刻机宇量昇", + "半导体设备", + "大湾区芯片展览会-新凯莱", + "新凯来概念股", + "新凯来示波器", + "电子束光刻机“羲之”" + ] + }, + { + "lv2": "半导体材料", + "lv2_id": "lv2_2_2", + "concepts": [ + "PSPI", + "先进陶瓷", + "光刻胶", + "半导体抛光液", + "半导体材料", + "国产光刻胶", + "石英砂", + "磷化铟" + ] + }, + { + "lv2": "芯片设计与制造", + "lv2_id": "lv2_2_3", + "concepts": [ + "ASIC", + "GAA晶体管", + "ISP视觉", + "RISC-V", + "功率半导体", + "半导体设计", + "第三代半导体", + "端侧AI芯片", + "碳化硅", + "英诺赛科概念股" + ] + }, + { + "lv2": "先进封装", + "lv2_id": "lv2_2_4", + "concepts": [ + "半导体封测", + "半导体混合键合技术", + "玻璃基板", + "盛合晶微", + "盛合晶微概念股" + ] + }, + { + "lv2": "重点企业与IPO", + "lv2_id": "lv2_2_5", + "concepts": [ + "地平线", + "地平线概念", + "紫光展锐IPO", + "长鑫存储", + "长鑫、长江产业链", + "高通概念" + ] + }, + { + "lv2": "综合与政策", + "lv2_id": "lv2_2_6", + "concepts": [ + "半导体产业链", + "国产半导体", + "国产芯片参股公司", + "科特估半导体", + "芯片替代", + "英特尔概念股" + ] + } + ] + }, + { + "lv1": "机器人", + "lv1_id": "lv1_3", + "children": [ + { + "lv2": "人形机器人整机", + "lv2_id": "lv2_3_1", + "concepts": [ + "Optimus特斯拉机器人", + "乐聚机器人", + "人形机器人", + "人形机器人Figure", + "云深处", + "优必选", + "优必选机器人", + "华为人形机器人", + "各厂商机器人", + "奇瑞机器人潜在产业链", + "天太机器人", + "天工机器人", + "宇树人形机器人", + "宇树机器人", + "小鹏机器人", + "小米机器人", + "小米智元机器人产业链机构版", + "开普勒机器人", + "松延动力机器人", + "特斯拉人形机器人", + "智元机器人", + "荣耀华为人形机器人", + "赛力斯机器人" + ] + }, + { + "lv2": "机器人核心零部件", + "lv2_id": "lv2_3_2", + "concepts": [ + "PCB轴向磁通电机", + "人形机器人-滚柱丝杆丝杠", + "人形机器人万向节", + "人形机器人腱绳", + "机电", + "机器人皮肤仿生皮肤", + "摆线减速器", + "电子皮肤", + "轴向磁通电机" + ] + }, + { + "lv2": "机器人产业链", + "lv2_id": "lv2_3_3", + "children": [ + { + "lv3": "综合与价值链", + "lv3_id": "lv3_3_3_1", + "concepts": [ + "人形机器产业链", + "人形机器人核心标的估值弹性测算", + "人形机器人核心标的概览", + "特斯拉人形机器人价值量", + "特斯拉人形机器人弹性测算", + "特斯拉产业链" + ] + }, + { + "lv3": "轻量化材料", + "lv3_id": "lv3_3_3_2", + "concepts": [ + "人形机器人轻量化-PEEK", + "人形机器人轻量化-PEEK材料", + "机器人轻量化-镁铝合金", + "机器人轻量化—碳纤维", + "超高分子量聚乙烯纤维" + ] + }, + { + "lv3": "制造工艺与设备", + "lv3_id": "lv3_3_3_3", + "concepts": [ + "MIM概念", + "冷锻产业链", + "机器人零部件加工设备", + "金属粉末注射成形MIM" + ] + } + ] + }, + { + "lv2": "机器人软件与AI", + "lv2_id": "lv2_3_4", + "concepts": [ + "机器人-神经网络", + "机器人动作捕捉" + ] + }, + { + "lv2": "其他类型机器人", + "lv2_id": "lv2_3_5", + "concepts": [ + "外骨骼机器人", + "工业机器人", + "机器狗四足机器人", + "美的库卡机器人" + ] + } + ] + }, + { + "lv1": "消费电子", + "lv1_id": "lv1_4", + "children": [ + { + "lv2": "智能终端(端侧AI)", + "lv2_id": "lv2_4_1", + "concepts": [ + "2025CES参展公司", + "AI PC", + "AIPC", + "AI手机" + ] + }, + { + "lv2": "XR与空间计算", + "lv2_id": "lv2_4_2", + "concepts": [ + "AI手势识别", + "AI眼镜", + "AI隔空投送", + "AR镀膜", + "AR眼镜", + "META智能眼镜", + "MR", + "Rokid AR", + "消费电子-玄玑感知系统", + "智能穿戴", + "智能眼镜", + "小米眼镜", + "阿里夸克AI眼镜", + "谷歌AI眼镜-合作XREAL" + ] + }, + { + "lv2": "华为产业链", + "lv2_id": "lv2_4_3", + "children": [ + { + "lv3": "终端产品", + "lv3_id": "lv3_4_3_1", + "concepts": [ + "华为P70", + "华为Mate80", + "华为Pura70", + "华为Mate70手表", + "华为MATE70" + ] + }, + { + "lv3": "技术与生态", + "lv3_id": "lv3_4_3_2", + "concepts": [ + "华为", + "华为5G", + "华为AI容器", + "华为云", + "华为昇腾", + "华为昇腾超节点", + "华为通信大模型", + "华为海思星闪", + "华为鸿蒙", + "华为鸿蒙甄选与支付", + "华字辈", + "昇腾异构计算架构-CANN", + "鸿蒙PC" + ] + }, + { + "lv3": "芯片与算力", + "lv3_id": "lv3_4_3_3", + "concepts": [ + "华为910C", + "华为AI存储", + "华为存储OceanStor", + "华为麒麟芯片", + "磁电存储" + ] + } + ] + }, + { + "lv2": "苹果产业链", + "lv2_id": "lv2_4_4", + "concepts": [ + "果链OPEN AI复用", + "苹果MR产业链", + "苹果供应商核心公司", + "苹果机器人", + "苹果手机产业链" + ] + }, + { + "lv2": "新型显示技术", + "lv2_id": "lv2_4_5", + "concepts": [ + "华为三折叠屏", + "折叠屏", + "显影液及硅基OLED", + "苹果OLED潜在受益", + "苹果折叠屏", + "面板" + ] + } + ] + }, + { + "lv1": "智能驾驶与汽车", + "lv1_id": "lv1_5", + "children": [ + { + "lv2": "自动驾驶解决方案", + "lv2_id": "lv2_5_1", + "concepts": [ + "Robotaxi", + "京东物流Robovan", + "小马智行", + "文远知行", + "无人物流", + "无人物流车九识智能", + "无人驾驶", + "特斯拉FSD", + "特斯拉RoboTaxi概念", + "特斯拉无人驾驶出租车Robotaxi", + "自动驾驶", + "菜鸟无人物流车" + ] + }, + { + "lv2": "智能汽车产业链", + "lv2_id": "lv2_5_2", + "concepts": [ + "比亚迪产业链", + "比亚迪智驾", + "孙潇雅团队概念股", + "小米YU7供应链弹性测算", + "小米大模型", + "小米概念", + "小米算力AI互联", + "小米汽车产业链", + "小米汽车产业链弹性", + "小米汽车产业链弹性测算", + "小鹏产业链", + "理想汽车" + ] + }, + { + "lv2": "车路协同与特定场景", + "lv2_id": "lv2_5_3", + "concepts": [ + "无人驾驶-线控转向", + "无人驾驶公交", + "无人环卫车", + "矿山智驾", + "车路云-车路协同运营建设", + "车路云一体化", + "车路协同" + ] + }, + { + "lv2": "产业链综合", + "lv2_id": "lv2_5_4", + "concepts": [ + "mobileye替代概念", + "智能驾驶产业链", + "汽车安全" + ] + }, + { + "lv2": "出行服务", + "lv2_id": "lv2_5_5", + "concepts": [ + "网约车" + ] + }, + { + "lv2": "智能座舱", + "lv2_id": "lv2_5_6", + "concepts": [ + "空中成像" + ] + } + ] + }, + { + "lv1": "新能源与电力", + "lv1_id": "lv1_6", + "children": [ + { + "lv2": "新型电池技术", + "lv2_id": "lv2_6_1", + "children": [ + { + "lv3": "固态电池", + "lv3_id": "lv3_6_1_1", + "concepts": [ + "固态电池", + "固态电池-硅基负极", + "固态电池-硫化物", + "固态电池产业链", + "固态电池设备", + "固态电池负极集流体材料-铜箔", + "陶瓷隔膜骨架膜" + ] + }, + { + "lv3": "其他材料与技术", + "lv3_id": "lv3_6_1_2", + "concepts": [ + "复合集流体", + "富锂锰基材料", + "硅基负极材料", + "钠离子电池", + "隔膜" + ] + } + ] + }, + { + "lv2": "电力设备与电网", + "lv2_id": "lv2_6_2", + "concepts": [ + "北美缺电AI电力", + "变压器出海", + "固体氧化物燃料电池-SOFC", + "固态变压器SST", + "燃料电池", + "燃气设备", + "燃气轮机HRSG", + "电力", + "电力产业链", + "电力电网", + "电力设备" + ] + }, + { + "lv2": "清洁能源", + "lv2_id": "lv2_6_3", + "children": [ + { + "lv3": "光伏", + "lv3_id": "lv3_6_3_1", + "concepts": [ + "N型产业链", + "光伏", + "光伏行业兼并重组", + "光伏产业链", + "反内卷光伏", + "叠层钙钛矿", + "钙钛矿电池" + ] + }, + { + "lv3": "核能", + "lv3_id": "lv3_6_3_2", + "concepts": [ + "可控核聚变", + "微型核电", + "核电产业链", + "核电钍基熔盐堆", + "核聚变超导", + "超聚变", + "高温概念" + ] + } + ] + }, + { + "lv2": "充电桩与补能", + "lv2_id": "lv2_6_4", + "concepts": [ + "充电桩", + "华为智能充电", + "华为智能充电网络", + "换电", + "换电重卡", + "机器人充电", + "汽车无线充电", + "比亚迪兆瓦闪充" + ] + } + ] + }, + { + "lv1": "空天经济", + "lv1_id": "lv1_7", + "children": [ + { + "lv2": "低空经济", + "lv2_id": "lv2_7_1", + "concepts": [ + "eVTOL材料", + "九天无人机", + "亿航智能订单量", + "低空经济", + "低空经济&飞行汽车", + "低空经济产业链汇集", + "低空经济亿航智能", + "低空管控", + "低空物流", + "低空设计", + "小鹏汇天", + "小鹏汇天供应商", + "飞行汽车eVTOL", + "长安飞行汽车机器人概念" + ] + }, + { + "lv2": "商业航天", + "lv2_id": "lv2_7_2", + "concepts": [ + "凌空天行", + "北斗信使", + "北斗导航", + "商业航天", + "商业航天卫星通信", + "卫星出海", + "卫星互联网", + "太空行走", + "太空旅行", + "太空算力", + "手机直连卫星", + "星河动力", + "星网", + "蓝箭航天朱雀三号" + ] + }, + { + "lv2": "通信技术", + "lv2_id": "lv2_7_3", + "concepts": [ + "5.5G", + "5G-A", + "5G毫米波", + "6G概念", + "eSIM概念", + "通感一体" + ] + }, + { + "lv2": "民用航空", + "lv2_id": "lv2_7_4", + "concepts": [ + "大飞机", + "空客合作" + ] + }, + { + "lv2": "综合与主题", + "lv2_id": "lv2_7_5", + "concepts": [ + "珠海航展" + ] + } + ] + }, + { + "lv1": "国防军工", + "lv1_id": "lv1_8", + "children": [ + { + "lv2": "无人作战与信息化", + "lv2_id": "lv2_8_1", + "concepts": [ + "AI军工", + "AI无人机军工信息化", + "信息支援概念整理", + "军工信息化", + "军用无人机反无人机", + "无人机蜂群" + ] + }, + { + "lv2": "海军装备", + "lv2_id": "lv2_8_2", + "concepts": [ + "军工水面水下作战", + "国产航母", + "水下军工", + "海军", + "电磁弹射概念股", + "电磁发射设备", + "航母福建舰240430" + ] + }, + { + "lv2": "空军装备", + "lv2_id": "lv2_8_3", + "concepts": [ + "军机" + ] + }, + { + "lv2": "陆军装备", + "lv2_id": "lv2_8_4", + "concepts": [ + "远程火力" + ] + }, + { + "lv2": "军贸出海", + "lv2_id": "lv2_8_5", + "concepts": [ + "军贸", + "巴印军贸", + "巴黎航展" + ] + }, + { + "lv2": "综合与主题", + "lv2_id": "lv2_8_6", + "concepts": [ + "军工", + "军工-阅兵", + "国防军工" + ] + } + ] + }, + { + "lv1": "政策与主题", + "lv1_id": "lv1_9", + "children": [ + { + "lv2": "国企改革与市值管理", + "lv2_id": "lv2_9_1", + "concepts": [ + "中兵集团并购重组", + "中字头", + "中字头央企", + "中船合并", + "央企市值管理", + "央国企", + "央国企地产", + "央国企重组", + "安徽国资", + "地面兵装", + "整车央企重组", + "河南国资能源集团重组", + "市值管理16条-破净股", + "珠海国资", + "破净央国企", + "破净股合集", + "福建国资", + "湖北三资改革", + "国资高息股", + "高送转概念股", + "央国企AI一张图" + ] + }, + { + "lv2": "并购重组", + "lv2_id": "lv2_9_2", + "concepts": [ + "IPO终止相关企业重组预期", + "上海并购重组", + "券商合并预期", + "宝德计算机", + "并购重组", + "并购重组预期", + "消费医疗重组预期", + "湘财合并大智慧", + "科创板并购重组", + "秦淮数据", + "科技重组", + "超聚变借壳预期", + "证券", + "荣耀股改", + "重组-中科院系&海光系" + ] + }, + { + "lv2": "信创与自主可控", + "lv2_id": "lv2_9_3", + "concepts": [ + "信创概念", + "关键软件", + "国产信创概览", + "工业软件", + "自主可控", + "软件自主可控", + "信息安全", + "安全概念股", + "网络安全", + "通信设备", + "通信安全" + ] + }, + { + "lv2": "重大基建", + "lv2_id": "lv2_9_4", + "concepts": [ + "三峡水运新通道", + "新藏铁路", + "新疆概念", + "水利", + "水利工程", + "混凝土减水剂、砂石设备", + "节水产业240423", + "西部大开发", + "西部大开发240424", + "西南水电", + "西南水电站", + "西南水电站-机构测算", + "隧洞设备盾构机", + "雅下水电对电力设备增量测算-机构", + "雅下水电站", + "雅下水电站大件物流" + ] + }, + { + "lv2": "供给侧改革", + "lv2_id": "lv2_9_5", + "concepts": [ + "反内卷", + "反内卷造纸", + "反内卷食用盐", + "反内卷快递", + "反内卷合集", + "生猪", + "物流", + "牛肉", + "统一大市场", + "钢铁" + ] + }, + { + "lv2": "产业升级与制造", + "lv2_id": "lv2_9_6", + "concepts": [ + "工业设备更新", + "工业母机", + "设备更新", + "灯塔工厂" + ] + }, + { + "lv2": "国家战略", + "lv2_id": "lv2_9_7", + "concepts": [ + "2025年政府工作报告利好行业及个股", + "新型经济", + "新质生产力", + "深海数智化", + "深海经济", + "深地经济", + "首发经济" + ] + }, + { + "lv2": "区域发展与自贸区", + "lv2_id": "lv2_9_8", + "concepts": [ + "上海自贸区", + "免税离境退税", + "新型离岸贸易", + "海南", + "海南自贸区", + "海南自贸港", + "零售消费免税" + ] + }, + { + "lv2": "行业监管与规范", + "lv2_id": "lv2_9_9", + "concepts": [ + "充电宝", + "农药证件厂家", + "食品安全", + "食品安全全链条", + "预制菜" + ] + } + ] + }, + { + "lv1": "周期与材料", + "lv1_id": "lv1_10", + "children": [ + { + "lv2": "化工", + "lv2_id": "lv2_10_1", + "children": [ + { + "lv3": "行业趋势", + "lv3_id": "lv3_10_1_1", + "concepts": [ + "化工", + "化工概念", + "化工品涨价", + "涨价概念" + ] + }, + { + "lv3": "具体品种", + "lv3_id": "lv3_10_1_2", + "concepts": [ + "TMA偏苯三酸酐", + "乙烷", + "光引发剂", + "六氟磷酸锂", + "农药杀虫剂-氯虫苯甲酰胺", + "双季戊四醇", + "己内酰胺", + "有机硅", + "正丙醇", + "烧碱", + "涤纶长丝", + "电解液添加剂", + "甲苯二异氰酸酯-TDI", + "环氧丙烷", + "纯碱", + "磷化工", + "磷化工六氟磷酸锂", + "聚酯产业", + "维生素", + "苯酚丙酮", + "超硬材料", + "除草剂-烯草酮" + ] + }, + { + "lv3": "产业链", + "lv3_id": "lv3_10_1_3", + "concepts": [ + "有机硅产业链", + "电解液产业链" + ] + } + ] + }, + { + "lv2": "有色金属", + "lv2_id": "lv2_10_2", + "concepts": [ + "化工有色元素周期表", + "有色金属", + "电解铝", + "稀土", + "白银", + "铅酸电池", + "铜", + "铜产业", + "钨金属", + "钴", + "钴金属", + "钼金属", + "锡矿" + ] + } + ] + }, + { + "lv1": "大消费", + "lv1_id": "lv1_11", + "children": [ + { + "lv2": "文化传媒", + "lv2_id": "lv2_11_1", + "concepts": [ + "AI游戏", + "乙游", + "传媒出海", + "出版传媒", + "国产游戏黑神话", + "周杰伦概念股", + "幻兽帕鲁", + "影视", + "影视IP", + "影视传媒", + "影视院线", + "春节档重点影片(哪吒2)", + "智象未来", + "漫剧", + "游戏", + "游戏出海", + "疯狂动物城2", + "短剧", + "诡秘之主", + "腾讯短剧重点名单" + ] + }, + { + "lv2": "新消费", + "lv2_id": "lv2_11_2", + "concepts": [ + "上市潮玩盲盒公司", + "卡游文创玩具", + "布鲁可IP衍生品", + "布鲁可谷子经济", + "泡泡玛特概念", + "潮玩产业", + "积木玩具10大", + "谷子商城", + "谷子经济" + ] + }, + { + "lv2": "人口与社会", + "lv2_id": "lv2_11_3", + "concepts": [ + "三胎", + "多胎", + "多胎辅助生殖概念240926", + "学前教育", + "育儿补贴", + "辅助生殖" + ] + }, + { + "lv2": "体育产业", + "lv2_id": "lv2_11_4", + "concepts": [ + "体育", + "体育产业", + "冰雪经济", + "川超联赛", + "第十五届全运会", + "足球", + "足球-苏超联赛、体彩" + ] + } + ] + }, + { + "lv1": "数字经济与金融科技", + "lv1_id": "lv1_12", + "children": [ + { + "lv2": "数据要素", + "lv2_id": "lv2_12_1", + "concepts": [ + "RDA概念股", + "RWA上链— IoT设备数据采集", + "版权", + "地理信息", + "数据可信", + "数据交易所", + "数据要素", + "政务云政务IT", + "跨境数据数据要素" + ] + }, + { + "lv2": "数字金融", + "lv2_id": "lv2_12_2", + "concepts": [ + "上海浦江数链", + "互联网金融", + "复星稳定币", + "数字货币", + "树图链概念", + "稳定币-蚂蚁国际", + "稳定币RWA概念股", + "稳定币一体机", + "蚂蚁金服", + "香港金融牌照" + ] + } + ] + }, + { + "lv1": "全球宏观与贸易", + "lv1_id": "lv1_13", + "children": [ + { + "lv2": "地缘政治与冲突", + "lv2_id": "lv2_13_1", + "concepts": [ + "以伊冲突-天然气", + "以伊冲突-油运仓储", + "以伊冲突-航运", + "以伊冲突-资源化工", + "乌克兰战后重建概念", + "乌克兰重建", + "俄乌重建", + "油气", + "海外港口", + "海事反制", + "石油", + "航运", + "远洋航运", + "柬泰战争", + "黄岩岛概念股" + ] + }, + { + "lv2": "贸易政策与关系", + "lv2_id": "lv2_13_2", + "concepts": [ + "中美关系", + "中俄贸易", + "中欧贸易", + "出口管制", + "反制关税涨价预期", + "后关税战受益", + "关税减免出口链", + "关税豁免", + "绒毛浆", + "芬太尼管制", + "转口贸易出口转内销" + ] + }, + { + "lv2": "供应链重构", + "lv2_id": "lv2_13_3", + "concepts": [ + "二轮车全地形车", + "墨西哥汽车零部件", + "越南工厂" + ] + } + ] + }, + { + "lv1": "医药健康", + "lv1_id": "lv1_14", + "children": [ + { + "lv2": "创新药", + "lv2_id": "lv2_14_1", + "concepts": [ + "AI制药", + "创新药", + "创新药双抗", + "创新药相关", + "医药" + ] + }, + { + "lv2": "细胞治疗", + "lv2_id": "lv2_14_2", + "concepts": [ + "干细胞", + "干细胞概念股" + ] + } + ] + }, + { + "lv1": "前沿科技", + "lv1_id": "lv1_15", + "children": [ + { + "lv2": "量子科技", + "lv2_id": "lv2_15_1", + "concepts": [ + "量子材料钛酸锶", + "量子科技", + "量子科技产业链", + "量子科技参股公司", + "量子计算" + ] + } + ] + } + ], + "uncategorized": [] +} \ No newline at end of file diff --git a/max.docx b/max.docx new file mode 100644 index 0000000000000000000000000000000000000000..6a9d0710e9517695c424b2b8f128def3f2322f4b GIT binary patch literal 31217 zcmZ^~1CT9ivn|?MZQHhu)wZqGwr$(CZQHipz1p^Izux7f1Byq+S0mOS!Tt_+xXKX2W_WZ@WOB9`R-*>u)rQK)Tf2s&oLhYP32mxnLljT zL<+B-Yo~imTgi5BMWm!uKThYuq!8Zk)8BE!OC@XmP3xg!?b~>cEXpseLXLe^rT>cO z;@PQ6hHO04}JmEeG`fic8GoYw88~b z_Lz(pr+BWe1Aa!2>3~yWngHcYQvSrFt9h>Qc+a3Yc9@Q-E8i^UmgTPqgHuqCb-Gv3 z|Hk5?(3{-o2Mf<1ED-)T76vx(22!O1t#)$o2BSOq#ZKBPPCH0-)@`6C^Fz%X}>-p{2qSa|96{S*!pQ^o6m0^`MF| zdp0${veJ0zn8&l3BrnQKxBd=qnn>26a6ZoyXVpf!a~~+H<42YyF6#@1 zP75zc-uCTt4^}{1sXFvW26*#~l=ywbA#rFl1h5Rn95m(RRNH<`Sp&%I`M8&7&oOYQ z>(fzV$KIwcnraVomBFXNSV;;OiejabnrE+2o@ESgu!AROx3Wan=8mct-sC&sApFXK ztMgQ*B9y2}1T+Dm?i>@~R5@r{HUcM}brZY`aR1cy|9gx2^;26H8+*h5YkPv{R*EtC zxnkV}008cPOdK5DER7ugDSeZcr1O3|Le_ESg14Ix)I8bw^9A-cuZoA)DK~7$At5I^mo^++i3=cxQ`TX*&oXz{kVAB(~cO*F=z1 z@z>W?_Vq1sY@S$l@z?&q;jqmJ9v_s46D&r9R)sfgv!V_X2@S2Jv> zPb2xlk_2&FYPZQdt+YE3Z_QC^<6oXr*`y2;-ASi1`1^?{`m}`doieabPtjh$L?zy7 zBqgBwWT^b44sGOrNlNVN-~rjbCHTq~&P4CRKoD46$&_Yqd`6Fe#AJ4GVuOyvfR zVD2ob1|6^$kE@yqG{&fM$oiPC z*7cY)th8!rX(=Afff|TfsW!P19brtAn+F4!u4Ww_8)=We_@h+zX;i6nR4{MDAR0JV zEIq-I-7KsNccX!o=;R=#)wzN6wXN~q@d_FSGq&zPsfY+ycN;&ay_$EQ`lvEe*xNXb z-|>Z(edFtcA8~fyebE|#PHSb+&ct`VqfOuKQKRWTUVpzXe}BE(I624K_)GElq?>-d z`AkGN)|LOZm@);I0bf*0GVrCHC`^3=8;Q-8?}Uy@$77 z&1B!NYem%?Rrby>h|#%bB48TrF8jVM;BX}hu;I!?;j5HOO33Hx^n`FNzXLJ-R&y}V zw2zX*QV3@=^uo89I|qh{0Q{Cw!+Vu;lt?Bqkr?azvilQv?Pp0* z4@B1=gu;+74i_>Rp97hi-<88*Skrg6Bu`DXg}Y0g8g5SsF6B>q-KoWI~b86H@6(>X@aAO zVSygv7&8?5+(xpFJ6sS=)(F2#igV+JNf-X8Q7h|c$Qf!(>IPbVoKO?rTN1L05m*b! z*itE!Zly4Wv!lG=$m5z#K&(|`rsUq1+r>K#$8j9<~; znj8J^ReR7@s(HQ6^K+T#-wZVlI#Xhk;rx0`M|9qhT{U9U`0#Vfs7%b? zB|5JkGyCsHZ@W*!oB?wg2Q0+`C76DT%{frv8b-fp1pPZ|gM+7i#dPUq5F@cH{Cn<} z+YISd%^%{NV0_?!^WBeR`^w0TsbwhI!`HsR;J;BKgB75~Ec|_HrAkAvfj_ph^AEV8 zLCz6UmR@|cr+xMT`9qX4U?I!O6ZX>HIX(T8*M7c4WP*Hf8B#UG87X$T0n%K1f%G7k!6=0Os9=0Of<^COkCfc$UMC{e!R!(yw0 zS$?ZMnmg9FFP<2pRLq%KBThQ7%M5CxNA0f_&?>&O{4x+>$F%O+tVsY z80ruqi*SjOLA}PzqTc%P(21w*>RIRe#Ix(wImD0pP6JyHEhj7Jn%V!Bec{CCQ(Xe3 zLNRDGn1epF-wXN0lK|D^EJ?~ep(O&(@mLzv7^1qH5QGR_JJ;lj%wBc~GWQ+SN6$*# zmhYCT`K#FcTJG>Ka3z2+Y_Ag3hCAbv2p0{MO*8h*%ITjPt%g0S&k?ty7rv^Z2LJs{ zZ%q%ila}*+6NEBY56Yc{aVHq`M52Bv(~H`y^2@P2UG2FYQ!*O?ar9 z-=p3?3p#Yq7ZCFMcu_HlO-SqIGf;oNFLxKt2a`Ld0A}#r*z1gJ1sQU|n~AM&z(ND3 zKCcguvblfN9)QrGG`E+AI;O)I)+vh)KsRxH3T2+kzhczP|btRHr;!>}oKfUy;RgJ8Z8vh&6He zA2-8@91(2E|AZHs{<+E?$%Yz2q_a+w)ykB)iRLF#1{brC)j!c9*zVdY3tSD>;e5d~&64hwE3&OJsjyqW`4!zq9$r%|G`|6t*#lCFze~L!Baxg(+?IPr{^dF!CCm zaY9#nQJKmop#Wg^!ka}ANo_0|M#Z;$IZXx6F(r``hytxQ&`KjSyS`>Q?aTr z-HNxT>U(nw=g(-c)EK4S@~N_%C9IQD3W-{iPTr-s02A?D17W70@ZigblNUh7dk}XTzoVesHdz zF#`VjeB_&kknF>&dW};mQ{KS^?Oet-;#z;tu1MX4qWJx8fBYeXlMcN2*JO42Vlq)3 z4NankO1EL$pX9GvuF87Vz5v@YBm+^Jc6VwaaSnwuh~3(cc2`it!z;O&FWY<$2>o)n6Sq$R^d z(zJnIfjBl5Y&`4P8T^LPBjZ5P5FbF_G?EPz>=G+*ZJO7`@>Q`!KbXZoX1maR;lILT*04R1oLDO1Xx4k20tjicxlTz18=&20g>IN9S)MmU~?fwgmh8l;`LCe}?HjD4xB?4*M7MvXK@B2$hE>oIDc8BKUZp^cU@$McE0I zh{#I)q80%%b~NU9{Il`5sQPG!f?;tb**O%so%=5n`gH_h#z|p^fnQq!;zyh6mU1iY zWX_mfn9P1y?0!lqe_qNVSKkDB;*B%JLG&${r*=4$cZQPLg7K-GTME=MA|@+)C$F2dnpuB_W- ztBd@NUwAB)p{z{OVX0L3vd<*I%Nj+&u1%z1YU%1*{Dymcs18KnBcLvSLD_ALU;*nT zgku)=>z+dvEKxKIVq_NHbVLz30W%=ZRuCO@Q*HDR-+-9ne1<{f0{7ZSd+Y+9X#x6; zAF--qLj(aC;Hd##4oB%hPX`V!}b?n@&OAKr)_z1CX*wIeM>U&G%^AU#$4+(JD-@s238L}x9|_j zn#8%%CP8<1XtLO02VP*N4`0n4IWA_j&j$k*?d=#e)nJc?fA>e;j9O1~9?CXP{oZIg z@xM`$pKojGKxuMKEHB|e$<6@+BFVImXMCKrUS?n>iyR7N&z2V?jjBe67ZxJWLw zvS^8uvr#|@iUE$G}!Hyr18$9i{}ZVH8E3 zCBUZ@Fy}1Ck<-*QAU(tZ2NP;CKnf0MjPjY}QBmt|b(Lt8e#>_x= zKvfUr()I(TU=M;}8jr?{2qOXIU^$x|MmtUu@$9r40fWk+7ql;{!bV8{M%tWdqua3N z3-JG+Yya$9|KZw>rbbpq{}rav{@d_hbB2F-_odoY3=$i1kLuYs zeLCj_@qvL_oNz{r5mZO*kDM_##?EBSy+7*gJXYmzP$Z#jwkR0qex7F@0ir&g+b_sen1Y~bF+g0*A2qnX% z;xeoBY=>p^q^4>o40?iH-a&2B=a=QMc{V9oh@D)QhlnzQbq4*u&G^uN(2;Vwv zzTv2Nt!%tfImaxj$dU^aGa{9sc{^Vud)laqtCN2&JgT}5_xJ7MeM#SGfJq6QH;32D z%|qXeNE}g9$lYMF`|Br~5APTA%lWAb9iG?W2o%=$yKK(q(|L+FzS3-eCYlcK>;7s2 z+P3G{FyK_I zbh5(c0(L0l?XdvyAibx!Dqd%BL?GNI1TkF){g=%-ux_NIBaq#W8=q(z1Z!f47Gf>b zRge*1Fhm+Ek2{`U+n5KKU|Yr#)*8%8#7o=dwk{&DobRu}2Y@13r0T3v{$z>0FQYk_ zC*Ig!6GNY2QPyfQV#D8kiNCtgp>`A6`ucRJPM352UKYIhOwu9_;;~;HuZV^%4O4KO z6cVL9S9r!0vEIYjfx`25V#V=3eA`g+w+P_KbuzBjK{!u&Ii7?20g5Fhh6d`zczJ+?EK7w^|?#v*8;VbrZar?fS$JDI+RJ*3DlCoqQI z1+Ke(QFGnWLQ@iWjSn7DSjP7kP#Vl$?I1==qzNIXBO*wp`ZnmbswBPfm8?eHu4K|s zO}Qgak0A66L>ou7j?3((LM<;~^1zv`H zex&mhDQdnsR;!0Nym0O%=MO3Ln^~9S9Q8qH%9Vl#^?^&P@1n~X+@wn5%LrU5!b|VK z8W6XQooCz_^#@%L#-&$UP^K*s5Fy_C##f?Pr(>rL&T1+ib}?S{AV&$-rXMY54j4^+ z!8s}3{++@)M0GvAgO3?;CAKnFCklr;YBFW9gls%5xfQ1`RP;syRHl+I%45ey0yOz{ z2XR!OvsG2DL0Rmo=KRE&=q`3zN}WK*e|$PWp!`_sHzQ(i^U z(BkMEnZo+Jn+5=DTq%h&rQW4#yUlll(L&FZ zYx~Qr`Q#h<;@yX}6HKcZ>bXjhGUyZlv+?s^Mfd+qlf)S3>t*Bs0I1o4|D9m`+Zz8& zo}8?VtR4UT3lL^Ut2&pZCBp!x+-f!y)uv4xIB6LQknAkG&#S))w`686FUOxQbGbStjCj5+%Ca&Bo27`@8gL zpdlN@u@P`zAlyEn#lGAn_Q0=UGxI3+@Qwwyme2d(^o?8K((>rMae8ub@(DKl1X8I- z*kb;cuf;Y6@wt@^k7J)eX82VX9h%({j(B`XlrhDLKjTV-c{PYlsKx!v*)B`BUth3` zc&(xMb}NW>yB{E9(G#+%{oQuInxZ+)WE$PfJy^hwbF*PfVm++IbHY!y4d&imBKvp| zdIcpgEe>LqDZMZlvnYoySpeiYD5c)T=(f)A8v%olj!6m61$5THy%rGThsUEXY*Apch*(5oQ|x`0l$NWO$PVX zKnPq7@5NQy)|L?@;>g+koApk~LV$TSw`tGo)U#Hc>3Wuo=DJ|b_nM&1CW|^gOAA5A zmC)5!z?ZW@!?&@EM&o5hM~k_67Y)s0A78*m(ff>i+lWio_X)I)hwJ@Fv&@COLjda+ zJgkck)|W?3)2c)JYYdLoZZ^|;$9gv$oA7`YFLMw<=SAOS0j~2#6c@B`yL&;L42Qd{ zj*AO2V_fiC^K~DN*fA6E*X-OP(rcdVmGt$APJz|tog_&9hqlSVZq9rAn#W=CmKa+U z5nCokQB@7wotBG+M{Aq#mHXSL#_Old$Yyjy$i^pD_N315?w;*;>1C}pRasSyXCIL- z_{&xg<854M7&ZSYmjk2i@>@4AmM2xWf?RtpIgTY-{>f3U3zeviE$13GZ0?>mq?Ev0 zAbY&GyA&SxX~$n5}eM?ad zIjR0(zD_`?;;n)iX$)#?rY8+n9TXCfibT}{=B~QW??X7~Qd_MK>3Pv! z5@I2t*fy<2phn<6>eXu2-B&0)Fo!q5((+j5vUVS!1E-@&rM^xZCyVECE#4nKm*)8t zrFeICo}iEjgK|VF^bsXy9A4ejhz<4fnH?-QT5#|uP)J`Kqx2v(5_F|`2@IyDBFVs2 z>58eYQhcs{(V2CyxAe~nsoJ2$A46QT>*uD=RP-tRh}4|rk%i$2Bm-WYQlv4jFL{sd zso?NG8fg19oq)1h7!w`ci|1uluuJ8KRmp4}2016HWrtpQ8ln@NKJ+8bA{Z76%YQYu zF>e+tn#z`n)iZ`BAd6Q_AlkS(-7wbI%#_h+3{_vCU>)`dO-uwOFCxO~9KK-H>W>XD zoc!bz1q~9GAN|FI5jJpyYqzA_r#8^Ba_Iq>8^xjp#4Vs~9)?omya^O|4`o#z%3wZ+ zkQlm&5gmGP`vtb)!8qTRVi{i4J?sT|X^b9|{_rqG>O5*H5Q`2@5HTbpF90`wKWRiW zgocT;0yQ28eYx`Fc$q)Qo{A>052gJv;Nc`xfMQ=_=UGBT+FLB}G!6ugUE2$PI!XJjFupZ(S@MEu6_P$G_$pgVf=<(1o3S}sLp3TdKZ51EUAR;+EbT`KHOrcoD8%A1cENG?+CE=dD&tIFI_TAh%X)^>K3 zXMEHaZ7fNdFKjXYfMI+ywt?8Ri69&RFO0xK%oySjvRIwdIFq2_^Jp;7jI@Yhy%QxX zd@9*$mnGDkwtKZz==D-H>!M`LBTz)`!6m(GOQ8>ZP?FNw{!+E<^J?hcXedK3xqIr- zkLbaLt^0%=Xu-q-(AeKQ4UW@x+J59n5$0jDxWGMt(+M40y^k1pt$1A_jt1-&;7OP= z0lOoM2ttF3bz)bEywv69-y`_l%ejAQ9uf2Cc4rp(z>|rK8aAzmfgZ<;4%jVt^&~L^ zgKWGn^1H;UDy=rGt}sp_D+kw;+EwgjEOQ+`7FAFmpH^XXDEdqgjIge3s(9$8{>!@f z##N9QblRPxx^_l_ial*c%~iU4z0aB_Bj}jCUa>W7ZMspcYxX!UMr+`yf5U>^F4oL* z**56r@kUH2JyWim|3G*A-5)D)=I~B*;q88`ANq=d8{MA3#s11eKH^%EWqfFl%8^z| zrY_$&7$J=pPHSZBsjWCdf-iWo6|1G5^?J>{y6NVV2MY_hb}AO^_IncBdi~uVvz)h_<+@C&K4l z@lWqh07^P6TVBsvigRAPD+ps&?t+Xlc_E|KV#RtDVf(4g4~V28&G5jkLU{h4qQxs_ zmTkXtR7fgOMyagM0NOzI4cC$FkoIG>4AH)XmfU%milcgv$1t`Or6MI2Hzy%)n1XXO zLt)H_dQRE`o15I@4yUgJvo)3-G&4%)OpK;d%|=->)a{9pd2&Yq~C#sAy9cppd7eV5u9sYe?I56!&g8?yk~X=Nzz-7k$OP0R9lgx^i;N z7iw*b`*KMZYnKrF9V9~V)X$D#xPkbJ#^?mVM(lHko((jRx+5ha8t_DoNL>xir>QcU zvs9tt-vLgHsp*+A+{|NUCbu(+zKu3^oCpy&)Z+exLL$47=F<6hNc)OpvACtFE|E!o z?0g|A;zprmh$3vV*2Wof<`Xz}?Cy@*FW4BFo+WIsK$I$uz5=dt zldF;{RW)^!3|$A6!_>txz7>Di{)X!uq_WHunjWA3s!qG3bfK|4mBS@(Xk;arkpE7_ zDb#j^e{~rVHpyAddmPyAHcG)R)l1r`1BV^+WjEWb`oKXp0=D4VA&8U2#d(GqdpF8RIR<4d$$%TvBf_8yKMT)rlGb59t!u(ZoH;M>nVs6tW`SDRXU%f71mVWnxdjxD3@7?Lqm|f|Z7i%NkcnysdIGQ{C*rEg#2%#FMM-^$VQXn?~n6fTvfSbUs>$ zH*t>4_}VgLE91f$wug*spI|2YOuk?4tc-AN)$=xF@^m-9~a` z;E%dXyf~ir{SWrj77v`RCnmTcPR{SYsN7zKu7ZYkt7#=RJggsQkG>#lnA6@~mfF2g z(J6ZfAzJhRI+ffQU*t?2kNh>K?2jVfs;Tqx>=mA7UQSMqpU=|Kb6dQKIK?cDPEsA6d6!2h zjTw@%G8iM-Z7p4w(IpqXTJSK=B?%dG>jyAy8K)P6Qlxl-44efHc3vNIPfUr+ueT!_ zGcO7u3juapA(EY!S_m1hPoioRR^0W9^QvyB-jqXzJzu(l4hB||Sqt@wo!vPZ%g+n1 z9;YyDL8@i$FAHQaX)`mg;U``*YfL6>37i5ZW|<^_6_4ZTl3Hh1**@wT7Sh9~**@90 zR#xW?&hdfO)Mp1Yy`0zCvlAwrG`xWYT1zY$y}puLWsHJ<*)fvh0*F)*#rC_^_r6#& z%q*Ld=oboC^pfNh7{rQ#c9ZnkYZec>9LPuMD$&n6r}%vgN|h-<(RHo9Y5QG@&@F?l ziqH$^-$A@iNrwP>k;fSaZ{lxXN^9+HF*awxd5Tj?LDX?i$pN-`ohHBWoj45twt|Oc3LGDEK%WB75&)<@X>dqh#If`g0J%kOc>#U5G zK8JKl^VlG~HYo5&yn6cI$)6d=)2zz2B3RoKcP{tAL){4SG?2*P>QriyM^@`g;iYl3 z?V%@ZFR%PLx1#O$q`a*e{3Z+i=Dd+G#GQ_e+~$x*Pu1Yzr&USgy=${c68#!k8+d?xZD0v9q(8enYy7$v_5ZO^0OSKnnQm z#w$`L?by1**ImigSo8)0=+KFT3q9a;W(!SX<_?P531`8H9WaXHq2}T|`s_P&|2%$W50%=-`}vumw4Zv8L>E=)qRH8PX}^wDOZdB< zR-}U5E*+JD!f%^9l3P{EkHtl-=GnzA<4-BOB9Yv_1$1a+Btvo&ehw&ns z6%NTV4bVj=$np2%&+U+IlEMpNVa(S7@5abkZEnV;K2aHjsvS0Pov5C5lkkqaNYs? zw9`vE$WRp)X1DJdz%=v3-QbX@rb=S9_m6}o;a~cy~owDI)eeU{Qj(A|Qj5^~kALHW1Ar%FP8y zzHG|nRz9O?YkM``qNUz{s7*8? zt<5*?*GWw|>nD=sf#h$fqP^dc5J%84pIGB4S-2p-+CE^wz0-8*Pc>~DBjR+|Cbjj9 zm)X~Z!NeWKM!vLTL@Pi0-*4nXBT!Vx+VFNr`n9%lM~M_v=}|&eyhrPjq%7Cx_aFQF!t<{{$Iv{*Z&&far3_ z*P?nH>!@8N1|%YoTazjTCyUk!m7tDtf(;bY+d#~W55OXt#O4>NH31H&#i98AY9OMz z3wIHuW#wcvJ)rs%I5rh>(3~Z(^YnnnJVJ!OT7PZ&3egoq|IS2-bs=@5g#`zk=xz;|c-juee=pw>dB|vO z-EY%mydj=`!acP-f7`OB@7L=2uDK@nu{pf6S)^BA?h4DMvmKgcXz^O^TE1&*fEmrg zk)4~A%8<4w2q1BuFl-XVD#@BOj5)~%$LOs^R_j*Rd+J**QT`IW2JPXSihAHpIQG1t z5@LJsjJROz5LaFCde@wQeXex87j_)%5V29!_s4ucG>+N~_}GPF;GiNM?lY&sx}Z@= z==rBT-le*7&^z?fvrB@&?nVQUJ?^CTU<+<43b{*b78-+O;8FcJ31 zciL`4(d+qPc<9)_oDZIKbQHso=O$2X?C=J!n1VH?()XiVk-iA72an#(20UBvM; zaG2#0{GNm4vz!VKf~m_v42hwqp7yU9zEb(p)qsvVbWn7+=F<_!C}a8k zE;lJwwWo@xtN?QF8!Hqvf@SL<-9{Z$kB;@X;XF2;TETA=fEifh!oVsmBJF1SNw$^w zb^-jLwGNgwmm^dL^<;Q1j+N6|Qv3D{!Iv5K_K5)PjFp*feci)JuHBmfdP_&<|sw@E)HGW z_+Cbm%VZ3ggZkG#x^qAwWj#1SeSHRTU7!E(6FJf=n&hdcu|$r0#!U47-;D;$cLyeO z#lM0)Cd)$&-d5M8P~9PKqDOx|sy zjvB2N^c^h?lh%Zzcu}oG$w$?qWYI&|DOZw*s zNI5u%?xLgQ5l`SlZ@WCN+ET@K%A1bC_UI`{LmrD#UX-PzB$yoQ`GQ}dEQtr08PaCD z`~ZY2{jJ!BL#h_V8Hb6j67BsRwk-f%(RevIk!Fj;0=-gEnn4s$itk;JX-LJVLI zccFyoz~A(qN+EH0^}a92hQ!#Ke+-(iy_Gy6Q}w`M+?IU0cpFO!^cH{MRn7^gB$l}Q@v0sV_liEle#(l^#Gd} znpNn2bLOODLsLBkAVl^pBTI~t*y7{%Vc~~y*Gg7bIWfNy0FeK7~To?4Z1YygBqHUK#Mkrual59l^J|0BPwVUGs=0yS{#Bf zVQPG2zv*{7qfE;6Mb6+%`VQI=0bb&$80X=NR;M63y=AcJduwZG60A44R$=bH)3e^E z*|Cy`FQrm3rY}`>)6dE$w<&J#SV6jlo66Sk(K-*vXzG7FO>O2|wc5Y? zW_Mp*nYNZ3|D!$HyyI>_h|V-Oe&-cbklhL9cmm^!hZ{FDsOP=vrgG^1L&!uB|4uKmOGVT|4l1(Awq0{7j_=kx;SWSAMmc%_ zVpFySW^mEnFL}~^?m&w?N?w<^sHz{@t=D#<$eBpV8!4|TD*`&>>hIhOHJE98#V!FZ z$&`bUPoAggeAd=1((XQ{^j`lVVUkTH&MQRQpcq<(dHU4=2SIQUg`+#vxbhnt#0xNQ zDd#3_SF;yg4wIqT`tAOH!UPs=Uvd;!;pAwdR|y@W1;r~sXT!6Y<3?NW9^dlry2K{BtlQ1GmmsxEI595SMu?J4zXGgYSC z$R%CYL=Za_nXS?~yk#{M@^W8!FM@Eq(oX6A`+M%>gB*7dG+|lqoLc;a;+Wl2%KXD> z3Wb23$4+T7kSM7f8olAEY}$(dsl1YHZx^Ess225?(Nk$=IimvT^a*?g_p?@-AlnA$ zSq3)t0d-!4Lj!3iNs#VbV4KlxQCKFmNWg+1jdv!!{g|+S0tvz4nms6tmNc-N{*QiD z+*y2oD+$t?6}M|ig~;$>Zwe)8n=U1{Hx|B>V+@SF#3mEPc<#wM;p{@tEXAMb+DXqc zQqmwBNONQw>gA_q`v$_~xD8xK6BXmW=$mz%D#(*EfhVmYBNj&7`d+Qu8v1TO#mx+%cvs5s1(hI1>^t%&h?MngOByN5g)fEl7FfcC61~#T#2Op zpbIj;?b?!9zdEu&y(fKS$U{(@I{9K4dQv7NWaP!`2OHYy<23N=3`Zioxo;n`o zV@l?ZyQ%DW(3&E7T@d;Xw~&_buDn%wRK#1a&1s)4J7EBywZhd0ukLfG`k&s#gf9d> zo!XSKOC3H5d9XyCgh4><_|+I4jx|3q(1w9MhR3YoK~anKCozCU#kMnnaV4;Rlw`6| zH&j%gag$J*SdMmf63Io>5hiewT62ai-nnGBF^aJ3)m>v^`>-nuPKY&h)$YjYb%!?9 z1&@L`7hv_H;Oh-ZcQVA+U4D&+kZbq#e30qWR*Pbn_BGtVpE+C(uTc8hnvZV1ti?8} z8p4prxact?sN%8J?}ARv*DcN?SAL;Nv25LEL=HF~i{9&dFwC0NSpGw<)SC?%`_jx2 zyK#2=R-O`q4tsj7rM29YE#b;!83YFo@r(Y^kD<5aq}NzkA5hxH1zJx~G*i&vxGa;U z05ioxW=+!_E$dAsAikUa5Z4T8siOx*Hy|s*B<2&Ou)tdSj*_CS?H#(_w_^v#o>S}@^Fp@?HY25RW^Mt#^Avq!C zMY_9OU}>u_p2l3jGciW9sGKk}MhmWvpqscO!JnnAElwPBWO8R=8A9WsSTGb=iLN3g z`@|ml3P$XWMFj;zxF|@6>Qzr@(0^#7M#mEd6pjlL0$u2a{!rs8tA_%RSC+j*Altmd z$4myeHP4(Z@LVNS#v$oHQ&2%*NX;f7MHxz&=aPKMS)e6O$9gEONYCe)7l+gh?k(#q(I&QjM!`JLgmnN?b}52W6TF0+ratQG7chhcn!!@VK-2i+R-E6Z+cz3& zE?hCV#}b6CT%)Y;x!epq1S4Vs7$vY?e?8^cMHWkB7#FK{13j1@?A3-p947=}=-aVH zl;l{WpDI7bYMJh81ioE)u=miUh960-WdIouMU`4$r3Xm+IlX)Itt6&wUBf&`465lub`1jgGx+Bo{5!9=ywX%U!8z_KpF-Z7RQDYaiwjt zRJxz7WQvdgpHbfQF)l-xFG4YO49{VCWvN3Y4V)fYSXr7vvuHVau%h@Y5R}&7gE&EW zY5xx1>Dl_z*}hrqW9?u-XKn|aZMWKmo|tReD=sBG*2wzM@N~rP!>sy8>u<>O>_e2N zhT~|O&e7)1C8~-E1Qp>k!DB-U(R@hm)L~`79}kO*Zf5<0J=^qoXu51`PXRON zEf_8p)5PW{)I5a78DbZWyH;~SvNf@d!jnT<8^%z5dC!LoyTk*cy`1=}wK6_sQ~-Ma zL(q7p+!!Fjp!6WpUe-GmE-V9eolq^Lovxeoxb>4n&}K>JyS5}8M9xZ&)ASa83Qk`k zup$*hHkN0zoHLjucIe{u%oPD09xOq)C$z?(qFR`K2i-vJKZJ-We(Yp_ZzKo}f^=TB zP}g-6vWu4`SnY*_p=`Rwl!**Dp0bS-ER>KY9x|!?M0Li9Ql{|48O7!X^F_{eP%I2` zV*`5ofV=%v0nYzkM3OI4xV)FAJ5iEkyjKsmv0B-MC6!Kf?xBbZ`3y+9vR-Apt2TH} z(?LDar$L=pno{8?R94)xx}^s+LFSa+gq7nZ>9>}1Kz&mU#a#S_t*$(24XkikKex-F zbl5nVuP!Y>LvP2BmJ5MCF?>pqQglx){e+?d6Z5nv)6+;B*aOX?x<8Cfy6pYdU~L`y z058BYU<81nRu4koi&H|ecvBn=o`9V#P9QzkW59>?^Z3Pyv)*Q$?6Ske=t1plpuftv zZ{#N@S=%&pE@5r%f>1^NsAl;>W(%EfUidfp!y)yEBM=RDsYWbA$GJa9e+*_ZsR}0=wz0N<`1eA%92}E-~`m!i3%iY3;rzR!zqpuk0SN<1eG)p;QVGIIG*d#;x(0C7J z&-xbmH{?YnTYD9AyuzQJ#amdX!nziz6~B-VJRNL@%-lhJ;rQ zk}+N%-MIVsMi=t`1}k7mSP7ky?D$AIyq343@)WG?JQc!g*Jy_yD^-#;8xtS_>T_L^ zq~Xq{yo(R|iV%3|7V6x|MnVKU%mwQci*TqsI{sB6?vRAff@l66c)e79>y5D@Fv^ag z5sw4?{^)cM+`BF!;g4RTpnuWE%@Z9Y7p!XpUqArXcwAO8+G&y`7|+}S)hGQ9T$8g- zmb*8b`gApT(Z$^aY)jgoEk)?zZie_aW1pPQc2^O*9MOP;blH~IIzvpK#?F#8Z5P^n zFT^Q@2n{P}mouF(6n|g1GdRa|wJ{bdjk66b(VM1W@!?p@``p#YV#?v| z+cKuHd{U))0Yw=-R|ev)4C$D2!RA#>5QCAI+$&?u074I72BO5zA6|zZBn7x=!P6pI z$pvLIiZ6{b;MWUwx%%hJ)GtFi9))5RT3Vb8!;$Xu}bXSg82Q|ePGH9`@h z#SzvP5)^9P6buwI;uHC-H_rgPxm%b!ZTF8ag1p3v>e`-UybB)-wj!8;C9?`^=!~2I zUF**aNMW_Jk2+7fUsyKp!k!nM^h5ZQ1UK>m4YWIgk2R2APn3COfyEI(Ln*cXHp^Pi zI{W%4;G~zY2(ImSX$WztdIczpfw7|S497+x804{wxvawIAmOACx(whDbP!|uY#*NJ zAe_DaM|P#+ zD;GxlbHvF>TJup!Dd76}cY@&bV`LLRY&lwB78oK1V`5lUdjuw}pe6(Z&K6O_j#6V> zIMw|Mz{ge^h=;X@tKl&=xscdMH4J^22c>R9HMWi_gK;3(m8$rdQ;)yrQ4 z@0^X%USUV{p>jlsRWl`G?REiUpDplluNw{^4AOtjISwvXRicUx#k6wA_N}1HV~ZJ& z5*buihc!>7R+_#`KQ~$WdnIGxLXJn}aV80O#6XvIQ3=HE2b97y)yy!$3Y6_qnSWX% zj`di*`*N;bL{mt2OfHylG5LOBh?FyAS{@3`)|gEO+Ruhce(*r z1dWxBg-NX%ogo?^mK`*nGk?UvobAKnu-b!jDqco)wBL%Vn zvWrPmC^ZJK3ZjWut`)4|{z?X38=plao5(il_X+^<#dTtz|EIBUjL)psyRL1!Q`?C{r*9tTgP=JYbgL(3A zx@V$yoIqLz^>Q9}NbUOanZHJM zWeTE|cqHdS61b#(@00)Gn}JiKS`!2z9FvmofZ`u#^mqv$iq{k&bfzxsEw2Hu8F~t-7sdS;Um`6N}{YoYs2br|~YtdBXytG|+QxJQ+?6CK8 z9niBm#24hvR3O46(_FKB5L`qwWjmFb_d~{Q#n14tE;xQ<_I|Wzk?X9M40fQXzZc-9uu4J#bg1Q8i6X2*?(6N1D*sjUjZ`2+w0R951uqM!yOJLP!iX zcg4bhVR|xIWl7;hyC+OmY};f2lV<;fZ=dB0{{B{4`D;L@*ZI4ty&{&p>WV<2-!mv- zhOVlH(437lP``_yT^9xzv6LW~5uyXSoE2b}@%xxFAoUuz<0(03z+iXj@qm%`YCOSL zf@YLS1b*gQI3&jHc_IV4s3%_5JeR)q#Qf;~H`)uA;ln);gDGf&ZVXlcOwrVZig~*r zdYIt`)L&eIhAH#pb%;T01%U6zXb(A z@`}D@WEW+fCaOaUX7C`Dkfro4(*PU`g7&De%eCv8=#VQ}K7=hx37)x<=v8S70{>3O z|AVK#rAYWJbvsB(N(Y~4-m~Ea5K*Vq=(S&$mt69kgEKCzhPw)$IldP}!WHZUULM1O zZVVovfoDT^4X@}LFxAx7Hn|21qN*8z$W+TXqxu3y6aECG&_^Y(%c7#P(&CD>WGsG! zyL3$9t4S8E-~@m#IP@aH0lAU%{^BG-+<=2xA2rRj?TL*KG0NGGPf{4ZX0q-|XfC-7 z@i&eetSlgSLQYfydWC44>K-N!FBTkH(f zbe0VI*GL=tH@>V%c@jHkqyy-oEKANy_y&a90(!VqOBLs^q2u48#6O%ZmpD+MxvVhu zC(O!oXaT`LIt_?ecR^g-eV=nfGsB?-Bn0*2n$uk)W^2L6iFrfvu6#WpCpc2r^h~ha zXNXA#L3tC%WPf!JytrA5#XeG!sd98Do`dKZHUh>SCFtyIWwJI%2U;D1#t4)2d_xTd zjBDwYR5S|T!jiX3jj&||WKhpi^$kV4n}1DUD(MSDqMD~r29PE;vDs#cXuQo6?h}BE z@on_er-9VrmJU*N4N@U&iS7X2p&4t128+dHW;8b}$QHr=efWGMS~7yWi&r_6pE=pg zE^8asl2k>0IMK%};zA&jt-()x(g2=^$&i>g6YY=8z#)Z?kt0n}VJDY>XC2>JSf%Y- zoj9i;UtL#-=UeTYV%Q6rY7El4)J9YqlMxVXJPPInx~${KX>b-A+Z!~tpMUtW!q6w5 z`;p4NF^S(HxE3DdSD!46C3O5Y4D%3BUk2~57kHwIh)o2qT$EHx*3x}xH!#Izdl$|= zb`p(D3)U&4tK6=V{p|gnqqAr)50CfR5tR4pw1Y@qv-!%48`0e-ThRf*lfV47hNml2 z%-VXd+bF7V2k-Wb&%xnd*y|{s$qgfPjCNmhip6HA1GV}UZLYMVHVt0Dv)A3mfgCUJZrFiV zYtn>*&S9k$OvzI64N#)v9-*ESChw28Zk)1JpH`qA$!xdaN#hSUgdtfubNDOT6vi5t ziNkklVw#!}B3;doPktL3@kba95Q2UHGev7YPTT0Pqu3$G^L9ASUfJY8a@ z0QdlORd<1*#Xtsz_8yafc0E@xkG>DF=B55dbRg1gr&- zZ$Poi9+sUP<$)xW9Dx2DP^xu(Jj1KD1cBY+HLUU=C&gsy`hGpK@**RiB~}br{<>U; z=%dJ9@2y2e3v!j_QU#V2Mv_WDX!?_o`|#MXgS(h2%M)=x;wW5HHN`{4TDtEM8_aUL z*eUxqDokq~4XnW#gM#x;NL$E2U`8%2eRzZs3!u|2FqVabq|Zb9ShHineYwhhavVG~ zDaMmZ^JHO@_GbFhGxpXX1P3os$(p*V3*Fzpb9UA(CG;0jH;ptc-L0YZK^me$Sy=84 zHw84TLG1+(ed%YbK3u51HTz1#DHR15+T82okd`+Dhaa7JFNEZ{ZRM?&8rM+v%0lQ|t;=sReHab371 zq6-i0t?la*N2qDAyB8VISdxAaVn z)BoZhgB!H_a3^)V6;>l-8yX8wNPIn<{RgR-;#TCxuwKTaP%?r|X>H-D(pF@mDHwk%kPMMtb%*`|~Up+IYD3cxd5gczvT zA{mFL5HdmPvpv0TH4${aTfum-;ds~SFAIq>jpUm=uO(y&Z4Ofq!A@Y6DbUZfl@O9> zpn2NXDLk)*WaUo*GFK=|D4q zlmPQ^>qF@NV#o(Sio%J`7Y1TM$+6|IhkQ^QW2~7Lu)@Th84>(3#|m<)p^;eh0>Bw$ zm?rpx-n=Bw@(7w(f6{aH)btXj`D8mY2NPT-mbJb7h7WXVQ2B)!#x+89UaGkcz@^0|RK!)6wQ+V|Q`*g#xo z^zYi>aOj0+?!yI8P{_vT2r=pVG^&gfR5ORp5N8X;tDe^1;Cx|{yvAmzuBPfatj1&M z?g(7+7H8CaL3mzs;SH}^o)c~>S;7-e6xS_FmVy_mdqu)+*ym4~7@02UfOKlnvvR4g zxq@rWAvDNsg8ul{L3#hMF=hK|m_#HE*FKlmoIm;RCa8-;8MeOM|H2+y{W>w)evIJ zgzb+kqcii_Cs1(Mw?C!+?~0{x)P(+SuUg( zJop&nN*&f)=(^F$&VZ|{`{i;@Dm8a0#c7MXksK!$R8usQ&Uo|4!Nz5^fy(hJIa&9D z%XuE9j|%gFh`d)Sg(iKayE`=Ly|u0r;q10t-3QdQ;JN4Zo+@9eM@=dUHsj%q%@zW+ z>Cgojg!3X_tB71W`KhG>`$O$6Q@_B1!89psu{43ZBfpaPW{?PFks||)2W9Z0B@FJQ zo3_(njhf94@tUV~VkrULo9-4E`jOP|b&`i@p7fu)>@5vPP zX9#n5Jk{rB2jD1=ANV}Oo)4P}dkdg(tF4*^hW55UMM)1e@lNteT8@W&J2F*xOF@&kU3ebM%SE2{O=^7q!qwAHWr z!-KyB1S4je$0^j8Z7PSToVA0Nyo15orNop{B9UVjw1t)rU?2GfFAMRMi zdKmWM9m0!j)rvLiRS;U=f$P+69+2e;LOJD2SuVU)vC^6i5XVEg>!G?DWLSS`=j!0L z6G*OmKKsFYBeGdg#7Oj!NE)DG6QXA84s)g8+C>LE3OkIi_qgM<0Ud=E@|(qzpi|)Cgj|CbhrL_?mXNng)}gZxiejFA;-?+0 zvaPDfpZfv?MIudTfw^NHsMeJ|ol#jSWxwF&RHv-X`G&(yBHw6L4}(mZ_+tf(tkjK+ zDVOBoD;9CduP%?H7LHlLUfF9N6K2d>1I8s)xa?P@WD!UrF;(i%PXWrO56l2UEv5)l z<#nGU#Ays|1{+nF^aQ7&s!oNzSvglSP!CMC7InTf8bedETpV~AvSd^4q^a8sYIE735Xx|&FW`Pg zMy-2Hqaq_`kNp=I&m^HbIh%cbRBP>F&fQYp9=0(N!Mm7CV7i+On*oI1NyTT#HZeOMyKwQvbc($w$NF1l!q&=v#}7$Eq)@vFKZLv1By|N3(MH)8@ZO zX-e{;UMD~AKDyve^+|4Bm1ba!rCycgHn6)<$qf=9YF=I5Q_T9Z0{0+=7@fVtgjaJ1 zYObDHSltIH-XavnBnrN}q1b7mQW)-u5b(uT7*~#tlSuKZic&%{B5zb?X&`}f5w=Ln zza*ymHV`FDTore0$hNTBlpdf(ESVBK{Fw6YnTWB+@${0Ht*qe>Vg^HLLeLOtbF&nP zRyi*Q)ez=la^8nL^qHJJGOJKYB`3*9w0U<6u+LEsUcL(i~Lc0BnUM_N-X6kKC*Ww4E5JfUH{{HV^t`M<|hl>J2x>Zbpod9fG zx=J3h;QjG*591I{4k`FAQHDaM*Gk-{qFW(ac&4)L#1Nd_uUi)wZcb{G7xgNFRa1S2 zdX%I^@@y^;M89sv5DsONCinWb89TKLvY2*JU7hD8l{iHwtkYQQVVGg5OY|AU`RPyW zraaGc7rpD-XjVR1Ga4-SS!g;6KXqaO$(nOXG}l>yv(b5*y$eXd1{3J6`ln3Pm*5izuD#ySg9w5?Vw-mM|?u!R(l{Or}G zupNNyR=w9X`p642*B9P0p4F`_8|ni|lf~bIH)$LkRUOv9CvyRG0t1l6YjOHKdF9<> zVu;1qV@EC)Z}}c+mcUD@c4Ad(b@h^_$cAMEKmlAr37azzHV4;f1)#KNL}5|Xz+nLv z3V=Bon7-in4_#&(IJI(8Of@2$QEWN`L!FiSxo2H6+H zugN02D?{mT(*SVb?68fkD~g=i=|-O6dZpal&HS~4PouMu3YWe%RNNcnW0w5z$Ti%F z3-x3JUh3&l=-Si>L*n1(f(%v!Tj}E|D<)R*r-Mr$2ja!uO(xn5;9dsc0(lKim%y3k zvWU;q`Jq0ZczTpQ*Z?vp*;)}x%I{g(=F320B@jwMzRi<_Ago`xQOd)?eW{RDgiH8m1q_0Xm&y0N{WXlm~I+97Ti~17al7je^_W% z(Jm3VG{FwK+g7Vej$sRZ;j8&6E620yGdh%I**KuntmSJtY;30n}%M3 z&8MKJ1YcfMR}g2>w1JPQ;GDj>j2;7u9{sG?;g&aZJ%bxy#187ZQdB*s$2JQ1PcP@g2m zoseO0->}PQ1zaD2fgy8NkM>9 z5ClD@V<@ia+cqjglR*X1Ll&q7l|4aT-Wmro5$KvpE4TQm85D?TBVb~ma-6cbI*C~{ z&Nv=#;B5KO#+?>$(S#XKowH@@09m*pk3=h_an~12u2_Buje?+t>tbp?jzX~>8{=yF z`fyJk-iXLwAqS7CH&`zVKsJ>e;1?J%jJzMF;ex9&RLMvyZVSHkJVlY5}mT@~6N zcHN24@B5Lm#b^9JQ|Gqbl0`vqXzN~jQHM4HUG}#7Sf&wj>7z>R`+aPu8mAVX9bl-N ziVwytOX&+(-8yVW#N zOb{vuJ+@qqWK0J24r@yLOU!)6siip_X-r+WhyIQj zGu=YU=A_5BDJ8Q4dgx|(aRIK_IuxR+xh<7)(+kn~hIF@Q`bkBwL0EI9qcam^m&l~r zrqL_YX9g6MgNgRN&WE8lvVo8zd@3bo2a#={ZyqRxy0E8-t3CqEFP{oidzG0W;5%+S zJYjHlxZfzN)qL`HzFQ)Ip`}N?Ee3mFGjQ|Nb80TOn zntMRfRSIGW1+dlI=fRYS?|lk`f9}PMT{{G>4OE;uLH2ZmGsv40wvD1-rul_oc1$M~QqU>)SrklE0?7rAQy=hs`4yTwj9 z6T8F}x(k%(dP3ON?=f%Kf}~C7z%V0neRB52*_TWu>U@81+gXkPh{Vv)Y$0 ze551e{mk{#NFD>*igJ0-X&MUN8}*>C^#tTZbb|2F$TM zq-L&*j2%*{a@ENeg&qj!H@k#8VlkDbXefX2SR6#U=q`<5T|`@7{!sO;X}zT+o-Tgbo`rK@zz%g0FO?-P2d zH?6#LpDU{G%F8+=)rsxOH?8#eqKB|y*^Q4GE!b?|<4eCg0kr3k?zM8cuY16D5Hdp& zQ4M=*xJ0HY=RKt@??!IY@yOD*yvI}*qh z^e>Z_D-vW$fr7W*@RjK8%*6{fw^xg4nL~lZlWXK>U|6^m$+Vre-%{63X!onbQmTf? zmS6YtH!eBSKia44re4E>0>MsK9FiKIH;*Y#ir=CXH{9y^{i<&ZK z(qy^|$<$8RrbswN#26imJniLX*UH3JlaP@Sawey1YN?Q}HG)rzJvfZi#+7eOsFoiI^%1W4YY}a9B;cKeh%o>r3#^H=V~sZS3)dR4#-EH8@%h$%hH8 z=pcf*FaSd!2woQi)C`v7JDN8m7mg$=GqXHG@GJzu@DC($CMaVXxScWz2~cM}Lf^F7 zSqYlpHGf34po-fDW)>Far*(Mgr++eg8xa0iX78Oy>&MbGY7+Jq`zYK+7S^+ZhK}_f zSHV{AIE_h%H&y56@sCy8hnWBK?9L(#+VZau59|4-c3!)UXR2{1?e@i8j&-!Yh=c?V zF#wkIEN=v%Rpd-MaH(_e!PM()XcN59e! zlKQ9#N1nvN6RbZ-&fi|XzWb=cH1^GU1T|#U zvhCCpr!p8i3ZpxHVq+ z>&z3X{*s<_ku3H4GW=`MEUVv8&b!aDZ9L;Bfi!)Ny=(}9fekuuIgNhz3Ac8WlL<(cZ5;#x-z+|If${_Gn8_mI#2|@kKy0)O@U!UT-F?G@ z0^iVdEMjxsQ@$`XD6M$FL$EhTzTg+_(p+1Q;FdgcdOHWf((896gs)~Ae3HkSQB!wK z0UpwL+2%aCFn#6->m%1rikV($KNUPg>5}n+b|W)PV?f`_@ z&hGP;<3DUSmaDAHOikf}P^K&63g^E_R^Ic|y=J>MYX6FBBqae(RJhcYt)qe-4g0Ax z+nLzDc(#mGVhisQrnfR!n>YNy?KZ+o35>7Dd;ov2B!W7~lrY7dhK;v@f;}?7TNy9^Jh-BOCx7j|0VKYP zc;Add#e=X6@9D@+FZ`k1j~oS^Y}8|4#+fm~mlXWj8 zueVRUsJ%H^Xi4eRLkIWJZ6Ot;TQ<>I`}L3@7rMN9TB{=HAA8gzjia{b7`m8(IUOu} z7FVwti2{W%ltyw|9D(wnx?;5G`9fTGgl3AEfSfkd(qv%=(oN6LQyH79D_i9jmU8ln z;?tBdc~uU=mEz(!Fe((eGh$BCb->b^Fk{yYp5%a8*ikIU(Qtjb7do||8y`Bg{W|c- zD@StMW+#ii1jK=%j&_`YyAqSe5$mwLNeU$;nKNw3nXf4V+zr$2_7UeHS-D|LFbg!i zBH28BbtT0}2dwp`us84GcTsAe zJoiY4`v-)Xy>R*YgedN5U(SnEk`JgA=VR!SFUz7RTpr{;h59pbcgN`q|$6C zd)x1-ko>rEU%FOm1p@1M2Zj)3qLHKgt086C9r7ak^o}_!ex2EGt1kI=^>^>s6|tnU zO-IUKDnJXbJ4?$5WtTcn)%f2m{0^p$#3<=li~c;Q0gl13v39e|UlQw5D9YGXUhH$! zz2^r7B-rEagk93nQ3Xnhot=D^qV>KTV_rKevO< zS1oQSt`ON1c)~Tp?JZGg7+X*&?IsO29f#a=vNaF#1!5{2ttME49kF3N_rxjGzLx%JSi;7&&s!+(p(lfC zupj`J?{Wl*2rD&-8NKoC8IaxlAUIUi(xvjqjn)P=E$!{TLnImsuzX7rHNXdvx6OLd zve7wcH7`z$WCk-wkH#4)@JPrHUy9~P?EvdY(hJ_+*wB{W2(N9vFx#i5P zG5WVg0o=|BHqjM_t}mimhy#}J2lSL~E>s!)BeZ!?#PlG!rC`d!0}{DYdK#$k-HP|x z<@xzVi|Z~}*zrrxV;)4NV?h4mYTNg6gNcl|N;%P9gouj6=^OHo~kcTjKJai?fyyICxCG zAZTqX(6m2gW&-n#d877(^|mNl9Fma05^-(l$bT_4;af=7>B=eN=6k4Q%U)wnZYUvf z91^)Wb_cb-1fd05f0K!}bF0d+$WuV@e=c;hC%mS4H0$29b4x)6Y?UH5$?XiI&1qo2 zp9lX*6X`V{pd31Dj!>NBke9s9B5q_MZ>Yaqgo0-FPTaqqDcJ=Q=i?N9# z*k|zq1(*<{>lxgApEkYplTgpI6$Fk^#3BlR;;qw#(2_hUB5?mM@_{mbJ?stt2Eu05vyF$P*i$_df#7G;6 z?r1Hw>%6<+6~?}fN=1zEjdFANbozQIFR(<1`ZBCCvx++sr^8-~FXM|_7+}pE3w52! zR9>bUF*{G$pSpb?9gZ@+)MY`#A2;cy=WxoRsO7GwnOad?m0LYiuoD@^Br%y_(fdUm z;hl7qPY${V^SU|oCB7uK(N36$ayV`IuxOt!XLXoW{*v(we$sQR*`y8LojVn03%2FN4vb*eV+z3 zaCFKqdpH7 z4Pu{2yy*jgeX(Y;teJF!dcx#)VqtK&gVFO=9X7~rnwY`WPfwLzg-DRg1V~X1*)%B# zBxJuCYm{Lv*Quf8mPR1NpFzkmU(U!gPn}tml`AAA?nue~{L0zEA9#sLhys?kl^cUG+wqGN=vQ?y`%Vbas9bbb zk*-1cV15jRggm*pFW!jCxUExakEB)54XWzpVpXXBy2WBcAjo^F>v00)*K9h@92i$b z00S|5t8$e>VI_$MDEU16>NJ|syoT*_SQh35Ht?_*alU}w_06D<4GUkw@u;!}wFefS z`k!s*uQm$IYY#&4X8~>pizf!9mTimJQCZdpCw36Tc%6!<_+Gegk>h}T`e734lxAw6 zGj;9Bya;!0>FTFdSeaAvk3KwIQ4{!FlVaaKHDRPp^brA9!SDXbNg>YcJa@Mt!y#$X zFk!QVE~(PJxwtr6pZ+S#Awe@FePi;>;m}DF=Co5p*r6WZImUtt(J;4I=94udsbr)b zR#9>rlf<7%HZ_A}?w~F^k|LrNvnUFx=3LHFof_C32BdFi4HVl<2_#RYXc;Y8jrIH)nVb0XnT2 zV^E1RHkG7lP3iEo-zcaSQ?-i$Oy!RO<5lb!n&B41<*DSRf%cx9eVA0 znLnRV5i|9{k2L=ayf~M@PfUb^k}&War=b?Y#GIK%Od=r;Q@NQWsml=;3hV?`xVV$S zuo6)G9jwd@ynSp*uMyla*za4EP6470@XSP){C>`!SC4IN^RE%&Z>kKxB3;71^(qkK z9N;8CjO~ft)-9Z|p^NOWTt1rQQ}T>VpE+7ey7JgP4|@dFJym43|C%|ghlmn+6b)s; z-8bbbuTapB!o_n!f}tLB(nATIXxZk|SYfYa76 zp|{%h8&q2Zi;ELCKbBIOdDgQ?TrMLAOHhOp zbOSF?pH4@7yL@=ix_NxeZp-fE{PNTp12}B9-D+^V{^U0)l!Ge{79)BX?jb`? ziy@Fji!f}_zWg+_5y=|H%p7H0wad1Yq#dyDMkXZ5NG&AA?`75~yzZ?Ojv)1uC=oJd8 zgw13@oY~_ah?Cm^?F|_Qwm!sy*m>zhkXJ>B)KxSFdN)-Lr-H~sHtl+c!WT7<6T+XR zpSV$MLf#NK)P^2iwfR=)+hyhjNZxG++xP1I{=@6n*-K}!`AGTTBVJnN;x?s@Jf=oh zAUX%#jq)^BJfwEPGpp*p#)9rVp+$|vQ(CuQ;dz%EM1fL2Wn~`+Wpf?gxflp(yLiTB zpm3t)>#Q+=rA#F`$Pvm2*_;tdjsXVt$Y68smt;kWTitvQEJ)j`EQnpjv-!ixJuG6%^LpCA7OvGF~0j@k)&4L;+&p>7U7gPRM@{Qdz|) znWVv#)%GaDmXvP^&#S;lE}$w;)G9BH2OPi@I}n$Q=XenxlMI{r`6`C@sGeS+m`oMzCJEMFs59B+s0tQE7ng~6r(gJeKRVWc~;bohJT}&8K z*PG6DHhy|uZ;y{gdw;+c8J3d^QBHkl?`Kg%DN z9xQLxu};-|J8V;#**bXFnorW>ezPoAFqK@Me52cb3hJpdn`LEwa5}Eeqf!b=KbW8s zbktHyjh`$f=jUwLSM z=d6rdmjGr!2>i7CBN+2!TcsqNv9uOG$`teTAxK?^U4M&_YV>sf6qT>3`gvu?Xfokp zKjdcb;O~-~V08tRqy+2N4ma>3(|voPXA4cpa6FK|YbfsSjFBfN91HUhW5DmuBq9JARogAsPXOSqK-3#*mnSv>JED zCa330J^8G~0N%ijZ|d=;$n9Ww6Ewz+G{7$EaGx#w62tMsH4A;U$EZ^?g4 z*#A$>DM|e#@$lJWRH$CJo_z29Hs=#q>$J7vQdf?7k5~r;*Cfny?Jjt0x5}sGs9M#FH685^SOb zik4w@+W<>D_|!2|zj^yZwYi%~u*mw;<1qEpgzvY*`amSx3>;#>igl_sXd; zq3wImr}N~aglF)OMIr{LqS9C0BO^=9BVdeIkct8HXLgj04l-Xu00{}RWUxhi%%VXVz4=zGj5%?r?M4P})ryC_{IjF=6h&$!< z1>K49vs>JDNFy@EIMWVab4lbxj7t1qPH3p)N{huHz6l+A#L;N^+-_Tm-k;QmqKcSI zm|R$?vNG@mc(>-9bTn&etvF_iut@S3>{^XfNU{p9K9t~;|GXwYqMzt%fBSmrj*AoG zF#Qv$iP0XS!23FyJ?1FSodiLj81&6On6*WR;Jd^Vyf0}pI{tAU@W$unt9h!gL2N^_ zV)xUM0T2iU;BQ^+KYQr2lK=n#zI^_ra`+$n>#qy^&sK}SXQ98+=Mn`V#P(nF&7Xz- zIV=6f{8ebhzcK%KhW@X@ep}D}m3|lYJN*|U+W)-CKdoqgXa1`^vi}PEr!noHwf)mP z^>1|j=g|Ilq5rZ{{S*GD&*$H8(a%}rZ}`7mKmV_r{%vOYSNeU^`JY}v|FpLJeZ&8A zr~IDK{z|_gGj{(mYyDZ-KPR}~z`t7R^*@3CZMysON`DHr|9vG#tbbPFzh2`{_@9g2 zzu_)c|Aha?()UmNpIf%S@i*rG#Q(pYT83MgN8m{HxFY z_8R>Q{QuK?@ShLvKg#}>zsi4(^FJL{{w|gMKY@Syt;k7&{^O}d{8ZIP_^jHf<8RRa E15O*c;Q#;t literal 0 HcmV?d00001 diff --git a/pro.docx b/pro.docx new file mode 100644 index 0000000000000000000000000000000000000000..35100838d6b1c244eaead79b2cb44d42a63fcf7a GIT binary patch literal 30873 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 literal 0 HcmV?d00001 diff --git a/src/mocks/handlers/concept.js b/src/mocks/handlers/concept.js index 8fda2180..26345eda 100644 --- a/src/mocks/handlers/concept.js +++ b/src/mocks/handlers/concept.js @@ -521,5 +521,184 @@ export const conceptHandlers = [ query: query, mode: mode }); + }), + + // ============ 层级结构 API ============ + + // 获取完整层级结构 + http.get('/concept-api/hierarchy', async () => { + await delay(300); + + console.log('[Mock Concept] 获取层级结构'); + + // 模拟层级结构数据 + const hierarchy = [ + { + id: 'lv1_1', + name: '人工智能', + concept_count: 98, + children: [ + { + id: 'lv2_1_1', + name: 'AI基础设施', + concept_count: 52, + children: [ + { id: 'lv3_1_1_1', name: 'AI算力硬件', concept_count: 16, concepts: ['AI芯片', 'GPU概念股', '服务器', 'AI一体机'] }, + { id: 'lv3_1_1_2', name: 'AI关键组件', concept_count: 24, concepts: ['HBM', 'PCB', '光通信', '存储芯片'] }, + { id: 'lv3_1_1_3', name: 'AI配套设施', concept_count: 12, concepts: ['数据中心', '液冷', '电力设备'] } + ] + }, + { + id: 'lv2_1_2', + name: 'AI模型与软件', + concept_count: 13, + concepts: ['DeepSeek', 'KIMI', 'SORA概念', '国产大模型'] + }, + { + id: 'lv2_1_3', + name: 'AI应用', + concept_count: 17, + children: [ + { id: 'lv3_1_3_1', name: '智能体与陪伴', concept_count: 11, concepts: ['AI伴侣', 'AI智能体', 'AI陪伴'] }, + { id: 'lv3_1_3_2', name: '行业应用', concept_count: 6, concepts: ['AI编程', '低代码'] } + ] + } + ] + }, + { + id: 'lv1_2', + name: '半导体', + concept_count: 45, + children: [ + { id: 'lv2_2_1', name: '半导体设备', concept_count: 10, concepts: ['光刻机', 'EDA', '半导体设备'] }, + { id: 'lv2_2_2', name: '半导体材料', concept_count: 8, concepts: ['光刻胶', '半导体材料', '石英砂'] }, + { id: 'lv2_2_3', name: '芯片设计与制造', concept_count: 10, concepts: ['第三代半导体', '碳化硅', '功率半导体'] }, + { id: 'lv2_2_4', name: '先进封装', concept_count: 5, concepts: ['玻璃基板', '半导体封测'] } + ] + }, + { + id: 'lv1_3', + name: '机器人', + concept_count: 42, + children: [ + { id: 'lv2_3_1', name: '人形机器人整机', concept_count: 20, concepts: ['特斯拉机器人', '人形机器人', '智元机器人'] }, + { id: 'lv2_3_2', name: '机器人核心零部件', concept_count: 12, concepts: ['滚柱丝杆', '电子皮肤', '轴向磁通电机'] }, + { id: 'lv2_3_3', name: '其他类型机器人', concept_count: 10, concepts: ['工业机器人', '机器狗', '外骨骼机器人'] } + ] + }, + { + id: 'lv1_4', + name: '消费电子', + concept_count: 38, + children: [ + { id: 'lv2_4_1', name: '智能终端', concept_count: 8, concepts: ['AI PC', 'AI手机'] }, + { id: 'lv2_4_2', name: 'XR与空间计算', concept_count: 14, concepts: ['AR眼镜', 'MR', '智能眼镜'] }, + { id: 'lv2_4_3', name: '华为产业链', concept_count: 16, concepts: ['华为Mate70', '鸿蒙', '华为昇腾'] } + ] + }, + { + id: 'lv1_5', + name: '智能驾驶与汽车', + concept_count: 35, + children: [ + { id: 'lv2_5_1', name: '自动驾驶解决方案', concept_count: 12, concepts: ['Robotaxi', '无人驾驶', '特斯拉FSD'] }, + { id: 'lv2_5_2', name: '智能汽车产业链', concept_count: 15, concepts: ['比亚迪产业链', '小米汽车产业链'] }, + { id: 'lv2_5_3', name: '车路协同', concept_count: 8, concepts: ['车路云一体化', '车路协同'] } + ] + }, + { + id: 'lv1_6', + name: '新能源与电力', + concept_count: 52, + children: [ + { id: 'lv2_6_1', name: '新型电池技术', concept_count: 18, concepts: ['固态电池', '钠离子电池', '硅基负极'] }, + { id: 'lv2_6_2', name: '电力设备与电网', concept_count: 20, concepts: ['电力', '变压器出海', '燃料电池'] }, + { id: 'lv2_6_3', name: '清洁能源', concept_count: 14, concepts: ['光伏', '核电', '可控核聚变'] } + ] + }, + { + id: 'lv1_7', + name: '空天经济', + concept_count: 28, + children: [ + { id: 'lv2_7_1', name: '低空经济', concept_count: 14, concepts: ['低空经济', 'eVTOL', '飞行汽车'] }, + { id: 'lv2_7_2', name: '商业航天', concept_count: 14, concepts: ['卫星互联网', '商业航天', '北斗导航'] } + ] + }, + { + id: 'lv1_8', + name: '国防军工', + concept_count: 25, + children: [ + { id: 'lv2_8_1', name: '无人作战与信息化', concept_count: 10, concepts: ['AI军工', '无人机蜂群', '军工信息化'] }, + { id: 'lv2_8_2', name: '海军装备', concept_count: 8, concepts: ['国产航母', '电磁弹射'] }, + { id: 'lv2_8_3', name: '军贸出海', concept_count: 7, concepts: ['军贸', '巴黎航展'] } + ] + } + ]; + + return HttpResponse.json({ + hierarchy, + total_lv1: hierarchy.length, + total_concepts: hierarchy.reduce((acc, h) => acc + h.concept_count, 0) + }); + }), + + // 获取层级统计数据(包含涨跌幅) + http.get('/concept-api/statistics/hierarchy', async () => { + await delay(300); + + console.log('[Mock Concept] 获取层级统计数据'); + + const statistics = [ + { lv1: '人工智能', concept_count: 98, avg_change_pct: 3.56, top_gainer: 'DeepSeek', top_gainer_change: 15.23 }, + { lv1: '半导体', concept_count: 45, avg_change_pct: 2.12, top_gainer: '光刻机', top_gainer_change: 8.76 }, + { lv1: '机器人', concept_count: 42, avg_change_pct: 4.28, top_gainer: '人形机器人', top_gainer_change: 12.45 }, + { lv1: '消费电子', concept_count: 38, avg_change_pct: 1.45, top_gainer: 'AR眼镜', top_gainer_change: 6.78 }, + { lv1: '智能驾驶与汽车', concept_count: 35, avg_change_pct: 2.89, top_gainer: 'Robotaxi', top_gainer_change: 9.32 }, + { lv1: '新能源与电力', concept_count: 52, avg_change_pct: -0.56, top_gainer: '固态电池', top_gainer_change: 5.67 }, + { lv1: '空天经济', concept_count: 28, avg_change_pct: 3.12, top_gainer: '低空经济', top_gainer_change: 11.23 }, + { lv1: '国防军工', concept_count: 25, avg_change_pct: 1.78, top_gainer: 'AI军工', top_gainer_change: 7.89 } + ]; + + return HttpResponse.json({ + statistics, + total_lv1: statistics.length, + total_concepts: statistics.reduce((acc, s) => acc + s.concept_count, 0), + market_avg_change: 2.34, + update_time: new Date().toISOString() + }); + }), + + // 获取指定层级的概念列表 + http.get('/concept-api/hierarchy/:lv1Id', async ({ params, request }) => { + await delay(300); + + const { lv1Id } = params; + const url = new URL(request.url); + const lv2Id = url.searchParams.get('lv2_id'); + + console.log('[Mock Concept] 获取层级概念列表:', { lv1Id, lv2Id }); + + // 返回该层级下的概念列表 + let concepts = generatePopularConcepts(20); + + // 添加层级信息 + concepts = concepts.map(c => ({ + ...c, + hierarchy: { + lv1: '人工智能', + lv1_id: lv1Id, + lv2: lv2Id ? 'AI基础设施' : null, + lv2_id: lv2Id + } + })); + + return HttpResponse.json({ + concepts, + total: concepts.length, + lv1_id: lv1Id, + lv2_id: lv2Id + }); }) ]; diff --git a/src/views/Concept/components/BreadcrumbNav.js b/src/views/Concept/components/BreadcrumbNav.js new file mode 100644 index 00000000..a8e1acf2 --- /dev/null +++ b/src/views/Concept/components/BreadcrumbNav.js @@ -0,0 +1,170 @@ +/** + * BreadcrumbNav - 层级筛选面包屑导航 + * + * 功能: + * 1. 显示当前选中的层级路径 + * 2. 支持点击返回上级或清除筛选 + */ +import React from 'react'; +import { + Box, + HStack, + Text, + Icon, + Badge, + IconButton, + Flex, + Tooltip, +} from '@chakra-ui/react'; +import { ChevronRightIcon, CloseIcon } from '@chakra-ui/icons'; +import { FaLayerGroup, FaFilter, FaTimes, FaHome } from 'react-icons/fa'; + +const BreadcrumbNav = ({ + filter, + onClearFilter, + onNavigate, +}) => { + // 如果没有筛选条件,不显示 + if (!filter || (!filter.lv1 && !filter.lv2 && !filter.lv3)) { + return null; + } + + const breadcrumbs = []; + + // 构建面包屑路径 + if (filter.lv1) { + breadcrumbs.push({ + label: filter.lv1, + level: 'lv1', + onClick: () => onNavigate({ lv1: filter.lv1, lv2: null, lv3: null }), + }); + } + + if (filter.lv2) { + breadcrumbs.push({ + label: filter.lv2, + level: 'lv2', + onClick: () => onNavigate({ lv1: filter.lv1, lv2: filter.lv2, lv3: null }), + }); + } + + if (filter.lv3) { + breadcrumbs.push({ + label: filter.lv3, + level: 'lv3', + onClick: null, // 最后一级不可点击 + }); + } + + return ( + + + + {/* 筛选图标 */} + + + + 层级筛选 + + + + {/* 首页入口 */} + + + + 全部 + + + + {/* 面包屑路径 */} + {breadcrumbs.map((crumb, index) => ( + + + + {crumb.label} + + + ))} + + + {/* 清除筛选按钮 */} + + } + colorScheme="red" + variant="ghost" + borderRadius="full" + onClick={onClearFilter} + aria-label="清除筛选" + /> + + + + {/* 筛选提示 */} + + 当前显示「{breadcrumbs.map(b => b.label).join(' > ')}」分类下的概念, + + 点击清除筛选 + + + + ); +}; + +export default BreadcrumbNav; diff --git a/src/views/Concept/components/HierarchyView.js b/src/views/Concept/components/HierarchyView.js new file mode 100644 index 00000000..511be394 --- /dev/null +++ b/src/views/Concept/components/HierarchyView.js @@ -0,0 +1,709 @@ +/** + * HierarchyView - 概念层级思维导图视图 + * + * 功能: + * 1. 思维导图式展示概念层级结构(lv1 → lv2 → lv3 → concepts) + * 2. 显示各层级的涨跌幅数据和概念数量 + * 3. 点击分类后切换到列表视图显示该分类下的概念 + */ +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { + Box, + VStack, + HStack, + Text, + Badge, + Icon, + Spinner, + Center, + Flex, + Collapse, + useBreakpointValue, + Tooltip, + Tag, + TagLabel, + Wrap, + WrapItem, +} from '@chakra-ui/react'; +import { + ChevronRightIcon, + ChevronDownIcon, +} from '@chakra-ui/icons'; +import { + FaLayerGroup, + FaArrowUp, + FaArrowDown, + FaTags, + FaChartLine, + FaBrain, + FaMicrochip, + FaRobot, + FaMobileAlt, + FaCar, + FaBolt, + FaPlane, + FaShieldAlt, + FaLandmark, + FaFlask, + FaShoppingCart, + FaCoins, + FaGlobe, + FaHeartbeat, + FaAtom, +} from 'react-icons/fa'; +import { keyframes } from '@emotion/react'; +import { logger } from '../../../utils/logger'; + +// 脉冲动画 +const pulseAnimation = keyframes` + 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.4); } + 70% { transform: scale(1.02); box-shadow: 0 0 0 10px rgba(139, 92, 246, 0); } + 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(139, 92, 246, 0); } +`; + +// 连接线动画 +const flowAnimation = keyframes` + 0% { stroke-dashoffset: 20; } + 100% { stroke-dashoffset: 0; } +`; + +// 一级分类图标映射 +const LV1_ICONS = { + '人工智能': FaBrain, + '半导体': FaMicrochip, + '机器人': FaRobot, + '消费电子': FaMobileAlt, + '智能驾驶与汽车': FaCar, + '新能源与电力': FaBolt, + '空天经济': FaPlane, + '国防军工': FaShieldAlt, + '政策与主题': FaLandmark, + '周期与材料': FaFlask, + '大消费': FaShoppingCart, + '数字经济与金融科技': FaCoins, + '全球宏观与贸易': FaGlobe, + '医药健康': FaHeartbeat, + '前沿科技': FaAtom, +}; + +// 一级分类颜色映射 +const LV1_COLORS = { + '人工智能': { bg: 'purple', gradient: 'linear(135deg, #667eea 0%, #764ba2 100%)' }, + '半导体': { bg: 'blue', gradient: 'linear(135deg, #4facfe 0%, #00f2fe 100%)' }, + '机器人': { bg: 'cyan', gradient: 'linear(135deg, #43e97b 0%, #38f9d7 100%)' }, + '消费电子': { bg: 'pink', gradient: 'linear(135deg, #fa709a 0%, #fee140 100%)' }, + '智能驾驶与汽车': { bg: 'orange', gradient: 'linear(135deg, #f093fb 0%, #f5576c 100%)' }, + '新能源与电力': { bg: 'green', gradient: 'linear(135deg, #11998e 0%, #38ef7d 100%)' }, + '空天经济': { bg: 'teal', gradient: 'linear(135deg, #667eea 0%, #764ba2 100%)' }, + '国防军工': { bg: 'red', gradient: 'linear(135deg, #eb3349 0%, #f45c43 100%)' }, + '政策与主题': { bg: 'yellow', gradient: 'linear(135deg, #f6d365 0%, #fda085 100%)' }, + '周期与材料': { bg: 'gray', gradient: 'linear(135deg, #bdc3c7 0%, #2c3e50 100%)' }, + '大消费': { bg: 'pink', gradient: 'linear(135deg, #ff758c 0%, #ff7eb3 100%)' }, + '数字经济与金融科技': { bg: 'blue', gradient: 'linear(135deg, #4776e6 0%, #8e54e9 100%)' }, + '全球宏观与贸易': { bg: 'teal', gradient: 'linear(135deg, #00cdac 0%, #8ddad5 100%)' }, + '医药健康': { bg: 'green', gradient: 'linear(135deg, #56ab2f 0%, #a8e063 100%)' }, + '前沿科技': { bg: 'purple', gradient: 'linear(135deg, #a18cd1 0%, #fbc2eb 100%)' }, +}; + +// 获取涨跌幅颜色 +const getChangeColor = (value) => { + if (value === null || value === undefined) return 'gray'; + return value > 0 ? 'red' : value < 0 ? 'green' : 'gray'; +}; + +// 格式化涨跌幅 +const formatChangePercent = (value) => { + if (value === null || value === undefined) return null; + const formatted = value.toFixed(2); + return value > 0 ? `+${formatted}%` : `${formatted}%`; +}; + +/** + * 一级分类卡片组件 + */ +const Lv1Card = ({ + item, + isExpanded, + onToggle, + onSelectCategory, + stats +}) => { + const IconComponent = LV1_ICONS[item.name] || FaLayerGroup; + const colorConfig = LV1_COLORS[item.name] || { bg: 'purple', gradient: 'linear(135deg, #667eea 0%, #764ba2 100%)' }; + const isMobile = useBreakpointValue({ base: true, md: false }); + + // 从统计数据中获取涨跌幅 + const avgChange = stats?.avg_change_pct; + const changeColor = getChangeColor(avgChange); + + return ( + + + {/* 背景装饰 */} + + + + + + {item.name} + + + + + {item.concept_count} 概念 + + + {avgChange !== null && avgChange !== undefined && ( + + 0 ? FaArrowUp : FaArrowDown} + boxSize={2} + /> + {formatChangePercent(avgChange)} + + )} + + + + + + + ); +}; + +/** + * 二级分类卡片组件 + */ +const Lv2Card = ({ + item, + parentName, + isExpanded, + onToggle, + onSelectCategory, + stats, + colorConfig +}) => { + const hasChildren = item.children && item.children.length > 0; + const avgChange = stats?.avg_change_pct; + const changeColor = getChangeColor(avgChange); + + return ( + + hasChildren ? onToggle() : onSelectCategory(parentName, item.name, null)} + transition="all 0.2s" + _hover={{ + borderColor: `${colorConfig.bg}.400`, + transform: 'translateX(4px)', + boxShadow: '0 6px 25px rgba(0, 0, 0, 0.12)', + }} + > + + + + + + {item.name} + + + + {item.concept_count} 概念 + + {avgChange !== null && avgChange !== undefined && ( + + {formatChangePercent(avgChange)} + + )} + + + + + {hasChildren ? ( + + ) : ( + + )} + + + + {/* 三级分类展开 */} + {hasChildren && ( + + + {item.children.map((lv3Item) => ( + + ))} + + + )} + + ); +}; + +/** + * 三级分类卡片组件 + */ +const Lv3Card = ({ + item, + parentLv1, + parentLv2, + onSelectCategory, + colorConfig +}) => { + return ( + onSelectCategory(parentLv1, parentLv2, item.name)} + transition="all 0.2s" + _hover={{ + bg: `${colorConfig.bg}.100`, + transform: 'translateX(4px)', + }} + > + + + + + {item.name} + + + + {item.concept_count} + + + + {/* 概念标签预览 */} + {item.concepts && item.concepts.length > 0 && ( + + {item.concepts.slice(0, 5).map((concept, idx) => ( + + + {concept} + + + ))} + {item.concepts.length > 5 && ( + + + +{item.concepts.length - 5} + + + )} + + )} + + ); +}; + +/** + * 思维导图连接线 SVG + */ +const ConnectionLine = ({ from, to, isActive }) => { + return ( + + + + ); +}; + +/** + * 主组件:层级视图 + */ +const HierarchyView = ({ + apiBaseUrl, + onSelectCategory, + selectedDate, +}) => { + const [hierarchy, setHierarchy] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [expandedLv1, setExpandedLv1] = useState(null); + const [expandedLv2, setExpandedLv2] = useState({}); + const [hierarchyStats, setHierarchyStats] = useState(null); + + const isMobile = useBreakpointValue({ base: true, md: false }); + + // 获取层级结构数据 + const fetchHierarchy = useCallback(async () => { + setLoading(true); + setError(null); + + try { + const response = await fetch(`${apiBaseUrl}/hierarchy`); + if (!response.ok) throw new Error('获取层级结构失败'); + + const data = await response.json(); + setHierarchy(data.hierarchy || []); + + logger.info('HierarchyView', '层级结构加载完成', { + totalLv1: data.hierarchy?.length, + totalConcepts: data.total_concepts + }); + } catch (err) { + logger.error('HierarchyView', 'fetchHierarchy', err); + setError(err.message); + } finally { + setLoading(false); + } + }, [apiBaseUrl]); + + // 获取层级统计数据(包含涨跌幅) + const fetchHierarchyStats = useCallback(async () => { + try { + const response = await fetch(`${apiBaseUrl}/statistics/hierarchy`); + if (!response.ok) return; + + const data = await response.json(); + setHierarchyStats(data); + + logger.info('HierarchyView', '层级统计加载完成', { + totalLv1: data.total_lv1, + totalConcepts: data.total_concepts + }); + } catch (err) { + logger.warn('HierarchyView', '获取层级统计失败', { error: err.message }); + } + }, [apiBaseUrl]); + + useEffect(() => { + fetchHierarchy(); + fetchHierarchyStats(); + }, [fetchHierarchy, fetchHierarchyStats]); + + // 获取某个 lv1 的统计数据 + const getLv1Stats = useCallback((lv1Name) => { + if (!hierarchyStats?.statistics) return null; + return hierarchyStats.statistics.find(s => s.lv1 === lv1Name); + }, [hierarchyStats]); + + // 切换一级分类展开状态 + const toggleLv1 = useCallback((lv1Id) => { + setExpandedLv1(prev => prev === lv1Id ? null : lv1Id); + setExpandedLv2({}); + }, []); + + // 切换二级分类展开状态 + const toggleLv2 = useCallback((lv2Id) => { + setExpandedLv2(prev => ({ + ...prev, + [lv2Id]: !prev[lv2Id] + })); + }, []); + + // 处理分类选择 + const handleSelectCategory = useCallback((lv1, lv2, lv3) => { + logger.info('HierarchyView', '选择分类', { lv1, lv2, lv3 }); + onSelectCategory && onSelectCategory({ lv1, lv2, lv3 }); + }, [onSelectCategory]); + + if (loading) { + return ( +
+ + + 正在加载概念层级... + +
+ ); + } + + if (error) { + return ( +
+ + + 加载失败:{error} + +
+ ); + } + + return ( + + {/* 标题 */} + + + + + 概念层级导航 + + + + 点击分类展开查看,点击具体类目筛选概念列表 + + + + {/* 思维导图布局 */} + + {/* 一级分类网格 - 居中展示 */} + + {hierarchy.map((lv1Item) => ( + toggleLv1(lv1Item.id)} + onSelectCategory={handleSelectCategory} + stats={getLv1Stats(lv1Item.name)} + /> + ))} + + + {/* 展开的二级分类 */} + {expandedLv1 && ( + + {(() => { + const lv1Item = hierarchy.find(h => h.id === expandedLv1); + if (!lv1Item) return null; + + const colorConfig = LV1_COLORS[lv1Item.name] || { bg: 'purple', gradient: 'linear(135deg, #667eea 0%, #764ba2 100%)' }; + + return ( + + {/* 展开分类的标题 */} + + + + {lv1Item.name} + + + {lv1Item.children?.length || 0} 个子分类 + + + {/* 点击筛选该一级分类 */} + handleSelectCategory(lv1Item.name, null, null)} + _hover={{ opacity: 0.8 }} + > + 筛选全部 → + + + + {/* 二级分类列表 */} + + {lv1Item.children?.map((lv2Item) => ( + + toggleLv2(lv2Item.id)} + onSelectCategory={handleSelectCategory} + colorConfig={colorConfig} + /> + + ))} + + + ); + })()} + + )} + + + {/* 统计信息 */} + + + 共 {hierarchy.length} 个一级分类 + + + {hierarchy.reduce((acc, h) => acc + (h.children?.length || 0), 0)} 个二级分类 + + + {hierarchy.reduce((acc, h) => acc + h.concept_count, 0)} 个概念 + + + + ); +}; + +export default HierarchyView; diff --git a/src/views/Concept/index.js b/src/views/Concept/index.js index ad159e46..eb8d2c2f 100644 --- a/src/views/Concept/index.js +++ b/src/views/Concept/index.js @@ -81,11 +81,13 @@ import { useBreakpointValue, } from '@chakra-ui/react'; import { SearchIcon, ViewIcon, CalendarIcon, ExternalLinkIcon, StarIcon, ChevronDownIcon, InfoIcon, CloseIcon, ChevronRightIcon } from '@chakra-ui/icons'; -import { FaThLarge, FaList, FaTags, FaChartLine, FaRobot, FaTable, FaHistory, FaBrain, FaLightbulb, FaRocket, FaShieldAlt, FaCalendarAlt, FaArrowUp, FaArrowDown, FaNewspaper, FaFileAlt, FaExpand, FaCompress, FaClock, FaLock } from 'react-icons/fa'; +import { FaThLarge, FaList, FaTags, FaChartLine, FaRobot, FaTable, FaHistory, FaBrain, FaLightbulb, FaRocket, FaShieldAlt, FaCalendarAlt, FaArrowUp, FaArrowDown, FaNewspaper, FaFileAlt, FaExpand, FaCompress, FaClock, FaLock, FaSitemap, FaLayerGroup } from 'react-icons/fa'; import { BsGraphUp, BsLightningFill } from 'react-icons/bs'; import { keyframes } from '@emotion/react'; import ConceptTimelineModal from './ConceptTimelineModal'; import ConceptStatsPanel from './components/ConceptStatsPanel'; +import HierarchyView from './components/HierarchyView'; +import BreadcrumbNav from './components/BreadcrumbNav'; import ConceptStocksModal from '@components/ConceptStocksModal'; import TradeDatePicker from '@components/TradeDatePicker'; // 导航栏已由 MainLayout 提供,无需在此导入 @@ -161,7 +163,10 @@ const ConceptCenter = () => { const [currentPage, setCurrentPage] = useState(1); const [totalConcepts, setTotalConcepts] = useState(0); const [totalPages, setTotalPages] = useState(1); - const [viewMode, setViewMode] = useState('grid'); + const [viewMode, setViewMode] = useState('list'); // 默认列表视图 + + // 层级筛选状态 + const [hierarchyFilter, setHierarchyFilter] = useState({ lv1: null, lv2: null, lv3: null }); // 日期相关状态 const [selectedDate, setSelectedDate] = useState(null); @@ -253,7 +258,11 @@ const ConceptCenter = () => { sort: searchParams.get('sort') || defaultSort, page: parseInt(searchParams.get('page') || '1', 10), date: searchParams.get('date') || null, - size: 12 + size: 12, + // 层级筛选参数 + lv1: searchParams.get('lv1') || null, + lv2: searchParams.get('lv2') || null, + lv3: searchParams.get('lv3') || null, }; }, [searchParams]); @@ -271,7 +280,7 @@ const ConceptCenter = () => { }, [searchParams, setSearchParams]); // 获取概念数据 - const fetchConcepts = useCallback(async (query = '', page = 1, date = selectedDate, customSortBy = null) => { + const fetchConcepts = useCallback(async (query = '', page = 1, date = selectedDate, customSortBy = null, filter = hierarchyFilter) => { setLoading(true); try { const sortToUse = customSortBy !== null ? customSortBy : sortBy; @@ -287,6 +296,14 @@ const ConceptCenter = () => { requestBody.trade_date = date.toISOString().split('T')[0]; } + // 添加层级筛选参数 + if (filter?.lv1) { + requestBody.filter_lv1 = filter.lv1; + } + if (filter?.lv2) { + requestBody.filter_lv2 = filter.lv2; + } + const response = await fetch(`${API_BASE_URL}/search`, { method: 'POST', headers: { @@ -308,24 +325,75 @@ const ConceptCenter = () => { setSelectedDate(new Date(data.price_date)); } } catch (error) { - logger.error('ConceptCenter', 'fetchConcepts', error, { query, page, date: date?.toISOString(), sortToUse }); + logger.error('ConceptCenter', 'fetchConcepts', error, { query, page, date: date?.toISOString(), sortToUse, filter }); // ❌ 移除获取数据失败toast // toast({ title: '获取数据失败', description: error.message, status: 'error', duration: 3000, isClosable: true }); } finally { setLoading(false); } - }, [pageSize, sortBy]); + }, [pageSize, sortBy, hierarchyFilter]); // 清除搜索 const handleClearSearch = () => { setSearchQuery(''); setSortBy('change_pct'); setCurrentPage(1); - updateUrlParams({ q: '', page: 1, sort: 'change_pct' }); - fetchConcepts('', 1, selectedDate, 'change_pct'); + setHierarchyFilter({ lv1: null, lv2: null, lv3: null }); + updateUrlParams({ q: '', page: 1, sort: 'change_pct', lv1: '', lv2: '', lv3: '' }); + fetchConcepts('', 1, selectedDate, 'change_pct', { lv1: null, lv2: null, lv3: null }); }; + // 处理层级筛选选择(从 HierarchyView 点击分类) + const handleHierarchySelect = useCallback((filter) => { + logger.info('ConceptCenter', '层级筛选选择', filter); + + setHierarchyFilter(filter); + setCurrentPage(1); + setViewMode('list'); // 切换到列表视图 + + // 更新 URL 参数 + updateUrlParams({ + lv1: filter.lv1 || '', + lv2: filter.lv2 || '', + lv3: filter.lv3 || '', + page: 1 + }); + + // 重新获取数据 + fetchConcepts(searchQuery, 1, selectedDate, sortBy, filter); + + // 显示提示 + toast({ + title: '已应用筛选', + description: `正在显示「${[filter.lv1, filter.lv2, filter.lv3].filter(Boolean).join(' > ')}」分类下的概念`, + status: 'info', + duration: 2000, + isClosable: true, + }); + }, [searchQuery, selectedDate, sortBy, updateUrlParams, fetchConcepts, toast]); + + // 清除层级筛选 + const handleClearHierarchyFilter = useCallback(() => { + setHierarchyFilter({ lv1: null, lv2: null, lv3: null }); + setCurrentPage(1); + updateUrlParams({ lv1: '', lv2: '', lv3: '', page: 1 }); + fetchConcepts(searchQuery, 1, selectedDate, sortBy, { lv1: null, lv2: null, lv3: null }); + }, [searchQuery, selectedDate, sortBy, updateUrlParams, fetchConcepts]); + + // 导航到特定层级 + const handleNavigateHierarchy = useCallback((filter) => { + setHierarchyFilter(filter); + setCurrentPage(1); + updateUrlParams({ + lv1: filter.lv1 || '', + lv2: filter.lv2 || '', + lv3: filter.lv3 || '', + page: 1 + }); + fetchConcepts(searchQuery, 1, selectedDate, sortBy, filter); + }, [searchQuery, selectedDate, sortBy, updateUrlParams, fetchConcepts]); + // 处理搜索 const handleSearch = () => { setCurrentPage(1); @@ -556,12 +624,20 @@ const ConceptCenter = () => { setSortBy(filters.sort); setCurrentPage(filters.page); + // 恢复层级筛选状态 + const hierarchyFilterFromUrl = { + lv1: filters.lv1, + lv2: filters.lv2, + lv3: filters.lv3, + }; + setHierarchyFilter(hierarchyFilterFromUrl); + const dateToUse = filters.date ? new Date(filters.date) : latestDate; if (dateToUse) { setSelectedDate(dateToUse); - fetchConcepts(filters.q, filters.page, dateToUse, filters.sort); + fetchConcepts(filters.q, filters.page, dateToUse, filters.sort, hierarchyFilterFromUrl); } else { - fetchConcepts(filters.q, filters.page, null, filters.sort); + fetchConcepts(filters.q, filters.page, null, filters.sort, hierarchyFilterFromUrl); } }; init(); @@ -1431,46 +1507,76 @@ const ConceptCenter = () => { - } - onClick={() => { - if (viewMode !== 'grid') { - trackViewModeChanged('grid', viewMode); - setViewMode('grid'); - } - }} - bg={viewMode === 'grid' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'transparent'} - color={viewMode === 'grid' ? 'white' : 'purple.500'} - borderColor="purple.500" - _hover={{ - bg: viewMode === 'grid' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'purple.50', - boxShadow: viewMode === 'grid' ? '0 0 10px rgba(139, 92, 246, 0.3)' : 'none', - }} - aria-label="网格视图" - /> - } - onClick={() => { - if (viewMode !== 'list') { - trackViewModeChanged('list', viewMode); - setViewMode('list'); - } - }} - bg={viewMode === 'list' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'transparent'} - color={viewMode === 'list' ? 'white' : 'purple.500'} - borderColor="purple.500" - _hover={{ - bg: viewMode === 'list' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'purple.50', - boxShadow: viewMode === 'list' ? '0 0 10px rgba(139, 92, 246, 0.3)' : 'none', - }} - aria-label="列表视图" - /> + + } + onClick={() => { + if (viewMode !== 'hierarchy') { + trackViewModeChanged('hierarchy', viewMode); + setViewMode('hierarchy'); + } + }} + bg={viewMode === 'hierarchy' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'transparent'} + color={viewMode === 'hierarchy' ? 'white' : 'purple.500'} + borderColor="purple.500" + _hover={{ + bg: viewMode === 'hierarchy' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'purple.50', + boxShadow: viewMode === 'hierarchy' ? '0 0 10px rgba(139, 92, 246, 0.3)' : 'none', + }} + aria-label="层级图" + /> + + + } + onClick={() => { + if (viewMode !== 'grid') { + trackViewModeChanged('grid', viewMode); + setViewMode('grid'); + } + }} + bg={viewMode === 'grid' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'transparent'} + color={viewMode === 'grid' ? 'white' : 'purple.500'} + borderColor="purple.500" + _hover={{ + bg: viewMode === 'grid' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'purple.50', + boxShadow: viewMode === 'grid' ? '0 0 10px rgba(139, 92, 246, 0.3)' : 'none', + }} + aria-label="网格视图" + /> + + + } + onClick={() => { + if (viewMode !== 'list') { + trackViewModeChanged('list', viewMode); + setViewMode('list'); + } + }} + bg={viewMode === 'list' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'transparent'} + color={viewMode === 'list' ? 'white' : 'purple.500'} + borderColor="purple.500" + _hover={{ + bg: viewMode === 'list' ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'purple.50', + boxShadow: viewMode === 'list' ? '0 0 10px rgba(139, 92, 246, 0.3)' : 'none', + }} + aria-label="列表视图" + /> + - {selectedDate && ( + {/* 面包屑导航 - 显示当前层级筛选 */} + + + {selectedDate && viewMode !== 'hierarchy' && ( @@ -1482,7 +1588,14 @@ const ConceptCenter = () => { )} - {loading ? ( + {/* 层级图视图 */} + {viewMode === 'hierarchy' ? ( + + ) : loading ? ( {[...Array(12)].map((_, i) => ( @@ -1590,17 +1703,32 @@ const ConceptCenter = () => { - ) : ( + ) : viewMode !== 'hierarchy' ? (
暂无概念数据 - 请尝试其他搜索关键词或选择其他日期 + + {hierarchyFilter?.lv1 + ? `「${[hierarchyFilter.lv1, hierarchyFilter.lv2, hierarchyFilter.lv3].filter(Boolean).join(' > ')}」分类下暂无数据` + : '请尝试其他搜索关键词或选择其他日期' + } + + {hierarchyFilter?.lv1 && ( + + )}
- )} + ) : null}
{/* 右侧统计面板 */}