重构预算管理模块,添加预算记录和服务,更新相关API,优化预算统计逻辑
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user