一、目标
在Ant Design Vue <a-table>表格中实现余额自动计算,公式为:剩余量 = 库存量 - 消耗量
二、二次开发基础
现有一个使用Ant Design Vue <a-table>表格的开源项目,原始表格有“消耗量”列,且带输入框,数据双向绑定
三、项目结构
stock\Stock.vue ———— 父组件 内容是库存清单
stock\StockAdd.vue ———— 已开发的入库物品列表 可以参考
stock\StockOut.vue ———— 需要二次开发的组件 内容是出库物品列表
四、功能描述
原始功能:在父组件中选择要出库的物品,点击出库,弹出出库物品列表,填写消耗量并提交,然后回到库存清单,库存量变为原库存量减去消耗量后的剩余量
修改后功能:在弹出的物品列表中添加库存量、剩余量两列只读数据,填写消耗量时,剩余量会随之变化,满足剩余量 = 库存量 - 消耗量。
五、遇到的问题
出库物品列表StockOut.vue中<a-table>表格的出库数据,来自传入参数,是从父组件库存清单表中提取的。因为库存清单表中没有消耗量和剩余量数据列,传递过来的对象数组中不含这两个数据项。需要通过map函数遍历数据数组,为每个对象添加 key、consumption、balance 属性,否则剩余量不能一直跟随消耗量变化(只能变化一次)。
this.dataList = this.stockoutData.map(item => {return {...item,key: item.id, // 加入父组件库存列表中不存在的key属性,使用 ID 值作为 keyconsumption: 0, // 加入父组件库存列表中不存在的“消耗量”列balance: item.amount // 加入父组件库存列表中不存在的“剩余量”列}
})
其中,dataList是StockOut.vue本地数组,stockoutData是StockOut.vue组件参数,值由父组件传入。
以下是StockOut.vue的参数定义:
props: {stockoutVisiable: {default: false},stockoutData: {type: Array}},
以下是Stock.vue中参数传递的代码:
<stock-out@close="handleStockoutClose"@success="handleStockoutSuccess":stockoutData="stockout.data":stockoutVisiable="stockout.visiable"></stock-out>
入参stockoutData被赋值为stockout.data,而stockout.data来自所选的行数据this.selectedRows:
// “出库”按键的响应方法outOfWarehouse () {if (!this.selectedRowKeys.length) {this.$message.warning('请选择需要出库的物品')return}let goods = this.selectedRowslet amt = falselet warningwords = '某个物品'goods.forEach(item => {item.max = item.amountif (item.amount === 0) {amt = truewarningwords = item.name}})if (amt) {this.$message.warning(`${warningwords}没有库存!`)return}this.stockout.data = JSON.parse(JSON.stringify(goods))this.stockout.visiable = true},
this.selectedRows的赋值在onSelectChange()函数中进行,onSelectChange()被设为<a-table>组件的响应函数。当选中每行前面的复选框时,this.selectedRows就会同步赋值为所选的行的数据。
<a-table ref="TableInfo":columns="columns":rowKey="record => record.id":dataSource="dataSource":pagination="pagination":loading="loading":rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}":scroll="{ x: 900 }"@change="handleTableChange">......onSelectChange (selectedRowKeys, selectedRows) {selectedRows.forEach(item => {if (item.amount === 0) {this.$message.warning(`${item.name}没有库存!`)return false}})this.selectedRowKeys = selectedRowKeysthis.selectedRows = selectedRows},
由于<a-table>组件是封装好的,想通过老办法console.log()看<a-table>内部的变量值得改源码,读懂源码的难度很高,而通过vue的谷歌浏览器插件Vue.js devtools查看<a-table>内部的变量值就非常简单了。
六、源码
stock\Stock.vue ———— 父组件 内容是库存清单
<template><a-card :bordered="false" class="card-area"><div :class="advanced ? 'search' : null"><!-- 搜索区域 --><a-form layout="horizontal"><a-row :gutter="15"><div :class="advanced ? null: 'fold'"><a-col :md="6" :sm="24"><a-form-itemlabel="物品名称":labelCol="{span: 4}":wrapperCol="{span: 18, offset: 2}"><a-input v-model="queryParams.name"/></a-form-item></a-col><a-col :md="6" :sm="24"><a-form-itemlabel="物品型号":labelCol="{span: 4}":wrapperCol="{span: 18, offset: 2}"><a-input v-model="queryParams.type"/></a-form-item></a-col><a-col :md="6" :sm="24"><a-form-itemlabel="物品类型":labelCol="{span: 4}":wrapperCol="{span: 18, offset: 2}"><a-select v-model="queryParams.typeId" style="width: 100%" allowClear><a-select-option v-for="(item, index) in consumableType" :value="item.id" :key="index">{{ item.name }}</a-select-option></a-select></a-form-item></a-col></div><span style="float: right; margin-top: 3px;"><a-button type="primary" @click="search">查询</a-button><a-button style="margin-left: 8px" @click="reset">重置</a-button></span></a-row></a-form></div><div><div class="operator"><a-button type="primary" ghost @click="warehouse">入库</a-button><a-button type="primary" ghost @click="outOfWarehouse">出库</a-button><!--<a-button @click="batchDelete">删除</a-button>--></div><!-- 表格区域 --><a-table ref="TableInfo":columns="columns":rowKey="record => record.id":dataSource="dataSource":pagination="pagination":loading="loading":rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}":scroll="{ x: 900 }"@change="handleTableChange"><template slot="titleShow" slot-scope="text, record"><template><a-badge status="processing"/><a-tooltip><template slot="title">{{ record.title }}</template>{{ record.title.slice(0, 8) }} ...</a-tooltip></template></template><template slot="contentShow" slot-scope="text, record"><template><a-tooltip><template slot="title">{{ record.content }}</template>{{ record.content.slice(0, 30) }} ...</a-tooltip></template></template><template slot="operation" slot-scope="text, record"><a-icon type="setting" theme="twoTone" twoToneColor="#4a9ff5" @click="edit(record)" title="修 改"></a-icon></template></a-table></div><!-- v-if="stockadd.visiable" StockAdd.vue中的a-drawer 有显示属性:visible="show" 不需要再使用v-if控制其是否显示 --><stock-add@close="handleStockAddClose"@success="handleStockAddSuccess":stockAddVisiable="stockadd.visiable"></stock-add><stock-out@close="handleStockoutClose"@success="handleStockoutSuccess":stockoutData="stockout.data":stockoutVisiable="stockout.visiable"></stock-out></a-card>
</template><script>
import RangeDate from '@/components/datetime/RangeDate'
import {mapState} from 'vuex'
import StockOut from './StockOut'
import StockAdd from './StockAdd'
import moment from 'moment'
moment.locale('zh-cn')export default {name: 'Stock',components: {StockOut, StockAdd, RangeDate},data () {return {advanced: false,stockadd: {visiable: false},stockout: {visiable: false,data: null},requestEdit: {visiable: false},queryParams: {},filteredInfo: null,sortedInfo: null,paginationInfo: null,dataSource: [],selectedRowKeys: [],selectedRows: [],loading: false,pagination: {pageSizeOptions: ['10', '20', '30', '40', '100'],defaultCurrent: 1,defaultPageSize: 10,showQuickJumper: true,showSizeChanger: true,showTotal: (total, range) => `显示 ${range[0]} ~ ${range[1]} 条记录,共 ${total} 条记录`},consumableType: []}},computed: {...mapState({currentUser: state => state.account.user}),columns () {return [{title: 'ID',dataIndex: 'id'}, {title: '物品名称',dataIndex: 'name'}, {title: '型号',dataIndex: 'type',customRender: (text, row, index) => {if (text !== null) {return text} else {return '- -'}}}, {title: '物品数量',dataIndex: 'amount',customRender: (text, row, index) => {if (text !== null) {return text} else {return '- -'}}}, {title: '单位',dataIndex: 'unit',customRender: (text, row, index) => {if (text !== null) {return text} else {return '- -'}}}, {title: '单价',dataIndex: 'price',customRender: (text, row, index) => {if (text !== null) {return '¥' + text.toFixed(2)} else {return '- -'}}}, {title: '总价',dataIndex: 'allPrice',customRender: (text, row, index) => {return '¥' + (row.price * row.amount).toFixed(2)}}, {title: '物品类型',dataIndex: 'consumableType',customRender: (text, row, index) => {if (text !== null) {return <a-tag>{text}</a-tag>} else {return '- -'}}}, {title: '备注',dataIndex: 'content',customRender: (text, row, index) => {if (text !== null) {return text} else {return '- -'}}}, {title: '入库时间',dataIndex: 'createDate',customRender: (text, row, index) => {if (text !== null) {return text} else {return '- -'}}}]}},mounted () {this.fetch()this.getConsumableType()},methods: {getConsumableType () {this.$get('/cos/consumable-type/list').then((r) => {this.consumableType = r.data.data})},onSelectChange (selectedRowKeys, selectedRows) {selectedRows.forEach(item => {if (item.amount === 0) {this.$message.warning(`${item.name}没有库存!`)return false}})this.selectedRowKeys = selectedRowKeysthis.selectedRows = selectedRows},toggleAdvanced () {this.advanced = !this.advanced},// “入库”按键响应warehouse () {this.stockadd.visiable = true},// “出库”按键的响应方法outOfWarehouse () {if (!this.selectedRowKeys.length) {this.$message.warning('请选择需要出库的物品')return}let goods = this.selectedRowslet amt = falselet warningwords = '某个物品'goods.forEach(item => {item.max = item.amountif (item.amount === 0) {amt = truewarningwords = item.name}})if (amt) {this.$message.warning(`${warningwords}没有库存!`)return}this.stockout.data = JSON.parse(JSON.stringify(goods))this.stockout.visiable = true},handleStockAddClose () {this.stockadd.visiable = false},handleStockAddSuccess () {this.stockadd.visiable = falsethis.$message.success('入库成功')this.search()},handleStockoutClose () {this.stockout.visiable = false},handleStockoutSuccess () {this.stockout.visiable = falsethis.selectedRows = []this.selectedRowKeys = []this.$message.success('出库成功')this.search()},handleDeptChange (value) {this.queryParams.deptId = value || ''},batchDelete () {if (!this.selectedRowKeys.length) {this.$message.warning('请选择需要删除的记录')return}let that = thisthis.$confirm({title: '确定删除所选中的记录?',content: '当您点击确定按钮后,这些记录将会被彻底删除',centered: true,onOk () {let ids = that.selectedRowKeys.join(',')that.$delete('/cos/request-type/' + ids).then(() => {that.$message.success('删除成功')that.selectedRowKeys = []that.selectedRows = []that.search()})},onCancel () {that.selectedRowKeys = []that.selectedRows = []}})},search () {let {sortedInfo, filteredInfo} = thislet sortField, sortOrder// 获取当前列的排序和列的过滤规则if (sortedInfo) {sortField = sortedInfo.fieldsortOrder = sortedInfo.order}this.fetch({sortField: sortField,sortOrder: sortOrder,...this.queryParams,...filteredInfo})},reset () {// 取消选中this.selectedRowKeys = []// 重置分页this.$refs.TableInfo.pagination.current = this.pagination.defaultCurrentif (this.paginationInfo) {this.paginationInfo.current = this.pagination.defaultCurrentthis.paginationInfo.pageSize = this.pagination.defaultPageSize}// 重置列过滤器规则this.filteredInfo = null// 重置列排序规则this.sortedInfo = null// 重置查询参数this.queryParams = {}this.fetch()},handleTableChange (pagination, filters, sorter) {// 将这三个参数赋值给Vue data,用于后续使用this.paginationInfo = paginationthis.filteredInfo = filtersthis.sortedInfo = sorterthis.fetch({sortField: sorter.field,sortOrder: sorter.order,...this.queryParams,...filters})},fetch (params = {}) {// 显示loadingthis.loading = trueif (this.paginationInfo) {// 如果分页信息不为空,则设置表格当前第几页,每页条数,并设置查询分页参数this.$refs.TableInfo.pagination.current = this.paginationInfo.currentthis.$refs.TableInfo.pagination.pageSize = this.paginationInfo.pageSizeparams.size = this.paginationInfo.pageSizeparams.current = this.paginationInfo.current} else {// 如果分页信息为空,则设置为默认值params.size = this.pagination.defaultPageSizeparams.current = this.pagination.defaultCurrent}if (params.typeId === undefined) {delete params.typeId}this.$get('/cos/stock-info/page', {...params}).then((r) => {let data = r.data.dataconst pagination = {...this.pagination}pagination.total = data.totalthis.dataSource = data.recordsthis.pagination = pagination// 数据加载完毕,关闭loadingthis.loading = false})}},watch: {}
}
</script>
<style lang="less" scoped>
@import "../../../../static/less/Common";
</style>
stock\StockAdd.vue ———— 已开发的入库物品列表 可以参考
<template><a-drawertitle="物品入库":maskClosable="false"placement="right":closable="false":visible="show":width="1200"@close="onClose"style="height: calc(100% - 55px);overflow: auto;padding-bottom: 53px;"><a-form :form="form" layout="horizontal"><a-row :gutter="32"><a-col :span="12"><a-form-item label='保管人' v-bind="formItemLayout"><a-input v-decorator="['custodian',{ rules: [{ required: true, message: '请输入保管人!' }] }]"/></a-form-item></a-col><a-col :span="12"><a-form-item label='入库人' v-bind="formItemLayout"><a-input v-decorator="['putUser',{ rules: [{ required: true, message: '请输入入库人!' }] }]"/></a-form-item></a-col><a-col :span="24"><a-form-item label='备注消息' v-bind="formItemLayout"><a-textarea :rows="4" v-decorator="['content',{ rules: [{ required: true, message: '请输入名称!' }] }]"/></a-form-item></a-col><a-col :span="24"><a-table :columns="columns" :data-source="dataList"><template slot="nameShow" slot-scope="text, record"><a-input v-model="record.name"></a-input></template><template slot="typeShow" slot-scope="text, record"><a-input v-model="record.type"></a-input></template><template slot="typeIdShow" slot-scope="text, record"><a-select v-model="record.typeId" style="width: 100%"><a-select-option v-for="(item, index) in consumableType" :value="item.id" :key="index">{{ item.name }}</a-select-option></a-select></template><template slot="unitShow" slot-scope="text, record"><a-input v-model="record.unit"></a-input></template><template slot="amountShow" slot-scope="text, record"><a-input-number v-model="record.amount" :min="1" :step="1" :precision="2" @change="handleChange(record)"/></template><template slot="consumptionShow" slot-scope="text, record"><a-input-number v-model="record.consumption" :min="0" :max="record.amount" :step="1" :precision="2" @change="handleChange(record)"/></template><template slot="priceShow" slot-scope="text, record"><a-input-number v-model="record.price" :min="0"/></template></a-table><a-button @click="dataAdd" type="primary" ghost size="large" style="margin-top: 10px;width: 100%">新增物品</a-button></a-col></a-row></a-form><div class="drawer-bootom-button"><a-popconfirm title="确定放弃编辑?" @confirm="onClose" okText="确定" cancelText="取消"><a-button style="margin-right: .8rem">取消</a-button></a-popconfirm><a-button @click="handleSubmit" type="primary" :loading="loading">提交</a-button></div></a-drawer>
</template><script>
import {mapState} from 'vuex'
const formItemLayout = {labelCol: { span: 24 },wrapperCol: { span: 24 }
}
export default {name: 'stockAdd',props: {stockAddVisiable: {default: false}},computed: {...mapState({currentUser: state => state.account.user}),show: {get: function () {return this.stockAddVisiable},set: function () {}},columns () {return [{title: '序号',dataIndex: 'key'}, {title: '物品名称',dataIndex: 'name',scopedSlots: {customRender: 'nameShow'}}, {title: '所属类型',dataIndex: 'typeId',width: 200,scopedSlots: {customRender: 'typeIdShow'}}, {title: '型号',dataIndex: 'type',scopedSlots: {customRender: 'typeShow'}}, {title: '采购量',dataIndex: 'amount',scopedSlots: {customRender: 'amountShow'}}, {title: '消耗量',dataIndex: 'consumption',scopedSlots: {customRender: 'consumptionShow'}}, {title: '剩余量',dataIndex: 'balance'}, {title: '单位',dataIndex: 'unit',scopedSlots: {customRender: 'unitShow'}}, {title: '单价',dataIndex: 'price',scopedSlots: {customRender: 'priceShow'}}]}},mounted () {this.getConsumableType()},data () {return {dataList: [],formItemLayout,form: this.$form.createForm(this),loading: false,consumableType: [],keynumber: 1}},methods: {getConsumableType () {this.$get('/cos/consumable-type/list').then((r) => {this.consumableType = r.data.data})},dataAdd () {this.dataList.push({key: this.keynumber++, name: '', type: '', typeId: '', unit: '', amount: 0, consumption: 0, balance: 0, price: 0})},reset () {this.loading = falsethis.form.resetFields()},onClose () {this.reset()this.$emit('close')},handleChange (record) {record.balance = (record.amount - record.consumption).toFixed(2)},handleSubmit () {let price = 0this.dataList.forEach(item => {price += item.price * item.amount})this.form.validateFields((err, values) => {values.price = pricevalues.goods = JSON.stringify(this.dataList)if (!err) {this.loading = truethis.$post('/cos/stock-info/put', {...values}).then((r) => {this.reset()this.$emit('success')}).catch(() => {this.loading = false})}})}}
}
</script><style scoped></style>
stock\StockOut.vue ———— 需要二次开发的组件 内容是出库物品列表
<template><a-drawer title="出库" :maskClosable="false" placement="right" :closable="false" :visible="show" :width="1200"@close="onClose" style="height: calc(100% - 55px);overflow: auto;padding-bottom: 53px;"><a-form :form="form" layout="vertical"><a-row :gutter="20"><a-col :span="12"><a-form-item label='出库对象' v-bind="formItemLayout"><a-select v-decorator="['userId',{ rules: [{ required: true, message: '请输入出库对象!' }] }]" style="width: 100%"><a-select-option v-for="(item, index) in studentList" :value="item.id" :key="index">{{ item.name}}</a-select-option></a-select></a-form-item></a-col><a-col :span="12"><a-form-item label='保管员' v-bind="formItemLayout"><a-input v-decorator="['custodian',{ rules: [{ required: true, message: '请输入保管员!' }] }]" /></a-form-item></a-col><a-col :span="12"><a-form-item label='经手人' v-bind="formItemLayout"><a-input v-decorator="['handler',{ rules: [{ required: true, message: '请输入经手人!' }] }]" /></a-form-item></a-col><a-col :span="24"><a-table :columns="columns" :data-source="dataList"><template slot="nameShow" slot-scope="text, record"><a-input v-model="record.name"></a-input></template><template slot="typeShow" slot-scope="text, record"><a-input v-model="record.type"></a-input></template><template slot="typeIdShow" slot-scope="text, record"><a-select v-model="record.typeId" style="width: 100%"><a-select-option v-for="(item, index) in consumableType" :value="item.id" :key="index">{{ item.name}}</a-select-option></a-select></template><template slot="unitShow" slot-scope="text, record"><a-input v-model="record.unit"></a-input></template><template slot="consumptionShow" slot-scope="text, record"><a-input-number v-model="record.consumption" :min="0" :max="record.amount" :step="1" :precision="2"@change="handleChange(record)" /></template><template slot="priceShow" slot-scope="text, record"><a-input-number v-model="record.price" :min="1" /></template></a-table></a-col></a-row></a-form><div class="drawer-bootom-button"><a-popconfirm title="确定放弃编辑?" @confirm="onClose" okText="确定" cancelText="取消"><a-button style="margin-right: .8rem">取消</a-button></a-popconfirm><a-button @click="handleSubmit" type="primary" :loading="loading">提交</a-button></div></a-drawer>
</template><script>
import { mapState } from 'vuex'
const formItemLayout = {labelCol: { span: 24 },wrapperCol: { span: 24 }
}
export default {name: 'StockOut',props: {stockoutVisiable: {default: false},stockoutData: {type: Array}},computed: {...mapState({currentUser: state => state.account.user}),show: {get: function () {return this.stockoutVisiable},set: function () {}},columns () {return [{title: '序号',dataIndex: 'key'}, {title: '物品名称',dataIndex: 'name',scopedSlots: { customRender: 'nameShow' }}, {title: '型号',dataIndex: 'type',scopedSlots: { customRender: 'typeShow' }}, {title: '库存量',dataIndex: 'amount'}, {title: '消耗量',dataIndex: 'consumption',scopedSlots: { customRender: 'consumptionShow' }}, {title: '剩余量',dataIndex: 'balance'}, {title: '所属类型',dataIndex: 'typeId',width: 200,scopedSlots: { customRender: 'typeIdShow' }}, {title: '单位',dataIndex: 'unit',scopedSlots: { customRender: 'unitShow' }}, {title: '单价',dataIndex: 'price',scopedSlots: { customRender: 'priceShow' }}]}},data () {return {formItemLayout,form: this.$form.createForm(this),loading: false,dataList: [],consumableType: [],studentList: []}},watch: {stockoutVisiable: function (value) {if (value) {this.dataList = this.stockoutData.map(item => {return {...item,key: item.id, // 加入父组件库存列表中不存在的key属性,使用 ID 值作为 keyconsumption: 0, // 加入父组件库存列表中不存在的“消耗量”列balance: item.amount // 加入父组件库存列表中不存在的“剩余量”列}})}}},mounted () {this.getConsumableType()this.getStudentList()},methods: {getStudentList () {this.$get('/cos/student-info/list').then((r) => {this.studentList = r.data.data})},getConsumableType () {this.$get('/cos/consumable-type/list').then((r) => {this.consumableType = r.data.data})},reset () {this.loading = falsethis.dataList = []this.form.resetFields()},onClose () {this.reset()this.$emit('close')},handleChange (record) {record.balance = (record.amount - record.consumption).toFixed(2)},handleSubmit () {if (this.dataList.length !== 0) {let price = 0this.dataList.forEach(item => {price += item.price * item.amount})this.form.validateFields((err, values) => {if (!err) {values.price = pricevalues.goods = JSON.stringify(this.dataList)this.loading = truethis.$post('/cos/stock-out/stockOut', {...values}).then((r) => {this.reset()this.$emit('success')}).catch(() => {this.loading = false})}})} else {this.$message.warning('添加出库记录')}}}
}
</script><style scoped></style>
七、新的疑问
columns必须是计算属性吗
computed: {...mapState({currentUser: state => state.account.user}),show: {get: function () {return this.stockoutVisiable},set: function () {}},columns () {return [{title: '序号',dataIndex: 'key'}, {title: '物品名称',dataIndex: 'name',scopedSlots: { customRender: 'nameShow' }}, {title: '型号',dataIndex: 'type',scopedSlots: { customRender: 'typeShow' }}, {title: '库存量',dataIndex: 'amount'}, {title: '消耗量',dataIndex: 'consumption',scopedSlots: { customRender: 'consumptionShow' }}, {title: '剩余量',dataIndex: 'balance'}, {title: '所属类型',dataIndex: 'typeId',width: 200,scopedSlots: { customRender: 'typeIdShow' }}, {title: '单位',dataIndex: 'unit',scopedSlots: { customRender: 'unitShow' }}, {title: '单价',dataIndex: 'price',scopedSlots: { customRender: 'priceShow' }}]}},
如何将后台api数据中的id列转为antdesignvue中的key?
AI参考答案:
要将后台 API 数据中的 ID 列转换为 Ant Design Vue 中的 key,你可以按照以下步骤进行操作:
首先,确保你已经使用 Ant Design Vue 创建了表格或其他组件,需要将数据作为数据源进行渲染。
在获取到后台 API 数据后,遍历数据数组中的每个对象。
对于每个对象,将其 ID 值提取出来,并将其赋值给一个名为 "key" 的属性。
最后,将更新后的数据数组作为数据源传递给 Ant Design Vue 的组件进行渲染。
以下是一段示例代码,展示了如何将后台 API 数据中的 ID 列转换为 Ant Design Vue 中的 key:
// 假设你已经从后台 API 获取到了数据数组 const apiData = [{ id: 1, name: 'John', age: 25 },{ id: 2, name: 'Jane', age: 30 },// ... ];// 遍历数据数组,为每个对象添加 key 属性 const transformedData = apiData.map(item => {return {...item,key: item.id, // 使用 ID 值作为 key}; });// 将更新后的数据传递给 Ant Design Vue 的组件进行渲染 <Table :dataSource="transformedData"><!-- 表格列配置 --><a-table-column title="ID" dataIndex="id" key="id" /><a-table-column title="Name" dataIndex="name" key="name" /><a-table-column title="Age" dataIndex="age" key="age" /><!-- 更多列... --> </Table>