重构存款预算
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 44s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 3s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s

This commit is contained in:
SunCheng
2026-01-20 19:11:05 +08:00
parent 44d9fbb0f6
commit 0ffeb41605
13 changed files with 1591 additions and 583 deletions

View File

@@ -1,10 +1,18 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<!-- 普通预算卡片 -->
<div v-if="!budget.noLimit" class="common-card budget-card" @click="toggleExpand">
<div
v-if="!budget.noLimit"
class="common-card budget-card"
:class="{ 'cursor-default': budget.category === 2 }"
@click="toggleExpand"
>
<div class="budget-content-wrapper">
<!-- 折叠状态 -->
<div v-if="!isExpanded" class="budget-collapsed">
<div
v-if="!isExpanded"
class="budget-collapsed"
>
<div class="collapsed-header">
<div class="budget-info">
<slot name="tag">
@@ -14,17 +22,26 @@
class="status-tag"
>
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
<span v-if="budget.isMandatoryExpense" class="mandatory-mark">📌</span>
<span
v-if="budget.isMandatoryExpense"
class="mandatory-mark"
>📌</span>
</van-tag>
</slot>
<h3 class="card-title">
{{ budget.name }}
</h3>
<span v-if="budget.selectedCategories?.length" class="card-subtitle">
<span
v-if="budget.selectedCategories?.length"
class="card-subtitle"
>
({{ budget.selectedCategories.join('、') }})
</span>
</div>
<van-icon name="arrow-down" class="expand-icon" />
<van-icon
name="arrow-down"
class="expand-icon"
/>
</div>
<div class="collapsed-footer">
@@ -43,14 +60,23 @@
<div class="collapsed-item">
<span class="compact-label">达成率</span>
<span class="compact-value" :class="percentClass">{{ percentage }}%</span>
<span
class="compact-value"
:class="percentClass"
>{{ percentage }}%</span>
</div>
</div>
</div>
<!-- 展开状态 -->
<div v-else class="budget-inner-card">
<div class="card-header" style="margin-bottom: 0">
<div
v-else
class="budget-inner-card"
>
<div
class="card-header"
style="margin-bottom: 0"
>
<div class="budget-info">
<slot name="tag">
<van-tag
@@ -59,10 +85,16 @@
class="status-tag"
>
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
<span v-if="budget.isMandatoryExpense" class="mandatory-mark">📌</span>
<span
v-if="budget.isMandatoryExpense"
class="mandatory-mark"
>📌</span>
</van-tag>
</slot>
<h3 class="card-title" style="max-width: 120px">
<h3
class="card-title"
style="max-width: 120px"
>
{{ budget.name }}
</h3>
</div>
@@ -84,14 +116,22 @@
@click.stop="handleQueryBills"
/>
<template v-if="budget.category !== 2">
<van-button icon="edit" size="small" plain @click.stop="$emit('click', budget)" />
<van-button
icon="edit"
size="small"
plain
@click.stop="$emit('click', budget)"
/>
</template>
</slot>
</div>
</div>
<div class="budget-body">
<div v-if="budget.selectedCategories?.length" class="category-tags">
<div
v-if="budget.selectedCategories?.length"
class="category-tags"
>
<van-tag
v-for="cat in budget.selectedCategories"
:key="cat"
@@ -109,16 +149,17 @@
<div class="progress-section">
<slot name="progress-info">
<span class="period-type"
>{{ periodLabel }}{{ budget.category === 0 ? '使用' : '达成' }}</span
>
<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>
<span
class="percent"
:class="percentClass"
>{{ percentage }}%</span>
</slot>
</div>
<div class="progress-section time-progress">
@@ -132,11 +173,25 @@
<span class="percent">{{ timePercentage }}%</span>
</div>
<van-collapse-transition>
<div v-if="budget.description && showDescription" class="budget-description">
<div class="description-content rich-html-content" v-html="budget.description" />
<transition
name="collapse"
@enter="onEnter"
@after-enter="onAfterEnter"
@leave="onLeave"
@after-leave="onAfterLeave"
>
<div
v-if="budget.description && showDescription"
class="budget-collapse-wrapper"
>
<div class="budget-description">
<div
class="description-content rich-html-content"
v-html="budget.description"
/>
</div>
</div>
</van-collapse-transition>
</transition>
</div>
<div class="card-footer">
@@ -146,7 +201,11 @@
</div>
<!-- 关联账单列表弹窗 -->
<PopupContainer v-model="showBillListModal" title="关联账单列表" height="75%">
<PopupContainer
v-model="showBillListModal"
title="关联账单列表"
height="75%"
>
<TransactionList
:transactions="billList"
:loading="billLoading"
@@ -160,25 +219,43 @@
</div>
<!-- 不记额预算卡片 -->
<div v-else class="common-card budget-card no-limit-card" @click="toggleExpand">
<div
v-else
class="common-card budget-card no-limit-card"
:class="{ 'cursor-default': budget.category === 2 }"
@click="toggleExpand"
>
<div class="budget-content-wrapper">
<!-- 折叠状态 -->
<div v-if="!isExpanded" class="budget-collapsed">
<div
v-if="!isExpanded"
class="budget-collapsed"
>
<div class="collapsed-header">
<div class="budget-info">
<slot name="tag">
<van-tag type="success" plain class="status-tag">
<van-tag
type="success"
plain
class="status-tag"
>
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
</van-tag>
</slot>
<h3 class="card-title">
{{ budget.name }}
</h3>
<span v-if="budget.selectedCategories?.length" class="card-subtitle">
<span
v-if="budget.selectedCategories?.length"
class="card-subtitle"
>
({{ budget.selectedCategories.join('、') }})
</span>
</div>
<van-icon name="arrow-down" class="expand-icon" />
<van-icon
name="arrow-down"
class="expand-icon"
/>
</div>
<div class="collapsed-footer no-limit-footer">
@@ -194,15 +271,28 @@
</div>
<!-- 展开状态 -->
<div v-else class="budget-inner-card">
<div class="card-header" style="margin-bottom: 0">
<div
v-else
class="budget-inner-card"
>
<div
class="card-header"
style="margin-bottom: 0"
>
<div class="budget-info">
<slot name="tag">
<van-tag type="success" plain class="status-tag">
<van-tag
type="success"
plain
class="status-tag"
>
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
</van-tag>
</slot>
<h3 class="card-title" style="max-width: 120px">
<h3
class="card-title"
style="max-width: 120px"
>
{{ budget.name }}
</h3>
</div>
@@ -224,14 +314,22 @@
@click.stop="handleQueryBills"
/>
<template v-if="budget.category !== 2">
<van-button icon="edit" size="small" plain @click.stop="$emit('click', budget)" />
<van-button
icon="edit"
size="small"
plain
@click.stop="$emit('click', budget)"
/>
</template>
</slot>
</div>
</div>
<div class="budget-body">
<div v-if="budget.selectedCategories?.length" class="category-tags">
<div
v-if="budget.selectedCategories?.length"
class="category-tags"
>
<van-tag
v-for="cat in budget.selectedCategories"
:key="cat"
@@ -248,31 +346,53 @@
<div class="amount-item">
<span>
<span class="label">实际</span>
<span class="value" style="margin-left: 12px"
>¥{{ budget.current?.toFixed(0) || 0 }}</span
>
<span
class="value"
style="margin-left: 12px"
>¥{{ budget.current?.toFixed(0) || 0 }}</span>
</span>
</div>
</div>
<div class="no-limit-notice">
<span>
<van-icon name="info-o" style="margin-right: 4px" />
<van-icon
name="info-o"
style="margin-right: 4px"
/>
不记额预算 - 直接计入存款明细
</span>
</div>
<van-collapse-transition>
<div v-if="budget.description && showDescription" class="budget-description">
<div class="description-content rich-html-content" v-html="budget.description" />
<transition
name="collapse"
@enter="onEnter"
@after-enter="onAfterEnter"
@leave="onLeave"
@after-leave="onAfterLeave"
>
<div
v-if="budget.description && showDescription"
class="budget-collapse-wrapper"
>
<div class="budget-description">
<div
class="description-content rich-html-content"
v-html="budget.description"
/>
</div>
</div>
</van-collapse-transition>
</transition>
</div>
</div>
</div>
<!-- 关联账单列表弹窗 -->
<PopupContainer v-model="showBillListModal" title="关联账单列表" height="75%">
<PopupContainer
v-model="showBillListModal"
title="关联账单列表"
height="75%"
>
<TransactionList
:transactions="billList"
:loading="billLoading"
@@ -321,6 +441,10 @@ const billList = ref([])
const billLoading = ref(false)
const toggleExpand = () => {
// 存款类型category === 2强制保持展开状态不可折叠
if (props.budget.category === 2) {
return
}
isExpanded.value = !isExpanded.value
}
@@ -387,6 +511,36 @@ const timePercentage = computed(() => {
return Math.round(((now - start) / (end - start)) * 100)
})
const onEnter = (el) => {
el.style.height = '0'
el.style.overflow = 'hidden'
// Force reflow
el.offsetHeight
el.style.transition = 'height 0.3s ease-in-out'
el.style.height = `${el.scrollHeight}px`
}
const onAfterEnter = (el) => {
el.style.height = ''
el.style.overflow = ''
el.style.transition = ''
}
const onLeave = (el) => {
el.style.height = `${el.scrollHeight}px`
el.style.overflow = 'hidden'
// Force reflow
el.offsetHeight
el.style.transition = 'height 0.3s ease-in-out'
el.style.height = '0'
}
const onAfterLeave = (el) => {
el.style.height = ''
el.style.overflow = ''
el.style.transition = ''
}
</script>
<style scoped>
@@ -400,6 +554,10 @@ const timePercentage = computed(() => {
cursor: pointer;
}
.budget-card.cursor-default {
cursor: default;
}
.no-limit-card {
border-left: 3px solid var(--van-success-color);
}
@@ -661,11 +819,13 @@ const timePercentage = computed(() => {
color: var(--van-success-color);
}
.budget-collapse-wrapper {
overflow: hidden;
}
.budget-description {
border-top: 1px solid var(--van-border-color);
margin-top: 8px;
background-color: var(--van-background);
border-radius: 4px;
padding: 8px;
}
.description-content {
@@ -680,15 +840,4 @@ const timePercentage = computed(() => {
display: inline-block;
}
/* @media (prefers-color-scheme: dark) {
.budget-description {
background-color: var(--van-background-2);
}
.description-content {
color: var(--van-text-color-2);
}
.collapsed-row .value {
color: var(--van-text-color);
}
} */
</style>