添加动态目标
All checks were successful
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
Docker Build & Deploy / Build Docker Image (push) Successful in 22s
Docker Build & Deploy / Deploy to Production (push) Successful in 10s
All checks were successful
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
Docker Build & Deploy / Build Docker Image (push) Successful in 22s
Docker Build & Deploy / Deploy to Production (push) Successful in 10s
This commit is contained in:
@@ -23,6 +23,16 @@ public class BudgetArchive : BaseEntity
|
||||
/// </summary>
|
||||
public DateTime ArchiveDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 支出结余(预算 - 实际,正数表示省钱,负数表示超支)
|
||||
/// </summary>
|
||||
public decimal ExpenseSurplus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 收入结余(实际 - 预算,正数表示超额收入,负数表示未达预期)
|
||||
/// </summary>
|
||||
public decimal IncomeSurplus { get; set; }
|
||||
|
||||
public string? Summary { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ public interface IBudgetArchiveRepository : IBaseRepository<BudgetArchive>
|
||||
Task<BudgetArchive?> GetArchiveAsync(int year, int month);
|
||||
|
||||
Task<List<BudgetArchive>> GetListAsync(int year, int month);
|
||||
|
||||
Task<List<BudgetArchive>> GetArchivesByYearAsync(int year);
|
||||
}
|
||||
|
||||
public class BudgetArchiveRepository(
|
||||
@@ -25,4 +27,12 @@ public class BudgetArchiveRepository(
|
||||
.Where(a => a.Year == year && a.Month == month)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<BudgetArchive>> GetArchivesByYearAsync(int year)
|
||||
{
|
||||
return await FreeSql.Select<BudgetArchive>()
|
||||
.Where(a => a.Year == year)
|
||||
.OrderBy(a => a.Month)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,11 @@ public interface IBudgetService
|
||||
Task<string?> GetArchiveSummaryAsync(int year, int month);
|
||||
|
||||
Task UpdateArchiveSummaryAsync(int year, int month, string? summary);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定周期的存款预算信息
|
||||
/// </summary>
|
||||
Task<BudgetResult?> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type);
|
||||
}
|
||||
|
||||
public class BudgetService(
|
||||
@@ -86,6 +91,12 @@ public class BudgetService(
|
||||
return dtos.Where(dto => dto != null).Cast<BudgetResult>().ToList();
|
||||
}
|
||||
|
||||
public async Task<BudgetResult?> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type)
|
||||
{
|
||||
var referenceDate = new DateTime(year, month, 1);
|
||||
return await GetVirtualSavingsDtoAsync(type, referenceDate);
|
||||
}
|
||||
|
||||
public async Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
|
||||
{
|
||||
var budgets = await GetListAsync(referenceDate);
|
||||
@@ -204,13 +215,14 @@ public class BudgetService(
|
||||
totalLimit += itemLimit;
|
||||
|
||||
// 当前值累加
|
||||
var selectedCategories = budget.SelectedCategories != null ? string.Join(',', budget.SelectedCategories) : string.Empty;
|
||||
var currentAmount = await CalculateCurrentAmountAsync(new()
|
||||
{
|
||||
Name = budget.Name,
|
||||
Type = budget.Type,
|
||||
Limit = budget.Limit,
|
||||
Category = budget.Category,
|
||||
SelectedCategories = string.Join(',', budget.SelectedCategories),
|
||||
SelectedCategories = selectedCategories,
|
||||
StartDate = new DateTime(referenceDate.Year, referenceDate.Month, 1)
|
||||
}, referenceDate);
|
||||
if (budget.Type == statType)
|
||||
@@ -242,6 +254,14 @@ public class BudgetService(
|
||||
|
||||
var budgets = await GetListAsync(referenceDate);
|
||||
|
||||
var expenseSurplus = budgets
|
||||
.Where(b => b.Category == BudgetCategory.Expense && !b.NoLimit && b.Type == BudgetPeriodType.Month)
|
||||
.Sum(b => b.Limit - b.Current);
|
||||
|
||||
var incomeSurplus = budgets
|
||||
.Where(b => b.Category == BudgetCategory.Income && !b.NoLimit && b.Type == BudgetPeriodType.Month)
|
||||
.Sum(b => b.Current - b.Limit);
|
||||
|
||||
var content = budgets.Select(b => new BudgetArchiveContent
|
||||
{
|
||||
Name = b.Name,
|
||||
@@ -260,6 +280,8 @@ public class BudgetService(
|
||||
{
|
||||
archive.Content = content;
|
||||
archive.ArchiveDate = DateTime.Now;
|
||||
archive.ExpenseSurplus = expenseSurplus;
|
||||
archive.IncomeSurplus = incomeSurplus;
|
||||
if (!await budgetArchiveRepository.UpdateAsync(archive))
|
||||
{
|
||||
return "更新预算归档失败";
|
||||
@@ -272,7 +294,9 @@ public class BudgetService(
|
||||
Year = year,
|
||||
Month = month,
|
||||
Content = content,
|
||||
ArchiveDate = DateTime.Now
|
||||
ArchiveDate = DateTime.Now,
|
||||
ExpenseSurplus = expenseSurplus,
|
||||
IncomeSurplus = incomeSurplus
|
||||
};
|
||||
|
||||
if (!await budgetArchiveRepository.AddAsync(archive))
|
||||
@@ -578,10 +602,8 @@ public class BudgetService(
|
||||
}
|
||||
description.Append($"<p>收入合计: <span class='income-value'><strong>{incomeLimitAtPeriod:N0}</strong></span></p>");
|
||||
|
||||
// 新增:显示不记额收入明细
|
||||
description.Append("<h3>不记额收入明细</h3>");
|
||||
if (noLimitIncomeItems.Count == 0) description.Append("<p>无不记额收入</p>");
|
||||
else
|
||||
if (noLimitIncomeItems.Count > 0)
|
||||
{
|
||||
description.Append("""
|
||||
<table>
|
||||
@@ -637,11 +659,10 @@ public class BudgetService(
|
||||
}
|
||||
description.Append($"<p>支出合计: <span class='expense-value'><strong>{expenseLimitAtPeriod:N0}</strong></span></p>");
|
||||
|
||||
// 新增:显示不记额支出明细
|
||||
description.Append("<h3>不记额支出明细</h3>");
|
||||
if (noLimitExpenseItems.Count == 0) description.Append("<p>无不记额支出</p>");
|
||||
else
|
||||
if (noLimitExpenseItems.Count > 0)
|
||||
{
|
||||
|
||||
description.Append("<h3>不记额支出明细</h3>");
|
||||
description.Append("""
|
||||
<table>
|
||||
<thead>
|
||||
@@ -663,6 +684,7 @@ public class BudgetService(
|
||||
}
|
||||
description.Append("</tbody></table>");
|
||||
}
|
||||
|
||||
description.Append($"<p>不记额支出合计: <span class='expense-value'><strong>{noLimitExpenseAtPeriod:N0}</strong></span></p>");
|
||||
|
||||
description.Append("<h3>存款计划结论</h3>");
|
||||
@@ -671,12 +693,70 @@ public class BudgetService(
|
||||
var totalExpense = expenseLimitAtPeriod + noLimitExpenseAtPeriod;
|
||||
description.Append($"<p>计划收入 = 预算 <span class='income-value'>{incomeLimitAtPeriod:N0}</span> + 不记额 <span class='income-value'>{noLimitIncomeAtPeriod:N0}</span> = <span class='income-value'><strong>{totalIncome:N0}</strong></span></p>");
|
||||
description.Append($"<p>计划支出 = 预算 <span class='expense-value'>{expenseLimitAtPeriod:N0}</span> + 不记额 <span class='expense-value'>{noLimitExpenseAtPeriod:N0}</span> = <span class='expense-value'><strong>{totalExpense:N0}</strong></span></p>");
|
||||
description.Append($"<p>最终目标:<span class='highlight'><strong>{totalIncome - totalExpense:N0}</strong></span></p>");
|
||||
|
||||
decimal historicalSurplus = 0;
|
||||
if (periodType == BudgetPeriodType.Year)
|
||||
{
|
||||
var archives = await budgetArchiveRepository.GetArchivesByYearAsync(date.Year);
|
||||
if (archives.Count > 0)
|
||||
{
|
||||
var expenseSurplus = archives.Sum(a => a.ExpenseSurplus);
|
||||
var incomeSurplus = archives.Sum(a => a.IncomeSurplus);
|
||||
historicalSurplus = expenseSurplus + incomeSurplus;
|
||||
|
||||
description.Append("<h3>历史月份盈亏</h3>");
|
||||
description.Append("""
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>月份</th>
|
||||
<th>支出结余</th>
|
||||
<th>收入结余</th>
|
||||
<th>合计</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
""");
|
||||
|
||||
foreach (var archive in archives)
|
||||
{
|
||||
var monthlyTotal = archive.ExpenseSurplus + archive.IncomeSurplus;
|
||||
var monthlyClass = monthlyTotal >= 0 ? "income-value" : "expense-value";
|
||||
description.Append($"""
|
||||
<tr>
|
||||
<td>{archive.Month}月</td>
|
||||
<td><span class='{(archive.ExpenseSurplus >= 0 ? "income-value" : "expense-value")}'>{archive.ExpenseSurplus:N0}</span></td>
|
||||
<td><span class='{(archive.IncomeSurplus >= 0 ? "income-value" : "expense-value")}'>{archive.IncomeSurplus:N0}</span></td>
|
||||
<td><span class='{monthlyClass}'>{monthlyTotal:N0}</span></td>
|
||||
</tr>
|
||||
""");
|
||||
}
|
||||
|
||||
var totalClass = historicalSurplus >= 0 ? "income-value" : "expense-value";
|
||||
description.Append($"""
|
||||
<tr>
|
||||
<td><strong>汇总</strong></td>
|
||||
<td><span class='{(expenseSurplus >= 0 ? "income-value" : "expense-value")}'><strong>{expenseSurplus:N0}</strong></span></td>
|
||||
<td><span class='{(incomeSurplus >= 0 ? "income-value" : "expense-value")}'><strong>{incomeSurplus:N0}</strong></span></td>
|
||||
<td><span class='{totalClass}'><strong>{historicalSurplus:N0}</strong></span></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
""");
|
||||
}
|
||||
var finalGoal = totalIncome - totalExpense + historicalSurplus;
|
||||
description.Append($"<p>动态目标 = 计划盈余 <span class='highlight'>{totalIncome - totalExpense:N0}</span> + 历史盈亏 <span class='highlight'>{historicalSurplus:N0}</span> = <span class='highlight'><strong>{finalGoal:N0}</strong></span></p>");
|
||||
}
|
||||
else
|
||||
{
|
||||
description.Append($"<p>最终目标:<span class='highlight'><strong>{totalIncome - totalExpense:N0}</strong></span></p>");
|
||||
}
|
||||
|
||||
var finalLimit = periodType == BudgetPeriodType.Year ? (totalIncome - totalExpense + historicalSurplus) : (totalIncome - totalExpense);
|
||||
|
||||
var virtualBudget = await BuildVirtualSavingsBudgetRecordAsync(
|
||||
periodType == BudgetPeriodType.Year ? -1 : -2,
|
||||
date,
|
||||
totalIncome - totalExpense); // 修改:使用总金额
|
||||
finalLimit);
|
||||
|
||||
// 计算实际发生的 收入 - 支出
|
||||
var current = await CalculateCurrentAmountAsync(new BudgetRecord
|
||||
|
||||
@@ -95,3 +95,17 @@ export function updateArchiveSummary(data) {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定周期的存款预算信息
|
||||
* @param {number} year 年份
|
||||
* @param {number} month 月份
|
||||
* @param {number} type 周期类型 (1:Month, 2:Year)
|
||||
*/
|
||||
export function getSavingsBudget(year, month, type) {
|
||||
return request({
|
||||
url: '/Budget/GetSavingsBudget',
|
||||
method: 'get',
|
||||
params: { year, month, type }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -133,6 +133,10 @@
|
||||
</div>
|
||||
</van-collapse-transition>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -170,6 +170,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="card-footer-actions">
|
||||
<van-button
|
||||
size="small"
|
||||
icon="arrow-left"
|
||||
plain
|
||||
type="primary"
|
||||
@click.stop="handleSavingsNav(budget, -1)"
|
||||
>
|
||||
{{ budget.type === BudgetPeriodType.Year ? '上一年' : '上一月' }}
|
||||
</van-button>
|
||||
<span class="current-date-label">
|
||||
{{ getSavingsDateLabel(budget) }}
|
||||
</span>
|
||||
<van-button
|
||||
size="small"
|
||||
icon="arrow"
|
||||
plain
|
||||
type="primary"
|
||||
icon-position="right"
|
||||
@click.stop="handleSavingsNav(budget, 1)"
|
||||
>
|
||||
{{ budget.type === BudgetPeriodType.Year ? '下一年' : '下一月' }}
|
||||
</van-button>
|
||||
</div>
|
||||
</template>
|
||||
</BudgetCard>
|
||||
</template>
|
||||
<van-empty v-else description="暂无存款计划" />
|
||||
@@ -251,7 +278,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { showToast, showConfirmDialog } from 'vant'
|
||||
import { getBudgetList, deleteBudget, getCategoryStats, getUncoveredCategories, getArchiveSummary, updateArchiveSummary } from '@/api/budget'
|
||||
import { getBudgetList, deleteBudget, getCategoryStats, getUncoveredCategories, getArchiveSummary, updateArchiveSummary, getSavingsBudget } from '@/api/budget'
|
||||
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
|
||||
import BudgetCard from '@/components/Budget/BudgetCard.vue'
|
||||
import BudgetSummary from '@/components/Budget/BudgetSummary.vue'
|
||||
@@ -539,9 +566,71 @@ const handleDelete = async (budget) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getSavingsDateLabel = (budget) => {
|
||||
if (!budget.periodStart) return ''
|
||||
const date = new Date(budget.periodStart)
|
||||
if (budget.type === BudgetPeriodType.Year) {
|
||||
return `${date.getFullYear()}年`
|
||||
} else {
|
||||
return `${date.getFullYear()}年${date.getMonth() + 1}月`
|
||||
}
|
||||
}
|
||||
|
||||
const handleSavingsNav = async (budget, offset) => {
|
||||
if (!budget.periodStart) return
|
||||
|
||||
const date = new Date(budget.periodStart)
|
||||
let year = date.getFullYear()
|
||||
let month = date.getMonth() + 1
|
||||
|
||||
if (budget.type === BudgetPeriodType.Year) {
|
||||
year += offset
|
||||
} else {
|
||||
month += offset
|
||||
if (month > 12) {
|
||||
month = 1
|
||||
year++
|
||||
} else if (month < 1) {
|
||||
month = 12
|
||||
year--
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getSavingsBudget(year, month, budget.type)
|
||||
if (res.success && res.data) {
|
||||
// 找到并更新对应的 budget 对象
|
||||
const index = savingsBudgets.value.findIndex(b => b.id === budget.id)
|
||||
if (index !== -1) {
|
||||
savingsBudgets.value[index] = res.data
|
||||
}
|
||||
} else {
|
||||
showToast('获取数据失败')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('切换日期失败', err)
|
||||
showToast('切换日期失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-footer-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed var(--van-gray-3);
|
||||
}
|
||||
|
||||
.current-date-label {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: var(--van-text-color);
|
||||
}
|
||||
|
||||
.budget-tabs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -102,6 +102,24 @@ public class BudgetController(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定周期的存款预算信息
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<BaseResponse<BudgetResult?>> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await budgetService.GetSavingsBudgetAsync(year, month, type);
|
||||
return result.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "获取存款预算信息失败");
|
||||
return $"获取存款预算信息失败: {ex.Message}".Fail<BudgetResult?>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除预算
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user