All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 23s
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
272 lines
6.8 KiB
Vue
272 lines
6.8 KiB
Vue
<template>
|
||
<PopupContainer
|
||
v-model="visible"
|
||
:title="isEdit ? `编辑${getCategoryName(form.category)}预算` : `新增${getCategoryName(form.category)}预算`"
|
||
height="85%"
|
||
>
|
||
<div class="add-budget-form">
|
||
<van-form>
|
||
<van-cell-group inset>
|
||
<van-field
|
||
v-model="form.name"
|
||
name="name"
|
||
label="预算名称"
|
||
placeholder="例如:每月餐饮、年度奖金"
|
||
:rules="[{ required: true, message: '请填写预算名称' }]"
|
||
/>
|
||
<van-field name="type" label="统计周期">
|
||
<template #input>
|
||
<van-radio-group v-model="form.type" direction="horizontal">
|
||
<van-radio :name="BudgetPeriodType.Week">周</van-radio>
|
||
<van-radio :name="BudgetPeriodType.Month">月</van-radio>
|
||
<van-radio :name="BudgetPeriodType.Year">年</van-radio>
|
||
</van-radio-group>
|
||
</template>
|
||
</van-field>
|
||
<van-field
|
||
v-model="form.limit"
|
||
type="number"
|
||
name="limit"
|
||
label="预算金额"
|
||
placeholder="0.00"
|
||
:rules="[{ required: true, message: '请填写预算金额' }]"
|
||
>
|
||
<template #extra>
|
||
<span>元</span>
|
||
</template>
|
||
</van-field>
|
||
<van-field label="相关分类">
|
||
<template #input>
|
||
<div v-if="form.selectedCategories.length === 0" style="color: #c8c9cc;">可多选分类</div>
|
||
<div v-else class="selected-categories">
|
||
<span class="ellipsis-text">
|
||
{{ form.selectedCategories.join('、') }}
|
||
</span>
|
||
</div>
|
||
</template>
|
||
</van-field>
|
||
<div class="classify-buttons">
|
||
<van-button
|
||
v-if="filteredCategories.length > 0"
|
||
:type="isAllSelected ? 'primary' : 'default'"
|
||
size="small"
|
||
class="classify-btn all-btn"
|
||
@click="toggleAll"
|
||
>
|
||
{{ isAllSelected ? '取消全选' : '全选' }}
|
||
</van-button>
|
||
<van-button
|
||
v-for="item in filteredCategories"
|
||
:key="item.id"
|
||
:type="form.selectedCategories.includes(item.name) ? 'primary' : 'default'"
|
||
size="small"
|
||
class="classify-btn"
|
||
@click="toggleCategory(item.name)"
|
||
>
|
||
{{ item.name }}
|
||
</van-button>
|
||
<div v-if="filteredCategories.length === 0" class="no-data">暂无分类</div>
|
||
</div>
|
||
|
||
</van-cell-group>
|
||
</van-form>
|
||
</div>
|
||
<template #footer>
|
||
<van-button block round type="primary" @click="onSubmit">保存预算</van-button>
|
||
</template>
|
||
</PopupContainer>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||
import { showToast } from 'vant'
|
||
import { getCategoryList } from '@/api/transactionCategory'
|
||
import { createBudget, updateBudget } from '@/api/budget'
|
||
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
|
||
import PopupContainer from '@/components/PopupContainer.vue'
|
||
|
||
const emit = defineEmits(['success'])
|
||
|
||
const visible = ref(false)
|
||
const isEdit = ref(false)
|
||
|
||
const categories = ref([])
|
||
const form = reactive({
|
||
id: undefined,
|
||
name: '',
|
||
type: BudgetPeriodType.Month,
|
||
category: BudgetCategory.Expense,
|
||
limit: '',
|
||
selectedCategories: []
|
||
})
|
||
|
||
const open = ({
|
||
data,
|
||
isEditFlag,
|
||
category
|
||
}) => {
|
||
if(category === undefined) {
|
||
showToast('缺少必要参数:category')
|
||
return
|
||
}
|
||
|
||
isEdit.value = isEditFlag
|
||
if (data) {
|
||
Object.assign(form, {
|
||
id: data.id,
|
||
name: data.name,
|
||
type: data.type,
|
||
category: category,
|
||
limit: data.limit,
|
||
selectedCategories: data.selectedCategories ? [...data.selectedCategories] : []
|
||
})
|
||
} else {
|
||
Object.assign(form, {
|
||
id: undefined,
|
||
name: '',
|
||
type: BudgetPeriodType.Month,
|
||
category: category,
|
||
limit: '',
|
||
selectedCategories: []
|
||
})
|
||
}
|
||
visible.value = true
|
||
}
|
||
|
||
defineExpose({
|
||
open
|
||
})
|
||
|
||
const filteredCategories = computed(() => {
|
||
const targetType = form.category === BudgetCategory.Expense ? 0 : (form.category === BudgetCategory.Income ? 1 : 2)
|
||
return categories.value.filter(c => c.type === targetType)
|
||
})
|
||
|
||
const isAllSelected = computed(() => {
|
||
return filteredCategories.value.length > 0 &&
|
||
filteredCategories.value.every(c => form.selectedCategories.includes(c.name))
|
||
})
|
||
|
||
const toggleCategory = (name) => {
|
||
const index = form.selectedCategories.indexOf(name)
|
||
if (index > -1) {
|
||
form.selectedCategories.splice(index, 1)
|
||
} else {
|
||
form.selectedCategories.push(name)
|
||
}
|
||
}
|
||
|
||
const toggleAll = () => {
|
||
if (isAllSelected.value) {
|
||
form.selectedCategories = []
|
||
} else {
|
||
form.selectedCategories = filteredCategories.value.map(c => c.name)
|
||
}
|
||
}
|
||
|
||
const fetchCategories = async () => {
|
||
try {
|
||
const res = await getCategoryList()
|
||
if (res.success) {
|
||
categories.value = res.data || []
|
||
}
|
||
} catch (err) {
|
||
console.error('获取分类列表失败', err)
|
||
}
|
||
}
|
||
|
||
watch(() => form.category, (newVal, oldVal) => {
|
||
// 只有在手动切换类型且不是初始化编辑数据时才清空
|
||
// 为简单起见,如果旧值存在(即不是第一次赋值),则清空已选分类
|
||
if (oldVal !== undefined) {
|
||
form.selectedCategories = []
|
||
}
|
||
})
|
||
|
||
const onSubmit = async () => {
|
||
try {
|
||
const data = {
|
||
...form,
|
||
limit: parseFloat(form.limit),
|
||
selectedCategories: form.selectedCategories
|
||
}
|
||
|
||
const res = form.id ? await updateBudget(data) : await createBudget(data)
|
||
if (res.success) {
|
||
showToast('保存成功')
|
||
visible.value = false
|
||
emit('success')
|
||
}
|
||
} catch (err) {
|
||
showToast(err.message || '保存失败')
|
||
console.error('保存预算失败', err)
|
||
}
|
||
}
|
||
|
||
const getCategoryName = (category) => {
|
||
switch(category) {
|
||
case BudgetCategory.Expense:
|
||
return '支出'
|
||
case BudgetCategory.Income:
|
||
return '收入'
|
||
case BudgetCategory.Savings:
|
||
return '存款'
|
||
default:
|
||
return ''
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchCategories()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.add-budget-form {
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.selected-categories {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 4px 0;
|
||
width: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.ellipsis-text {
|
||
font-size: 14px;
|
||
color: #323233;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
width: 100%;
|
||
}
|
||
|
||
.classify-buttons {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
padding: 12px 16px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.classify-btn {
|
||
flex: 0 0 auto;
|
||
min-width: 60px;
|
||
border-radius: 16px;
|
||
padding: 0 12px;
|
||
}
|
||
|
||
.all-btn {
|
||
font-weight: bold;
|
||
border-style: dashed;
|
||
}
|
||
|
||
.no-data {
|
||
font-size: 13px;
|
||
color: #969799;
|
||
padding: 8px 0;
|
||
}
|
||
</style>
|