本篇文章我们一起来实现收支分类功能。收支分类和前篇文章的主币种设置界面大体类似。我们将详细介绍如何创建和管理不同的收支分类,以便用户可以更好地组织和跟踪他们的财务状况。
一、功能
先来看一下原型界面,界面很简单,这里就不多讲解页面功能了。我们主要关注如何实现收支分类的核心功能。
二、实现
表格、分页这两个功能的实现在前篇中已经展示了,在这里就不过多讲解了,我们重点看一下查询、新增、修改以及删除的实现。我们先在Config
文件夹下新建IncomeExpenditureType.vue
文件,这个文件就是用来实现收支类型功能的。接着在Interface
文件夹下新建文件IncomeExpenditureClassification.ts
,在这个文件中编写收支分类与服务端交互所使用的类型,代码如下:
import type {PageRequest} from "./request.ts";/*** 收支分类*/
export interface IncomeExpenditureClassification {id: string;name: string;type: number;parentId: string;parentName: string;
}/*** 收支分类提交*/
export interface IncomeExpenditureClassificationRequest{id: string;name: string;type: number;parentClassificationId: string;
}/*** 分页查询*/
export interface IncomeExpenditureClassificationPage extends PageRequest {classificationName: string;type: number;parentClassificationId: string;
}
2.1 查询
搜索功能包含两个下拉选择框和一个输入框,分别用于选择收支分类、父级类型以及输入类型名称进行搜索,还包括“查询”和“新增”两个按钮,分别用于执行搜索操作和添加新类型。
<div class="container"><!--搜索功能区域--><el-row :gutter="20"><el-col :span="3"><el-text>收支分类:</el-text><el-selectv-model="incomeExpenditureClassificationPage.type":default-first-option="true"placeholder="请选择收支分类"@change="incomeExpenditureCategoryChange"style="width: 65%"><el-option label="全部" :value="2"></el-option><el-option label="收入" :value="0"></el-option><el-option label="支出" :value="1"></el-option><el-option label="其他" :value="-1"></el-option></el-select></el-col><el-col :span="3"><el-text>父级类型:</el-text><el-select v-model="incomeExpenditureClassificationPage.parentClassificationId" :default-first-option="true"placeholder="请选择父级类型" style="width: 65%"><el-option label="全部" value="-1"></el-option><el-optionv-for="item in incomeExpenditureClassificationParentOptions":key="item.id":label="item.name":value="item.id"></el-option></el-select></el-col><el-col :span="4"><el-text>类型名:</el-text><el-input placeholder="请输入类型名" v-model="incomeExpenditureClassificationPage.classificationName"style="width: 65%"></el-input></el-col><el-col :span="6"><el-button type="primary" @click="queryIncomeExpenditure">搜索</el-button><el-button type="success" @click="editIncomeExpenditureClassification">新增</el-button></el-col></el-row>
</div>
这段代码是一个使用 Vue 3 和 Element Plus 组件库实现的搜索功能区域,包含三个筛选条件和两个操作按钮。外层容器 <div class="container">
定义了一个容器,可能用于设置布局或样式。<el-row :gutter="20">
使用了 Element Plus 的 el-row
组件定义网格布局,gutter="20"
表示列之间的间距为 20px。<el-col :span="3">
定义了列的宽度,span="3"
表示该列占用 24 栅格系统中的 3 格。第一个下拉框用于选择收支分类,绑定到 incomeExpenditureClassificationPage.type
,通过 @change="incomeExpenditureCategoryChange"
监听变化事件,选项包括“全部”“收入”“支出”“其他”。第二个下拉框用于选择父级类型,绑定到 incomeExpenditureClassificationPage.parentClassificationId
,通过 v-for
从 incomeExpenditureClassificationParentOptions
中动态生成选项。输入框用于输入类型名称,绑定到 incomeExpenditureClassificationPage.classificationName
,用于模糊或精确搜索。第一个按钮设置为 type="primary"
,点击时触发 queryIncomeExpenditure
方法,执行搜索操作。第二个按钮设置为 type="success"
,点击时触发 editIncomeExpenditureClassification
方法,执行新增操作。数据绑定通过 data
定义,示例如下:incomeExpenditureClassificationPage
中保存收支分类、父级类型和类型名称,incomeExpenditureClassificationParentOptions
保存父级类型选项。方法包括 incomeExpenditureCategoryChange
处理分类变化,queryIncomeExpenditure
处理查询,editIncomeExpenditureClassification
处理新增。
// 查询收支分类参数IncomeExpenditureClassificationPage
const incomeExpenditureClassificationPage = ref<IncomeExpenditureClassificationPage>({pageSize: 10,pageNumber: 1,type: 2,parentClassificationId: '-1',classificationName: ''
});
// 接收父级类型的数据IncomeExpenditureClassification
const incomeExpenditureClassificationParent = reactive<IncomeExpenditureClassification[]>([]);
// 父级类型操作的数据
const incomeExpenditureClassificationParentOptions = reactive<IncomeExpenditureClassification[]>([]);// 查询父级类型
const queryIncomeExpenditureType = () => {axios.get(import.meta.env.VITE_API_BASE_URL + '/api/IncomeExpenditureClassification/QueryParent', {headers: {Authorization: localStorage.getItem('token')}}).then((res: any) => {const response = res.data;if (response.statusCode === 200) {// 接收返回值incomeExpenditureClassificationParent.splice(0, incomeExpenditureClassificationParent.length);incomeExpenditureClassificationParentOptions.splice(0, incomeExpenditureClassificationParentOptions.length);incomeExpenditureClassificationParent.push(...response.data);incomeExpenditureClassificationParentOptions.push(...response.data);} else {ElMessage.error(response.errorMessage)}}).catch((err: any) => {ElMessage.error(err.message);});
}
// 收支分类下拉选择框
const incomeExpenditureCategoryChange = (value: any) => {// 过滤出来对应分类的类型,不修改元数据,过滤后的数据绑定到父级分类下拉菜单incomeExpenditureClassificationParentOptions.splice(0, incomeExpenditureClassificationParentOptions.length, ...incomeExpenditureClassificationParent);if (value === 2) {return;}const filteredData = incomeExpenditureClassificationParent.filter(item => item.type === Number(value));incomeExpenditureClassificationParentOptions.splice(0, incomeExpenditureClassificationParent.length, ...filteredData);
}
这段代码是使用 Vue 3 和 TypeScript 通过 ref 和 reactive 定义的一个用于收支分类筛选和父级分类管理的功能。首先,定义了查询参数对象 incomeExpenditureClassificationPage
,它是一个 ref
响应式对象,类型为 IncomeExpenditureClassificationPage
,其中 pageSize
定义每页显示的记录数量为 10,pageNumber
定义当前显示的页码为 1,type
为 2 表示默认选中“全部”,parentClassificationId
为 ‘-1’ 表示默认选择“全部父级分类”,classificationName
为空字符串表示默认不输入具体的分类名称。然后,定义了两个响应式数组 incomeExpenditureClassificationParent
和 incomeExpenditureClassificationParentOptions
,类型为 IncomeExpenditureClassification[]
,其中 incomeExpenditureClassificationParent
用于存储从接口返回的原始父级类型数据,incomeExpenditureClassificationParentOptions
用于存储筛选后的父级类型数据,并绑定到下拉框。queryIncomeExpenditureType
是一个异步查询函数,通过 axios.get
方法向后端接口 /api/IncomeExpenditureClassification/QueryParent
发送 GET 请求,请求头中通过 Authorization
传入本地存储的 token
进行身份认证。成功返回后,使用 splice
方法清空原始数据和筛选数据,随后通过 push
方法将接口返回的数据添加到 incomeExpenditureClassificationParent
和 incomeExpenditureClassificationParentOptions
中。如果接口返回状态码不为 200,则使用 ElMessage.error
显示后端返回的错误信息。如果请求失败,catch
捕获异常并通过 ElMessage.error
显示错误消息。incomeExpenditureCategoryChange
是收支分类下拉框的 change
事件处理方法,value
是当前选择的收支分类值。首先使用 splice
方法清空筛选数据,并将原始数据重新绑定到下拉框选项中。如果 value
为 2(即“全部”),直接返回所有选项,不做筛选。否则使用 filter
方法根据选中的收支分类值筛选出对应的父级类型,并通过 splice
方法将筛选结果更新到下拉框选项中。
2.2 新增/修改/删除
新增和修改操作都需要通过弹窗完成,因此使用 element-plus 提供的 el-dialog
组件来实现。
<el-dialog title="收支类型" v-model="dialogVisible" width="400"><el-form :model="incomeExpenditureClassification" :rules="rules" ref="incomeExpenditureClassificationFormRef"label-width="80px"><el-form-item label="收支类型" prop="name"><el-input v-model="incomeExpenditureClassification.name"></el-input></el-form-item><el-form-item label="收支分类" prop="type"><el-radio-group v-model="incomeExpenditureClassification.type"><el-radio :value="0">收入</el-radio><el-radio :value="1">支出</el-radio><el-radio :value="-1">其他</el-radio></el-radio-group></el-form-item><el-form-item label="父级类型" prop="parentClassificationId"><el-selectclearablev-model="incomeExpenditureClassification.parentClassificationId"placeholder="请选择父级类型"><el-optionv-for="item in incomeExpenditureClassificationParentOptions":key="item.id":label="item.name":value="item.id"></el-option></el-select></el-form-item></el-form><template #footer><el-button @click="dialogVisible = false">取 消</el-button><el-button type="primary" @click="saveIncomeExpenditureClassification(incomeExpenditureClassificationFormRef)">确 定</el-button></template>
</el-dialog>
<el-dialog>
是 Element Plus 提供的弹窗组件,v-model="dialogVisible"
绑定弹窗的显示状态,title="收支类型"
设置弹窗标题为“收支类型”,width="400"
设置弹窗宽度为 400px,保证在不同屏幕尺寸下有合适的显示宽度。当 dialogVisible
变为 true
时弹窗显示,变为 false
时弹窗关闭。
在弹窗内部,<el-form>
是 Element Plus 的表单组件,model="incomeExpenditureClassification"
绑定表单数据对象,rules="rules"
绑定表单验证规则,ref="incomeExpenditureClassificationFormRef"
定义一个 ref,用于在保存时对表单进行校验,label-width="80px"
定义标签的宽度为 80px,保证标签和输入框的对齐。<el-form-item>
用于定义表单中的每个字段项。
第一个 <el-form-item>
定义了“收支类型”字段,prop="name"
绑定表单数据中的 name
字段,并使用 <el-input>
组件定义输入框,v-model="incomeExpenditureClassification.name"
绑定输入框的值到表单对象的 name
字段,输入框中的值会自动同步到数据对象中。
第二个 <el-form-item>
定义了“收支分类”字段,prop="type"
绑定表单数据中的 type
字段,使用 <el-radio-group>
定义单选框组,v-model="incomeExpenditureClassification.type"
绑定当前选中的单选框值到表单对象中的 type
字段。<el-radio>
定义单选项,:value="0"
表示收入,:value="1"
表示支出,:value="-1"
表示其他,用户选择不同选项时,incomeExpenditureClassification.type
的值会自动更新。
第三个 <el-form-item>
定义了“父级类型”字段,prop="parentClassificationId"
绑定到表单数据中的 parentClassificationId
字段,使用 <el-select>
定义下拉框,v-model="incomeExpenditureClassification.parentClassificationId"
绑定下拉框当前选择的值到表单对象,clearable
属性允许用户清空已选择的选项,placeholder="请选择父级类型"
定义占位文本。通过 v-for
遍历 incomeExpenditureClassificationParentOptions
渲染选项,:key="item.id"
定义唯一标识符,:label="item.name"
定义显示文本,:value="item.id"
定义选项值,选择不同选项时 incomeExpenditureClassification.parentClassificationId
的值会自动更新。
<template #footer>
定义弹窗的底部操作按钮,<el-button @click="dialogVisible = false">取 消</el-button>
定义取消按钮,点击时将 dialogVisible
设为 false
以关闭弹窗。<el-button type="primary" @click="saveIncomeExpenditureClassification(incomeExpenditureClassificationFormRef)">确 定</el-button>
定义保存按钮,点击时触发 saveIncomeExpenditureClassification
方法,并传入表单的 ref 进行校验和保存操作。
//删除收支类型
const deleteIncomeExpenditureClassification = (id: string) => {// 弹出确认框ElMessageBox.confirm('此操作将永久删除该收支类型, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => {// 确认删除axios.delete(import.meta.env.VITE_API_BASE_URL + '/api/IncomeExpenditureClassification/Delete/' + id, {headers: {Authorization: localStorage.getItem('token')}}).then((res: any) => {const response = res.data;if (response.statusCode === 200) {ElMessage.success('删除成功');queryIncomeExpenditure();queryIncomeExpenditureType();} else {ElMessage.error(response.errorMessage)}}).catch((err: any) => {ElMessage.error(err.message);});}).catch(() => {ElMessage.info('已取消删除');});
}
// 收支分类弹窗事件
const editIncomeExpenditureClassification = (row: any) => {dialogVisible.value = true;if (row) {console.log(row);// 修改incomeExpenditureClassification.id = row.id;incomeExpenditureClassification.name = row.name;incomeExpenditureClassification.type = row.type;incomeExpenditureClassification.parentClassificationId = row.parentId;} else {// 新增incomeExpenditureClassification.id = '';incomeExpenditureClassification.name = '';incomeExpenditureClassification.type = 0;incomeExpenditureClassification.parentClassificationId = '';}
}/*** 保存收支分类*/
const saveIncomeExpenditureClassification = (formEl: FormInstance | undefined) => {if (!formEl) returnformEl.validate().then(() => {if (incomeExpenditureClassification.id) {// 修改axios.put(import.meta.env.VITE_API_BASE_URL + '/api/IncomeExpenditureClassification/Update',incomeExpenditureClassification, {headers: {Authorization: localStorage.getItem('token')}}).then((res: any) => {const response = res.data;if (response.statusCode === 200) {ElMessage.success('修改成功');dialogVisible.value = false;queryIncomeExpenditure();} else {ElMessage.error(response.errorMessage)}}).catch((err: any) => {ElMessage.error(err.message);});} else {// 新增axios.post(import.meta.env.VITE_API_BASE_URL + '/api/IncomeExpenditureClassification/Add',incomeExpenditureClassification, {headers: {Authorization: localStorage.getItem('token')}}).then((res: any) => {const response = res.data;if (response.statusCode === 200) {ElMessage.success('新增成功');dialogVisible.value = false;queryIncomeExpenditure();} else {ElMessage.error(response.errorMessage)}}).catch((err: any) => {ElMessage.error(err.message);});}}).catch(() => {ElMessage.error('请检查输入项');});
}// 收支类型验证
const rules = reactive<FormRules<IncomeExpenditureClassificationRequest>>({name: [{required: true, message: '分类名称不能为空', trigger: 'blur'}],type: [{required: true, message: '收支分类不能为空', trigger: 'blur'},]
})
这段代码定义了删除、编辑和保存收支类型的完整操作逻辑,同时包括了表单验证规则.
deleteIncomeExpenditureClassification
是删除收支类型的函数,参数 id
是待删除的收支类型的唯一标识符。首先调用 ElMessageBox.confirm
弹出确认框,提示用户“此操作将永久删除该收支类型,是否继续?”标题为“提示”,确认按钮文本为“确定”,取消按钮文本为“取消”,对话框类型为“warning”表示警告提示。如果用户点击“确定”,会触发 then
逻辑,调用 axios.delete
发送 DELETE 请求,目标地址为 /api/IncomeExpenditureClassification/Delete/
+ id
,并在请求头中携带身份认证 token。请求成功后,返回数据通过 res.data
进行解析,若 statusCode === 200
表示删除成功,调用 ElMessage.success
显示“删除成功”,随后调用 queryIncomeExpenditure()
刷新列表,调用 queryIncomeExpenditureType()
刷新父级分类列表。如果 statusCode
不是 200,调用 ElMessage.error
显示后端返回的错误信息。如果请求失败,catch 捕获异常并通过 ElMessage.error
显示错误信息。若用户点击取消或关闭对话框,catch 捕获后调用 ElMessage.info('已取消删除')
显示提示。
editIncomeExpenditureClassification
是编辑和新增收支分类的函数,参数 row
是选中的收支类型对象。当用户点击编辑按钮时,首先将 dialogVisible.value = true
打开弹窗。如果 row
存在,表示是修改操作,通过 row.id
、row.name
、row.type
和 row.parentId
分别赋值给 incomeExpenditureClassification
对象中的 id
、name
、type
和 parentClassificationId
,将选中的记录数据绑定到表单中。否则,表示是新增操作,将 id
设为空,name
设为空字符串,type
设为 0
(默认选中“收入”),parentClassificationId
设为空字符串,确保表单为空白状态。
saveIncomeExpenditureClassification
是保存收支分类的函数,参数 formEl
是表单的 ref 实例。如果 formEl
不存在则直接返回。调用 formEl.validate()
触发表单验证,验证通过后判断 incomeExpenditureClassification.id
是否存在。如果存在,表示是修改操作,调用 axios.put
发送 PUT 请求到 /api/IncomeExpenditureClassification/Update
,请求体传入 incomeExpenditureClassification
,请求头中携带 token。如果返回数据的 statusCode === 200
,调用 ElMessage.success('修改成功')
显示修改成功提示,将 dialogVisible.value = false
关闭弹窗,调用 queryIncomeExpenditure()
刷新列表。如果 statusCode
不是 200,调用 ElMessage.error
显示错误信息。请求失败则捕获异常并通过 ElMessage.error
显示错误信息。如果 incomeExpenditureClassification.id
不存在,表示是新增操作,调用 axios.post
发送 POST 请求到 /api/IncomeExpenditureClassification/Add
,请求体传入 incomeExpenditureClassification
,请求头中携带 token。返回数据成功后调用 ElMessage.success('新增成功')
,关闭弹窗并刷新列表。请求失败同样捕获异常并显示错误提示。如果表单验证失败,catch 捕获后显示“请检查输入项”。
rules
定义了表单字段的验证规则,使用 reactive
定义为响应式对象,类型为 FormRules<IncomeExpenditureClassificationRequest>
。name
字段规则要求为必填,未填时触发 blur
事件显示“分类名称不能为空”;type
字段规则要求为必填,未填时触发 blur
事件显示“收支分类不能为空”。
这段代码完整地实现了收支分类的新增、修改、删除和表单验证功能。通过 ElMessageBox.confirm
处理删除确认,通过 axios
调用后端接口完成数据交互,通过 ElMessage
反馈操作结果。通过 ref
绑定表单实例,利用 validate
进行数据校验,确保输入内容合法。整个流程遵循 Vue 3 的响应式和组件化设计思想,保证了交互的流畅性和数据的一致性。
三、小结
本篇文章介绍了如何实现收支分类功能,帮助用户更好地管理和跟踪财务状况。收支分类功能在界面和逻辑上与主币种设置类似,重点在于分类的查询、新增、修改和删除操作的实现。
首先,定义了与收支分类相关的数据结构,包括 IncomeExpenditureClassification
、IncomeExpenditureClassificationRequest
和 IncomeExpenditureClassificationPage
,用于描述分类的基本信息、提交格式和分页查询参数。查询功能通过两个下拉框和一个输入框实现,分别用于选择收支分类、父级类型以及输入类型名称,配合“查询”和“新增”按钮执行相应的操作。下拉框中的选项数据通过从接口获取的原始数据进行动态生成。通过 axios.get
请求后端接口,接收返回的数据并存储在响应式对象中,供下拉框进行动态展示和筛选。通过 incomeExpenditureCategoryChange
方法,根据所选分类筛选对应的父级类型。
新增和修改操作通过 Element Plus 的 el-dialog
组件实现弹窗。弹窗中包含一个表单,定义了“收支类型”“收支分类”和“父级类型”三个字段,分别使用 el-input
、el-radio-group
和 el-select
进行绑定。用户在弹窗中输入或选择数据后,点击“确定”按钮触发 saveIncomeExpenditureClassification
方法,首先对表单进行校验,校验通过后通过 axios.put
或 axios.post
请求后端接口,执行保存或修改操作,成功后弹出提示并刷新数据。
yChange` 方法,根据所选分类筛选对应的父级类型。
新增和修改操作通过 Element Plus 的 el-dialog
组件实现弹窗。弹窗中包含一个表单,定义了“收支类型”“收支分类”和“父级类型”三个字段,分别使用 el-input
、el-radio-group
和 el-select
进行绑定。用户在弹窗中输入或选择数据后,点击“确定”按钮触发 saveIncomeExpenditureClassification
方法,首先对表单进行校验,校验通过后通过 axios.put
或 axios.post
请求后端接口,执行保存或修改操作,成功后弹出提示并刷新数据。
删除操作通过 ElMessageBox.confirm
弹出确认框,用户确认后通过 axios.delete
请求接口,成功后刷新数据。