7. 【Vue实战--孢子记账--Web 版开发】-- 收支分类设置
本篇文章我们一起来实现收支分类功能。收支分类和前篇文章的主币种设置界面大体类似。我们将详细介绍如何创建和管理不同的收支分类,以便用户可以更好地组织和跟踪他们的财务状况。
一、功能
先来看一下原型界面,界面很简单,这里就不多讲解页面功能了。我们主要关注如何实现收支分类的核心功能。
二、实现
表格、分页这两个功能的实现在前篇中已经展示了,在这里就不过多讲解了,我们重点看一下查询、新增、修改以及删除的实现。我们先在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-select
v-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-option
v-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-select
clearable
v-model="incomeExpenditureClassification.parentClassificationId"
placeholder="请选择父级类型"
>
<el-option
v-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) return
formEl.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
请求接口,成功后刷新数据。