更新预算归档功能,添加归档总结和更新归档总结接口,优化预算统计逻辑,调整相关样式
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 34s
Docker Build & Deploy / Deploy to Production (push) Successful in 9s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 34s
Docker Build & Deploy / Deploy to Production (push) Successful in 9s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Email & MIME Libraries -->
|
<!-- Email & MIME Libraries -->
|
||||||
<PackageVersion Include="FreeSql" Version="3.5.304" />
|
<PackageVersion Include="FreeSql" Version="3.5.305" />
|
||||||
|
<PackageVersion Include="FreeSql.Extensions.JsonMap" Version="3.5.305" />
|
||||||
<PackageVersion Include="MailKit" Version="4.14.1" />
|
<PackageVersion Include="MailKit" Version="4.14.1" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
|
||||||
<PackageVersion Include="MimeKit" Version="4.14.0" />
|
<PackageVersion Include="MimeKit" Version="4.14.0" />
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||||
<PackageVersion Include="Scalar.AspNetCore" Version="2.11.9" />
|
<PackageVersion Include="Scalar.AspNetCore" Version="2.11.9" />
|
||||||
<!-- Database -->
|
<!-- Database -->
|
||||||
<PackageVersion Include="FreeSql.Provider.Sqlite" Version="3.5.304" />
|
<PackageVersion Include="FreeSql.Provider.Sqlite" Version="3.5.305" />
|
||||||
<PackageVersion Include="WebPush" Version="1.0.12" />
|
<PackageVersion Include="WebPush" Version="1.0.12" />
|
||||||
<PackageVersion Include="Yitter.IdGenerator" Version="1.0.14" />
|
<PackageVersion Include="Yitter.IdGenerator" Version="1.0.14" />
|
||||||
<!-- File Processing -->
|
<!-- File Processing -->
|
||||||
|
|||||||
@@ -2,31 +2,6 @@
|
|||||||
|
|
||||||
public class BudgetArchive : BaseEntity
|
public class BudgetArchive : BaseEntity
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 预算Id
|
|
||||||
/// </summary>
|
|
||||||
public long BudgetId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 预算周期类型
|
|
||||||
/// </summary>
|
|
||||||
public BudgetPeriodType BudgetType { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 预算金额
|
|
||||||
/// </summary>
|
|
||||||
public decimal BudgetedAmount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 周期内实际发生金额
|
|
||||||
/// </summary>
|
|
||||||
public decimal RealizedAmount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 详细描述
|
|
||||||
/// </summary>
|
|
||||||
public string? Description { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 归档目标年份
|
/// 归档目标年份
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -37,8 +12,54 @@ public class BudgetArchive : BaseEntity
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Month { get; set; }
|
public int Month { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 归档内容
|
||||||
|
/// </summary>
|
||||||
|
[JsonMap]
|
||||||
|
public BudgetArchiveContent[] Content { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 归档日期
|
/// 归档日期
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime ArchiveDate { get; set; } = DateTime.Now;
|
public DateTime ArchiveDate { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
public string? Summary { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record BudgetArchiveContent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 预算名称
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统计周期
|
||||||
|
/// </summary>
|
||||||
|
public BudgetPeriodType Type { get; set; } = BudgetPeriodType.Month;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预算金额
|
||||||
|
/// </summary>
|
||||||
|
public decimal Limit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实际金额
|
||||||
|
/// </summary>
|
||||||
|
public decimal Actual { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预算类别
|
||||||
|
/// </summary>
|
||||||
|
public BudgetCategory Category { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 相关分类 (逗号分隔的分类名称)
|
||||||
|
/// </summary>
|
||||||
|
public string[] SelectedCategories { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 描述说明
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FreeSql" />
|
<PackageReference Include="FreeSql" />
|
||||||
|
<PackageReference Include="FreeSql.Extensions.JsonMap" />
|
||||||
<PackageReference Include="Yitter.IdGenerator" />
|
<PackageReference Include="Yitter.IdGenerator" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
public interface IBudgetArchiveRepository : IBaseRepository<BudgetArchive>
|
public interface IBudgetArchiveRepository : IBaseRepository<BudgetArchive>
|
||||||
{
|
{
|
||||||
Task<BudgetArchive?> GetArchiveAsync(long budgetId, int year, int month);
|
Task<BudgetArchive?> GetArchiveAsync(int year, int month);
|
||||||
|
|
||||||
Task<List<BudgetArchive>> GetListAsync(int year, int month);
|
Task<List<BudgetArchive>> GetListAsync(int year, int month);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10,11 +11,10 @@ public class BudgetArchiveRepository(
|
|||||||
IFreeSql freeSql
|
IFreeSql freeSql
|
||||||
) : BaseRepository<BudgetArchive>(freeSql), IBudgetArchiveRepository
|
) : BaseRepository<BudgetArchive>(freeSql), IBudgetArchiveRepository
|
||||||
{
|
{
|
||||||
public async Task<BudgetArchive?> GetArchiveAsync(long budgetId, int year, int month)
|
public async Task<BudgetArchive?> GetArchiveAsync(int year, int month)
|
||||||
{
|
{
|
||||||
return await FreeSql.Select<BudgetArchive>()
|
return await FreeSql.Select<BudgetArchive>()
|
||||||
.Where(a => a.BudgetId == budgetId &&
|
.Where(a => a.Year == year &&
|
||||||
a.Year == year &&
|
|
||||||
a.Month == month)
|
a.Month == month)
|
||||||
.ToOneAsync();
|
.ToOneAsync();
|
||||||
}
|
}
|
||||||
@@ -22,13 +22,7 @@ public class BudgetArchiveRepository(
|
|||||||
public async Task<List<BudgetArchive>> GetListAsync(int year, int month)
|
public async Task<List<BudgetArchive>> GetListAsync(int year, int month)
|
||||||
{
|
{
|
||||||
return await FreeSql.Select<BudgetArchive>()
|
return await FreeSql.Select<BudgetArchive>()
|
||||||
.Where(
|
.Where(a => a.Year == year && a.Month == month)
|
||||||
a => a.BudgetType == BudgetPeriodType.Month &&
|
|
||||||
a.Year == year &&
|
|
||||||
a.Month == month ||
|
|
||||||
a.BudgetType == BudgetPeriodType.Year &&
|
|
||||||
a.Year == year
|
|
||||||
)
|
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,21 +2,23 @@
|
|||||||
|
|
||||||
public interface IBudgetService
|
public interface IBudgetService
|
||||||
{
|
{
|
||||||
Task<List<BudgetResult>> GetListAsync(DateTime? referenceDate = null);
|
Task<List<BudgetResult>> GetListAsync(DateTime referenceDate);
|
||||||
|
|
||||||
Task<BudgetResult?> GetStatisticsAsync(long id, DateTime referenceDate);
|
|
||||||
|
|
||||||
Task<string> ArchiveBudgetsAsync(int year, int month);
|
Task<string> ArchiveBudgetsAsync(int year, int month);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取指定分类的统计信息(月度和年度)
|
/// 获取指定分类的统计信息(月度和年度)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime? referenceDate = null);
|
Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取未被预算覆盖的分类统计信息
|
/// 获取未被预算覆盖的分类统计信息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<List<UncoveredCategoryDetail>> GetUncoveredCategoriesAsync(BudgetCategory category, DateTime? referenceDate = null);
|
Task<List<UncoveredCategoryDetail>> GetUncoveredCategoriesAsync(BudgetCategory category, DateTime? referenceDate = null);
|
||||||
|
|
||||||
|
Task<string?> GetArchiveSummaryAsync(int year, int month);
|
||||||
|
|
||||||
|
Task UpdateArchiveSummaryAsync(int year, int month, string? summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BudgetService(
|
public class BudgetService(
|
||||||
@@ -29,8 +31,38 @@ public class BudgetService(
|
|||||||
ILogger<BudgetService> logger
|
ILogger<BudgetService> logger
|
||||||
) : IBudgetService
|
) : IBudgetService
|
||||||
{
|
{
|
||||||
public async Task<List<BudgetResult>> GetListAsync(DateTime? referenceDate = null)
|
public async Task<List<BudgetResult>> 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 budgets = await budgetRepository.GetAllAsync();
|
||||||
var dtos = new List<BudgetResult?>();
|
var dtos = new List<BudgetResult?>();
|
||||||
|
|
||||||
@@ -53,104 +85,17 @@ public class BudgetService(
|
|||||||
return dtos.Where(dto => dto != null).Cast<BudgetResult>().ToList();
|
return dtos.Where(dto => dto != null).Cast<BudgetResult>().ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<BudgetResult?> GetStatisticsAsync(long id, DateTime referenceDate)
|
public async Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
|
||||||
{
|
{
|
||||||
bool isArchive = false;
|
var budgets = await GetListAsync(referenceDate);
|
||||||
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<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime? referenceDate = null)
|
|
||||||
{
|
|
||||||
var budgets = (await budgetRepository.GetAllAsync()).ToList();
|
|
||||||
var refDate = referenceDate ?? DateTime.Now;
|
|
||||||
|
|
||||||
var result = new BudgetCategoryStats();
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -190,8 +135,30 @@ public class BudgetService(
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string?> 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<BudgetStatsDto> CalculateCategoryStatsAsync(
|
private async Task<BudgetStatsDto> CalculateCategoryStatsAsync(
|
||||||
List<BudgetRecord> budgets,
|
List<BudgetResult> budgets,
|
||||||
BudgetCategory category,
|
BudgetCategory category,
|
||||||
BudgetPeriodType statType,
|
BudgetPeriodType statType,
|
||||||
DateTime referenceDate)
|
DateTime referenceDate)
|
||||||
@@ -236,7 +203,15 @@ public class BudgetService(
|
|||||||
totalLimit += itemLimit;
|
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)
|
if (budget.Type == statType)
|
||||||
{
|
{
|
||||||
totalCurrent += currentAmount;
|
totalCurrent += currentAmount;
|
||||||
@@ -263,55 +238,49 @@ public class BudgetService(
|
|||||||
public async Task<string> ArchiveBudgetsAsync(int year, int month)
|
public async Task<string> ArchiveBudgetsAsync(int year, int month)
|
||||||
{
|
{
|
||||||
var referenceDate = new DateTime(year, month, 1);
|
var referenceDate = new DateTime(year, month, 1);
|
||||||
|
|
||||||
var budgets = await GetListAsync(referenceDate);
|
var budgets = await GetListAsync(referenceDate);
|
||||||
|
|
||||||
var addArchives = new List<BudgetArchive>();
|
var content = budgets.Select(b => new BudgetArchiveContent
|
||||||
var updateArchives = new List<BudgetArchive>();
|
|
||||||
foreach (var budget in budgets)
|
|
||||||
{
|
{
|
||||||
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)
|
var archive = await budgetArchiveRepository.GetArchiveAsync(year, month);
|
||||||
{
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
addArchives.Add(archive);
|
if (archive != null)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addArchives.Count > 0)
|
|
||||||
{
|
{
|
||||||
if (!await budgetArchiveRepository.AddRangeAsync(addArchives))
|
archive.Content = content;
|
||||||
{
|
archive.ArchiveDate = DateTime.Now;
|
||||||
return "保存预算归档失败";
|
if (!await budgetArchiveRepository.UpdateAsync(archive))
|
||||||
}
|
|
||||||
}
|
|
||||||
if (updateArchives.Count > 0)
|
|
||||||
{
|
|
||||||
if (!await budgetArchiveRepository.UpdateRangeAsync(updateArchives))
|
|
||||||
{
|
{
|
||||||
return "更新预算归档失败";
|
return "更新预算归档失败";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
archive = new BudgetArchive
|
||||||
|
{
|
||||||
|
Year = year,
|
||||||
|
Month = month,
|
||||||
|
Content = content,
|
||||||
|
ArchiveDate = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!await budgetArchiveRepository.AddAsync(archive))
|
||||||
|
{
|
||||||
|
return "保存预算归档失败";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ = NotifyAsync(year, month);
|
_ = NotifyAsync(year, month);
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,22 +289,16 @@ public class BudgetService(
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var archives = await budgetArchiveRepository.GetListAsync(year, month);
|
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);
|
c.Name,
|
||||||
var name = br?.Name ?? (a.BudgetId == -1 ? "年度存款" : a.BudgetId == -2 ? "月度存款" : "未知");
|
Type = c.Type.ToString(),
|
||||||
return new
|
c.Limit,
|
||||||
{
|
c.Actual,
|
||||||
Name = name,
|
Category = c.Category.ToString(),
|
||||||
Type = a.BudgetType.ToString(),
|
c.SelectedCategories
|
||||||
Limit = a.BudgetedAmount,
|
})).ToList();
|
||||||
Actual = a.RealizedAmount,
|
|
||||||
Category = br?.Category.ToString() ?? (a.BudgetId < 0 ? "Savings" : "Unknown")
|
|
||||||
};
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
var yearTransactions = await transactionRecordRepository.ExecuteDynamicSqlAsync(
|
var yearTransactions = await transactionRecordRepository.ExecuteDynamicSqlAsync(
|
||||||
$"""
|
$"""
|
||||||
@@ -368,9 +331,9 @@ public class BudgetService(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 分析未被预算覆盖的分类 (仅针对支出类型 Type=0)
|
// 分析未被预算覆盖的分类 (仅针对支出类型 Type=0)
|
||||||
var budgetedCategories = budgets
|
var budgetedCategories = archiveData
|
||||||
.Where(b => !string.IsNullOrEmpty(b.SelectedCategories))
|
.SelectMany(b => b.SelectedCategories)
|
||||||
.SelectMany(b => b.SelectedCategories.Split(','))
|
.Where(c => !string.IsNullOrEmpty(c))
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
@@ -567,10 +530,10 @@ public class BudgetService(
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th">名称</th>
|
<th>名称</th>
|
||||||
<th">金额</th>
|
<th>金额</th>
|
||||||
<th">折算</th>
|
<th>折算</th>
|
||||||
<th">合计</th>
|
<th>合计</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ public class BudgetArchiveJob(
|
|||||||
|
|
||||||
using var scope = serviceProvider.CreateScope();
|
using var scope = serviceProvider.CreateScope();
|
||||||
var budgetService = scope.ServiceProvider.GetRequiredService<IBudgetService>();
|
var budgetService = scope.ServiceProvider.GetRequiredService<IBudgetService>();
|
||||||
|
|
||||||
|
// 归档月度数据
|
||||||
var result = await budgetService.ArchiveBudgetsAsync(year, month);
|
var result = await budgetService.ArchiveBudgetsAsync(year, month);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(result))
|
if (string.IsNullOrEmpty(result))
|
||||||
|
|||||||
@@ -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 预算数据
|
* @param {object} data 预算数据
|
||||||
@@ -84,15 +71,27 @@ export function getUncoveredCategories(category, referenceDate) {
|
|||||||
params: { category, referenceDate }
|
params: { category, referenceDate }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 归档预算
|
* 获取归档总结
|
||||||
* @param {number} year 年份
|
* @param {string} referenceDate 参考日期
|
||||||
* @param {number} month 月份
|
|
||||||
*/
|
*/
|
||||||
export function archiveBudgets(year, month) {
|
export function getArchiveSummary(referenceDate) {
|
||||||
return request({
|
return request({
|
||||||
url: `/Budget/ArchiveBudgetsAsync/${year}/${month}`,
|
url: '/Budget/GetArchiveSummary',
|
||||||
method: 'post'
|
method: 'get',
|
||||||
|
params: { referenceDate }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新归档总结
|
||||||
|
* @param {object} data 数据 { referenceDate, summary }
|
||||||
|
*/
|
||||||
|
export function updateArchiveSummary(data) {
|
||||||
|
return request({
|
||||||
|
url: '/Budget/UpdateArchiveSummary',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,9 +42,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 展开状态 -->
|
<!-- 展开状态 -->
|
||||||
<Transition v-else :name="transitionName">
|
<div v-else class="budget-inner-card">
|
||||||
<div :key="budget.period" class="budget-inner-card">
|
<div class="card-header" style="margin-bottom: 0;">
|
||||||
<div class="card-header" style="margin-bottom: 0;">
|
|
||||||
<div class="budget-info">
|
<div class="budget-info">
|
||||||
<slot name="tag">
|
<slot name="tag">
|
||||||
<van-tag
|
<van-tag
|
||||||
@@ -133,31 +132,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</van-collapse-transition>
|
</van-collapse-transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-footer">
|
|
||||||
<div class="period-navigation" @click.stop>
|
|
||||||
<van-button
|
|
||||||
icon="arrow-left"
|
|
||||||
class="nav-icon"
|
|
||||||
plain
|
|
||||||
size="small"
|
|
||||||
style="width: 50px;"
|
|
||||||
@click="handleSwitch(-1)"
|
|
||||||
/>
|
|
||||||
<span class="period-text">{{ budget.period }}</span>
|
|
||||||
<van-button
|
|
||||||
icon="arrow"
|
|
||||||
class="nav-icon"
|
|
||||||
plain
|
|
||||||
size="small"
|
|
||||||
style="width: 50px;"
|
|
||||||
:disabled="isNextDisabled"
|
|
||||||
@click="handleSwitch(1)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 关联账单列表弹窗 -->
|
<!-- 关联账单列表弹窗 -->
|
||||||
@@ -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 isExpanded = ref(props.budget.category === 2)
|
||||||
const transitionName = ref('slide-left')
|
|
||||||
const showDescription = ref(false)
|
const showDescription = ref(false)
|
||||||
const showBillListModal = ref(false)
|
const showBillListModal = ref(false)
|
||||||
const billList = ref([])
|
const billList = ref([])
|
||||||
@@ -218,16 +192,6 @@ const toggleExpand = () => {
|
|||||||
isExpanded.value = !isExpanded.value
|
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 () => {
|
const handleQueryBills = async () => {
|
||||||
showBillListModal.value = true
|
showBillListModal.value = true
|
||||||
billLoading.value = true
|
billLoading.value = true
|
||||||
@@ -402,40 +366,6 @@ const timePercentage = computed(() => {
|
|||||||
cursor: pointer;
|
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 {
|
.budget-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -564,42 +494,7 @@ const timePercentage = computed(() => {
|
|||||||
line-height: 1.4;
|
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) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.card-footer {
|
|
||||||
border-top-color: #2c2c2c;
|
|
||||||
}
|
|
||||||
.period-text {
|
|
||||||
color: #f5f5f5;
|
|
||||||
}
|
|
||||||
.budget-description {
|
.budget-description {
|
||||||
background-color: #2c2c2c;
|
background-color: #2c2c2c;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,8 +71,9 @@
|
|||||||
.rich-html-content thead,
|
.rich-html-content thead,
|
||||||
.rich-html-content tbody {
|
.rich-html-content tbody {
|
||||||
display: table;
|
display: table;
|
||||||
min-width: 100%;
|
width: 100%;
|
||||||
table-layout: fixed; /* 核心:强制列宽分配逻辑一致 */
|
table-layout: fixed; /* 核心:强制列宽分配逻辑一致 */
|
||||||
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rich-html-content tr {
|
.rich-html-content tr {
|
||||||
@@ -86,10 +87,11 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid var(--van-border-color-light);
|
border-bottom: 1px solid var(--van-border-color-light);
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rich-html-content th {
|
.rich-html-content th {
|
||||||
|
|||||||
@@ -3,23 +3,36 @@
|
|||||||
<van-nav-bar title="预算管理" placeholder>
|
<van-nav-bar title="预算管理" placeholder>
|
||||||
<template #right>
|
<template #right>
|
||||||
<van-icon
|
<van-icon
|
||||||
v-if="activeTab !== BudgetCategory.Savings && uncoveredCategories.length > 0"
|
v-if="activeTab !== BudgetCategory.Savings
|
||||||
|
&& uncoveredCategories.length > 0
|
||||||
|
&& !isArchive"
|
||||||
name="warning-o"
|
name="warning-o"
|
||||||
size="20"
|
size="20"
|
||||||
color="#ee0a24"
|
color="#ee0a24"
|
||||||
style="margin-right: 12px"
|
style="margin-right: 12px"
|
||||||
|
title="查看未覆盖预算的分类"
|
||||||
@click="showUncoveredDetails = true"
|
@click="showUncoveredDetails = true"
|
||||||
/>
|
/>
|
||||||
|
<van-icon
|
||||||
|
v-if="isArchive"
|
||||||
|
name="records-o"
|
||||||
|
size="20"
|
||||||
|
title="已归档月份总结"
|
||||||
|
style="margin-right: 12px"
|
||||||
|
@click="showArchiveSummary()"
|
||||||
|
/>
|
||||||
<van-icon
|
<van-icon
|
||||||
v-if="activeTab !== BudgetCategory.Savings"
|
v-if="activeTab !== BudgetCategory.Savings"
|
||||||
name="plus"
|
name="plus"
|
||||||
size="20"
|
size="20"
|
||||||
|
title="添加预算"
|
||||||
@click="budgetEditRef.open({ category: activeTab })"
|
@click="budgetEditRef.open({ category: activeTab })"
|
||||||
/>
|
/>
|
||||||
<van-icon
|
<van-icon
|
||||||
v-else
|
v-else
|
||||||
name="setting-o"
|
name="setting-o"
|
||||||
size="20"
|
size="20"
|
||||||
|
title="储蓄分类配置"
|
||||||
@click="savingsConfigRef.open()"
|
@click="savingsConfigRef.open()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -43,7 +56,6 @@
|
|||||||
:progress-color="getProgressColor(budget)"
|
:progress-color="getProgressColor(budget)"
|
||||||
:percent-class="{ 'warning': (budget.current / budget.limit) > 0.8 }"
|
:percent-class="{ 'warning': (budget.current / budget.limit) > 0.8 }"
|
||||||
:period-label="getPeriodLabel(budget.type)"
|
:period-label="getPeriodLabel(budget.type)"
|
||||||
@switch-period="(dir) => handleSwitchPeriod(budget, dir)"
|
|
||||||
@click="budgetEditRef.open({
|
@click="budgetEditRef.open({
|
||||||
data: budget,
|
data: budget,
|
||||||
isEditFlag: true,
|
isEditFlag: true,
|
||||||
@@ -95,7 +107,6 @@
|
|||||||
:progress-color="getIncomeProgressColor(budget)"
|
:progress-color="getIncomeProgressColor(budget)"
|
||||||
:percent-class="{ 'income': (budget.current / budget.limit) >= 1 }"
|
:percent-class="{ 'income': (budget.current / budget.limit) >= 1 }"
|
||||||
:period-label="getPeriodLabel(budget.type)"
|
:period-label="getPeriodLabel(budget.type)"
|
||||||
@switch-period="(dir) => handleSwitchPeriod(budget, dir)"
|
|
||||||
@click="budgetEditRef.open({
|
@click="budgetEditRef.open({
|
||||||
data: budget,
|
data: budget,
|
||||||
isEditFlag: true,
|
isEditFlag: true,
|
||||||
@@ -142,7 +153,6 @@
|
|||||||
:percent-class="{ 'income': (budget.current / budget.limit) >= 1 }"
|
:percent-class="{ 'income': (budget.current / budget.limit) >= 1 }"
|
||||||
:period-label="getPeriodLabel(budget.type)"
|
:period-label="getPeriodLabel(budget.type)"
|
||||||
style="margin: 0 12px 12px;"
|
style="margin: 0 12px 12px;"
|
||||||
@switch-period="(dir) => handleSwitchPeriod(budget, dir)"
|
|
||||||
>
|
>
|
||||||
<template #amount-info>
|
<template #amount-info>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
@@ -204,13 +214,44 @@
|
|||||||
</van-button>
|
</van-button>
|
||||||
</template>
|
</template>
|
||||||
</PopupContainer>
|
</PopupContainer>
|
||||||
|
|
||||||
|
<PopupContainer
|
||||||
|
v-model="showSummaryPopup"
|
||||||
|
title="月份归档总结"
|
||||||
|
:subtitle="`${selectedDate.getFullYear()}年${selectedDate.getMonth() + 1}月`"
|
||||||
|
height="50%"
|
||||||
|
>
|
||||||
|
<div style="padding: 16px;">
|
||||||
|
<van-field
|
||||||
|
v-model="archiveSummary"
|
||||||
|
rows="6"
|
||||||
|
autosize
|
||||||
|
label="总结语"
|
||||||
|
type="textarea"
|
||||||
|
:placeholder="`请输入${selectedDate.getFullYear()}年${selectedDate.getMonth() + 1}月预算执行的总结或感悟...`"
|
||||||
|
show-word-limit
|
||||||
|
maxlength="500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<van-button
|
||||||
|
block
|
||||||
|
round
|
||||||
|
type="primary"
|
||||||
|
:loading="isSavingSummary"
|
||||||
|
@click="handleSaveSummary"
|
||||||
|
>
|
||||||
|
保存总结
|
||||||
|
</van-button>
|
||||||
|
</template>
|
||||||
|
</PopupContainer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { showToast, showConfirmDialog } from 'vant'
|
import { showToast, showConfirmDialog } from 'vant'
|
||||||
import { getBudgetList, deleteBudget, getBudgetStatistics, getCategoryStats, getUncoveredCategories } from '@/api/budget'
|
import { getBudgetList, deleteBudget, getCategoryStats, getUncoveredCategories, getArchiveSummary, updateArchiveSummary } from '@/api/budget'
|
||||||
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
|
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
|
||||||
import BudgetCard from '@/components/Budget/BudgetCard.vue'
|
import BudgetCard from '@/components/Budget/BudgetCard.vue'
|
||||||
import BudgetSummary from '@/components/Budget/BudgetSummary.vue'
|
import BudgetSummary from '@/components/Budget/BudgetSummary.vue'
|
||||||
@@ -226,6 +267,10 @@ const isRefreshing = ref(false)
|
|||||||
const showUncoveredDetails = ref(false)
|
const showUncoveredDetails = ref(false)
|
||||||
const uncoveredCategories = ref([])
|
const uncoveredCategories = ref([])
|
||||||
|
|
||||||
|
const showSummaryPopup = ref(false)
|
||||||
|
const archiveSummary = ref('')
|
||||||
|
const isSavingSummary = ref(false)
|
||||||
|
|
||||||
const expenseBudgets = ref([])
|
const expenseBudgets = ref([])
|
||||||
const incomeBudgets = ref([])
|
const incomeBudgets = ref([])
|
||||||
const savingsBudgets = ref([])
|
const savingsBudgets = ref([])
|
||||||
@@ -239,6 +284,12 @@ const activeTabTitle = computed(() => {
|
|||||||
return '达成'
|
return '达成'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isArchive = computed(() => {
|
||||||
|
const now = new Date()
|
||||||
|
return selectedDate.value.getFullYear() < now.getFullYear() ||
|
||||||
|
(selectedDate.value.getFullYear() === now.getFullYear() && selectedDate.value.getMonth() < now.getMonth())
|
||||||
|
})
|
||||||
|
|
||||||
watch(activeTab, async () => {
|
watch(activeTab, async () => {
|
||||||
await Promise.all([fetchCategoryStats(), fetchUncoveredCategories()])
|
await Promise.all([fetchCategoryStats(), fetchUncoveredCategories()])
|
||||||
})
|
})
|
||||||
@@ -378,30 +429,6 @@ const getIncomeProgressColor = (budget) => {
|
|||||||
return '#1989fa'
|
return '#1989fa'
|
||||||
}
|
}
|
||||||
|
|
||||||
const refDateMap = {}
|
|
||||||
|
|
||||||
const handleSwitchPeriod = async (budget, direction) => {
|
|
||||||
let currentRefDate = refDateMap[budget.id] || new Date()
|
|
||||||
const date = new Date(currentRefDate)
|
|
||||||
|
|
||||||
if (budget.type === BudgetPeriodType.Month) {
|
|
||||||
date.setMonth(date.getMonth() + direction)
|
|
||||||
} else if (budget.type === BudgetPeriodType.Year) {
|
|
||||||
date.setFullYear(date.getFullYear() + direction)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await getBudgetStatistics(budget.id, date.toISOString())
|
|
||||||
if (res.success) {
|
|
||||||
refDateMap[budget.id] = date
|
|
||||||
Object.assign(budget, res.data)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showToast('加载历史统计失败')
|
|
||||||
console.error('加载预算历史统计失败', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDelete = (budget) => {
|
const handleDelete = (budget) => {
|
||||||
showConfirmDialog({
|
showConfirmDialog({
|
||||||
title: '确认删除',
|
title: '确认删除',
|
||||||
@@ -419,6 +446,39 @@ const handleDelete = (budget) => {
|
|||||||
}
|
}
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showArchiveSummary = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getArchiveSummary(selectedDate.value.toISOString())
|
||||||
|
if (res.success) {
|
||||||
|
archiveSummary.value = res.data || ''
|
||||||
|
showSummaryPopup.value = true
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取总结失败', err)
|
||||||
|
showToast('获取总结失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveSummary = async () => {
|
||||||
|
if (isSavingSummary.value) return
|
||||||
|
isSavingSummary.value = true
|
||||||
|
try {
|
||||||
|
const res = await updateArchiveSummary({
|
||||||
|
referenceDate: selectedDate.value.toISOString(),
|
||||||
|
summary: archiveSummary.value
|
||||||
|
})
|
||||||
|
if (res.success) {
|
||||||
|
showToast('已保存')
|
||||||
|
showSummaryPopup.value = false
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('保存总结失败', err)
|
||||||
|
showToast('保存总结失败')
|
||||||
|
} finally {
|
||||||
|
isSavingSummary.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public class BudgetController(
|
|||||||
/// 获取预算列表
|
/// 获取预算列表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<BaseResponse<List<BudgetResult>>> GetListAsync([FromQuery] DateTime? referenceDate = null)
|
public async Task<BaseResponse<List<BudgetResult>>> GetListAsync([FromQuery] DateTime referenceDate)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -30,41 +30,11 @@ public class BudgetController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取单个预算统计信息
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<BaseResponse<BudgetResult>> GetStatisticsAsync([FromQuery] long id, [FromQuery] DateTime referenceDate)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 参数验证
|
|
||||||
if (id == 0)
|
|
||||||
{
|
|
||||||
return "预算 Id 无效".Fail<BudgetResult>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await budgetService.GetStatisticsAsync(id, referenceDate);
|
|
||||||
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
return "预算不存在".Fail<BudgetResult>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Ok();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "获取预算统计失败, Id: {Id}", id);
|
|
||||||
return $"获取预算统计失败: {ex.Message}".Fail<BudgetResult>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取分类统计信息(月度和年度)
|
/// 获取分类统计信息(月度和年度)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<BaseResponse<BudgetCategoryStats>> GetCategoryStatsAsync([FromQuery] BudgetCategory category, [FromQuery] DateTime? referenceDate = null)
|
public async Task<BaseResponse<BudgetCategoryStats>> GetCategoryStatsAsync([FromQuery] BudgetCategory category, [FromQuery] DateTime referenceDate)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -96,6 +66,42 @@ public class BudgetController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取归档总结
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<BaseResponse<string?>> GetArchiveSummaryAsync([FromQuery] DateTime referenceDate)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await budgetService.GetArchiveSummaryAsync(referenceDate.Year, referenceDate.Month);
|
||||||
|
return result.Ok<string?>();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "获取归档总结失败");
|
||||||
|
return $"获取归档总结失败: {ex.Message}".Fail<string?>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新归档总结
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<BaseResponse> UpdateArchiveSummaryAsync([FromBody] UpdateArchiveSummaryDto dto)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await budgetService.UpdateArchiveSummaryAsync(dto.ReferenceDate.Year, dto.ReferenceDate.Month, dto.Summary);
|
||||||
|
return BaseResponse.Done();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "更新归档总结失败");
|
||||||
|
return $"更新归档总结失败: {ex.Message}".Fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 删除预算
|
/// 删除预算
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -189,30 +195,6 @@ public class BudgetController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 归档预算
|
|
||||||
/// </summary>
|
|
||||||
[HttpPost("{year}/{month}")]
|
|
||||||
public async Task<BaseResponse> ArchiveBudgetsAsync(int year, int month)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var msg = await budgetService.ArchiveBudgetsAsync(year, month);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(msg))
|
|
||||||
{
|
|
||||||
return msg.Fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
return BaseResponse.Done();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "归档预算失败, 归档日期: {Year}-{Month}", year, month);
|
|
||||||
return $"归档预算失败: {ex.Message}".Fail();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> ValidateBudgetSelectedCategoriesAsync(BudgetRecord record)
|
private async Task<string> ValidateBudgetSelectedCategoriesAsync(BudgetRecord record)
|
||||||
{
|
{
|
||||||
var allBudgets = await budgetRepository.GetAllAsync();
|
var allBudgets = await budgetRepository.GetAllAsync();
|
||||||
|
|||||||
@@ -15,3 +15,9 @@ public class UpdateBudgetDto : CreateBudgetDto
|
|||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class UpdateArchiveSummaryDto
|
||||||
|
{
|
||||||
|
public DateTime ReferenceDate { get; set; }
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ var fsql = new FreeSqlBuilder()
|
|||||||
)
|
)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
fsql.UseJsonMap();
|
||||||
|
|
||||||
builder.Services.AddSingleton(fsql);
|
builder.Services.AddSingleton(fsql);
|
||||||
|
|
||||||
// 自动扫描注册服务和仓储
|
// 自动扫描注册服务和仓储
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FreeSql.Extensions.JsonMap" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||||
<PackageReference Include="Scalar.AspNetCore" />
|
<PackageReference Include="Scalar.AspNetCore" />
|
||||||
|
|||||||
Reference in New Issue
Block a user