更新Company页面的UI为FUI风格
This commit is contained in:
138
app.py
138
app.py
@@ -8650,6 +8650,144 @@ def get_stock_basic_info(stock_code):
|
|||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
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'])
|
@app.route('/api/stock/<stock_code>/announcements', methods=['GET'])
|
||||||
def get_stock_announcements(stock_code):
|
def get_stock_announcements(stock_code):
|
||||||
"""获取股票公告列表"""
|
"""获取股票公告列表"""
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* useStockQuoteData - 股票行情数据获取 Hook
|
* useStockQuoteData - 股票行情数据获取 Hook
|
||||||
*
|
*
|
||||||
* 合并获取行情数据和基本信息,供 StockQuoteCard 内部使用
|
* 使用 /api/stock/{code}/quote-detail 接口获取完整行情数据
|
||||||
|
* 供 StockQuoteCard 内部使用
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { stockService } from '@services/eventService';
|
|
||||||
import { logger } from '@utils/logger';
|
import { logger } from '@utils/logger';
|
||||||
import axios from '@utils/axiosConfig';
|
import axios from '@utils/axiosConfig';
|
||||||
import type { StockQuoteCardData } from '../types';
|
import type { StockQuoteCardData } from '../types';
|
||||||
@@ -77,16 +77,23 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
|
|||||||
const refetchRef = useCallback(async () => {
|
const refetchRef = useCallback(async () => {
|
||||||
if (!stockCode) return;
|
if (!stockCode) return;
|
||||||
|
|
||||||
// 获取行情数据
|
// 标准化股票代码(去除后缀)
|
||||||
|
const baseCode = stockCode.split('.')[0];
|
||||||
|
|
||||||
|
// 获取行情详情数据(使用新的 quote-detail 接口)
|
||||||
setQuoteLoading(true);
|
setQuoteLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
logger.debug('useStockQuoteData', '获取股票行情', { stockCode });
|
logger.debug('useStockQuoteData', '获取股票行情详情', { stockCode, baseCode });
|
||||||
const quotes = await stockService.getQuotes([stockCode]);
|
const { data: result } = await axios.get(`/api/stock/${baseCode}/quote-detail`);
|
||||||
const quoteResult = quotes?.[stockCode] || quotes;
|
if (result.success && result.data) {
|
||||||
const transformedData = transformQuoteData(quoteResult, stockCode);
|
const transformedData = transformQuoteData(result.data, stockCode);
|
||||||
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
|
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
|
||||||
setQuoteData(transformedData);
|
setQuoteData(transformedData);
|
||||||
|
} else {
|
||||||
|
setError('获取行情数据失败');
|
||||||
|
setQuoteData(null);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('useStockQuoteData', '获取行情失败', err);
|
logger.error('useStockQuoteData', '获取行情失败', err);
|
||||||
setError('获取行情数据失败');
|
setError('获取行情数据失败');
|
||||||
@@ -95,10 +102,10 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
|
|||||||
setQuoteLoading(false);
|
setQuoteLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取基本信息
|
// 获取基本信息(公司简介等)
|
||||||
setBasicLoading(true);
|
setBasicLoading(true);
|
||||||
try {
|
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) {
|
if (result.success) {
|
||||||
setBasicInfo(result.data);
|
setBasicInfo(result.data);
|
||||||
}
|
}
|
||||||
@@ -120,18 +127,27 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
let isCancelled = false;
|
let isCancelled = false;
|
||||||
|
|
||||||
|
// 标准化股票代码(去除后缀)
|
||||||
|
const baseCode = stockCode.split('.')[0];
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
// 获取行情数据
|
// 获取行情详情数据(使用新的 quote-detail 接口)
|
||||||
setQuoteLoading(true);
|
setQuoteLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
logger.debug('useStockQuoteData', '获取股票行情', { stockCode });
|
logger.debug('useStockQuoteData', '获取股票行情详情', { stockCode, baseCode });
|
||||||
const quotes = await stockService.getQuotes([stockCode]);
|
const { data: result } = await axios.get(`/api/stock/${baseCode}/quote-detail`, {
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
if (isCancelled) return;
|
if (isCancelled) return;
|
||||||
const quoteResult = quotes?.[stockCode] || quotes;
|
if (result.success && result.data) {
|
||||||
const transformedData = transformQuoteData(quoteResult, stockCode);
|
const transformedData = transformQuoteData(result.data, stockCode);
|
||||||
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
|
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
|
||||||
setQuoteData(transformedData);
|
setQuoteData(transformedData);
|
||||||
|
} else {
|
||||||
|
setError('获取行情数据失败');
|
||||||
|
setQuoteData(null);
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (isCancelled || err.name === 'CanceledError') return;
|
if (isCancelled || err.name === 'CanceledError') return;
|
||||||
logger.error('useStockQuoteData', '获取行情失败', err);
|
logger.error('useStockQuoteData', '获取行情失败', err);
|
||||||
@@ -141,10 +157,10 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
|
|||||||
if (!isCancelled) setQuoteLoading(false);
|
if (!isCancelled) setQuoteLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取基本信息
|
// 获取基本信息(公司简介等)
|
||||||
setBasicLoading(true);
|
setBasicLoading(true);
|
||||||
try {
|
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,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
if (isCancelled) return;
|
if (isCancelled) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user