feat: 添加获取未被预算覆盖的分类统计信息接口;更新前端以展示未覆盖分类的详细信息
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 22s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
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 22s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
This commit is contained in:
@@ -12,6 +12,11 @@ public interface IBudgetService
|
||||
/// 获取指定分类的统计信息(月度和年度)
|
||||
/// </summary>
|
||||
Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime? referenceDate = null);
|
||||
|
||||
/// <summary>
|
||||
/// 获取未被预算覆盖的分类统计信息
|
||||
/// </summary>
|
||||
Task<List<UncoveredCategoryDetail>> GetUncoveredCategoriesAsync(BudgetCategory category, DateTime? referenceDate = null);
|
||||
}
|
||||
|
||||
public class BudgetService(
|
||||
@@ -146,6 +151,41 @@ public class BudgetService(
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<UncoveredCategoryDetail>> GetUncoveredCategoriesAsync(BudgetCategory category, DateTime? referenceDate = null)
|
||||
{
|
||||
var date = referenceDate ?? DateTime.Now;
|
||||
var transactionType = category switch
|
||||
{
|
||||
BudgetCategory.Expense => TransactionType.Expense,
|
||||
BudgetCategory.Income => TransactionType.Income,
|
||||
_ => TransactionType.None
|
||||
};
|
||||
|
||||
if (transactionType == TransactionType.None) return new List<UncoveredCategoryDetail>();
|
||||
|
||||
// 1. 获取所有预算
|
||||
var budgets = (await budgetRepository.GetAllAsync()).ToList();
|
||||
var coveredCategories = budgets
|
||||
.Where(b => b.Category == category)
|
||||
.SelectMany(b => b.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||
.ToHashSet();
|
||||
|
||||
// 2. 获取分类统计
|
||||
var stats = await transactionRecordRepository.GetCategoryStatisticsAsync(date.Year, date.Month, transactionType);
|
||||
|
||||
// 3. 过滤未覆盖的
|
||||
return stats
|
||||
.Where(s => !coveredCategories.Contains(s.Classify))
|
||||
.Select(s => new UncoveredCategoryDetail
|
||||
{
|
||||
Category = s.Classify,
|
||||
TransactionCount = s.Count,
|
||||
TotalAmount = s.Amount
|
||||
})
|
||||
.OrderByDescending(x => x.TotalAmount)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<BudgetStatsDto> CalculateCategoryStatsAsync(
|
||||
List<BudgetRecord> budgets,
|
||||
BudgetCategory category,
|
||||
@@ -674,3 +714,9 @@ public class BudgetCategoryStats
|
||||
/// </summary>
|
||||
public BudgetStatsDto Year { get; set; } = new();
|
||||
}
|
||||
public class UncoveredCategoryDetail
|
||||
{
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public int TransactionCount { get; set; }
|
||||
public decimal TotalAmount { get; set; }
|
||||
}
|
||||
|
||||
@@ -72,7 +72,18 @@ export function getCategoryStats(category, referenceDate) {
|
||||
params: { category, referenceDate }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未被预算覆盖的分类统计信息
|
||||
* @param {number} category 预算分类
|
||||
* @param {string} referenceDate 参考日期
|
||||
*/
|
||||
export function getUncoveredCategories(category, referenceDate) {
|
||||
return request({
|
||||
url: '/Budget/GetUncoveredCategories',
|
||||
method: 'get',
|
||||
params: { category, referenceDate }
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 归档预算
|
||||
* @param {number} year 年份
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
<div class="page-container-flex">
|
||||
<van-nav-bar title="预算管理" placeholder>
|
||||
<template #right>
|
||||
<van-icon
|
||||
v-if="activeTab !== BudgetCategory.Savings && uncoveredCategories.length > 0"
|
||||
name="warning-o"
|
||||
size="20"
|
||||
color="#ee0a24"
|
||||
style="margin-right: 12px"
|
||||
@click="showUncoveredDetails = true"
|
||||
/>
|
||||
<van-icon
|
||||
v-if="activeTab !== BudgetCategory.Savings"
|
||||
name="plus"
|
||||
@@ -167,23 +175,53 @@
|
||||
ref="savingsConfigRef"
|
||||
@success="fetchBudgetList"
|
||||
/>
|
||||
|
||||
<PopupContainer
|
||||
v-model="showUncoveredDetails"
|
||||
title="未覆盖预算的分类"
|
||||
:subtitle="`本月共 <b style='color:var(--van-primary-color)'>${uncoveredCategories.length}</b> 个分类未设置预算`"
|
||||
height="60%"
|
||||
>
|
||||
<div class="uncovered-list">
|
||||
<div v-for="item in uncoveredCategories" :key="item.category" class="uncovered-item">
|
||||
<div class="item-left">
|
||||
<div class="category-name">{{ item.category }}</div>
|
||||
<div class="transaction-count">{{ item.transactionCount }} 笔记录</div>
|
||||
</div>
|
||||
<div class="item-right">
|
||||
<div class="item-amount" :class="activeTab === BudgetCategory.Expense ? 'expense' : 'income'">
|
||||
¥{{ formatMoney(item.totalAmount) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<van-button block round type="primary" @click="showUncoveredDetails = false">
|
||||
我知道了
|
||||
</van-button>
|
||||
</template>
|
||||
</PopupContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { showToast, showConfirmDialog } from 'vant'
|
||||
import { getBudgetList, deleteBudget, getBudgetStatistics, getCategoryStats } from '@/api/budget'
|
||||
import { getBudgetList, deleteBudget, getBudgetStatistics, getCategoryStats, getUncoveredCategories } from '@/api/budget'
|
||||
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'
|
||||
import PopupContainer from '@/components/PopupContainer.vue'
|
||||
|
||||
const activeTab = ref(BudgetCategory.Expense)
|
||||
const budgetEditRef = ref(null)
|
||||
const savingsConfigRef = ref(null)
|
||||
const isRefreshing = ref(false)
|
||||
const showUncoveredDetails = ref(false)
|
||||
const uncoveredCategories = ref([])
|
||||
|
||||
const expenseBudgets = ref([])
|
||||
const incomeBudgets = ref([])
|
||||
@@ -199,7 +237,7 @@ const activeTabTitle = computed(() => {
|
||||
})
|
||||
|
||||
watch(activeTab, async () => {
|
||||
await fetchCategoryStats()
|
||||
await Promise.all([fetchCategoryStats(), fetchUncoveredCategories()])
|
||||
})
|
||||
|
||||
const getValueClass = (rate) => {
|
||||
@@ -232,7 +270,7 @@ const fetchBudgetList = async () => {
|
||||
|
||||
const onRefresh = async () => {
|
||||
try {
|
||||
await Promise.all([fetchBudgetList(), fetchCategoryStats()])
|
||||
await Promise.all([fetchBudgetList(), fetchCategoryStats(), fetchUncoveredCategories()])
|
||||
} catch (err) {
|
||||
console.error('刷新失败', err)
|
||||
} finally {
|
||||
@@ -266,10 +304,28 @@ const fetchCategoryStats = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchUncoveredCategories = async () => {
|
||||
if (activeTab.value === BudgetCategory.Savings) {
|
||||
uncoveredCategories.value = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getUncoveredCategories(activeTab.value)
|
||||
if (res.success) {
|
||||
uncoveredCategories.value = res.data || []
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取未覆盖分类失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await fetchBudgetList()
|
||||
await fetchCategoryStats()
|
||||
await Promise.all([
|
||||
fetchBudgetList(),
|
||||
fetchCategoryStats(),
|
||||
fetchUncoveredCategories()
|
||||
])
|
||||
} catch (err) {
|
||||
console.error('获取初始化数据失败', err)
|
||||
}
|
||||
@@ -387,4 +443,46 @@ const handleDelete = (budget) => {
|
||||
:deep(.van-tabs__nav--card) {
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.uncovered-list {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.uncovered-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background-color: var(--van-background-2, #ffffff);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.item-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--van-text-color, #323233);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.transaction-count {
|
||||
font-size: 12px;
|
||||
color: var(--van-text-color-2, #969799);
|
||||
}
|
||||
|
||||
.item-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.item-amount {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
font-family: DIN Alternate, system-ui;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -78,6 +78,24 @@ public class BudgetController(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取未被预算覆盖的分类统计信息
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<BaseResponse<List<UncoveredCategoryDetail>>> GetUncoveredCategoriesAsync([FromQuery] BudgetCategory category, [FromQuery] DateTime? referenceDate = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await budgetService.GetUncoveredCategoriesAsync(category, referenceDate);
|
||||
return result.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "获取未覆盖分类统计失败, Category: {Category}", category);
|
||||
return $"获取未覆盖分类统计失败: {ex.Message}".Fail<List<UncoveredCategoryDetail>>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除预算
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user