Actual Budget 余额预测部件粒度跟随报告设置Daily / Monthly 切换的实现解析【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual本篇围绕 Actual Budget 的余额预测Balance Forecast报告与仪表盘部件展开核心主题是部件/卡片在展示余额预测曲线时应当使用报告本身定义的粒度每日 Daily / 每月 Monthly而不是固定按月渲染。读完本文你将理解granularity元数据在部件中的存储与读取方式、Daily 与 Monthly 两种模式在图表数据构建上的差异、前后端调用链以及该修复对应的测试验证从而掌握余额预测部件粒度机制的完整实现。变更背景余额预测部件的两种粒度余额预测是 Actual Budget 仪表盘Dashboard上的核心部件之一它根据已入账交易与未来的计划交易Scheduled Transactions推算账户余额的走向。在正式报告中用户可以通过下拉选择器在Monthly按月与Daily按日两种粒度之间切换从而决定预测曲线是按月汇总取点还是按每一天精确取点。在修复之前仪表盘上的余额预测卡片widget/card在渲染时固定使用月度粒度即无论用户在完整报告中选择了哪种粒度卡片上显示的曲线始终按月绘制导致两个视图之间出现不一致。本次变更见 upcoming-release-notes/forecast-widget-granularity.md的修复目标非常明确对于余额预测报告卡片/部件使用报告中定义的粒度daily / monthly而不是总是按月。即让部件读取并沿用报告保存的granularity设置实现报告怎么设、卡片怎么画的体验一致性。粒度元数据部件的持久化配置部件的配置信息存放在meta字段中。在类型定义层面BalanceForecastWidget明确声明了granularity可选项取值限定为Daily | Monthly两种参见 packages/loot-core/src/types/models/dashboard.tsexport type BalanceForecastWidget AbstractWidget balance-forecast-card, { name?: string; startDate?: string; endDate?: string; accounts?: string[]; conditions?: RuleConditionEntity[]; conditionsOp?: and | or; timeFrame?: TimeFrame; granularity?: Daily | Monthly; source?: ForecastSource; } | null ;granularity为可选字段未设置时默认回退为Monthly这保证了老数据与旧部件的向后兼容同一部件还包含账户列表accounts、过滤条件conditions/conditionsOp、时间范围timeFrame与预测数据来源source等配置粒度只是其中一环。该部件类型在仪表盘部件菜单中注册为balance-forecast-card相关入口位于 packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts。修复核心卡片读取报告定义的粒度修复的关键改动在余额预测卡片的渲染组件 packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx 中卡片直接从meta中读取粒度const granularity meta?.granularity || Monthly;见 BalanceForecastCard.tsx这一行正是修复的题眼卡片不再硬编码为月度而是优先采用报告保存的granularity仅在元数据缺失时才回退到Monthly。随后该值被同时用于两处图表数据构建buildBalanceForecastChartData({ forecastData, start, end, granularity })由粒度决定取点方式详见下一节计划交易出现次数统计countForecastScheduledOccurrences({ forecastData, start, end, granularity })统计卡片当前可见范围内实际纳入的计划交易次数并与日期边界处理保持一致。在完整的报告页面 packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx 中粒度以本地 state 管理并提供下拉选择控件const [granularity, setGranularity] useStateDaily | Monthly( widget?.meta?.granularity ?? Monthly, );见 BalanceForecast.tsxSelect value{granularity} onChange{setGranularity} disabled{isTrackingBudgetForecast} options{[ [Monthly, t(Monthly)], [Daily, t(Daily)], ]} /见 BalanceForecast.tsx需要注意两个细节Tracking Budget 数据源强制月度当预测来源切换为tracking-budget时setGranularity(Monthly)会被强制调用粒度选择器同时被disabled禁用保存时也会强制写入Monthly见 BalanceForecast.tsx 与onSourceChange。这是因为追踪预算预测的公式起始余额 预算收入 − 预算支出本身就是按月计算的不具备按日的语义保存时回写 meta用户点击 Save widget 后granularity随其余配置一起通过dashboard-update-widget接口写入部件元数据从而让卡片在仪表盘上复用同一份配置。图表数据构建Daily 与 Monthly 的分支逻辑粒度对曲线的影响最终落在 packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts 的buildBalanceForecastChartData中。该函数根据granularity走两条完全不同的路径type Granularity Daily | Monthly; type ChartDataPoint { date: string; balance: number };见 balanceForecastChartData.tsMonthly 分支见 balanceForecastChartData.ts先把每个账户的数据点按date.substring(0, 7)归并到月份再按账户求和得到当月组合余额getCombinedBalanceByMonth然后从起始月到结束月逐月推进采用当月有数据则更新 running balance否则沿用上月值的游标方式生成数据点日期格式为yyyy-MM如2024-03日级别的起止边界会被折叠为月monthUtils.getMonth(end)保证月份 key 对齐。Daily 分支见 balanceForecastChartData.ts按yyyy-MM-dd精确到天组合各账户余额getCombinedBalanceByDate对月形状的起止边界如2024-03自动展开为整月首日 / 末日对日形状的边界如2024-03-10则保持精确不做扩展若可见范围起点前已有数据点则把起点前最近一期的余额结转进图priorDaterunningBalance避免日形状起点时曲线从零开始逐日推进current.setDate(current.getDate() 1)当天有数据则更新余额否则沿用上一天的值直至结束日。两种模式共用同一个ChartDataPoint结构{ date, balance }因此下游的折线图渲染BalanceForecast.tsx无需关心粒度Daily 模式下 X 轴刻度间隔按Math.ceil(chartData.length / 10)稀疏化并格式化为MMM dMonthly 模式则直接显示月份。此外卡片的今日参考线ReferenceLine也随粒度联动Daily 模式使用monthUtils.currentDay()Monthly 模式使用monthUtils.currentMonth()见 BalanceForecast.tsx确保参考线与取点密度匹配。前后端调用链从查询到图表粒度只影响前端取点方式不影响预测数据的计算本身。预测数据的获取链路如下前端查询 hookpackages/desktop-client/src/hooks/useBalanceForecast.ts 通过tanstack/react-query封装请求queryKey 包含账户、条件、起止日期与source并通过placeholderData: keepPreviousData在参数变化时保留旧数据配合isPlaceholderData实现更新中的降透明度动画后端生成器hook 调用forecast/generate对应 packages/loot-core/src/server/forecast/app.ts 的generateForecast。它根据source分两条路径tracking-budget读取budgetType偏好校验必须是 Tracking Budget再调用projectTrackingBudgetForecast按月投影schedules默认解析过滤条件buildFilterInfo、解析账户、生成未来计划交易出现buildFutureScheduleOccurrences最终由projectForecastData生成逐日数据点。返回的ForecastResult包含dataPoints按日期、账户、含transactions明细、lowestBalance、forecastStartDate与forecastEndDate。无论 Daily 还是 Monthly后端都返回同一份数据前端再按粒度聚合取点这也正是本次修复能够纯粹在展示层完成的原因。值得注意的是卡片与完整报告都调用同一个useBalanceForecast只是卡片版额外把start/end通过committedChartRange缓存在isPlaceholderData期间保持图表范围稳定见 BalanceForecastCard.tsx。测试验证粒度行为被固化仓库为上述逻辑提供了完整的单测见 packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts覆盖的关键场景包括测试场景断言要点Monthly 跨账户合并同一月份多个账户余额求和为一个点日期格式为yyyy-MMMonthly 取当月最新值同月出现多次数据点时使用最后一条余额Daily 展开月边界月形状边界展开为整月3 月 4 月生成 61 个日数据点月末余额结转Daily 保持日边界精确日形状边界2024-03-10至2024-03-20精确生成 11 个点不扩展到整月Daily 起点前余额结转起点前最近一期的余额作为曲线起始值不从零开始计划交易出现计数转账类计划在两个账户腿上只计一次Daily 模式只统计日边界内的出现Monthly 模式扩展为整月统计最后两项balanceForecastChartData.test.ts验证了countForecastScheduledOccurrences与buildBalanceForecastChartData在日期边界处理上保持镜像一致避免出现图中画了某次计划交易、计数却对不上的偏差。这些测试确保 Daily / Monthly 的粒度行为在后续迭代中不会回退。对用户的实际影响视图一致性在仪表盘上打开余额预测卡片后点击进入完整报告卡片链接到/reports/forecast/:widgetId切换粒度并保存返回仪表盘时卡片会以同一粒度渲染不再出现报告是每日曲线、卡片却是月度曲线的割裂默认行为不变从未设置过粒度的旧部件与新建部件仍默认按月现有用户升级后无需手动调整追踪预算场景例外Tracking Budget 来源的预测仍强制按月这是由预测公式本身的语义决定的属于预期内的限制。小结本次修复的实质是让余额预测部件的granularity元数据真正贯通报告设置 → 元数据持久化 → 卡片渲染整条链路类型层在 dashboard.ts 约束取值渲染层在 BalanceForecastCard.tsx 读取回退取点逻辑在 balanceForecastChartData.ts 分派最终由 balanceForecastChartData.test.ts 将行为固化。理解这条链路后你可以放心地预期余额预测部件与完整报告在任何粒度设置下都保持一致的呈现。【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考