# # Story 016.009: 创建 React 图表组件封装 ## Status Approved ## Story **作为** 小程序开发者, **我想要** 创建现代 React 函数式组件封装 u-charts 核心库, **以便** 简化图表使用方式,无需手动管理 Canvas 上下文和事件处理,提高开发效率。 ## 背景 u-charts 原库需要手动管理 Canvas 上下文和事件处理,使用类组件的方式较为繁琐。现代小程序开发使用 React 函数式组件和 Hooks,需要创建易于使用的图表组件。 **参考实现**: `docs/小程序图表库示例/使用示例.md` 提供了完整的 React + Taro 使用示例,使用的是类组件方式。 **前置故事完成状态**: - ✅ 故事 016.001-016.008: u-charts 核心库已完成模块化搬迁和类型定义 - ✅ uCharts 类可通过 `@d8d/mini-charts` 导入使用 - ✅ 所有核心功能函数已模块化,可供组件导入 ## 前置故事完成状态 **故事 016.001**: ✅ 已完成 - 创建 mini-charts 包基础结构 **故事 016.002**: ✅ 已完成 - 模块化 config 和 utils **故事 016.003**: ✅ 已完成 - 模块化 data-processing 函数 **故事 016.004**: ✅ 已完成 - 模块化 charts-data 函数 **故事 016.005**: ✅ 已完成 - 模块化 renderers 函数 **故事 016.006**: ✅ 已完成 - 搬迁核心类并完成模块化 **故事 016.007**: ✅ 已完成 - 搬迁遗漏的辅助函数 **故事 016.008**: ✅ 已完成 - 搬迁核心绘制函数完成模块化 ## Acceptance Criteria 1. 创建 BaseChart 基础组件,封装 Canvas 创建和销毁逻辑 2. 至少实现5种图表组件(ColumnChart、LineChart、CandleChart、PieChart、RadarChart) 3. 组件支持 Props 配置,类型定义完整 4. 组件支持触摸事件处理(tooltip 显示) 5. 组件支持响应式尺寸计算和像素比适配 6. 类型定义完整,类型检查通过(`pnpm typecheck`) 7. 构建成功(`pnpm build`),自动生成 .d.ts 声明文件 ## Tasks / Subtasks - [ ] Task 1: 创建 BaseChart 基础组件 (AC: 1, 5, 6) - [ ] 1.1 创建 `src/components/BaseChart.tsx` 基础组件 - [ ] 1.2 实现 Canvas 创建和销毁逻辑(useEffect cleanup) - [ ] 1.3 实现响应式尺寸计算(useMemo + Taro.getSystemInfoSync) - [ ] 1.4 实现像素比适配逻辑 - [ ] 1.5 定义 BaseChartProps 接口(canvasId, width, height, data, config等) - [ ] 1.6 使用 useRef 管理 uCharts 实例 - [ ] Task 2: 创建 ColumnChart 柱状图组件 (AC: 2, 3, 4, 6) - [ ] 2.1 创建 `src/components/ColumnChart.tsx` - [ ] 2.2 定义 ColumnChartProps 接口 - [ ] 2.3 实现柱状图数据配置 - [ ] 2.4 实现 tooltip 事件处理(onTouchStart) - [ ] 2.5 添加类型注解 - [ ] Task 3: 创建 LineChart 折线图组件 (AC: 2, 3, 4, 6) - [ ] 3.1 创建 `src/components/LineChart.tsx` - [ ] 3.2 定义 LineChartProps 接口 - [ ] 3.3 实现折线图数据配置(支持 dataPointShape、enableScroll等) - [ ] 3.4 实现拖拽滚动事件处理(onTouchStart、onTouchMove、onTouchEnd) - [ ] 3.5 添加类型注解 - [ ] Task 4: 创建 CandleChart K线图组件 (AC: 2, 3, 4, 6) - [ ] 4.1 创建 `src/components/CandleChart.tsx` - [ ] 4.2 定义 CandleChartProps 接口 - [ ] 4.3 实现K线图数据配置(支持移动平均线 MA5/MA10/MA30) - [ ] 4.4 实现拖拽滚动和tooltip事件处理 - [ ] 4.5 添加类型注解 - [ ] Task 5: 创建 PieChart 饼图组件 (AC: 2, 3, 4, 6) - [ ] 5.1 创建 `src/components/PieChart.tsx` - [ ] 5.2 定义 PieChartProps 接口 - [ ] 5.3 实现饼图数据配置 - [ ] 5.4 实现tooltip事件处理 - [ ] 5.5 添加类型注解 - [ ] Task 6: 创建 RadarChart 雷达图组件 (AC: 2, 3, 4, 6) - [ ] 6.1 创建 `src/components/RadarChart.tsx` - [ ] 6.2 定义 RadarChartProps 接口 - [ ] 6.3 实现雷达图数据配置 - [ ] 6.4 实现tooltip事件处理 - [ ] 6.5 添加类型注解 - [ ] Task 7: 创建组件导出文件 (AC: 3, 6) - [ ] 7.1 创建 `src/components/index.ts` - [ ] 7.2 导出所有图表组件 - [ ] 7.3 导出所有Props类型定义 - [ ] Task 8: 更新 src/index.ts 主入口 (AC: 3, 6, 7) - [ ] 8.1 从 components 导出所有图表组件 - [ ] 8.2 导出组件Props类型定义 - [ ] 8.3 更新 package.json 的 exports 字段支持 components 导出 - [ ] Task 9: 验证组件实现 (AC: 6, 7) - [ ] 9.1 运行类型检查验证类型注解正确(`pnpm typecheck`) - [ ] 9.2 运行构建验证生成 .d.ts 文件(`pnpm build`) - [ ] 9.3 检查生成的 .d.ts 文件正确导出组件类型 - [ ] Task 10: 创建基础测试(可选) (AC: 3, 4) - [ ] 10.1 创建测试 Canvas mock - [ ] 10.2 为 BaseChart 创建基础渲染测试 - [ ] 10.3 为一个图表组件创建基础测试 ## Dev Notes ### 前置故事见解 **故事 016.001-016.008 完成状态总结**: - ✅ u-charts 核心库已完全模块化搬迁完成 - ✅ 所有函数都有完整的 TypeScript 类型注解 - ✅ `src/lib/charts/u-charts.ts` 导出 uCharts 主类 - ✅ `src/lib/charts/u-charts-event.ts` 导出 uChartsEvent 事件类 - ✅ 所有渲染函数、数据处理函数、辅助函数都已模块化 - ✅ 类型检查通过(`pnpm typecheck`) - ✅ 构建成功(`pnpm build`),自动生成 .d.ts 声明文件 - ✅ 原始 u-charts.ts 已备份为 u-charts.ts.backup **关键技术决策**: - 保持代码逻辑完全不变,只改变文件组织方式 - 只在搬迁过程中添加 TypeScript 类型注解,不修改代码的实现逻辑 - 使用 ES6 `export` 语法导出函数和类型 ### 技术栈要求 **来源**: [mini-charts/package.json](../../mini-ui-packages/mini-charts/package.json) - **React**: 18.0.0(mini 包使用 React 18,不是 React 19) - **Taro**: 4.1.4(包括 @tarojs/components, @tarojs/react, @tarojs/taro) - **TypeScript**: 5.4.5 - **Node.js**: 20.18.3(运行时环境) - **包管理器**: pnpm workspace **重要**: mini 包使用 **React 18**,不是主项目使用的 React 19。 ### 项目结构指南 **来源**: [source-tree.md](../../architecture/source-tree.md) ``` mini-ui-packages/ └── mini-charts/ # mini-charts 包 ├── src/ │ ├── index.ts # 主入口文件(需要更新) │ ├── components/ # [本故事创建] React 图表组件 │ │ ├── BaseChart.tsx # 基础图表组件 │ │ ├── ColumnChart.tsx # 柱状图组件 │ │ ├── LineChart.tsx # 折线图组件 │ │ ├── CandleChart.tsx # K线图组件 │ │ ├── PieChart.tsx # 饼图组件 │ │ ├── RadarChart.tsx # 雷达图组件 │ │ └── index.ts # 组件导出 │ └── lib/ # u-charts 核心库(已完成) │ ├── config.ts │ ├── utils/ │ ├── data-processing/ │ ├── charts-data/ │ ├── renderers/ │ ├── helper-functions/ │ ├── draw-controllers/ │ └── charts/ │ ├── u-charts-event.ts │ ├── u-charts.ts │ └── index.ts ├── tests/ # 测试目录 ├── package.json ├── tsconfig.json └── jest.config.cjs ``` ### TypeScript 配置规范 **来源**: [ui-package-standards.md](../../architecture/ui-package-standards.md#typescript配置) ```json { "compilerOptions": { "target": "ES2020", "lib": ["DOM", "DOM.Iterable", "ES2020"], "module": "ESNext", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true } } ``` **本故事重要**: 需要为所有组件Props接口添加完整的类型注解,确保 `strict: true` 模式下无类型错误。 ### uCharts 使用参考 **来源**: [docs/小程序图表库示例/使用示例.md](../../小程序图表库示例/使用示例.md) #### 原始类组件使用方式 ```jsx import React, { Component } from 'react'; import Taro from '@tarojs/taro'; import { View, Canvas } from '@tarojs/components'; import uCharts from '../../js_sdk/u-charts/u-charts.js'; export default class Index extends Component { constructor() { super(...arguments) this.state = { cWidth: '', cHeight: '', pixelRatio: 1, } } componentDidMount() { const sysInfo = Taro.getSystemInfoSync(); let pixelRatio = 1; if (Taro.getEnv() === Taro.ENV_TYPE.ALIPAY) { pixelRatio = sysInfo.pixelRatio; } const cWidth = pixelRatio * sysInfo.windowWidth; const cHeight = 500 / 750 * cWidth; this.setState({ cWidth, cHeight, pixelRatio }, () => this.getServerData()); } showColumn = (canvasId, chartData) => { const { cWidth, cHeight, pixelRatio } = this.state; let ctx = Taro.createCanvasContext(canvasId); canvaColumn = new uCharts({ type: 'column', context: ctx, legend: true, fontSize: 11, background: '#FFFFFF', pixelRatio, animation: true, categories: chartData.categories, series: chartData.series, xAxis: { disableGrid: true }, yAxis: {}, dataLabel: true, width: cWidth, height: cHeight, extra: { column: { type: 'group', width: cWidth * 0.45 / chartData.categories.length } } }); } touchColumn = (e) => { canvaColumn.showToolTip(e, { formatter: function (item, category) { return category + ' ' + item.name + ':' + item.data } }); } render() { const { cWidth, cHeight } = this.state; return ( ) } } ``` #### 关键API说明 | API | 说明 | |-----|------| | `Taro.getSystemInfoSync()` | 获取系统信息(屏幕宽度、像素比等) | | `Taro.createCanvasContext(canvasId)` | 创建Canvas上下文 | | `Taro.getEnv()` | 获取当前环境(微信/支付宝/H5等) | | `new uCharts(config)` | 创建图表实例 | | `chart.showToolTip(e, options)` | 显示tooltip | | `chart.scrollStart(e)` / `chart.scroll(e)` / `chart.scrollEnd(e)` | 滚动事件处理 | ### 组件设计模式 #### BaseChart 基础组件 ```typescript // src/components/BaseChart.tsx import React, { useEffect, useRef, useMemo } from 'react'; import Taro from '@tarojs/taro'; import { Canvas } from '@tarojs/components'; import { uCharts } from '../lib/charts'; export interface BaseChartProps { canvasId: string; width?: number; height?: number; pixelRatio?: number; type: string; categories: string[]; series: any[]; config?: Record; onTouchStart?: (e: any) => void; onTouchMove?: (e: any) => void; onTouchEnd?: (e: any) => void; } export const BaseChart: React.FC = (props) => { const { canvasId, width, height, pixelRatio, type, categories, series, config = {}, onTouchStart, onTouchMove, onTouchEnd, } = props; const chartRef = useRef(null); // 计算响应式尺寸 const { cWidth, cHeight, actualPixelRatio } = useMemo(() => { const sysInfo = Taro.getSystemInfoSync(); const pr = pixelRatio ?? (Taro.getEnv() === Taro.ENV_TYPE.ALIPAY ? sysInfo.pixelRatio : 1); const cw = width ?? pr * sysInfo.windowWidth; const ch = height ?? (500 / 750 * cw); return { cWidth: cw, cHeight: ch, actualPixelRatio: pr }; }, [width, height, pixelRatio]); // Canvas props const canvasProps = useMemo(() => { if (Taro.getEnv() === Taro.ENV_TYPE.ALIPAY) { return { width: cWidth, height: cHeight, style: { width: '100%', height: '100%' } }; } return { style: { width: `${cWidth}px`, height: `${cHeight}px` } }; }, [cWidth, cHeight]); // 初始化图表 useEffect(() => { const ctx = Taro.createCanvasContext(canvasId); chartRef.current = new uCharts({ type, context: ctx, categories, series, width: cWidth, height: cHeight, pixelRatio: actualPixelRatio, ...config, }); return () => { // 清理图表实例 chartRef.current = null; }; }, [canvasId, type, categories, series, cWidth, cHeight, actualPixelRatio, config]); // 事件处理 const handleTouchStart = (e: any) => { onTouchStart?.(e); }; const handleTouchMove = (e: any) => { onTouchMove?.(e); }; const handleTouchEnd = (e: any) => { onTouchEnd?.(e); }; return ( ); }; export default BaseChart; ``` #### 具体图表组件示例 ```typescript // src/components/ColumnChart.tsx import React from 'react'; import { BaseChart, BaseChartProps } from './BaseChart'; export interface ColumnChartProps extends Omit { // 柱状图特有配置 dataLabel?: boolean; columnType?: 'group' | 'stack'; } export const ColumnChart: React.FC = (props) => { const { dataLabel = true, columnType = 'group', ...baseProps } = props; const defaultConfig = { legend: true, fontSize: 11, background: '#FFFFFF', animation: true, dataLabel, xAxis: { disableGrid: true }, yAxis: {}, extra: { column: { type: columnType, // width 会在组件内部动态计算 } } }; // tooltip 事件处理 const handleTouchStart = (e: any) => { if (baseProps.onTouchStart) { baseProps.onTouchStart(e); } // 使用 chartRef 调用 showToolTip }; return ( ); }; export default ColumnChart; ``` ### 类型定义规范 **来源**: [ui-package-standards.md](../../architecture/ui-package-standards.md#类型定义规范) **类型定义原则**: 1. 为所有组件Props定义 TypeScript 接口 2. 导出所有公共类型定义 3. 使用泛型增强类型安全性 4. 避免使用 `any` 类型(除非必要) ### 编码标准 **来源**: [coding-standards.md](../../architecture/coding-standards.md) - **代码风格**: TypeScript 严格模式,一致的缩进和命名 - **组件类型**: 使用 React 函数式组件,使用 `React.FC` 类型 - **Hooks**: 使用 useEffect、useRef、useMemo 等 Hooks 管理状态和副作用 - **事件处理**: 使用箭头函数或 useCallback 处理事件 - **导出规范**: 使用 ES6 `export` 语法导出组件和类型 ### 环境配置 **来源**: [CLAUDE.md](../../CLAUDE.md) - **Node.js**: 20.19.2 - **包管理器**: pnpm - **测试框架**: Jest(mini 包使用 Jest,不是 Vitest) - **类型检查**: 使用 `pnpm typecheck` 验证类型 - **构建**: 使用 `pnpm build` 生成 .d.ts 声明文件 ### 文件位置规范 所有新组件应创建在: - 组件目录: `mini-ui-packages/mini-charts/src/components/` - 组件导出: `mini-ui-packages/mini-charts/src/components/index.ts` - 主入口: `mini-ui-packages/mini-charts/src/index.ts`(需要更新) ### 技术约束 1. **React版本**: 必须使用 React 18 函数式组件,不能使用类组件 2. **Taro版本**: 使用 @tarojs/taro 4.1.4 API 3. **类型安全**: 避免使用 `any` 类型(除非必要) 4. **Canvas上下文**: 使用 Taro.createCanvasContext 创建上下文 5. **事件处理**: 正确处理触摸事件,支持 tooltip 和滚动 6. **响应式设计**: 支持不同屏幕尺寸和像素比 7. **清理逻辑**: useEffect cleanup 函数必须清理图表实例 ### 验证标准 完成本故事后,应该满足: 1. ✅ `src/components/` 目录下所有文件存在 2. ✅ BaseChart 和5种图表组件有完整类型注解 3. ✅ `src/index.ts` 正确导出所有组件 4. ✅ 运行 `pnpm typecheck` 无类型错误 5. ✅ 运行 `pnpm build` 成功,自动生成 .d.ts 声明文件 6. ✅ 检查生成的 .d.ts 文件正确导出组件类型 7. ✅ 组件可以被其他 mini 包引用使用 ### 不包含在本故事中的工作 以下工作**不在**本故事范围内: - ❌ 创建使用示例和文档(故事 016.010) - ❌ 创建完整的测试套件(故事 016.011) - ❌ 在实际小程序页面中集成使用 ## Testing **测试框架**: Jest(mini 包使用 Jest,不是 Vitest) **测试要求**: 1. 创建基础测试验证组件的正确导入和导出 2. 测试组件渲染(可选) 3. 测试 Props 传递(可选) 4. 测试事件处理(可选) 5. Mock Taro 和 Canvas 相关API **测试命令**: ```bash # 运行所有测试 pnpm test # 运行特定测试 pnpm test --testNamePattern "图表组件" ``` **测试文件位置**: `tests/components/` ## Change Log | Date | Version | Description | Author | |------|---------|-------------|--------| | 2025-12-24 | 1.0 | 创建故事文档 | Bob (Scrum Master) | | 2025-12-24 | 1.1 | 更新状态为 Approved | Bob (Scrum Master) | ## Dev Agent Record *此部分由开发代理在实施过程中填写* ### Agent Model Used ### Debug Log References ### Completion Notes List ### File List ## QA Results *此部分由 QA 代理在审查完成后填写*