BaseChart.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import React, { useEffect, useRef, useMemo } from 'react';
  2. import Taro from '@tarojs/taro';
  3. import { Canvas } from '@tarojs/components';
  4. import { uCharts } from '../lib/charts/index';
  5. import type { ChartsConfig, TouchEvent } from '../lib/charts/index';
  6. import type { ExtendedCanvasContext } from '../types';
  7. /**
  8. * BaseChart 组件的 Props 接口
  9. */
  10. export interface BaseChartProps {
  11. /** Canvas 元素的 ID,必须唯一 */
  12. canvasId: string;
  13. /** 图表宽度(像素),默认为屏幕宽度 */
  14. width?: number;
  15. /** 图表高度(像素),默认根据宽高比计算 */
  16. height?: number;
  17. /** 设备像素比,默认根据环境自动设置 */
  18. pixelRatio?: number;
  19. /** 图表类型 */
  20. type: ChartsConfig['type'];
  21. /** X 轴分类数据 */
  22. categories?: string[];
  23. /** 系列数据 */
  24. series?: ChartsConfig['series'];
  25. /** 额外的图表配置 */
  26. config?: Partial<ChartsConfig>;
  27. /** 触摸开始事件 */
  28. onTouchStart?: (e: TouchEvent) => void;
  29. /** 触摸移动事件 */
  30. onTouchMove?: (e: TouchEvent) => void;
  31. /** 触摸结束事件 */
  32. onTouchEnd?: (e: TouchEvent) => void;
  33. }
  34. /**
  35. * BaseChart 基础图表组件
  36. *
  37. * 封装 Canvas 创建和销毁逻辑,提供响应式尺寸计算和像素比适配
  38. */
  39. export const BaseChart: React.FC<BaseChartProps> = (props) => {
  40. const {
  41. canvasId,
  42. width,
  43. height,
  44. pixelRatio,
  45. type,
  46. categories = [],
  47. series = [],
  48. config = {},
  49. onTouchStart,
  50. onTouchMove,
  51. onTouchEnd,
  52. } = props;
  53. const chartRef = useRef<uCharts | null>(null);
  54. /**
  55. * 计算响应式尺寸和像素比
  56. */
  57. const { cWidth, cHeight, actualPixelRatio } = useMemo(() => {
  58. const sysInfo = Taro.getSystemInfoSync();
  59. // 支付宝小程序需要使用实际的 pixelRatio,其他平台使用 1
  60. const pr = pixelRatio ?? (Taro.getEnv() === Taro.ENV_TYPE.ALIPAY ? sysInfo.pixelRatio : 1);
  61. const cw = width ?? pr * sysInfo.windowWidth;
  62. const ch = height ?? (500 / 750 * cw);
  63. return { cWidth: cw, cHeight: ch, actualPixelRatio: pr };
  64. }, [width, height, pixelRatio]);
  65. /**
  66. * Canvas props 根据 platform 动态计算
  67. */
  68. const canvasProps = useMemo(() => {
  69. if (Taro.getEnv() === Taro.ENV_TYPE.ALIPAY) {
  70. return {
  71. width: String(cWidth),
  72. height: String(cHeight),
  73. style: { width: '100%', height: '100%' }
  74. };
  75. }
  76. return {
  77. style: { width: `${cWidth}px`, height: `${cHeight}px` }
  78. };
  79. }, [cWidth, cHeight]);
  80. /**
  81. * 初始化图表实例
  82. */
  83. useEffect(() => {
  84. // 使用 setTimeout 确保 Canvas DOM 元素已渲染完成
  85. const timer = setTimeout(() => {
  86. const rawCtx = Taro.createCanvasContext(canvasId);
  87. // 检查 canvas 上下文是否有效
  88. if (!rawCtx) {
  89. console.error('[BaseChart] CanvasContext创建失败!')
  90. return
  91. }
  92. // 将 Taro CanvasContext 转换为 uCharts 需要的 CanvasContext
  93. // 注意:不能使用展开运算符,因为这会复制方法但丢失 this 指向
  94. const ctx = rawCtx as ExtendedCanvasContext;
  95. // 添加 uCharts 需要的 width 和 height 属性
  96. ctx.width = cWidth;
  97. ctx.height = cHeight;
  98. const chartConfig: ChartsConfig = {
  99. type,
  100. context: ctx,
  101. categories,
  102. series,
  103. width: cWidth,
  104. height: cHeight,
  105. pixelRatio: actualPixelRatio,
  106. ...config,
  107. };
  108. chartRef.current = new uCharts(chartConfig);
  109. }, 0)
  110. return () => {
  111. // 清理定时器和图表实例
  112. clearTimeout(timer)
  113. chartRef.current = null;
  114. };
  115. }, [canvasId, type, categories, series, cWidth, cHeight, actualPixelRatio, config]);
  116. /**
  117. * 触摸开始事件处理
  118. */
  119. const handleTouchStart = (e: any) => {
  120. onTouchStart?.(e as TouchEvent);
  121. };
  122. /**
  123. * 触摸移动事件处理
  124. */
  125. const handleTouchMove = (e: any) => {
  126. onTouchMove?.(e as TouchEvent);
  127. };
  128. /**
  129. * 触摸结束事件处理
  130. */
  131. const handleTouchEnd = (e: any) => {
  132. onTouchEnd?.(e as TouchEvent);
  133. };
  134. return (
  135. <Canvas
  136. canvasId={canvasId}
  137. id={canvasId}
  138. {...canvasProps}
  139. onTouchStart={handleTouchStart}
  140. onTouchMove={handleTouchMove}
  141. onTouchEnd={handleTouchEnd}
  142. type="2d"
  143. />
  144. );
  145. };
  146. export default BaseChart;