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

打造高效对账单管理组件:Vue3 + Element Plus 实现客户账单与单据选择

在企业管理系统中,客户对账和单据选择是财务和业务管理的核心功能。本文分享一个基于 Vue3Element Plus 的对账单管理组件,包含父组件(对账单表单 StatementForm.vue)和子组件(单据选择弹窗 DeliverySelect.vue),实现客户信息录入、单据选择和对账数据管理。代码清晰、模块化、易于复用,非常适合团队开发或社区分享。以下是完整的实现思路和代码。

功能亮点

  • 客户选择:通过弹窗选择客户,自动填充客户名称和ID。
  • 单据选择:支持按销售单号、出库单号、规格型号和出库时间搜索出库单据,支持分页和跨页多选。
  • 动态对账:支持录入本次对账数量、其他费用,实时计算含税总额和对账总额。
  • 只读模式:支持查看、编辑和新建模式,灵活适配不同场景。
  • 用户友好:界面美观,操作流畅,支持表单验证和数据重置。

实现思路

  1. 父组件(StatementForm.vue)
    • 使用 v-dialog 创建弹窗表单,集成 el-form 和 el-table。
    • 通过 el-input-number 动态录入对账数量和其他费用,自动计算含税总额。
    • 调用 statementService 接口获取对账单详情或保存数据。
    • 通过 ref 调用子组件 DeliverySelect 实现单据选择。
  2. 子组件(DeliverySelect.vue)
    • 使用 v-dialog 创建单据选择弹窗,集成 el-form 和 el-table。
    • 支持按条件(销售单号、出库单号、规格型号、出库时间)搜索单据。
    • 使用 vxe-pager 实现分页,跨页记忆选中的单据数据。
    • 通过事件将选中的单据数据传递给父组件。

完整代码

父组件:StatementForm.vue

<template><v-dialog:title="title":close-on-click-modal="false"v-model="visible"width="1200px"><el-form:model="inputForm"ref="inputForm"v-loading="loading":class="method === 'view' ? 'readonly' : ''":disabled="method === 'view'"label-width="120px"><el-row :gutter="15"><el-col :span="12"><el-form-item label="客户名称" prop="clientName" :rules="[]"><el-inputv-model="inputForm.clientName"placeholder="请选择客户名称":readonly="true"><template #append><el-button @click="showSelectDialog" icon="Search" /></template></el-input><ClientSelectsref="clientSelectsInfo"class="w100"@selected="handleSelected"/></el-form-item></el-col><el-col :span="24"><el-buttontype="primary"@click="toOpenProduct"v-if="method !== 'view'"class="mab20">选择单据</el-button><el-table:data="dataList"v-loading="loading"ref="gridTable"height="420px"><el-table-columntype="index"label="序号"width="80px"align="center"fixed="left"/><el-table-columnprop="deliveryNo"header-align="center"align="center"label="出库单号"width="160"fixed="left"/><el-table-columnprop="productCode"header-align="center"align="center"label="产品编码"width="160"fixed="left"/><el-table-columnprop="productName"header-align="center"align="center"label="产品名称"fixed="left"/><el-table-columnprop="specificationsName"header-align="center"align="center"label="规格型号"fixed="left"/><el-table-columnprop="unitName"header-align="center"align="center"label="单位"/><el-table-columnprop="quantity"header-align="center"align="center"label="入库数量"/><el-table-columnprop="unreconciledQuantity"header-align="center"align="center"label="未对账"/><el-table-columnprop="reconciledQuantity"header-align="center"align="center"label="本次对账"width="200"><template #default="{ row }"><span v-if="method == 'view'">{{row.reconciledQuantity || 0}}</span><el-input-numberv-else:precision="2"v-model.number="row.reconciledQuantity":step="0.01":min="0"@change="handleTaxPrice(row)"placeholder="0.00":value-on-clear="0"/></template></el-table-column><el-table-columnprop="taxRate"header-align="center"align="center"label="税率"><template #default="{ row }"> {{ row.taxRate }}% </template></el-table-column><el-table-columnprop="taxIncludedPrice"header-align="center"align="center"label="含税单价"/><el-table-columnprop="taxIncludedTotal"header-align="center"align="center"label="含税总额"><template #default="{ row }">{{ (row.taxIncludedTotal || 0).toFixed(4) }}</template></el-table-column><el-table-columnprop="otherAmount"header-align="center"align="center"label="其他费用"width="200"><template #default="{ row }"><span v-if="method == 'view'">{{ row.otherAmount || 0 }}</span><el-input-numberv-else:precision="4"v-model.number="row.otherAmount":step="0.0001"@change="handleTaxPrice(row)"placeholder="0.0000":value-on-clear="0"/></template></el-table-column><el-table-columnprop="remark"header-align="center"align="center"label="备注信息"width="200"><template #default="{ row }"><span v-if="method == 'view'">{{ row.remark }}</span><el-input v-model="row.remark"></el-input></template></el-table-column><el-table-column label="操作" fixed="right" v-if="method != 'view'"><template #default="{ $index }"><el-buttontype="danger"texticon="del-filled"@click="delRow($index)">移除</el-button></template></el-table-column></el-table><div class="count-footer"><div>对账总数:<span style="color: #409eff">{{checkingCount.toFixed(2)}}</span></div><div>含税总额:<span>{{ checkingAmount.toFixed(2) }}</span></div><div>对账总额:<span>{{(checkingAmount + checkingOther).toFixed(2)}}</span></div></div></el-col></el-row></el-form><template #footer><span class="dialog-footer"><el-button @click="visible = false" icon="circle-close">关闭</el-button><el-buttontype="primary"v-if="method != 'view'"@click="doSubmit()"icon="circle-check"v-noMoreClick>确定</el-button></span></template><DeliverySelectref="deliverySelectRef"@handleSelectProduct="handleSelectProduct":clientSupplierId="inputForm.clientId"/></v-dialog>
</template><script>
import OfficeSelect from "@/components/officeSelect";
import ClientSelects from "@/components/clientSelects";
import statementService from "@/api/crm/client/statementService";
import DeliverySelect from "@/components/deliverySelect";export default {data() {return {title: "",method: "",visible: false,loading: false,inputForm: {id: "",statementNo: "",invoiceNo: "",invoiceType: "",clientId: "",clientName: "",statementQuantity: "",statementTotal: "",attachmentUrl: "",statementStatus: "",remark: "",createByName: "",office: {id: "",},},dataList: [],};},computed: {checkingCount() {let count = 0;this.dataList.forEach((item) => {count += item.reconciledQuantity || 0;});return count;},checkingAmount() {let count = 0;this.dataList.forEach((item) => {count += item.taxIncludedTotal || 0;});return count;},checkingOther() {let count = 0;this.dataList.forEach((item) => {count += item.otherAmount || 0;});return count;},},components: {OfficeSelect,ClientSelects,DeliverySelect,},created() {},methods: {init(method, id) {this.method = method;this.inputForm.id = id;this.dataList = [];if (method === "add") {this.title = `新建应收对账`;} else if (method === "edit") {this.title = "修改应收对账";} else if (method === "view") {this.title = "查看应收对账";}this.visible = true;this.loading = false;this.$nextTick(() => {this.$refs.inputForm.resetFields();if (method === "edit" || method === "view") {this.loading = true;statementService.fetchById(this.inputForm.id).then((data) => {this.inputForm = this.recover(this.inputForm, data);this.dataList = data.details.map((item) => ({...item,reconciledQuantity: item.reconciledQuantity || 0,otherAmount: item.otherAmount || 0,taxIncludedTotal: item.taxIncludedTotal || 0,taxRate: parseFloat(item.taxRate),}));this.loading = false;});}});},doSubmit() {this.$refs["inputForm"].validate((valid) => {if (valid) {this.loading = true;this.inputForm.statementQuantity = this.checkingCount.toFixed(2);this.inputForm.statementTotal = (this.checkingAmount + this.checkingOther).toFixed(2);statementService.saveStatement({ ...this.inputForm, details: this.dataList }).then((data) => {this.visible = false;this.$message.success(data);this.$emit("refreshDataList");this.loading = false;}).catch(() => {this.loading = false;});}});},showSelectDialog() {this.$refs.clientSelectsInfo.initselect(this.inputForm.clientName);},handleSelected(selectedRow) {this.inputForm.clientName = selectedRow.name;this.inputForm.clientId = selectedRow.id;},toOpenProduct() {if (!this.inputForm.clientName) {this.$message.warning("请先选择客户");return;}this.$refs.deliverySelectRef.init(this.inputForm.clientId);},handleSelectProduct(data) {data.forEach((item) => {if (!this.dataList.some((s) => s.id === item.id)) {this.dataList.push({...item,taxRate: parseFloat(item.taxRate) * 100,taxIncludedTotal: 0,reconciledQuantity: 0,otherAmount: 0,});}});},delRow(i) {this.dataList.splice(i, 1);},handleTaxPrice(row) {row.reconciledQuantity = row.reconciledQuantity || 0;row.otherAmount = row.otherAmount || 0;if (row.reconciledQuantity > (row.unreconciledQuantity || 0)) {this.$message.warning("本次对账不能大于未对账!");row.reconciledQuantity = 0;return;}row.taxIncludedTotal = (row.taxIncludedPrice || 0) * (row.reconciledQuantity || 0);},},
};
</script>
<style scoped>
.mab20 {margin-bottom: 20px;
}
.count-footer {display: flex;justify-content: flex-end;font-size: 16px;margin-top: 16px;
}
.count-footer span {color: red;font-weight: bold;margin-right: 20px;
}
</style>

子组件:DeliverySelect.vue

<template><v-dialogtitle="选择单据":close-on-click-modal="false":append-to-body="true"width="1000px"class="unitGridDialog"v-model="visible"><el-form:inline="true"ref="searchForm":model="searchForms"class="query-form"@keyup.enter="refreshList()"@submit.prevent><el-form-item label="销售单号:" prop="orderNo"><el-inputv-model="searchForms.orderNo"placeholder="请输入销售单号"></el-input></el-form-item><el-form-item label="出库单号:" prop="deliveryNo"><el-inputv-model="searchForms.deliveryNo"placeholder="请输入出库单号"></el-input></el-form-item><el-form-item label="规格型号:" prop="specificationsName"><el-inputv-model="searchForms.specificationsName"placeholder="请输入规格型号"></el-input></el-form-item><el-form-item prop="deliveryDate" label="出库时间:"><el-date-pickerv-model="searchForms.deliveryDate"type="daterange"align="right"value-format="YYYY-MM-DD HH:mm:ss"unlink-panelsrange-separator="至"start-placeholder="开始日期"end-placeholder="结束日期"clearable:default-time="defaultTime"></el-date-picker></el-form-item><el-form-item><el-buttontype="primary"@click="refreshList()"icon="search":loading="loading">查询</el-button><el-button type="default" @click="resetSearch()" icon="refresh-right">重置</el-button></el-form-item></el-form><el-table:data="dataList"ref="productGridTable"border="inner"auto-resizeresizable:loading="loading"@selection-change="handleSelectionChange"><el-table-column type="selection" width="55" align="center" /><el-table-columnlabel="序号"width="60"align="center"type="index"></el-table-column><el-table-columnprop="deliveryNo"header-align="center"align="center"width="180"show-overflow-tooltiplabel="出库单号"></el-table-column><el-table-columnprop="deliveryDate"header-align="center"align="center"width="180"show-overflow-tooltiplabel="出库时间"></el-table-column><el-table-columnprop="orderNo"header-align="center"align="center"width="180"show-overflow-tooltiplabel="销售单号"></el-table-column><el-table-columnprop="productCode"header-align="center"align="center"width="160"show-overflow-tooltiplabel="产品编号"></el-table-column><el-table-columnprop="productName"header-align="center"align="center"show-overflow-tooltiplabel="产品名称"></el-table-column><el-table-columnprop="specificationsName"header-align="center"align="center"show-overflow-tooltiplabel="规格型号"></el-table-column><el-table-columnprop="unitName"header-align="center"align="center"show-overflow-tooltiplabel="单位"></el-table-column><el-table-columnprop="quantity"header-align="center"align="center"show-overflow-tooltiplabel="出库数量"></el-table-column><el-table-columnprop="unreconciledQuantity"header-align="center"align="center"show-overflow-tooltiplabel="未勾稽"></el-table-column></el-table><vxe-pagerbackgroundsize="small":current-page="tablePage.currentPage":page-size="tablePage.pageSize":total="tablePage.total":page-sizes="[10, 20, 100, 1000, { label: '全量数据', value: 1000000 }]":layouts="['PrevPage','JumpNumber','NextPage','FullJump','Sizes','Total',]"@page-change="currentChangeHandle"></vxe-pager><template #footer><span class="dialog-footer"><el-button @click="visible = false" icon="el-icon-circle-close">关闭</el-button><el-buttontype="primary"icon="el-icon-circle-check"@click="doSubmit()">确定</el-button></span></template></v-dialog>
</template><script>
import statementService from "@/api/crm/client/statementService";
export default {data() {return {idKey: "id",dataListAllSelections: [],visible: false,loading: false,searchForms: {deliveryNo: "",orderNo: "",specificationsName: "",deliveryDate: "",},dataList: [],selectRow: {},tablePage: {total: 0,currentPage: 1,pageSize: 10,},method: "",};},props: {clientSupplierId: {type: String,default: () => {return "";},},},methods: {init(method = "") {this.method = method;this.visible = true;this.$nextTick(() => {this.dataListAllSelections = [];this.resetSearch();});},handleSelectionChange(selection) {this.selectRow = selection;this.$nextTick(() => {this.changePageCoreRecordData();});},changePageCoreRecordData() {let idKey = this.idKey;let that = this;if (this.dataListAllSelections.length <= 0) {this.selectRow.forEach((row) => {that.dataListAllSelections.push(row);});return;}let selectAllIds = [];this.dataListAllSelections.forEach((row) => {selectAllIds.push(row[idKey]);});let selectIds = [];this.selectRow.forEach((row) => {selectIds.push(row[idKey]);if (selectAllIds.indexOf(row[idKey]) < 0) {that.dataListAllSelections.push(row);}});let noSelectIds = [];this.dataList.forEach((row) => {if (selectIds.indexOf(row[idKey]) < 0) {noSelectIds.push(row[idKey]);}});noSelectIds.forEach((id) => {if (selectAllIds.indexOf(id) >= 0) {for (let i = 0; i < that.dataListAllSelections.length; i++) {if (that.dataListAllSelections[i][idKey] === id) {that.dataListAllSelections.splice(i, 1);break;}}}});},del(tag) {this.dataListAllSelections.splice(this.dataListAllSelections.indexOf(tag),1);this.$nextTick(() => {this.setSelectRow();});},setSelectRow() {if (!this.dataListAllSelections ||this.dataListAllSelections.length <= 0) {this.$refs.productGridTable.clearSelection();return;}let idKey = this.idKey;let selectAllIds = [];this.dataListAllSelections.forEach((row) => {selectAllIds.push(row[idKey]);});this.$refs.productGridTable.clearSelection();for (var i = 0; i < this.dataList.length; i++) {if (selectAllIds.indexOf(this.dataList[i][idKey]) >= 0) {this.$refs.productGridTable.toggleRowSelection(this.dataList[i],true);}}},refreshList() {this.loading = true;statementService.fetchDeliveryList({current: this.tablePage.currentPage,size: this.tablePage.pageSize,clientSupplierId: this.clientSupplierId,beginDeliveryDate:this.searchForms.deliveryDate && this.searchForms.deliveryDate[0],endDeliveryDate:this.searchForms.deliveryDate && this.searchForms.deliveryDate[1],...this.lodash.omit(this.searchForms, "deliveryDate"),}).then((data) => {this.dataList = data.records;this.tablePage.total = data.total;this.loading = false;this.$nextTick(() => {this.setSelectRow();});});},currentChangeHandle({ currentPage, pageSize }) {this.tablePage.currentPage = currentPage;this.tablePage.pageSize = pageSize;this.refreshList();this.$nextTick(() => {this.changePageCoreRecordData();});},resetSearch() {this.$refs.searchForm.resetFields();this.refreshList();},doSubmit() {this.visible = false;this.$emit("handleSelectProduct", this.dataListAllSelections);},},
};
</script>
<style lang="scss">
.unitGridDialog {.el-dialog__body {padding: 10px 0px 0px 10px;color: #606266;font-size: 14px;word-break: break-all;}.el-pagination {margin-top: 5px;margin-bottom: 4px;}.el-row {margin-right: 20px;}.tag-box {display: flex;margin: 10px 0;flex-wrap: wrap;min-width: fit-content;}
}
</style>

使用方法

  1. 安装依赖
    • 确保项目已安装 Vue3、Element Plus、vxe-table 和 lodash。
    • 示例命令:

      bash

      npm install vue@3 element-plus vxe-table lodash
    • 将 ClientSelects 和 OfficeSelect 替换为实际的客户选择和组织选择组件,或根据需求实现。
  2. 配置服务接口
    • 创建 statementService 服务,路径为 @/api/crm/client/statementService,定义以下接口:
      • fetchById(id):获取对账单详情,返回包含 id、statementNo、clientId、clientName、statementQuantity、statementTotal 和 details 的对象。
      • fetchDeliveryList(params):获取出库单据列表,返回分页数据(包含 records 和 total)。
      • saveStatement(data):保存对账单数据,返回成功提示。
    • 示例接口实现(伪代码)
  3. // @/api/crm/client/statementService.js
    import axios from 'axios';export default {fetchById(id) {return axios.get(`/api/statement/${id}`);},fetchDeliveryList(params) {return axios.get('/api/delivery/list', { params });},saveStatement(data) {return axios.post('/api/statement/save', data);},
    };

    组件集成

  4. 在父组件中引入 StatementForm.vue,通过 init(method, id) 方法触发弹窗(method 为 add、edit 或 view)。
  5. 示例调用:
  6. <template><StatementForm ref="statementForm" @refreshDataList="refreshList" /><el-button @click="$refs.statementForm.init('add')">新建对账单</el-button>
    </template>

    注意事项

  7. 确保接口返回的字段与组件中使用的字段一致(如 deliveryNo、reconciledQuantity 等)。
  8. 如果需要调整表格样式,可修改 scoped 样式部分。
  9. 跨页多选功能依赖 dataListAllSelections 数组,确保分页切换时正确记忆选中数据。
  10. 验证规则(:rules="[]") 可根据需求添加,例如:
:rules="[{ required: true, message: '客户名称不能为空', trigger: 'change' }]"

优化建议

  • 性能优化:为搜索请求添加防抖或节流,减少大数据量场景下的接口调用。
  • 扩展功能:支持批量导入对账单明细,或添加导出 Excel 功能。
  • 国际化:将硬编码的中文(如“新建应收对账”)提取到 i18n 配置文件。
  • 错误处理:增强接口错误提示,显示详细的错误信息。

总结

这个对账单管理组件结合 Vue3Element Plus,实现了高效的客户对账和单据选择功能。代码结构清晰、模块化,易于维护和扩展,适合企业级 CRM 或 ERP 系统。无论是前端开发者还是团队协作,这套代码都能快速集成到项目中。欢迎大家 fork、star 或留言讨论优化方案!快来试试,打造属于你的高效对账系统吧!

http://www.dtcms.com/a/387592.html

相关文章:

  • 第二章 Arm C1-Premium Core技术架构
  • Bartender 6 多功能菜单栏管理(Mac)
  • 嵌入式科普(38)C语言预编译X-Macros深度分析和实际项目代码分享
  • Docker compose 与 docker swarm 的区别
  • 【嵌入式硬件实例】-555定时器实现水位检测
  • AbMole小课堂丨R-spondin-1(RSPO1):高活性Wnt通路激活剂,如何在多种类器官/干细胞培养中发挥重要功能
  • 【C语言代码】打印九九乘法口诀表
  • vue3和element plus, node和express实现大文件上传, 分片上传,断点续传完整开发代码
  • electron-egg使用ThinkPHP项目指南
  • 温州工业自动化科技工厂如何实现1台服务器10个研发设计同时用
  • 如何用PM2托管静态文件
  • Java程序设计:基本数据类型
  • 在k8s环境下部署kanboard项目管理平台
  • 为什么 MySQL utf8 存不下 Emoji?utf8mb4 实战演示
  • 2025 年 PHP 常见面试题整理以及对应答案和代码示例
  • (二十五)、在 k8s 中部署证书,为网站增加https安全认证
  • 风机巡检目前有什么新技术?
  • 震坤行工业超市开放平台接口实战:工业品精准检索与详情解析全方案
  • 河南萌新联赛2025第(八)场:南阳理工学院
  • docker回收和mysql备份导入导致数据丢失恢复---惜分飞
  • 「Memene 摸鱼日报 2025.9.17」上海张江人工智能创新小镇正式启动,华为 DCP 技术获网络顶会奖项
  • 【数据结构】顺序表,ArrayList
  • 第十二章 Arm C1-Premium GIC CPU接口详解
  • 【数据结构---并查集】(并查集的原理,实现与应用)
  • 【数据结构-KMP算法(学习篇)】
  • Start application catch exception
  • 机器视觉在半导体封装检测中的应用
  • 雅菲奥朗SRE知识墙分享(九):『变更管理的定义与实践』
  • 51c视觉~3D~合集6
  • webRTC 的协议族