From aa8fc7a8b3b8238f7d9bfec27a5913417e2bf9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E8=AF=9A?= Date: Wed, 7 Jan 2026 20:31:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AD=98=E6=AC=BE?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E8=AE=BE=E7=BD=AE=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=A2=84=E7=AE=97=E7=AE=A1=E7=90=86=E7=95=8C?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Web/src/components/Budget/BudgetEditPopup.vue | 10 +- .../components/Budget/SavingsConfigPopup.vue | 181 ++++++++++++++++++ Web/src/views/BudgetView.vue | 32 +++- WebApi/Controllers/BudgetController.cs | 92 +++++++++ 4 files changed, 300 insertions(+), 15 deletions(-) create mode 100644 Web/src/components/Budget/SavingsConfigPopup.vue diff --git a/Web/src/components/Budget/BudgetEditPopup.vue b/Web/src/components/Budget/BudgetEditPopup.vue index 36d8386..a02fa25 100644 --- a/Web/src/components/Budget/BudgetEditPopup.vue +++ b/Web/src/components/Budget/BudgetEditPopup.vue @@ -2,7 +2,7 @@
@@ -175,14 +175,6 @@ const fetchCategories = async () => { } } -watch(() => form.category, (newVal, oldVal) => { - // 只有在手动切换类型且不是初始化编辑数据时才清空 - // 为简单起见,如果旧值存在(即不是第一次赋值),则清空已选分类 - if (oldVal !== undefined) { - form.selectedCategories = [] - } -}) - const onSubmit = async () => { try { const data = { diff --git a/Web/src/components/Budget/SavingsConfigPopup.vue b/Web/src/components/Budget/SavingsConfigPopup.vue new file mode 100644 index 0000000..8c429fc --- /dev/null +++ b/Web/src/components/Budget/SavingsConfigPopup.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/Web/src/views/BudgetView.vue b/Web/src/views/BudgetView.vue index 2e3ecb0..0926c05 100644 --- a/Web/src/views/BudgetView.vue +++ b/Web/src/views/BudgetView.vue @@ -2,7 +2,18 @@
@@ -123,12 +134,15 @@ status-tag-text="积累中" @toggle-stop="handleToggleStop" @switch-period="(dir) => handleSwitchPeriod(budget, dir)" - @click="budgetEditRef.open({ - data: budget, - isEditFlag: true, - category: budget.category - })" > + + @@ -176,9 +194,11 @@ import { BudgetPeriodType, BudgetCategory } from '@/constants/enums' import BudgetCard from '@/components/Budget/BudgetCard.vue' import BudgetSummary from '@/components/Budget/BudgetSummary.vue' import BudgetEditPopup from '@/components/Budget/BudgetEditPopup.vue' +import SavingsConfigPopup from '@/components/Budget/SavingsConfigPopup.vue' const activeTab = ref(BudgetCategory.Expense) const budgetEditRef = ref(null) +const savingsConfigRef = ref(null) const expenseBudgets = ref([]) const incomeBudgets = ref([]) diff --git a/WebApi/Controllers/BudgetController.cs b/WebApi/Controllers/BudgetController.cs index 079adfa..a4af796 100644 --- a/WebApi/Controllers/BudgetController.cs +++ b/WebApi/Controllers/BudgetController.cs @@ -4,6 +4,7 @@ [Route("api/[controller]/[action]")] public class BudgetController( IBudgetService budgetService, + IConfigService configService, ILogger logger) : ControllerBase { /// @@ -23,6 +24,10 @@ public class BudgetController( dtos.Add(BudgetDto.FromEntity(budget, currentAmount, referenceDate)); } + // 创造虚拟的存款预算 + dtos.Add(await GetVirtualSavingsDtoAsync(BudgetPeriodType.Month, referenceDate)); + dtos.Add(await GetVirtualSavingsDtoAsync(BudgetPeriodType.Year, referenceDate)); + return dtos.Ok(); } catch (Exception ex) @@ -40,6 +45,15 @@ public class BudgetController( { try { + if (id == -1) + { + return (await GetVirtualSavingsDtoAsync(BudgetPeriodType.Year, referenceDate)).Ok(); + } + if (id == -2) + { + return (await GetVirtualSavingsDtoAsync(BudgetPeriodType.Month, referenceDate)).Ok(); + } + var budget = await budgetService.GetByIdAsync(id); if (budget == null) return "预算不存在".Fail(); @@ -147,6 +161,84 @@ public class BudgetController( } } + private async Task GetVirtualSavingsDtoAsync(BudgetPeriodType periodType, DateTime? referenceDate = null) + { + var allBudgets = await budgetService.GetAllAsync(); + var date = referenceDate ?? DateTime.Now; + + decimal incomeLimitAtPeriod = 0; + decimal expenseLimitAtPeriod = 0; + + var savingsCategories = await configService.GetConfigByKeyAsync("SavingsCategories") ?? string.Empty; + var selectedCategoryList = savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries); + + foreach (var b in allBudgets) + { + if (b.IsStopped || b.Category == BudgetCategory.Savings) continue; + + // 如果设置了存款分类,并且预算有指定分类,则只统计相关的预算 + if (selectedCategoryList.Length > 0) + { + var budgetCategories = b.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries); + if (budgetCategories.Length > 0 && !budgetCategories.Intersect(selectedCategoryList).Any()) + { + continue; + } + } + + // 折算系数:根据当前请求的 periodType (Year 或 Month),将预算 b 的 Limit 折算过来 + decimal factor = 1.0m; + + if (periodType == BudgetPeriodType.Year) + { + factor = b.Type switch + { + BudgetPeriodType.Month => 12, + BudgetPeriodType.Week => 52, + BudgetPeriodType.Year => 1, + _ => 0 + }; + } + else if (periodType == BudgetPeriodType.Month) + { + factor = b.Type switch + { + BudgetPeriodType.Month => 1, + BudgetPeriodType.Week => 52m / 12m, + BudgetPeriodType.Year => 1m / 12m, + _ => 0 + }; + } + else + { + factor = 0; // 其他周期暂不计算虚拟存款 + } + + if (b.Category == BudgetCategory.Income) incomeLimitAtPeriod += b.Limit * factor; + else if (b.Category == BudgetCategory.Expense) expenseLimitAtPeriod += b.Limit * factor; + } + + var virtualBudget = new BudgetRecord + { + Id = periodType == BudgetPeriodType.Year ? -1 : -2, + Name = periodType == BudgetPeriodType.Year ? "年度存款" : "月度存款", + Category = BudgetCategory.Savings, + Type = periodType, + Limit = incomeLimitAtPeriod - expenseLimitAtPeriod, + StartDate = periodType == BudgetPeriodType.Year ? new DateTime(date.Year, 1, 1) : new DateTime(date.Year, date.Month, 1), + SelectedCategories = savingsCategories + }; + + // 计算实际发生的 收入 - 支出 + var incomeHelper = new BudgetRecord { Category = BudgetCategory.Income, Type = periodType, StartDate = virtualBudget.StartDate, SelectedCategories = savingsCategories }; + var expenseHelper = new BudgetRecord { Category = BudgetCategory.Expense, Type = periodType, StartDate = virtualBudget.StartDate, SelectedCategories = savingsCategories }; + + var actualIncome = await budgetService.CalculateCurrentAmountAsync(incomeHelper, date); + var actualExpense = await budgetService.CalculateCurrentAmountAsync(expenseHelper, date); + + return BudgetDto.FromEntity(virtualBudget, actualIncome - actualExpense, date); + } + private async Task ValidateBudgetSelectedCategoriesAsync(BudgetRecord record) { var allBudgets = await budgetService.GetAllAsync();