fix: 修复收入预算计算和添加存款计划明细功能
问题1:修复收入预算实际金额计算 - 在 BudgetRepository.cs 中修复 SelectedCategories.Split 逻辑 - 添加 StringSplitOptions.RemoveEmptyEntries 和 StringSplitOptions.TrimEntries 参数 - 添加单元测试验证修复 问题2:添加存款计划明细按钮和弹窗 - 在 BudgetCard.vue 中添加 'show-detail' emit - 为存款计划卡片(category === 2)添加明细按钮 - 在 SavingsBudgetContent.vue 中实现明细弹窗 - 弹窗显示:收入预算、支出预算、计划存款公式、存款结果 问题3:统一卡片样式 - 修复 BudgetChartAnalysis.vue 的卡片样式 - 使用 16px 圆角、增强阴影和边框
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
namespace Repository;
|
||||
namespace Repository;
|
||||
|
||||
public interface IBudgetRepository : IBaseRepository<BudgetRecord>
|
||||
{
|
||||
@@ -16,7 +16,7 @@ public class BudgetRepository(IFreeSql freeSql) : BaseRepository<BudgetRecord>(f
|
||||
|
||||
if (!string.IsNullOrEmpty(budget.SelectedCategories))
|
||||
{
|
||||
var categoryList = budget.SelectedCategories.Split(',');
|
||||
var categoryList = budget.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
query = query.Where(t => categoryList.Contains(t.Classify));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<!-- 普通预算卡片 -->
|
||||
<div
|
||||
@@ -115,6 +115,14 @@
|
||||
title="查询关联账单"
|
||||
@click.stop="handleQueryBills"
|
||||
/>
|
||||
<van-button
|
||||
v-if="budget.category === 2"
|
||||
icon="info-o"
|
||||
size="small"
|
||||
plain
|
||||
title="计划存款明细"
|
||||
@click.stop="$emit('show-detail', budget)"
|
||||
/>
|
||||
<template v-if="budget.category !== 2">
|
||||
<van-button
|
||||
icon="edit"
|
||||
@@ -313,6 +321,14 @@
|
||||
title="查询关联账单"
|
||||
@click.stop="handleQueryBills"
|
||||
/>
|
||||
<van-button
|
||||
v-if="budget.category === 2"
|
||||
icon="info-o"
|
||||
size="small"
|
||||
plain
|
||||
title="计划存款明细"
|
||||
@click.stop="$emit('show-detail', budget)"
|
||||
/>
|
||||
<template v-if="budget.category !== 2">
|
||||
<van-button
|
||||
icon="edit"
|
||||
@@ -432,7 +448,7 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
const emit = defineEmits(['click', 'show-detail'])
|
||||
|
||||
const isExpanded = ref(props.budget.category === 2)
|
||||
const showDescription = ref(false)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
:percent-class="{ income: budget.current / budget.limit >= 1 }"
|
||||
:period-label="getPeriodLabel(budget.type)"
|
||||
style="margin: 0 12px 12px"
|
||||
@show-detail="handleShowDetail"
|
||||
>
|
||||
<template #amount-info>
|
||||
<div class="info-item">
|
||||
@@ -68,14 +69,130 @@
|
||||
description="暂无存款计划"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 计划存款明细弹窗 -->
|
||||
<van-popup
|
||||
v-model:show="showDetailPopup"
|
||||
position="bottom"
|
||||
round
|
||||
:style="{ height: '80%' }"
|
||||
>
|
||||
<div class="detail-popup-content">
|
||||
<div class="popup-header">
|
||||
<h3 class="popup-title">
|
||||
计划存款明细
|
||||
</h3>
|
||||
<van-icon
|
||||
name="cross"
|
||||
size="20"
|
||||
class="close-icon"
|
||||
@click="showDetailPopup = false"
|
||||
/>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div
|
||||
v-if="currentBudget"
|
||||
class="detail-content"
|
||||
>
|
||||
<div class="detail-section income-section">
|
||||
<div class="section-title">
|
||||
<van-icon name="balance-o" />
|
||||
收入预算
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">预算限额</span>
|
||||
<span class="detail-value income">¥{{ formatMoney(incomeLimit) }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">实际收入</span>
|
||||
<span class="detail-value">¥{{ formatMoney(incomeCurrent) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section expense-section">
|
||||
<div class="section-title">
|
||||
<van-icon name="bill-o" />
|
||||
支出预算
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">预算限额</span>
|
||||
<span class="detail-value expense">¥{{ formatMoney(expenseLimit) }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">实际支出</span>
|
||||
<span class="detail-value">¥{{ formatMoney(expenseCurrent) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section formula-section">
|
||||
<div class="section-title">
|
||||
<van-icon name="calculator-o" />
|
||||
计划存款公式
|
||||
</div>
|
||||
<div class="formula-box">
|
||||
<div class="formula-item">
|
||||
<span class="formula-label">收入预算</span>
|
||||
<span class="formula-value income">¥{{ formatMoney(incomeLimit) }}</span>
|
||||
</div>
|
||||
<div class="formula-operator">
|
||||
-
|
||||
</div>
|
||||
<div class="formula-item">
|
||||
<span class="formula-label">支出预算</span>
|
||||
<span class="formula-value expense">¥{{ formatMoney(expenseLimit) }}</span>
|
||||
</div>
|
||||
<div class="formula-operator">
|
||||
=
|
||||
</div>
|
||||
<div class="formula-item">
|
||||
<span class="formula-label">计划存款</span>
|
||||
<span class="formula-value">¥{{ formatMoney(currentBudget.limit) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section result-section">
|
||||
<div class="section-title">
|
||||
<van-icon name="chart-trending-o" />
|
||||
存款结果
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">计划存款</span>
|
||||
<span class="detail-value">¥{{ formatMoney(currentBudget.limit) }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">实际存款</span>
|
||||
<span
|
||||
class="detail-value"
|
||||
:class="{ income: currentBudget.current >= currentBudget.limit }"
|
||||
>¥{{ formatMoney(currentBudget.current) }}</span>
|
||||
</div>
|
||||
<div class="detail-row highlight">
|
||||
<span class="detail-label">还差</span>
|
||||
<span class="detail-value expense">¥{{
|
||||
formatMoney(Math.max(0, currentBudget.limit - currentBudget.current))
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import BudgetCard from '@/components/Budget/BudgetCard.vue'
|
||||
import { BudgetPeriodType } from '@/constants/enums'
|
||||
|
||||
// Props
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
budgets: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -83,7 +200,17 @@ defineProps({
|
||||
})
|
||||
|
||||
// Emits
|
||||
defineEmits(['savings-nav'])
|
||||
const emit = defineEmits(['savings-nav'])
|
||||
|
||||
// 明细弹窗状态
|
||||
const showDetailPopup = ref(false)
|
||||
const currentBudget = ref(null)
|
||||
|
||||
// 处理显示明细
|
||||
const handleShowDetail = (budget) => {
|
||||
currentBudget.value = budget
|
||||
showDetailPopup.value = true
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
const formatMoney = (val) => {
|
||||
@@ -209,7 +336,9 @@ const getProgressColor = (budget) => {
|
||||
.info-item .value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-family: DIN Alternate, system-ui;
|
||||
font-family:
|
||||
DIN Alternate,
|
||||
system-ui;
|
||||
}
|
||||
|
||||
.info-item .value.expense {
|
||||
@@ -219,4 +348,149 @@ const getProgressColor = (budget) => {
|
||||
.info-item .value.income {
|
||||
color: var(--van-success-color);
|
||||
}
|
||||
|
||||
.detail-popup-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--van-background-2);
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--van-border-color);
|
||||
background-color: var(--van-background-2);
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--van-text-color);
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
color: var(--van-text-color-2);
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
background-color: var(--van-background);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--van-border-color);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--van-text-color);
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--van-border-color);
|
||||
}
|
||||
|
||||
.income-section .section-title {
|
||||
color: var(--van-success-color);
|
||||
}
|
||||
|
||||
.expense-section .section-title {
|
||||
color: var(--van-danger-color);
|
||||
}
|
||||
|
||||
.section-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.detail-row.highlight {
|
||||
margin-top: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed var(--van-border-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 14px;
|
||||
color: var(--van-text-color-2);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--van-text-color);
|
||||
}
|
||||
|
||||
.detail-value.income {
|
||||
color: var(--van-success-color);
|
||||
}
|
||||
|
||||
.detail-value.expense {
|
||||
color: var(--van-danger-color);
|
||||
}
|
||||
|
||||
.formula-box {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background-color: var(--van-light-gray);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.formula-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.formula-label {
|
||||
font-size: 12px;
|
||||
color: var(--van-text-color-2);
|
||||
}
|
||||
|
||||
.formula-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--van-text-color);
|
||||
}
|
||||
|
||||
.formula-operator {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--van-text-color-2);
|
||||
padding: 0 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WebApi.Test.Repository;
|
||||
namespace WebApi.Test.Repository;
|
||||
|
||||
public class BudgetRepositoryTest : TransactionTestBase
|
||||
{
|
||||
@@ -64,4 +64,62 @@ public class BudgetRepositoryTest : TransactionTestBase
|
||||
var b2_updated = all.First(b => b.Name == "B2");
|
||||
b2_updated.SelectedCategories.Should().Be("美食");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCurrentAmountAsync_收入预算家庭年终奖金_Test()
|
||||
{
|
||||
// Arrange
|
||||
// 创建收入交易记录,分类为"家庭年终奖金"
|
||||
await _transactionRepository.AddAsync(CreateIncome(50000, classify: "家庭年终奖金", reason: "年终奖"));
|
||||
await _transactionRepository.AddAsync(CreateIncome(30000, classify: "家庭年终奖金", reason: "绩效奖"));
|
||||
|
||||
// 创建其他收入交易,不应计入该预算
|
||||
await _transactionRepository.AddAsync(CreateIncome(20000, classify: "工资", reason: "月工资"));
|
||||
|
||||
// 创建收入预算,包含"家庭年终奖金"分类
|
||||
var budget = new BudgetRecord
|
||||
{
|
||||
Limit = 100000,
|
||||
Category = BudgetCategory.Income,
|
||||
SelectedCategories = "家庭年终奖金",
|
||||
Name = "家庭年终奖金"
|
||||
};
|
||||
|
||||
var startDate = DateTime.Now.AddDays(-1);
|
||||
var endDate = DateTime.Now.AddDays(1);
|
||||
|
||||
// Act
|
||||
var amount = await _repository.GetCurrentAmountAsync(budget, startDate, endDate);
|
||||
|
||||
// Assert
|
||||
// 应该汇总两条"家庭年终奖金"交易:50000 + 30000 = 80000
|
||||
amount.Should().Be(80000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCurrentAmountAsync_收入预算多个分类_Test()
|
||||
{
|
||||
// Arrange
|
||||
await _transactionRepository.AddAsync(CreateIncome(50000, classify: "家庭年终奖金"));
|
||||
await _transactionRepository.AddAsync(CreateIncome(20000, classify: "工资"));
|
||||
|
||||
// 创建收入预算,包含多个分类
|
||||
var budget = new BudgetRecord
|
||||
{
|
||||
Limit = 80000,
|
||||
Category = BudgetCategory.Income,
|
||||
SelectedCategories = "家庭年终奖金,工资",
|
||||
Name = "年收入"
|
||||
};
|
||||
|
||||
var startDate = DateTime.Now.AddDays(-1);
|
||||
var endDate = DateTime.Now.AddDays(1);
|
||||
|
||||
// Act
|
||||
var amount = await _repository.GetCurrentAmountAsync(budget, startDate, endDate);
|
||||
|
||||
// Assert
|
||||
// 应该汇总两个分类的交易:50000 + 20000 = 70000
|
||||
amount.Should().Be(70000);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user