更新预算归档功能,添加归档总结和更新归档总结接口,优化预算统计逻辑,调整相关样式
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 34s
Docker Build & Deploy / Deploy to Production (push) Successful in 9s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s

This commit is contained in:
2026-01-12 22:29:39 +08:00
parent 03115a04ec
commit 556fc5af20
14 changed files with 337 additions and 408 deletions

View File

@@ -3,23 +3,36 @@
<van-nav-bar title="预算管理" placeholder>
<template #right>
<van-icon
v-if="activeTab !== BudgetCategory.Savings && uncoveredCategories.length > 0"
v-if="activeTab !== BudgetCategory.Savings
&& uncoveredCategories.length > 0
&& !isArchive"
name="warning-o"
size="20"
color="#ee0a24"
style="margin-right: 12px"
title="查看未覆盖预算的分类"
@click="showUncoveredDetails = true"
/>
<van-icon
v-if="isArchive"
name="records-o"
size="20"
title="已归档月份总结"
style="margin-right: 12px"
@click="showArchiveSummary()"
/>
<van-icon
v-if="activeTab !== BudgetCategory.Savings"
name="plus"
size="20"
title="添加预算"
@click="budgetEditRef.open({ category: activeTab })"
/>
<van-icon
v-else
name="setting-o"
size="20"
title="储蓄分类配置"
@click="savingsConfigRef.open()"
/>
</template>
@@ -43,7 +56,6 @@
:progress-color="getProgressColor(budget)"
:percent-class="{ 'warning': (budget.current / budget.limit) > 0.8 }"
:period-label="getPeriodLabel(budget.type)"
@switch-period="(dir) => handleSwitchPeriod(budget, dir)"
@click="budgetEditRef.open({
data: budget,
isEditFlag: true,
@@ -95,7 +107,6 @@
:progress-color="getIncomeProgressColor(budget)"
:percent-class="{ 'income': (budget.current / budget.limit) >= 1 }"
:period-label="getPeriodLabel(budget.type)"
@switch-period="(dir) => handleSwitchPeriod(budget, dir)"
@click="budgetEditRef.open({
data: budget,
isEditFlag: true,
@@ -142,7 +153,6 @@
: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">
@@ -204,13 +214,44 @@
</van-button>
</template>
</PopupContainer>
<PopupContainer
v-model="showSummaryPopup"
title="月份归档总结"
:subtitle="`${selectedDate.getFullYear()}年${selectedDate.getMonth() + 1}月`"
height="50%"
>
<div style="padding: 16px;">
<van-field
v-model="archiveSummary"
rows="6"
autosize
label="总结语"
type="textarea"
:placeholder="`请输入${selectedDate.getFullYear()}年${selectedDate.getMonth() + 1}月预算执行的总结或感悟...`"
show-word-limit
maxlength="500"
/>
</div>
<template #footer>
<van-button
block
round
type="primary"
:loading="isSavingSummary"
@click="handleSaveSummary"
>
保存总结
</van-button>
</template>
</PopupContainer>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { showToast, showConfirmDialog } from 'vant'
import { getBudgetList, deleteBudget, getBudgetStatistics, getCategoryStats, getUncoveredCategories } from '@/api/budget'
import { getBudgetList, deleteBudget, getCategoryStats, getUncoveredCategories, getArchiveSummary, updateArchiveSummary } from '@/api/budget'
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
import BudgetCard from '@/components/Budget/BudgetCard.vue'
import BudgetSummary from '@/components/Budget/BudgetSummary.vue'
@@ -226,6 +267,10 @@ const isRefreshing = ref(false)
const showUncoveredDetails = ref(false)
const uncoveredCategories = ref([])
const showSummaryPopup = ref(false)
const archiveSummary = ref('')
const isSavingSummary = ref(false)
const expenseBudgets = ref([])
const incomeBudgets = ref([])
const savingsBudgets = ref([])
@@ -239,6 +284,12 @@ const activeTabTitle = computed(() => {
return '达成'
})
const isArchive = computed(() => {
const now = new Date()
return selectedDate.value.getFullYear() < now.getFullYear() ||
(selectedDate.value.getFullYear() === now.getFullYear() && selectedDate.value.getMonth() < now.getMonth())
})
watch(activeTab, async () => {
await Promise.all([fetchCategoryStats(), fetchUncoveredCategories()])
})
@@ -378,30 +429,6 @@ const getIncomeProgressColor = (budget) => {
return '#1989fa'
}
const refDateMap = {}
const handleSwitchPeriod = async (budget, direction) => {
let currentRefDate = refDateMap[budget.id] || new Date()
const date = new Date(currentRefDate)
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)
}
}
const handleDelete = (budget) => {
showConfirmDialog({
title: '确认删除',
@@ -419,6 +446,39 @@ const handleDelete = (budget) => {
}
}).catch(() => {})
}
const showArchiveSummary = async () => {
try {
const res = await getArchiveSummary(selectedDate.value.toISOString())
if (res.success) {
archiveSummary.value = res.data || ''
showSummaryPopup.value = true
}
} catch (err) {
console.error('获取总结失败', err)
showToast('获取总结失败')
}
}
const handleSaveSummary = async () => {
if (isSavingSummary.value) return
isSavingSummary.value = true
try {
const res = await updateArchiveSummary({
referenceDate: selectedDate.value.toISOString(),
summary: archiveSummary.value
})
if (res.success) {
showToast('已保存')
showSummaryPopup.value = false
}
} catch (err) {
console.error('保存总结失败', err)
showToast('保存总结失败')
} finally {
isSavingSummary.value = false
}
}
</script>
<style scoped>