feat: add budget update functionality and enhance budget management UI
- Implemented UpdateAsync method in BudgetController for updating budget details. - Created UpdateBudgetDto for handling budget update requests. - Added BudgetCard component for displaying budget information with progress tracking. - Developed BudgetEditPopup component for creating and editing budget entries. - Introduced BudgetSummary component for summarizing budget statistics by period. - Enhanced budget period display logic in BudgetDto to support various timeframes.
This commit is contained in:
219
Web/src/components/Budget/BudgetCard.vue
Normal file
219
Web/src/components/Budget/BudgetCard.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<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="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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
budget: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
statusTagType: {
|
||||
type: String,
|
||||
default: 'success'
|
||||
},
|
||||
statusTagText: {
|
||||
type: String,
|
||||
default: '进行中'
|
||||
},
|
||||
progressColor: {
|
||||
type: String,
|
||||
default: '#1989fa'
|
||||
},
|
||||
percentClass: {
|
||||
type: [String, Object],
|
||||
default: ''
|
||||
},
|
||||
periodLabel: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['toggle-stop', 'switch-period', 'click'])
|
||||
|
||||
const percentage = computed(() => {
|
||||
if (!props.budget.limit) return 0
|
||||
return Math.round((props.budget.current / props.budget.limit) * 100)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.budget-card {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.budget-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.amount-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 16px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.info-item) .label {
|
||||
font-size: 12px;
|
||||
color: #969799;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
:deep(.info-item) .value {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.value.expense) {
|
||||
color: #ee0a24;
|
||||
}
|
||||
|
||||
:deep(.value.income) {
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
color: #646566;
|
||||
}
|
||||
|
||||
.percent.warning {
|
||||
color: #ff976a;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.percent.income {
|
||||
color: #07c160;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user