Oinone
Product
Oinone
Oinone Framework
100% Metadata-Driven Enterprise Low-Code Framework
Aino
Aino
AI Innovation, Now I Know — Enterprise AI Agent Platform
Use CasesPricingCommunity
Resources
📖
Documentation
Developer docs & API reference
💬
Support
Technical support
📄
Changelog
Product release notes
🏡
About
About Us
0571-88757863

Views:Table Column Merging


I. Scenario Overview

This article explains how to implement table cell merging and header grouping through customization.

II. Code Example

Click to download the corresponding code

III. Operation Steps

(Ⅰ) Customize widget

Create a custom MergeTableWidget to support cell merging and header grouping.

// MergeTableWidget.ts
import { BaseElementWidget, SPI, ViewType, TableWidget, Widget, DslRender } from '@oinone/kunlun-dependencies';
import MergeTable from './MergeTable.vue';

@SPI.ClassFactory(
  BaseElementWidget.Token({
    viewType: ViewType.Table,
    widget: 'MergeTableWidget'
  })
)
  export class MergeTableWidget extends TableWidget {
    public initialize(props) {
      super.initialize(props);
      this.setComponent(MergeTable);
      return this;
    }

    /**
   * Table display fields
   */
    @Widget.Reactive()
    public get currentModelFields() {
      return this.metadataRuntimeContext.model.modelFields.filter((f) => !f.invisible);
    }

    /**
   * Render in-row action VNodes
   */
    @Widget.Method()
    protected renderRowActionVNodes() {
      const table = this.metadataRuntimeContext.viewDsl!;

      const rowAction = table?.widgets.find((w) => w.slot === 'rowActions');
      if (rowAction) {
        return rowAction.widgets.map((w) => DslRender.render(w));
      }

      return null;
    }
  }

(Ⅱ) Create Corresponding Vue Component

Define a Vue component that supports cell merging and header grouping.

<!-- MergeTable.vue -->
<template>
  <vxe-table
    border
    height="500"
    :column-config="{ resizable: true }"
    :merge-cells="mergeCells"
    :data="showDataSource"
    @checkbox-change="checkboxChange"
    @checkbox-all="checkedAllChange"
    >
    <vxe-column type="checkbox" width="50"></vxe-column>
    <!-- Render fields configured in the interface designer -->
    <vxe-column
      v-for="field in currentModelFields"
      :key="field.name"
      :field="field.name"
      :title="field.label"
      ></vxe-column>
    <!-- Header grouping  https://vxetable.cn/v4.6/#/table/base/group -->
    <vxe-colgroup title="更多信息">
      <vxe-column field="role" title="Role"></vxe-column>
      <vxe-colgroup title="详细信息">
        <vxe-column field="sex" title="Sex"></vxe-column>
        <vxe-column field="age" title="Age"></vxe-column>
      </vxe-colgroup>
    </vxe-colgroup>
    <vxe-column title="操作" width="120">
      <template #default="{ row, $rowIndex }">
        <!-- Render in-row actions configured in the interface designer -->
        <row-action-render
          :renderRowActionVNodes="renderRowActionVNodes"
          :row="row"
          :rowIndex="$rowIndex"
          :parentHandle="currentHandle"
          ></row-action-render>
      </template>
    </vxe-column>
  </vxe-table>
  <!-- Pagination -->
  <oio-pagination
    :pageSizeOptions="pageSizeOptions"
    :currentPage="pagination.current"
    :pageSize="pagination.pageSize"
    :total="pagination.total"
    show-total
    :showJumper="paginationStyle != ListPaginationStyle.SIMPLE"
    :showLastPage="paginationStyle != ListPaginationStyle.SIMPLE"
    :onChange="onPaginationChange"
    ></oio-pagination>
</template>
<script lang="ts">
  import { defineComponent, PropType, ref } from 'vue';
  import { CheckedChangeEvent } from '@oinone/kunlun-vue-ui';
  import { ActiveRecord, ActiveRecords, ManualWidget, Pagination, RuntimeModelField } from '@oinone/kunlun-dependencies';
  import { ListPaginationStyle, OioPagination, OioSpin, ReturnPromise } from '@oinone/kunlun-vue-ui-antd';
  import RowActionRender from './RowActionRender.vue';

  export default defineComponent({
    mixins: [ManualWidget],
    components: {
      OioSpin,
      OioPagination,
      RowActionRender
    },
    inheritAttrs: false,
    props: {
      currentHandle: {
        type: String,
        required: true
      },
      // loading
      loading: {
        type: Boolean,
        default: undefined
      },
      // Table display data
      showDataSource: {
        type: Array as PropType<ActiveRecord[]>
          },

      // Pagination
      pagination: {
        type: Object as PropType<Pagination>,
        required: true
      },

      pageSizeOptions: {
        type: Array as PropType<(number | string)[]>,
          required: true
          },

      paginationStyle: {
        type: String as PropType<ListPaginationStyle>
          },

      // Modify pagination
      onPaginationChange: {
        type: Function as PropType<(currentPage: number, pageSize: number) => ReturnPromise<void>>
          },

          // Table selection
          onCheckedChange: {
            type: Function as PropType<(data: ActiveRecords, event?: CheckedChangeEvent) => void>
              },

              // Table full selection
              onCheckedAllChange: {
                type: Function as PropType<(selected: boolean, data: ActiveRecord[], event?: CheckedChangeEvent) => void>
                  },

                  // Display fields
                  currentModelFields: {
                    type: Array as PropType<RuntimeModelField[]>
                  },

                  // Render in-row actions
                  renderRowActionVNodes: {
                    type: Function as PropType<(row: any) => any>,
        required: true
      }
    },
    setup(props, ctx) {
      /**
     * Cell merging
     * https://vxetable.cn/v4.6/#/table/advanced/span
     */
      const mergeCells = ref([
        { row: 1, col: 1, rowspan: 3, colspan: 3 },
        { row: 5, col: 0, rowspan: 2, colspan: 2 }
      ]);

      // Single selection
      const checkboxChange = (e) => {
        const { checked, record, records } = e;
        const event: CheckedChangeEvent = {
          checked,
          record,
          records,
          origin: e
        };

        props.onCheckedChange?.(records, event);
      };

      // Full selection
      const checkedAllChange = (e) => {
        const { checked, record, records } = e;
        const event: CheckedChangeEvent = {
          checked,
          record,
          records,
          origin: e
        };

        props.onCheckedAllChange?.(checked, records, event);
      };

      return {
        mergeCells,
        ListPaginationStyle,
        checkboxChange,
        checkedAllChange
      };
    }
  });
</script>
<style lang="scss"></style>

(Ⅲ) Create In-Row Actions

<script lang="ts">
  import { ActionBar, RowActionBarWidget } from '@oinone/kunlun-dependencies';
  import { debounce } from 'lodash-es';
  import { createVNode, defineComponent } from 'vue';

  export default defineComponent({
    inheritAttrs: false,
    props: {
      row: {
        type: Object,
        required: true
      },
      rowIndex: {
        type: Number,
        required: true
      },
      renderRowActionVNodes: {
        type: Function,
        required: true
      },
      parentHandle: {
        type: String,
        required: true
      }
    },
    render() {
      const vnode = this.renderRowActionVNodes();

      return createVNode(
        ActionBar,
        {
          widget: 'rowAction',
          parentHandle: this.parentHandle,
          inline: true,
          activeRecords: this.row,
          rowIndex: this.rowIndex,
          key: this.rowIndex,
          refreshWidgetRecord: debounce((widget?: RowActionBarWidget) => {
            if (widget) {
              widget.setCurrentActiveRecords(this.row);
            }
          })
        },
        {
          default: () => vnode
        }
      );
    }
  });
</script>

(Ⅳ) Register Layout

// registry.ts

import { registerLayout, ViewType } from '@oinone/kunlun-dependencies';

registerLayout(
  `<view type="TABLE">
    <pack widget="group">
        <view type="SEARCH">
            <element widget="search" slot="search" slotSupport="field">
                <xslot name="searchFields" slotSupport="field" />
            </element>
        </view>
    </pack>
    <pack widget="group" slot="tableGroup">
        <element widget="actionBar" slot="actionBar" slotSupport="action">
            <xslot name="actions" slotSupport="action" />
        </element>
        <element widget="MergeTableWidget" slot="table" slotSupport="field">
            <element widget="expandColumn" slot="expandRow" />
            <xslot name="fields" slotSupport="field" />
            <element widget="rowActions" slot="rowActions" slotSupport="action" />
        </element>
    </pack>
</view>`,
  {
    model: '模型',
    viewType: ViewType.Table,
    actionName: '动作名称'
  }
);

Through the above steps, the custom table can achieve cell merging and header grouping functions, while supporting dynamic rendering of fields and actions configured in the interface designer.

Edit this page
Last Updated:1/15/26, 4:02 AM
Prev
Views:Implementing Multiple View Switching
Next
Views:Table Column Footer Statistics
默认页脚
Copyright © 2026 Mr.Hope