update pay function
This commit is contained in:
254
app.py
254
app.py
@@ -4609,20 +4609,10 @@ def get_my_following_future_events():
|
|||||||
)
|
)
|
||||||
|
|
||||||
events = []
|
events = []
|
||||||
|
# 所有返回的事件都是已关注的
|
||||||
|
following_ids = set(future_event_ids)
|
||||||
for row in result:
|
for row in result:
|
||||||
event_data = {
|
event_data = process_future_event_row(row, following_ids)
|
||||||
'id': row.data_id,
|
|
||||||
'title': row.title,
|
|
||||||
'type': row.type,
|
|
||||||
'calendar_time': row.calendar_time.isoformat(),
|
|
||||||
'star': row.star,
|
|
||||||
'former': row.former,
|
|
||||||
'forecast': row.forecast,
|
|
||||||
'fact': row.fact,
|
|
||||||
'is_following': True, # 这些都是已关注的
|
|
||||||
'related_stocks': parse_json_field(row.related_stocks),
|
|
||||||
'concepts': parse_json_field(row.concepts)
|
|
||||||
}
|
|
||||||
events.append(event_data)
|
events.append(event_data)
|
||||||
|
|
||||||
return jsonify({'success': True, 'data': events})
|
return jsonify({'success': True, 'data': events})
|
||||||
@@ -6094,17 +6084,9 @@ def account_calendar_events():
|
|||||||
|
|
||||||
future_events = []
|
future_events = []
|
||||||
if future_event_ids:
|
if future_event_ids:
|
||||||
|
# 使用 SELECT * 以便获取所有字段(包括新字段)
|
||||||
base_sql = """
|
base_sql = """
|
||||||
SELECT data_id, \
|
SELECT *
|
||||||
title, \
|
|
||||||
type, \
|
|
||||||
calendar_time, \
|
|
||||||
star, \
|
|
||||||
former, \
|
|
||||||
forecast, \
|
|
||||||
fact, \
|
|
||||||
related_stocks, \
|
|
||||||
concepts
|
|
||||||
FROM future_events
|
FROM future_events
|
||||||
WHERE data_id IN :event_ids \
|
WHERE data_id IN :event_ids \
|
||||||
"""
|
"""
|
||||||
@@ -6122,12 +6104,24 @@ def account_calendar_events():
|
|||||||
|
|
||||||
result = db.session.execute(text(base_sql), params)
|
result = db.session.execute(text(base_sql), params)
|
||||||
for row in result:
|
for row in result:
|
||||||
# related_stocks 形如 [[code,name,reason,score], ...]
|
# 使用新字段回退逻辑获取 former
|
||||||
rs = parse_json_field(row.related_stocks)
|
former_value = get_future_event_field(row, 'second_modified_text', 'former')
|
||||||
|
|
||||||
|
# 获取 related_stocks,优先使用 best_matches
|
||||||
|
best_matches = getattr(row, 'best_matches', None) if hasattr(row, 'best_matches') else None
|
||||||
|
if best_matches and str(best_matches).strip():
|
||||||
|
rs = parse_best_matches(best_matches)
|
||||||
|
else:
|
||||||
|
rs = parse_json_field(getattr(row, 'related_stocks', None))
|
||||||
|
|
||||||
|
# 生成股票标签列表
|
||||||
stock_tags = []
|
stock_tags = []
|
||||||
try:
|
try:
|
||||||
for it in rs:
|
for it in rs:
|
||||||
if isinstance(it, (list, tuple)) and len(it) >= 2:
|
if isinstance(it, dict):
|
||||||
|
# 新结构
|
||||||
|
stock_tags.append(f"{it.get('code', '')} {it.get('name', '')}")
|
||||||
|
elif isinstance(it, (list, tuple)) and len(it) >= 2:
|
||||||
stock_tags.append(f"{it[0]} {it[1]}")
|
stock_tags.append(f"{it[0]} {it[1]}")
|
||||||
elif isinstance(it, str):
|
elif isinstance(it, str):
|
||||||
stock_tags.append(it)
|
stock_tags.append(it)
|
||||||
@@ -6140,7 +6134,7 @@ def account_calendar_events():
|
|||||||
'event_date': (row.calendar_time.date().isoformat() if row.calendar_time else None),
|
'event_date': (row.calendar_time.date().isoformat() if row.calendar_time else None),
|
||||||
'type': 'future_event',
|
'type': 'future_event',
|
||||||
'importance': int(row.star) if getattr(row, 'star', None) is not None else 3,
|
'importance': int(row.star) if getattr(row, 'star', None) is not None else 3,
|
||||||
'description': row.former or '',
|
'description': former_value or '',
|
||||||
'stocks': stock_tags,
|
'stocks': stock_tags,
|
||||||
'is_following': True,
|
'is_following': True,
|
||||||
'source': 'future'
|
'source': 'future'
|
||||||
@@ -7548,47 +7542,8 @@ def get_calendar_events():
|
|||||||
user_following_ids = {f.future_event_id for f in follows}
|
user_following_ids = {f.future_event_id for f in follows}
|
||||||
|
|
||||||
for row in result:
|
for row in result:
|
||||||
event_data = {
|
# 使用统一的处理函数,支持新字段回退和 best_matches 解析
|
||||||
'id': row.data_id,
|
event_data = process_future_event_row(row, user_following_ids)
|
||||||
'title': row.title,
|
|
||||||
'type': row.type,
|
|
||||||
'calendar_time': row.calendar_time.isoformat(),
|
|
||||||
'star': row.star,
|
|
||||||
'former': row.former,
|
|
||||||
'forecast': row.forecast,
|
|
||||||
'fact': row.fact,
|
|
||||||
'is_following': row.data_id in user_following_ids
|
|
||||||
}
|
|
||||||
|
|
||||||
# 解析相关股票和概念
|
|
||||||
if row.related_stocks:
|
|
||||||
try:
|
|
||||||
if isinstance(row.related_stocks, str):
|
|
||||||
if row.related_stocks.startswith('['):
|
|
||||||
event_data['related_stocks'] = json.loads(row.related_stocks)
|
|
||||||
else:
|
|
||||||
event_data['related_stocks'] = row.related_stocks.split(',')
|
|
||||||
else:
|
|
||||||
event_data['related_stocks'] = row.related_stocks
|
|
||||||
except:
|
|
||||||
event_data['related_stocks'] = []
|
|
||||||
else:
|
|
||||||
event_data['related_stocks'] = []
|
|
||||||
|
|
||||||
if row.concepts:
|
|
||||||
try:
|
|
||||||
if isinstance(row.concepts, str):
|
|
||||||
if row.concepts.startswith('['):
|
|
||||||
event_data['concepts'] = json.loads(row.concepts)
|
|
||||||
else:
|
|
||||||
event_data['concepts'] = row.concepts.split(',')
|
|
||||||
else:
|
|
||||||
event_data['concepts'] = row.concepts
|
|
||||||
except:
|
|
||||||
event_data['concepts'] = []
|
|
||||||
else:
|
|
||||||
event_data['concepts'] = []
|
|
||||||
|
|
||||||
events.append(event_data)
|
events.append(event_data)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -7614,28 +7569,18 @@ def get_calendar_event_detail(event_id):
|
|||||||
'error': 'Event not found'
|
'error': 'Event not found'
|
||||||
}), 404
|
}), 404
|
||||||
|
|
||||||
event_data = {
|
|
||||||
'id': result.data_id,
|
|
||||||
'title': result.title,
|
|
||||||
'type': result.type,
|
|
||||||
'calendar_time': result.calendar_time.isoformat(),
|
|
||||||
'star': result.star,
|
|
||||||
'former': result.former,
|
|
||||||
'forecast': result.forecast,
|
|
||||||
'fact': result.fact,
|
|
||||||
'related_stocks': parse_json_field(result.related_stocks),
|
|
||||||
'concepts': parse_json_field(result.concepts)
|
|
||||||
}
|
|
||||||
|
|
||||||
# 检查当前用户是否关注了该未来事件
|
# 检查当前用户是否关注了该未来事件
|
||||||
|
user_following_ids = set()
|
||||||
if 'user_id' in session:
|
if 'user_id' in session:
|
||||||
is_following = FutureEventFollow.query.filter_by(
|
is_following = FutureEventFollow.query.filter_by(
|
||||||
user_id=session['user_id'],
|
user_id=session['user_id'],
|
||||||
future_event_id=event_id
|
future_event_id=event_id
|
||||||
).first() is not None
|
).first() is not None
|
||||||
event_data['is_following'] = is_following
|
if is_following:
|
||||||
else:
|
user_following_ids.add(event_id)
|
||||||
event_data['is_following'] = False
|
|
||||||
|
# 使用统一的处理函数,支持新字段回退和 best_matches 解析
|
||||||
|
event_data = process_future_event_row(result, user_following_ids)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
@@ -7727,6 +7672,147 @@ def parse_json_field(field_value):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_future_event_field(row, new_field, old_field):
|
||||||
|
"""
|
||||||
|
获取 future_events 表字段值,支持新旧字段回退
|
||||||
|
如果新字段存在且不为空,使用新字段;否则使用旧字段
|
||||||
|
"""
|
||||||
|
new_value = getattr(row, new_field, None) if hasattr(row, new_field) else None
|
||||||
|
old_value = getattr(row, old_field, None) if hasattr(row, old_field) else None
|
||||||
|
|
||||||
|
# 如果新字段有值(不为空字符串),使用新字段
|
||||||
|
if new_value is not None and str(new_value).strip():
|
||||||
|
return new_value
|
||||||
|
return old_value
|
||||||
|
|
||||||
|
|
||||||
|
def parse_best_matches(best_matches_value):
|
||||||
|
"""
|
||||||
|
解析新的 best_matches 数据结构(含研报引用信息)
|
||||||
|
|
||||||
|
新结构示例:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"stock_code": "300451.SZ",
|
||||||
|
"company_name": "创业慧康",
|
||||||
|
"original_description": "核心标的,医疗信息化...",
|
||||||
|
"best_report_title": "报告标题",
|
||||||
|
"best_report_author": "作者",
|
||||||
|
"best_report_sentences": "相关内容",
|
||||||
|
"best_report_match_score": "好",
|
||||||
|
"best_report_match_ratio": 0.9285714285714286,
|
||||||
|
"best_report_declare_date": "2023-04-25T00:00:00",
|
||||||
|
"total_reports": 9,
|
||||||
|
"high_score_reports": 6
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
返回统一格式的股票列表,兼容旧格式
|
||||||
|
"""
|
||||||
|
if not best_matches_value:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 解析 JSON
|
||||||
|
if isinstance(best_matches_value, str):
|
||||||
|
data = json.loads(best_matches_value)
|
||||||
|
else:
|
||||||
|
data = best_matches_value
|
||||||
|
|
||||||
|
if not isinstance(data, list):
|
||||||
|
return []
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for item in data:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
# 新结构:包含研报信息的字典
|
||||||
|
stock_info = {
|
||||||
|
'code': item.get('stock_code', ''),
|
||||||
|
'name': item.get('company_name', ''),
|
||||||
|
'description': item.get('original_description', ''),
|
||||||
|
'score': item.get('best_report_match_ratio', 0),
|
||||||
|
# 研报引用信息
|
||||||
|
'report': {
|
||||||
|
'title': item.get('best_report_title', ''),
|
||||||
|
'author': item.get('best_report_author', ''),
|
||||||
|
'sentences': item.get('best_report_sentences', ''),
|
||||||
|
'match_score': item.get('best_report_match_score', ''),
|
||||||
|
'match_ratio': item.get('best_report_match_ratio', 0),
|
||||||
|
'declare_date': item.get('best_report_declare_date', ''),
|
||||||
|
'total_reports': item.get('total_reports', 0),
|
||||||
|
'high_score_reports': item.get('high_score_reports', 0)
|
||||||
|
} if item.get('best_report_title') else None
|
||||||
|
}
|
||||||
|
result.append(stock_info)
|
||||||
|
elif isinstance(item, (list, tuple)) and len(item) >= 2:
|
||||||
|
# 旧结构:[code, name, description, score]
|
||||||
|
result.append({
|
||||||
|
'code': item[0],
|
||||||
|
'name': item[1],
|
||||||
|
'description': item[2] if len(item) > 2 else '',
|
||||||
|
'score': item[3] if len(item) > 3 else 0,
|
||||||
|
'report': None
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
print(f"parse_best_matches error: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def process_future_event_row(row, user_following_ids=None):
|
||||||
|
"""
|
||||||
|
统一处理 future_events 表的行数据
|
||||||
|
支持新字段回退和 best_matches 解析
|
||||||
|
"""
|
||||||
|
if user_following_ids is None:
|
||||||
|
user_following_ids = set()
|
||||||
|
|
||||||
|
# 获取字段值,支持新旧回退
|
||||||
|
# second_modified_text -> former
|
||||||
|
# second_modified_text.1 -> forecast (MySQL 中用反引号)
|
||||||
|
former_value = get_future_event_field(row, 'second_modified_text', 'former')
|
||||||
|
|
||||||
|
# 处理 second_modified_text.1 字段(特殊字段名)
|
||||||
|
forecast_new = None
|
||||||
|
if hasattr(row, 'second_modified_text.1'):
|
||||||
|
forecast_new = getattr(row, 'second_modified_text.1', None)
|
||||||
|
# 尝试其他可能的属性名
|
||||||
|
for attr_name in ['second_modified_text.1', 'second_modified_text_1']:
|
||||||
|
if hasattr(row, attr_name):
|
||||||
|
val = getattr(row, attr_name, None)
|
||||||
|
if val and str(val).strip():
|
||||||
|
forecast_new = val
|
||||||
|
break
|
||||||
|
forecast_value = forecast_new if (forecast_new and str(forecast_new).strip()) else getattr(row, 'forecast', None)
|
||||||
|
|
||||||
|
# best_matches -> related_stocks
|
||||||
|
best_matches = getattr(row, 'best_matches', None) if hasattr(row, 'best_matches') else None
|
||||||
|
if best_matches and str(best_matches).strip():
|
||||||
|
related_stocks = parse_best_matches(best_matches)
|
||||||
|
else:
|
||||||
|
related_stocks = parse_json_field(getattr(row, 'related_stocks', None))
|
||||||
|
|
||||||
|
# 构建事件数据
|
||||||
|
event_data = {
|
||||||
|
'id': row.data_id,
|
||||||
|
'title': row.title,
|
||||||
|
'type': getattr(row, 'type', None),
|
||||||
|
'calendar_time': row.calendar_time.isoformat() if row.calendar_time else None,
|
||||||
|
'star': row.star,
|
||||||
|
'former': former_value,
|
||||||
|
'forecast': forecast_value,
|
||||||
|
'fact': getattr(row, 'fact', None),
|
||||||
|
'is_following': row.data_id in user_following_ids,
|
||||||
|
'related_stocks': related_stocks,
|
||||||
|
'concepts': parse_json_field(getattr(row, 'concepts', None)),
|
||||||
|
'update_time': getattr(row, 'update_time', None).isoformat() if getattr(row, 'update_time', None) else None
|
||||||
|
}
|
||||||
|
|
||||||
|
return event_data
|
||||||
|
|
||||||
|
|
||||||
# ==================== 行业API ====================
|
# ==================== 行业API ====================
|
||||||
@app.route('/api/classifications', methods=['GET'])
|
@app.route('/api/classifications', methods=['GET'])
|
||||||
def get_classifications():
|
def get_classifications():
|
||||||
|
|||||||
23
app_vx.py
23
app_vx.py
@@ -6407,10 +6407,31 @@ def api_method_not_allowed(error):
|
|||||||
return error
|
return error
|
||||||
|
|
||||||
|
|
||||||
|
# 应用启动时自动初始化(兼容 Gunicorn 和直接运行)
|
||||||
|
_sywg_cache_initialized = False
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_sywg_cache_initialized():
|
||||||
|
"""确保申银万国行业分类缓存已初始化(懒加载,首次请求时触发)"""
|
||||||
|
global _sywg_cache_initialized
|
||||||
|
if not _sywg_cache_initialized:
|
||||||
|
init_sywg_industry_cache()
|
||||||
|
_sywg_cache_initialized = True
|
||||||
|
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def before_request_init():
|
||||||
|
"""首次请求时初始化缓存"""
|
||||||
|
global _sywg_cache_initialized
|
||||||
|
if not _sywg_cache_initialized:
|
||||||
|
ensure_sywg_cache_initialized()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# 初始化申银万国行业分类缓存
|
# 直接运行时,立即初始化缓存
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
init_sywg_industry_cache()
|
init_sywg_industry_cache()
|
||||||
|
_sywg_cache_initialized = True
|
||||||
|
|
||||||
app.run(
|
app.run(
|
||||||
host='0.0.0.0',
|
host='0.0.0.0',
|
||||||
|
|||||||
1096
concept_api_openapi.json
Normal file
1096
concept_api_openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
39
gunicorn_config.py
Normal file
39
gunicorn_config.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Gunicorn 配置文件
|
||||||
|
|
||||||
|
# 基本配置
|
||||||
|
bind = "0.0.0.0:5002"
|
||||||
|
workers = 4
|
||||||
|
threads = 4
|
||||||
|
timeout = 120
|
||||||
|
worker_class = "gthread"
|
||||||
|
|
||||||
|
# SSL 配置(如需要)
|
||||||
|
# certfile = "/etc/letsencrypt/live/api.valuefrontier.cn/fullchain.pem"
|
||||||
|
# keyfile = "/etc/letsencrypt/live/api.valuefrontier.cn/privkey.pem"
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
loglevel = "info"
|
||||||
|
accesslog = "-"
|
||||||
|
errorlog = "-"
|
||||||
|
|
||||||
|
# 预加载应用(在 fork 前加载,加快 worker 启动)
|
||||||
|
preload_app = True
|
||||||
|
|
||||||
|
|
||||||
|
def on_starting(server):
|
||||||
|
"""主进程启动时调用"""
|
||||||
|
print("Gunicorn 主进程启动...")
|
||||||
|
|
||||||
|
|
||||||
|
def post_fork(server, worker):
|
||||||
|
"""Worker 进程 fork 后调用"""
|
||||||
|
print(f"Worker {worker.pid} 已启动")
|
||||||
|
|
||||||
|
|
||||||
|
def when_ready(server):
|
||||||
|
"""服务准备就绪时调用,初始化缓存"""
|
||||||
|
print("Gunicorn 服务准备就绪,开始初始化...")
|
||||||
|
from app_vx import app, init_sywg_industry_cache
|
||||||
|
with app.app_context():
|
||||||
|
init_sywg_industry_cache()
|
||||||
|
print("初始化完成!")
|
||||||
@@ -93,16 +93,60 @@ const InvestmentCalendar = () => {
|
|||||||
return code.split('.')[0];
|
return code.split('.')[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归一化股票数据格式
|
||||||
|
* 支持两种格式:
|
||||||
|
* 1. 旧格式数组:[code, name, description, score]
|
||||||
|
* 2. 新格式对象:{ code, name, description, score, report }
|
||||||
|
* 返回统一的对象格式
|
||||||
|
*/
|
||||||
|
const normalizeStock = (stock) => {
|
||||||
|
if (!stock) return null;
|
||||||
|
|
||||||
|
// 新格式:对象
|
||||||
|
if (typeof stock === 'object' && !Array.isArray(stock)) {
|
||||||
|
return {
|
||||||
|
code: stock.code || '',
|
||||||
|
name: stock.name || '',
|
||||||
|
description: stock.description || '',
|
||||||
|
score: stock.score || 0,
|
||||||
|
report: stock.report || null // 研报引用信息
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 旧格式:数组 [code, name, description, score]
|
||||||
|
if (Array.isArray(stock)) {
|
||||||
|
return {
|
||||||
|
code: stock[0] || '',
|
||||||
|
name: stock[1] || '',
|
||||||
|
description: stock[2] || '',
|
||||||
|
score: stock[3] || 0,
|
||||||
|
report: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归一化股票列表
|
||||||
|
*/
|
||||||
|
const normalizeStocks = (stocks) => {
|
||||||
|
if (!stocks || !Array.isArray(stocks)) return [];
|
||||||
|
return stocks.map(normalizeStock).filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
// 加载股票行情
|
// 加载股票行情
|
||||||
const loadStockQuotes = async (stocks, eventTime) => {
|
const loadStockQuotes = async (stocks, eventTime) => {
|
||||||
try {
|
try {
|
||||||
const codes = stocks.map(stock => getSixDigitCode(stock[0])); // 确保使用六位代码
|
const normalizedStocks = normalizeStocks(stocks);
|
||||||
|
const codes = normalizedStocks.map(stock => getSixDigitCode(stock.code));
|
||||||
const quotes = {};
|
const quotes = {};
|
||||||
|
|
||||||
// 使用市场API获取最新行情数据
|
// 使用市场API获取最新行情数据
|
||||||
for (let i = 0; i < codes.length; i++) {
|
for (let i = 0; i < codes.length; i++) {
|
||||||
const code = codes[i];
|
const code = codes[i];
|
||||||
const originalCode = stocks[i][0]; // 保持原始代码作为key
|
const originalCode = normalizedStocks[i].code; // 使用归一化后的代码作为key
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/market/trade/${code}?days=1`);
|
const response = await fetch(`/api/market/trade/${code}?days=1`);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -257,11 +301,13 @@ const InvestmentCalendar = () => {
|
|||||||
message.info('暂无相关股票');
|
message.info('暂无相关股票');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 按相关度排序(限降序)
|
|
||||||
const sortedStocks = [...stocks].sort((a, b) => (b[3] || 0) - (a[3] || 0));
|
// 归一化数据后按相关度排序(降序)
|
||||||
|
const normalizedList = normalizeStocks(stocks);
|
||||||
|
const sortedStocks = normalizedList.sort((a, b) => (b.score || 0) - (a.score || 0));
|
||||||
setSelectedStocks(sortedStocks);
|
setSelectedStocks(sortedStocks);
|
||||||
setStockModalVisible(true);
|
setStockModalVisible(true);
|
||||||
loadStockQuotes(sortedStocks, eventTime);
|
loadStockQuotes(stocks, eventTime); // 传原始数据给 loadStockQuotes,它内部会归一化
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加交易所后缀
|
// 添加交易所后缀
|
||||||
@@ -281,24 +327,27 @@ const InvestmentCalendar = () => {
|
|||||||
return sixDigitCode;
|
return sixDigitCode;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 显示K线图
|
// 显示K线图(支持新旧格式)
|
||||||
const showKline = (stock) => {
|
const showKline = (stock) => {
|
||||||
const stockCode = addExchangeSuffix(stock[0]);
|
// 兼容新旧格式
|
||||||
|
const code = stock.code || stock[0];
|
||||||
|
const name = stock.name || stock[1];
|
||||||
|
const stockCode = addExchangeSuffix(code);
|
||||||
|
|
||||||
// 将 selectedDate 转换为 YYYY-MM-DD 格式(日K线只需要日期,不需要时间)
|
// 将 selectedDate 转换为 YYYY-MM-DD 格式(日K线只需要日期,不需要时间)
|
||||||
const formattedEventTime = selectedDate ? selectedDate.format('YYYY-MM-DD') : null;
|
const formattedEventTime = selectedDate ? selectedDate.format('YYYY-MM-DD') : null;
|
||||||
|
|
||||||
console.log('[InvestmentCalendar] 打开K线图:', {
|
console.log('[InvestmentCalendar] 打开K线图:', {
|
||||||
originalCode: stock[0],
|
originalCode: code,
|
||||||
processedCode: stockCode,
|
processedCode: stockCode,
|
||||||
stockName: stock[1],
|
stockName: name,
|
||||||
selectedDate: selectedDate?.format('YYYY-MM-DD'),
|
selectedDate: selectedDate?.format('YYYY-MM-DD'),
|
||||||
formattedEventTime: formattedEventTime
|
formattedEventTime: formattedEventTime
|
||||||
});
|
});
|
||||||
|
|
||||||
setSelectedStock({
|
setSelectedStock({
|
||||||
stock_code: stockCode, // 添加交易所后缀
|
stock_code: stockCode, // 添加交易所后缀
|
||||||
stock_name: stock[1]
|
stock_name: name
|
||||||
});
|
});
|
||||||
setSelectedEventTime(formattedEventTime);
|
setSelectedEventTime(formattedEventTime);
|
||||||
setKlineModalVisible(true);
|
setKlineModalVisible(true);
|
||||||
@@ -330,10 +379,13 @@ const InvestmentCalendar = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加单只股票到自选
|
// 添加单只股票到自选(支持新旧格式)
|
||||||
const addSingleToWatchlist = async (stock) => {
|
const addSingleToWatchlist = async (stock) => {
|
||||||
const stockCode = getSixDigitCode(stock[0]);
|
// 兼容新旧格式
|
||||||
|
const code = stock.code || stock[0];
|
||||||
|
const name = stock.name || stock[1];
|
||||||
|
const stockCode = getSixDigitCode(code);
|
||||||
|
|
||||||
setAddingToWatchlist(prev => ({ ...prev, [stockCode]: true }));
|
setAddingToWatchlist(prev => ({ ...prev, [stockCode]: true }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -345,20 +397,20 @@ const InvestmentCalendar = () => {
|
|||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
stock_code: stockCode, // 使用六位代码
|
stock_code: stockCode, // 使用六位代码
|
||||||
stock_name: stock[1] // 股票名称
|
stock_name: name // 股票名称
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
message.success(`已将 ${stock[1]}(${stockCode}) 添加到自选`);
|
message.success(`已将 ${name}(${stockCode}) 添加到自选`);
|
||||||
} else {
|
} else {
|
||||||
message.error(data.error || '添加失败');
|
message.error(data.error || '添加失败');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('InvestmentCalendar', 'addSingleToWatchlist', error, {
|
logger.error('InvestmentCalendar', 'addSingleToWatchlist', error, {
|
||||||
stockCode,
|
stockCode,
|
||||||
stockName: stock[1]
|
stockName: name
|
||||||
});
|
});
|
||||||
message.error('添加失败,请重试');
|
message.error('添加失败,请重试');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -415,7 +467,23 @@ const InvestmentCalendar = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '未来推演',
|
||||||
|
dataIndex: 'forecast',
|
||||||
|
key: 'forecast',
|
||||||
|
width: 80,
|
||||||
|
render: (text) => (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
icon={<RobotOutlined />}
|
||||||
|
onClick={() => showContentDetail(text, '未来推演')}
|
||||||
|
disabled={!text}
|
||||||
|
>
|
||||||
|
{text ? '查看' : '无'}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: (
|
title: (
|
||||||
<span>
|
<span>
|
||||||
@@ -484,17 +552,17 @@ const InvestmentCalendar = () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// 股票表格列定义
|
// 股票表格列定义(使用归一化后的对象格式)
|
||||||
const stockColumns = [
|
const stockColumns = [
|
||||||
{
|
{
|
||||||
title: '代码',
|
title: '代码',
|
||||||
dataIndex: '0',
|
dataIndex: 'code',
|
||||||
key: 'code',
|
key: 'code',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (code) => {
|
render: (code) => {
|
||||||
const sixDigitCode = getSixDigitCode(code);
|
const sixDigitCode = getSixDigitCode(code);
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={`https://valuefrontier.cn/company?scode=${sixDigitCode}`}
|
href={`https://valuefrontier.cn/company?scode=${sixDigitCode}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -506,13 +574,13 @@ const InvestmentCalendar = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '名称',
|
title: '名称',
|
||||||
dataIndex: '1',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (name, record) => {
|
render: (name, record) => {
|
||||||
const sixDigitCode = getSixDigitCode(record[0]);
|
const sixDigitCode = getSixDigitCode(record.code);
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={`https://valuefrontier.cn/company?scode=${sixDigitCode}`}
|
href={`https://valuefrontier.cn/company?scode=${sixDigitCode}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -527,7 +595,7 @@ const InvestmentCalendar = () => {
|
|||||||
key: 'price',
|
key: 'price',
|
||||||
width: 80,
|
width: 80,
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const quote = stockQuotes[record[0]];
|
const quote = stockQuotes[record.code];
|
||||||
if (quote && quote.price !== undefined) {
|
if (quote && quote.price !== undefined) {
|
||||||
return (
|
return (
|
||||||
<Text type={quote.change > 0 ? 'danger' : 'success'}>
|
<Text type={quote.change > 0 ? 'danger' : 'success'}>
|
||||||
@@ -543,7 +611,7 @@ const InvestmentCalendar = () => {
|
|||||||
key: 'change',
|
key: 'change',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const quote = stockQuotes[record[0]];
|
const quote = stockQuotes[record.code];
|
||||||
if (quote && quote.changePercent !== undefined) {
|
if (quote && quote.changePercent !== undefined) {
|
||||||
const changePercent = quote.changePercent || 0;
|
const changePercent = quote.changePercent || 0;
|
||||||
return (
|
return (
|
||||||
@@ -557,11 +625,12 @@ const InvestmentCalendar = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '关联理由',
|
title: '关联理由',
|
||||||
dataIndex: '2',
|
dataIndex: 'description',
|
||||||
key: 'reason',
|
key: 'reason',
|
||||||
render: (reason, record) => {
|
render: (description, record) => {
|
||||||
const stockCode = record[0];
|
const stockCode = record.code;
|
||||||
const isExpanded = expandedReasons[stockCode] || false;
|
const isExpanded = expandedReasons[stockCode] || false;
|
||||||
|
const reason = description || '';
|
||||||
const shouldTruncate = reason && reason.length > 100;
|
const shouldTruncate = reason && reason.length > 100;
|
||||||
|
|
||||||
const toggleExpanded = () => {
|
const toggleExpanded = () => {
|
||||||
@@ -571,8 +640,8 @@ const InvestmentCalendar = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查是否有引用数据(reason 就是 record[2])
|
// 检查是否有引用数据
|
||||||
const citationData = reason;
|
const citationData = description;
|
||||||
const hasCitation = citationData && citationData.data && Array.isArray(citationData.data);
|
const hasCitation = citationData && citationData.data && Array.isArray(citationData.data);
|
||||||
|
|
||||||
if (hasCitation) {
|
if (hasCitation) {
|
||||||
@@ -582,11 +651,11 @@ const InvestmentCalendar = () => {
|
|||||||
if (processed) {
|
if (processed) {
|
||||||
// 计算所有段落的总长度
|
// 计算所有段落的总长度
|
||||||
const totalLength = processed.segments.reduce((sum, seg) => sum + seg.text.length, 0);
|
const totalLength = processed.segments.reduce((sum, seg) => sum + seg.text.length, 0);
|
||||||
const shouldTruncate = totalLength > 100;
|
const shouldTruncateProcessed = totalLength > 100;
|
||||||
|
|
||||||
// 确定要显示的段落
|
// 确定要显示的段落
|
||||||
let displaySegments = processed.segments;
|
let displaySegments = processed.segments;
|
||||||
if (shouldTruncate && !isExpanded) {
|
if (shouldTruncateProcessed && !isExpanded) {
|
||||||
// 需要截断:计算应该显示到哪个段落
|
// 需要截断:计算应该显示到哪个段落
|
||||||
let charCount = 0;
|
let charCount = 0;
|
||||||
displaySegments = [];
|
displaySegments = [];
|
||||||
@@ -621,7 +690,7 @@ const InvestmentCalendar = () => {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{shouldTruncate && (
|
{shouldTruncateProcessed && (
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -665,7 +734,44 @@ const InvestmentCalendar = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '研报引用',
|
||||||
|
dataIndex: 'report',
|
||||||
|
key: 'report',
|
||||||
|
width: 200,
|
||||||
|
render: (report, record) => {
|
||||||
|
if (!report || !report.title) {
|
||||||
|
return <Text type="secondary">-</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ fontSize: '12px' }}>
|
||||||
|
<Tooltip title={report.sentences || report.title}>
|
||||||
|
<div>
|
||||||
|
<Text strong style={{ display: 'block', marginBottom: 2 }}>
|
||||||
|
{report.title.length > 20 ? `${report.title.slice(0, 20)}...` : report.title}
|
||||||
|
</Text>
|
||||||
|
{report.author && (
|
||||||
|
<Text type="secondary" style={{ display: 'block', fontSize: '11px' }}>
|
||||||
|
{report.author}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{report.declare_date && (
|
||||||
|
<Text type="secondary" style={{ fontSize: '11px' }}>
|
||||||
|
{dayjs(report.declare_date).format('YYYY-MM-DD')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{report.match_score && (
|
||||||
|
<Tag color={report.match_score === '好' ? 'green' : 'blue'} style={{ marginLeft: 4, fontSize: '10px' }}>
|
||||||
|
匹配度: {report.match_score}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'K线图',
|
title: 'K线图',
|
||||||
key: 'kline',
|
key: 'kline',
|
||||||
@@ -685,9 +791,9 @@ const InvestmentCalendar = () => {
|
|||||||
key: 'action',
|
key: 'action',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const stockCode = getSixDigitCode(record[0]);
|
const stockCode = getSixDigitCode(record.code);
|
||||||
const isAdding = addingToWatchlist[stockCode] || false;
|
const isAdding = addingToWatchlist[stockCode] || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
|
|||||||
Reference in New Issue
Block a user