vue2的a-table树形结构的合并单元格解决方案

31 阅读4分钟

通用封装表格

<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;
}