All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 22s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
118 lines
4.3 KiB
Markdown
118 lines
4.3 KiB
Markdown
## Context
|
|
|
|
`SavingsBudgetContent.vue` 是一个显示存款计划列表和明细的组件。每个存款计划卡片可以点击查看详细信息,弹窗会显示:
|
|
1. 收入预算(预算限额和实际收入)
|
|
2. 支出预算(预算限额和实际支出)
|
|
3. 计划存款公式(收入预算 - 支出预算 = 计划存款)
|
|
4. 存款结果(计划存款、实际存款、还差)
|
|
|
|
问题在于弹窗模板引用了 `incomeLimit`、`incomeCurrent`、`expenseLimit`、`expenseCurrent` 这些计算属性,但在 `<script setup>` 中并未定义,导致弹窗内容为空。
|
|
|
|
父组件 `Index.vue` 维护了三个独立的预算数组:
|
|
- `expenseBudgets` - 支出预算列表
|
|
- `incomeBudgets` - 收入预算列表
|
|
- `savingsBudgets` - 存款计划列表
|
|
|
|
当前 `SavingsBudgetContent.vue` 只接收 `savingsBudgets` 数组,无法访问收入和支出预算数据。
|
|
|
|
## Goals / Non-Goals
|
|
|
|
**Goals:**
|
|
- 修复存款明细弹窗显示为空的问题
|
|
- 正确显示收入预算和支出预算的限额及实际值
|
|
- 确保计算逻辑与后端逻辑一致
|
|
- 保持组件的单一职责,不引入不必要的依赖
|
|
|
|
**Non-Goals:**
|
|
- 不修改存款计划的计算逻辑(后端已有)
|
|
- 不改变预算数据的加载方式
|
|
- 不重构整个预算页面的架构
|
|
|
|
## Decisions
|
|
|
|
### 决策 1: 数据传递方式
|
|
|
|
**选择:** 通过 props 传递收入和支出预算数据
|
|
|
|
**理由:**
|
|
- **为什么不用 provide/inject?** 数据流更清晰,易于追踪和调试
|
|
- **为什么不在子组件中调用 API?** 违反单一数据源原则,会导致重复加载和不一致
|
|
- **为什么不用 Pinia store?** 这是页面级别的临时数据,不需要全局状态管理
|
|
|
|
**实现:**
|
|
在 `Index.vue` 中传递 `incomeBudgets` 和 `expenseBudgets` 给 `SavingsBudgetContent.vue`:
|
|
```vue
|
|
<SavingsBudgetContent
|
|
:budgets="savingsBudgets"
|
|
:income-budgets="incomeBudgets"
|
|
:expense-budgets="expenseBudgets"
|
|
@savings-nav="handleSavingsNav"
|
|
/>
|
|
```
|
|
|
|
### 决策 2: 匹配收入/支出预算的逻辑
|
|
|
|
**选择:** 根据 `periodStart` 和 `type` 进行匹配
|
|
|
|
**理由:**
|
|
- 存款计划、收入预算、支出预算都有相同的 `periodStart`(周期开始时间)和 `type`(月度/年度)字段
|
|
- 同一周期、同一类型的预算应该对应同一个存款计划
|
|
- 后端逻辑中存款 = 收入预算限额 - 支出预算限额
|
|
|
|
**实现:**
|
|
在 `SavingsBudgetContent.vue` 中添加计算属性:
|
|
```javascript
|
|
const matchedIncomeBudget = computed(() => {
|
|
if (!currentBudget.value) return null
|
|
return props.incomeBudgets?.find(
|
|
b => b.periodStart === currentBudget.value.periodStart
|
|
&& b.type === currentBudget.value.type
|
|
)
|
|
})
|
|
|
|
const matchedExpenseBudget = computed(() => {
|
|
if (!currentBudget.value) return null
|
|
return props.expenseBudgets?.find(
|
|
b => b.periodStart === currentBudget.value.periodStart
|
|
&& b.type === currentBudget.value.type
|
|
)
|
|
})
|
|
|
|
const incomeLimit = computed(() => matchedIncomeBudget.value?.limit || 0)
|
|
const incomeCurrent = computed(() => matchedIncomeBudget.value?.current || 0)
|
|
const expenseLimit = computed(() => matchedExpenseBudget.value?.limit || 0)
|
|
const expenseCurrent = computed(() => matchedExpenseBudget.value?.current || 0)
|
|
```
|
|
|
|
### 决策 3: 处理数据缺失情况
|
|
|
|
**选择:** 使用默认值 0,不显示错误提示
|
|
|
|
**理由:**
|
|
- 如果找不到对应的收入或支出预算,说明用户可能还未设置
|
|
- 显示 0 比显示错误信息更友好,符合"计划存款 = 收入 - 支出"的语义
|
|
- 用户可以在主界面看到预算设置情况,弹窗只是详情展示
|
|
|
|
## Risks / Trade-offs
|
|
|
|
### [风险] 如果收入/支出预算数据未加载完成
|
|
|
|
**缓解措施:**
|
|
- 父组件 `Index.vue` 已经统一加载所有预算数据
|
|
- 弹窗在用户点击时打开,此时数据应已加载完成
|
|
- 使用可选链和默认值避免运行时错误
|
|
|
|
### [权衡] Props 增加导致组件耦合度提高
|
|
|
|
**接受理由:**
|
|
- 父组件本身就维护了所有预算数据,传递给子组件是合理的
|
|
- 子组件不负责数据加载,只负责展示,职责依然清晰
|
|
- 如果未来需要重构,可以考虑引入 Pinia store 统一管理预算数据
|
|
|
|
### [权衡] 按周期和类型匹配可能不够健壮
|
|
|
|
**接受理由:**
|
|
- 这是后端设计的数据模型,前端保持一致
|
|
- 后端保证同一周期同一类型只有一条预算记录
|
|
- 如果后端逻辑变更,前端需要同步调整(这是正常的维护成本)
|