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
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:
@@ -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>
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<template>
|
||||
<div class="summary-card common-card">
|
||||
<div v-if="stats && (stats.month || stats.year)" class="summary-card common-card">
|
||||
<template v-for="(config, key) in periodConfigs" :key="key">
|
||||
<div class="summary-item">
|
||||
<div class="label">{{ config.label }}{{ title }}率</div>
|
||||
<div class="value" :class="getValueClass(stats[key].rate)">
|
||||
{{ stats[key].rate }}<span class="unit">%</span>
|
||||
<div class="value" :class="getValueClass(stats[key]?.rate || '0.0')">
|
||||
{{ stats[key]?.rate || '0.0' }}<span class="unit">%</span>
|
||||
</div>
|
||||
<div class="sub-info">
|
||||
<span class="amount">¥{{ formatMoney(stats[key]?.current || 0) }}</span>
|
||||
<span class="separator">/</span>
|
||||
<span class="amount">¥{{ formatMoney(stats[key]?.limit || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="config.showDivider" class="divider"></div>
|
||||
@@ -32,6 +37,10 @@ const periodConfigs = {
|
||||
month: { label: '本月', showDivider: true },
|
||||
year: { label: '年度', showDivider: false }
|
||||
}
|
||||
|
||||
const formatMoney = (val) => {
|
||||
return parseFloat(val || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -58,7 +67,6 @@ const periodConfigs = {
|
||||
.summary-item .value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 2px;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
@@ -80,8 +88,20 @@ const periodConfigs = {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.summary-item .sub-label {
|
||||
font-size: 11px;
|
||||
.summary-item .sub-info {
|
||||
font-size: 12px;
|
||||
color: #c8c9cc;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.summary-item .amount {
|
||||
color: #646566;
|
||||
}
|
||||
|
||||
.summary-item .separator {
|
||||
color: #c8c9cc;
|
||||
}
|
||||
|
||||
@@ -96,6 +116,9 @@ const periodConfigs = {
|
||||
.summary-item .value {
|
||||
color: #f5f5f5;
|
||||
}
|
||||
.summary-item .amount {
|
||||
color: #c8c9cc;
|
||||
}
|
||||
.divider {
|
||||
background-color: #2c2c2c;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user