|
|
@@ -4,6 +4,8 @@ import type { EChartsType, EChartsOption } from 'echarts';
|
|
|
import { StockChart as StockChartLib } from '../lib/index';
|
|
|
import type { StockData, DateMemo, TradeRecord, ActiveType } from '../types/index';
|
|
|
import { ChartDrawingTools } from '../lib/drawing/ChartDrawingTools';
|
|
|
+import { MobileChartConfig } from '../lib/config/MobileChartConfig';
|
|
|
+import { useIsMobile } from '@/client/hooks/use-mobile';
|
|
|
|
|
|
// 将 StockChartRef 接口移到 Props 定义之前
|
|
|
interface StockChartRef {
|
|
|
@@ -21,6 +23,7 @@ interface StockChartProps {
|
|
|
className?: string;
|
|
|
onChartReady?: (chart: EChartsType) => void;
|
|
|
trades?: TradeRecord[];
|
|
|
+ isMobile?: boolean; // 新增移动端检测参数
|
|
|
}
|
|
|
|
|
|
// 添加自定义类型定义
|
|
|
@@ -48,7 +51,7 @@ interface ScatterDataItemOption {
|
|
|
|
|
|
// 修改组件定义为 forwardRef,添加解构参数的类型
|
|
|
const StockChart = forwardRef<StockChartRef, StockChartProps>((
|
|
|
- props: StockChartProps,
|
|
|
+ props: StockChartProps,
|
|
|
ref: React.ForwardedRef<StockChartRef>
|
|
|
) => {
|
|
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
|
@@ -56,15 +59,19 @@ const StockChart = forwardRef<StockChartRef, StockChartProps>((
|
|
|
const stockChartRef = useRef<StockChartLib | null>(null);
|
|
|
const drawingToolsRef = useRef<ChartDrawingTools | null>(null);
|
|
|
|
|
|
+ // 使用统一的移动端检测Hook
|
|
|
+ const isMobileHook = useIsMobile();
|
|
|
+ const isMobile = props.isMobile !== undefined ? props.isMobile : isMobileHook;
|
|
|
+
|
|
|
// 初始化图表和工具 - 只执行一次
|
|
|
useEffect(() => {
|
|
|
if (!chartRef.current) return;
|
|
|
|
|
|
const chartInstance = echarts.init(chartRef.current);
|
|
|
chartInstanceRef.current = chartInstance;
|
|
|
-
|
|
|
- // 创建 StockChart 实例
|
|
|
- const stockChart = new StockChartLib(props.stockData, props.memoData, chartInstance);
|
|
|
+
|
|
|
+ // 创建 StockChart 实例,传入移动端标识
|
|
|
+ const stockChart = new StockChartLib(props.stockData, props.memoData, chartInstance, isMobile);
|
|
|
stockChartRef.current = stockChart;
|
|
|
|
|
|
// 初始化画线工具 - 只初始化一次
|
|
|
@@ -72,6 +79,12 @@ const StockChart = forwardRef<StockChartRef, StockChartProps>((
|
|
|
|
|
|
// 设置初始配置
|
|
|
const option = stockChart.createChartOption();
|
|
|
+
|
|
|
+ // 如果是移动端,添加手势配置
|
|
|
+ if (isMobile) {
|
|
|
+ Object.assign(option, MobileChartConfig.getMobileGestureConfig());
|
|
|
+ }
|
|
|
+
|
|
|
chartInstance.setOption(option as EChartsOption);
|
|
|
|
|
|
// 在设置完图表配置后初始化绘图工具
|
|
|
@@ -91,6 +104,9 @@ const StockChart = forwardRef<StockChartRef, StockChartProps>((
|
|
|
// stockChart.handleMouseEvent('mousemove', params);
|
|
|
// });
|
|
|
|
|
|
+ // 添加移动端触摸事件处理
|
|
|
+ const cleanupTouchEvents = isMobile ? setupMobileTouchEvents(chartInstance, stockChart) : null;
|
|
|
+
|
|
|
// 通知外部图表已准备就绪
|
|
|
props.onChartReady?.(chartInstance);
|
|
|
|
|
|
@@ -99,28 +115,34 @@ const StockChart = forwardRef<StockChartRef, StockChartProps>((
|
|
|
// zr.off('click');
|
|
|
// zr.off('mousedown');
|
|
|
// zr.off('mousemove');
|
|
|
+ if (cleanupTouchEvents) {
|
|
|
+ cleanupTouchEvents();
|
|
|
+ }
|
|
|
chartInstance.dispose();
|
|
|
};
|
|
|
- }, []); // 空依赖数组,只执行一次
|
|
|
+ }, [isMobile]); // 依赖 isMobile,当设备类型变化时重新初始化
|
|
|
|
|
|
// 处理数据更新
|
|
|
useEffect(() => {
|
|
|
if (!chartRef.current || !chartInstanceRef.current || !stockChartRef.current) return;
|
|
|
-
|
|
|
+
|
|
|
const chartInstance = chartInstanceRef.current;
|
|
|
const stockChart = stockChartRef.current;
|
|
|
-
|
|
|
+
|
|
|
// 更新 StockChart 实例的数据
|
|
|
stockChart.updateData(props.stockData, props.memoData);
|
|
|
-
|
|
|
+
|
|
|
// 更新图表数据
|
|
|
const option = stockChart.createChartOption();
|
|
|
- // console.log('option', option);
|
|
|
- // if (!option) return;
|
|
|
+
|
|
|
+ // 如果是移动端,添加手势配置
|
|
|
+ if (isMobile) {
|
|
|
+ Object.assign(option, MobileChartConfig.getMobileGestureConfig());
|
|
|
+ }
|
|
|
|
|
|
// 保持原有的 markLine 数据
|
|
|
const currentOption = chartInstance.getOption() as EChartsOption;
|
|
|
-
|
|
|
+
|
|
|
if (currentOption && currentOption.series && Array.isArray(currentOption.series)) {
|
|
|
const series = currentOption.series as echarts.SeriesOption[];
|
|
|
const existingMarkLine = series[0] && (series[0] as any).markLine;
|
|
|
@@ -130,10 +152,9 @@ const StockChart = forwardRef<StockChartRef, StockChartProps>((
|
|
|
}
|
|
|
|
|
|
chartInstance.setOption(option);
|
|
|
- // console.log('currentOption', chartInstance.getOption());
|
|
|
// 重新绘制所有线条
|
|
|
drawingToolsRef.current?.redrawLines();
|
|
|
- }, [props.stockData, props.memoData]);
|
|
|
+ }, [props.stockData, props.memoData, isMobile]);
|
|
|
|
|
|
// 处理窗口大小变化
|
|
|
useEffect(() => {
|
|
|
@@ -145,6 +166,188 @@ const StockChart = forwardRef<StockChartRef, StockChartProps>((
|
|
|
return () => window.removeEventListener('resize', handleResize);
|
|
|
}, []);
|
|
|
|
|
|
+ // 移动端触摸事件设置
|
|
|
+ const setupMobileTouchEvents = (chartInstance: EChartsType, stockChart: any) => {
|
|
|
+ const zr = chartInstance.getZr();
|
|
|
+ let lastTapTime = 0;
|
|
|
+ let tapTimeout: NodeJS.Timeout;
|
|
|
+ let longPressTimeout: NodeJS.Timeout;
|
|
|
+
|
|
|
+ // 触摸防误操作变量
|
|
|
+ let touchStartTime = 0;
|
|
|
+ let touchStartX = 0;
|
|
|
+ let touchStartY = 0;
|
|
|
+ let isDragging = false;
|
|
|
+
|
|
|
+ // 双击重置视图功能
|
|
|
+ const clickHandler = (params: any) => {
|
|
|
+ const currentTime = Date.now();
|
|
|
+ const tapLength = currentTime - lastTapTime;
|
|
|
+
|
|
|
+ // 防误操作检查:避免在拖动后误触发点击
|
|
|
+ if (isDragging) {
|
|
|
+ isDragging = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 防误操作检查:点击持续时间过长视为拖动
|
|
|
+ const clickDuration = currentTime - touchStartTime;
|
|
|
+ if (clickDuration > 300) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (tapLength < 300 && tapLength > 0) {
|
|
|
+ // 双击事件 - 重置视图
|
|
|
+ clearTimeout(tapTimeout);
|
|
|
+ resetChartView(chartInstance, stockChart);
|
|
|
+ } else {
|
|
|
+ // 单击事件 - 设置定时器
|
|
|
+ clearTimeout(tapTimeout);
|
|
|
+ tapTimeout = setTimeout(() => {
|
|
|
+ // 单击处理逻辑
|
|
|
+ handleSingleTap();
|
|
|
+ }, 300);
|
|
|
+ }
|
|
|
+
|
|
|
+ lastTapTime = currentTime;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 触摸开始处理
|
|
|
+ const mousedownHandler = (params: any) => {
|
|
|
+ touchStartTime = Date.now();
|
|
|
+ touchStartX = params.offsetX;
|
|
|
+ touchStartY = params.offsetY;
|
|
|
+ isDragging = false;
|
|
|
+
|
|
|
+ // 长按事件处理
|
|
|
+ longPressTimeout = setTimeout(() => {
|
|
|
+ handleLongPress(params);
|
|
|
+ }, 500);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 触摸移动处理
|
|
|
+ const mousemoveHandler = (params: any) => {
|
|
|
+ if (touchStartTime === 0) return;
|
|
|
+
|
|
|
+ const moveDistance = Math.sqrt(
|
|
|
+ Math.pow(params.offsetX - touchStartX, 2) +
|
|
|
+ Math.pow(params.offsetY - touchStartY, 2)
|
|
|
+ );
|
|
|
+
|
|
|
+ // 移动距离超过阈值视为拖动
|
|
|
+ if (moveDistance > 5) {
|
|
|
+ isDragging = true;
|
|
|
+ clearTimeout(longPressTimeout);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const mouseupHandler = () => {
|
|
|
+ touchStartTime = 0;
|
|
|
+ clearTimeout(longPressTimeout);
|
|
|
+ };
|
|
|
+
|
|
|
+ const mouseoutHandler = () => {
|
|
|
+ touchStartTime = 0;
|
|
|
+ clearTimeout(longPressTimeout);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 触摸反馈效果
|
|
|
+ const touchFeedbackMousedown = (params: any) => {
|
|
|
+ addTouchFeedback(params, chartInstance);
|
|
|
+ };
|
|
|
+
|
|
|
+ const touchFeedbackMouseup = () => {
|
|
|
+ removeTouchFeedback(chartInstance);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 绑定事件
|
|
|
+ zr.on('click', clickHandler);
|
|
|
+ zr.on('mousedown', mousedownHandler);
|
|
|
+ zr.on('mousemove', mousemoveHandler);
|
|
|
+ zr.on('mouseup', mouseupHandler);
|
|
|
+ zr.on('mouseout', mouseoutHandler);
|
|
|
+ zr.on('mousedown', touchFeedbackMousedown);
|
|
|
+ zr.on('mouseup', touchFeedbackMouseup);
|
|
|
+
|
|
|
+ // 返回清理函数
|
|
|
+ return () => {
|
|
|
+ zr.off('click', clickHandler);
|
|
|
+ zr.off('mousedown', mousedownHandler);
|
|
|
+ zr.off('mousemove', mousemoveHandler);
|
|
|
+ zr.off('mouseup', mouseupHandler);
|
|
|
+ zr.off('mouseout', mouseoutHandler);
|
|
|
+ zr.off('mousedown', touchFeedbackMousedown);
|
|
|
+ zr.off('mouseup', touchFeedbackMouseup);
|
|
|
+ clearTimeout(tapTimeout);
|
|
|
+ clearTimeout(longPressTimeout);
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ // 重置图表视图
|
|
|
+ const resetChartView = (chartInstance: EChartsType, stockChart: any) => {
|
|
|
+ const splitData = stockChart.getSplitData();
|
|
|
+ const option = chartInstance.getOption();
|
|
|
+
|
|
|
+ // 重置dataZoom到默认范围
|
|
|
+ if (option.dataZoom && Array.isArray(option.dataZoom)) {
|
|
|
+ option.dataZoom.forEach((zoom: any) => {
|
|
|
+ if (zoom.type === 'inside' || zoom.type === 'slider') {
|
|
|
+ zoom.startValue = splitData.categoryData.length - 20;
|
|
|
+ zoom.endValue = splitData.categoryData.length - 1;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ chartInstance.setOption(option);
|
|
|
+
|
|
|
+ // 添加重置动画效果
|
|
|
+ chartInstance.dispatchAction({
|
|
|
+ type: 'highlight',
|
|
|
+ seriesIndex: 0
|
|
|
+ });
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ chartInstance.dispatchAction({
|
|
|
+ type: 'downplay',
|
|
|
+ seriesIndex: 0
|
|
|
+ });
|
|
|
+ }, 300);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 单击处理
|
|
|
+ const handleSingleTap = () => {
|
|
|
+ // 可以在这里添加单击处理逻辑
|
|
|
+ // 例如显示详细信息等
|
|
|
+ };
|
|
|
+
|
|
|
+ // 长按处理
|
|
|
+ const handleLongPress = (params: any) => {
|
|
|
+ // 长按显示详细信息或操作菜单
|
|
|
+ chartInstanceRef.current?.dispatchAction({
|
|
|
+ type: 'showTip',
|
|
|
+ seriesIndex: 0,
|
|
|
+ dataIndex: params.dataIndex
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // 触摸反馈效果
|
|
|
+ const addTouchFeedback = (params: any, chartInstance: EChartsType) => {
|
|
|
+ // 添加触摸反馈效果
|
|
|
+ chartInstance.dispatchAction({
|
|
|
+ type: 'highlight',
|
|
|
+ seriesIndex: 0,
|
|
|
+ dataIndex: params.dataIndex
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const removeTouchFeedback = (chartInstance: EChartsType) => {
|
|
|
+ // 移除触摸反馈效果
|
|
|
+ chartInstance.dispatchAction({
|
|
|
+ type: 'downplay',
|
|
|
+ seriesIndex: 0
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
// 将 toggleMemoVisibility 方法暴露给父组件
|
|
|
useImperativeHandle(ref, () => ({
|
|
|
toggleMemoVisibility: (visible: boolean) => {
|
|
|
@@ -156,7 +359,7 @@ const StockChart = forwardRef<StockChartRef, StockChartProps>((
|
|
|
stockChart.toggleMemoVisibility(visible);
|
|
|
stockChart.updateMemoVisibility({
|
|
|
...currentOption,
|
|
|
- series: currentOption.series
|
|
|
+ series: currentOption.series as any[]
|
|
|
});
|
|
|
chartInstanceRef.current.setOption(currentOption);
|
|
|
},
|
|
|
@@ -174,18 +377,18 @@ const StockChart = forwardRef<StockChartRef, StockChartProps>((
|
|
|
// 添加交易标记渲染
|
|
|
useEffect(() => {
|
|
|
if (!chartInstanceRef.current || !stockChartRef.current || !props.trades?.length) return;
|
|
|
-
|
|
|
+
|
|
|
const tradeMarkSeries: echarts.ScatterSeriesOption = {
|
|
|
name: "Mark",
|
|
|
type: "scatter",
|
|
|
xAxisIndex: 0,
|
|
|
yAxisIndex: 0,
|
|
|
- data: props.trades.map((trade: TradeRecord, index: number) => {
|
|
|
+ data: props.trades.map((trade: TradeRecord) => {
|
|
|
const dataIndex = props.stockData.findIndex((data: StockData) => data.d === trade.date);
|
|
|
if (dataIndex === -1) return null;
|
|
|
|
|
|
const dayData = props.stockData[dataIndex];
|
|
|
- const price = trade.type === 'BUY'
|
|
|
+ const price = trade.type === 'BUY'
|
|
|
? parseFloat(dayData.h)
|
|
|
: parseFloat(dayData.l);
|
|
|
|
|
|
@@ -216,17 +419,19 @@ const StockChart = forwardRef<StockChartRef, StockChartProps>((
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const currentOption = chartInstanceRef.current.getOption();
|
|
|
-
|
|
|
- // 找到并移除旧的 Mark 系列(如果存在)
|
|
|
- const markSeriesIndex = currentOption.series.findIndex((s: { name?: string }) => s.name === 'Mark');
|
|
|
- if (markSeriesIndex > -1) {
|
|
|
- currentOption.series.splice(markSeriesIndex, 1);
|
|
|
+ const currentOption = chartInstanceRef.current.getOption() as EChartsOption;
|
|
|
+
|
|
|
+ if (currentOption && currentOption.series && Array.isArray(currentOption.series)) {
|
|
|
+ // 找到并移除旧的 Mark 系列(如果存在)
|
|
|
+ const markSeriesIndex = currentOption.series.findIndex((s: any) => s.name === 'Mark');
|
|
|
+ if (markSeriesIndex > -1) {
|
|
|
+ currentOption.series.splice(markSeriesIndex, 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ currentOption.series.push(tradeMarkSeries);
|
|
|
+
|
|
|
+ chartInstanceRef.current.setOption(currentOption);
|
|
|
}
|
|
|
-
|
|
|
- (currentOption.series as echarts.SeriesOption[]).push(tradeMarkSeries);
|
|
|
-
|
|
|
- chartInstanceRef.current.setOption(currentOption);
|
|
|
}, [props.trades, props.stockData]);
|
|
|
|
|
|
return (
|