更新预算归档功能,添加归档总结和更新归档总结接口,优化预算统计逻辑,调整相关样式
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

@@ -12,19 +12,6 @@ export function getBudgetList(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 预算数据
@@ -84,15 +71,27 @@ export function getUncoveredCategories(category, referenceDate) {
params: { category, referenceDate }
})
}
/**
* 归档预算
* @param {number} year 年份
* @param {number} month 月份
* 获取归档总结
* @param {string} referenceDate 参考日期
*/
export function archiveBudgets(year, month) {
export function getArchiveSummary(referenceDate) {
return request({
url: `/Budget/ArchiveBudgetsAsync/${year}/${month}`,
method: 'post'
url: '/Budget/GetArchiveSummary',
method: 'get',
params: { referenceDate }
})
}
/**
* 更新归档总结
* @param {object} data 数据 { referenceDate, summary }
*/
export function updateArchiveSummary(data) {
return request({
url: '/Budget/UpdateArchiveSummary',
method: 'post',
data
})
}

View File

@@ -42,9 +42,8 @@
</div>
<!-- 展开状态 -->
<Transition v-else :name="transitionName">
<div :key="budget.period" class="budget-inner-card">
<div class="card-header" style="margin-bottom: 0;">
<div v-else class="budget-inner-card">
<div class="card-header" style="margin-bottom: 0;">
<div class="budget-info">
<slot name="tag">
<van-tag
@@ -133,31 +132,7 @@
</div>
</van-collapse-transition>
</div>
<div class="card-footer">
<div class="period-navigation" @click.stop>
<van-button
icon="arrow-left"
class="nav-icon"
plain
size="small"
style="width: 50px;"
@click="handleSwitch(-1)"
/>
<span class="period-text">{{ budget.period }}</span>
<van-button
icon="arrow"
class="nav-icon"
plain
size="small"
style="width: 50px;"
:disabled="isNextDisabled"
@click="handleSwitch(1)"
/>
</div>
</div>
</div>
</Transition>
</div>
<!-- 关联账单列表弹窗 -->
@@ -205,10 +180,9 @@ const props = defineProps({
}
})
const emit = defineEmits(['switch-period', 'click'])
const emit = defineEmits(['click'])
const isExpanded = ref(props.budget.category === 2)
const transitionName = ref('slide-left')
const showDescription = ref(false)
const showBillListModal = ref(false)
const billList = ref([])
@@ -218,16 +192,6 @@ const toggleExpand = () => {
isExpanded.value = !isExpanded.value
}
const isNextDisabled = computed(() => {
if (!props.budget.periodEnd) return false
return new Date(props.budget.periodEnd) > new Date()
})
const handleSwitch = (direction) => {
transitionName.value = direction > 0 ? 'slide-left' : 'slide-right'
emit('switch-period', direction)
}
const handleQueryBills = async () => {
showBillListModal.value = true
billLoading.value = true
@@ -402,40 +366,6 @@ const timePercentage = computed(() => {
cursor: pointer;
}
/* 切换动画 */
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active {
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-left-enter-from {
transform: translateX(100%);
opacity: 0;
}
.slide-left-leave-to {
transform: translateX(-100%);
opacity: 0;
}
.slide-right-enter-from {
transform: translateX(-100%);
opacity: 0;
}
.slide-right-leave-to {
transform: translateX(100%);
opacity: 0;
}
.slide-left-leave-active,
.slide-right-leave-active {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.budget-info {
display: flex;
align-items: center;
@@ -564,42 +494,7 @@ const timePercentage = computed(() => {
line-height: 1.4;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
color: #969799;
padding: 12px 12px 0;
padding-top: 8px;
border-top: 1px solid #ebedf0;
}
.period-navigation {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.period-text {
font-size: 14px;
font-weight: 500;
color: #323233;
}
.nav-icon {
padding: 4px;
font-size: 12px;
color: #1989fa;
}
@media (prefers-color-scheme: dark) {
.card-footer {
border-top-color: #2c2c2c;
}
.period-text {
color: #f5f5f5;
}
.budget-description {
background-color: #2c2c2c;
}

View File

@@ -71,8 +71,9 @@
.rich-html-content thead,
.rich-html-content tbody {
display: table;
min-width: 100%;
width: 100%;
table-layout: fixed; /* 核心:强制列宽分配逻辑一致 */
border-collapse: collapse;
}
.rich-html-content tr {
@@ -86,10 +87,11 @@
text-align: left;
border: none;
border-bottom: 1px solid var(--van-border-color-light);
font-size: 12px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
}
.rich-html-content th {

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>