fix
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 18s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 0s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 18s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 0s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
This commit is contained in:
@@ -1,843 +0,0 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<!-- 普通预算卡片 -->
|
||||
<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 class="collapsed-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 ? '年度' : '月度' }}
|
||||
<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"
|
||||
>
|
||||
({{ budget.selectedCategories.join('、') }})
|
||||
</span>
|
||||
</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(0) || 0} / ¥${budget.limit?.toFixed(0) || 0}`
|
||||
: '--'
|
||||
}}
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="collapsed-item">
|
||||
<span class="compact-label">达成率</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 class="budget-info">
|
||||
<slot name="tag">
|
||||
<van-tag
|
||||
:type="budget.type === BudgetPeriodType.Year ? 'warning' : 'primary'"
|
||||
plain
|
||||
class="status-tag"
|
||||
>
|
||||
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
|
||||
<span
|
||||
v-if="budget.isMandatoryExpense"
|
||||
class="mandatory-mark"
|
||||
>📌</span>
|
||||
</van-tag>
|
||||
</slot>
|
||||
<h3
|
||||
class="card-title"
|
||||
style="max-width: 120px"
|
||||
>
|
||||
{{ budget.name }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<slot name="actions">
|
||||
<van-button
|
||||
v-if="budget.description"
|
||||
:icon="showDescription ? 'info' : 'info-o'"
|
||||
size="small"
|
||||
:type="showDescription ? 'primary' : 'default'"
|
||||
plain
|
||||
@click.stop="showDescription = !showDescription"
|
||||
/>
|
||||
<van-button
|
||||
icon="orders-o"
|
||||
size="small"
|
||||
plain
|
||||
title="查询关联账单"
|
||||
@click.stop="handleQueryBills"
|
||||
/>
|
||||
<template v-if="budget.category !== 2">
|
||||
<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"
|
||||
>
|
||||
<van-tag
|
||||
v-for="cat in budget.selectedCategories"
|
||||
:key="cat"
|
||||
size="mini"
|
||||
class="category-tag"
|
||||
plain
|
||||
round
|
||||
>
|
||||
{{ cat }}
|
||||
</van-tag>
|
||||
</div>
|
||||
<div class="amount-info">
|
||||
<slot name="amount-info" />
|
||||
</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="var(--van-gray-6)"
|
||||
:show-pivot="false"
|
||||
/>
|
||||
<span class="percent">{{ timePercentage }}%</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关联账单列表弹窗 -->
|
||||
<PopupContainer
|
||||
v-model="showBillListModal"
|
||||
title="关联账单列表"
|
||||
height="75%"
|
||||
>
|
||||
<TransactionList
|
||||
:transactions="billList"
|
||||
:loading="billLoading"
|
||||
:finished="true"
|
||||
:show-delete="false"
|
||||
:show-checkbox="false"
|
||||
@click="handleBillClick"
|
||||
@delete="handleBillDelete"
|
||||
/>
|
||||
</PopupContainer>
|
||||
</div>
|
||||
|
||||
<!-- 不记额预算卡片 -->
|
||||
<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 class="collapsed-header">
|
||||
<div class="budget-info">
|
||||
<slot name="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"
|
||||
>
|
||||
({{ budget.selectedCategories.join('、') }})
|
||||
</span>
|
||||
</div>
|
||||
<van-icon
|
||||
name="arrow-down"
|
||||
class="expand-icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="collapsed-footer no-limit-footer">
|
||||
<div class="collapsed-item">
|
||||
<span class="compact-label">实际</span>
|
||||
<span class="compact-value">
|
||||
<slot name="collapsed-amount">
|
||||
{{ budget.current !== undefined ? `¥${budget.current?.toFixed(0) || 0}` : '--' }}
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展开状态 -->
|
||||
<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"
|
||||
>
|
||||
{{ budget.type === BudgetPeriodType.Year ? '年度' : '月度' }}
|
||||
</van-tag>
|
||||
</slot>
|
||||
<h3
|
||||
class="card-title"
|
||||
style="max-width: 120px"
|
||||
>
|
||||
{{ budget.name }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<slot name="actions">
|
||||
<van-button
|
||||
v-if="budget.description"
|
||||
:icon="showDescription ? 'info' : 'info-o'"
|
||||
size="small"
|
||||
:type="showDescription ? 'primary' : 'default'"
|
||||
plain
|
||||
@click.stop="showDescription = !showDescription"
|
||||
/>
|
||||
<van-button
|
||||
icon="orders-o"
|
||||
size="small"
|
||||
plain
|
||||
title="查询关联账单"
|
||||
@click.stop="handleQueryBills"
|
||||
/>
|
||||
<template v-if="budget.category !== 2">
|
||||
<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"
|
||||
>
|
||||
<van-tag
|
||||
v-for="cat in budget.selectedCategories"
|
||||
:key="cat"
|
||||
size="mini"
|
||||
class="category-tag"
|
||||
plain
|
||||
round
|
||||
>
|
||||
{{ cat }}
|
||||
</van-tag>
|
||||
</div>
|
||||
|
||||
<div class="no-limit-amount-info">
|
||||
<div class="amount-item">
|
||||
<span>
|
||||
<span class="label">实际</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"
|
||||
/>
|
||||
不记额预算 - 直接计入存款明细
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关联账单列表弹窗 -->
|
||||
<PopupContainer
|
||||
v-model="showBillListModal"
|
||||
title="关联账单列表"
|
||||
height="75%"
|
||||
>
|
||||
<TransactionList
|
||||
:transactions="billList"
|
||||
:loading="billLoading"
|
||||
:finished="true"
|
||||
:show-delete="false"
|
||||
:show-checkbox="false"
|
||||
@click="handleBillClick"
|
||||
@delete="handleBillDelete"
|
||||
/>
|
||||
</PopupContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { BudgetPeriodType } from '@/constants/enums'
|
||||
import PopupContainer from '@/components/PopupContainer.vue'
|
||||
import TransactionList from '@/components/TransactionList.vue'
|
||||
import { getTransactionList } from '@/api/transactionRecord'
|
||||
|
||||
const props = defineProps({
|
||||
budget: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
progressColor: {
|
||||
type: String,
|
||||
default: 'var(--van-primary-color)'
|
||||
},
|
||||
percentClass: {
|
||||
type: [String, Object],
|
||||
default: ''
|
||||
},
|
||||
periodLabel: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
const isExpanded = ref(props.budget.category === 2)
|
||||
const showDescription = ref(false)
|
||||
const showBillListModal = ref(false)
|
||||
const billList = ref([])
|
||||
const billLoading = ref(false)
|
||||
|
||||
const toggleExpand = () => {
|
||||
// 存款类型(category === 2)强制保持展开状态,不可折叠
|
||||
if (props.budget.category === 2) {
|
||||
return
|
||||
}
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
|
||||
const handleQueryBills = async () => {
|
||||
showBillListModal.value = true
|
||||
billLoading.value = true
|
||||
|
||||
try {
|
||||
const classify = props.budget.selectedCategories
|
||||
? props.budget.selectedCategories.join(',')
|
||||
: ''
|
||||
|
||||
if (classify === '') {
|
||||
// 如果没有选中任何分类,则不查询
|
||||
billList.value = []
|
||||
billLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const response = await getTransactionList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
startDate: props.budget.periodStart,
|
||||
endDate: props.budget.periodEnd,
|
||||
classify: classify,
|
||||
type: props.budget.category,
|
||||
sortByAmount: true
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
billList.value = response.data || []
|
||||
} else {
|
||||
billList.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询账单列表失败:', error)
|
||||
billList.value = []
|
||||
} finally {
|
||||
billLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
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>
|
||||
.budget-card {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
padding: 8px 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.budget-card.cursor-default {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.no-limit-card {
|
||||
border-left: 3px solid var(--van-success-color);
|
||||
}
|
||||
|
||||
.collapsed-footer.no-limit-footer {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.budget-content-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.budget-inner-card {
|
||||
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: var(--van-text-color-2);
|
||||
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: var(--van-warning-color);
|
||||
}
|
||||
|
||||
.compact-value.income {
|
||||
color: var(--van-success-color);
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
color: var(--van-primary-color);
|
||||
font-size: 14px;
|
||||
transition: transform 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
color: var(--van-primary-color);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.budget-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--van-text-color-2);
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.category-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
|
||||
.category-tag {
|
||||
opacity: 0.7;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.amount-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 12px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.info-item) .label {
|
||||
font-size: 12px;
|
||||
color: var(--van-text-color-2);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
:deep(.info-item) .value {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.value.expense) {
|
||||
color: var(--van-danger-color);
|
||||
}
|
||||
|
||||
:deep(.value.income) {
|
||||
color: var(--van-success-color);
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--van-gray-6);
|
||||
}
|
||||
|
||||
.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: var(--van-warning-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.percent.income {
|
||||
color: var(--van-success-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.time-progress {
|
||||
margin-top: -8px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.time-progress .period-type,
|
||||
.time-progress .percent {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.no-limit-notice {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--van-text-color-2);
|
||||
background-color: var(--van-light-gray);
|
||||
border-radius: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.no-limit-amount-info {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0px 0;
|
||||
}
|
||||
|
||||
.amount-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.amount-item .label {
|
||||
font-size: 12px;
|
||||
color: var(--van-text-color-2);
|
||||
}
|
||||
|
||||
.amount-item .value {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--van-success-color);
|
||||
}
|
||||
|
||||
.budget-collapse-wrapper {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.budget-description {
|
||||
border-top: 1px solid var(--van-border-color);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.description-content {
|
||||
font-size: 11px;
|
||||
color: var(--van-gray-6);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.mandatory-mark {
|
||||
margin-left: 4px;
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -6,14 +6,15 @@
|
||||
<div class="chart-card gauge-card">
|
||||
<div class="chart-header">
|
||||
<div class="chart-title">
|
||||
<!-- 月度健康度 -->
|
||||
{{ activeTab === BudgetCategory.Expense ? '使用情况(月度)' : '完成情况(月度)' }}
|
||||
<span class="chart-title-text">
|
||||
{{ activeTab === BudgetCategory.Expense ? '使用情况(月度)' : '完成情况(月度)' }}
|
||||
</span>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
size="16"
|
||||
color="var(--van-primary-color)"
|
||||
style="margin-left: auto; cursor: pointer"
|
||||
@click="showDescriptionPopup = true; activeDescTab = 'month'"
|
||||
class="info-icon"
|
||||
@click="handleShowDescription('month')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,15 +28,15 @@
|
||||
/>
|
||||
<div class="gauge-text-overlay">
|
||||
<div class="balance-label">
|
||||
余额
|
||||
{{ activeTab === BudgetCategory.Expense ? '余额' : '差额' }}
|
||||
</div>
|
||||
<div
|
||||
class="balance-value"
|
||||
:style="{
|
||||
color:
|
||||
overallStats.month.current > overallStats.month.limit
|
||||
? 'var(--van-danger-color)'
|
||||
: ''
|
||||
activeTab === BudgetCategory.Expense
|
||||
? (overallStats.month.current > overallStats.month.limit ? 'var(--van-danger-color)' : '')
|
||||
: (overallStats.month.current < overallStats.month.limit ? 'var(--van-danger-color)' : '')
|
||||
}"
|
||||
>
|
||||
¥{{ formatMoney(Math.abs(overallStats.month.limit - overallStats.month.current)) }}
|
||||
@@ -44,11 +45,11 @@
|
||||
</div>
|
||||
<div class="gauge-footer">
|
||||
<div class="gauge-item">
|
||||
<span class="label">已用</span>
|
||||
<span class="label">{{ activeTab === BudgetCategory.Expense ? '已用' : '已获得' }}</span>
|
||||
<span class="value">¥{{ formatMoney(overallStats.month.current) }}</span>
|
||||
</div>
|
||||
<div class="gauge-item">
|
||||
<span class="label">预算</span>
|
||||
<span class="label">{{ activeTab === BudgetCategory.Expense ? '预算' : '目标' }}</span>
|
||||
<span class="value">¥{{ formatMoney(overallStats.month.limit) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,13 +59,15 @@
|
||||
<div class="chart-card gauge-card">
|
||||
<div class="chart-header">
|
||||
<div class="chart-title">
|
||||
{{ activeTab === BudgetCategory.Expense ? '使用情况(年度)' : '完成情况(年度)' }}
|
||||
<span class="chart-title-text">
|
||||
{{ activeTab === BudgetCategory.Expense ? '使用情况(年度)' : '完成情况(年度)' }}
|
||||
</span>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
size="16"
|
||||
color="var(--van-primary-color)"
|
||||
style="margin-left: auto; cursor: pointer"
|
||||
@click="showDescriptionPopup = true; activeDescTab = 'year'"
|
||||
class="info-icon"
|
||||
@click="handleShowDescription('year')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,16 +81,15 @@
|
||||
/>
|
||||
<div class="gauge-text-overlay">
|
||||
<div class="balance-label">
|
||||
余额
|
||||
{{ activeTab === BudgetCategory.Expense ? '余额' : '差额' }}
|
||||
</div>
|
||||
<div
|
||||
class="balance-value"
|
||||
:style="{
|
||||
color:
|
||||
activeTab === BudgetCategory.Expense &&
|
||||
overallStats.year.current > overallStats.year.limit
|
||||
? 'var(--van-danger-color)'
|
||||
: ''
|
||||
activeTab === BudgetCategory.Expense
|
||||
? (overallStats.year.current > overallStats.year.limit ? 'var(--van-danger-color)' : '')
|
||||
: (overallStats.year.current < overallStats.year.limit ? 'var(--van-danger-color)' : '')
|
||||
}"
|
||||
>
|
||||
¥{{ formatMoney(Math.abs(overallStats.year.limit - overallStats.year.current)) }}
|
||||
@@ -96,11 +98,11 @@
|
||||
</div>
|
||||
<div class="gauge-footer">
|
||||
<div class="gauge-item">
|
||||
<span class="label">已用</span>
|
||||
<span class="label">{{ activeTab === BudgetCategory.Expense ? '已用' : '已获得' }}</span>
|
||||
<span class="value">¥{{ formatMoney(overallStats.year.current) }}</span>
|
||||
</div>
|
||||
<div class="gauge-item">
|
||||
<span class="label">预算</span>
|
||||
<span class="label">{{ activeTab === BudgetCategory.Expense ? '预算' : '目标' }}</span>
|
||||
<span class="value">¥{{ formatMoney(overallStats.year.limit) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,7 +119,7 @@
|
||||
预算进度(月度)
|
||||
</div>
|
||||
<div class="chart-subtitle">
|
||||
预算剩余消耗趋势
|
||||
{{ activeTab === BudgetCategory.Expense ? '预算剩余消耗趋势' : '收入积累趋势' }}
|
||||
</div>
|
||||
</div>
|
||||
<BaseChart
|
||||
@@ -204,7 +206,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, nextTick, onUnmounted, computed } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { BudgetCategory, BudgetPeriodType } from '@/constants/enums'
|
||||
import { getCssVar } from '@/utils/theme'
|
||||
import PopupContainer from '@/components/PopupContainer.vue'
|
||||
@@ -239,6 +241,12 @@ const props = defineProps({
|
||||
const showDescriptionPopup = ref(false)
|
||||
const activeDescTab = ref('month')
|
||||
|
||||
// 显示描述弹窗
|
||||
const handleShowDescription = (tab) => {
|
||||
activeDescTab.value = tab
|
||||
showDescriptionPopup.value = true
|
||||
}
|
||||
|
||||
// Chart.js 相关
|
||||
const { getChartOptions } = useChartTheme()
|
||||
|
||||
@@ -595,10 +603,19 @@ const varianceChartOptions = computed(() => {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const item = context.dataset._meta[context.dataIndex]
|
||||
const diffText =
|
||||
item.value > 0
|
||||
const isExpense = props.activeTab === BudgetCategory.Expense
|
||||
|
||||
let diffText
|
||||
if (isExpense) {
|
||||
diffText = item.value > 0
|
||||
? `超支: ¥${formatMoney(item.value)}`
|
||||
: `结余: ¥${formatMoney(Math.abs(item.value))}`
|
||||
} else {
|
||||
diffText = item.value > 0
|
||||
? `超额: ¥${formatMoney(item.value)}`
|
||||
: `未达标: ¥${formatMoney(Math.abs(item.value))}`
|
||||
}
|
||||
|
||||
return [
|
||||
`预算: ¥${formatMoney(item.limit)}`,
|
||||
`实际: ¥${formatMoney(item.current)}`,
|
||||
@@ -1038,9 +1055,24 @@ const yearBurndownChartOptions = computed(() => {
|
||||
font-weight: 600;
|
||||
color: var(--van-text-color);
|
||||
margin-bottom: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chart-title-text {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
margin: -4px;
|
||||
}
|
||||
|
||||
.chart-subtitle {
|
||||
|
||||
@@ -121,6 +121,7 @@ const formattedTitle = computed(() => {
|
||||
background: transparent !important;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-height: 60px; /* 与 balance-header 保持一致,防止切换抖动 */
|
||||
}
|
||||
|
||||
.header-content {
|
||||
|
||||
180
Web/src/components/PopupContainerV2.vue
Normal file
180
Web/src/components/PopupContainerV2.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<!--
|
||||
PopupContainer V2 - 通用底部弹窗组件(采用 TransactionDetailSheet 样式风格)
|
||||
|
||||
## 与 V1 的区别
|
||||
- V1 (PopupContainer.vue): 使用 Vant 主题变量,标准化布局,默认高度 80%
|
||||
- V2 (PopupContainerV2.vue): 使用 Inter 字体,16px 圆角,纯白背景,更现代化的视觉风格
|
||||
|
||||
## 基础用法
|
||||
<PopupContainerV2 v-model:show="show" title="标题">
|
||||
<div class="content">内容区域</div>
|
||||
<template #footer>
|
||||
<van-button type="primary">确定</van-button>
|
||||
</template>
|
||||
</PopupContainerV2>
|
||||
|
||||
## Props
|
||||
- modelValue (Boolean, required): 控制弹窗显示/隐藏
|
||||
- title (String, required): 标题文本
|
||||
- height (String, default: 'auto'): 弹窗高度,支持 'auto', '80%', '500px' 等
|
||||
- maxHeight (String, default: '85%'): 最大高度
|
||||
|
||||
## Slots
|
||||
- default: 可滚动的内容区域(不提供默认 padding,由使用方控制)
|
||||
- footer: 固定底部区域(操作按钮等)
|
||||
|
||||
## Events
|
||||
- update:modelValue: 弹窗显示/隐藏状态变更
|
||||
-->
|
||||
<template>
|
||||
<van-popup
|
||||
v-model:show="visible"
|
||||
position="bottom"
|
||||
:style="{
|
||||
height: height === 'auto' ? maxHeight : height,
|
||||
borderTopLeftRadius: '16px',
|
||||
borderTopRightRadius: '16px'
|
||||
}"
|
||||
teleport="body"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="popup-container-v2">
|
||||
<!-- 固定头部 -->
|
||||
<div class="popup-header">
|
||||
<h3 class="popup-title">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<van-icon
|
||||
name="cross"
|
||||
class="popup-close"
|
||||
@click="handleClose"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 可滚动内容区域 -->
|
||||
<div class="popup-content">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- 固定底部 -->
|
||||
<div
|
||||
v-if="hasFooter"
|
||||
class="popup-footer"
|
||||
>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, useSlots } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: 'auto'
|
||||
},
|
||||
maxHeight: {
|
||||
type: String,
|
||||
default: '85%'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
// 双向绑定
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
// 判断是否有 footer 插槽
|
||||
const hasFooter = computed(() => !!slots.footer)
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.popup-container-v2 {
|
||||
background: #ffffff;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// 固定头部
|
||||
.popup-header {
|
||||
flex-shrink: 0;
|
||||
padding: 24px;
|
||||
padding-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.popup-title {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #09090b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
font-size: 24px;
|
||||
color: #71717a;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 可滚动内容区域
|
||||
.popup-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
// 不提供默认 padding,由使用方控制
|
||||
}
|
||||
|
||||
// 固定底部
|
||||
.popup-footer {
|
||||
flex-shrink: 0;
|
||||
padding: 24px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
// 暗色模式
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.popup-container-v2 {
|
||||
background: #18181b;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
.popup-title {
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
color: #a1a1aa;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,152 +1,134 @@
|
||||
<template>
|
||||
<van-popup
|
||||
<PopupContainerV2
|
||||
v-model:show="visible"
|
||||
position="bottom"
|
||||
:style="{
|
||||
height: 'auto',
|
||||
maxHeight: '85%',
|
||||
borderTopLeftRadius: '16px',
|
||||
borderTopRightRadius: '16px'
|
||||
}"
|
||||
teleport="body"
|
||||
@close="handleClose"
|
||||
title="交易详情"
|
||||
height="85%"
|
||||
>
|
||||
<div class="transaction-detail-sheet">
|
||||
<!-- 头部 -->
|
||||
<div class="sheet-header">
|
||||
<div class="header-title">
|
||||
交易详情
|
||||
</div>
|
||||
<van-icon
|
||||
name="cross"
|
||||
class="header-close"
|
||||
@click="handleClose"
|
||||
/>
|
||||
<!-- 金额区域 -->
|
||||
<div class="amount-section">
|
||||
<div class="amount-label">
|
||||
金额
|
||||
</div>
|
||||
|
||||
<!-- 金额区域 -->
|
||||
<div class="amount-section">
|
||||
<div class="amount-label">
|
||||
金额
|
||||
</div>
|
||||
<!-- 只读显示模式 -->
|
||||
<div
|
||||
v-if="!isEditingAmount"
|
||||
class="amount-value"
|
||||
@click="startEditAmount"
|
||||
>
|
||||
¥ {{ formatAmount(parseFloat(editForm.amount) || 0) }}
|
||||
</div>
|
||||
<!-- 编辑模式 -->
|
||||
<div
|
||||
v-else
|
||||
class="amount-input-wrapper"
|
||||
>
|
||||
<span class="currency-symbol">¥</span>
|
||||
<input
|
||||
ref="amountInputRef"
|
||||
v-model="editForm.amount"
|
||||
type="number"
|
||||
inputmode="decimal"
|
||||
class="amount-input"
|
||||
placeholder="0.00"
|
||||
step="0.01"
|
||||
min="0"
|
||||
@blur="finishEditAmount"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表单字段 -->
|
||||
<div class="form-section">
|
||||
<div class="form-row">
|
||||
<div class="form-label">
|
||||
时间
|
||||
</div>
|
||||
<div
|
||||
class="form-value clickable"
|
||||
@click="showDatePicker = true"
|
||||
>
|
||||
{{ formatDateTime(editForm.occurredAt) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row no-border">
|
||||
<div class="form-label">
|
||||
备注
|
||||
</div>
|
||||
<div class="form-value">
|
||||
<input
|
||||
v-model="editForm.reason"
|
||||
type="text"
|
||||
class="reason-input"
|
||||
placeholder="请输入备注"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-label">
|
||||
类型
|
||||
</div>
|
||||
<div class="form-value">
|
||||
<van-radio-group
|
||||
v-model="editForm.type"
|
||||
direction="horizontal"
|
||||
@change="handleTypeChange"
|
||||
>
|
||||
<van-radio
|
||||
:name="0"
|
||||
class="type-radio"
|
||||
>
|
||||
支出
|
||||
</van-radio>
|
||||
<van-radio
|
||||
:name="1"
|
||||
class="type-radio"
|
||||
>
|
||||
收入
|
||||
</van-radio>
|
||||
<van-radio
|
||||
:name="2"
|
||||
class="type-radio"
|
||||
>
|
||||
不计
|
||||
</van-radio>
|
||||
</van-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-label">
|
||||
分类
|
||||
</div>
|
||||
<div
|
||||
class="form-value clickable"
|
||||
@click="showClassifySelector = !showClassifySelector"
|
||||
>
|
||||
<span v-if="editForm.classify">{{ editForm.classify }}</span>
|
||||
<span
|
||||
v-else
|
||||
class="placeholder"
|
||||
>请选择分类</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分类选择器(展开/收起) -->
|
||||
<!-- 只读显示模式 -->
|
||||
<div
|
||||
v-if="showClassifySelector"
|
||||
class="classify-section"
|
||||
v-if="!isEditingAmount"
|
||||
class="amount-value"
|
||||
@click="startEditAmount"
|
||||
>
|
||||
<ClassifySelector
|
||||
v-model="editForm.classify"
|
||||
:type="editForm.type"
|
||||
:show-add="false"
|
||||
:show-clear="false"
|
||||
:show-all="false"
|
||||
@change="handleClassifyChange"
|
||||
/>
|
||||
¥ {{ formatAmount(parseFloat(editForm.amount) || 0) }}
|
||||
</div>
|
||||
<!-- 编辑模式 -->
|
||||
<div
|
||||
v-else
|
||||
class="amount-input-wrapper"
|
||||
>
|
||||
<span class="currency-symbol">¥</span>
|
||||
<input
|
||||
ref="amountInputRef"
|
||||
v-model="editForm.amount"
|
||||
type="number"
|
||||
inputmode="decimal"
|
||||
class="amount-input"
|
||||
placeholder="0.00"
|
||||
step="0.01"
|
||||
min="0"
|
||||
@blur="finishEditAmount"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表单字段 -->
|
||||
<div class="form-section">
|
||||
<div class="form-row">
|
||||
<div class="form-label">
|
||||
时间
|
||||
</div>
|
||||
<div
|
||||
class="form-value clickable"
|
||||
@click="showDatePicker = true"
|
||||
>
|
||||
{{ formatDateTime(editForm.occurredAt) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="form-row no-border">
|
||||
<div class="form-label">
|
||||
备注
|
||||
</div>
|
||||
<div class="form-value">
|
||||
<input
|
||||
v-model="editForm.reason"
|
||||
type="text"
|
||||
class="reason-input"
|
||||
placeholder="请输入备注"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-label">
|
||||
类型
|
||||
</div>
|
||||
<div class="form-value">
|
||||
<van-radio-group
|
||||
v-model="editForm.type"
|
||||
direction="horizontal"
|
||||
@change="handleTypeChange"
|
||||
>
|
||||
<van-radio
|
||||
:name="0"
|
||||
class="type-radio"
|
||||
>
|
||||
支出
|
||||
</van-radio>
|
||||
<van-radio
|
||||
:name="1"
|
||||
class="type-radio"
|
||||
>
|
||||
收入
|
||||
</van-radio>
|
||||
<van-radio
|
||||
:name="2"
|
||||
class="type-radio"
|
||||
>
|
||||
不计
|
||||
</van-radio>
|
||||
</van-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-label">
|
||||
分类
|
||||
</div>
|
||||
<div
|
||||
class="form-value clickable"
|
||||
@click="showClassifySelector = !showClassifySelector"
|
||||
>
|
||||
<span v-if="editForm.classify">{{ editForm.classify }}</span>
|
||||
<span
|
||||
v-else
|
||||
class="placeholder"
|
||||
>请选择分类</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类选择器(展开/收起) -->
|
||||
<div
|
||||
v-if="showClassifySelector"
|
||||
class="classify-section"
|
||||
>
|
||||
<ClassifySelector
|
||||
v-model="editForm.classify"
|
||||
:type="editForm.type"
|
||||
:show-add="false"
|
||||
:show-clear="false"
|
||||
:show-all="false"
|
||||
@change="handleClassifyChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮(固定底部) -->
|
||||
<template #footer>
|
||||
<div class="actions-section">
|
||||
<van-button
|
||||
class="delete-btn"
|
||||
@@ -164,31 +146,32 @@
|
||||
保存
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</PopupContainerV2>
|
||||
|
||||
<!-- 日期时间选择器 -->
|
||||
<van-popup
|
||||
v-model:show="showDatePicker"
|
||||
position="bottom"
|
||||
round
|
||||
>
|
||||
<van-datetime-picker
|
||||
v-model="currentDateTime"
|
||||
type="datetime"
|
||||
title="选择日期时间"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
@confirm="handleDateTimeConfirm"
|
||||
@cancel="showDatePicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
<!-- 日期时间选择器 -->
|
||||
<van-popup
|
||||
v-model:show="showDatePicker"
|
||||
position="bottom"
|
||||
round
|
||||
>
|
||||
<van-datetime-picker
|
||||
v-model="currentDateTime"
|
||||
type="datetime"
|
||||
title="选择日期时间"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
@confirm="handleDateTimeConfirm"
|
||||
@cancel="showDatePicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch, computed } from 'vue'
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { showToast, showDialog } from 'vant'
|
||||
import dayjs from 'dayjs'
|
||||
import PopupContainerV2 from '@/components/PopupContainerV2.vue'
|
||||
import ClassifySelector from '@/components/ClassifySelector.vue'
|
||||
import { updateTransaction, deleteTransaction } from '@/api/transactionRecord'
|
||||
|
||||
@@ -399,291 +382,249 @@ const handleDelete = async () => {
|
||||
// 用户取消删除
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.transaction-detail-sheet {
|
||||
background: #ffffff;
|
||||
padding: 24px;
|
||||
// 金额区域
|
||||
.amount-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 24px 24px;
|
||||
|
||||
.sheet-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.amount-label {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #09090b;
|
||||
}
|
||||
.amount-value {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #09090b;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
.header-close {
|
||||
font-size: 24px;
|
||||
color: #71717a;
|
||||
cursor: pointer;
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.amount-section {
|
||||
.amount-input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 16px 0;
|
||||
|
||||
.amount-label {
|
||||
.currency-symbol {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #09090b;
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
max-width: 200px;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #09090b;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 2px solid #e4e4e7;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
&:focus {
|
||||
border-bottom-color: #6366f1;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #a1a1aa;
|
||||
}
|
||||
|
||||
// 移除 number 类型的上下箭头
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Firefox
|
||||
&[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单区域
|
||||
.form-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 0 24px 16px;
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
border-bottom: 1px solid #e4e4e7;
|
||||
|
||||
&.no-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
color: #71717a;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
.form-value {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
color: #09090b;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: opacity 0.2s;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
.amount-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.currency-symbol {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #09090b;
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
max-width: 200px;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #09090b;
|
||||
.placeholder {
|
||||
color: #a1a1aa;
|
||||
}
|
||||
|
||||
.reason-input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: right;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 16px;
|
||||
color: #09090b;
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 2px solid #e4e4e7;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
&:focus {
|
||||
border-bottom-color: #6366f1;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #a1a1aa;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除 number 类型的上下箭头
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
:deep(.van-radio-group) {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
// Firefox
|
||||
&[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
:deep(.van-radio) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.van-radio__label) {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分类选择器
|
||||
.classify-section {
|
||||
padding: 16px 24px;
|
||||
background: #f4f4f5;
|
||||
border-radius: 8px;
|
||||
margin: 0 24px 16px;
|
||||
}
|
||||
|
||||
// 操作按钮
|
||||
.actions-section {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.delete-btn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ef4444;
|
||||
background: transparent;
|
||||
color: #ef4444;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
background: #6366f1;
|
||||
color: #fafafa;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 暗色模式
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.amount-section {
|
||||
.amount-label {
|
||||
color: #a1a1aa;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.amount-input-wrapper {
|
||||
.currency-symbol {
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
color: #fafafa;
|
||||
border-bottom-color: #27272a;
|
||||
|
||||
&:focus {
|
||||
border-bottom-color: #6366f1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
border-bottom: 1px solid #e4e4e7;
|
||||
|
||||
&.no-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
border-bottom-color: #27272a;
|
||||
|
||||
.form-label {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
color: #71717a;
|
||||
color: #a1a1aa;
|
||||
}
|
||||
|
||||
.form-value {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
color: #09090b;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #a1a1aa;
|
||||
}
|
||||
color: #fafafa;
|
||||
|
||||
.reason-input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: right;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 16px;
|
||||
color: #09090b;
|
||||
background: transparent;
|
||||
|
||||
&::placeholder {
|
||||
color: #a1a1aa;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.van-radio-group) {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
:deep(.van-radio) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.van-radio__label) {
|
||||
margin-left: 4px;
|
||||
color: #fafafa;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.classify-section {
|
||||
padding: 16px;
|
||||
background: #f4f4f5;
|
||||
border-radius: 8px;
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.actions-section {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
|
||||
.delete-btn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ef4444;
|
||||
background: transparent;
|
||||
color: #ef4444;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
background: #6366f1;
|
||||
color: #fafafa;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暗色模式
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.transaction-detail-sheet {
|
||||
background: #18181b;
|
||||
|
||||
.sheet-header {
|
||||
.header-title {
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.header-close {
|
||||
color: #a1a1aa;
|
||||
}
|
||||
}
|
||||
|
||||
.amount-section {
|
||||
.amount-label {
|
||||
color: #a1a1aa;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.amount-input-wrapper {
|
||||
.currency-symbol {
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
color: #fafafa;
|
||||
border-bottom-color: #27272a;
|
||||
|
||||
&:focus {
|
||||
border-bottom-color: #6366f1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-section {
|
||||
.form-row {
|
||||
border-bottom-color: #27272a;
|
||||
|
||||
.form-label {
|
||||
color: #a1a1aa;
|
||||
}
|
||||
|
||||
.form-value {
|
||||
color: #fafafa;
|
||||
|
||||
.reason-input {
|
||||
color: #fafafa;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.classify-section {
|
||||
background: #27272a;
|
||||
}
|
||||
background: #27272a;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -110,6 +110,7 @@ const messageViewRef = ref(null)
|
||||
background: transparent;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-height: 60px; /* 与 calendar-header 保持一致,防止切换抖动 */
|
||||
}
|
||||
|
||||
.header-title {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<template>
|
||||
<div class="page-container-flex">
|
||||
<van-nav-bar
|
||||
title="设置"
|
||||
placeholder
|
||||
/>
|
||||
<!-- 自定义头部 -->
|
||||
<header class="setting-header">
|
||||
<h1 class="header-title">
|
||||
设置
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div class="scroll-content">
|
||||
<div
|
||||
class="detail-header"
|
||||
@@ -384,12 +387,30 @@ const handleScheduledTasks = () => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页面背景色 */
|
||||
:deep(body) {
|
||||
background-color: var(--van-background);
|
||||
<style scoped lang="scss">
|
||||
@import '@/assets/theme.css';
|
||||
|
||||
/* ========== 自定义头部 ========== */
|
||||
.setting-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 24px;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-height: 60px; /* 与其他 header 保持一致,防止切换抖动 */
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-family: var(--font-primary);
|
||||
font-size: var(--font-2xl);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ========== 页面内容 ========== */
|
||||
/* 增加卡片对比度 */
|
||||
:deep(.van-cell-group--inset) {
|
||||
background-color: var(--van-background-2);
|
||||
@@ -407,9 +428,4 @@ const handleScheduledTasks = () => {
|
||||
color: var(--van-text-color-2);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* 设置页面容器背景色 */
|
||||
:deep(.van-nav-bar) {
|
||||
background: transparent !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user