|
@@ -1,22 +1,21 @@
|
|
|
-import React, { useEffect, useRef, useMemo } from 'react';
|
|
|
|
|
|
|
+import React, { useState, useRef, useLayoutEffect, useMemo } from 'react';
|
|
|
import Taro from '@tarojs/taro';
|
|
import Taro from '@tarojs/taro';
|
|
|
import { Canvas } from '@tarojs/components';
|
|
import { Canvas } from '@tarojs/components';
|
|
|
-import { uCharts } from '../lib/charts/index';
|
|
|
|
|
-import type { ChartsConfig, TouchEvent } from '../lib/charts/index';
|
|
|
|
|
|
|
+import uChartsClass from '../lib/u-charts-original.js';
|
|
|
|
|
+import type { ChartsConfig, TouchEvent } from '../lib/u-charts-original';
|
|
|
import type { ExtendedCanvasContext } from '../types';
|
|
import type { ExtendedCanvasContext } from '../types';
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* BaseChart 组件的 Props 接口
|
|
* BaseChart 组件的 Props 接口
|
|
|
|
|
+ * 使用原始 u-charts.js + Canvas 2D API
|
|
|
*/
|
|
*/
|
|
|
export interface BaseChartProps {
|
|
export interface BaseChartProps {
|
|
|
/** Canvas 元素的 ID,必须唯一 */
|
|
/** Canvas 元素的 ID,必须唯一 */
|
|
|
canvasId: string;
|
|
canvasId: string;
|
|
|
- /** 图表宽度(像素),默认为屏幕宽度 */
|
|
|
|
|
|
|
+ /** 图表宽度(像素),默认为 750 */
|
|
|
width?: number;
|
|
width?: number;
|
|
|
- /** 图表高度(像素),默认根据宽高比计算 */
|
|
|
|
|
|
|
+ /** 图表高度(像素),默认为 500 */
|
|
|
height?: number;
|
|
height?: number;
|
|
|
- /** 设备像素比,默认根据环境自动设置 */
|
|
|
|
|
- pixelRatio?: number;
|
|
|
|
|
/** 图表类型 */
|
|
/** 图表类型 */
|
|
|
type: ChartsConfig['type'];
|
|
type: ChartsConfig['type'];
|
|
|
/** X 轴分类数据 */
|
|
/** X 轴分类数据 */
|
|
@@ -34,16 +33,16 @@ export interface BaseChartProps {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * BaseChart 基础图表组件
|
|
|
|
|
|
|
+ * BaseChartInner 内部组件
|
|
|
*
|
|
*
|
|
|
- * 封装 Canvas 创建和销毁逻辑,提供响应式尺寸计算和像素比适配
|
|
|
|
|
|
|
+ * 实际的图表渲染组件,只在数据准备好后才会挂载
|
|
|
|
|
+ * 使用空依赖数组确保只初始化一次
|
|
|
*/
|
|
*/
|
|
|
-export const BaseChart: React.FC<BaseChartProps> = (props) => {
|
|
|
|
|
|
|
+const BaseChartInner: React.FC<BaseChartProps> = (props) => {
|
|
|
const {
|
|
const {
|
|
|
canvasId,
|
|
canvasId,
|
|
|
- width,
|
|
|
|
|
- height,
|
|
|
|
|
- pixelRatio,
|
|
|
|
|
|
|
+ width = 750,
|
|
|
|
|
+ height = 500,
|
|
|
type,
|
|
type,
|
|
|
categories = [],
|
|
categories = [],
|
|
|
series = [],
|
|
series = [],
|
|
@@ -53,109 +52,138 @@ export const BaseChart: React.FC<BaseChartProps> = (props) => {
|
|
|
onTouchEnd,
|
|
onTouchEnd,
|
|
|
} = props;
|
|
} = props;
|
|
|
|
|
|
|
|
- const chartRef = useRef<uCharts | null>(null);
|
|
|
|
|
|
|
+ const [cWidth, setCWidth] = useState(750);
|
|
|
|
|
+ const [cHeight, setCHeight] = useState(500);
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 计算响应式尺寸和像素比
|
|
|
|
|
- */
|
|
|
|
|
- const { cWidth, cHeight, actualPixelRatio } = useMemo(() => {
|
|
|
|
|
- const sysInfo = Taro.getSystemInfoSync();
|
|
|
|
|
- // Canvas 2D 需要使用实际的 pixelRatio 来匹配设备像素
|
|
|
|
|
- // 这样绘制的内容才不会模糊或被放大
|
|
|
|
|
- const pr = pixelRatio ?? sysInfo.pixelRatio;
|
|
|
|
|
- // width 和 height 是逻辑像素(CSS 像素)
|
|
|
|
|
- // Canvas 2D 的实际像素尺寸 = 逻辑像素 * pixelRatio
|
|
|
|
|
- const cw = width ?? pr * sysInfo.windowWidth;
|
|
|
|
|
- const ch = height ?? (500 / 750 * cw);
|
|
|
|
|
- return { cWidth: cw, cHeight: ch, actualPixelRatio: pr };
|
|
|
|
|
- }, [width, height, pixelRatio]);
|
|
|
|
|
|
|
+ const chartRef = useRef<any>(null);
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Canvas props 根据 platform 动态计算
|
|
|
|
|
|
|
+ * 初始化图表实例
|
|
|
|
|
+ * 使用 Canvas 2D API + 原始 u-charts.js
|
|
|
|
|
+ * 参考 ColumnChartFCExample 的实现方式
|
|
|
|
|
+ *
|
|
|
|
|
+ * 注意:使用空依赖数组,只在组件首次挂载时执行一次
|
|
|
|
|
+ * 数据变化通过 Wrapper 组件控制重新挂载来实现
|
|
|
*/
|
|
*/
|
|
|
- const canvasProps = useMemo(() => {
|
|
|
|
|
- if (Taro.getEnv() === Taro.ENV_TYPE.ALIPAY) {
|
|
|
|
|
- return {
|
|
|
|
|
- width: String(cWidth),
|
|
|
|
|
- height: String(cHeight),
|
|
|
|
|
- style: { width: '100%', height: '100%' }
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ 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);
|
|
|
|
|
+
|
|
|
|
|
+ // 确保数据已准备好:categories 和 series 都有数据
|
|
|
|
|
+ const isDataReady = categories.length > 0 && series.length > 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (!isDataReady) {
|
|
|
|
|
+ console.log('[BaseChart] 数据未准备好,等待数据...', {
|
|
|
|
|
+ canvasId,
|
|
|
|
|
+ categoriesLength: categories.length,
|
|
|
|
|
+ seriesLength: series.length
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
- // Canvas 2D API:
|
|
|
|
|
- // - width/height 属性:实际像素尺寸(逻辑像素 * pixelRatio),需要转为字符串
|
|
|
|
|
- // - style.width/style.height:CSS 显示尺寸(逻辑像素)
|
|
|
|
|
- return {
|
|
|
|
|
- width: String(cWidth * actualPixelRatio),
|
|
|
|
|
- height: String(cHeight * actualPixelRatio),
|
|
|
|
|
- style: { width: `${cWidth}px`, height: `${cHeight}px` }
|
|
|
|
|
- };
|
|
|
|
|
- }, [cWidth, cHeight, actualPixelRatio]);
|
|
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 初始化图表实例
|
|
|
|
|
- */
|
|
|
|
|
- useEffect(() => {
|
|
|
|
|
- // 使用 setTimeout 确保 Canvas DOM 元素已渲染完成
|
|
|
|
|
- const timer = setTimeout(() => {
|
|
|
|
|
- const rawCtx = Taro.createCanvasContext(canvasId);
|
|
|
|
|
-
|
|
|
|
|
- // 检查 canvas 上下文是否有效
|
|
|
|
|
- if (!rawCtx) {
|
|
|
|
|
- console.error('[BaseChart] CanvasContext创建失败!')
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 将 Taro CanvasContext 转换为 uCharts 需要的 CanvasContext
|
|
|
|
|
- // 注意:不能使用展开运算符,因为这会复制方法但丢失 this 指向
|
|
|
|
|
- const ctx = rawCtx as ExtendedCanvasContext;
|
|
|
|
|
-
|
|
|
|
|
- // Canvas 2D: 传入 uCharts 的 width/height 需要乘以 pixelRatio
|
|
|
|
|
- // 因为原始 u-charts.js 的绘制坐标基于这些值计算
|
|
|
|
|
- const chartConfig: ChartsConfig = {
|
|
|
|
|
- type,
|
|
|
|
|
- context: ctx,
|
|
|
|
|
- categories,
|
|
|
|
|
- series,
|
|
|
|
|
- width: cWidth * actualPixelRatio,
|
|
|
|
|
- height: cHeight * actualPixelRatio,
|
|
|
|
|
- pixelRatio: actualPixelRatio,
|
|
|
|
|
- ...config,
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- chartRef.current = new uCharts(chartConfig);
|
|
|
|
|
- }, 0)
|
|
|
|
|
-
|
|
|
|
|
- return () => {
|
|
|
|
|
- // 清理定时器和图表实例
|
|
|
|
|
- clearTimeout(timer)
|
|
|
|
|
- chartRef.current = null;
|
|
|
|
|
- };
|
|
|
|
|
- }, [canvasId, type, categories, series, cWidth, cHeight, actualPixelRatio, config]);
|
|
|
|
|
|
|
+ // 延迟初始化图表,等待 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 }]
|
|
|
|
|
+ },
|
|
|
|
|
+ extra: {
|
|
|
|
|
+ column: {
|
|
|
|
|
+ type: 'group',
|
|
|
|
|
+ width: 30,
|
|
|
|
|
+ activeBgColor: '#000000',
|
|
|
|
|
+ activeBgOpacity: 0.08
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ ...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) => {
|
|
const handleTouchStart = (e: any) => {
|
|
|
|
|
+ if (chartRef.current) {
|
|
|
|
|
+ chartRef.current.touchLegend(e);
|
|
|
|
|
+ chartRef.current.showToolTip(e);
|
|
|
|
|
+ }
|
|
|
onTouchStart?.(e as TouchEvent);
|
|
onTouchStart?.(e as TouchEvent);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 触摸移动事件处理
|
|
|
|
|
- */
|
|
|
|
|
const handleTouchMove = (e: any) => {
|
|
const handleTouchMove = (e: any) => {
|
|
|
|
|
+ if (chartRef.current) {
|
|
|
|
|
+ chartRef.current.scroll(e);
|
|
|
|
|
+ }
|
|
|
onTouchMove?.(e as TouchEvent);
|
|
onTouchMove?.(e as TouchEvent);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 触摸结束事件处理
|
|
|
|
|
- */
|
|
|
|
|
const handleTouchEnd = (e: any) => {
|
|
const handleTouchEnd = (e: any) => {
|
|
|
|
|
+ if (chartRef.current) {
|
|
|
|
|
+ chartRef.current.touchLegend(e);
|
|
|
|
|
+ chartRef.current.showToolTip(e);
|
|
|
|
|
+ }
|
|
|
onTouchEnd?.(e as TouchEvent);
|
|
onTouchEnd?.(e as TouchEvent);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const canvasProps = { style: { width: cWidth, height: cHeight } };
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<Canvas
|
|
<Canvas
|
|
|
- canvasId={canvasId}
|
|
|
|
|
|
|
+ canvas-id={canvasId}
|
|
|
id={canvasId}
|
|
id={canvasId}
|
|
|
{...canvasProps}
|
|
{...canvasProps}
|
|
|
onTouchStart={handleTouchStart}
|
|
onTouchStart={handleTouchStart}
|
|
@@ -166,4 +194,55 @@ export const BaseChart: React.FC<BaseChartProps> = (props) => {
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * BaseChart 组件
|
|
|
|
|
+ *
|
|
|
|
|
+ * 外层 Wrapper 组件,负责:
|
|
|
|
|
+ * - 检查数据是否准备好
|
|
|
|
|
+ * - 缓存 config 引用
|
|
|
|
|
+ * - 只有数据准备好后才挂载 Inner 组件
|
|
|
|
|
+ *
|
|
|
|
|
+ * 使用原始 u-charts.js + Canvas 2D API
|
|
|
|
|
+ * 参考 docs/小程序图表库示例/taro-2d柱状图使用示例.md
|
|
|
|
|
+ */
|
|
|
|
|
+export const BaseChart: React.FC<BaseChartProps> = (props) => {
|
|
|
|
|
+ const {
|
|
|
|
|
+ canvasId,
|
|
|
|
|
+ width,
|
|
|
|
|
+ height,
|
|
|
|
|
+ type,
|
|
|
|
|
+ categories = [],
|
|
|
|
|
+ series = [],
|
|
|
|
|
+ config = {},
|
|
|
|
|
+ onTouchStart,
|
|
|
|
|
+ onTouchMove,
|
|
|
|
|
+ onTouchEnd,
|
|
|
|
|
+ } = props;
|
|
|
|
|
+
|
|
|
|
|
+ // 缓存配置,避免每次渲染创建新对象
|
|
|
|
|
+ const stableConfig = useMemo(() => config, [JSON.stringify(config)]);
|
|
|
|
|
+
|
|
|
|
|
+ // 只有数据准备好才渲染 Inner 组件
|
|
|
|
|
+ const isReady = categories.length > 0 && series.length > 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (!isReady) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <BaseChartInner
|
|
|
|
|
+ canvasId={canvasId}
|
|
|
|
|
+ width={width}
|
|
|
|
|
+ height={height}
|
|
|
|
|
+ type={type}
|
|
|
|
|
+ categories={categories}
|
|
|
|
|
+ series={series}
|
|
|
|
|
+ config={stableConfig}
|
|
|
|
|
+ onTouchStart={onTouchStart}
|
|
|
|
|
+ onTouchMove={onTouchMove}
|
|
|
|
|
+ onTouchEnd={onTouchEnd}
|
|
|
|
|
+ />
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
export default BaseChart;
|
|
export default BaseChart;
|