发布
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 28s
Docker Build & Deploy / Deploy to Production (push) Successful in 11s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s

This commit is contained in:
孙诚
2026-01-16 15:56:53 +08:00
parent f77cc57cab
commit 0c95b6aa6e
8 changed files with 1150 additions and 906 deletions

View File

@@ -5,6 +5,15 @@
title="预算管理"
placeholder
>
<template #title>
<div
class="nav-date-picker"
@click="showDatePicker = true"
>
<span>{{ currentYear }}{{ currentMonth }}</span>
<van-icon name="arrow-down" />
</div>
</template>
<template #right>
<van-icon
v-if="
@@ -26,14 +35,7 @@
@click="showArchiveSummary()"
/>
<van-icon
v-if="activeTab !== BudgetCategory.Savings"
name="plus"
size="20"
title="添加预算"
@click="budgetEditRef.open({ category: activeTab })"
/>
<van-icon
v-else
v-if="activeTab === BudgetCategory.Savings"
name="setting-o"
size="20"
title="储蓄分类配置"
@@ -52,174 +54,26 @@
title="支出"
:name="BudgetCategory.Expense"
>
<BudgetSummary
v-if="activeTab !== BudgetCategory.Savings"
v-model:date="selectedDate"
:stats="overallStats"
:title="activeTabTitle"
:get-value-class="getValueClass"
/>
<van-pull-refresh
v-model="isRefreshing"
class="scroll-content"
@refresh="onRefresh"
>
<div class="budget-list">
<template v-if="expenseBudgets?.length > 0">
<van-swipe-cell
v-for="budget in expenseBudgets"
:key="budget.id"
>
<BudgetCard
:budget="budget"
:progress-color="getProgressColor(budget)"
:percent-class="{
warning: budget.current / budget.limit > 0.8
}"
:period-label="getPeriodLabel(budget.type)"
@click="
budgetEditRef.open({
data: budget,
isEditFlag: true,
category: budget.category
})
"
>
<template #amount-info>
<div class="info-item">
<div class="label">
已支出
</div>
<div class="value expense">
¥{{ 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"
:class="budget.limit - budget.current >= 0 ? 'income' : 'expense'"
>
¥{{ formatMoney(budget.limit - budget.current) }}
</div>
</div>
</template>
</BudgetCard>
<template #right>
<van-button
square
text="删除"
type="danger"
class="delete-button"
@click="handleDelete(budget)"
/>
</template>
</van-swipe-cell>
</template>
<van-empty
v-else
description="暂无支出预算"
/>
</div>
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))" />
</van-pull-refresh>
<div class="scroll-content">
<BudgetChartAnalysis
:overall-stats="overallStats"
:budgets="expenseBudgets"
:active-tab="activeTab"
/>
</div>
</van-tab>
<van-tab
title="收入"
:name="BudgetCategory.Income"
>
<BudgetSummary
v-if="activeTab !== BudgetCategory.Savings"
v-model:date="selectedDate"
:stats="overallStats"
:title="activeTabTitle"
:get-value-class="getValueClass"
/>
<van-pull-refresh
v-model="isRefreshing"
class="scroll-content"
@refresh="onRefresh"
>
<div class="budget-list">
<template v-if="incomeBudgets?.length > 0">
<van-swipe-cell
v-for="budget in incomeBudgets"
:key="budget.id"
>
<BudgetCard
:budget="budget"
:progress-color="getProgressColor(budget)"
:percent-class="{
income: budget.current / budget.limit >= 1
}"
:period-label="getPeriodLabel(budget.type)"
@click="
budgetEditRef.open({
data: budget,
isEditFlag: true,
category: budget.category
})
"
>
<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"
:class="budget.current >= budget.limit ? 'income' : 'expense'"
>
¥{{ formatMoney(Math.abs(budget.limit - budget.current)) }}
</div>
</div>
</template>
</BudgetCard>
<template #right>
<van-button
square
text="删除"
type="danger"
class="delete-button"
@click="handleDelete(budget)"
/>
</template>
</van-swipe-cell>
</template>
<van-empty
v-else
description="暂无收入预算"
/>
</div>
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))" />
</van-pull-refresh>
<div class="scroll-content">
<BudgetChartAnalysis
:overall-stats="overallStats"
:budgets="incomeBudgets"
:active-tab="activeTab"
/>
</div>
</van-tab>
<van-tab
@@ -305,6 +159,16 @@
</van-tab>
</van-tabs>
<!-- 悬浮编辑按钮 -->
<van-floating-bubble
v-if="activeTab !== BudgetCategory.Savings"
v-model:offset="bubbleOffset"
icon="edit"
axis="xy"
magnetic="x"
@click="showListPopup = true"
/>
<BudgetEditPopup
ref="budgetEditRef"
@success="fetchBudgetList"
@@ -314,6 +178,247 @@
@success="fetchBudgetList"
/>
<!-- 预算明细列表弹窗 -->
<PopupContainer
v-model="showListPopup"
:title="popupTitle"
height="80%"
>
<template #header-actions>
<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>
<van-pull-refresh
v-model="isRefreshing"
style="min-height: 100%"
@refresh="onRefresh"
>
<div class="budget-list">
<!-- 支出列表 -->
<template v-if="activeTab === BudgetCategory.Expense">
<template v-if="expenseBudgets?.length > 0">
<van-swipe-cell
v-for="budget in expenseBudgets"
:key="budget.id"
>
<BudgetCard
:budget="budget"
:progress-color="getProgressColor(budget)"
:percent-class="{
warning: budget.current / budget.limit > 0.8
}"
:period-label="getPeriodLabel(budget.type)"
@click="
budgetEditRef.open({
data: budget,
isEditFlag: true,
category: budget.category
})
"
>
<template #amount-info>
<div class="info-item">
<div class="label">
已支出
</div>
<div class="value expense">
¥{{ 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"
:class="budget.limit - budget.current >= 0 ? 'income' : 'expense'"
>
¥{{ formatMoney(budget.limit - budget.current) }}
</div>
</div>
</template>
</BudgetCard>
<template #right>
<van-button
square
text="删除"
type="danger"
class="delete-button"
@click="handleDelete(budget)"
/>
</template>
</van-swipe-cell>
</template>
<van-empty
v-else
description="暂无支出预算"
/>
</template>
<!-- 收入列表 -->
<template v-if="activeTab === BudgetCategory.Income">
<template v-if="incomeBudgets?.length > 0">
<van-swipe-cell
v-for="budget in incomeBudgets"
:key="budget.id"
>
<BudgetCard
:budget="budget"
:progress-color="getProgressColor(budget)"
:percent-class="{
income: budget.current / budget.limit >= 1
}"
:period-label="getPeriodLabel(budget.type)"
@click="
budgetEditRef.open({
data: budget,
isEditFlag: true,
category: budget.category
})
"
>
<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"
:class="budget.current >= budget.limit ? 'income' : 'expense'"
>
¥{{ formatMoney(Math.abs(budget.limit - budget.current)) }}
</div>
</div>
</template>
</BudgetCard>
<template #right>
<van-button
square
text="删除"
type="danger"
class="delete-button"
@click="handleDelete(budget)"
/>
</template>
</van-swipe-cell>
</template>
<van-empty
v-else
description="暂无收入预算"
/>
</template>
<!-- 存款列表 -->
<template v-if="activeTab === BudgetCategory.Savings">
<template v-if="savingsBudgets?.length > 0">
<BudgetCard
v-for="budget in savingsBudgets"
:key="budget.id"
:budget="budget"
:progress-color="getProgressColor(budget)"
:percent-class="{ income: budget.current / budget.limit >= 1 }"
:period-label="getPeriodLabel(budget.type)"
style="margin: 0 12px 12px"
>
<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>
</template>
<template #footer>
<div class="card-footer-actions">
<van-button
size="small"
icon="arrow-left"
plain
type="primary"
@click.stop="handleSavingsNav(budget, -1)"
/>
<span class="current-date-label">
{{ getSavingsDateLabel(budget) }}
</span>
<van-button
size="small"
icon="arrow"
plain
type="primary"
icon-position="right"
:disabled="disabledSavingsNextNav(budget)"
@click.stop="handleSavingsNav(budget, 1)"
/>
</div>
</template>
</BudgetCard>
</template>
<van-empty
v-else
description="暂无存款计划"
/>
</template>
</div>
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))" />
</van-pull-refresh>
</PopupContainer>
<PopupContainer
v-model="showUncoveredDetails"
title="未覆盖预算的分类"
@@ -373,6 +478,23 @@
/>
</div>
</PopupContainer>
<!-- 日期选择弹窗 -->
<van-popup
v-model:show="showDatePicker"
position="bottom"
round
>
<van-date-picker
v-model="pickerDate"
title="选择年月"
:min-date="minDate"
:max-date="maxDate"
:columns-type="['year', 'month']"
@confirm="onConfirmDate"
@cancel="showDatePicker = false"
/>
</van-popup>
</div>
</template>
@@ -392,16 +514,36 @@ import BudgetCard from '@/components/Budget/BudgetCard.vue'
import BudgetSummary from '@/components/Budget/BudgetSummary.vue'
import BudgetEditPopup from '@/components/Budget/BudgetEditPopup.vue'
import SavingsConfigPopup from '@/components/Budget/SavingsConfigPopup.vue'
import BudgetChartAnalysis from '@/components/Budget/BudgetChartAnalysis.vue'
import PopupContainer from '@/components/PopupContainer.vue'
const activeTab = ref(BudgetCategory.Expense)
const selectedDate = ref(new Date())
const showDatePicker = ref(false)
const minDate = new Date(2020, 0, 1)
const maxDate = new Date(2030, 11, 31)
const pickerDate = ref([
selectedDate.value.getFullYear().toString(),
(selectedDate.value.getMonth() + 1).toString().padStart(2, '0')
])
const currentYear = computed(() => selectedDate.value.getFullYear())
const currentMonth = computed(() => selectedDate.value.getMonth() + 1)
const budgetEditRef = ref(null)
const savingsConfigRef = ref(null)
const isRefreshing = ref(false)
const showUncoveredDetails = ref(false)
const showListPopup = ref(false)
const uncoveredCategories = ref([])
// 初始化悬浮按钮位置,避免遮挡底部导航栏
// 默认位置:右下角,距离底部 100px (避开 Tabbar),距离右侧 24px
const bubbleOffset = ref({
x: window.innerWidth - 48 - 24,
y: window.innerHeight - 48 - 100
})
const showSummaryPopup = ref(false)
const archiveSummary = ref('')
@@ -420,6 +562,19 @@ const activeTabTitle = computed(() => {
return '达成'
})
const popupTitle = computed(() => {
switch (activeTab.value) {
case BudgetCategory.Expense:
return '支出预算明细'
case BudgetCategory.Income:
return '收入预算明细'
case BudgetCategory.Savings:
return '存款计划明细'
default:
return '预算明细'
}
})
const isArchive = computed(() => {
const now = new Date()
return (
@@ -437,6 +592,12 @@ watch(selectedDate, async () => {
await Promise.all([fetchBudgetList(), fetchCategoryStats(), fetchUncoveredCategories()])
})
const onConfirmDate = ({ selectedValues }) => {
const [year, month] = selectedValues
selectedDate.value = new Date(parseInt(year), parseInt(month) - 1, 1)
showDatePicker.value = false
}
const getValueClass = (rate) => {
const numRate = parseFloat(rate)
if (numRate === 0) {
@@ -844,4 +1005,13 @@ const disabledSavingsNextNav = (budget) => {
:deep(.van-nav-bar) {
background: transparent !important;
}
.nav-date-picker {
display: flex;
align-items: center;
gap: 4px;
font-size: 16px;
font-weight: 600;
color: var(--van-text-color);
}
</style>