重构预算管理模块,添加预算记录和服务,更新相关API,优化预算统计逻辑
This commit is contained in:
83
Entity/BudgetRecord.cs
Normal file
83
Entity/BudgetRecord.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
namespace Entity;
|
||||
|
||||
/// <summary>
|
||||
/// 预算管理
|
||||
/// </summary>
|
||||
public class BudgetRecord : BaseEntity
|
||||
{
|
||||
/// <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 BudgetCategory Category { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 相关分类 (逗号分隔的分类名称)
|
||||
/// </summary>
|
||||
public string SelectedCategories { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 是否停止
|
||||
/// </summary>
|
||||
public bool IsStopped { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 开始日期
|
||||
/// </summary>
|
||||
public DateTime StartDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 上次同步时间
|
||||
/// </summary>
|
||||
public DateTime? LastSync { get; set; }
|
||||
}
|
||||
|
||||
public enum BudgetPeriodType
|
||||
{
|
||||
/// <summary>
|
||||
/// 周
|
||||
/// </summary>
|
||||
Week,
|
||||
/// <summary>
|
||||
/// 月
|
||||
/// </summary>
|
||||
Month,
|
||||
/// <summary>
|
||||
/// 年
|
||||
/// </summary>
|
||||
Year,
|
||||
/// <summary>
|
||||
/// 长期
|
||||
/// </summary>
|
||||
Longterm
|
||||
}
|
||||
|
||||
public enum BudgetCategory
|
||||
{
|
||||
/// <summary>
|
||||
/// 支出
|
||||
/// </summary>
|
||||
Expense = 0,
|
||||
/// <summary>
|
||||
/// 收入
|
||||
/// </summary>
|
||||
Income = 1,
|
||||
/// <summary>
|
||||
/// 存款
|
||||
/// </summary>
|
||||
Savings = 2
|
||||
}
|
||||
36
Repository/BudgetRepository.cs
Normal file
36
Repository/BudgetRepository.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
namespace Repository;
|
||||
|
||||
public interface IBudgetRepository : IBaseRepository<BudgetRecord>
|
||||
{
|
||||
Task<decimal> GetCurrentAmountAsync(BudgetRecord budget, DateTime startDate, DateTime endDate);
|
||||
}
|
||||
|
||||
public class BudgetRepository(IFreeSql freeSql) : BaseRepository<BudgetRecord>(freeSql), IBudgetRepository
|
||||
{
|
||||
public async Task<decimal> GetCurrentAmountAsync(BudgetRecord budget, DateTime startDate, DateTime endDate)
|
||||
{
|
||||
var query = FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => t.OccurredAt >= startDate && t.OccurredAt <= endDate);
|
||||
|
||||
if (!string.IsNullOrEmpty(budget.SelectedCategories))
|
||||
{
|
||||
var categoryList = budget.SelectedCategories.Split(',');
|
||||
query = query.Where(t => categoryList.Contains(t.Classify));
|
||||
}
|
||||
|
||||
if (budget.Category == BudgetCategory.Expense)
|
||||
{
|
||||
query = query.Where(t => t.Type == TransactionType.Expense);
|
||||
}
|
||||
else if (budget.Category == BudgetCategory.Income)
|
||||
{
|
||||
query = query.Where(t => t.Type == TransactionType.Income);
|
||||
}
|
||||
else if (budget.Category == BudgetCategory.Savings)
|
||||
{
|
||||
query = query.Where(t => t.Type == TransactionType.None);
|
||||
}
|
||||
|
||||
return await query.SumAsync(t => t.Amount);
|
||||
}
|
||||
}
|
||||
94
Service/BudgetService.cs
Normal file
94
Service/BudgetService.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
namespace Service;
|
||||
|
||||
public interface IBudgetService
|
||||
{
|
||||
Task<List<BudgetRecord>> GetAllAsync();
|
||||
Task<BudgetRecord?> GetByIdAsync(long id);
|
||||
Task<bool> AddAsync(BudgetRecord budget);
|
||||
Task<bool> DeleteAsync(long id);
|
||||
Task<bool> UpdateAsync(BudgetRecord budget);
|
||||
Task<decimal> CalculateCurrentAmountAsync(BudgetRecord budget, DateTime? now = null);
|
||||
Task<bool> ToggleStopAsync(long id);
|
||||
}
|
||||
|
||||
public class BudgetService(
|
||||
IBudgetRepository budgetRepository,
|
||||
ILogger<BudgetService> logger) : IBudgetService
|
||||
{
|
||||
public async Task<List<BudgetRecord>> GetAllAsync()
|
||||
{
|
||||
var list = await budgetRepository.GetAllAsync();
|
||||
return list.ToList();
|
||||
}
|
||||
|
||||
public async Task<BudgetRecord?> GetByIdAsync(long id)
|
||||
{
|
||||
return await budgetRepository.GetByIdAsync(id);
|
||||
}
|
||||
|
||||
public async Task<bool> AddAsync(BudgetRecord budget)
|
||||
{
|
||||
return await budgetRepository.AddAsync(budget);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(long id)
|
||||
{
|
||||
return await budgetRepository.DeleteAsync(id);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(BudgetRecord budget)
|
||||
{
|
||||
return await budgetRepository.UpdateAsync(budget);
|
||||
}
|
||||
|
||||
public async Task<bool> ToggleStopAsync(long id)
|
||||
{
|
||||
var budget = await budgetRepository.GetByIdAsync(id);
|
||||
if (budget == null) return false;
|
||||
budget.IsStopped = !budget.IsStopped;
|
||||
return await budgetRepository.UpdateAsync(budget);
|
||||
}
|
||||
|
||||
public async Task<decimal> CalculateCurrentAmountAsync(BudgetRecord budget, DateTime? now = null)
|
||||
{
|
||||
if (budget.IsStopped) return 0;
|
||||
|
||||
var referenceDate = now ?? DateTime.Now;
|
||||
var (startDate, endDate) = GetPeriodRange(budget.StartDate, budget.Type, referenceDate);
|
||||
|
||||
return await budgetRepository.GetCurrentAmountAsync(budget, startDate, endDate);
|
||||
}
|
||||
|
||||
public static (DateTime start, DateTime end) GetPeriodRange(DateTime startDate, BudgetPeriodType type, DateTime referenceDate)
|
||||
{
|
||||
if (type == BudgetPeriodType.Longterm) return (startDate, DateTime.MaxValue);
|
||||
|
||||
DateTime start;
|
||||
DateTime end;
|
||||
|
||||
if (type == BudgetPeriodType.Week)
|
||||
{
|
||||
var daysFromStart = (referenceDate.Date - startDate.Date).Days;
|
||||
var weeksFromStart = daysFromStart / 7;
|
||||
start = startDate.Date.AddDays(weeksFromStart * 7);
|
||||
end = start.AddDays(6).AddHours(23).AddMinutes(59).AddSeconds(59);
|
||||
}
|
||||
else if (type == BudgetPeriodType.Month)
|
||||
{
|
||||
start = new DateTime(referenceDate.Year, referenceDate.Month, 1);
|
||||
end = start.AddMonths(1).AddDays(-1).AddHours(23).AddMinutes(59).AddSeconds(59);
|
||||
}
|
||||
else if (type == BudgetPeriodType.Year)
|
||||
{
|
||||
start = new DateTime(referenceDate.Year, 1, 1);
|
||||
end = new DateTime(referenceDate.Year, 12, 31).AddHours(23).AddMinutes(59).AddSeconds(59);
|
||||
}
|
||||
else
|
||||
{
|
||||
start = startDate;
|
||||
end = DateTime.MaxValue;
|
||||
}
|
||||
|
||||
return (start, end);
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,6 @@
|
||||
<van-tabbar-item name="statistics" icon="chart-trending-o" to="/" @click="handleTabClick('/statistics')">
|
||||
统计
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item name="budget" icon="bill-o" to="/budget" @click="handleTabClick('/budget')">
|
||||
预算
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item
|
||||
name="balance"
|
||||
icon="balance-list"
|
||||
@@ -21,6 +18,9 @@
|
||||
>
|
||||
账单
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item name="budget" icon="bill-o" to="/budget" @click="handleTabClick('/budget')">
|
||||
预算
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item name="setting" icon="setting" to="/setting">
|
||||
设置
|
||||
</van-tabbar-item>
|
||||
|
||||
74
Web/src/api/budget.js
Normal file
74
Web/src/api/budget.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import request from './request'
|
||||
|
||||
/**
|
||||
* 获取预算列表
|
||||
* @param {string} referenceDate 参考日期 (可选)
|
||||
*/
|
||||
export function getBudgetList(referenceDate) {
|
||||
return request({
|
||||
url: '/Budget/GetList',
|
||||
method: 'get',
|
||||
params: { 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 预算数据
|
||||
*/
|
||||
export function createBudget(data) {
|
||||
return request({
|
||||
url: '/Budget/Create',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除预算
|
||||
* @param {number} id 预算ID
|
||||
*/
|
||||
export function deleteBudget(id) {
|
||||
return request({
|
||||
url: `/Budget/DeleteById/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换预算状态 (停止/恢复)
|
||||
* @param {number} id 预算ID
|
||||
*/
|
||||
export function toggleStopBudget(id) {
|
||||
return request({
|
||||
url: '/Budget/ToggleStop',
|
||||
method: 'post',
|
||||
params: { id }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步预算进度
|
||||
* @param {number} id 预算ID
|
||||
* @param {string} referenceDate 参考日期 (可选)
|
||||
*/
|
||||
export function syncBudget(id, referenceDate) {
|
||||
return request({
|
||||
url: '/Budget/Sync',
|
||||
method: 'post',
|
||||
params: { id, referenceDate }
|
||||
})
|
||||
}
|
||||
27
Web/src/constants/enums.js
Normal file
27
Web/src/constants/enums.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 预算周期类型
|
||||
*/
|
||||
export const BudgetPeriodType = {
|
||||
Week: 0,
|
||||
Month: 1,
|
||||
Year: 2,
|
||||
Longterm: 3
|
||||
}
|
||||
|
||||
/**
|
||||
* 预算类别
|
||||
*/
|
||||
export const BudgetCategory = {
|
||||
Expense: 0,
|
||||
Income: 1,
|
||||
Savings: 2
|
||||
}
|
||||
|
||||
/**
|
||||
* 交易类型 (与后端 TransactionType 对应)
|
||||
*/
|
||||
export const TransactionType = {
|
||||
Expense: 0,
|
||||
Income: 1,
|
||||
None: 2
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<div class="page-content">
|
||||
<van-tabs v-model:active="activeTab" sticky offset-top="46" type="card">
|
||||
<van-tab title="支出" name="expense">
|
||||
<van-tab title="支出" :name="BudgetCategory.Expense">
|
||||
<div class="budget-list">
|
||||
<template v-if="expenseBudgets?.length > 0">
|
||||
<van-swipe-cell v-for="budget in expenseBudgets" :key="budget.id">
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
</van-tab>
|
||||
|
||||
<van-tab title="收入" name="income">
|
||||
<van-tab title="收入" :name="BudgetCategory.Income">
|
||||
<div class="budget-list">
|
||||
<template v-if="incomeBudgets?.length > 0">
|
||||
<van-swipe-cell v-for="budget in incomeBudgets" :key="budget.id">
|
||||
@@ -146,7 +146,7 @@
|
||||
</div>
|
||||
</van-tab>
|
||||
|
||||
<van-tab title="存款" name="savings">
|
||||
<van-tab title="存款" :name="BudgetCategory.Savings">
|
||||
<div class="budget-list">
|
||||
<template v-if="savingsBudgets?.length > 0">
|
||||
<van-swipe-cell v-for="budget in savingsBudgets" :key="budget.id">
|
||||
@@ -222,7 +222,7 @@
|
||||
<!-- 添加预算弹窗 -->
|
||||
<PopupContainer v-model="showAddPopup" title="新增预算" height="70%">
|
||||
<div class="add-budget-form">
|
||||
<van-form @submit="onSubmit">
|
||||
<van-form>
|
||||
<van-cell-group inset>
|
||||
<van-field
|
||||
v-model="form.name"
|
||||
@@ -234,10 +234,10 @@
|
||||
<van-field name="type" label="统计周期">
|
||||
<template #input>
|
||||
<van-radio-group v-model="form.type" direction="horizontal">
|
||||
<van-radio name="week">周</van-radio>
|
||||
<van-radio name="month">月</van-radio>
|
||||
<van-radio name="year">年</van-radio>
|
||||
<van-radio name="longterm">长期</van-radio>
|
||||
<van-radio :name="BudgetPeriodType.Week">周</van-radio>
|
||||
<van-radio :name="BudgetPeriodType.Month">月</van-radio>
|
||||
<van-radio :name="BudgetPeriodType.Year">年</van-radio>
|
||||
<van-radio :name="BudgetPeriodType.Longterm">长期</van-radio>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
</van-field>
|
||||
@@ -256,9 +256,9 @@
|
||||
<van-field name="category" label="类型">
|
||||
<template #input>
|
||||
<van-radio-group v-model="form.category" direction="horizontal">
|
||||
<van-radio name="expense">支出</van-radio>
|
||||
<van-radio name="income">收入</van-radio>
|
||||
<van-radio name="savings">存款</van-radio>
|
||||
<van-radio :name="BudgetCategory.Expense">支出</van-radio>
|
||||
<van-radio :name="BudgetCategory.Income">收入</van-radio>
|
||||
<van-radio :name="BudgetCategory.Savings">存款</van-radio>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
</van-field>
|
||||
@@ -296,137 +296,41 @@
|
||||
</div>
|
||||
|
||||
</van-cell-group>
|
||||
<div style="margin: 32px 16px;">
|
||||
<van-button round block type="primary" native-type="submit">
|
||||
保 存
|
||||
</van-button>
|
||||
</div>
|
||||
</van-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<van-button block type="primary" @click="onSubmit">保存预算</van-button>
|
||||
</template>
|
||||
</PopupContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { showToast, showConfirmDialog } from 'vant'
|
||||
import PopupContainer from '@/components/PopupContainer.vue'
|
||||
import { getCategoryList } from '@/api/transactionCategory'
|
||||
import { getBudgetList, createBudget, deleteBudget, toggleStopBudget, syncBudget, getBudgetStatistics } from '@/api/budget'
|
||||
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
|
||||
import PopupContainer from '@/components/PopupContainer.vue'
|
||||
|
||||
const activeTab = ref('expense')
|
||||
const activeTab = ref(BudgetCategory.Expense)
|
||||
const showAddPopup = ref(false)
|
||||
const categories = ref([])
|
||||
|
||||
// 模拟数据 (提前定义以防止模板渲染时 undefined)
|
||||
const expenseBudgets = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '总支出预算',
|
||||
type: 'month',
|
||||
limit: 5000,
|
||||
current: 3250.5,
|
||||
isStopped: false,
|
||||
startDate: '2026-01-01',
|
||||
period: '2026-01-01 ~ 2026-01-31',
|
||||
lastSync: '2026-01-06 10:00',
|
||||
syncing: false
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '餐饮美食',
|
||||
type: 'month',
|
||||
limit: 1500,
|
||||
current: 1420,
|
||||
isStopped: false,
|
||||
startDate: '2026-01-01',
|
||||
period: '2026-01-01 ~ 2026-01-31',
|
||||
lastSync: '2026-01-06 09:30',
|
||||
syncing: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '年度旅游',
|
||||
type: 'year',
|
||||
limit: 10000,
|
||||
current: 0,
|
||||
isStopped: true,
|
||||
startDate: '2026-01-01',
|
||||
period: '2026-01-01 ~ 2026-12-31',
|
||||
lastSync: '2026-01-01 00:00',
|
||||
syncing: false
|
||||
}
|
||||
])
|
||||
|
||||
const incomeBudgets = ref([
|
||||
{
|
||||
id: 101,
|
||||
name: '月度薪资',
|
||||
type: 'month',
|
||||
limit: 10000,
|
||||
current: 10000,
|
||||
isStopped: false,
|
||||
startDate: '2026-01-01',
|
||||
period: '2026-01-01 ~ 2026-01-31',
|
||||
lastSync: '2026-01-06 10:05',
|
||||
syncing: false
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
name: '理财收益',
|
||||
type: 'year',
|
||||
limit: 2000,
|
||||
current: 450.8,
|
||||
isStopped: false,
|
||||
startDate: '2026-01-01',
|
||||
period: '2026-01-01 ~ 2026-12-31',
|
||||
lastSync: '2026-01-06 10:05',
|
||||
syncing: false
|
||||
}
|
||||
])
|
||||
|
||||
const savingsBudgets = ref([
|
||||
{
|
||||
id: 201,
|
||||
name: '买房基金',
|
||||
type: 'year',
|
||||
limit: 500000,
|
||||
current: 125000,
|
||||
isStopped: false,
|
||||
startDate: '2025-01-01',
|
||||
period: '2025-01-01 ~ 2030-12-31',
|
||||
lastSync: '2026-01-06 11:00',
|
||||
syncing: false
|
||||
},
|
||||
{
|
||||
id: 202,
|
||||
name: '养老金',
|
||||
type: 'year',
|
||||
limit: 1000000,
|
||||
current: 50000,
|
||||
isStopped: false,
|
||||
startDate: '2026-01-01',
|
||||
period: '长期',
|
||||
lastSync: '2026-01-06 11:00',
|
||||
syncing: false
|
||||
}
|
||||
])
|
||||
const expenseBudgets = ref([])
|
||||
const incomeBudgets = ref([])
|
||||
const savingsBudgets = ref([])
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
type: 'month',
|
||||
category: 'expense',
|
||||
type: BudgetPeriodType.Month,
|
||||
category: BudgetCategory.Expense,
|
||||
limit: '',
|
||||
selectedCategories: []
|
||||
})
|
||||
|
||||
const categoryTypeMap = {
|
||||
expense: 0,
|
||||
income: 1,
|
||||
savings: 2
|
||||
}
|
||||
|
||||
const filteredCategories = computed(() => {
|
||||
const targetType = categoryTypeMap[form.category]
|
||||
const targetType = form.category === BudgetCategory.Expense ? 0 : (form.category === BudgetCategory.Income ? 1 : 2)
|
||||
return categories.value.filter(c => c.type === targetType)
|
||||
})
|
||||
|
||||
@@ -452,14 +356,31 @@ const toggleAll = () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const fetchBudgetList = async () => {
|
||||
try {
|
||||
const res = await getCategoryList()
|
||||
const res = await getBudgetList()
|
||||
if (res.success) {
|
||||
categories.value = res.data || []
|
||||
const data = res.data || []
|
||||
expenseBudgets.value = data.filter(b => b.category === BudgetCategory.Expense)
|
||||
incomeBudgets.value = data.filter(b => b.category === BudgetCategory.Income)
|
||||
savingsBudgets.value = data.filter(b => b.category === BudgetCategory.Savings)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载分类失败', err)
|
||||
console.error('加载预算列表失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [catRes] = await Promise.all([
|
||||
getCategoryList(),
|
||||
fetchBudgetList()
|
||||
])
|
||||
if (catRes.success) {
|
||||
categories.value = catRes.data || []
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取初始化数据失败', err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -468,14 +389,15 @@ watch(() => form.category, () => {
|
||||
})
|
||||
|
||||
const formatMoney = (val) => {
|
||||
return parseFloat(val).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
return parseFloat(val || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
const getPeriodLabel = (type) => {
|
||||
const map = {
|
||||
week: '本周',
|
||||
month: '本月',
|
||||
year: '本年'
|
||||
[BudgetPeriodType.Week]: '本周',
|
||||
[BudgetPeriodType.Month]: '本月',
|
||||
[BudgetPeriodType.Year]: '本年',
|
||||
[BudgetPeriodType.Longterm]: '长期'
|
||||
}
|
||||
return map[type] || '周期'
|
||||
}
|
||||
@@ -493,113 +415,109 @@ const getIncomeProgressColor = (budget) => {
|
||||
return '#1989fa' // 蓝色
|
||||
}
|
||||
|
||||
const getPeriodRange = (startDate, type) => {
|
||||
if (!startDate || startDate === '长期') return startDate
|
||||
const start = new Date(startDate)
|
||||
let end = new Date(startDate)
|
||||
|
||||
if (type === 'week') {
|
||||
end.setDate(start.getDate() + 6)
|
||||
} else if (type === 'month') {
|
||||
end = new Date(start.getFullYear(), start.getMonth() + 1, 0)
|
||||
} else if (type === 'year') {
|
||||
end = new Date(start.getFullYear(), 11, 31)
|
||||
}
|
||||
|
||||
const format = (d) => d.toISOString().split('T')[0]
|
||||
return `${format(start)} ~ ${format(end)}`
|
||||
}
|
||||
const refDateMap = {}
|
||||
|
||||
const handleSwitchPeriod = (budget, direction) => {
|
||||
if (!budget.startDate || budget.period === '长期') return
|
||||
|
||||
const currentStart = new Date(budget.startDate)
|
||||
if (budget.type === 'week') {
|
||||
currentStart.setDate(currentStart.getDate() + direction * 7)
|
||||
} else if (budget.type === 'month') {
|
||||
currentStart.setMonth(currentStart.getMonth() + direction)
|
||||
currentStart.setDate(1)
|
||||
} else if (budget.type === 'year') {
|
||||
currentStart.setFullYear(currentStart.getFullYear() + direction)
|
||||
currentStart.setMonth(0)
|
||||
currentStart.setDate(1)
|
||||
const handleSwitchPeriod = async (budget, direction) => {
|
||||
if (budget.type === BudgetPeriodType.Longterm) {
|
||||
showToast('长期预算不支持切换周期')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取或初始化该预算的参考日期
|
||||
let currentRefDate = refDateMap[budget.id] || new Date()
|
||||
const date = new Date(currentRefDate)
|
||||
|
||||
if (budget.type === BudgetPeriodType.Week) {
|
||||
date.setDate(date.getDate() + direction * 7)
|
||||
} else 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)
|
||||
}
|
||||
|
||||
budget.startDate = currentStart.toISOString().split('T')[0]
|
||||
budget.period = getPeriodRange(budget.startDate, budget.type)
|
||||
|
||||
// 模拟数据更新
|
||||
budget.current = Math.floor(Math.random() * budget.limit * 1.2 * 100) / 100
|
||||
budget.lastSync = new Date().toLocaleString()
|
||||
}
|
||||
|
||||
const handleDelete = (budget) => {
|
||||
showConfirmDialog({
|
||||
title: '确认删除',
|
||||
message: `确定要删除预算 "${budget.name}" 吗?`,
|
||||
}).then(() => {
|
||||
expenseBudgets.value = expenseBudgets.value.filter(b => b.id !== budget.id)
|
||||
incomeBudgets.value = incomeBudgets.value.filter(b => b.id !== budget.id)
|
||||
savingsBudgets.value = savingsBudgets.value.filter(b => b.id !== budget.id)
|
||||
showToast('已删除')
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteBudget(budget.id)
|
||||
if (res.success) {
|
||||
showToast('已删除')
|
||||
delete refDateMap[budget.id]
|
||||
fetchBudgetList()
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('删除失败')
|
||||
console.error('删除预算失败', err)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const handleSync = (budget) => {
|
||||
const handleSync = async (budget) => {
|
||||
budget.syncing = true
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const refDate = refDateMap[budget.id] ? refDateMap[budget.id].toISOString() : null
|
||||
const res = await syncBudget(budget.id, refDate)
|
||||
if (res.success) {
|
||||
showToast('同步成功')
|
||||
Object.assign(budget, res.data)
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('同步失败')
|
||||
console.error('同步预算失败', err)
|
||||
} finally {
|
||||
budget.syncing = false
|
||||
budget.lastSync = new Date().toLocaleString()
|
||||
showToast('同步成功')
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleStop = (budget) => {
|
||||
budget.isStopped = !budget.isStopped
|
||||
showToast(budget.isStopped ? '已停止' : '已恢复')
|
||||
const handleToggleStop = async (budget) => {
|
||||
try {
|
||||
const res = await toggleStopBudget(budget.id)
|
||||
if (res.success) {
|
||||
showToast(budget.isStopped ? '已恢复' : '已停止')
|
||||
// 切换停止状态后刷新列表
|
||||
fetchBudgetList()
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('操作失败')
|
||||
console.error('切换预算状态失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
const startDate = new Date()
|
||||
if (form.type === 'month') startDate.setDate(1)
|
||||
if (form.type === 'year') {
|
||||
startDate.setMonth(0)
|
||||
startDate.setDate(1)
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
const res = await createBudget({
|
||||
...form,
|
||||
limit: parseFloat(form.limit),
|
||||
categoryNames: form.selectedCategories
|
||||
})
|
||||
if (res.success) {
|
||||
showToast('保存成功')
|
||||
showAddPopup.value = false
|
||||
fetchBudgetList()
|
||||
// 重置表单
|
||||
form.name = ''
|
||||
form.limit = ''
|
||||
form.selectedCategories = []
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('保存失败')
|
||||
console.error('保存预算失败', err)
|
||||
}
|
||||
const startDateStr = startDate.toISOString().split('T')[0]
|
||||
|
||||
const newBudget = {
|
||||
id: Date.now(),
|
||||
name: form.name,
|
||||
type: form.type,
|
||||
limit: parseFloat(form.limit),
|
||||
categories: [...form.selectedCategories],
|
||||
current: 0,
|
||||
isStopped: false,
|
||||
startDate: startDateStr,
|
||||
period: getPeriodRange(startDateStr, form.type),
|
||||
lastSync: '刚刚',
|
||||
syncing: false
|
||||
}
|
||||
|
||||
if (form.category === 'expense') {
|
||||
expenseBudgets.value.unshift(newBudget)
|
||||
activeTab.value = 'expense'
|
||||
} else if (form.category === 'income') {
|
||||
incomeBudgets.value.unshift(newBudget)
|
||||
activeTab.value = 'income'
|
||||
} else {
|
||||
savingsBudgets.value.unshift(newBudget)
|
||||
activeTab.value = 'savings'
|
||||
}
|
||||
|
||||
showAddPopup.value = false
|
||||
showToast('添加成功')
|
||||
|
||||
// 重置表单
|
||||
form.name = ''
|
||||
form.limit = ''
|
||||
form.selectedCategories = []
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
151
WebApi/Controllers/BudgetController.cs
Normal file
151
WebApi/Controllers/BudgetController.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
namespace WebApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]/[action]")]
|
||||
public class BudgetController(
|
||||
IBudgetService budgetService,
|
||||
ILogger<BudgetController> logger) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取预算列表
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<BaseResponse<List<BudgetDto>>> GetListAsync([FromQuery] DateTime? referenceDate = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var budgets = await budgetService.GetAllAsync();
|
||||
var dtos = new List<BudgetDto>();
|
||||
|
||||
foreach (var budget in budgets)
|
||||
{
|
||||
var currentAmount = await budgetService.CalculateCurrentAmountAsync(budget, referenceDate);
|
||||
dtos.Add(BudgetDto.FromEntity(budget, currentAmount, referenceDate));
|
||||
}
|
||||
|
||||
return dtos.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "获取预算列表失败");
|
||||
return $"获取预算列表失败: {ex.Message}".Fail<List<BudgetDto>>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个预算统计信息
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<BaseResponse<BudgetDto>> GetStatisticsAsync([FromQuery] long id, [FromQuery] DateTime? referenceDate = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var budget = await budgetService.GetByIdAsync(id);
|
||||
if (budget == null) return "预算不存在".Fail<BudgetDto>();
|
||||
|
||||
var currentAmount = await budgetService.CalculateCurrentAmountAsync(budget, referenceDate);
|
||||
return BudgetDto.FromEntity(budget, currentAmount, referenceDate).Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "获取预算统计失败, Id: {Id}", id);
|
||||
return $"获取预算统计失败: {ex.Message}".Fail<BudgetDto>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建预算
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<BaseResponse<long>> CreateAsync([FromBody] CreateBudgetDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var budget = new BudgetRecord
|
||||
{
|
||||
Name = dto.Name,
|
||||
Type = dto.Type,
|
||||
Limit = dto.Limit,
|
||||
Category = dto.Category,
|
||||
SelectedCategories = dto.SelectedCategories != null ? string.Join(",", dto.SelectedCategories) : string.Empty,
|
||||
StartDate = dto.StartDate ?? DateTime.Now,
|
||||
LastSync = DateTime.Now
|
||||
};
|
||||
|
||||
var success = await budgetService.AddAsync(budget);
|
||||
if (success)
|
||||
{
|
||||
return budget.Id.Ok();
|
||||
}
|
||||
return "创建预算失败".Fail<long>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "创建预算失败");
|
||||
return $"创建预算失败: {ex.Message}".Fail<long>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除预算
|
||||
/// </summary>
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<BaseResponse> DeleteByIdAsync(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var success = await budgetService.DeleteAsync(id);
|
||||
return success ? BaseResponse.Done() : "删除预算失败".Fail();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "删除预算失败, Id: {Id}", id);
|
||||
return $"删除预算失败: {ex.Message}".Fail();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换预算暂停状态
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<BaseResponse> ToggleStopAsync([FromQuery] long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var success = await budgetService.ToggleStopAsync(id);
|
||||
return success ? BaseResponse.Done() : "操作失败".Fail();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "切换预算状态失败, Id: {Id}", id);
|
||||
return $"操作失败: {ex.Message}".Fail();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步预算数据
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<BaseResponse<BudgetDto>> SyncAsync([FromQuery] long id, [FromQuery] DateTime? referenceDate = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var budget = await budgetService.GetByIdAsync(id);
|
||||
if (budget == null)
|
||||
{
|
||||
return "预算不存在".Fail<BudgetDto>();
|
||||
}
|
||||
|
||||
budget.LastSync = DateTime.Now;
|
||||
await budgetService.UpdateAsync(budget);
|
||||
|
||||
var currentAmount = await budgetService.CalculateCurrentAmountAsync(budget, referenceDate);
|
||||
return BudgetDto.FromEntity(budget, currentAmount, referenceDate).Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "同步预算失败, Id: {Id}", id);
|
||||
return $"同步失败: {ex.Message}".Fail<BudgetDto>();
|
||||
}
|
||||
}
|
||||
}
|
||||
49
WebApi/Controllers/Dto/BudgetDto.cs
Normal file
49
WebApi/Controllers/Dto/BudgetDto.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
namespace WebApi.Controllers.Dto;
|
||||
|
||||
public class BudgetDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public BudgetPeriodType Type { get; set; }
|
||||
public decimal Limit { get; set; }
|
||||
public decimal Current { get; set; }
|
||||
public BudgetCategory Category { get; set; }
|
||||
public string[] SelectedCategories { get; set; } = Array.Empty<string>();
|
||||
public bool IsStopped { get; set; }
|
||||
public string StartDate { get; set; } = string.Empty;
|
||||
public string Period { get; set; } = string.Empty;
|
||||
public string LastSync { get; set; } = string.Empty;
|
||||
|
||||
public static BudgetDto FromEntity(BudgetRecord entity, decimal currentAmount = 0, DateTime? referenceDate = null)
|
||||
{
|
||||
var date = referenceDate ?? DateTime.Now;
|
||||
var (start, end) = BudgetService.GetPeriodRange(entity.StartDate, entity.Type, date);
|
||||
|
||||
return new BudgetDto
|
||||
{
|
||||
Id = entity.Id,
|
||||
Name = entity.Name,
|
||||
Type = entity.Type,
|
||||
Limit = entity.Limit,
|
||||
Current = currentAmount,
|
||||
Category = entity.Category,
|
||||
SelectedCategories = string.IsNullOrEmpty(entity.SelectedCategories)
|
||||
? Array.Empty<string>()
|
||||
: entity.SelectedCategories.Split(','),
|
||||
IsStopped = entity.IsStopped,
|
||||
StartDate = entity.StartDate.ToString("yyyy-MM-dd"),
|
||||
Period = entity.Type == BudgetPeriodType.Longterm ? "长期" : $"{start:yyyy-MM-dd} ~ {end:yyyy-MM-dd}",
|
||||
LastSync = entity.LastSync?.ToString("yyyy-MM-dd HH:mm") ?? "未同步"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateBudgetDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public BudgetPeriodType Type { get; set; } = BudgetPeriodType.Month;
|
||||
public decimal Limit { get; set; }
|
||||
public BudgetCategory Category { get; set; }
|
||||
public string[] SelectedCategories { get; set; } = Array.Empty<string>();
|
||||
public DateTime? StartDate { get; set; }
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using FreeSql;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
Reference in New Issue
Block a user