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>
|
public interface IBudgetRepository : IBaseRepository<BudgetRecord>
|
||||||
{
|
{
|
||||||
@@ -16,7 +16,7 @@ public class BudgetRepository(IFreeSql freeSql) : BaseRepository<BudgetRecord>(f
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(budget.SelectedCategories))
|
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));
|
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>
|
<template>
|
||||||
<!-- 普通预算卡片 -->
|
<!-- 普通预算卡片 -->
|
||||||
<div
|
<div
|
||||||
@@ -115,6 +115,14 @@
|
|||||||
title="查询关联账单"
|
title="查询关联账单"
|
||||||
@click.stop="handleQueryBills"
|
@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">
|
<template v-if="budget.category !== 2">
|
||||||
<van-button
|
<van-button
|
||||||
icon="edit"
|
icon="edit"
|
||||||
@@ -313,6 +321,14 @@
|
|||||||
title="查询关联账单"
|
title="查询关联账单"
|
||||||
@click.stop="handleQueryBills"
|
@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">
|
<template v-if="budget.category !== 2">
|
||||||
<van-button
|
<van-button
|
||||||
icon="edit"
|
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 isExpanded = ref(props.budget.category === 2)
|
||||||
const showDescription = ref(false)
|
const showDescription = ref(false)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
: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"
|
||||||
|
@show-detail="handleShowDetail"
|
||||||
>
|
>
|
||||||
<template #amount-info>
|
<template #amount-info>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
@@ -68,14 +69,130 @@
|
|||||||
description="暂无存款计划"
|
description="暂无存款计划"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
import BudgetCard from '@/components/Budget/BudgetCard.vue'
|
import BudgetCard from '@/components/Budget/BudgetCard.vue'
|
||||||
import { BudgetPeriodType } from '@/constants/enums'
|
import { BudgetPeriodType } from '@/constants/enums'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
budgets: {
|
budgets: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
@@ -83,7 +200,17 @@ defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Emits
|
// 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) => {
|
const formatMoney = (val) => {
|
||||||
@@ -209,7 +336,9 @@ const getProgressColor = (budget) => {
|
|||||||
.info-item .value {
|
.info-item .value {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-family: DIN Alternate, system-ui;
|
font-family:
|
||||||
|
DIN Alternate,
|
||||||
|
system-ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item .value.expense {
|
.info-item .value.expense {
|
||||||
@@ -219,4 +348,149 @@ const getProgressColor = (budget) => {
|
|||||||
.info-item .value.income {
|
.info-item .value.income {
|
||||||
color: var(--van-success-color);
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace WebApi.Test.Repository;
|
namespace WebApi.Test.Repository;
|
||||||
|
|
||||||
public class BudgetRepositoryTest : TransactionTestBase
|
public class BudgetRepositoryTest : TransactionTestBase
|
||||||
{
|
{
|
||||||
@@ -64,4 +64,62 @@ public class BudgetRepositoryTest : TransactionTestBase
|
|||||||
var b2_updated = all.First(b => b.Name == "B2");
|
var b2_updated = all.First(b => b.Name == "B2");
|
||||||
b2_updated.SelectedCategories.Should().Be("美食");
|
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