9. 【Vue实战--孢子记账--Web 版开发】-- 账户账本管理(二)

发布于:2025-07-07 ⋅ 阅读:(19) ⋅ 点赞:(0)

本篇文章我们一起来详细讲解记账模块文件的实现。这个文件在记账应用中扮演着核心角色,负责账目的展示、查询、新增、编辑和删除功能。我们将从页面结构、数据定义、表单校验、方法实现等多个角度进行剖析,帮助你全面掌握其开发逻辑。

一、功能概览

记账模块负责账目的管理功能。页面支持通过日期范围进行账目查询,查询结果以分页列表的形式展示,用户可以通过表格查看每条账目的具体信息。页面还支持通过弹窗形式新增或编辑账目数据,同时提供删除功能以清理无用账目记录。此外,在表单填写过程中,会自动拉取收支分类和币种列表,确保数据选择的准确性与完整性。

二、页面结构实现

我们先来看一下页面的主要结构,使用了 Element Plus 组件库完成布局与交互。

2.1 面包屑导航
<el-breadcrumb separator="/">
  <el-breadcrumb-item :to="{path: '/'}">首页</el-breadcrumb-item>
  <el-breadcrumb-item :to="{path: '/accountBook'}">账本</el-breadcrumb-item>
  <el-breadcrumb-item><span style="font-weight: bold">记账</span></el-breadcrumb-item>
</el-breadcrumb>

该部分是页面顶部的路径指示器,用户可以直观地看到当前所在位置,方便在不同页面间快速切换。el-breadcrumb 组件通过 separator="/" 属性将各级路径用斜杠分隔,符合常见的导航习惯。每个 el-breadcrumb-item 代表一个导航节点,可以通过 :to 属性设置跳转路径,实现点击跳转。例如,点击“首页”会返回主页面,点击“账本”则进入账本列表。最后一个节点通常为当前页面,使用加粗样式突出显示,提升用户的定位感和操作体验。面包屑导航不仅提升了页面的可用性,也有助于用户理解系统的层级结构。

2.2 筛选区域
<el-row :gutter="20">
  <el-col :span="4">
    <el-text>开始日期:</el-text>
    <el-date-picker v-model="recordPage.startDate" type="date" value-format="YYYY-MM-DD" style="width: 65%" />
  </el-col>
  <el-col :span="4">
    <el-text>结束日期:</el-text>
    <el-date-picker v-model="recordPage.endDate" type="date" value-format="YYYY-MM-DD" style="width: 65%" />
  </el-col>
  <el-col :span="6">
    <el-button type="primary" @click="queryRecords">搜索</el-button>
    <el-button type="success" @click="openAddDialog">新增账目</el-button>
  </el-col>
</el-row>

这一块是用于查询和筛选账目的区域,采用了 el-rowel-col 组件实现响应式的栅格布局,保证在不同屏幕尺寸下也能良好展示。筛选区域主要包含两个日期选择器和两个操作按钮:

  • 日期选择器:分别用于选择“开始日期”和“结束日期”,通过 v-model 绑定到 recordPage.startDaterecordPage.endDate,实现数据的双向绑定。value-format="YYYY-MM-DD" 保证了日期数据以统一的字符串格式传递,便于后端接口处理和前端展示。用户可以灵活选择任意时间范围进行账目查询。

  • 操作按钮

    • “搜索”按钮绑定了 queryRecords 方法,点击后会根据当前选择的日期范围重新请求并刷新账目列表,实现按条件筛选。
    • “新增账目”按钮绑定了 openAddDialog 方法,点击后弹出新增账目表单,方便用户快速录入新的账目信息。

常用筛选条件和操作按钮集中在一行,提升了用户的操作效率和页面的易用性。通过合理的组件组合和数据绑定,极大地简化了账目筛选和新增的交互流程。

2.3 账目列表
<el-table :data="recordPageResponse.data" style="width: 100%">
  <el-table-column type="index" label="编号" width="100" />
  <el-table-column prop="recordDate" label="日期" width="120">
    <template #default="scope">
      {{ new Date(scope.row.recordDate).toLocaleDateString() }}
    </template>
  </el-table-column>
  <el-table-column prop="incomeExpenditureClassificationName" label="分类" width="120" />
  <el-table-column prop="afterAmount" label="金额" width="100" />
  <el-table-column prop="currencyName" label="货币" width="80" />
  <el-table-column prop="remark" label="备注" />
  <el-table-column label="操作" width="180">
    <template #default="scope">
      <el-button link type="warning" size="small" @click="openEditDialog(scope.row)">编辑</el-button>
      <el-button link type="danger" size="small" @click="deleteRecord(scope.row.incomeExpenditureRecordId)">删除</el-button>
    </template>
  </el-table-column>
</el-table>

账目展示区域采用了 el-table 组件来渲染账目信息列表,整体结构清晰、功能丰富。表格的每一列通过 el-table-column 组件定义,首列为自动编号,便于用户快速定位和查找。日期列利用 template 插槽对原始时间戳进行格式化处理,将其转化为本地化的日期字符串,提升了数据的可读性。分类、金额、货币和备注等字段则直接绑定相应的数据属性,确保账目信息的完整展示。操作列集成了“编辑”和“删除”两个按钮,分别关联打开编辑弹窗和删除账目的事件方法,方便用户对账目进行灵活管理。整体表格布局合理,交互直观,极大提升了账目管理的效率和用户体验。

2.4 分页组件
<el-pagination
  @size-change="handleSizeChange"
  @current-change="handleCurrentChange"
  :current-page="currentPage"
  :page-size="pageSize"
  :total="recordPageResponse.rowCount"
  :page-sizes="[10, 20, 30, 40]"
  layout="total, sizes, prev, pager, next, jumper"
  style="margin-top: 20px"/>

分页器用于控制账目表格的页数切换和数据量展示。它通过 currentPage 绑定当前页码,pageSize 控制每页显示的记录数,recordPageResponse.rowCount 提供总记录数,确保分页信息与后端数据同步。layout 属性灵活配置了分页器的显示内容,包括总条数、每页数量选择、上一页、页码、下一页和跳转输入框等,满足不同用户的操作习惯。

分页器的两个核心事件分别是:

  • @size-change:当用户切换每页显示条数时触发,通常会重置当前页为第一页,并重新请求数据,保证分页逻辑的正确性。
  • @current-change:当用户点击页码或使用跳转功能切换当前页时触发,自动刷新表格数据,展示对应页的账目信息。

通过这两个事件与数据的双向绑定,分页器能够与账目表格无缝联动,实现高效、流畅的分页体验。用户可以根据实际需求灵活调整每页条数和快速定位到目标页,大大提升了账目管理的便捷性和可用性。

2.5 新增/编辑弹窗
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="400">
  <el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
    <el-form-item label="收支分类" prop="classificationType">
      <el-radio-group v-model="form.classificationType" @change="handleClassificationTypeChange">
        <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="incomeExpenditureClassificationId">
      <el-select v-model="form.incomeExpenditureClassificationId" placeholder="请选择收支类型">
        <el-option
            v-for="item in incomeExpenditureClassifications"
            :key="item.id"
            :label="item.name"
            :value="item.id">
        </el-option>
      </el-select>
    </el-form-item>
    <el-form-item label="金额" prop="amount">
      <el-input v-model.number="form.amount" type="number" min="0"/>
    </el-form-item>
    <el-form-item label="货币" prop="currencyId">
      <el-select v-model="form.currencyId" placeholder="请选择货币">
        <el-option
            v-for="item in currencies"
            :key="item.id"
            :label="item.name"
            :value="item.id">
        </el-option>
      </el-select>
    </el-form-item>
    <el-form-item label="备注" prop="remark">
      <el-input v-model="form.remark" maxlength="100" show-word-limit/>
    </el-form-item>
    <el-form-item label="日期" prop="recordDate">
      <el-date-picker v-model="form.recordDate" type="date" value-format="YYYY-MM-DD"/>
    </el-form-item>
  </el-form>
  <template #footer>
    <el-button @click="dialogVisible = false">取 消</el-button>
    <el-button type="primary" @click="submitForm(formRef)">确 定</el-button>
  </template>
</el-dialog>

这段代码展示了一个基于 Element Plus 组件库实现的账目新增或编辑弹窗。整个弹窗由 <el-dialog> 组件包裹,通过 v-model="dialogVisible" 控制显示与隐藏,dialogTitle 则根据当前操作动态切换标题,比如“新增账目”或“编辑账目”,让用户一目了然当前的操作类型。弹窗内部是一个 <el-form> 表单,利用 :model="form" 实现与响应式数据的双向绑定,用户在表单中输入的所有内容都会实时同步到 form 对象上。表单还通过 :rules="rules" 绑定校验规则,确保每个字段都能按照预期格式填写,避免无效或不完整的数据提交。每个 <el-form-item> 对应一个输入项,比如“收支分类”用单选按钮组 <el-radio-group> 实现,用户可以选择收入、支出或其他类型,选择变化时会触发 handleClassificationTypeChange 方法,动态调整后续选项。收支类型和货币类型分别用下拉选择框 <el-select> 实现,选项内容通过 v-for 动态渲染,保证数据的灵活性和可扩展性。金额输入框 <el-input> 绑定了 v-model.number,确保输入内容为数字类型,并限制最小值为0。备注输入框支持最多100字,并显示字数统计,方便用户控制输入长度。日期选择器 <el-date-picker> 让用户可以方便地选择账目日期,并通过 value-format 保证数据格式统一。弹窗底部的操作按钮通过插槽 #footer 自定义,点击“取消”按钮会关闭弹窗,而点击“确定”按钮则会调用 submitForm(formRef) 方法,先进行表单校验,校验通过后再提交数据。整体设计既保证了用户体验的流畅性,也通过数据绑定和校验机制提升了数据的准确性和安全性。

三、核心逻辑与数据定义

3.1 响应式数据定义
const recordPage = ref<RecordPage>({...})
const recordPageResponse = reactive<PageResponse<RecordItem>>({...})
const form = reactive<RecordRequest>({...})
const dialogVisible = ref(false)
const dialogTitle = ref('新增账目')
const pageSize = ref(10)
const currentPage = ref(1)
const formRef = ref<FormInstance>()
let editId: string | null = null

这些响应式数据变量共同维护着页面的核心状态,包括账目筛选条件、分页参数、弹窗的显示与隐藏、表单内容的双向绑定以及表单引用等。recordPage 用于存储当前的查询条件,如日期范围等,recordPageResponse 保存后端返回的账目列表及分页信息,确保表格与分页器的数据同步。form 负责绑定弹窗表单的各项输入内容,实现新增和编辑时的数据回显与收集。dialogVisible 控制弹窗的显示状态,dialogTitle 动态切换弹窗标题以区分不同操作。pageSizecurrentPage 管理分页器的当前页码和每页条数,formRef 用于表单校验和重置操作的引用。editId 标记当前编辑的账目 ID,便于区分新增与编辑逻辑。这些数据的合理组织和响应式管理,为页面的高效交互和数据一致性提供了坚实基础。

3.2 表单验证规则
const rules = reactive<FormRules>({
  amount: [
    { required: true, message: '请输入金额', trigger: 'blur' },
    { type: 'number', min: 0.01, message: '金额必须大于0', trigger: 'blur' }
  ],
  recordDate: [
    { required: true, message: '请选择日期', trigger: 'change' }
  ],
  incomeExpenditureClassificationId: [
    { required: true, message: '请选择分类', trigger: 'change' }
  ],
  currencyId: [
    { required: true, message: '请选择货币', trigger: 'change' }
  ]
})

上述代码定义了金额、日期、分类、货币等字段的校验规则,确保用户在填写表单时输入的数据符合业务要求。例如,金额字段不仅要求必填,还必须大于 0,防止录入无效或错误的账目信息;日期、分类和货币字段同样设置为必选项,避免因缺失关键信息导致数据不完整。通过这些细致的校验规则,能够在用户提交表单前及时发现并提示输入错误,有效提升数据的准确性和系统的健壮性,为后续的账目管理和统计分析打下坚实基础。

四、方法实现与接口调用

4.1 查询账目列表
const queryRecords = () => {
  recordPage.value.pageNumber = currentPage.value
  recordPage.value.pageSize = pageSize.value
  recordPage.value.accountBookId = accountBookId.value

  // 构建查询参数,只包含有值的日期字段
  const queryParams: any = {
    pageNumber: recordPage.value.pageNumber,
    pageSize: recordPage.value.pageSize,
    accountBookId: recordPage.value.accountBookId
  }

  // 只有当日期不为空时才添加到查询参数中
  if (recordPage.value.startDate) {
    queryParams.startDate = recordPage.value.startDate + 'T00:00:00.000Z'
  }
  if (recordPage.value.endDate) {
    queryParams.endDate = recordPage.value.endDate + 'T23:59:59.999Z'
  }

  axios.post(import.meta.env.VITE_API_BASE_URL + '/api/IncomeExpenditureRecord/Query', queryParams, {
    headers: {
      Authorization: localStorage.getItem('token')
    }
  }).then((res: any) => {
    const response = res.data
    if (response.statusCode === 200) {
      recordPageResponse.pageCount = response.data.pageCount
      recordPageResponse.rowCount = response.data.rowCount
      recordPageResponse.data = response.data.data
    } else {
      ElMessage.error(response.errorMessage)
    }
  }).catch((err: any) => {
    ElMessage.error(err.message)
  })
}

这段代码是账目查询的核心方法,主要用于根据当前筛选条件(如页码、每页数量、账本ID、日期范围)向后端请求账目列表,并将结果更新到页面。

  1. 同步分页和账本ID参数

    recordPage.value.pageNumber = currentPage.value
    recordPage.value.pageSize = pageSize.value
    recordPage.value.accountBookId = accountBookId.value
    

    这三行代码将当前页码、每页条数和账本ID同步到查询参数对象 recordPage,确保每次查询时参数都是最新的。

  2. 构建查询参数

    const queryParams: any = {
      pageNumber: recordPage.value.pageNumber,
      pageSize: recordPage.value.pageSize,
      accountBookId: recordPage.value.accountBookId
    }
    

    这里创建了一个 queryParams 对象,初始只包含分页和账本ID。

  3. 按需添加日期筛选

    if (recordPage.value.startDate) {
      queryParams.startDate = recordPage.value.startDate + 'T00:00:00.000Z'
    }
    if (recordPage.value.endDate) {
      queryParams.endDate = recordPage.value.endDate + 'T23:59:59.999Z'
    }
    

    如果用户选择了开始或结束日期,则将其拼接为 ISO 格式字符串(分别为当天的零点和23:59:59),并加入查询参数。这样后端可以根据日期范围筛选账目。

  4. 发送请求

  axios.post(import.meta.env.VITE_API_BASE_URL + '/api/IncomeExpenditureRecord/Query', queryParams, {
    headers: {
      Authorization: localStorage.getItem('token')
    }
  })

这里使用 axios.post 方法向后端接口 /api/IncomeExpenditureRecord/Query 发送 POST 请求。第一个参数是接口地址,通过 import.meta.env.VITE_API_BASE_URL 读取环境变量拼接得到,方便在不同环境下灵活切换 API 地址。第二个参数 queryParams 是前面构建的查询参数对象,包含分页、账本ID和可选的日期范围。第三个参数用于设置请求头,这里主要添加了 Authorization 字段,将本地存储中的 token 作为身份认证信息传递给后端,确保接口安全访问。这样可以保证只有已登录的用户才能查询账目信息,符合常见的权限控制要求。

  1. 处理响应
    .then((res: any) => {
      const response = res.data
      if (response.statusCode === 200) {
        recordPageResponse.pageCount = response.data.pageCount
        recordPageResponse.rowCount = response.data.rowCount
        recordPageResponse.data = response.data.data
      } else {
        ElMessage.error(response.errorMessage)
      }
    }).catch((err: any) => {
      ElMessage.error(err.message)
    })
    

如果接口返回的状态码为 200,表示查询成功,此时会将后端返回的分页信息和账目数据分别赋值给响应式对象 recordPageResponse 的相关属性,页面上的表格和分页器会自动根据最新数据进行刷新;如果状态码不是 200,则通过 ElMessage.error 弹出后端返回的错误信息;如果请求过程中发生异常(如网络错误),同样会弹出错误提示,确保用户能够及时获知操作结果。

4.2 删除账目
const deleteRecord = (id: string) => {
  ElMessageBox.confirm('确定要删除该账目吗?')
    .then(() => axios.delete(...))
    .then(() => queryRecords())
}

这段代码实现了账目删除的功能。当用户点击“删除”按钮时,会先弹出一个确认对话框(ElMessageBox.confirm('确定要删除该账目吗?')),防止误操作。如果用户点击“确定”,则会继续执行后续操作,调用 axios.delete(...) 发送删除请求到后端接口(实际代码中需要传入具体的接口地址和账目ID参数)。删除操作完成后,再调用 queryRecords() 方法,重新查询并刷新账目列表,确保页面数据与后端保持同步。这样可以保证删除操作的安全性和数据的实时更新,提升用户体验。

4.3 打开新增/编辑弹窗
const openAddDialog = () => {
  Object.assign(form, {...})
  dialogTitle.value = '新增账目'
  dialogVisible.value = true
}

const openEditDialog = (row: RecordItem) => {
  Object.assign(form, {...row})
  editId = row.id
  dialogTitle.value = '编辑账目'
  dialogVisible.value = true
}

这两段代码分别用于打开新增和编辑账目的弹窗。openAddDialog 方法会重置表单数据,设置弹窗标题为“新增账目”,并将 dialogVisible 设置为 true,使弹窗显示出来。这里使用 Object.assign(form, {...}) 将表单数据重置为初始状态,确保每次新增时表单都是空的。openEditDialog 方法接收一个账目对象 row,将其数据复制到表单中,设置 editId 为当前编辑的账目 ID,并将弹窗标题改为“编辑账目”。同样通过 dialogVisible 控制弹窗显示。这样用户可以在弹窗中查看和修改现有账目信息,完成编辑操作。

4.4 表单提交
const submitForm = (formEl: FormInstance | undefined) => {
  if (!formEl) return
  formEl.validate().then(() => {
    // 构建请求参数,确保日期格式正确
    const requestData = {
      ...form,
      recordDate: form.recordDate ? new Date(form.recordDate).toISOString() : new Date().toISOString()
    }

    if (editId) {
      // 修改
      const updateData = {
        incomeExpenditureRecordId: editId,
        amount: requestData.amount,
        incomeExpenditureClassificationId: requestData.incomeExpenditureClassificationId,
        accountBookId: requestData.accountBookId,
        recordDate: requestData.recordDate,
        currencyId: requestData.currencyId,
        remark: requestData.remark
      }
      axios.put(import.meta.env.VITE_API_BASE_URL + '/api/IncomeExpenditureRecord/Update', updateData, {
        headers: {
          Authorization: localStorage.getItem('token')
        }
      }).then((res: any) => {
        const response = res.data
        if (response.statusCode === 200) {
          ElMessage.success('修改成功')
          dialogVisible.value = false
          queryRecords()
        } else {
          ElMessage.error(response.errorMessage)
        }
      }).catch((err: any) => {
        ElMessage.error(err.message)
      })
    } else {
      // 新增
      axios.post(import.meta.env.VITE_API_BASE_URL + '/api/IncomeExpenditureRecord/Add', requestData, {
        headers: {
          Authorization: localStorage.getItem('token')
        }
      }).then((res: any) => {
        const response = res.data
        if (response.statusCode === 200) {
          ElMessage.success('新增成功')
          dialogVisible.value = false
          queryRecords()
        } else {
          ElMessage.error(response.errorMessage)
        }
      }).catch((err: any) => {
        ElMessage.error(err.message)
      })
    }
  }).catch(() => {
    ElMessage.error('请检查输入项')
  })
}

这段代码实现了表单的提交逻辑,涵盖了账目的新增和编辑两种场景。首先,通过 formEl.validate() 方法对表单进行校验,确保所有必填项(如金额、日期、分类、货币等)都已正确填写。如果校验未通过,则会自动提示用户检查输入项,阻止后续提交操作。校验通过后,会将表单中的数据组装成请求参数对象 requestData,其中日期字段会被格式化为 ISO 字符串,确保与后端接口的数据格式一致。如果 editId 有值,说明当前是编辑操作。此时会构建包含账目 ID 及其他表单字段的 updateData 对象,并通过 axios.put 方法调用后端的更新接口。请求成功后,弹窗关闭并刷新账目列表,提示“修改成功”;如有错误则弹出错误信息。如果 editId 为空,说明是新增操作。此时直接将 requestData 通过 axios.post 方法提交到新增接口。新增成功后同样关闭弹窗并刷新列表,提示“新增成功”;如有错误则弹出错误信息。在接口请求过程中,如果发生网络异常或后端返回错误,都会通过 ElMessage.error 进行友好提示,确保用户能够及时获知操作结果。

五、小结

通过对记账模块文件的详细分析,我们可以看到该文件在记账应用中承担了重要的功能。它不仅实现了账目的查询、展示、分页、筛选等基本功能,还提供了新增和编辑账目的交互体验。通过 Element Plus 组件库的灵活使用,结合 Vue 的响应式特性,使得整个页面既美观又易用。
在实现过程中,我们还注意到了数据的双向绑定、表单校验、接口调用等关键技术点,这些都是构建现代 Web 应用不可或缺的部分。
希望通过本篇文章的讲解,能够帮助你更好地理解和掌握 Vue.js 和 Element Plus 在实际项目中的应用。如果你对记账应用的其他部分或相关技术有兴趣,欢迎继续关注我们的后续文章,我们将持续分享更多实用的前端开发经验和技巧。
如果你对记账应用的其他部分或相关技术有兴趣,欢迎继续关注我们的后续文章,我们将持续分享更多实用的前端开发经验和技巧。如果你有任何问题或建议,欢迎在评论区留言,我们会尽快回复并与你交流。感谢你的阅读,祝你在前端开发的道路上越走越远!