feat: StockChartModal.tsx 替换 KLine 实现

This commit is contained in:
zdl
2025-11-24 13:59:29 +08:00
parent b4dcbd1db9
commit 2f125a9207
10 changed files with 768 additions and 630 deletions

View File

@@ -9,7 +9,8 @@ import type { Chart } from 'klinecharts';
import type { ChartType, KLineDataPoint, RawDataPoint } from '../types';
import { processChartData } from '../utils/dataAdapter';
import { logger } from '@utils/logger';
import { stockService } from '@services/stockService';
import { stockService } from '@services/eventService';
import { klineDataCache, getCacheKey } from '@views/Community/components/StockDetailPanel/utils/klineDataCache';
export interface UseKLineDataOptions {
/** KLineChart 实例 */
@@ -91,22 +92,38 @@ export const useKLineData = (
eventTime,
});
// 调用后端 API 获取数据
const response = await stockService.getKlineData(
stockCode,
chartType,
eventTime
);
// 1. 先检查缓存
const cacheKey = getCacheKey(stockCode, eventTime, chartType);
const cachedData = klineDataCache.get(cacheKey);
if (!response || !response.data) {
throw new Error('后端返回数据为空');
let rawDataList;
if (cachedData && cachedData.length > 0) {
// 使用缓存数据
rawDataList = cachedData;
} else {
// 2. 缓存没有数据,调用 API 请求
const response = await stockService.getKlineData(
stockCode,
chartType,
eventTime
);
if (!response || !response.data) {
throw new Error('后端返回数据为空');
}
rawDataList = response.data;
// 3. 将数据写入缓存(避免下次重复请求)
klineDataCache.set(cacheKey, rawDataList);
}
const rawDataList = response.data;
setRawData(rawDataList);
// 数据转换和处理
const processedData = processChartData(rawDataList, chartType, eventTime);
setData(processedData);
logger.info('useKLineData', 'loadData', '数据加载成功', {
@@ -130,7 +147,7 @@ export const useKLineData = (
}, [stockCode, chartType, eventTime]);
/**
* 更新图表数据(使用 DataLoader 模式
* 更新图表数据(使用 setDataLoader 方法
*/
const updateChartData = useCallback(
(klineData: KLineDataPoint[]) => {
@@ -139,29 +156,120 @@ export const useKLineData = (
}
try {
// KLineChart 10.0: 使用 setDataLoader 方法
chart.setDataLoader({
getBars: (params) => {
// 将数据传递给图表
params.callback(klineData, { more: false });
// 步骤 1: 设置 symbol必需getBars 调用的前置条件)
(chart as any).setSymbol({
ticker: stockCode || 'UNKNOWN', // 股票代码
pricePrecision: 2, // 价格精度2位小数
volumePrecision: 0 // 成交量精度(整数)
});
logger.debug('useKLineData', 'updateChartData', 'DataLoader 回调', {
dataCount: klineData.length,
// 步骤 2: 设置 period必需getBars 调用的前置条件)
const periodType = chartType === 'timeline' ? 'minute' : 'day';
(chart as any).setPeriod({
type: periodType, // 分时图=minute日K=day
span: 1 // 周期跨度1分钟/1天
});
// 步骤 3: 设置 DataLoader同步数据加载器
(chart as any).setDataLoader({
getBars: (params: any) => {
if (params.type === 'init') {
// 初始化加载:返回完整数据
params.callback(klineData, false); // false = 无更多数据可加载
} else if (params.type === 'forward' || params.type === 'backward') {
// 向前/向后加载:我们没有更多数据,返回空数组
params.callback([], false);
}
}
});
// 步骤 4: 触发初始化加载(这会调用 getBars with type="init"
(chart as any).resetData();
// 步骤 5: 根据数据量调整可见范围和柱子间距(让 K 线柱子填满图表区域)
setTimeout(() => {
try {
const dataLength = klineData.length;
if (dataLength > 0) {
// 获取图表容器宽度
const chartDom = (chart as any).getDom();
const chartWidth = chartDom?.clientWidth || 1200;
// 计算最优柱子间距
// 公式barSpace = (图表宽度 / 数据数量) * 0.7
// 0.7 是为了留出一些间距,让图表不会太拥挤
const optimalBarSpace = Math.max(8, Math.min(50, (chartWidth / dataLength) * 0.7));
(chart as any).setBarSpace(optimalBarSpace);
// 减少右侧空白(默认值可能是 100-200调小会减少右侧空白
(chart as any).setOffsetRightDistance(50);
}
} catch (err) {
logger.error('useKLineData', 'updateChartData', err as Error, {
step: '调整可见范围失败',
});
},
});
}
}, 100); // 延迟 100ms 确保数据已加载和渲染
logger.debug('useKLineData', 'updateChartData', '图表数据已更新', {
dataCount: klineData.length,
chartId: chart.id,
});
// ✅ 步骤 4: 分时图添加均价线(使用自定义 AVG 指标)
if (chartType === 'timeline' && klineData.length > 0) {
setTimeout(() => {
try {
// 在主图窗格创建 AVG 均价线指标
(chart as any).createIndicator('AVG', true, {
id: 'candle_pane', // 主图窗格
});
console.log('[DEBUG] ✅ 均价线AVG指标添加成功');
} catch (err) {
console.error('[DEBUG] ❌ 均价线添加失败:', err);
}
}, 150); // 延迟 150ms确保数据加载完成后再创建指标
// ✅ 步骤 5: 添加昨收价基准线(灰色虚线)
setTimeout(() => {
try {
const prevClose = klineData[0]?.prev_close;
if (prevClose && prevClose > 0) {
// 创建水平线覆盖层
(chart as any).createOverlay({
name: 'horizontalStraightLine',
id: 'prev_close_line',
points: [{ value: prevClose }],
styles: {
line: {
style: 'dashed',
dashValue: [4, 2],
size: 1,
color: '#888888', // 灰色虚线
},
},
extendData: {
label: `昨收: ${prevClose.toFixed(2)}`,
},
});
console.log('[DEBUG] ✅ 昨收价基准线添加成功:', prevClose);
}
} catch (err) {
console.error('[DEBUG] ❌ 昨收价基准线添加失败:', err);
}
}, 200); // 延迟 200ms确保均价线创建完成后再添加
}
logger.debug(
'useKLineData',
`updateChartData - ${stockCode} (${chartType}) - ${klineData.length}条数据加载成功`
);
} catch (err) {
logger.error('useKLineData', 'updateChartData', err as Error, {
dataCount: klineData.length,
});
}
},
[chart]
[chart, stockCode, chartType]
);
/**
@@ -172,9 +280,10 @@ export const useKLineData = (
setData(newData);
updateChartData(newData);
logger.debug('useKLineData', 'updateData', '手动更新数据', {
newDataCount: newData.length,
});
logger.debug(
'useKLineData',
`updateData - ${stockCode} (${chartType}) - ${newData.length}条数据手动更新`
);
},
[updateChartData]
);
@@ -189,9 +298,7 @@ export const useKLineData = (
if (chart) {
chart.resetData();
logger.debug('useKLineData', 'clearData', '清空数据', {
chartId: chart.id,
});
logger.debug('useKLineData', `clearData - chartId: ${(chart as any).id}`);
}
}, [chart]);