更新Company页面的UI为FUI风格

This commit is contained in:
2025-12-17 21:11:34 +08:00
parent 318a83434a
commit 067b720263
2 changed files with 174 additions and 20 deletions

138
app.py
View File

@@ -8650,6 +8650,144 @@ def get_stock_basic_info(stock_code):
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/stock/<stock_code>/quote-detail', methods=['GET'])
def get_stock_quote_detail(stock_code):
"""获取股票完整行情数据 - 供 StockQuoteCard 使用
返回数据包括:
- 基础信息:名称、代码、行业分类
- 价格信息:现价、涨跌幅、开盘、收盘、最高、最低
- 关键指标市盈率、市净率、流通市值、52周高低
- 主力动态:主力净流入、机构持仓(如有)
"""
try:
# 标准化股票代码(去除后缀)
base_code = stock_code.split('.')[0] if '.' in stock_code else stock_code
result_data = {
'code': stock_code,
'name': '',
'industry': '',
'industry_l1': '',
'sw_industry_l1': '',
'sw_industry_l2': '',
# 价格信息
'current_price': None,
'change_percent': None,
'today_open': None,
'yesterday_close': None,
'today_high': None,
'today_low': None,
# 关键指标
'pe': None,
'pb': None,
'eps': None,
'market_cap': None,
'circ_mv': None,
'turnover_rate': None,
'week52_high': None,
'week52_low': None,
# 主力动态(预留字段)
'main_net_inflow': None,
'institution_holding': None,
'buy_ratio': None,
'sell_ratio': None,
'update_time': None
}
with engine.connect() as conn:
# 1. 获取最新交易数据(来自 ea_trade
trade_query = text("""
SELECT
t.SECCODE,
t.SECNAME,
t.TRADEDATE,
t.F002N as pre_close,
t.F003N as open_price,
t.F004N as volume,
t.F005N as high,
t.F006N as low,
t.F007N as close_price,
t.F010N as change_pct,
t.F011N as amount,
t.F012N as turnover_rate,
t.F020N as total_shares,
t.F021N as float_shares,
t.F026N as pe_ratio,
b.F034V as sw_industry_l1,
b.F036V as sw_industry_l2,
b.F030V as industry_l1
FROM ea_trade t
LEFT JOIN ea_baseinfo b ON t.SECCODE = b.SECCODE
WHERE t.SECCODE = :stock_code
ORDER BY t.TRADEDATE DESC
LIMIT 1
""")
trade_result = conn.execute(trade_query, {'stock_code': base_code}).fetchone()
if trade_result:
row = row_to_dict(trade_result)
result_data['name'] = row.get('SECNAME') or ''
result_data['current_price'] = float(row.get('close_price') or 0)
result_data['change_percent'] = float(row.get('change_pct') or 0)
result_data['today_open'] = float(row.get('open_price') or 0)
result_data['yesterday_close'] = float(row.get('pre_close') or 0)
result_data['today_high'] = float(row.get('high') or 0)
result_data['today_low'] = float(row.get('low') or 0)
result_data['pe'] = float(row.get('pe_ratio') or 0) if row.get('pe_ratio') else None
result_data['turnover_rate'] = float(row.get('turnover_rate') or 0)
result_data['sw_industry_l1'] = row.get('sw_industry_l1') or ''
result_data['sw_industry_l2'] = row.get('sw_industry_l2') or ''
result_data['industry_l1'] = row.get('industry_l1') or ''
result_data['industry'] = row.get('sw_industry_l2') or row.get('sw_industry_l1') or ''
# 计算流通市值(亿元)
float_shares = float(row.get('float_shares') or 0)
close_price = float(row.get('close_price') or 0)
if float_shares > 0 and close_price > 0:
circ_mv = (float_shares * close_price) / 100000000 # 转为亿
result_data['circ_mv'] = round(circ_mv, 2)
result_data['market_cap'] = f"{round(circ_mv, 2)}亿"
trade_date = row.get('TRADEDATE')
if trade_date:
if hasattr(trade_date, 'strftime'):
result_data['update_time'] = trade_date.strftime('%Y-%m-%d')
else:
result_data['update_time'] = str(trade_date)
# 2. 获取52周高低价
week52_query = text("""
SELECT
MAX(F005N) as week52_high,
MIN(F006N) as week52_low
FROM ea_trade
WHERE SECCODE = :stock_code
AND TRADEDATE >= DATE_SUB(CURDATE(), INTERVAL 52 WEEK)
AND F005N > 0 AND F006N > 0
""")
week52_result = conn.execute(week52_query, {'stock_code': base_code}).fetchone()
if week52_result:
w52 = row_to_dict(week52_result)
result_data['week52_high'] = float(w52.get('week52_high') or 0)
result_data['week52_low'] = float(w52.get('week52_low') or 0)
return jsonify({
'success': True,
'data': result_data
})
except Exception as e:
app.logger.error(f"Error getting stock quote detail: {e}", exc_info=True)
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/stock/<stock_code>/announcements', methods=['GET'])
def get_stock_announcements(stock_code):
"""获取股票公告列表"""

View File

@@ -1,11 +1,11 @@
/**
* useStockQuoteData - 股票行情数据获取 Hook
*
* 合并获取行情数据和基本信息,供 StockQuoteCard 内部使用
* 使用 /api/stock/{code}/quote-detail 接口获取完整行情数据
* 供 StockQuoteCard 内部使用
*/
import { useState, useEffect, useCallback } from 'react';
import { stockService } from '@services/eventService';
import { logger } from '@utils/logger';
import axios from '@utils/axiosConfig';
import type { StockQuoteCardData } from '../types';
@@ -77,16 +77,23 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
const refetchRef = useCallback(async () => {
if (!stockCode) return;
// 获取行情数据
// 标准化股票代码(去除后缀)
const baseCode = stockCode.split('.')[0];
// 获取行情详情数据(使用新的 quote-detail 接口)
setQuoteLoading(true);
setError(null);
try {
logger.debug('useStockQuoteData', '获取股票行情', { stockCode });
const quotes = await stockService.getQuotes([stockCode]);
const quoteResult = quotes?.[stockCode] || quotes;
const transformedData = transformQuoteData(quoteResult, stockCode);
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
setQuoteData(transformedData);
logger.debug('useStockQuoteData', '获取股票行情详情', { stockCode, baseCode });
const { data: result } = await axios.get(`/api/stock/${baseCode}/quote-detail`);
if (result.success && result.data) {
const transformedData = transformQuoteData(result.data, stockCode);
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
setQuoteData(transformedData);
} else {
setError('获取行情数据失败');
setQuoteData(null);
}
} catch (err) {
logger.error('useStockQuoteData', '获取行情失败', err);
setError('获取行情数据失败');
@@ -95,10 +102,10 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
setQuoteLoading(false);
}
// 获取基本信息
// 获取基本信息(公司简介等)
setBasicLoading(true);
try {
const { data: result } = await axios.get(`/api/stock/${stockCode}/basic-info`);
const { data: result } = await axios.get(`/api/stock/${baseCode}/basic-info`);
if (result.success) {
setBasicInfo(result.data);
}
@@ -120,18 +127,27 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
const controller = new AbortController();
let isCancelled = false;
// 标准化股票代码(去除后缀)
const baseCode = stockCode.split('.')[0];
const fetchData = async () => {
// 获取行情数据
// 获取行情详情数据(使用新的 quote-detail 接口)
setQuoteLoading(true);
setError(null);
try {
logger.debug('useStockQuoteData', '获取股票行情', { stockCode });
const quotes = await stockService.getQuotes([stockCode]);
logger.debug('useStockQuoteData', '获取股票行情详情', { stockCode, baseCode });
const { data: result } = await axios.get(`/api/stock/${baseCode}/quote-detail`, {
signal: controller.signal,
});
if (isCancelled) return;
const quoteResult = quotes?.[stockCode] || quotes;
const transformedData = transformQuoteData(quoteResult, stockCode);
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
setQuoteData(transformedData);
if (result.success && result.data) {
const transformedData = transformQuoteData(result.data, stockCode);
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
setQuoteData(transformedData);
} else {
setError('获取行情数据失败');
setQuoteData(null);
}
} catch (err: any) {
if (isCancelled || err.name === 'CanceledError') return;
logger.error('useStockQuoteData', '获取行情失败', err);
@@ -141,10 +157,10 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
if (!isCancelled) setQuoteLoading(false);
}
// 获取基本信息
// 获取基本信息(公司简介等)
setBasicLoading(true);
try {
const { data: result } = await axios.get(`/api/stock/${stockCode}/basic-info`, {
const { data: result } = await axios.get(`/api/stock/${baseCode}/basic-info`, {
signal: controller.signal,
});
if (isCancelled) return;