427 lines
18 KiB
Markdown
427 lines
18 KiB
Markdown
# 资产消耗图表 (LifePlanChart)
|
||
|
||
<cite>
|
||
**本文引用的文件**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx)
|
||
- [types.ts](file://src/types.ts)
|
||
- [render-props-types.ts](file://frontend/src/render-props-types.ts)
|
||
- [mock-data.json](file://src/mock-data.json)
|
||
- [Root.tsx](file://src/Root.tsx)
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx)
|
||
</cite>
|
||
|
||
## 目录
|
||
1. [简介](#简介)
|
||
2. [项目结构](#项目结构)
|
||
3. [核心组件](#核心组件)
|
||
4. [架构总览](#架构总览)
|
||
5. [详细组件分析](#详细组件分析)
|
||
6. [依赖关系分析](#依赖关系分析)
|
||
7. [性能考量](#性能考量)
|
||
8. [故障排查指南](#故障排查指南)
|
||
9. [结论](#结论)
|
||
10. [附录](#附录)
|
||
|
||
## 简介
|
||
本文件围绕 LifePlanChart 组件展开,系统性阐述其作为“资产消耗趋势可视化图表”的实现方式。组件基于 Recharts 的 AreaChart 展示用户生命周期内资产余额的变化,结合 Remotion 的 useCurrentFrame 和 interpolate 实现逐帧动画的数据流;通过自定义 X 轴刻度 CustomXAxisTick 渲染“年份+主被保险人年龄+配偶年龄”的三行标签;采用渐变填充策略(蓝色代表正资产、红色代表负资产)直观反映资产状态;HUD 区域实时显示当前帧对应的年份、资产余额、收入与支出;并通过数据插值算法实现平滑的逐帧动画效果。文档还给出与 render_props.json 数据结构的映射关系示例,说明 YearlySnapshot 类型如何驱动图表渲染,并提供面向初学者的使用示例与面向高级开发者的性能优化建议。
|
||
|
||
## 项目结构
|
||
本项目采用前端 Remotion 视频渲染管线,LifePlanChart 位于 src 目录下,配合 Root.tsx 注册为 Composition,mock-data.json 提供演示数据,frontend/src/render-props-types.ts 定义了后端生成的 render_props.json 的类型契约。
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph "前端渲染层"
|
||
Root["Root.tsx<br/>注册 Composition"]
|
||
LifePlan["LifePlanChart.tsx<br/>资产消耗趋势图"]
|
||
Dual["DualChartView.tsx<br/>双图联动视图"]
|
||
end
|
||
subgraph "数据层"
|
||
Mock["mock-data.json<br/>演示数据"]
|
||
Types["types.ts<br/>YearlySnapshot 接口"]
|
||
RenderTypes["render-props-types.ts<br/>render_props.json 类型契约"]
|
||
end
|
||
Root --> LifePlan
|
||
Root --> Dual
|
||
LifePlan --> Types
|
||
Dual --> LifePlan
|
||
LifePlan -. 使用 .-> Mock
|
||
Dual -. 使用 .-> RenderTypes
|
||
```
|
||
|
||
**图表来源**
|
||
- [Root.tsx](file://src/Root.tsx#L1-L68)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L1-L349)
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L1-L176)
|
||
- [types.ts](file://src/types.ts#L1-L30)
|
||
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L1-L251)
|
||
- [mock-data.json](file://src/mock-data.json#L1-L61)
|
||
|
||
**章节来源**
|
||
- [Root.tsx](file://src/Root.tsx#L1-L68)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L1-L349)
|
||
- [types.ts](file://src/types.ts#L1-L30)
|
||
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L1-L251)
|
||
- [mock-data.json](file://src/mock-data.json#L1-L61)
|
||
|
||
## 核心组件
|
||
- 组件名称:LifePlanChart
|
||
- 功能定位:资产余额随生命周期变化的趋势可视化,支持 HUD 实时信息与平滑动画
|
||
- 关键特性:
|
||
- 基于 Recharts 的 AreaChart
|
||
- 自定义 X 轴三行标签(年份、主被保险人年龄、配偶年龄)
|
||
- 渐变填充(正资产蓝、负资产红)
|
||
- 基于 Remotion 的逐帧插值动画
|
||
- HUD 实时显示年份、资产余额、收入与支出
|
||
|
||
**章节来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L1-L349)
|
||
- [types.ts](file://src/types.ts#L1-L30)
|
||
|
||
## 架构总览
|
||
LifePlanChart 在 Remotion 中作为 Composition 的子组件运行,通过 useCurrentFrame 获取当前帧号,用 interpolate 将帧号映射到 timeline 的索引区间,从而计算出可见数据片段与插值点,实现“从左至右”推进的平滑动画。HUD 区域根据当前数据点实时更新;Y 轴域基于全量 timeline 计算,避免动画过程中因极值变化导致的跳变;X 轴域在短序列时固定范围,保证标签可读性。
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant R as "Remotion 播放器"
|
||
participant C as "LifePlanChart"
|
||
participant RC as "Recharts 图表"
|
||
participant U as "HUD 区域"
|
||
R->>C : "useCurrentFrame() 获取当前帧"
|
||
C->>C : "interpolate(帧号) => exactIndex"
|
||
C->>C : "计算 dataEndIndex 与 fraction"
|
||
C->>C : "构建 visibleData含插值点"
|
||
C->>RC : "传入 visibleData、渐变配置、Y/X 轴域"
|
||
RC-->>U : "渲染图表与 HUD"
|
||
U-->>R : "实时显示年份/资产余额/收入/支出"
|
||
```
|
||
|
||
**图表来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L70-L141)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L175-L210)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L292-L341)
|
||
|
||
## 详细组件分析
|
||
|
||
### 数据模型与类型映射
|
||
- YearlySnapshot:单年度数据结构,包含年份、年龄(主被保险人与可选配偶)、财务指标(收入、支出、资产余额),以及可选事件与关键标记。
|
||
- LifePlanChartProps:组件属性,包括 timeline、meta(货币单位、起始年份)、是否隐藏头部、外部传入的索引与进度,用于与上层联动或外部控制动画。
|
||
- render_props.json 映射:后端生成的 render_props.json 中的 charts.assetDepletion.timeline 字段与 YearlySnapshot 结构一致,可直接作为 defaultProps 传入 LifePlanChart。
|
||
|
||
```mermaid
|
||
erDiagram
|
||
YEARLY_SNAPSHOT {
|
||
number year
|
||
number rep_age
|
||
number? spouse_age
|
||
number income
|
||
number expenditure
|
||
number asset_balance
|
||
string[]? events
|
||
boolean? is_critical
|
||
}
|
||
LIFE_PLAN_CHART_PROPS {
|
||
YearlySnapshot[] timeline
|
||
object meta
|
||
boolean hide_header
|
||
number? external_data_index
|
||
number? external_fraction
|
||
}
|
||
RENDER_PROPS {
|
||
object meta
|
||
object charts
|
||
object script
|
||
object assets
|
||
}
|
||
RENDER_PROPS ||--|| LIFE_PLAN_CHART_PROPS : "defaultProps"
|
||
LIFE_PLAN_CHART_PROPS ||--o{ YEARLY_SNAPSHOT : "timeline"
|
||
```
|
||
|
||
**图表来源**
|
||
- [types.ts](file://src/types.ts#L1-L30)
|
||
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L55-L61)
|
||
- [Root.tsx](file://src/Root.tsx#L17-L29)
|
||
|
||
**章节来源**
|
||
- [types.ts](file://src/types.ts#L1-L30)
|
||
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L55-L61)
|
||
- [Root.tsx](file://src/Root.tsx#L17-L29)
|
||
|
||
### 自定义 X 轴刻度:三行标签渲染
|
||
- 渲染内容:年份(第一行)、主被保险人年龄(第二行,加粗)、配偶年龄(第三行,若无则显示“-”)。
|
||
- 实现要点:
|
||
- 通过 payload.index 获取当前刻度对应 timeline 的索引。
|
||
- 从 timelineData 中取出对应 YearlySnapshot,安全地渲染三行文本。
|
||
- 使用 translate(x,y) 对齐刻度位置,设置字体大小、颜色与对齐方式。
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Start["进入 CustomXAxisTick 渲染"] --> GetIndex["根据 payload.index 获取索引"]
|
||
GetIndex --> Lookup["从 timelineData 查找 YearlySnapshot"]
|
||
Lookup --> HasData{"是否存在数据点?"}
|
||
HasData --> |否| EndNull["返回空节点"]
|
||
HasData --> |是| RenderRows["绘制三行文本:<br/>1) 年份<br/>2) 主被保险人年龄加粗<br/>3) 配偶年龄若存在"]
|
||
RenderRows --> End["完成渲染"]
|
||
```
|
||
|
||
**图表来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L16-L65)
|
||
|
||
**章节来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L16-L65)
|
||
|
||
### 渐变填充逻辑:正负资产分色
|
||
- 计算规则:
|
||
- 若全部资产余额非负:渐变完全为蓝色。
|
||
- 若全部资产余额非正:渐变完全为红色。
|
||
- 若混合:按最大值与最小值的比例计算零线位置,形成蓝到红的渐变过渡。
|
||
- 关键点:
|
||
- 基于“可见数据”计算梯度偏移,确保与当前屏幕显示一致。
|
||
- 通过 <linearGradient> 的两个 stop 点共同决定颜色分界,实现平滑过渡。
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Start["开始计算渐变偏移"] --> CheckEmpty{"visibleData 是否为空?"}
|
||
CheckEmpty --> |是| Off0["返回偏移 0"]
|
||
CheckEmpty --> |否| Extrema["计算 visibleData 中的最大值与最小值"]
|
||
Extrema --> AllPos{"最小值 >= 0 ?"}
|
||
AllPos --> |是| Off1["返回偏移 1全蓝"]
|
||
AllPos --> |否| AllNeg{"最大值 <= 0 ?"}
|
||
AllNeg --> |是| Off0["返回偏移 0全红"]
|
||
AllNeg --> |否| Mix["偏移 = 最大值/(最大值-最小值)"]
|
||
Off1 --> End["结束"]
|
||
Off0 --> End
|
||
Mix --> End
|
||
```
|
||
|
||
**图表来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L142-L163)
|
||
|
||
**章节来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L142-L163)
|
||
|
||
### HUD 区域:实时信息展示
|
||
- 内容:
|
||
- 当前年份
|
||
- 资产余额(正数为蓝色,负数为红色)
|
||
- 收入(绿色,带“+”前缀)
|
||
- 支出(红色,带“-”前缀)
|
||
- 数据来源:基于当前帧计算出的 dataEndIndex 对应的 YearlySnapshot。
|
||
- 设计考虑:使用统一背景与阴影提升可读性,布局在隐藏头部模式下仍保持一致性。
|
||
|
||
**章节来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L203-L289)
|
||
|
||
### 动画数据流与插值算法
|
||
- 插值映射:将帧号映射到 timeline 的索引区间,得到 exactIndex。
|
||
- 数据切片:dataEndIndex 为 floor(exactIndex),fraction 为小数部分。
|
||
- 可见数据:取 timeline.slice(0, dataEndIndex+1),并在末尾插入按 fraction 插值得到的“当前帧”插值点。
|
||
- 插值字段:
|
||
- year:线性插值
|
||
- ages.rep/spouse:线性插值(若一方缺失则不参与插值)
|
||
- financials.income/expenditure/assetBalance:线性插值
|
||
- 外部控制:支持 externalDataIndex 与 externalFraction,便于与其他图表联动(例如 DualChartView)。
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
F0["useCurrentFrame()"] --> Map["interpolate(帧号) => exactIndex"]
|
||
Map --> Split["dataEndIndex = floor(exactIndex)<br/>fraction = exactIndex - dataEndIndex"]
|
||
Split --> Slice["visibleData = timeline.slice(0, dataEndIndex+1)"]
|
||
Split --> Interp{"fraction > 0 且 dataEndIndex < length-1 ?"}
|
||
Interp --> |是| Build["构造插值点year/ages/financials"]
|
||
Interp --> |否| Skip["跳过插值"]
|
||
Build --> Append["追加到 visibleData"]
|
||
Skip --> Append
|
||
Append --> Render["Recharts 渲染"]
|
||
```
|
||
|
||
**图表来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L81-L141)
|
||
|
||
**章节来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L81-L141)
|
||
|
||
### Y 轴域与 X 轴域策略
|
||
- Y 轴域:基于全量 timeline 的资产余额极值计算,添加 10% 边距,避免动画过程中的剧烈跳变。
|
||
- X 轴域:当可见数据长度小于 10 时,固定 X 轴范围为 timeline[0].year 到 timeline[0].year + 9,确保标签可读;否则使用类别类型自动适配。
|
||
|
||
**章节来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L175-L189)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L311-L319)
|
||
|
||
### 与 render_props.json 的映射关系示例
|
||
- 后端输出:charts.assetDepletion.timeline 为 YearlySnapshot 数组,meta 包含 currencyUnit 与 startYear。
|
||
- 前端使用:在 Root.tsx 中将 mock-data.json 的 timeline 与 meta 作为 defaultProps 传给 LifePlanChart。
|
||
- 双图联动:在 DualChartView.tsx 中将 CashFlowData 转换为 YearlySnapshot 形态,再传给 LifePlanChart,实现两图同步动画。
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Backend as "Python 后端"
|
||
participant JSON as "render_props.json"
|
||
participant Frontend as "Remotion 前端"
|
||
participant Root as "Root.tsx"
|
||
participant LifePlan as "LifePlanChart"
|
||
Backend->>JSON : "生成 charts.assetDepletion"
|
||
Frontend->>Root : "加载 render_props.json"
|
||
Root->>LifePlan : "defaultProps = { timeline, meta }"
|
||
LifePlan-->>Frontend : "渲染资产消耗趋势图"
|
||
```
|
||
|
||
**图表来源**
|
||
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L55-L61)
|
||
- [Root.tsx](file://src/Root.tsx#L17-L29)
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L48-L76)
|
||
|
||
**章节来源**
|
||
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L55-L61)
|
||
- [Root.tsx](file://src/Root.tsx#L17-L29)
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L48-L76)
|
||
|
||
### 组件类图(代码级)
|
||
```mermaid
|
||
classDiagram
|
||
class YearlySnapshot {
|
||
+number year
|
||
+object ages
|
||
+number ages.rep
|
||
+number? ages.spouse
|
||
+object financials
|
||
+number financials.income
|
||
+number financials.expenditure
|
||
+number financials.assetBalance
|
||
+string[]? events
|
||
+boolean? isCritical
|
||
}
|
||
class LifePlanChartProps {
|
||
+YearlySnapshot[] timeline
|
||
+object meta
|
||
+string meta.currencyUnit
|
||
+number meta.startYear
|
||
+boolean hideHeader
|
||
+number? externalDataIndex
|
||
+number? externalFraction
|
||
}
|
||
class LifePlanChart {
|
||
+React.FC<LifePlanChartProps>
|
||
+useCurrentFrame()
|
||
+interpolate()
|
||
+React.memo()
|
||
}
|
||
LifePlanChartProps --> YearlySnapshot : "timeline"
|
||
LifePlanChart --> LifePlanChartProps : "接收"
|
||
```
|
||
|
||
**图表来源**
|
||
- [types.ts](file://src/types.ts#L1-L30)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L70-L110)
|
||
|
||
**章节来源**
|
||
- [types.ts](file://src/types.ts#L1-L30)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L70-L110)
|
||
|
||
## 依赖关系分析
|
||
- 组件依赖:
|
||
- Recharts:AreaChart、XAxis、YAxis、CartesianGrid、ReferenceLine、ResponsiveContainer
|
||
- Remotion:useCurrentFrame、useVideoConfig、interpolate
|
||
- React:memo、useMemo、useCurrentFrame
|
||
- 数据依赖:
|
||
- YearlySnapshot 接口定义数据结构
|
||
- render_props.json 的 charts.assetDepletion.timeline 与 meta
|
||
- 上层依赖:
|
||
- Root.tsx 注册 Composition 并传入 defaultProps
|
||
- DualChartView.tsx 将 CashFlowData 转换为 YearlySnapshot 传入 LifePlanChart
|
||
|
||
```mermaid
|
||
graph LR
|
||
LifePlan["LifePlanChart.tsx"] --> Recharts["Recharts 组件"]
|
||
LifePlan --> Remotion["Remotion hooks"]
|
||
LifePlan --> Types["types.ts"]
|
||
Root["Root.tsx"] --> LifePlan
|
||
Dual["DualChartView.tsx"] --> LifePlan
|
||
RenderTypes["render-props-types.ts"] --> Root
|
||
Mock["mock-data.json"] --> Root
|
||
```
|
||
|
||
**图表来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L1-L30)
|
||
- [Root.tsx](file://src/Root.tsx#L1-L68)
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L1-L176)
|
||
- [types.ts](file://src/types.ts#L1-L30)
|
||
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L55-L61)
|
||
- [mock-data.json](file://src/mock-data.json#L1-L61)
|
||
|
||
**章节来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L1-L30)
|
||
- [Root.tsx](file://src/Root.tsx#L1-L68)
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L1-L176)
|
||
- [types.ts](file://src/types.ts#L1-L30)
|
||
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L55-L61)
|
||
- [mock-data.json](file://src/mock-data.json#L1-L61)
|
||
|
||
## 性能考量
|
||
- 使用 React.memo 包裹 LifePlanChart,避免 props 未变化时的重渲染。
|
||
- 使用 React.useMemo:
|
||
- 缓存 gradientOffset,仅在 visibleData 变化时重新计算。
|
||
- 缓存 xAxisDomain,避免每帧重建数组。
|
||
- 在上层(如 DualChartView)将 CashFlowData 转换为 YearlySnapshot 的结果缓存,避免每帧重建数组。
|
||
- 关闭 AreaChart 内置动画(isAnimationActive=false),改用插值驱动的平滑动画,减少不必要的重绘。
|
||
- Y 轴域基于全量 timeline 计算,避免动画过程中的跳变,降低视觉抖动。
|
||
- X 轴域在短序列时固定范围,提高标签可读性与渲染稳定性。
|
||
|
||
**章节来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L67-L70)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L142-L174)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L175-L189)
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L48-L76)
|
||
|
||
## 故障排查指南
|
||
- HUD 数值异常:
|
||
- 检查 currentData 来源是否正确(dataEndIndex 对应的 timeline 元素)。
|
||
- 确认 financials 字段是否包含 income、expenditure、assetBalance。
|
||
- 渐变颜色不符合预期:
|
||
- 确认 visibleData 是否为空或仅包含正/负值。
|
||
- 检查 gradientOffset 的计算逻辑与 <linearGradient> 的 stop 配置。
|
||
- X 轴标签错位或拥挤:
|
||
- 检查 CustomXAxisTick 的 payload.index 与 timelineData 的索引映射。
|
||
- 调整 interval 与 domain,或在短序列时固定 X 轴范围。
|
||
- 动画卡顿或跳变:
|
||
- 确保关闭 AreaChart 内置动画,使用插值驱动。
|
||
- 检查 externalDataIndex 与 externalFraction 是否由上层正确传递。
|
||
- 使用 memo 与 useMemo 缓存昂贵计算。
|
||
|
||
**章节来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L16-L65)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L142-L189)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L311-L341)
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L120-L130)
|
||
|
||
## 结论
|
||
LifePlanChart 通过 Remotion 的逐帧插值与 Recharts 的 AreaChart,实现了资产余额随生命周期平滑推进的可视化。其三行 X 轴标签清晰呈现时间与年龄信息,渐变填充直观表达资产正负状态,HUD 实时反馈关键财务指标。借助 memo 与 useMemo 的性能优化,以及与 render_props.json 的强类型映射,组件在复杂视频场景中具备良好的稳定性与扩展性。
|
||
|
||
## 附录
|
||
|
||
### 初学者使用示例
|
||
- 直接渲染(使用 mock 数据):
|
||
- 在 Root.tsx 中注册 LifePlanChart Composition,并将 mock-data.json 的 timeline 与 meta 作为 defaultProps 传入。
|
||
- 运行 Remotion 渲染,即可看到资产消耗趋势动画。
|
||
- 双图联动:
|
||
- 在 DualChartView.tsx 中将 CashFlowData 转换为 YearlySnapshot,分别传给 LifePlanChart 与 CashFlowChart,两者共享同一动画进度,实现同步推进。
|
||
|
||
**章节来源**
|
||
- [Root.tsx](file://src/Root.tsx#L17-L29)
|
||
- [DualChartView.tsx](file://src/DualChartView.tsx#L48-L76)
|
||
|
||
### 高级开发者优化建议
|
||
- 性能优化:
|
||
- 在上层(如 DualChartView)对数据转换结果使用 useMemo 缓存。
|
||
- 在 LifePlanChart 内部对 gradientOffset、xAxisDomain、visibleData 的构建进行 memo 化。
|
||
- 避免在渲染函数中创建新对象或数组,减少不必要的重渲染。
|
||
- 自定义动画曲线:
|
||
- 通过自定义 interpolate 的缓动函数(如 easeInOut)替换默认线性映射,改变推进速度曲线。
|
||
- 在外部控制外部动画进度(externalDataIndex 与 externalFraction),实现多图表联动与同步暂停/加速。
|
||
- 数据校验:
|
||
- 使用 render-props-types.ts 中的 Zod Schema 对 render_props.json 进行运行时校验,确保数据结构一致性。
|
||
|
||
**章节来源**
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L67-L70)
|
||
- [LifePlanChart.tsx](file://src/LifePlanChart.tsx#L142-L174)
|
||
- [render-props-types.ts](file://frontend/src/render-props-types.ts#L194-L231) |