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'])
def get_batch_kline_data():
"""批量获取多只股票的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:
data = request.json
codes = data.get('codes', [])
chart_type = data.get('type', 'timeline')
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:
return jsonify({'success': False, 'error': '请提供股票代码列表'}), 400
@@ -6435,10 +6443,21 @@ def get_batch_kline_data():
if 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)}
# 确定查询的日期范围
# 如果指定了 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需要转换日期格式
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['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"""
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 返回)
# 同时计算最早日期,用于判断是否还有更多数据
earliest_dates = {}
for orig_code in original_codes:
base_code = orig_code.split('.')[0]
stock_name = stock_names.get(base_code, f'股票{base_code}')
data_list = stock_data.get(base_code, [])
# 记录每只股票的最早日期
if data_list:
earliest_dates[orig_code] = data_list[0]['time']
results[orig_code] = {
'code': orig_code,
'name': stock_name,
'data': data_list,
'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({
'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:

View File

@@ -1,9 +1,9 @@
// 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 * as echarts from 'echarts';
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;
}
/**
* 批量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> = ({
isOpen,
onClose,
@@ -51,8 +76,12 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
const chartRef = useRef<HTMLDivElement>(null);
const chartInstance = useRef<echarts.ECharts | null>(null);
const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState<string | null>(null);
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] 渲染状态:', {
@@ -61,48 +90,102 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
eventTime,
dataLength: data.length,
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;
setLoading(true);
setError(null);
setData([]);
setHasMore(true);
setEarliestDate(null);
setTotalDaysLoaded(0);
try {
// 标准化事件时间
const stableEventTime = eventTime ? dayjs(eventTime).format('YYYY-MM-DD HH:mm') : '';
// 先检查缓存
const cacheKey = getCacheKey(stock.stock_code, stableEventTime, 'daily');
const cachedData = klineDataCache.get(cacheKey);
// 使用新的带分页参数的接口
const response = await stockService.getBatchKlineData(
[stock.stock_code],
'daily',
stableEventTime,
{ days_before: DAYS_PER_LOAD, end_date: '' }
) as BatchKlineResponse;
if (cachedData && cachedData.length > 0) {
console.log('[KLineChartModal] 使用缓存数据, 数据条数:', cachedData.length);
setData(cachedData);
setLoading(false);
return;
}
if (response?.success && response.data) {
const stockData = response.data[stock.stock_code];
const klineData = stockData?.data || [];
// 缓存没有则请求(会自动存入缓存)
console.log('[KLineChartModal] 缓存未命中,发起请求');
const result = await fetchKlineData(stock.stock_code, stableEventTime, 'daily');
if (!result || result.length === 0) {
if (klineData.length === 0) {
throw new Error('暂无K线数据');
}
console.log('[KLineChartModal] 数据条数:', result.length);
setData(result);
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('数据加载失败');
}
} catch (err) {
const errorMsg = err instanceof Error ? err.message : '数据加载失败';
setError(errorMsg);
} finally {
setLoading(false);
}
};
}, [stock?.stock_code, eventTime]);
// 用于防抖的 ref
const loadMoreDebounceRef = useRef<NodeJS.Timeout | null>(null);
// 初始化图表
useEffect(() => {
@@ -135,6 +218,9 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
return () => {
clearTimeout(timer);
if (loadMoreDebounceRef.current) {
clearTimeout(loadMoreDebounceRef.current);
}
if (chartInstance.current) {
chartInstance.current.dispose();
chartInstance.current = null;
@@ -142,6 +228,35 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
};
}, [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(() => {
if (data.length === 0) {
@@ -515,7 +630,22 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
</span>
{data.length > 0 && (
<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>
)}
</div>

View File

@@ -364,9 +364,12 @@ export const stockService = {
* @param {string[]} stockCodes - 股票代码数组
* @param {string} chartType - 图表类型 (timeline/daily)
* @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 {
const requestBody = {
codes: stockCodes,
@@ -375,8 +378,15 @@ export const stockService = {
if (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', {
method: 'POST',