All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 21s
Docker Build & Deploy / Deploy to Production (push) Successful in 8s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
425 lines
9.0 KiB
Vue
425 lines
9.0 KiB
Vue
<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
|
|
:type="budget.type === BudgetPeriodType.Year ? 'warning' : 'primary'"
|
|
plain
|
|
class="status-tag"
|
|
>
|
|
{{ budget.type === BudgetPeriodType.Year ? '年度预算' : '月度预算' }}
|
|
</van-tag>
|
|
</slot>
|
|
</div>
|
|
<div class="header-actions">
|
|
<slot name="actions">
|
|
<van-button
|
|
v-if="budget.description"
|
|
:icon="showDescription ? 'info' : 'info-o'"
|
|
size="mini"
|
|
: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)"
|
|
/>
|
|
</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 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>
|
|
|
|
<van-collapse-transition>
|
|
<div v-if="budget.description && showDescription" class="budget-description">
|
|
<div class="description-content" v-html="budget.description"></div>
|
|
</div>
|
|
</van-collapse-transition>
|
|
</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
|
|
},
|
|
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 showDescription = ref(false)
|
|
|
|
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) 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;
|
|
}
|
|
|
|
.budget-description {
|
|
margin-top: 8px;
|
|
background-color: #f7f8fa;
|
|
border-radius: 4px;
|
|
padding: 8px;
|
|
}
|
|
|
|
.description-content {
|
|
font-size: 11px;
|
|
color: #646566;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.description-content :deep(h3) {
|
|
margin: 12px 0 6px;
|
|
font-size: 13px;
|
|
color: #323233;
|
|
border-left: 3px solid #1989fa;
|
|
padding-left: 8px;
|
|
}
|
|
|
|
.description-content :deep(table) {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 8px 0;
|
|
background: #fff;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.description-content :deep(th),
|
|
.description-content :deep(td) {
|
|
text-align: left;
|
|
padding: 6px 4px;
|
|
border-bottom: 1px solid #f2f3f5;
|
|
}
|
|
|
|
.description-content :deep(th) {
|
|
background-color: #f7f8fa;
|
|
color: #969799;
|
|
font-weight: normal;
|
|
font-size: 10px;
|
|
}
|
|
|
|
.description-content :deep(p) {
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.description-content :deep(.income-value) {
|
|
color: #07c160;
|
|
}
|
|
|
|
.description-content :deep(.expense-value) {
|
|
color: #ee0a24;
|
|
}
|
|
|
|
.description-content :deep(.highlight) {
|
|
background-color: #fffbe6;
|
|
color: #ed6a0c;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-weight: bold;
|
|
border: 1px solid #ffe58f;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.description-content :deep(h3) {
|
|
color: #f5f5f5;
|
|
}
|
|
.description-content :deep(table) {
|
|
background: #1a1a1a;
|
|
}
|
|
.description-content :deep(th) {
|
|
background-color: #242424;
|
|
}
|
|
.description-content :deep(td) {
|
|
border-bottom-color: #2c2c2c;
|
|
}
|
|
.description-content :deep(.highlight) {
|
|
background-color: #3e371a;
|
|
color: #ff976a;
|
|
border-color: #594a1a;
|
|
}
|
|
}
|
|
|
|
.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;
|
|
}
|
|
.budget-description {
|
|
background-color: #2c2c2c;
|
|
}
|
|
.description-content {
|
|
color: #969799;
|
|
}
|
|
}
|
|
</style>
|