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:
@@ -17,8 +17,6 @@
|
||||
</template>
|
||||
</van-nav-bar>
|
||||
|
||||
|
||||
|
||||
<van-tabs v-model:active="activeTab" type="card" class="budget-tabs">
|
||||
<van-tab title="支出" :name="BudgetCategory.Expense">
|
||||
<BudgetSummary
|
||||
@@ -46,7 +44,7 @@
|
||||
>
|
||||
<template #amount-info>
|
||||
<div class="info-item">
|
||||
<div class="label">当前</div>
|
||||
<div class="label">已支出</div>
|
||||
<div class="value expense">¥{{ formatMoney(budget.current) }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
@@ -54,7 +52,7 @@
|
||||
<div class="value">¥{{ formatMoney(budget.limit) }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">结余</div>
|
||||
<div class="label">余额</div>
|
||||
<div class="value" :class="budget.limit - budget.current >= 0 ? 'income' : 'expense'">
|
||||
¥{{ formatMoney(budget.limit - budget.current) }}
|
||||
</div>
|
||||
@@ -68,20 +66,19 @@
|
||||
</template>
|
||||
<van-empty v-else description="暂无支出预算" />
|
||||
</div>
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
</div>
|
||||
</van-tab>
|
||||
</van-tab>
|
||||
|
||||
<van-tab title="收入" :name="BudgetCategory.Income">
|
||||
<BudgetSummary
|
||||
v-if="activeTab !== BudgetCategory.Savings"
|
||||
:stats="overallStats"
|
||||
:title="activeTabTitle"
|
||||
:get-value-class="getValueClass"
|
||||
/>
|
||||
<div class="scroll-content">
|
||||
<div class="budget-list">
|
||||
<van-tab title="收入" :name="BudgetCategory.Income">
|
||||
<BudgetSummary
|
||||
v-if="activeTab !== BudgetCategory.Savings"
|
||||
:stats="overallStats"
|
||||
:title="activeTabTitle"
|
||||
:get-value-class="getValueClass"
|
||||
/>
|
||||
<div class="scroll-content">
|
||||
<div class="budget-list">
|
||||
<template v-if="incomeBudgets?.length > 0">
|
||||
<van-swipe-cell v-for="budget in incomeBudgets" :key="budget.id">
|
||||
<BudgetCard
|
||||
@@ -99,11 +96,11 @@
|
||||
>
|
||||
<template #amount-info>
|
||||
<div class="info-item">
|
||||
<div class="label">当前已收</div>
|
||||
<div class="label">已收入</div>
|
||||
<div class="value income">¥{{ formatMoney(budget.current) }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">目标收入</div>
|
||||
<div class="label">目标</div>
|
||||
<div class="value">¥{{ formatMoney(budget.limit) }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
@@ -121,52 +118,49 @@
|
||||
</template>
|
||||
<van-empty v-else description="暂无收入预算" />
|
||||
</div>
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
</div>
|
||||
</van-tab>
|
||||
</van-tab>
|
||||
|
||||
<van-tab title="存款" :name="BudgetCategory.Savings">
|
||||
<div class="scroll-content" style="padding-top:4px">
|
||||
<div class="budget-list">
|
||||
<van-tab title="存款" :name="BudgetCategory.Savings">
|
||||
<div class="scroll-content" style="padding-top:4px">
|
||||
<div class="budget-list">
|
||||
<template v-if="savingsBudgets?.length > 0">
|
||||
<BudgetCard
|
||||
v-for="budget in savingsBudgets"
|
||||
:key="budget.id"
|
||||
:budget="budget"
|
||||
progress-color="#07c160"
|
||||
:percent-class="{ 'income': (budget.current / budget.limit) >= 1 }"
|
||||
:period-label="getPeriodLabel(budget.type)"
|
||||
style="margin: 0 12px 12px;"
|
||||
@switch-period="(dir) => handleSwitchPeriod(budget, dir)"
|
||||
>
|
||||
<template #amount-info>
|
||||
<div class="info-item">
|
||||
<div class="label">已存</div>
|
||||
<div class="value income">¥{{ formatMoney(budget.current) }}</div>
|
||||
v-for="budget in savingsBudgets"
|
||||
:key="budget.id"
|
||||
:budget="budget"
|
||||
progress-color="#07c160"
|
||||
:percent-class="{ 'income': (budget.current / budget.limit) >= 1 }"
|
||||
:period-label="getPeriodLabel(budget.type)"
|
||||
style="margin: 0 12px 12px;"
|
||||
@switch-period="(dir) => handleSwitchPeriod(budget, dir)"
|
||||
>
|
||||
<template #amount-info>
|
||||
<div class="info-item">
|
||||
<div class="label">已存</div>
|
||||
<div class="value income">¥{{ formatMoney(budget.current) }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">目标</div>
|
||||
<div class="value">¥{{ formatMoney(budget.limit) }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">还差</div>
|
||||
<div class="value expense">
|
||||
¥{{ formatMoney(Math.max(0, budget.limit - budget.current)) }}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">目标</div>
|
||||
<div class="value">¥{{ formatMoney(budget.limit) }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">还差</div>
|
||||
<div class="value expense">
|
||||
¥{{ formatMoney(Math.max(0, budget.limit - budget.current)) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</BudgetCard>
|
||||
</template>
|
||||
<van-empty v-else description="暂无存款计划" />
|
||||
</div>
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||
</div>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
|
||||
<!-- 添加/编辑预算弹窗 -->
|
||||
<BudgetEditPopup
|
||||
ref="budgetEditRef"
|
||||
@success="fetchBudgetList"
|
||||
@@ -181,7 +175,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { showToast, showConfirmDialog } from 'vant'
|
||||
import { getBudgetList, deleteBudget, toggleStopBudget, getBudgetStatistics } from '@/api/budget'
|
||||
import { getBudgetList, deleteBudget, toggleStopBudget, getBudgetStatistics, getCategoryStats } from '@/api/budget'
|
||||
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
|
||||
import BudgetCard from '@/components/Budget/BudgetCard.vue'
|
||||
import BudgetSummary from '@/components/Budget/BudgetSummary.vue'
|
||||
@@ -195,65 +189,18 @@ const savingsConfigRef = ref(null)
|
||||
const expenseBudgets = ref([])
|
||||
const incomeBudgets = ref([])
|
||||
const savingsBudgets = ref([])
|
||||
const overallStats = ref({
|
||||
month: { rate: '0.0', current: 0, limit: 0, count: 0 },
|
||||
year: { rate: '0.0', current: 0, limit: 0, count: 0 }
|
||||
})
|
||||
|
||||
const activeTabTitle = computed(() => {
|
||||
if (activeTab.value === BudgetCategory.Expense) return '使用'
|
||||
return '达成'
|
||||
})
|
||||
|
||||
const overallStats = computed(() => {
|
||||
const allBudgetsList = [...expenseBudgets.value, ...incomeBudgets.value, ...savingsBudgets.value]
|
||||
|
||||
const getStats = (statType) => {
|
||||
const category = activeTab.value
|
||||
// 获取当前 tab 类别下所有未停止的预算
|
||||
const relevant = allBudgetsList.filter(b => b.category === category && !b.isStopped)
|
||||
|
||||
if (relevant.length === 0) return { rate: '0.0', current: 0, limit: 0, count: 0 }
|
||||
|
||||
let totalC = 0
|
||||
let totalL = 0
|
||||
|
||||
relevant.forEach(b => {
|
||||
// 限额折算
|
||||
let itemLimit = b.limit || 0
|
||||
if (statType === BudgetPeriodType.Month && b.type === BudgetPeriodType.Year) {
|
||||
itemLimit = b.limit / 12
|
||||
} else if (statType === BudgetPeriodType.Year && b.type === BudgetPeriodType.Month) {
|
||||
itemLimit = b.limit * 12
|
||||
}
|
||||
totalL += itemLimit
|
||||
|
||||
// 当前值累加
|
||||
// 注意:由于前端 items 只有当前周期的 current,如果是跨周期统计,这里只能视为一种“参考”或“当前进度”
|
||||
if (b.type === statType) {
|
||||
totalC += (b.current || 0)
|
||||
} else {
|
||||
// 如果周期不匹配(例如在年度统计中统计月度预算),
|
||||
// 只有在当前是 1 月的情况下,月度支出才等同于年度累计。
|
||||
// 为保持统计的严谨性,这里仅在类型匹配时计入 Current,或者根据业务需求进行估计。
|
||||
// 但为了解决用户反馈的“统计不对”,我们需要把所有的匹配项都算进来。
|
||||
if (statType === BudgetPeriodType.Year) {
|
||||
// 在年度视图下,月度预算我们也计入它当前的 current(作为它对年度目前的贡献)
|
||||
totalC += (b.current || 0)
|
||||
}
|
||||
// 月度视图下,年度预算的 current 无法直接折算,此处暂不计入支出。
|
||||
}
|
||||
})
|
||||
|
||||
const rate = totalL > 0 ? (totalC / totalL) * 100 : 0
|
||||
return {
|
||||
rate: rate.toFixed(1),
|
||||
current: totalC,
|
||||
limit: totalL,
|
||||
count: relevant.length
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
month: getStats(BudgetPeriodType.Month),
|
||||
year: getStats(BudgetPeriodType.Year)
|
||||
}
|
||||
watch(activeTab, async () => {
|
||||
await fetchCategoryStats()
|
||||
})
|
||||
|
||||
const getValueClass = (rate) => {
|
||||
@@ -284,9 +231,36 @@ const fetchBudgetList = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchCategoryStats = async () => {
|
||||
try {
|
||||
const res = await getCategoryStats(activeTab.value)
|
||||
if (res.success) {
|
||||
// 转换后端返回的数据格式为前端需要的格式
|
||||
const data = res.data
|
||||
overallStats.value = {
|
||||
month: {
|
||||
rate: data.month?.rate?.toFixed(1) || '0.0',
|
||||
current: data.month?.current || 0,
|
||||
limit: data.month?.limit || 0,
|
||||
count: data.month?.count || 0
|
||||
},
|
||||
year: {
|
||||
rate: data.year?.rate?.toFixed(1) || '0.0',
|
||||
current: data.year?.current || 0,
|
||||
limit: data.year?.limit || 0,
|
||||
count: data.year?.count || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载分类统计失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await fetchBudgetList()
|
||||
await fetchCategoryStats()
|
||||
} catch (err) {
|
||||
console.error('获取初始化数据失败', err)
|
||||
}
|
||||
@@ -306,21 +280,20 @@ const getPeriodLabel = (type) => {
|
||||
|
||||
const getProgressColor = (budget) => {
|
||||
const ratio = budget.current / budget.limit
|
||||
if (ratio >= 1) return '#ee0a24' // 危险红色
|
||||
if (ratio > 0.8) return '#ff976a' // 警告橙色
|
||||
return '#1989fa' // 正常蓝色
|
||||
if (ratio >= 1) return '#ee0a24'
|
||||
if (ratio > 0.8) return '#ff976a'
|
||||
return '#1989fa'
|
||||
}
|
||||
|
||||
const getIncomeProgressColor = (budget) => {
|
||||
const ratio = budget.current / budget.limit
|
||||
if (ratio >= 1) return '#07c160' // 完成绿色
|
||||
return '#1989fa' // 蓝色
|
||||
if (ratio >= 1) return '#07c160'
|
||||
return '#1989fa'
|
||||
}
|
||||
|
||||
const refDateMap = {}
|
||||
|
||||
const handleSwitchPeriod = async (budget, direction) => {
|
||||
// 获取或初始化该预算的参考日期
|
||||
let currentRefDate = refDateMap[budget.id] || new Date()
|
||||
const date = new Date(currentRefDate)
|
||||
|
||||
@@ -334,7 +307,6 @@ const handleSwitchPeriod = async (budget, direction) => {
|
||||
const res = await getBudgetStatistics(budget.id, date.toISOString())
|
||||
if (res.success) {
|
||||
refDateMap[budget.id] = date
|
||||
// 更新当前列表中的预算对象信息
|
||||
Object.assign(budget, res.data)
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -366,7 +338,6 @@ const handleToggleStop = async (budget) => {
|
||||
const res = await toggleStopBudget(budget.id)
|
||||
if (res.success) {
|
||||
showToast(budget.isStopped ? '已恢复' : '已停止')
|
||||
// 切换停止状态后刷新列表
|
||||
fetchBudgetList()
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user