Files
EmailBill/Web/src/components/Budget/BudgetCard.vue

319 lines
6.7 KiB
Vue
Raw Normal View History

<template>
<div class="common-card budget-card" @click="$emit('click')">
<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">
<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>
</Transition>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { BudgetPeriodType } from '@/constants/enums'
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: ''
}
})
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>
.budget-card {
margin-left: 0;
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 {
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: 12px 0;
text-align: center;
}
:deep(.info-item) .label {
font-size: 12px;
color: #969799;
margin-bottom: 2px;
}
:deep(.info-item) .value {
font-size: 15px;
font-weight: 600;
}
:deep(.value.expense) {
color: #ee0a24;
}
:deep(.value.income) {
color: #07c160;
}
.progress-section {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
font-size: 13px;
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 {
color: #ff976a;
font-weight: bold;
}
.percent.income {
color: #07c160;
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;
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>