update pay ui

This commit is contained in:
2025-12-03 15:19:23 +08:00
parent ea1adcb2ca
commit c136c2aed8
3 changed files with 212 additions and 37 deletions

49
app.py
View File

@@ -6314,14 +6314,22 @@ def get_stock_kline(stock_code):
@app.route('/api/stock/batch-kline', methods=['POST']) @app.route('/api/stock/batch-kline', methods=['POST'])
def get_batch_kline_data(): def get_batch_kline_data():
"""批量获取多只股票的K线/分时数据 """批量获取多只股票的K线/分时数据
请求体:{ codes: string[], type: 'timeline'|'daily', event_time?: string } 请求体:{
返回:{ success: true, data: { [code]: { data: [], trade_date: '', ... } } } codes: string[],
type: 'timeline'|'daily',
event_time?: string,
days_before?: number, # 查询事件日期前多少天的数据默认60最大365
end_date?: string # 分页加载时指定结束日期(用于加载更早的数据)
}
返回:{ success: true, data: { [code]: { data: [], trade_date: '', ... } }, has_more: boolean }
""" """
try: try:
data = request.json data = request.json
codes = data.get('codes', []) codes = data.get('codes', [])
chart_type = data.get('type', 'timeline') chart_type = data.get('type', 'timeline')
event_time = data.get('event_time') event_time = data.get('event_time')
days_before = min(int(data.get('days_before', 60)), 365) # 默认60天最多365天
custom_end_date = data.get('end_date') # 用于分页加载更早数据
if not codes: if not codes:
return jsonify({'success': False, 'error': '请提供股票代码列表'}), 400 return jsonify({'success': False, 'error': '请提供股票代码列表'}), 400
@@ -6435,10 +6443,21 @@ def get_batch_kline_data():
if base_codes: if base_codes:
placeholders = ','.join([f':code{i}' for i in range(len(base_codes))]) placeholders = ','.join([f':code{i}' for i in range(len(base_codes))])
params = {f'code{i}': code for i, code in enumerate(base_codes)} params = {f'code{i}': code for i, code in enumerate(base_codes)}
# 确定查询的日期范围
# 如果指定了 custom_end_date用于分页加载更早的数据
if custom_end_date:
try:
end_date_obj = datetime.strptime(custom_end_date, '%Y-%m-%d').date()
except ValueError:
end_date_obj = target_date
else:
end_date_obj = target_date
# TRADEDATE 是整数格式 YYYYMMDD需要转换日期格式 # TRADEDATE 是整数格式 YYYYMMDD需要转换日期格式
start_date = target_date - timedelta(days=60) start_date = end_date_obj - timedelta(days=days_before)
params['start_date'] = int(start_date.strftime('%Y%m%d')) params['start_date'] = int(start_date.strftime('%Y%m%d'))
params['end_date'] = int(target_date.strftime('%Y%m%d')) params['end_date'] = int(end_date_obj.strftime('%Y%m%d'))
daily_result = conn.execute(text(f""" daily_result = conn.execute(text(f"""
SELECT SECCODE, TRADEDATE, F003N as open, F005N as high, F006N as low, F007N as close, F004N as volume SELECT SECCODE, TRADEDATE, F003N as open, F005N as high, F006N as low, F007N as close, F004N as volume
@@ -6473,24 +6492,40 @@ def get_batch_kline_data():
}) })
# 组装结果(使用原始代码作为 key 返回) # 组装结果(使用原始代码作为 key 返回)
# 同时计算最早日期,用于判断是否还有更多数据
earliest_dates = {}
for orig_code in original_codes: for orig_code in original_codes:
base_code = orig_code.split('.')[0] base_code = orig_code.split('.')[0]
stock_name = stock_names.get(base_code, f'股票{base_code}') stock_name = stock_names.get(base_code, f'股票{base_code}')
data_list = stock_data.get(base_code, []) data_list = stock_data.get(base_code, [])
# 记录每只股票的最早日期
if data_list:
earliest_dates[orig_code] = data_list[0]['time']
results[orig_code] = { results[orig_code] = {
'code': orig_code, 'code': orig_code,
'name': stock_name, 'name': stock_name,
'data': data_list, 'data': data_list,
'trade_date': target_date.strftime('%Y-%m-%d'), 'trade_date': target_date.strftime('%Y-%m-%d'),
'type': 'daily' 'type': 'daily',
'earliest_date': data_list[0]['time'] if data_list else None
} }
print(f"批量K线查询完成: {len(codes)} 只股票, 类型: {chart_type}, 交易日: {target_date}") # 计算是否还有更多历史数据基于事件日期往前推365天
event_date = event_datetime.date()
one_year_ago = event_date - timedelta(days=365)
# 如果当前查询的起始日期还没到一年前,则还有更多数据
has_more = start_date > one_year_ago if chart_type == 'daily' else False
print(f"批量K线查询完成: {len(codes)} 只股票, 类型: {chart_type}, 交易日: {target_date}, days_before: {days_before}, has_more: {has_more}")
return jsonify({ return jsonify({
'success': True, 'success': True,
'data': results 'data': results,
'has_more': has_more,
'query_start_date': start_date.strftime('%Y-%m-%d') if chart_type == 'daily' else None,
'query_end_date': end_date_obj.strftime('%Y-%m-%d') if chart_type == 'daily' else None
}) })
except Exception as e: except Exception as e:

View File

@@ -1,9 +1,9 @@
// src/components/StockChart/KLineChartModal.tsx - K线图弹窗组件 // src/components/StockChart/KLineChartModal.tsx - K线图弹窗组件
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState, useCallback } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { klineDataCache, getCacheKey, fetchKlineData } from '@views/Community/components/StockDetailPanel/utils/klineDataCache'; import { stockService } from '@services/eventService';
/** /**
* 股票信息 * 股票信息
@@ -41,6 +41,31 @@ interface KLineDataPoint {
volume: number; volume: number;
} }
/**
* 批量K线API响应
*/
interface BatchKlineResponse {
success: boolean;
data: {
[stockCode: string]: {
code: string;
name: string;
data: KLineDataPoint[];
trade_date: string;
type: string;
earliest_date?: string;
};
};
has_more: boolean;
query_start_date?: string;
query_end_date?: string;
}
// 每次加载的天数
const DAYS_PER_LOAD = 60;
// 最大加载天数(一年)
const MAX_DAYS = 365;
const KLineChartModal: React.FC<KLineChartModalProps> = ({ const KLineChartModal: React.FC<KLineChartModalProps> = ({
isOpen, isOpen,
onClose, onClose,
@@ -51,8 +76,12 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
const chartRef = useRef<HTMLDivElement>(null); const chartRef = useRef<HTMLDivElement>(null);
const chartInstance = useRef<echarts.ECharts | null>(null); const chartInstance = useRef<echarts.ECharts | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<KLineDataPoint[]>([]); const [data, setData] = useState<KLineDataPoint[]>([]);
const [hasMore, setHasMore] = useState(true);
const [earliestDate, setEarliestDate] = useState<string | null>(null);
const [totalDaysLoaded, setTotalDaysLoaded] = useState(0);
// 调试日志 // 调试日志
console.log('[KLineChartModal] 渲染状态:', { console.log('[KLineChartModal] 渲染状态:', {
@@ -61,48 +90,102 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
eventTime, eventTime,
dataLength: data.length, dataLength: data.length,
loading, loading,
error loadingMore,
hasMore,
earliestDate
}); });
// 加载K线数据优先使用缓存 // 加载更多历史数据
const loadData = async () => { const loadMoreData = useCallback(async () => {
if (!stock?.stock_code || !hasMore || loadingMore || !earliestDate) return;
console.log('[KLineChartModal] 加载更多历史数据, earliestDate:', earliestDate);
setLoadingMore(true);
try {
const stableEventTime = eventTime ? dayjs(eventTime).format('YYYY-MM-DD HH:mm') : '';
// 请求更早的数据end_date 设置为当前最早日期的前一天
const endDate = dayjs(earliestDate).subtract(1, 'day').format('YYYY-MM-DD');
const response = await stockService.getBatchKlineData(
[stock.stock_code],
'daily',
stableEventTime,
{ days_before: DAYS_PER_LOAD, end_date: endDate }
) as BatchKlineResponse;
if (response?.success && response.data) {
const stockData = response.data[stock.stock_code];
const newData = stockData?.data || [];
if (newData.length > 0) {
// 将新数据添加到现有数据的前面
setData(prevData => [...newData, ...prevData]);
setEarliestDate(newData[0].time);
setTotalDaysLoaded(prev => prev + DAYS_PER_LOAD);
console.log('[KLineChartModal] 加载了更多数据:', newData.length, '条');
}
// 检查是否还有更多数据
const noMoreData = !response.has_more || totalDaysLoaded + DAYS_PER_LOAD >= MAX_DAYS || newData.length === 0;
setHasMore(!noMoreData);
}
} catch (err) {
console.error('[KLineChartModal] 加载更多数据失败:', err);
} finally {
setLoadingMore(false);
}
}, [stock?.stock_code, hasMore, loadingMore, earliestDate, eventTime, totalDaysLoaded]);
// 初始加载K线数据
const loadData = useCallback(async () => {
if (!stock?.stock_code) return; if (!stock?.stock_code) return;
setLoading(true); setLoading(true);
setError(null); setError(null);
setData([]);
setHasMore(true);
setEarliestDate(null);
setTotalDaysLoaded(0);
try { try {
// 标准化事件时间
const stableEventTime = eventTime ? dayjs(eventTime).format('YYYY-MM-DD HH:mm') : ''; const stableEventTime = eventTime ? dayjs(eventTime).format('YYYY-MM-DD HH:mm') : '';
// 先检查缓存 // 使用新的带分页参数的接口
const cacheKey = getCacheKey(stock.stock_code, stableEventTime, 'daily'); const response = await stockService.getBatchKlineData(
const cachedData = klineDataCache.get(cacheKey); [stock.stock_code],
'daily',
stableEventTime,
{ days_before: DAYS_PER_LOAD, end_date: '' }
) as BatchKlineResponse;
if (cachedData && cachedData.length > 0) { if (response?.success && response.data) {
console.log('[KLineChartModal] 使用缓存数据, 数据条数:', cachedData.length); const stockData = response.data[stock.stock_code];
setData(cachedData); const klineData = stockData?.data || [];
setLoading(false);
return; if (klineData.length === 0) {
throw new Error('暂无K线数据');
}
console.log('[KLineChartModal] 初始数据条数:', klineData.length);
setData(klineData);
setEarliestDate(klineData[0]?.time || null);
setTotalDaysLoaded(DAYS_PER_LOAD);
setHasMore(response.has_more !== false);
} else {
throw new Error('数据加载失败');
} }
// 缓存没有则请求(会自动存入缓存)
console.log('[KLineChartModal] 缓存未命中,发起请求');
const result = await fetchKlineData(stock.stock_code, stableEventTime, 'daily');
if (!result || result.length === 0) {
throw new Error('暂无K线数据');
}
console.log('[KLineChartModal] 数据条数:', result.length);
setData(result);
} catch (err) { } catch (err) {
const errorMsg = err instanceof Error ? err.message : '数据加载失败'; const errorMsg = err instanceof Error ? err.message : '数据加载失败';
setError(errorMsg); setError(errorMsg);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [stock?.stock_code, eventTime]);
// 用于防抖的 ref
const loadMoreDebounceRef = useRef<NodeJS.Timeout | null>(null);
// 初始化图表 // 初始化图表
useEffect(() => { useEffect(() => {
@@ -135,6 +218,9 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
return () => { return () => {
clearTimeout(timer); clearTimeout(timer);
if (loadMoreDebounceRef.current) {
clearTimeout(loadMoreDebounceRef.current);
}
if (chartInstance.current) { if (chartInstance.current) {
chartInstance.current.dispose(); chartInstance.current.dispose();
chartInstance.current = null; chartInstance.current = null;
@@ -142,6 +228,35 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
}; };
}, [isOpen]); }, [isOpen]);
// 监听 dataZoom 事件,当滑到左边界时加载更多数据
useEffect(() => {
if (!chartInstance.current || !hasMore || loadingMore) return;
const handleDataZoom = (params: any) => {
// 获取当前 dataZoom 的 start 值
const start = params.start ?? params.batch?.[0]?.start ?? 0;
// 当 start 接近 0左边界触发加载更多
if (start <= 5 && hasMore && !loadingMore) {
console.log('[KLineChartModal] 检测到滑动到左边界,准备加载更多数据');
// 防抖处理
if (loadMoreDebounceRef.current) {
clearTimeout(loadMoreDebounceRef.current);
}
loadMoreDebounceRef.current = setTimeout(() => {
loadMoreData();
}, 300);
}
};
chartInstance.current.on('datazoom', handleDataZoom);
return () => {
chartInstance.current?.off('datazoom', handleDataZoom);
};
}, [hasMore, loadingMore, loadMoreData]);
// 更新图表数据 // 更新图表数据
useEffect(() => { useEffect(() => {
if (data.length === 0) { if (data.length === 0) {
@@ -515,7 +630,22 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
</span> </span>
{data.length > 0 && ( {data.length > 0 && (
<span style={{ fontSize: '12px', color: '#666', fontStyle: 'italic' }}> <span style={{ fontSize: '12px', color: '#666', fontStyle: 'italic' }}>
{data.length}1 {data.length}
{hasMore ? '(向左滑动加载更多)' : '(已加载全部)'}
</span>
)}
{loadingMore && (
<span style={{ fontSize: '12px', color: '#3182ce', display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{
width: '12px',
height: '12px',
border: '2px solid #404040',
borderTop: '2px solid #3182ce',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
display: 'inline-block'
}} />
...
</span> </span>
)} )}
</div> </div>

View File

@@ -364,9 +364,12 @@ export const stockService = {
* @param {string[]} stockCodes - 股票代码数组 * @param {string[]} stockCodes - 股票代码数组
* @param {string} chartType - 图表类型 (timeline/daily) * @param {string} chartType - 图表类型 (timeline/daily)
* @param {string} eventTime - 事件时间 * @param {string} eventTime - 事件时间
* @returns {Promise<Object>} { [stockCode]: data[] } * @param {Object} options - 额外选项
* @param {number} options.days_before - 查询事件日期前多少天的数据默认60最大365
* @param {string} options.end_date - 分页加载时指定结束日期(用于加载更早的数据)
* @returns {Promise<Object>} { success, data: { [stockCode]: data[] }, has_more, query_start_date, query_end_date }
*/ */
getBatchKlineData: async (stockCodes, chartType = 'timeline', eventTime = null) => { getBatchKlineData: async (stockCodes, chartType = 'timeline', eventTime = null, options = {}) => {
try { try {
const requestBody = { const requestBody = {
codes: stockCodes, codes: stockCodes,
@@ -375,8 +378,15 @@ export const stockService = {
if (eventTime) { if (eventTime) {
requestBody.event_time = eventTime; requestBody.event_time = eventTime;
} }
// 添加分页参数
if (options.days_before) {
requestBody.days_before = options.days_before;
}
if (options.end_date) {
requestBody.end_date = options.end_date;
}
logger.debug('stockService', '批量获取K线数据', { stockCount: stockCodes.length, chartType, eventTime }); logger.debug('stockService', '批量获取K线数据', { stockCount: stockCodes.length, chartType, eventTime, options });
const response = await apiRequest('/api/stock/batch-kline', { const response = await apiRequest('/api/stock/batch-kline', {
method: 'POST', method: 'POST',