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.