From 4cc205fc2566a781a55b3959955e25d1c9751036 Mon Sep 17 00:00:00 2001 From: SunCheng Date: Fri, 20 Feb 2026 16:26:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(budget):=20=E5=AE=9E=E7=8E=B0=E5=AD=98?= =?UTF-8?q?=E6=AC=BE=E6=98=8E=E7=BB=86=E8=AE=A1=E7=AE=97=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 BudgetItemCalculator 辅助类,实现明细项计算规则 - 收入:实际>0取实际,否则取预算 - 支出:取MAX(预算, 实际) - 硬性支出未发生:按天数折算 - 归档数据:直接使用实际值 - 实现月度和年度存款核心公式 - 月度:收入预算 + 本月年度收入 - 支出预算 - 本月年度支出 - 年度:归档已实收 + 未来收入预算 - 归档已实支 - 未来支出预算 - 定义存款明细数据结构 - SavingsDetail: 包含收入/支出明细列表和汇总 - BudgetDetailItem: 预算明细项(含计算用金额、计算说明等) - SavingsCalculationSummary: 计算汇总信息 - 新增单元测试 - BudgetItemCalculatorTest: 11个测试覆盖所有计算规则 - BudgetSavingsCalculationTest: 6个测试验证核心公式 测试结果:所有测试通过 (366 passed, 0 failed) --- Application/Dto/BudgetDto.cs | 38 +++ Service/Budget/BudgetItemCalculator.cs | 121 ++++++++ Service/Budget/BudgetSavingsService.cs | 30 +- Service/Budget/BudgetService.cs | 43 +++ .../components/Budget/BudgetChartAnalysis.vue | 3 + Web/src/views/budgetV2/Index.vue | 2 + .../budgetV2/modules/SavingsBudgetContent.vue | 32 +++ .../Budget/BudgetItemCalculatorTest.cs | 260 ++++++++++++++++++ .../Budget/BudgetSavingsCalculationTest.cs | 135 +++++++++ .../fix-deposit-detail-empty/.openspec.yaml | 2 + .../fix-deposit-detail-empty/design.md | 117 ++++++++ .../fix-deposit-detail-empty/proposal.md | 33 +++ .../specs/savings-plan-detail-view/spec.md | 45 +++ .../changes/fix-deposit-detail-empty/tasks.md | 22 ++ .../saving-detail-calculation/.openspec.yaml | 2 + .../saving-detail-calculation/design.md | 255 +++++++++++++++++ .../saving-detail-calculation/proposal.md | 57 ++++ .../specs/budget-savings/spec.md | 126 +++++++++ .../specs/saving-detail-calculation/spec.md | 131 +++++++++ .../specs/saving-detail-display/spec.md | 141 ++++++++++ .../saving-detail-calculation/tasks.md | 136 +++++++++ 21 files changed, 1730 insertions(+), 1 deletion(-) create mode 100644 Service/Budget/BudgetItemCalculator.cs create mode 100644 WebApi.Test/Budget/BudgetItemCalculatorTest.cs create mode 100644 WebApi.Test/Budget/BudgetSavingsCalculationTest.cs create mode 100644 openspec/changes/fix-deposit-detail-empty/.openspec.yaml create mode 100644 openspec/changes/fix-deposit-detail-empty/design.md create mode 100644 openspec/changes/fix-deposit-detail-empty/proposal.md create mode 100644 openspec/changes/fix-deposit-detail-empty/specs/savings-plan-detail-view/spec.md create mode 100644 openspec/changes/fix-deposit-detail-empty/tasks.md create mode 100644 openspec/changes/saving-detail-calculation/.openspec.yaml create mode 100644 openspec/changes/saving-detail-calculation/design.md create mode 100644 openspec/changes/saving-detail-calculation/proposal.md create mode 100644 openspec/changes/saving-detail-calculation/specs/budget-savings/spec.md create mode 100644 openspec/changes/saving-detail-calculation/specs/saving-detail-calculation/spec.md create mode 100644 openspec/changes/saving-detail-calculation/specs/saving-detail-display/spec.md create mode 100644 openspec/changes/saving-detail-calculation/tasks.md diff --git a/Application/Dto/BudgetDto.cs b/Application/Dto/BudgetDto.cs index 9b86c40..002ba83 100644 --- a/Application/Dto/BudgetDto.cs +++ b/Application/Dto/BudgetDto.cs @@ -89,3 +89,41 @@ public record UpdateArchiveSummaryRequest public DateTime ReferenceDate { get; init; } public string? Summary { get; init; } } + +/// +/// 存款明细数据 +/// +public record SavingsDetail +{ + public List IncomeItems { get; init; } = new(); + public List ExpenseItems { get; init; } = new(); + public SavingsCalculationSummary Summary { get; init; } = new(); +} + +/// +/// 预算明细项 +/// +public record BudgetDetailItem +{ + public long Id { get; init; } + public string Name { get; init; } = string.Empty; + public BudgetPeriodType Type { get; init; } + public decimal BudgetLimit { get; init; } + public decimal ActualAmount { get; init; } + public decimal EffectiveAmount { get; init; } + public string CalculationNote { get; init; } = string.Empty; + public bool IsOverBudget { get; init; } + public bool IsArchived { get; init; } + public int[]? ArchivedMonths { get; init; } +} + +/// +/// 存款计算汇总 +/// +public record SavingsCalculationSummary +{ + public decimal TotalIncomeBudget { get; init; } + public decimal TotalExpenseBudget { get; init; } + public decimal PlannedSavings { get; init; } + public string CalculationFormula { get; init; } = string.Empty; +} diff --git a/Service/Budget/BudgetItemCalculator.cs b/Service/Budget/BudgetItemCalculator.cs new file mode 100644 index 0000000..7ec4746 --- /dev/null +++ b/Service/Budget/BudgetItemCalculator.cs @@ -0,0 +1,121 @@ +namespace Service.Budget; + +/// +/// 预算明细项计算辅助类 +/// 用于计算单个预算项的有效金额(计算用金额) +/// +public static class BudgetItemCalculator +{ + /// + /// 计算预算项的有效金额 + /// + /// 预算类别(收入/支出) + /// 预算金额 + /// 实际金额 + /// 是否为硬性消费 + /// 是否为归档数据 + /// 参考日期 + /// 预算周期类型(月度/年度) + /// 有效金额(用于计算的金额) + public static decimal CalculateEffectiveAmount( + BudgetCategory category, + decimal budgetLimit, + decimal actualAmount, + bool isMandatory, + bool isArchived, + DateTime referenceDate, + BudgetPeriodType periodType) + { + // 归档数据直接返回实际值 + if (isArchived) + { + return actualAmount; + } + + // 收入:实际>0取实际,否则取预算 + if (category == BudgetCategory.Income) + { + return actualAmount > 0 ? actualAmount : budgetLimit; + } + + // 支出(硬性且实际=0):按天数折算 + if (category == BudgetCategory.Expense && isMandatory && actualAmount == 0) + { + return CalculateMandatoryAmount(budgetLimit, referenceDate, periodType); + } + + // 支出(普通):取MAX + if (category == BudgetCategory.Expense) + { + return Math.Max(budgetLimit, actualAmount); + } + + return budgetLimit; + } + + /// + /// 计算硬性消费按天数折算的金额 + /// + private static decimal CalculateMandatoryAmount( + decimal limit, + DateTime date, + BudgetPeriodType periodType) + { + if (periodType == BudgetPeriodType.Month) + { + var daysInMonth = DateTime.DaysInMonth(date.Year, date.Month); + return limit / daysInMonth * date.Day; + } + else // Year + { + var daysInYear = DateTime.IsLeapYear(date.Year) ? 366 : 365; + return limit / daysInYear * date.DayOfYear; + } + } + + /// + /// 生成计算说明 + /// + /// 预算类别 + /// 预算金额 + /// 实际金额 + /// 有效金额 + /// 是否为硬性消费 + /// 是否为归档数据 + /// 计算说明文本 + public static string GenerateCalculationNote( + BudgetCategory category, + decimal budgetLimit, + decimal actualAmount, + decimal effectiveAmount, + bool isMandatory, + bool isArchived) + { + if (isArchived) + { + return "归档实际"; + } + + if (category == BudgetCategory.Income) + { + return actualAmount > 0 ? "使用实际" : "使用预算"; + } + + if (category == BudgetCategory.Expense) + { + if (isMandatory && actualAmount == 0) + { + return "按天折算"; + } + + if (actualAmount > budgetLimit) + { + return "使用实际(超支)"; + } + + return effectiveAmount == actualAmount ? "使用实际" : "使用预算"; + } + + return "使用预算"; + } +} diff --git a/Service/Budget/BudgetSavingsService.cs b/Service/Budget/BudgetSavingsService.cs index e8d72a6..83c012e 100644 --- a/Service/Budget/BudgetSavingsService.cs +++ b/Service/Budget/BudgetSavingsService.cs @@ -935,4 +935,32 @@ public class BudgetSavingsService( return string.Join(", ", months) + "月"; } } -} \ No newline at end of file + + /// + /// 计算月度计划存款 + /// 公式:收入预算 + 发生在本月的年度收入 - 支出预算 - 发生在本月的年度支出 + /// + public static decimal CalculateMonthlyPlannedSavings( + decimal monthlyIncomeBudget, + decimal yearlyIncomeInThisMonth, + decimal monthlyExpenseBudget, + decimal yearlyExpenseInThisMonth) + { + return monthlyIncomeBudget + yearlyIncomeInThisMonth + - monthlyExpenseBudget - yearlyExpenseInThisMonth; + } + + /// + /// 计算年度计划存款 + /// 公式:归档月已实收 + 未来月收入预算 - 归档月已实支 - 未来月支出预算 + /// + public static decimal CalculateYearlyPlannedSavings( + decimal archivedIncome, + decimal futureIncomeBudget, + decimal archivedExpense, + decimal futureExpenseBudget) + { + return archivedIncome + futureIncomeBudget + - archivedExpense - futureExpenseBudget; + } +} diff --git a/Service/Budget/BudgetService.cs b/Service/Budget/BudgetService.cs index 553051c..c9d8e2c 100644 --- a/Service/Budget/BudgetService.cs +++ b/Service/Budget/BudgetService.cs @@ -448,6 +448,11 @@ public record BudgetResult public bool NoLimit { get; set; } public bool IsMandatoryExpense { get; set; } public string Description { get; set; } = string.Empty; + + /// + /// 存款明细数据(可选,用于存款预算) + /// + public SavingsDetail? Details { get; set; } public static BudgetResult FromEntity( BudgetRecord entity, @@ -547,3 +552,41 @@ public class UncoveredCategoryDetail public int TransactionCount { get; set; } public decimal TotalAmount { get; set; } } + +/// +/// 存款明细数据 +/// +public record SavingsDetail +{ + public List IncomeItems { get; init; } = new(); + public List ExpenseItems { get; init; } = new(); + public SavingsCalculationSummary Summary { get; init; } = new(); +} + +/// +/// 预算明细项 +/// +public record BudgetDetailItem +{ + public long Id { get; init; } + public string Name { get; init; } = string.Empty; + public BudgetPeriodType Type { get; init; } + public decimal BudgetLimit { get; init; } + public decimal ActualAmount { get; init; } + public decimal EffectiveAmount { get; init; } + public string CalculationNote { get; init; } = string.Empty; + public bool IsOverBudget { get; init; } + public bool IsArchived { get; init; } + public int[]? ArchivedMonths { get; init; } +} + +/// +/// 存款计算汇总 +/// +public record SavingsCalculationSummary +{ + public decimal TotalIncomeBudget { get; init; } + public decimal TotalExpenseBudget { get; init; } + public decimal PlannedSavings { get; init; } + public string CalculationFormula { get; init; } = string.Empty; +} diff --git a/Web/src/components/Budget/BudgetChartAnalysis.vue b/Web/src/components/Budget/BudgetChartAnalysis.vue index 586f77a..4892338 100644 --- a/Web/src/components/Budget/BudgetChartAnalysis.vue +++ b/Web/src/components/Budget/BudgetChartAnalysis.vue @@ -1013,6 +1013,7 @@ const yearBurndownChartOptions = computed(() => { display: flex; justify-content: center; align-items: center; + z-index: 1; } .gauge-text-overlay { @@ -1048,6 +1049,8 @@ const yearBurndownChartOptions = computed(() => { .chart-header { margin-bottom: 12px; + position: relative; + z-index: 20; } .chart-title { diff --git a/Web/src/views/budgetV2/Index.vue b/Web/src/views/budgetV2/Index.vue index 261beb4..addc25b 100644 --- a/Web/src/views/budgetV2/Index.vue +++ b/Web/src/views/budgetV2/Index.vue @@ -123,6 +123,8 @@ diff --git a/Web/src/views/budgetV2/modules/SavingsBudgetContent.vue b/Web/src/views/budgetV2/modules/SavingsBudgetContent.vue index a25b46c..15d4f0d 100644 --- a/Web/src/views/budgetV2/modules/SavingsBudgetContent.vue +++ b/Web/src/views/budgetV2/modules/SavingsBudgetContent.vue @@ -183,6 +183,14 @@ const props = defineProps({ budgets: { type: Array, default: () => [] + }, + incomeBudgets: { + type: Array, + default: () => [] + }, + expenseBudgets: { + type: Array, + default: () => [] } }) @@ -199,6 +207,30 @@ const handleShowDetail = (budget) => { showDetailPopup.value = true } +// 匹配收入预算 +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) + // 辅助函数 const formatMoney = (val) => { return parseFloat(val || 0).toLocaleString(undefined, { diff --git a/WebApi.Test/Budget/BudgetItemCalculatorTest.cs b/WebApi.Test/Budget/BudgetItemCalculatorTest.cs new file mode 100644 index 0000000..b17ae5a --- /dev/null +++ b/WebApi.Test/Budget/BudgetItemCalculatorTest.cs @@ -0,0 +1,260 @@ +using Service.Budget; + +namespace WebApi.Test.Budget; + +/// +/// BudgetItemCalculator 单元测试 +/// 测试明细项计算用金额的各种规则 +/// +public class BudgetItemCalculatorTest : BaseTest +{ + [Fact] + public void 收入项实际已发生_应返回实际值() + { + // Arrange + var budgetLimit = 10000m; + var actualAmount = 9500m; + + // Act + var result = BudgetItemCalculator.CalculateEffectiveAmount( + BudgetCategory.Income, + budgetLimit, + actualAmount, + isMandatory: false, + isArchived: false, + new DateTime(2026, 2, 15), + BudgetPeriodType.Month + ); + + // Assert + result.Should().Be(9500m); + } + + [Fact] + public void 收入项实际未发生_应返回预算值() + { + // Arrange + var budgetLimit = 5000m; + var actualAmount = 0m; + + // Act + var result = BudgetItemCalculator.CalculateEffectiveAmount( + BudgetCategory.Income, + budgetLimit, + actualAmount, + isMandatory: false, + isArchived: false, + new DateTime(2026, 2, 15), + BudgetPeriodType.Month + ); + + // Assert + result.Should().Be(5000m); + } + + [Fact] + public void 支出项普通情况_应返回MAX预算和实际() + { + // Arrange + var budgetLimit = 2000m; + var actualAmount = 2500m; + + // Act + var result = BudgetItemCalculator.CalculateEffectiveAmount( + BudgetCategory.Expense, + budgetLimit, + actualAmount, + isMandatory: false, + isArchived: false, + new DateTime(2026, 2, 15), + BudgetPeriodType.Month + ); + + // Assert + result.Should().Be(2500m); + } + + [Fact] + public void 支出项未超预算_应返回预算值() + { + // Arrange + var budgetLimit = 2000m; + var actualAmount = 1800m; + + // Act + var result = BudgetItemCalculator.CalculateEffectiveAmount( + BudgetCategory.Expense, + budgetLimit, + actualAmount, + isMandatory: false, + isArchived: false, + new DateTime(2026, 2, 15), + BudgetPeriodType.Month + ); + + // Assert + result.Should().Be(2000m); + } + + [Fact] + public void 支出项超预算_应返回实际值() + { + // Arrange + var budgetLimit = 2000m; + var actualAmount = 2500m; + + // Act + var result = BudgetItemCalculator.CalculateEffectiveAmount( + BudgetCategory.Expense, + budgetLimit, + actualAmount, + isMandatory: false, + isArchived: false, + new DateTime(2026, 2, 15), + BudgetPeriodType.Month + ); + + // Assert + result.Should().Be(2500m); + } + + [Fact] + public void 支出项硬性且实际为0_月度_应按天数折算() + { + // Arrange + var budgetLimit = 3000m; + var actualAmount = 0m; + var date = new DateTime(2026, 2, 15); // 2月共28天,当前15号 + + // Act + var result = BudgetItemCalculator.CalculateEffectiveAmount( + BudgetCategory.Expense, + budgetLimit, + actualAmount, + isMandatory: true, + isArchived: false, + date, + BudgetPeriodType.Month + ); + + // Assert + var expected = 3000m / 28 * 15; // ≈ 1607.14 + result.Should().BeApproximately(expected, 0.01m); + } + + [Fact] + public void 支出项硬性且实际为0_年度_应按天数折算() + { + // Arrange + var budgetLimit = 12000m; + var actualAmount = 0m; + var date = new DateTime(2026, 2, 15); // 2026年第46天(31+15) + + // Act + var result = BudgetItemCalculator.CalculateEffectiveAmount( + BudgetCategory.Expense, + budgetLimit, + actualAmount, + isMandatory: true, + isArchived: false, + date, + BudgetPeriodType.Year + ); + + // Assert + var expected = 12000m / 365 * 46; // ≈ 1512.33 + result.Should().BeApproximately(expected, 0.01m); + } + + [Fact] + public void 支出项硬性且实际大于0_应返回MAX值() + { + // Arrange + var budgetLimit = 3000m; + var actualAmount = 3200m; + + // Act + var result = BudgetItemCalculator.CalculateEffectiveAmount( + BudgetCategory.Expense, + budgetLimit, + actualAmount, + isMandatory: true, + isArchived: false, + new DateTime(2026, 2, 15), + BudgetPeriodType.Month + ); + + // Assert + result.Should().Be(3200m); + } + + [Fact] + public void 归档数据_应直接返回实际值() + { + // Arrange + var budgetLimit = 2000m; + var actualAmount = 1800m; + + // Act + var result = BudgetItemCalculator.CalculateEffectiveAmount( + BudgetCategory.Expense, + budgetLimit, + actualAmount, + isMandatory: false, + isArchived: true, // 归档数据 + new DateTime(2026, 2, 15), + BudgetPeriodType.Month + ); + + // Assert + result.Should().Be(1800m); // 归档数据直接返回实际值,不走MAX逻辑 + } + + [Fact] + public void 闰年2月按天折算边界情况() + { + // Arrange + var budgetLimit = 3000m; + var actualAmount = 0m; + var date = new DateTime(2024, 2, 29); // 闰年2月29日 + + // Act + var result = BudgetItemCalculator.CalculateEffectiveAmount( + BudgetCategory.Expense, + budgetLimit, + actualAmount, + isMandatory: true, + isArchived: false, + date, + BudgetPeriodType.Month + ); + + // Assert + var expected = 3000m / 29 * 29; // = 3000 + result.Should().Be(expected); + } + + [Fact] + public void 平年2月按天折算边界情况() + { + // Arrange + var budgetLimit = 3000m; + var actualAmount = 0m; + var date = new DateTime(2026, 2, 28); // 平年2月28日 + + // Act + var result = BudgetItemCalculator.CalculateEffectiveAmount( + BudgetCategory.Expense, + budgetLimit, + actualAmount, + isMandatory: true, + isArchived: false, + date, + BudgetPeriodType.Month + ); + + // Assert + var expected = 3000m / 28 * 28; // = 3000 + result.Should().Be(expected); + } +} diff --git a/WebApi.Test/Budget/BudgetSavingsCalculationTest.cs b/WebApi.Test/Budget/BudgetSavingsCalculationTest.cs new file mode 100644 index 0000000..0fd4c36 --- /dev/null +++ b/WebApi.Test/Budget/BudgetSavingsCalculationTest.cs @@ -0,0 +1,135 @@ +using Service.Budget; + +namespace WebApi.Test.Budget; + +/// +/// 存款计划核心公式单元测试 +/// +public class BudgetSavingsCalculationTest : BaseTest +{ + [Fact] + public void 月度计划存款公式_纯月度预算场景() + { + // Arrange + var monthlyIncomeBudget = 15000m; // 工资10000 + 奖金5000 + var yearlyIncomeInThisMonth = 0m; + var monthlyExpenseBudget = 5000m; // 房租3000 + 餐饮2000 + var yearlyExpenseInThisMonth = 0m; + + // Act + var result = BudgetSavingsService.CalculateMonthlyPlannedSavings( + monthlyIncomeBudget, + yearlyIncomeInThisMonth, + monthlyExpenseBudget, + yearlyExpenseInThisMonth + ); + + // Assert + result.Should().Be(10000m); // 15000 - 5000 + } + + [Fact] + public void 月度计划存款公式_月度预算加本月发生的年度预算() + { + // Arrange + var monthlyIncomeBudget = 10000m; // 工资 + var yearlyIncomeInThisMonth = 0m; + var monthlyExpenseBudget = 3000m; // 房租 + var yearlyExpenseInThisMonth = 3000m; // 旅游实际发生 + + // Act + var result = BudgetSavingsService.CalculateMonthlyPlannedSavings( + monthlyIncomeBudget, + yearlyIncomeInThisMonth, + monthlyExpenseBudget, + yearlyExpenseInThisMonth + ); + + // Assert + result.Should().Be(4000m); // 10000 - 3000 - 3000 + } + + [Fact] + public void 月度计划存款公式_年度预算未在本月发生应不计入() + { + // Arrange + var monthlyIncomeBudget = 10000m; + var yearlyIncomeInThisMonth = 0m; // 年终奖未发生 + var monthlyExpenseBudget = 3000m; + var yearlyExpenseInThisMonth = 0m; // 旅游未发生 + + // Act + var result = BudgetSavingsService.CalculateMonthlyPlannedSavings( + monthlyIncomeBudget, + yearlyIncomeInThisMonth, + monthlyExpenseBudget, + yearlyExpenseInThisMonth + ); + + // Assert + result.Should().Be(7000m); // 10000 - 3000 + } + + [Fact] + public void 年度计划存款公式_年初无归档数据场景() + { + // Arrange + var archivedIncome = 0m; + var futureIncomeBudget = 120000m; // 10000×12 + var archivedExpense = 0m; + var futureExpenseBudget = 36000m; // 3000×12 + + // Act + var result = BudgetSavingsService.CalculateYearlyPlannedSavings( + archivedIncome, + futureIncomeBudget, + archivedExpense, + futureExpenseBudget + ); + + // Assert + result.Should().Be(84000m); // 120000 - 36000 + } + + [Fact] + public void 年度计划存款公式_年中有归档数据场景() + { + // Arrange + var archivedIncome = 29000m; // 1月15000 + 2月14000 + var futureIncomeBudget = 100000m; // 10000×10月 + var archivedExpense = 10000m; // 1月4800 + 2月5200 + var futureExpenseBudget = 30000m; // 3000×10月 + + // Act + var result = BudgetSavingsService.CalculateYearlyPlannedSavings( + archivedIncome, + futureIncomeBudget, + archivedExpense, + futureExpenseBudget + ); + + // Assert + result.Should().Be(89000m); // 29000 + 100000 - 10000 - 30000 + } + + [Fact] + public void 年度计划存款公式_归档数据包含年度预算() + { + // Arrange + var archivedIncome = 15000m; + var futureIncomeBudget = 110000m; + var archivedExpense = 7800m; // 包含1月旅游3000的年度支出 + var futureExpenseBudget = 30000m; + + // Act + var result = BudgetSavingsService.CalculateYearlyPlannedSavings( + archivedIncome, + futureIncomeBudget, + archivedExpense, + futureExpenseBudget + ); + + // Assert + result.Should().Be(87200m); // 15000 + 110000 - 7800 - 30000 + } +} diff --git a/openspec/changes/fix-deposit-detail-empty/.openspec.yaml b/openspec/changes/fix-deposit-detail-empty/.openspec.yaml new file mode 100644 index 0000000..d0ec88b --- /dev/null +++ b/openspec/changes/fix-deposit-detail-empty/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-02-20 diff --git a/openspec/changes/fix-deposit-detail-empty/design.md b/openspec/changes/fix-deposit-detail-empty/design.md new file mode 100644 index 0000000..3c2a74f --- /dev/null +++ b/openspec/changes/fix-deposit-detail-empty/design.md @@ -0,0 +1,117 @@ +## Context + +`SavingsBudgetContent.vue` 是一个显示存款计划列表和明细的组件。每个存款计划卡片可以点击查看详细信息,弹窗会显示: +1. 收入预算(预算限额和实际收入) +2. 支出预算(预算限额和实际支出) +3. 计划存款公式(收入预算 - 支出预算 = 计划存款) +4. 存款结果(计划存款、实际存款、还差) + +问题在于弹窗模板引用了 `incomeLimit`、`incomeCurrent`、`expenseLimit`、`expenseCurrent` 这些计算属性,但在 `