diff --git a/Directory.Packages.props b/Directory.Packages.props index dad241b..945c95f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,8 @@  - + + @@ -20,7 +21,7 @@ - + diff --git a/Entity/BudgetArchive.cs b/Entity/BudgetArchive.cs index 22bca5f..2791a59 100644 --- a/Entity/BudgetArchive.cs +++ b/Entity/BudgetArchive.cs @@ -2,31 +2,6 @@ public class BudgetArchive : BaseEntity { - /// - /// 预算Id - /// - public long BudgetId { get; set; } - - /// - /// 预算周期类型 - /// - public BudgetPeriodType BudgetType { get; set; } - - /// - /// 预算金额 - /// - public decimal BudgetedAmount { get; set; } - - /// - /// 周期内实际发生金额 - /// - public decimal RealizedAmount { get; set; } - - /// - /// 详细描述 - /// - public string? Description { get; set; } - /// /// 归档目标年份 /// @@ -37,8 +12,54 @@ public class BudgetArchive : BaseEntity /// public int Month { get; set; } + /// + /// 归档内容 + /// + [JsonMap] + public BudgetArchiveContent[] Content { get; set; } = []; + /// /// 归档日期 /// public DateTime ArchiveDate { get; set; } = DateTime.Now; + + public string? Summary { get; set; } } + +public record BudgetArchiveContent +{ + /// + /// 预算名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 统计周期 + /// + public BudgetPeriodType Type { get; set; } = BudgetPeriodType.Month; + + /// + /// 预算金额 + /// + public decimal Limit { get; set; } + + /// + /// 实际金额 + /// + public decimal Actual { get; set; } + + /// + /// 预算类别 + /// + public BudgetCategory Category { get; set; } + + /// + /// 相关分类 (逗号分隔的分类名称) + /// + public string[] SelectedCategories { get; set; } = []; + + /// + /// 描述说明 + /// + public string Description { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Entity/Entity.csproj b/Entity/Entity.csproj index 0a0375f..eca8fde 100644 --- a/Entity/Entity.csproj +++ b/Entity/Entity.csproj @@ -1,6 +1,7 @@  + diff --git a/Repository/BudgetArchiveRepository.cs b/Repository/BudgetArchiveRepository.cs index 1824206..02ff05e 100644 --- a/Repository/BudgetArchiveRepository.cs +++ b/Repository/BudgetArchiveRepository.cs @@ -2,7 +2,8 @@ public interface IBudgetArchiveRepository : IBaseRepository { - Task GetArchiveAsync(long budgetId, int year, int month); + Task GetArchiveAsync(int year, int month); + Task> GetListAsync(int year, int month); } @@ -10,11 +11,10 @@ public class BudgetArchiveRepository( IFreeSql freeSql ) : BaseRepository(freeSql), IBudgetArchiveRepository { - public async Task GetArchiveAsync(long budgetId, int year, int month) + public async Task GetArchiveAsync(int year, int month) { return await FreeSql.Select() - .Where(a => a.BudgetId == budgetId && - a.Year == year && + .Where(a => a.Year == year && a.Month == month) .ToOneAsync(); } @@ -22,13 +22,7 @@ public class BudgetArchiveRepository( public async Task> GetListAsync(int year, int month) { return await FreeSql.Select() - .Where( - a => a.BudgetType == BudgetPeriodType.Month && - a.Year == year && - a.Month == month || - a.BudgetType == BudgetPeriodType.Year && - a.Year == year - ) + .Where(a => a.Year == year && a.Month == month) .ToListAsync(); } } \ No newline at end of file diff --git a/Service/BudgetService.cs b/Service/BudgetService.cs index 9e0031f..bb28431 100644 --- a/Service/BudgetService.cs +++ b/Service/BudgetService.cs @@ -2,21 +2,23 @@ public interface IBudgetService { - Task> GetListAsync(DateTime? referenceDate = null); - - Task GetStatisticsAsync(long id, DateTime referenceDate); + Task> GetListAsync(DateTime referenceDate); Task ArchiveBudgetsAsync(int year, int month); /// /// 获取指定分类的统计信息(月度和年度) /// - Task GetCategoryStatsAsync(BudgetCategory category, DateTime? referenceDate = null); + Task GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate); /// /// 获取未被预算覆盖的分类统计信息 /// Task> GetUncoveredCategoriesAsync(BudgetCategory category, DateTime? referenceDate = null); + + Task GetArchiveSummaryAsync(int year, int month); + + Task UpdateArchiveSummaryAsync(int year, int month, string? summary); } public class BudgetService( @@ -29,8 +31,38 @@ public class BudgetService( ILogger logger ) : IBudgetService { - public async Task> GetListAsync(DateTime? referenceDate = null) + public async Task> GetListAsync(DateTime referenceDate) { + var year = referenceDate.Year; + var month = referenceDate.Month; + + var isArchive = year < DateTime.Now.Year + || (year == DateTime.Now.Year && month < DateTime.Now.Month); + + if (isArchive) + { + var archive = await budgetArchiveRepository.GetArchiveAsync(year, month); + + if (archive != null) + { + var periodRange = GetPeriodRange(DateTime.Now, BudgetPeriodType.Month, referenceDate); + return archive.Content.Select(c => new BudgetResult + { + Name = c.Name, + Type = c.Type, + Limit = c.Limit, + Current = c.Actual, + Category = c.Category, + SelectedCategories = c.SelectedCategories, + Description = c.Description, + PeriodStart = periodRange.start, + PeriodEnd = periodRange.end, + }).ToList(); + } + + logger.LogWarning("获取预算列表时发现归档数据缺失,Year: {Year}, Month: {Month}", year, month); + } + var budgets = await budgetRepository.GetAllAsync(); var dtos = new List(); @@ -53,104 +85,17 @@ public class BudgetService( return dtos.Where(dto => dto != null).Cast().ToList(); } - public async Task GetStatisticsAsync(long id, DateTime referenceDate) + public async Task GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate) { - bool isArchive = false; - BudgetRecord? budget = null; - if (id == -1) - { - if (isAcrhiveFunc(BudgetPeriodType.Year)) - { - isArchive = true; - budget = await BuildVirtualSavingsBudgetRecordAsync(-1, referenceDate, 0); - } - - } - else if (id == -2) - { - if (isAcrhiveFunc(BudgetPeriodType.Month)) - { - isArchive = true; - budget = await BuildVirtualSavingsBudgetRecordAsync(-2, referenceDate, 0); - } - } - else - { - budget = await budgetRepository.GetByIdAsync(id); - - if (budget == null) - { - return null; - } - - isArchive = isAcrhiveFunc(budget.Type); - } - - if (isArchive && budget != null) - { - var archive = await budgetArchiveRepository.GetArchiveAsync( - id, - referenceDate.Year, - referenceDate.Month); - - if (archive != null) // 存在归档 直接读取归档数据 - { - budget.Limit = archive.BudgetedAmount; - return BudgetResult.FromEntity( - budget, - archive.RealizedAmount, - referenceDate, - archive.Description ?? string.Empty); - } - } - - - if (id == -1) - { - return await GetVirtualSavingsDtoAsync(BudgetPeriodType.Year, referenceDate); - } - if (id == -2) - { - return await GetVirtualSavingsDtoAsync(BudgetPeriodType.Month, referenceDate); - } - - budget = await budgetRepository.GetByIdAsync(id); - if (budget == null) - { - return null; - } - - var currentAmount = await CalculateCurrentAmountAsync(budget, referenceDate); - return BudgetResult.FromEntity(budget, currentAmount, referenceDate); - - bool isAcrhiveFunc(BudgetPeriodType periodType) - { - if (periodType == BudgetPeriodType.Year) - { - return DateTime.Now.Year > referenceDate.Year; - } - else if (periodType == BudgetPeriodType.Month) - { - return DateTime.Now.Year > referenceDate.Year - || (DateTime.Now.Year == referenceDate.Year - && DateTime.Now.Month > referenceDate.Month); - } - return false; - } - } - - public async Task GetCategoryStatsAsync(BudgetCategory category, DateTime? referenceDate = null) - { - var budgets = (await budgetRepository.GetAllAsync()).ToList(); - var refDate = referenceDate ?? DateTime.Now; + var budgets = await GetListAsync(referenceDate); var result = new BudgetCategoryStats(); // 获取月度统计 - result.Month = await CalculateCategoryStatsAsync(budgets, category, BudgetPeriodType.Month, refDate); + result.Month = await CalculateCategoryStatsAsync(budgets, category, BudgetPeriodType.Month, referenceDate); // 获取年度统计 - result.Year = await CalculateCategoryStatsAsync(budgets, category, BudgetPeriodType.Year, refDate); + result.Year = await CalculateCategoryStatsAsync(budgets, category, BudgetPeriodType.Year, referenceDate); return result; } @@ -190,8 +135,30 @@ public class BudgetService( .ToList(); } + public async Task GetArchiveSummaryAsync(int year, int month) + { + var archive = await budgetArchiveRepository.GetArchiveAsync(year, month); + return archive?.Summary; + } + + public async Task UpdateArchiveSummaryAsync(int year, int month, string? summary) + { + var archive = await budgetArchiveRepository.GetArchiveAsync(year, month); + if (archive == null) + { + await ArchiveBudgetsAsync(year, month); + archive = await budgetArchiveRepository.GetArchiveAsync(year, month); + } + + if (archive != null) + { + archive.Summary = summary; + await budgetArchiveRepository.UpdateAsync(archive); + } + } + private async Task CalculateCategoryStatsAsync( - List budgets, + List budgets, BudgetCategory category, BudgetPeriodType statType, DateTime referenceDate) @@ -236,7 +203,15 @@ public class BudgetService( totalLimit += itemLimit; // 当前值累加 - var currentAmount = await CalculateCurrentAmountAsync(budget, referenceDate); + var currentAmount = await CalculateCurrentAmountAsync(new() + { + Name = budget.Name, + Type = budget.Type, + Limit = budget.Limit, + Category = budget.Category, + SelectedCategories = string.Join(',', budget.SelectedCategories), + StartDate = new DateTime(referenceDate.Year, referenceDate.Month, 1) + }, referenceDate); if (budget.Type == statType) { totalCurrent += currentAmount; @@ -263,55 +238,49 @@ public class BudgetService( public async Task ArchiveBudgetsAsync(int year, int month) { var referenceDate = new DateTime(year, month, 1); + var budgets = await GetListAsync(referenceDate); - var addArchives = new List(); - var updateArchives = new List(); - foreach (var budget in budgets) + var content = budgets.Select(b => new BudgetArchiveContent { - var archive = await budgetArchiveRepository.GetArchiveAsync(budget.Id, year, month); + Name = b.Name, + Type = b.Type, + Limit = b.Limit, + Actual = b.Current, + Category = b.Category, + SelectedCategories = b.SelectedCategories, + Description = b.Description + }).ToArray(); - if (archive != null) - { - archive.RealizedAmount = budget.Current; - archive.ArchiveDate = DateTime.Now; - archive.Description = budget.Description; - updateArchives.Add(archive); - } - else - { - archive = new BudgetArchive - { - BudgetId = budget.Id, - BudgetType = budget.Type, - Year = year, - Month = month, - BudgetedAmount = budget.Limit, - RealizedAmount = budget.Current, - Description = budget.Description, - ArchiveDate = DateTime.Now - }; + var archive = await budgetArchiveRepository.GetArchiveAsync(year, month); - addArchives.Add(archive); - } - } - - if (addArchives.Count > 0) + if (archive != null) { - if (!await budgetArchiveRepository.AddRangeAsync(addArchives)) - { - return "保存预算归档失败"; - } - } - if (updateArchives.Count > 0) - { - if (!await budgetArchiveRepository.UpdateRangeAsync(updateArchives)) + archive.Content = content; + archive.ArchiveDate = DateTime.Now; + if (!await budgetArchiveRepository.UpdateAsync(archive)) { return "更新预算归档失败"; } } + else + { + archive = new BudgetArchive + { + Year = year, + Month = month, + Content = content, + ArchiveDate = DateTime.Now + }; + + if (!await budgetArchiveRepository.AddAsync(archive)) + { + return "保存预算归档失败"; + } + } _ = NotifyAsync(year, month); + return string.Empty; } @@ -320,22 +289,16 @@ public class BudgetService( try { var archives = await budgetArchiveRepository.GetListAsync(year, month); - var budgets = await budgetRepository.GetAllAsync(); - var budgetMap = budgets.ToDictionary(b => b.Id, b => b); - var archiveData = archives.Select(a => + var archiveData = archives.SelectMany(a => a.Content.Select(c => new { - budgetMap.TryGetValue(a.BudgetId, out var br); - var name = br?.Name ?? (a.BudgetId == -1 ? "年度存款" : a.BudgetId == -2 ? "月度存款" : "未知"); - return new - { - Name = name, - Type = a.BudgetType.ToString(), - Limit = a.BudgetedAmount, - Actual = a.RealizedAmount, - Category = br?.Category.ToString() ?? (a.BudgetId < 0 ? "Savings" : "Unknown") - }; - }).ToList(); + c.Name, + Type = c.Type.ToString(), + c.Limit, + c.Actual, + Category = c.Category.ToString(), + c.SelectedCategories + })).ToList(); var yearTransactions = await transactionRecordRepository.ExecuteDynamicSqlAsync( $""" @@ -368,9 +331,9 @@ public class BudgetService( ); // 分析未被预算覆盖的分类 (仅针对支出类型 Type=0) - var budgetedCategories = budgets - .Where(b => !string.IsNullOrEmpty(b.SelectedCategories)) - .SelectMany(b => b.SelectedCategories.Split(',')) + var budgetedCategories = archiveData + .SelectMany(b => b.SelectedCategories) + .Where(c => !string.IsNullOrEmpty(c)) .Distinct() .ToHashSet(); @@ -567,10 +530,10 @@ public class BudgetService( - 名称 - 金额 - 折算 - 合计 + + + + diff --git a/Service/Jobs/BudgetArchiveJob.cs b/Service/Jobs/BudgetArchiveJob.cs index e93a195..cde9bca 100644 --- a/Service/Jobs/BudgetArchiveJob.cs +++ b/Service/Jobs/BudgetArchiveJob.cs @@ -23,6 +23,8 @@ public class BudgetArchiveJob( using var scope = serviceProvider.CreateScope(); var budgetService = scope.ServiceProvider.GetRequiredService(); + + // 归档月度数据 var result = await budgetService.ArchiveBudgetsAsync(year, month); if (string.IsNullOrEmpty(result)) diff --git a/Web/src/api/budget.js b/Web/src/api/budget.js index 0b49aba..4720485 100644 --- a/Web/src/api/budget.js +++ b/Web/src/api/budget.js @@ -12,19 +12,6 @@ export function getBudgetList(referenceDate) { }) } -/** - * 获取单个预算统计 - * @param {number} id 预算ID - * @param {string} referenceDate 参考日期 - */ -export function getBudgetStatistics(id, referenceDate) { - return request({ - url: '/Budget/GetStatistics', - method: 'get', - params: { id, referenceDate } - }) -} - /** * 创建预算 * @param {object} data 预算数据 @@ -84,15 +71,27 @@ export function getUncoveredCategories(category, referenceDate) { params: { category, referenceDate } }) } + /** - * 归档预算 - * @param {number} year 年份 - * @param {number} month 月份 + * 获取归档总结 + * @param {string} referenceDate 参考日期 */ -export function archiveBudgets(year, month) { +export function getArchiveSummary(referenceDate) { return request({ - url: `/Budget/ArchiveBudgetsAsync/${year}/${month}`, - method: 'post' + url: '/Budget/GetArchiveSummary', + method: 'get', + params: { referenceDate } }) } +/** + * 更新归档总结 + * @param {object} data 数据 { referenceDate, summary } + */ +export function updateArchiveSummary(data) { + return request({ + url: '/Budget/UpdateArchiveSummary', + method: 'post', + data + }) +} diff --git a/Web/src/components/Budget/BudgetCard.vue b/Web/src/components/Budget/BudgetCard.vue index 6c2c397..c4da0cc 100644 --- a/Web/src/components/Budget/BudgetCard.vue +++ b/Web/src/components/Budget/BudgetCard.vue @@ -42,9 +42,8 @@ - -
-
+
+
- -
-
@@ -205,10 +180,9 @@ const props = defineProps({ } }) -const emit = defineEmits(['switch-period', 'click']) +const emit = defineEmits(['click']) const isExpanded = ref(props.budget.category === 2) -const transitionName = ref('slide-left') const showDescription = ref(false) const showBillListModal = ref(false) const billList = ref([]) @@ -218,16 +192,6 @@ const toggleExpand = () => { isExpanded.value = !isExpanded.value } -const isNextDisabled = computed(() => { - if (!props.budget.periodEnd) return false - return new Date(props.budget.periodEnd) > new Date() -}) - -const handleSwitch = (direction) => { - transitionName.value = direction > 0 ? 'slide-left' : 'slide-right' - emit('switch-period', direction) -} - const handleQueryBills = async () => { showBillListModal.value = true billLoading.value = true @@ -402,40 +366,6 @@ const timePercentage = computed(() => { cursor: pointer; } -/* 切换动画 */ -.slide-left-enter-active, -.slide-left-leave-active, -.slide-right-enter-active, -.slide-right-leave-active { - transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); -} - -.slide-left-enter-from { - transform: translateX(100%); - opacity: 0; -} -.slide-left-leave-to { - transform: translateX(-100%); - opacity: 0; -} - -.slide-right-enter-from { - transform: translateX(-100%); - opacity: 0; -} -.slide-right-leave-to { - transform: translateX(100%); - opacity: 0; -} - -.slide-left-leave-active, -.slide-right-leave-active { - position: absolute; - top: 0; - left: 0; - width: 100%; -} - .budget-info { display: flex; align-items: center; @@ -564,42 +494,7 @@ const timePercentage = computed(() => { line-height: 1.4; } -.card-footer { - display: flex; - justify-content: space-between; - align-items: center; - color: #969799; - padding: 12px 12px 0; - padding-top: 8px; - border-top: 1px solid #ebedf0; -} - -.period-navigation { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; -} - -.period-text { - font-size: 14px; - font-weight: 500; - color: #323233; -} - -.nav-icon { - padding: 4px; - font-size: 12px; - color: #1989fa; -} - @media (prefers-color-scheme: dark) { - .card-footer { - border-top-color: #2c2c2c; - } - .period-text { - color: #f5f5f5; - } .budget-description { background-color: #2c2c2c; } diff --git a/Web/src/styles/rich-content.css b/Web/src/styles/rich-content.css index ffae849..fe09869 100644 --- a/Web/src/styles/rich-content.css +++ b/Web/src/styles/rich-content.css @@ -71,8 +71,9 @@ .rich-html-content thead, .rich-html-content tbody { display: table; - min-width: 100%; + width: 100%; table-layout: fixed; /* 核心:强制列宽分配逻辑一致 */ + border-collapse: collapse; } .rich-html-content tr { @@ -86,10 +87,11 @@ text-align: left; border: none; border-bottom: 1px solid var(--van-border-color-light); - font-size: 12px; + font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + box-sizing: border-box; } .rich-html-content th { diff --git a/Web/src/views/BudgetView.vue b/Web/src/views/BudgetView.vue index 1f3f8fb..2e1fead 100644 --- a/Web/src/views/BudgetView.vue +++ b/Web/src/views/BudgetView.vue @@ -3,23 +3,36 @@ @@ -43,7 +56,6 @@ :progress-color="getProgressColor(budget)" :percent-class="{ 'warning': (budget.current / budget.limit) > 0.8 }" :period-label="getPeriodLabel(budget.type)" - @switch-period="(dir) => handleSwitchPeriod(budget, dir)" @click="budgetEditRef.open({ data: budget, isEditFlag: true, @@ -95,7 +107,6 @@ :progress-color="getIncomeProgressColor(budget)" :percent-class="{ 'income': (budget.current / budget.limit) >= 1 }" :period-label="getPeriodLabel(budget.type)" - @switch-period="(dir) => handleSwitchPeriod(budget, dir)" @click="budgetEditRef.open({ data: budget, isEditFlag: true, @@ -142,7 +153,6 @@ :percent-class="{ 'income': (budget.current / budget.limit) >= 1 }" :period-label="getPeriodLabel(budget.type)" style="margin: 0 12px 12px;" - @switch-period="(dir) => handleSwitchPeriod(budget, dir)" > + + +
+ +
+ +
名称金额折算合计