feat: 优化预算管理界面,增强预算编辑功能,添加预算删除接口
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
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:
孙诚
2026-01-07 19:19:53 +08:00
parent 851886a601
commit 35a856c6e3
5 changed files with 267 additions and 134 deletions

View File

@@ -1,75 +1,88 @@
<template>
<div class="common-card budget-card" @click="$emit('click')">
<div class="card-header">
<div class="budget-info">
<h3 class="card-title">{{ budget.name }}</h3>
<slot name="tag">
<van-tag v-if="budget.isStopped" type="danger" size="small" plain>已停止</van-tag>
<van-tag v-else :type="statusTagType" size="small" plain>{{ statusTagText }}</van-tag>
</slot>
</div>
<div class="header-actions">
<slot name="actions">
<van-button
:icon="budget.isStopped ? 'play' : 'pause'"
size="mini"
plain
round
@click.stop="$emit('toggle-stop', budget)"
/>
</slot>
</div>
</div>
<div class="budget-body">
<div class="amount-info">
<slot name="amount-info"></slot>
</div>
<div class="budget-content-wrapper">
<Transition :name="transitionName">
<div :key="budget.period" class="budget-inner-card">
<div class="card-header">
<div class="budget-info">
<h3 class="card-title">{{ budget.name }}</h3>
<slot name="tag">
<van-tag v-if="budget.isStopped" type="danger" size="small" plain>已停止</van-tag>
<van-tag v-else :type="statusTagType" size="small" plain>{{ statusTagText }}</van-tag>
</slot>
</div>
<div class="header-actions">
<slot name="actions">
<van-button
:icon="budget.isStopped ? 'play' : 'pause'"
size="mini"
plain
round
@click.stop="$emit('toggle-stop', budget)"
/>
</slot>
</div>
</div>
<div class="budget-body">
<div class="amount-info">
<slot name="amount-info"></slot>
</div>
<div class="progress-section">
<div class="progress-info">
<slot name="progress-info">
<span class="period-type">{{ periodLabel }}进度</span>
<span class="percent" :class="percentClass">
{{ percentage }}%
</span>
</slot>
<div class="progress-section">
<slot name="progress-info">
<span class="period-type">{{ periodLabel }}{{ budget.category === 0 ? '使用' : '达成' }}</span>
<van-progress
:percentage="Math.min(percentage, 100)"
stroke-width="8"
:color="progressColor"
:show-pivot="false"
/>
<span class="percent" :class="percentClass">{{ percentage }}%</span>
</slot>
</div>
<div v-if="budget.type !== BudgetPeriodType.Longterm" class="progress-section time-progress">
<span class="period-type">时间进度</span>
<van-progress
:percentage="timePercentage"
stroke-width="4"
color="#969799"
:show-pivot="false"
/>
<span class="percent">{{ timePercentage }}%</span>
</div>
</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;"
@click="handleSwitch(1)"
/>
</div>
</div>
</div>
<van-progress
:percentage="Math.min(percentage, 100)"
stroke-width="8"
:color="progressColor"
:show-pivot="false"
/>
</div>
</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="$emit('switch-period', -1)"
/>
<span class="period-text">{{ budget.period }}</span>
<van-button
icon="arrow"
class="nav-icon"
plain
size="small"
style="width: 50px;"
@click="$emit('switch-period', 1)"
/>
</div>
</Transition>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { BudgetPeriodType } from '@/constants/enums'
const props = defineProps({
budget: {
@@ -98,12 +111,31 @@ const props = defineProps({
}
})
defineEmits(['toggle-stop', 'switch-period', 'click'])
const emit = defineEmits(['toggle-stop', 'switch-period', 'click'])
const transitionName = ref('slide-left')
const handleSwitch = (direction) => {
transitionName.value = direction > 0 ? 'slide-left' : 'slide-right'
emit('switch-period', direction)
}
const percentage = computed(() => {
if (!props.budget.limit) return 0
return Math.round((props.budget.current / props.budget.limit) * 100)
})
const timePercentage = computed(() => {
if (!props.budget.periodStart || !props.budget.periodEnd || props.budget.type === BudgetPeriodType.Longterm) return 0
const start = new Date(props.budget.periodStart).getTime()
const end = new Date(props.budget.periodEnd).getTime()
const now = new Date().getTime()
if (now <= start) return 0
if (now >= end) return 100
return Math.round(((now - start) / (end - start)) * 100)
})
</script>
<style scoped>
@@ -112,6 +144,51 @@ const percentage = computed(() => {
margin-right: 0;
margin-bottom: 0;
padding-bottom: 8px;
overflow: hidden;
position: relative;
}
.budget-content-wrapper {
position: relative;
width: 100%;
}
.budget-inner-card {
width: 100%;
}
/* 切换动画 */
.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 {
@@ -134,14 +211,14 @@ const percentage = computed(() => {
.amount-info {
display: flex;
justify-content: space-between;
margin: 16px 0;
margin: 12px 0;
text-align: center;
}
:deep(.info-item) .label {
font-size: 12px;
color: #969799;
margin-bottom: 4px;
margin-bottom: 2px;
}
:deep(.info-item) .value {
@@ -158,15 +235,27 @@ const percentage = computed(() => {
}
.progress-section {
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
font-size: 13px;
color: #646566;
}
.progress-info {
display: flex;
justify-content: space-between;
font-size: 13px;
margin-bottom: 6px;
color: #646566;
.progress-section :deep(.van-progress) {
flex: 1;
}
.period-type {
white-space: nowrap;
width: 65px;
}
.percent {
white-space: nowrap;
width: 35px;
text-align: right;
}
.percent.warning {
@@ -179,6 +268,16 @@ const percentage = computed(() => {
font-weight: bold;
}
.time-progress {
margin-top: -8px;
opacity: 0.8;
}
.time-progress .period-type,
.time-progress .percent {
font-size: 11px;
}
.card-footer {
display: flex;
justify-content: space-between;