当前位置: 首页 > news >正文

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-forincomeExpenditureClassificationParentOptions 中动态生成选项。输入框用于输入类型名称,绑定到 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 为空字符串表示默认不输入具体的分类名称。然后,定义了两个响应式数组 incomeExpenditureClassificationParentincomeExpenditureClassificationParentOptions,类型为 IncomeExpenditureClassification[],其中 incomeExpenditureClassificationParent 用于存储从接口返回的原始父级类型数据,incomeExpenditureClassificationParentOptions 用于存储筛选后的父级类型数据,并绑定到下拉框。queryIncomeExpenditureType 是一个异步查询函数,通过 axios.get 方法向后端接口 /api/IncomeExpenditureClassification/QueryParent 发送 GET 请求,请求头中通过 Authorization 传入本地存储的 token 进行身份认证。成功返回后,使用 splice 方法清空原始数据和筛选数据,随后通过 push 方法将接口返回的数据添加到 incomeExpenditureClassificationParentincomeExpenditureClassificationParentOptions 中。如果接口返回状态码不为 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.idrow.namerow.typerow.parentId 分别赋值给 incomeExpenditureClassification 对象中的 idnametypeparentClassificationId,将选中的记录数据绑定到表单中。否则,表示是新增操作,将 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 的响应式和组件化设计思想,保证了交互的流畅性和数据的一致性。

三、小结

本篇文章介绍了如何实现收支分类功能,帮助用户更好地管理和跟踪财务状况。收支分类功能在界面和逻辑上与主币种设置类似,重点在于分类的查询、新增、修改和删除操作的实现。

首先,定义了与收支分类相关的数据结构,包括 IncomeExpenditureClassificationIncomeExpenditureClassificationRequestIncomeExpenditureClassificationPage,用于描述分类的基本信息、提交格式和分页查询参数。查询功能通过两个下拉框和一个输入框实现,分别用于选择收支分类、父级类型以及输入类型名称,配合“查询”和“新增”按钮执行相应的操作。下拉框中的选项数据通过从接口获取的原始数据进行动态生成。通过 axios.get 请求后端接口,接收返回的数据并存储在响应式对象中,供下拉框进行动态展示和筛选。通过 incomeExpenditureCategoryChange 方法,根据所选分类筛选对应的父级类型。

新增和修改操作通过 Element Plus 的 el-dialog 组件实现弹窗。弹窗中包含一个表单,定义了“收支类型”“收支分类”和“父级类型”三个字段,分别使用 el-inputel-radio-groupel-select 进行绑定。用户在弹窗中输入或选择数据后,点击“确定”按钮触发 saveIncomeExpenditureClassification 方法,首先对表单进行校验,校验通过后通过 axios.putaxios.post 请求后端接口,执行保存或修改操作,成功后弹出提示并刷新数据。

yChange` 方法,根据所选分类筛选对应的父级类型。

新增和修改操作通过 Element Plus 的 el-dialog 组件实现弹窗。弹窗中包含一个表单,定义了“收支类型”“收支分类”和“父级类型”三个字段,分别使用 el-inputel-radio-groupel-select 进行绑定。用户在弹窗中输入或选择数据后,点击“确定”按钮触发 saveIncomeExpenditureClassification 方法,首先对表单进行校验,校验通过后通过 axios.putaxios.post 请求后端接口,执行保存或修改操作,成功后弹出提示并刷新数据。

删除操作通过 ElMessageBox.confirm 弹出确认框,用户确认后通过 axios.delete 请求接口,成功后刷新数据。

相关文章:

  • MySQL 调优:查询慢除了索引还能因为什么?
  • 设计模式之责任链模式:原理、实现与应用
  • 各软件快捷键
  • 【CXX-Qt】2.5 继承
  • 基于认证的 Harbor 容器镜像仓库
  • 基于koajsAdmin+mongodb的后台管理快速开发框架安装运行记录
  • 深度学习-151-Dify工具之创建一个生成财务报表的智能体Agent
  • 【容器运维】docker搭建私有仓库
  • 【MySQL篇】复合查询
  • 数学爱好者写的编程系列文章
  • Linux | make和Makefile命令详细篇
  • 深度学习:让机器学会“思考”的魔法
  • webpack使用详细步骤
  • SpringBootAdmin-clinet自定义监控CPU、内存、磁盘等health
  • Linux:xxx is not in the sudoers file. This incident will be reported.
  • macOS Sequoia 15.3 一直弹出“xx正在访问你的屏幕”
  • 深度学习篇---对角矩阵矩阵的秩奇异矩阵
  • 异地灾备介绍
  • STM32中断
  • 【Android】基于udp通信的智能家居移动应用开发
  • 上海“世行对标改革”的税务样本:设立全国首个税务审判庭、制定首个税务行政复议简易程序
  • 体坛联播|曼联热刺会师欧联杯决赛,多哈世乒赛首日赛程出炉
  • 首批证券公司科创债来了!拟发行规模超160亿元
  • 青岛双星名人集团管理权之争:公司迁址,管理层更迭
  • 【社论】三个“靠谱”为市场注入确定性
  • 用社群活动维系“不开发”古镇的生命力