feat: 优化预算管理界面,增强预算编辑功能,添加预算删除接口
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
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 24s
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,75 +1,88 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="common-card budget-card" @click="$emit('click')">
|
<div class="common-card budget-card" @click="$emit('click')">
|
||||||
<div class="card-header">
|
<div class="budget-content-wrapper">
|
||||||
<div class="budget-info">
|
<Transition :name="transitionName">
|
||||||
<h3 class="card-title">{{ budget.name }}</h3>
|
<div :key="budget.period" class="budget-inner-card">
|
||||||
<slot name="tag">
|
<div class="card-header">
|
||||||
<van-tag v-if="budget.isStopped" type="danger" size="small" plain>已停止</van-tag>
|
<div class="budget-info">
|
||||||
<van-tag v-else :type="statusTagType" size="small" plain>{{ statusTagText }}</van-tag>
|
<h3 class="card-title">{{ budget.name }}</h3>
|
||||||
</slot>
|
<slot name="tag">
|
||||||
</div>
|
<van-tag v-if="budget.isStopped" type="danger" size="small" plain>已停止</van-tag>
|
||||||
<div class="header-actions">
|
<van-tag v-else :type="statusTagType" size="small" plain>{{ statusTagText }}</van-tag>
|
||||||
<slot name="actions">
|
</slot>
|
||||||
<van-button
|
</div>
|
||||||
:icon="budget.isStopped ? 'play' : 'pause'"
|
<div class="header-actions">
|
||||||
size="mini"
|
<slot name="actions">
|
||||||
plain
|
<van-button
|
||||||
round
|
:icon="budget.isStopped ? 'play' : 'pause'"
|
||||||
@click.stop="$emit('toggle-stop', budget)"
|
size="mini"
|
||||||
/>
|
plain
|
||||||
</slot>
|
round
|
||||||
</div>
|
@click.stop="$emit('toggle-stop', budget)"
|
||||||
</div>
|
/>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="budget-body">
|
<div class="budget-body">
|
||||||
<div class="amount-info">
|
<div class="amount-info">
|
||||||
<slot name="amount-info"></slot>
|
<slot name="amount-info"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="progress-section">
|
<div class="progress-section">
|
||||||
<div class="progress-info">
|
<slot name="progress-info">
|
||||||
<slot name="progress-info">
|
<span class="period-type">{{ periodLabel }}{{ budget.category === 0 ? '使用' : '达成' }}</span>
|
||||||
<span class="period-type">{{ periodLabel }}进度</span>
|
<van-progress
|
||||||
<span class="percent" :class="percentClass">
|
:percentage="Math.min(percentage, 100)"
|
||||||
{{ percentage }}%
|
stroke-width="8"
|
||||||
</span>
|
:color="progressColor"
|
||||||
</slot>
|
: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>
|
</div>
|
||||||
<van-progress
|
</Transition>
|
||||||
:percentage="Math.min(percentage, 100)"
|
|
||||||
stroke-width="8"
|
|
||||||
:color="progressColor"
|
|
||||||
:show-pivot="false"
|
|
||||||
/>
|
|
||||||
</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="$emit('switch-period', -1)"
|
|
||||||
/>
|
|
||||||
<span class="period-text">{{ budget.period }}</span>
|
|
||||||
<van-button
|
|
||||||
icon="arrow"
|
|
||||||
class="nav-icon"
|
|
||||||
plain
|
|
||||||
size="small"
|
|
||||||
style="width: 50px;"
|
|
||||||
@click="$emit('switch-period', 1)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import { BudgetPeriodType } from '@/constants/enums'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
budget: {
|
budget: {
|
||||||
@@ -98,12 +111,31 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
defineEmits(['toggle-stop', 'switch-period', 'click'])
|
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(() => {
|
const percentage = computed(() => {
|
||||||
if (!props.budget.limit) return 0
|
if (!props.budget.limit) return 0
|
||||||
return Math.round((props.budget.current / props.budget.limit) * 100)
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -112,6 +144,51 @@ const percentage = computed(() => {
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
padding-bottom: 8px;
|
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 {
|
.budget-info {
|
||||||
@@ -134,14 +211,14 @@ const percentage = computed(() => {
|
|||||||
.amount-info {
|
.amount-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 16px 0;
|
margin: 12px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.info-item) .label {
|
:deep(.info-item) .label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #969799;
|
color: #969799;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.info-item) .value {
|
:deep(.info-item) .value {
|
||||||
@@ -158,15 +235,27 @@ const percentage = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.progress-section {
|
.progress-section {
|
||||||
margin-bottom: 16px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #646566;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-info {
|
.progress-section :deep(.van-progress) {
|
||||||
display: flex;
|
flex: 1;
|
||||||
justify-content: space-between;
|
}
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 6px;
|
.period-type {
|
||||||
color: #646566;
|
white-space: nowrap;
|
||||||
|
width: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.percent {
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 35px;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.percent.warning {
|
.percent.warning {
|
||||||
@@ -179,6 +268,16 @@ const percentage = computed(() => {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time-progress {
|
||||||
|
margin-top: -8px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-progress .period-type,
|
||||||
|
.time-progress .percent {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.card-footer {
|
.card-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<PopupContainer
|
<PopupContainer
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
:title="isEdit ? '编辑预算' : '新增预算'"
|
:title="isEdit ? `编辑${getCategoryName(form.category)}预算` : `新增${getCategoryName(form.category)}预算`"
|
||||||
height="70%"
|
height="85%"
|
||||||
>
|
>
|
||||||
<div class="add-budget-form">
|
<div class="add-budget-form">
|
||||||
<van-form>
|
<van-form>
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
<van-radio :name="BudgetPeriodType.Week">周</van-radio>
|
<van-radio :name="BudgetPeriodType.Week">周</van-radio>
|
||||||
<van-radio :name="BudgetPeriodType.Month">月</van-radio>
|
<van-radio :name="BudgetPeriodType.Month">月</van-radio>
|
||||||
<van-radio :name="BudgetPeriodType.Year">年</van-radio>
|
<van-radio :name="BudgetPeriodType.Year">年</van-radio>
|
||||||
<van-radio :name="BudgetPeriodType.Longterm">长期</van-radio>
|
|
||||||
</van-radio-group>
|
</van-radio-group>
|
||||||
</template>
|
</template>
|
||||||
</van-field>
|
</van-field>
|
||||||
@@ -36,15 +35,6 @@
|
|||||||
<span>元</span>
|
<span>元</span>
|
||||||
</template>
|
</template>
|
||||||
</van-field>
|
</van-field>
|
||||||
<van-field name="category" label="类型">
|
|
||||||
<template #input>
|
|
||||||
<van-radio-group v-model="form.category" direction="horizontal" :disabled="isEdit">
|
|
||||||
<van-radio :name="BudgetCategory.Expense">支出</van-radio>
|
|
||||||
<van-radio :name="BudgetCategory.Income">收入</van-radio>
|
|
||||||
<van-radio :name="BudgetCategory.Savings">存款</van-radio>
|
|
||||||
</van-radio-group>
|
|
||||||
</template>
|
|
||||||
</van-field>
|
|
||||||
<van-field label="相关分类">
|
<van-field label="相关分类">
|
||||||
<template #input>
|
<template #input>
|
||||||
<div v-if="form.selectedCategories.length === 0" style="color: #c8c9cc;">可多选分类</div>
|
<div v-if="form.selectedCategories.length === 0" style="color: #c8c9cc;">可多选分类</div>
|
||||||
@@ -95,17 +85,10 @@ import { createBudget, updateBudget } from '@/api/budget'
|
|||||||
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
|
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
|
||||||
import PopupContainer from '@/components/PopupContainer.vue'
|
import PopupContainer from '@/components/PopupContainer.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
editData: {
|
|
||||||
type: Object,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['success'])
|
const emit = defineEmits(['success'])
|
||||||
|
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
const isEdit = computed(() => !!props.editData)
|
const isEdit = ref(false)
|
||||||
|
|
||||||
const categories = ref([])
|
const categories = ref([])
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
@@ -117,13 +100,23 @@ const form = reactive({
|
|||||||
selectedCategories: []
|
selectedCategories: []
|
||||||
})
|
})
|
||||||
|
|
||||||
const open = (data = null) => {
|
const open = ({
|
||||||
|
data,
|
||||||
|
isEditFlag,
|
||||||
|
category
|
||||||
|
}) => {
|
||||||
|
if(category === undefined) {
|
||||||
|
showToast('缺少必要参数:category')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isEdit.value = isEditFlag
|
||||||
if (data) {
|
if (data) {
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
category: data.category,
|
category: category,
|
||||||
limit: data.limit,
|
limit: data.limit,
|
||||||
selectedCategories: data.selectedCategories ? [...data.selectedCategories] : []
|
selectedCategories: data.selectedCategories ? [...data.selectedCategories] : []
|
||||||
})
|
})
|
||||||
@@ -132,7 +125,7 @@ const open = (data = null) => {
|
|||||||
id: undefined,
|
id: undefined,
|
||||||
name: '',
|
name: '',
|
||||||
type: BudgetPeriodType.Month,
|
type: BudgetPeriodType.Month,
|
||||||
category: BudgetCategory.Expense,
|
category: category,
|
||||||
limit: '',
|
limit: '',
|
||||||
selectedCategories: []
|
selectedCategories: []
|
||||||
})
|
})
|
||||||
@@ -199,18 +192,30 @@ const onSubmit = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const res = form.id ? await updateBudget(data) : await createBudget(data)
|
const res = form.id ? await updateBudget(data) : await createBudget(data)
|
||||||
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
showToast('保存成功')
|
showToast('保存成功')
|
||||||
visible.value = false
|
visible.value = false
|
||||||
emit('success')
|
emit('success')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast('保存失败')
|
showToast(err.message || '保存失败')
|
||||||
console.error('保存预算失败', err)
|
console.error('保存预算失败', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getCategoryName = (category) => {
|
||||||
|
switch(category) {
|
||||||
|
case BudgetCategory.Expense:
|
||||||
|
return '支出'
|
||||||
|
case BudgetCategory.Income:
|
||||||
|
return '收入'
|
||||||
|
case BudgetCategory.Savings:
|
||||||
|
return '存款'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchCategories()
|
fetchCategories()
|
||||||
})
|
})
|
||||||
@@ -243,7 +248,6 @@ onMounted(() => {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="page-container-flex">
|
<div class="page-container-flex">
|
||||||
<van-nav-bar title="预算管理" placeholder>
|
<van-nav-bar title="预算管理" placeholder>
|
||||||
<template #right>
|
<template #right>
|
||||||
<van-icon name="plus" size="20" @click="budgetEditRef.open()" />
|
<van-icon name="plus" size="20" @click="budgetEditRef.open({ category: activeTab })" />
|
||||||
</template>
|
</template>
|
||||||
</van-nav-bar>
|
</van-nav-bar>
|
||||||
|
|
||||||
@@ -25,7 +25,11 @@
|
|||||||
:period-label="getPeriodLabel(budget.type)"
|
:period-label="getPeriodLabel(budget.type)"
|
||||||
@toggle-stop="handleToggleStop"
|
@toggle-stop="handleToggleStop"
|
||||||
@switch-period="(dir) => handleSwitchPeriod(budget, dir)"
|
@switch-period="(dir) => handleSwitchPeriod(budget, dir)"
|
||||||
@click="budgetEditRef.open(budget)"
|
@click="budgetEditRef.open({
|
||||||
|
data: budget,
|
||||||
|
isEditFlag: true,
|
||||||
|
category: budget.category
|
||||||
|
})"
|
||||||
>
|
>
|
||||||
<template #amount-info>
|
<template #amount-info>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
@@ -88,12 +92,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #progress-info>
|
|
||||||
<span class="period-type">{{ getPeriodLabel(budget.type) }}达成度</span>
|
|
||||||
<span class="percent" :class="{ 'income': (budget.current / budget.limit) >= 1 }">
|
|
||||||
{{ Math.round((budget.current / budget.limit) * 100) }}%
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</BudgetCard>
|
</BudgetCard>
|
||||||
<template #right>
|
<template #right>
|
||||||
<van-button square text="删除" type="danger" class="delete-button" @click="handleDelete(budget)" />
|
<van-button square text="删除" type="danger" class="delete-button" @click="handleDelete(budget)" />
|
||||||
@@ -117,6 +115,7 @@
|
|||||||
:budget="budget"
|
:budget="budget"
|
||||||
progress-color="#07c160"
|
progress-color="#07c160"
|
||||||
:percent-class="{ 'income': (budget.current / budget.limit) >= 1 }"
|
:percent-class="{ 'income': (budget.current / budget.limit) >= 1 }"
|
||||||
|
:period-label="getPeriodLabel(budget.type)"
|
||||||
status-tag-text="积累中"
|
status-tag-text="积累中"
|
||||||
@toggle-stop="handleToggleStop"
|
@toggle-stop="handleToggleStop"
|
||||||
@switch-period="(dir) => handleSwitchPeriod(budget, dir)"
|
@switch-period="(dir) => handleSwitchPeriod(budget, dir)"
|
||||||
@@ -138,12 +137,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #progress-info>
|
|
||||||
<span class="period-type">储蓄进度</span>
|
|
||||||
<span class="percent" :class="{ 'income': (budget.current / budget.limit) >= 1 }">
|
|
||||||
{{ Math.round((budget.current / budget.limit) * 100) }}%
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</BudgetCard>
|
</BudgetCard>
|
||||||
<template #right>
|
<template #right>
|
||||||
<van-button square text="删除" type="danger" class="delete-button" @click="handleDelete(budget)" />
|
<van-button square text="删除" type="danger" class="delete-button" @click="handleDelete(budget)" />
|
||||||
@@ -168,7 +161,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { showToast, showConfirmDialog } from 'vant'
|
import { showToast, showConfirmDialog } from 'vant'
|
||||||
import { getBudgetList, deleteBudget, toggleStopBudget, getBudgetStatistics } from '@/api/budget'
|
import { getBudgetList, deleteBudget, toggleStopBudget, getBudgetStatistics } from '@/api/budget'
|
||||||
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
|
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
|
||||||
|
|||||||
@@ -53,6 +53,24 @@ public class BudgetController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除预算
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<BaseResponse> DeleteByIdAsync(long id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var success = await budgetService.DeleteAsync(id);
|
||||||
|
return success ? BaseResponse.Done() : "删除预算失败".Fail();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "删除预算失败, Id: {Id}", id);
|
||||||
|
return $"删除预算失败: {ex.Message}".Fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建预算
|
/// 创建预算
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -71,6 +89,12 @@ public class BudgetController(
|
|||||||
StartDate = dto.StartDate ?? DateTime.Now
|
StartDate = dto.StartDate ?? DateTime.Now
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var varidationError = await ValidateBudgetSelectedCategoriesAsync(budget);
|
||||||
|
if (!string.IsNullOrEmpty(varidationError))
|
||||||
|
{
|
||||||
|
return varidationError.Fail<long>();
|
||||||
|
}
|
||||||
|
|
||||||
var success = await budgetService.AddAsync(budget);
|
var success = await budgetService.AddAsync(budget);
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
@@ -85,24 +109,6 @@ public class BudgetController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 删除预算
|
|
||||||
/// </summary>
|
|
||||||
[HttpDelete("{id}")]
|
|
||||||
public async Task<BaseResponse> DeleteByIdAsync(long id)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var success = await budgetService.DeleteAsync(id);
|
|
||||||
return success ? BaseResponse.Done() : "删除预算失败".Fail();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "删除预算失败, Id: {Id}", id);
|
|
||||||
return $"删除预算失败: {ex.Message}".Fail();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 更新预算
|
/// 更新预算
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -125,6 +131,12 @@ public class BudgetController(
|
|||||||
budget.StartDate = dto.StartDate.Value;
|
budget.StartDate = dto.StartDate.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var varidationError = await ValidateBudgetSelectedCategoriesAsync(budget);
|
||||||
|
if (!string.IsNullOrEmpty(varidationError))
|
||||||
|
{
|
||||||
|
return varidationError.Fail();
|
||||||
|
}
|
||||||
|
|
||||||
var success = await budgetService.UpdateAsync(budget);
|
var success = await budgetService.UpdateAsync(budget);
|
||||||
return success ? BaseResponse.Done() : "更新预算失败".Fail();
|
return success ? BaseResponse.Done() : "更新预算失败".Fail();
|
||||||
}
|
}
|
||||||
@@ -135,6 +147,27 @@ public class BudgetController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<string> ValidateBudgetSelectedCategoriesAsync(BudgetRecord record)
|
||||||
|
{
|
||||||
|
var allBudgets = await budgetService.GetAllAsync();
|
||||||
|
|
||||||
|
var recordSelectedCategories = record.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
foreach (var budget in allBudgets)
|
||||||
|
{
|
||||||
|
var selectedCategories = budget.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (budget.Id != record.Id)
|
||||||
|
{
|
||||||
|
if (budget.Category == record.Category &&
|
||||||
|
recordSelectedCategories.Intersect(selectedCategories).Any())
|
||||||
|
{
|
||||||
|
return $"和 {budget.Name} 存在分类冲突,请调整相关分类。";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 切换预算暂停状态
|
/// 切换预算暂停状态
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ public class BudgetDto
|
|||||||
public bool IsStopped { get; set; }
|
public bool IsStopped { get; set; }
|
||||||
public string StartDate { get; set; } = string.Empty;
|
public string StartDate { get; set; } = string.Empty;
|
||||||
public string Period { get; set; } = string.Empty;
|
public string Period { get; set; } = string.Empty;
|
||||||
|
public DateTime? PeriodStart { get; set; }
|
||||||
|
public DateTime? PeriodEnd { get; set; }
|
||||||
|
|
||||||
public static BudgetDto FromEntity(BudgetRecord entity, decimal currentAmount = 0, DateTime? referenceDate = null)
|
public static BudgetDto FromEntity(BudgetRecord entity, decimal currentAmount = 0, DateTime? referenceDate = null)
|
||||||
{
|
{
|
||||||
@@ -38,7 +40,9 @@ public class BudgetDto
|
|||||||
BudgetPeriodType.Month => $"{start:yy}年第{start.Month}月",
|
BudgetPeriodType.Month => $"{start:yy}年第{start.Month}月",
|
||||||
BudgetPeriodType.Week => $"{start:yy}年第{System.Globalization.CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(start, System.Globalization.CalendarWeekRule.FirstDay, DayOfWeek.Monday)}周",
|
BudgetPeriodType.Week => $"{start:yy}年第{System.Globalization.CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(start, System.Globalization.CalendarWeekRule.FirstDay, DayOfWeek.Monday)}周",
|
||||||
_ => $"{start:yyyy-MM-dd} ~ {end:yyyy-MM-dd}"
|
_ => $"{start:yyyy-MM-dd} ~ {end:yyyy-MM-dd}"
|
||||||
}
|
},
|
||||||
|
PeriodStart = start,
|
||||||
|
PeriodEnd = end
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user