diff --git a/Repository/TransactionRecordRepository.cs b/Repository/TransactionRecordRepository.cs index 38ad5a2..3578cac 100644 --- a/Repository/TransactionRecordRepository.cs +++ b/Repository/TransactionRecordRepository.cs @@ -198,6 +198,16 @@ public interface ITransactionRecordRepository : IBaseRepository影响行数 Task ConfirmAllUnconfirmedAsync(long[] ids); + /// + /// 获取指定分类在指定时间范围内的每日/每月统计趋势 + /// + Task> GetFilteredTrendStatisticsAsync( + DateTime startDate, + DateTime endDate, + TransactionType type, + IEnumerable classifies, + bool groupByMonth = false); + /// /// 更新分类名称 /// @@ -719,6 +729,37 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository ids.Contains(t.Id)) .ExecuteAffrowsAsync(); } + + public async Task> GetFilteredTrendStatisticsAsync( + DateTime startDate, + DateTime endDate, + TransactionType type, + IEnumerable classifies, + bool groupByMonth = false) + { + var query = FreeSql.Select() + .Where(t => t.OccurredAt >= startDate && t.OccurredAt <= endDate && t.Type == type); + + if (classifies != null && classifies.Any()) + { + query = query.Where(t => classifies.Contains(t.Classify)); + } + + var list = await query.ToListAsync(t => new { t.OccurredAt, t.Amount }); + + if (groupByMonth) + { + return list + .GroupBy(t => new DateTime(t.OccurredAt.Year, t.OccurredAt.Month, 1)) + .ToDictionary(g => g.Key, g => g.Sum(x => Math.Abs(x.Amount))); + } + else + { + return list + .GroupBy(t => t.OccurredAt.Date) + .ToDictionary(g => g.Key, g => g.Sum(x => Math.Abs(x.Amount))); + } + } } /// diff --git a/Service/BudgetService.cs b/Service/BudgetService.cs index e3a6824..7ee0b23 100644 --- a/Service/BudgetService.cs +++ b/Service/BudgetService.cs @@ -230,6 +230,91 @@ public class BudgetService( result.Current = totalCurrent; result.Rate = totalLimit > 0 ? totalCurrent / totalLimit * 100 : 0; + // 计算每日/每月趋势 + var transactionType = category switch + { + BudgetCategory.Expense => TransactionType.Expense, + BudgetCategory.Income => TransactionType.Income, + _ => TransactionType.None + }; + + if (transactionType != TransactionType.None) + { + var hasGlobalBudget = relevant.Any(b => b.SelectedCategories == null || b.SelectedCategories.Length == 0); + + var allClassifies = hasGlobalBudget + ? new List() + : relevant + .SelectMany(b => b.SelectedCategories) + .Distinct() + .ToList(); + + DateTime startDate, endDate; + bool groupByMonth = false; + + if (statType == BudgetPeriodType.Month) + { + startDate = new DateTime(referenceDate.Year, referenceDate.Month, 1); + endDate = startDate.AddMonths(1).AddDays(-1); + groupByMonth = false; + } + else // Year + { + startDate = new DateTime(referenceDate.Year, 1, 1); + endDate = startDate.AddYears(1).AddDays(-1); + groupByMonth = true; + } + + var dailyStats = await transactionRecordRepository.GetFilteredTrendStatisticsAsync( + startDate, + endDate, + transactionType, + allClassifies, + groupByMonth); + + decimal accumulated = 0; + var now = DateTime.Now; + + if (statType == BudgetPeriodType.Month) + { + var daysInMonth = DateTime.DaysInMonth(startDate.Year, startDate.Month); + for (int i = 1; i <= daysInMonth; i++) + { + var currentDate = new DateTime(startDate.Year, startDate.Month, i); + if (currentDate.Date > now.Date) + { + result.Trend.Add(null); + continue; + } + + if (dailyStats.TryGetValue(currentDate.Date, out var amount)) + { + accumulated += amount; + } + result.Trend.Add(accumulated); + } + } + else // Year + { + for (int i = 1; i <= 12; i++) + { + var currentMonthDate = new DateTime(startDate.Year, i, 1); + + if (currentMonthDate.Year > now.Year || (currentMonthDate.Year == now.Year && i > now.Month)) + { + result.Trend.Add(null); + continue; + } + + if (dailyStats.TryGetValue(currentMonthDate, out var amount)) + { + accumulated += amount; + } + result.Trend.Add(accumulated); + } + } + } + return result; } @@ -896,6 +981,11 @@ public class BudgetStatsDto /// 预算项数量 /// public int Count { get; set; } + + /// + /// 每日/每月累计金额趋势(对应当前周期内的实际发生额累计值) + /// + public List Trend { get; set; } = new(); } /// diff --git a/Web/src/components/Budget/BudgetChartAnalysis.vue b/Web/src/components/Budget/BudgetChartAnalysis.vue index fd4c0d6..44ac709 100644 --- a/Web/src/components/Budget/BudgetChartAnalysis.vue +++ b/Web/src/components/Budget/BudgetChartAnalysis.vue @@ -80,22 +80,22 @@
- {{ monthBarChartExpanded ? '收起详情' : '展开详情' }} + {{ monthVarianceExpanded ? '收起偏差分析' : '展开偏差分析' }}
@@ -120,44 +120,22 @@
- {{ yearBarChartExpanded ? '收起详情' : '展开详情' }} + {{ yearVarianceExpanded ? '收起偏差分析' : '展开偏差分析' }}
-
- - -
-
-
- 预算偏差分析 -
-
- 红条超支 / - 绿条结余 -
-
-
@@ -175,7 +153,7 @@ @@ -1146,13 +945,12 @@ onUnmounted(() => { /* 调小高度 */ } -.bar-chart { +.variance-chart { min-height: 200px; - max-height: 400px; } .burndown-chart { - height: 200px; + height: 230px; } .gauge-footer { diff --git a/Web/src/views/BudgetView.vue b/Web/src/views/BudgetView.vue index 94919a9..2dafe40 100644 --- a/Web/src/views/BudgetView.vue +++ b/Web/src/views/BudgetView.vue @@ -657,13 +657,15 @@ const fetchCategoryStats = async () => { rate: data.month?.rate?.toFixed(1) || '0.0', current: data.month?.current || 0, limit: data.month?.limit || 0, - count: data.month?.count || 0 + count: data.month?.count || 0, + trend: data.month?.trend || [] }, year: { rate: data.year?.rate?.toFixed(1) || '0.0', current: data.year?.current || 0, limit: data.year?.limit || 0, - count: data.year?.count || 0 + count: data.year?.count || 0, + trend: data.year?.trend || [] } } } diff --git a/Web/src/views/CalendarView.vue b/Web/src/views/CalendarView.vue index cbea8bb..ce1efe0 100644 --- a/Web/src/views/CalendarView.vue +++ b/Web/src/views/CalendarView.vue @@ -1,4 +1,4 @@ -