update pay ui
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -60,3 +60,6 @@ src/assets/img/original-backup/
|
|||||||
|
|
||||||
# 涨停分析静态数据(由 export_zt_data.py 生成,不提交到 Git)
|
# 涨停分析静态数据(由 export_zt_data.py 生成,不提交到 Git)
|
||||||
public/data/zt/
|
public/data/zt/
|
||||||
|
|
||||||
|
# 概念涨跌幅静态数据(由 export_concept_data.py 生成,不提交到 Git)
|
||||||
|
public/data/concept/
|
||||||
|
|||||||
189
export_concept_data.py
Normal file
189
export_concept_data.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
概念涨跌幅数据导出脚本
|
||||||
|
从 MySQL 导出最新的热门概念数据到静态 JSON 文件
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
python export_concept_data.py # 导出最新数据
|
||||||
|
python export_concept_data.py --limit 100 # 限制导出数量
|
||||||
|
|
||||||
|
输出:public/data/concept/latest.json
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
import pymysql
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
MYSQL_CONFIG = {
|
||||||
|
'host': '192.168.1.5',
|
||||||
|
'port': 3306,
|
||||||
|
'user': 'root',
|
||||||
|
'password': 'Zzl5588161!',
|
||||||
|
'db': 'stock',
|
||||||
|
'charset': 'utf8mb4',
|
||||||
|
}
|
||||||
|
|
||||||
|
# 输出文件路径
|
||||||
|
OUTPUT_FILE = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
'public', 'data', 'concept', 'latest.json'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 层级结构文件
|
||||||
|
HIERARCHY_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'concept_hierarchy_v3.json')
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 层级映射缓存
|
||||||
|
concept_to_hierarchy = {}
|
||||||
|
|
||||||
|
|
||||||
|
def load_hierarchy():
|
||||||
|
"""加载层级结构"""
|
||||||
|
global concept_to_hierarchy
|
||||||
|
|
||||||
|
if not os.path.exists(HIERARCHY_FILE):
|
||||||
|
logger.warning(f"层级文件不存在: {HIERARCHY_FILE}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(HIERARCHY_FILE, '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', '')
|
||||||
|
|
||||||
|
if 'children' in child:
|
||||||
|
for lv3_child in child.get('children', []):
|
||||||
|
lv3_name = lv3_child.get('lv3', '')
|
||||||
|
lv3_id = lv3_child.get('lv3_id', '')
|
||||||
|
|
||||||
|
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:
|
||||||
|
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_connection():
|
||||||
|
"""获取数据库连接"""
|
||||||
|
return pymysql.connect(**MYSQL_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
|
def export_latest(limit=100):
|
||||||
|
"""导出最新的热门概念数据"""
|
||||||
|
conn = get_connection()
|
||||||
|
try:
|
||||||
|
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||||
|
# 获取最新交易日期
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT MAX(trade_date) as max_date
|
||||||
|
FROM concept_daily_stats
|
||||||
|
WHERE concept_type = 'leaf'
|
||||||
|
""")
|
||||||
|
result = cursor.fetchone()
|
||||||
|
if not result or not result['max_date']:
|
||||||
|
logger.error("无可用数据")
|
||||||
|
return None
|
||||||
|
|
||||||
|
trade_date = result['max_date']
|
||||||
|
logger.info(f"最新交易日期: {trade_date}")
|
||||||
|
|
||||||
|
# 按涨跌幅降序获取概念列表
|
||||||
|
cursor.execute("""
|
||||||
|
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 = 'leaf'
|
||||||
|
ORDER BY avg_change_pct DESC
|
||||||
|
LIMIT %s
|
||||||
|
""", (trade_date, limit))
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
concepts = []
|
||||||
|
for row in rows:
|
||||||
|
concept_name = row['concept_name']
|
||||||
|
hierarchy = concept_to_hierarchy.get(concept_name)
|
||||||
|
|
||||||
|
concepts.append({
|
||||||
|
'concept_id': row['concept_id'],
|
||||||
|
'concept': concept_name,
|
||||||
|
'price_info': {
|
||||||
|
'trade_date': row['trade_date'].strftime('%Y-%m-%d'),
|
||||||
|
'avg_change_pct': float(row['avg_change_pct']) if row['avg_change_pct'] else None
|
||||||
|
},
|
||||||
|
'stock_count': row['stock_count'],
|
||||||
|
'hierarchy': hierarchy
|
||||||
|
})
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'trade_date': trade_date.strftime('%Y-%m-%d'),
|
||||||
|
'total': len(concepts),
|
||||||
|
'results': concepts,
|
||||||
|
'updated_at': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# 确保目录存在
|
||||||
|
os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
|
||||||
|
|
||||||
|
# 保存文件
|
||||||
|
with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
logger.info(f"已保存: {OUTPUT_FILE} ({len(concepts)} 个概念)")
|
||||||
|
return data
|
||||||
|
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='导出热门概念涨跌幅数据')
|
||||||
|
parser.add_argument('--limit', type=int, default=100, help='导出的概念数量限制')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
load_hierarchy()
|
||||||
|
export_latest(args.limit)
|
||||||
|
logger.info("导出完成!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
116
src/services/conceptStaticService.js
Normal file
116
src/services/conceptStaticService.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* 概念涨跌幅静态数据服务
|
||||||
|
* 从 /data/concept/ 目录读取预生成的 JSON 文件
|
||||||
|
* 不依赖后端 API,适合静态部署
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 数据基础路径
|
||||||
|
const DATA_BASE_URL = '/data/concept';
|
||||||
|
|
||||||
|
// 内存缓存
|
||||||
|
const cache = {
|
||||||
|
latest: null,
|
||||||
|
dates: null,
|
||||||
|
daily: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最新的热门概念数据
|
||||||
|
* 这是 HeroPanel 滚动窗口的主要数据源
|
||||||
|
*/
|
||||||
|
export const fetchPopularConcepts = async () => {
|
||||||
|
try {
|
||||||
|
// 使用缓存
|
||||||
|
if (cache.latest) {
|
||||||
|
return { success: true, data: cache.latest, from_cache: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${DATA_BASE_URL}/latest.json`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// 缓存结果
|
||||||
|
cache.latest = data;
|
||||||
|
|
||||||
|
return { success: true, data, from_cache: false };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[conceptStaticService] fetchPopularConcepts error:', error);
|
||||||
|
return { success: false, error: error.message, data: null };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可用日期列表
|
||||||
|
*/
|
||||||
|
export const fetchAvailableDates = async () => {
|
||||||
|
try {
|
||||||
|
// 使用缓存
|
||||||
|
if (cache.dates) {
|
||||||
|
return { success: true, dates: cache.dates };
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${DATA_BASE_URL}/dates.json`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// 缓存结果
|
||||||
|
cache.dates = data.dates || [];
|
||||||
|
|
||||||
|
return { success: true, dates: cache.dates, total: data.total };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[conceptStaticService] fetchAvailableDates error:', error);
|
||||||
|
return { success: false, error: error.message, dates: [] };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定日期的概念数据
|
||||||
|
*/
|
||||||
|
export const fetchDailyConcepts = async (date) => {
|
||||||
|
try {
|
||||||
|
// 使用缓存
|
||||||
|
if (cache.daily.has(date)) {
|
||||||
|
return { success: true, data: cache.daily.get(date), from_cache: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${DATA_BASE_URL}/daily/${date}.json`);
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
return { success: false, error: `日期 ${date} 的数据不存在` };
|
||||||
|
}
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// 缓存结果
|
||||||
|
cache.daily.set(date, data);
|
||||||
|
|
||||||
|
return { success: true, data, from_cache: false };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[conceptStaticService] fetchDailyConcepts error:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除缓存
|
||||||
|
*/
|
||||||
|
export const clearCache = () => {
|
||||||
|
cache.latest = null;
|
||||||
|
cache.dates = null;
|
||||||
|
cache.daily.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetchPopularConcepts,
|
||||||
|
fetchAvailableDates,
|
||||||
|
fetchDailyConcepts,
|
||||||
|
clearCache,
|
||||||
|
};
|
||||||
@@ -30,6 +30,7 @@ import ReactECharts from 'echarts-for-react';
|
|||||||
import { logger } from '@utils/logger';
|
import { logger } from '@utils/logger';
|
||||||
import { getApiBase } from '@utils/apiConfig';
|
import { getApiBase } from '@utils/apiConfig';
|
||||||
import { useIndexQuote } from '@hooks/useIndexQuote';
|
import { useIndexQuote } from '@hooks/useIndexQuote';
|
||||||
|
import conceptStaticService from '@services/conceptStaticService';
|
||||||
|
|
||||||
// 定义动画
|
// 定义动画
|
||||||
const animations = `
|
const animations = `
|
||||||
@@ -74,18 +75,13 @@ const fetchIndexKline = async (indexCode) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取热门概念数据
|
* 获取热门概念数据(使用静态文件)
|
||||||
*/
|
*/
|
||||||
const fetchPopularConcepts = async () => {
|
const fetchPopularConcepts = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${getApiBase()}/concept-api/search`, {
|
const result = await conceptStaticService.fetchPopularConcepts();
|
||||||
method: 'POST',
|
if (result.success && result.data?.results?.length > 0) {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
return result.data.results.map(item => ({
|
||||||
body: JSON.stringify({ query: '', size: 60, page: 1, sort_by: 'change_pct' })
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.results?.length > 0) {
|
|
||||||
return data.results.map(item => ({
|
|
||||||
name: item.concept,
|
name: item.concept,
|
||||||
change_pct: item.price_info?.avg_change_pct || 0,
|
change_pct: item.price_info?.avg_change_pct || 0,
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user