Files
EmailBill/Web/src/components/Bill/BillForm.vue
孙诚 10b02df6e2
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 38s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
添加消息类型枚举和相关字段,优化消息记录服务的添加方法,更新多个组件以支持新增分类对话框
2026-01-06 13:45:39 +08:00

324 lines
8.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="bill-form">
<van-form @submit="handleSubmit">
<van-cell-group inset>
<!-- 日期时间 -->
<van-field label="时间">
<template #input>
<div style="display: flex; gap: 16px">
<div @click="showDatePicker = true">{{ form.date }}</div>
<div @click="showTimePicker = true">{{ form.time }}</div>
</div>
</template>
</van-field>
<!-- 金额 -->
<van-field
v-model="form.amount"
name="amount"
label="金额"
type="number"
placeholder="0.00"
:rules="[{ required: true, message: '请输入金额' }]"
/>
<!-- 备注 -->
<van-field
v-model="form.note"
name="note"
label="摘要"
placeholder="摘要信息"
rows="2"
autosize
type="textarea"
/>
<!-- 交易类型 -->
<van-field name="type" label="类型">
<template #input>
<van-radio-group v-model="form.type" direction="horizontal" @change="handleTypeChange">
<van-radio :name="0">支出</van-radio>
<van-radio :name="1">收入</van-radio>
<van-radio :name="2">不计</van-radio>
</van-radio-group>
</template>
</van-field>
<!-- 分类 -->
<van-field name="category" label="分类">
<template #input>
<span v-if="!categoryName" style="color: #c8c9cc;">请选择分类</span>
<span v-else>{{ categoryName }}</span>
</template>
</van-field>
<!-- 分类按钮网格 -->
<div class="classify-buttons">
<van-button
type="success"
size="small"
class="classify-btn"
@click="addClassifyDialogRef.open()"
>
+ 新增
</van-button>
<van-button
v-for="item in categoryList"
:key="item.id"
:type="categoryName === item.name ? 'primary' : 'default'"
size="small"
class="classify-btn"
@click="selectClassify(item)"
>
{{ item.name }}
</van-button>
</div>
</van-cell-group>
<div class="actions">
<van-button round block type="primary" native-type="submit" :loading="loading">
{{ submitText }}
</van-button>
<slot name="actions"></slot>
</div>
</van-form>
<!-- 新增分类对话框 -->
<AddClassifyDialog
ref="addClassifyDialogRef"
@confirm="handleAddClassify"
/>
<!-- 日期选择弹窗 -->
<van-popup v-model:show="showDatePicker" position="bottom" round>
<van-date-picker
v-model="currentDate"
title="选择日期"
@confirm="onConfirmDate"
@cancel="showDatePicker = false"
/>
</van-popup>
<!-- 时间选择弹窗 -->
<van-popup v-model:show="showTimePicker" position="bottom" round>
<van-time-picker
v-model="currentTime"
title="选择时间"
@confirm="onConfirmTime"
@cancel="showTimePicker = false"
/>
</van-popup>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { showToast } from 'vant'
import dayjs from 'dayjs'
import AddClassifyDialog from '@/components/AddClassifyDialog.vue'
import { getCategoryList, createCategory } from '@/api/transactionCategory'
const props = defineProps({
initialData: {
type: Object,
default: () => ({})
},
loading: {
type: Boolean,
default: false
},
submitText: {
type: String,
default: '保存'
}
})
const emit = defineEmits(['submit'])
const addClassifyDialogRef = ref()
// 表单数据
const form = ref({
type: 0, // 0: 支出, 1: 收入, 2: 不计
amount: '',
categoryId: null,
date: dayjs().format('YYYY-MM-DD'),
time: dayjs().format('HH:mm'),
note: ''
})
const categoryName = ref('')
// 弹窗控制
const showDatePicker = ref(false)
const showTimePicker = ref(false)
// 选择器数据
const categoryList = ref([])
// 日期时间临时变量 (Vant DatePicker 需要数组或特定格式)
const currentDate = ref(dayjs().format('YYYY-MM-DD').split('-'))
const currentTime = ref(dayjs().format('HH:mm').split(':'))
// 初始化数据
const initForm = async () => {
if (props.initialData) {
const { occurredAt, amount, reason, type, classify } = props.initialData
if (occurredAt) {
const dt = dayjs(occurredAt)
form.value.date = dt.format('YYYY-MM-DD')
form.value.time = dt.format('HH:mm')
currentDate.value = form.value.date.split('-')
currentTime.value = form.value.time.split(':')
}
if (amount !== undefined) form.value.amount = amount
if (reason !== undefined) form.value.note = reason
if (type !== undefined) form.value.type = type
// 加载分类列表
await loadClassifyList(form.value.type)
// 如果有传入分类名称,尝试匹配
if (classify) {
const found = categoryList.value.find(c => c.name === classify)
if (found) {
selectClassify(found)
} else {
// 如果没找到对应分类但有分类名称可能需要特殊处理或者就显示名称但不关联ID
// 这里暂时只显示名称ID为空或者需要自动创建
// 按照原有逻辑,后端需要分类名称,所以这里只要设置 categoryName 即可
// 但是 ManualBillAdd 原逻辑是需要 categoryId 的。
// 不过 createTransaction 接口传的是 classify (name)。
// 让我们看 ManualBillAdd 的 handleSave:
// classify: categoryName.value
// 所以只要 categoryName 有值就行。
categoryName.value = classify
}
}
} else {
await loadClassifyList(form.value.type)
}
}
onMounted(() => {
initForm()
})
// 监听 initialData 变化 (例如重新解析后)
watch(() => props.initialData, () => {
initForm()
}, { deep: true })
const handleTypeChange = (newType) => {
categoryName.value = ''
form.value.categoryId = null
loadClassifyList(newType)
}
const loadClassifyList = async (type = null) => {
try {
const response = await getCategoryList(type)
if (response.success) {
categoryList.value = response.data || []
}
} catch (error) {
console.error('加载分类列表出错:', error)
}
}
const selectClassify = (item) => {
categoryName.value = item.name
form.value.categoryId = item.id
}
const handleAddClassify = async (name) => {
try {
// 调用API创建分类
const response = await createCategory({
name: name,
type: form.value.type
})
if (response.success) {
showToast('分类创建成功')
const newId = response.data
// 重新加载分类列表
await loadClassifyList(form.value.type)
// 选中新创建的分类
categoryName.value = name
form.value.categoryId = newId
} else {
showToast(response.message || '创建分类失败')
}
} catch (error) {
console.error('创建分类出错:', error)
showToast('创建分类失败')
}
}
const onConfirmDate = ({ selectedValues }) => {
form.value.date = selectedValues.join('-')
showDatePicker.value = false
}
const onConfirmTime = ({ selectedValues }) => {
form.value.time = selectedValues.join(':')
showTimePicker.value = false
}
const handleSubmit = () => {
if (!form.value.amount) {
showToast('请输入金额')
return
}
if (!categoryName.value) {
showToast('请选择分类')
return
}
const fullDateTime = `${form.value.date}T${form.value.time}:00`
const payload = {
occurredAt: fullDateTime,
classify: categoryName.value,
amount: parseFloat(form.value.amount),
reason: form.value.note || '',
type: form.value.type
}
emit('submit', payload)
}
// 暴露重置方法给父组件
const reset = () => {
form.value.amount = ''
form.value.note = ''
// 保留日期和类型
}
defineExpose({ reset })
</script>
<style scoped>
.bill-form {
padding-top: 10px;
}
.actions {
margin: 20px 16px;
}
.classify-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px 16px;
}
.classify-btn {
flex: 0 0 auto;
min-width: 70px;
border-radius: 16px;
}
</style>