import React, { useState, useRef, useLayoutEffect, useMemo } from 'react'; import Taro from '@tarojs/taro'; import { Canvas } from '@tarojs/components'; import uChartsClass from '../lib/u-charts-original'; import type { ChartsConfig, TouchEvent } from '../types/u-charts-original'; import type { ExtendedCanvasContext } from '../types'; /** * BaseChart 组件的 Props 接口 * 使用原始 u-charts.js + Canvas 2D API */ export interface BaseChartProps { /** Canvas 元素的 ID,必须唯一 */ canvasId: string; /** 图表宽度(像素),默认为 750 */ width?: number; /** 图表高度(像素),默认为 500 */ height?: number; /** 图表类型 */ type: ChartsConfig['type']; /** X 轴分类数据 */ categories?: string[]; /** 系列数据 */ series?: ChartsConfig['series']; /** 额外的图表配置 */ config?: Partial; /** 触摸开始事件 */ onTouchStart?: (e: TouchEvent) => void; /** 触摸移动事件 */ onTouchMove?: (e: TouchEvent) => void; /** 触摸结束事件 */ onTouchEnd?: (e: TouchEvent) => void; } /** * BaseChartInner 内部组件 * * 实际的图表渲染组件,只在数据准备好后才会挂载 * 使用空依赖数组确保只初始化一次 */ const BaseChartInner: React.FC = (props) => { const { canvasId, width = 750, height = 500, type, categories = [], series = [], config = {}, onTouchStart, onTouchMove, onTouchEnd, } = props; const [cWidth, setCWidth] = useState(750); const [cHeight, setCHeight] = useState(500); const chartRef = useRef(null); /** * 初始化图表实例 * 使用 Canvas 2D API + 原始 u-charts.js * 参考 ColumnChartFCExample 的实现方式 * * 注意:使用空依赖数组,只在组件首次挂载时执行一次 * 数据变化通过 Wrapper 组件控制重新挂载来实现 */ useLayoutEffect(() => { console.debug('[BaseChart] useLayoutEffect 开始', { canvasId, width, height }); // 计算响应式尺寸 const sysInfo = Taro.getSystemInfoSync(); // 这里的第一个 750 对应 css .charts 的 width const cw = width / 750 * sysInfo.windowWidth; // 这里的 500 对应 css .charts 的 height const ch = height / 750 * sysInfo.windowWidth; setCWidth(cw); setCHeight(ch); // 确保数据已准备好 // 对于饼图(pie/ring),只需要 series 有数据 // 对于其他图表,需要 categories 和 series 都有数据 const isPieChart = type === 'pie' || type === 'ring'; const isDataReady = isPieChart ? series.length > 0 : categories.length > 0 && series.length > 0; if (!isDataReady) { console.log('[BaseChart] 数据未准备好,等待数据...', { canvasId, type, isPieChart, categoriesLength: categories.length, seriesLength: series.length }); return; } // 延迟初始化图表,等待 Canvas 尺寸渲染完成(参考 FC 示例) setTimeout(() => { // 使用 Canvas 2D API 的方式获取 context const query = Taro.createSelectorQuery(); query.select('#' + canvasId).fields({ node: true, size: true }).exec((res) => { if (res[0]) { const canvas = res[0].node; const ctx = canvas.getContext('2d'); console.debug('[BaseChartOriginal2D] canvas.width', canvas.width); console.debug('[BaseChartOriginal2D] canvas.height', canvas.height); // 将 Taro CanvasContext 转换为 uCharts 需要的 CanvasContext const extendedCtx = ctx as ExtendedCanvasContext; // Canvas 2D: 使用 canvas 的实际 width/height // 基础配置 const chartConfig: ChartsConfig = { type, context: extendedCtx, categories, series, width: canvas.width, height: canvas.height, animation: true, background: '#FFFFFF', color: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ef4444'], padding: [15, 15, 0, 5], enableScroll: false, legend: {}, xAxis: { disableGrid: true }, yAxis: { data: [{ min: 0 }] }, ...config, }; chartRef.current = new uChartsClass(chartConfig); console.log('[BaseChart] 图表初始化完成:', canvasId, { cWidth, cHeight, canvasWidth: canvas.width, canvasHeight: canvas.height, categoriesLength: categories.length, seriesLength: series.length, categories, series, }); } else { console.error('[BaseChart] 未获取到 canvas node:', canvasId); } }); }, 500); // 延迟 500ms,等待 Canvas 尺寸渲染完成 }, []); // 空依赖数组:只在首次挂载时执行一次 /** * 触摸事件处理 */ const handleTouchStart = (e: any) => { if (chartRef.current) { chartRef.current.touchLegend(e); chartRef.current.showToolTip(e); } onTouchStart?.(e as TouchEvent); }; const handleTouchMove = (e: any) => { if (chartRef.current) { chartRef.current.scroll(e); } onTouchMove?.(e as TouchEvent); }; const handleTouchEnd = (e: any) => { if (chartRef.current) { chartRef.current.touchLegend(e); chartRef.current.showToolTip(e); } onTouchEnd?.(e as TouchEvent); }; const canvasProps = { style: { width: cWidth, height: cHeight } }; return ( ); }; /** * BaseChart 组件 * * 外层 Wrapper 组件,负责: * - 检查数据是否准备好 * - 缓存 config 引用 * - 只有数据准备好后才挂载 Inner 组件 * * 使用原始 u-charts.js + Canvas 2D API * 参考 docs/小程序图表库示例/taro-2d柱状图使用示例.md */ export const BaseChart: React.FC = (props) => { const { canvasId, width, height, type, categories = [], series = [], config = {}, onTouchStart, onTouchMove, onTouchEnd, } = props; // 缓存配置,避免每次渲染创建新对象 const stableConfig = useMemo(() => config, [JSON.stringify(config)]); // 只有数据准备好才渲染 Inner 组件 // 对于饼图(pie/ring),只需要 series 有数据 // 对于其他图表,需要 categories 和 series 都有数据 const isPieChart = type === 'pie' || type === 'ring'; const isReady = isPieChart ? series.length > 0 : categories.length > 0 && series.length > 0; if (!isReady) { return null; } return ( ); }; export default BaseChart;