feat: 添加分类统计功能,支持获取月度和年度预算统计信息
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 22s
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-09 15:42:59 +08:00
parent b41121400d
commit 2244d08b43
9 changed files with 524 additions and 177 deletions

View File

@@ -1,11 +1,10 @@
<template>
<div class="common-card budget-card" @click="$emit('click')">
<div class="common-card budget-card" @click="toggleExpand">
<div class="budget-content-wrapper">
<Transition :name="transitionName">
<div :key="budget.period" class="budget-inner-card">
<div class="card-header">
<!-- 折叠状态 -->
<div v-if="!isExpanded" class="budget-collapsed">
<div class="collapsed-header">
<div class="budget-info">
<h3 class="card-title">{{ budget.name }}</h3>
<slot name="tag">
<van-tag
:type="budget.type === BudgetPeriodType.Year ? 'warning' : 'primary'"
@@ -15,6 +14,45 @@
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
</van-tag>
</slot>
<h3 class="card-title">{{ budget.name }}</h3>
</div>
<van-icon name="arrow-down" class="expand-icon" />
</div>
<div class="collapsed-footer">
<div class="collapsed-item">
<span class="compact-label">实际/目标</span>
<span class="compact-value">
<slot name="collapsed-amount">
{{ budget.current !== undefined && budget.limit !== undefined
? `¥${budget.current?.toFixed(2) || 0} / ¥${budget.limit?.toFixed(2) || 0}`
: '--' }}
</slot>
</span>
</div>
<div class="collapsed-item">
<span class="compact-label">达成率</span>
<span class="compact-value" :class="percentClass">{{ percentage }}%</span>
</div>
</div>
</div>
<!-- 展开状态 -->
<Transition v-else :name="transitionName">
<div :key="budget.period" class="budget-inner-card">
<div class="card-header">
<div class="budget-info">
<slot name="tag">
<van-tag
:type="budget.type === BudgetPeriodType.Year ? 'warning' : 'primary'"
plain
class="status-tag"
>
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
</van-tag>
</slot>
<h3 class="card-title">{{ budget.name }}</h3>
</div>
<div class="header-actions">
<slot name="actions">
@@ -25,17 +63,23 @@
:type="showDescription ? 'primary' : 'default'"
plain
round
style="margin-right: 4px;"
@click.stop="showDescription = !showDescription"
/>
<van-button
v-if="budget.category !== 2"
:icon="budget.isStopped ? 'play' : 'pause'"
size="mini"
plain
round
@click.stop="$emit('toggle-stop', budget)"
/>
<template v-if="budget.category !== 2">
<van-button
icon="edit"
size="mini"
plain
@click.stop="$emit('click', budget)"
/>
<van-button
:icon="budget.isStopped ? 'play' : 'pause'"
size="mini"
plain
@click.stop="$emit('toggle-stop', budget)"
/>
</template>
</slot>
</div>
</div>
@@ -128,9 +172,14 @@ const props = defineProps({
const emit = defineEmits(['toggle-stop', 'switch-period', 'click'])
const isExpanded = ref(props.budget.category === 2)
const transitionName = ref('slide-left')
const showDescription = ref(false)
const toggleExpand = () => {
isExpanded.value = !isExpanded.value
}
const isNextDisabled = computed(() => {
if (!props.budget.periodEnd) return false
return new Date(props.budget.periodEnd) > new Date()
@@ -167,6 +216,7 @@ const timePercentage = computed(() => {
padding-bottom: 8px;
overflow: hidden;
position: relative;
cursor: pointer;
}
.budget-content-wrapper {
@@ -178,6 +228,102 @@ const timePercentage = computed(() => {
width: 100%;
}
/* 折叠状态样式 */
.budget-collapsed {
display: flex;
flex-direction: column;
gap: 6px;
}
.collapsed-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.collapsed-left {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
min-width: 0;
}
.card-title-compact {
margin: 0;
font-size: 14px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.status-tag-compact) {
padding: 2px 6px !important;
font-size: 11px !important;
height: auto !important;
flex-shrink: 0;
}
.collapsed-footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
}
.collapsed-item {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
}
.collapsed-item:first-child {
flex: 2;
}
.collapsed-item:last-child {
flex: 1;
}
.compact-label {
font-size: 12px;
color: #969799;
line-height: 1.2;
}
.compact-value {
font-size: 13px;
font-weight: 600;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.compact-value.warning {
color: #ff976a;
}
.compact-value.income {
color: #07c160;
}
.expand-icon {
color: #1989fa;
font-size: 14px;
transition: transform 0.3s ease;
flex-shrink: 0;
}
.collapse-icon {
color: #1989fa;
font-size: 16px;
cursor: pointer;
}
/* 切换动画 */
.slide-left-enter-active,
.slide-left-leave-active,
@@ -354,5 +500,8 @@ const timePercentage = computed(() => {
.description-content {
color: #969799;
}
.collapsed-row .value {
color: #f5f5f5;
}
}
</style>