update pay ui
This commit is contained in:
49
app.py
49
app.py
@@ -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:
|
||||||
|
|||||||
@@ -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) {
|
||||||
console.log('[KLineChartModal] 缓存未命中,发起请求');
|
|
||||||
const result = await fetchKlineData(stock.stock_code, stableEventTime, 'daily');
|
|
||||||
|
|
||||||
if (!result || result.length === 0) {
|
|
||||||
throw new Error('暂无K线数据');
|
throw new Error('暂无K线数据');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[KLineChartModal] 数据条数:', result.length);
|
console.log('[KLineChartModal] 初始数据条数:', klineData.length);
|
||||||
setData(result);
|
setData(klineData);
|
||||||
|
setEarliestDate(klineData[0]?.time || null);
|
||||||
|
setTotalDaysLoaded(DAYS_PER_LOAD);
|
||||||
|
setHasMore(response.has_more !== false);
|
||||||
|
} else {
|
||||||
|
throw new Error('数据加载失败');
|
||||||
|
}
|
||||||
} 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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user