通用封装表格
<template>
<div>
<div ref="top" :class="{ 'pt-3 my-search': $slots['searchForm'] || $slots['operationBtn'] }">
<slot name="searchForm"></slot>
<div class="my-3 my-btn" v-if="$slots['operationBtn']">
<slot name="operationBtn"></slot>
</div>
</div>
<a-table
class="mt-3"
v-bind="tableData"
:scroll="{ x: tableWidth, y: tableHeight }"
:bordered="bordered"
:size="size"
:rowKey="(record) => JSON.stringify(record)"
:pagination="
isPage
? {
total: total,
current: mycurrent,
showTotal: (total) => `共 ${total} 条数据`,
showSizeChanger: showSizeChanger,
showQuickJumper: showQuickJumper,
onChange: onPageChange,
onShowSizeChange: onShowSizeChange,
pageSizeOptions: ['10', '20', '50', '100']
}
: false
"
:row-selection="
isSelectRow
? {
selectedRowKeys: selectedRowKeys,
onChange: onSelectChange,
columnWidth: 50,
type: selectRowType,
fixed: isSelectRowFixed
}
: null
"
:loading="isLoading ? $store.getters.loading : false">
<template v-for="(_, name) in $scopedSlots" :slot="name" slot-scope="text, record, index">
<slot :name="name" :text="text" :record="record" :index="index" />
</template>
<template v-for="(_, name) in $slots" :slot="name">
<slot :name="name" />
</template>
</a-table>
<slot name="page"></slot>
</div>
</template>
<script>
export default {
name: "page",
components: {},
props: {
tableData: {
type: Object,
default: function () {
return {
columns: {},
dataSource: []
}
}
},
bordered: {
type: Boolean,
default: false
},
size: {
type: String,
default: "middle"
},
current: {
type: Number,
default: 1
},
total: {
type: Number,
default: 0
},
tableWidth: {
type: [Number, Boolean],
default: false
},
fromHeight: {
type: Number
},
showSizeChanger: {
type: Boolean,
default: true
},
showQuickJumper: {
type: Boolean,
default: true
},
isSelectRow: {
type: Boolean,
default: true
},
selectRowType: {
type: String,
default: "checkbox" // checkbox | radio
},
isSelectRowFixed: {
type: Boolean,
default: false
},
isLoading: {
type: Boolean,
default: true
},
isPage: {
type: Boolean,
default: true
}
},
data() {
return {
tableHeight: 400,
selectedRowKeys: []
}
},
computed: {
mycurrent: {
get: function () {
return this.current
},
set: function (v) {
return v
}
}
},
watch: {
current: {
//深度监听,可监听到对象、数组的变化
handler(val, oldVal) {
console.log(val, oldVal, "123")
this.mycurrent = val
},
deep: true //true 深度监听
}
},
created() {},
mounted() {
this.$nextTick(() => {
if (this.fromHeight) {
this.tableHeight = this.fromHeight
} else {
this.tableHeight = window.innerHeight - 50 - this.$refs.top.offsetHeight - 46 - 160
}
})
},
destroyed() {},
methods: {
onPageChange(page, pageSize) {
this.$emit("on-page-change", { page, pageSize })
},
onShowSizeChange(current, size) {
this.$emit("on-show-size-change", { current, size })
},
onSelectChange(selectedRowKeys, selectedRows) {
// console.log(selectedRowKeys, selectedRows)
this.selectedRowKeys = selectedRowKeys
this.$emit("on-select-change", selectedRows)
}
}
}
</script>
<style scoped></style>
使用方案
- 解决customRender后表头和表格能单独渲染
- 自定义多选框的解决方案
<template>
<div>
<a-modal :title="detailDialog['title']" :visible="detailDialog['visible']" :maskClosable="false" @cancel="resetDetailObj" @ok="onSubmitDetail" width="1000px">
<div>
<my-page :isPage="false" :tableWidth="2000" :table-data="tableData" :isSelectRow="false" :current="tableData['pageConfig']['current']" :total="tableData['pageConfig']['total']" :bordered="true" @on-page-change="onPageChange" @on-show-size-change="onShowSizeChange">
<a-form layout="inline" :label-col="{ span: 7 }" :wrapper-col="{ span: 14 }" :form="tableForm" @submit="handleSubmit" slot="searchForm">
<a-form-item label="交易流水号">
<a-input placeholder="请输入交易流水号" v-decorator="['mercOrderNo']"></a-input>
</a-form-item>
<a-form-item label="子订单号">
<a-input placeholder="请输入子订单号" v-decorator="['childOrderNo']"></a-input>
</a-form-item>
<a-form-item label="交易类型">
<a-select style="width: 150px" v-decorator="['transactionType']" placeholder="请选择">
<a-select-option value="">全部</a-select-option>
<a-select-option value="1">消费</a-select-option>
<a-select-option value="2">退款</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="主单状态">
<a-select style="width: 150px" placeholder="请选择" v-decorator="['mainOrderStatus']">
<a-select-option v-for="item of orderStatus" :key="item.key" :value="item.key" :disabled="item.disabled">{{ item.value }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="分账单号">
<a-input placeholder="请输入分账单号" v-decorator="['splitNo']"></a-input>
</a-form-item>
<a-form-item label="指令结算状态">
<a-select style="width: 150px" v-decorator="['settleStatus']" placeholder="请选择">
<a-select-option value="">全部</a-select-option>
<a-select-option value="1">待送盘</a-select-option>
<a-select-option value="2">结算中</a-select-option>
<a-select-option value="3">结算成功</a-select-option>
<a-select-option value="4">结算失败</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<div class="d-flex justify-content-between py-1">
<a-button type="danger" html-type="submit"> 查询 </a-button>
<a-button class="ml-2" html-type="reset" @click="tableForm.resetFields()"> 重置 </a-button>
</div>
</a-form-item>
</a-form>
<template slot="selectRowTitle"> 头 </template>
<div slot="operationBtn">
<div>
<a-button type="primary" @click="onExport" :loading="isExportLoading"> 导出 </a-button>
<a class="text-primary ml-2" @click="onDownload('sendFilePath')">下载送盘文件</a>
<a class="text-primary ml-2" @click="onDownload('backFilePath')">下载回盘文件</a>
</div>
</div>
<template #page>
<div class="mt-2 text-right">
<a-pagination size="small" :total="tableData['pageConfig']['total']" :pageSize.sync="tableData['pageConfig']['limit']" :current="tableData['pageConfig']['current']" :showTotal="(total) => `共 ${total} 条数据`" :pageSizeOptions="['20', '50', '100']" @change="onPageChange" @showSizeChange="onShowSizeChange" show-size-changer show-quick-jumper />
</div>
</template>
</my-page>
</div>
<template slot="footer">
<a-button @click="resetDetailObj">取消</a-button>
<a-button class="mr-2" type="danger">撤销推送</a-button>
<a-button class="mr-2" type="primary" @click="onSubmitDetail">确认送盘</a-button>
</template>
</a-modal>
</div>
</template>
<script>
import { h } from "vue"
const customOneRender = (text, record) => ({
children: text,
attrs: { rowSpan: record.childOrderRowSpan }
})
const customTwoRender = (text, record) => ({
children: text,
attrs: { rowSpan: record.detailRowSpan }
})
export default {
props: {
rowObj: {
type: Object,
default: () => {}
},
visible: {
type: Boolean,
default: false
}
},
watch: {
visible: {
handler(newVal, oldVal) {
this.detailDialog.visible = newVal
if (newVal === true) {
this.detailDialog.title = `送盘明细(批次号${this.rowObj["batchNo"]})`
this.tableData["pageConfig"]["page"] = 1
this.getOrderStatus()
this.handleSubmit()
this.$nextTick(() => {
this.tableData.columns = this.columns
})
}
},
immediate: true,
deep: true
}
},
computed: {
columns() {
return [
{
title: () => {
return h("a-checkbox", {
props: {
checked: this.isSelectAll
},
on: {
change: (e) => {
this.onCheckboxAll()
}
}
})
},
dataIndex: "selectRow",
align: "center",
customRender: (text, record) => ({
children: h("a-checkbox", {
props: {
checked: record.selectRow
},
on: {
change: (e) => {
record.selectRow = !record.selectRow
this.isAllSelectBox()
}
}
}),
attrs: { rowSpan: record.childOrderRowSpan }
})
},
{
title: "子单号",
dataIndex: "childOrderNo",
align: "center",
customRender: customOneRender
},
{
title: "主单状态",
dataIndex: "mainOrderStatusStr",
align: "center",
customRender: customOneRender
},
{
title: "商品sku",
dataIndex: "productCode",
align: "center",
customRender: customOneRender
},
{
title: "商品名称",
dataIndex: "productName",
align: "center",
customRender: customOneRender
},
{
title: "税率",
dataIndex: "taxRate",
align: "center",
customRender: customOneRender
},
{
title: "商品单价",
dataIndex: "unitPrice",
align: "center",
customRender: customOneRender
},
{
title: "数量",
dataIndex: "productCount",
align: "center",
customRender: customOneRender
},
////
{
title: "交易流水号",
dataIndex: "mercOrderNo",
align: "center",
width: 300,
customRender: customTwoRender
},
{
title: "交易时间",
dataIndex: "transactionTime",
align: "center",
width: 160,
customRender: customTwoRender
},
{
title: "分账类型",
dataIndex: "transactionTypeStr",
align: "center",
customRender: customTwoRender
},
////
{
title: "分账金额",
dataIndex: "splitAmt",
align: "center"
},
{
title: "支付工具",
dataIndex: "payInstrumentStr",
align: "center"
},
{
title: "分账单号",
dataIndex: "splitNo",
align: "center",
width: 400
},
{
title: "指令结算状态",
dataIndex: "settleStatusStr",
align: "center"
}
]
}
},
data() {
return {
detailDialog: {
title: "",
visible: false
},
//table-start
tableForm: this.$form.createForm(this, { name: "xxx_form" }),
selectedRows: [],
tableData: {
columns: this.columns,
dataSource: [],
pageConfig: {
current: 1,
page: 1,
limit: 10,
total: 0
}
},
//table-end
orderStatus: [],
isExportLoading: false,
isSelectAll: false //是否全选
}
},
methods: {
resetDetailObj() {
this.$emit("update:visible", false)
this.$emit("close")
},
onSubmitDetail() {
this.$emit("success")
},
processOrderData(orders) {
const result = []
orders.forEach((order) => {
// 计算子订单详情和拆分明细的总数量
let totalDetailCount = 0
order.childOrderDetail.forEach((detail) => {
totalDetailCount += detail.splitDetail.length
})
// 处理每个子订单详情
order.childOrderDetail.forEach((detail, detailIndex) => {
const splitCount = detail.splitDetail.length
const isFirstDetail = detailIndex === 0
// 处理每个拆分明细
detail.splitDetail.forEach((split, splitIndex) => {
const isFirstSplit = splitIndex === 0
// 生成唯一键
const uniqueKey = `${order.id}_${detailIndex}_${splitIndex}`
// 添加行数据
result.push({
uniqueKey,
// 子订单级别字段
childOrderNo: isFirstDetail && isFirstSplit ? order.childOrderNo : "",
mainOrderStatusStr: isFirstDetail && isFirstSplit ? order.mainOrderStatusStr : "",
productCode: isFirstDetail && isFirstSplit ? order.productCode : "",
productName: isFirstDetail && isFirstSplit ? order.productName : "",
taxRate: isFirstDetail && isFirstSplit ? order.taxRate : "",
unitPrice: isFirstDetail && isFirstSplit ? order.unitPrice : "",
productCount: isFirstDetail && isFirstSplit ? order.productCount : "",
childOrderRowSpan: isFirstDetail && isFirstSplit ? totalDetailCount : 0,
selectRow: false,
// 订单详情级别字段
transactionTypeStr: isFirstSplit ? detail.transactionTypeStr : "",
transactionTime: isFirstSplit ? detail.transactionTime : "",
mercOrderNo: isFirstSplit ? detail.mercOrderNo : "",
detailRowSpan: isFirstSplit ? splitCount : 0,
// 拆分明细级别字段
payInstrumentStr: split.payInstrumentStr,
splitNo: split.splitNo,
settleStatusStr: split.settleStatusStr,
splitAmt: split.splitAmt
})
})
})
})
return result
},
getParamsObj() {
const { mercOrderNo, childOrderNo, transactionType, mainOrderStatus, splitNo, settleStatus } = this.tableForm.getFieldsValue()
return { mercOrderNo, childOrderNo, transactionType, mainOrderStatus, splitNo, settleStatus, settlementId: this.rowObj["id"] }
},
handleSubmit(e) {
// 获取表单查询内容值
if (e) {
this.tableData["pageConfig"]["page"] = 1
e.preventDefault()
}
let params = { ...this.getParamsObj(), limit: this.tableData["pageConfig"]["limit"], page: this.tableData["pageConfig"]["page"] }
this.$until
.httpPost("/oneWalletCommandSettlement/getCommandSettlementDetailList", params, "form")
.then((res) => {
const { status, data } = res
if (status && !this.$until.validatenull(data["list"])) {
const { list, total } = data
this.tableData.dataSource = this.processOrderData(list)
this.tableData["pageConfig"]["total"] = total
return
}
this.tableData.dataSource = []
this.tableData["pageConfig"]["total"] = 0
})
.catch((err) => {
this.tableData.dataSource = []
this.tableData["pageConfig"]["total"] = 0
})
},
onPageChange(page, pageSize) {
this.tableData["pageConfig"]["page"] = page
this.tableData["pageConfig"]["limit"] = pageSize
this.tableData["pageConfig"]["current"] = this.tableData["pageConfig"]["page"]
this.handleSubmit()
},
onShowSizeChange(current, size) {
this.tableData["pageConfig"]["page"] = current
this.tableData["pageConfig"]["limit"] = size
this.tableData["pageConfig"]["current"] = this.tableData["pageConfig"]["page"]
this.handleSubmit()
},
getOrderStatus() {
if (!this.$until.validatenull(this.orderStatus)) {
return
}
this.$axios.post("/common/getOrderStatus").then((res) => {
this.$message.destroy()
if (!res.data.status) {
this.$message.error(res.data.message)
return
}
this.orderStatus = res.data.data
})
},
onExport() {
this.isExportLoading = true
this.$until
.httpPost("/oneWalletCommandSettlement/exportCommandSettlementDetailList", this.getParamsObj(), "form")
.then((res) => {
this.isExportLoading = false
const { status, message } = res
if (status) {
this.$message.success("正在生成表格文件,导出完成后请在导出中心下载。'")
return
}
this.$message.error(message || "操作失败")
})
.catch((err) => {
this.isExportLoading = false
})
},
onDownload(type) {
if (this.$until.validatenull(this.rowObj[type + ""])) {
this.$message.error("无下载资源文件")
return
}
this.$until.downloadByUrl(this.rowObj[type + ""])
},
onCheckboxAll() {
this.isSelectAll = !this.isSelectAll
for (let item of this.tableData.dataSource) {
if (!this.$until.validatenull(item["childOrderNo"])) item["selectRow"] = this.isSelectAll
}
},
isAllSelectBox() {
let isAll = true
for (let item of this.tableData.dataSource) {
if (item["selectRow"] === false && !this.$until.validatenull(item["childOrderNo"])) {
isAll = false
break
}
}
this.isSelectAll = isAll
}
}
}
</script>
<style lang="less" scoped></style>
网络请求后的数据结构(树形)
{
"status": true,
"code": null,
"message": null,
"errorMsg": null,
"requestId": null,
"data": {
"total": 1,
"list": [
{
"unitPrice": 18.00,
"settleStatusParam": null,
"backFilePath": "",
"splitNoParam": null,
"mercOrderNoParam": null,
"childOrderNo": "25072218912379135264A1",
"mainOrderStatusStr": "已退款",
"orderProductId": 3329079,
"productCount": 1,
"productName": "柑橘-优选",
"taxRate": 6.00,
"transactionTypeParam": null,
"productCode": "xx_1745830645347",
"sendFilePath": "http://www.example.com/xxx.zip",
"id": 14,
"mainOrderStatus": 5,
"childOrderDetail": [
{
"transactionType": 1,
"settleStatusParam": null,
"splitDetail": [
{
"payInstrument": "MKTEBP-q1",
"splitNo": "CS202507222036965549541491_FserialNo1753077971412",
"settleStatus": 4,
"settleStatusStr": "结算失败",
"splitAmt": 18.00,
"payInstrumentStr": "额度"
},
{
"payInstrument": "MKTEBP-q1",
"splitNo": "CS202507222036965549541491_FserialNo1753077971412",
"settleStatus": 2,
"settleStatusStr": "结算中",
"splitAmt": 18.00,
"payInstrumentStr": "额度"
}
],
"splitNoParam": null,
"transactionTypeStr": "消费",
"mercOrderNo": "431615250722204144713489",
"transactionTime": "2025-07-22 20:42:18"
},
{
"transactionType": 2,
"settleStatusParam": null,
"splitDetail": [
{
"payInstrument": "MKTEBP-q1",
"splitNo": "CS202507222038374298814579_FserialNo1753077971412",
"settleStatus": 4,
"settleStatusStr": "结算失败",
"splitAmt": 18.00,
"payInstrumentStr": "额度"
},
{
"payInstrument": "MKTEBP-q1",
"splitNo": "CS202507222038374298814579_FserialNo1753077971412",
"settleStatus": 2,
"settleStatusStr": "结算中",
"splitAmt": 18.00,
"payInstrumentStr": "额度"
}
],
"splitNoParam": null,
"transactionTypeStr": "退款",
"mercOrderNo": "s3431533820716",
"transactionTime": "2025-07-23 10:10:07"
},
{
"transactionType": 1,
"settleStatusParam": null,
"splitDetail": [
{
"payInstrument": "MKTEBP-q1",
"splitNo": "CS202507222036965549541491_FserialNo1753077971412",
"settleStatus": 4,
"settleStatusStr": "结算失败",
"splitAmt": 18.00,
"payInstrumentStr": "额度"
},
{
"payInstrument": "MKTEBP-q1",
"splitNo": "CS202507222036965549541491_FserialNo1753077971412",
"settleStatus": 2,
"settleStatusStr": "结算中",
"splitAmt": 18.00,
"payInstrumentStr": "额度"
}
],
"splitNoParam": null,
"transactionTypeStr": "消费",
"mercOrderNo": "431615250722204144713489",
"transactionTime": "2025-07-22 20:42:18"
},
{
"transactionType": 2,
"settleStatusParam": null,
"splitDetail": [
{
"payInstrument": "MKTEBP-q1",
"splitNo": "CS202507222038374298814579_FserialNo1753077971412",
"settleStatus": 4,
"settleStatusStr": "结算失败",
"splitAmt": 18.00,
"payInstrumentStr": "额度"
},
{
"payInstrument": "MKTEBP-q1",
"splitNo": "CS202507222038374298814579_FserialNo1753077971412",
"settleStatus": 2,
"settleStatusStr": "结算中",
"splitAmt": 18.00,
"payInstrumentStr": "额度"
}
],
"splitNoParam": null,
"transactionTypeStr": "退款",
"mercOrderNo": "s3431533820716",
"transactionTime": "2025-07-23 10:10:07"
}
]
}
],
"pageNum": 1,
"pageSize": 10,
"size": 1,
"startRow": 1,
"endRow": 1,
"pages": 1,
"prePage": 0,
"nextPage": 0,
"isFirstPage": true,
"isLastPage": true,
"hasPreviousPage": false,
"hasNextPage": false,
"navigatePages": 8,
"navigatepageNums": [
1
],
"navigateFirstPage": 1,
"navigateLastPage": 1
}
}
validatenull方法
export function validatenull(val) {
if (typeof val == 'boolean') {
return false;
}
if (typeof val == 'number') {
return false;
}
if (val instanceof Array) {
if (val.length == 0) return true;
} else if (val instanceof Object) {
if (JSON.stringify(val) === '{}') return true;
} else {
if (val == 'null' || val == null || val == 'undefined' || val == undefined || val == '') return true;
return false;
}
return false;
}