自定义表格支持合并或列、表头分组

本文将讲解如何通过自定义实现表格支持单元格合并和表头分组。
自定义表格支持合并或列、表头分组

点击下载对应的代码

在学习该文章之前,你需要先了解:

1: 自定义视图
2: 自定义视图、字段只修改 UI,不修改数据和逻辑
3: 自定义视图动态渲染界面设计器配置的视图、动作


1. 自定义 widget

创建自定义的 MergeTableWidget,用于支持合并单元格和表头分组。

// MergeTableWidget.ts
import { BaseElementWidget, SPI, ViewType, TableWidget, Widget, DslRender } from '@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;
  }

  /**
   * 表格展示字段
   */
  @Widget.Reactive()
  public get currentModelFields() {
    return this.metadataRuntimeContext.model.modelFields.filter((f) => !f.invisible);
  }

  /**
   * 渲染行内动作VNode
   */
  @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;
  }
}

2. 创建对应的 Vue 组件

定义一个支持合并单元格与表头分组的 Vue 组件。

<!-- 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>
    <!-- 渲染界面设计器配置的字段 -->
    <vxe-column
      v-for="field in currentModelFields"
      :key="field.name"
      :field="field.name"
      :title="field.label"
    ></vxe-column>

    <!-- 表头分组  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 }">
        <!-- 渲染界面设计器配置的行内动作 -->
        <row-action-render
          :renderRowActionVNodes="renderRowActionVNodes"
          :row="row"
          :rowIndex="$rowIndex"
          :parentHandle="currentHandle"
        ></row-action-render>
      </template>
    </vxe-column>
  </vxe-table>

  <!-- 分页 -->
  <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 '@kunlun/vue-ui';
import { ActiveRecord, ActiveRecords, ManualWidget, Pagination, RuntimeModelField } from '@kunlun/dependencies';
import { ListPaginationStyle, OioPagination, OioSpin, ReturnPromise } from '@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
    },
    // 表格展示的数据
    showDataSource: {
      type: Array as PropType<ActiveRecord[]>
    },

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

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

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

    // 修改分页
    onPaginationChange: {
      type: Function as PropType<(currentPage: number, pageSize: number) => ReturnPromise<void>>
    },

    // 表格选中
    onCheckedChange: {
      type: Function as PropType<(data: ActiveRecords, event?: CheckedChangeEvent) => void>
    },

    // 表格全选
    onCheckedAllChange: {
      type: Function as PropType<(selected: boolean, data: ActiveRecord[], event?: CheckedChangeEvent) => void>
    },

    // 展示字段
    currentModelFields: {
      type: Array as PropType<RuntimeModelField[]>
    },

    // 渲染行内动作
    renderRowActionVNodes: {
      type: Function as PropType<(row: any) => any>,
      required: true
    }
  },
  setup(props, ctx) {
    /**
     * 单元格合并
     * 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 }
    ]);

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

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

    // 全选
    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>

3. 创建行内动作

<script lang="ts">
import { ActionBar, RowActionBarWidget } from '@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>

4. 注册布局

// registry.ts

import { registerLayout, ViewType } from '@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: '动作名称'
  }
);

通过上述步骤,自定义表格可以实现单元格合并和表头分组功能,同时支持动态渲染界面设计器配置的字段和动作。

Oinone社区 作者:汤乾华原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/20274.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

(0)
汤乾华的头像汤乾华数式员工
上一篇 2025年1月9日 pm5:12
下一篇 2025年1月10日 pm7:57

相关推荐

  • 【前端】IOC容器(v4)

    什么是IOC容器? IOC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合,更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IOC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的使程序的整个体系结构变得非常灵活。在运行期,在外部容器动态的将依赖对象注入组件,当外部容器启动后,外部容器就会初始化。创建并管理对象实例,以及销毁,这种应用本身不负责依赖对象的创建和维护,依赖对象的创建和维护是由外部容器负责的称为控制反转。 IOC(控制反转)和DI(依赖注入) IOC(Inversion of Control, 控制反转):通过外部容器管理对象实例的一种思想。DI(Dependency Injection, 依赖注入):IOC的一种实现方式。 作者简述 IOC是Spring框架(一种以Java为语言开发的框架)的核心,并贯穿始终。其面向接口的开发能力,使得服务调用方和服务提供方可以做到完全解耦,只要遵循接口定义的规则进行调用,具体服务的实现可以是多样化的。 对于前端,我们使用inversify进行了IOC的实现。其强大的解耦能力可以使得平台进行大量的抽象,而无需关系具体的实现。 接下来,我们将介绍IOC在开发中的基本运用。 API 为了方便起见,我们将IOC相关功能与组件SPI的调用方式放在了一起。(更高版本的平台版本将自动获得该能力) export class SPI { /** * register singleton service */ public static Service; /** * autowired service property/parameter in service */ public static Autowired; /** * service construct after execute method */ public static PostConstruct; /** * autowired service in widget */ public static Instantiate; /** * autowired services in widget */ public static Instantiates; /** * service construct after execute method in widget */ public static InstantiatePostConstruct; } 创建第一个服务 service/ProductService.ts import { ServiceIdentifier } from '@kunlun/dependencies'; /** * 产品 */ export interface Product { id: string; name: string; } /** * 产品服务 */ export interface ProductService { /** * 获取产品列表 */ getProducts(): Promise<Product[]>; /** * 通过ID获取产品 * @param id 产品ID */ getProductById(id: string): Promise<Product | undefined>; } /** * 产品服务Token */ export const ProductServiceToken = ServiceIdentifier<ProductService>('ProductService'); service/impl/ProductServiceImpl.ts import { SPI } from '@kunlun/dependencies'; import { Product, ProductService, ProductServiceToken } from '../ProductService'; @SPI.Service(ProductServiceToken) export class ProductServiceImpl implements ProductService { public async getProducts(): Promise<Product[]> { // request api get products return []; } public async getProductById(id:…

    前端 2023年11月1日
    1.1K00
  • 移动端端默认布局模板

    默认布局 表格视图(TABLE) <view type="TABLE"> <view type="SEARCH"> <element widget="search" slot="search" slotSupport="field"> <xslot name="searchFields" slotSupport="field" /> </element> </view> <pack widget="group" class="oio-m-default-view-element" style="height: 100%;flex: 1;overflow-y: hidden;" wrapperStyle="height: 100%;box-sizing:border-box;"> <pack widget="row" style="height: 100%;"> <pack widget="col" mode="full" style="min-height: 234px;height: 100%;"> <element widget="table" slot="table" slotSupport="field" checkbox="false"> <xslot name="fields" slotSupport="field" /> <element widget="rowActions" slot="rowActions" /> </element> </pack> </pack> </pack> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> </view> 表单视图(FORM) <view type="FORM"> <element widget="form" slot="form"> <xslot name="fields" slotSupport="pack,field" /> </element> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> </view> 详情视图(DETAIL) <view type="DETAIL"> <element widget="detail" slot="detail"> <xslot name="fields" slotSupport="pack,field" /> </element> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> </view> 画廊视图(GALLERY) 默认内嵌布局(inline=true) 内嵌表格视图(TABLE) 内嵌表单视图(FORM) 内嵌详情视图(DETAIL) <view type="DETAIL"> <element widget="detail" slot="detail"> <xslot name="fields" slotSupport="pack,field" /> </element> </view>`

    2024年12月11日
    2.5K00
  • 【界面设计器】自定义字段组件实战——表格字段内嵌表格

    阅读之前 此文章为实战教程,已假定你熟悉了【界面设计器】较为完整的【自定义组件】相关内容。 如果在阅读过程中出现的部分概念无法理解,请自行学习相关内容。【前端】文章目录 业务背景 表格中的一对多(O2M)或多对多(M2M)字段使用表格展开。 演示内容:在【商品】的表格中存在【库存信息】这一列,这一列的内容通过表格展示【尺码】和【数量】两列。 业务分析及实现思路 从需求来看,我们需要实现一个【内嵌表格】组件,并且该组件允许在【表格】视图中使用。与之前不同的是,这里我们需要支持两个业务类型一对多(O2M)和多对多(M2M),即一个组件中包含两个元件。 在【内嵌表格】组件的属性面板中,我们需要再定义一个【内嵌表格配置】组件,用来选择内嵌表格中需要哪些字段进行组合,以及为每个组合提供一些基础配置。 这里需要理解一个基本概念,即【内嵌表格】的属性面板是【内嵌表格配置】的【执行页面】。所有组件的属性面板在【执行页面】时都是【表单】视图。 因此我们可以实现一个【内嵌表格配置】组件,并且该组件允许在【表单】视图中使用。其业务类型使用【文本】,我们在保存配置数据时,可以使用JSON数据结构来存储复杂结构。(这里的实现思路并非是最符合协议设定的,但可以满足绝大多数组件场景) 在【内嵌表格配置】组件中,我们可以允许用户添加/移除组合,并且每个组合有两个属性,【标题】和【字段】。 一些解释 看过【界面设计器】自定义字段组件实战——表格字段组合展示文章的读者可能很熟悉这一实现思路,会想当然的尝试将两个组件进行合并。这里我觉得有必要作出一些实现思路上的解释。 虽然在表面上看起来【组合列配置】和【内嵌表格配置】用到的属性完全一样,但在实现上,由于关联关系的查询需要在组件中特殊处理【透出字段(选项字段列表)】字段(【界面设计器】组件开发常见问题中对该属性进行了解释),才能查询到相应的关联数据。 不仅如此,这两个组件所代表的含义也完全不同。【组合列配置】是在一列中配置需要展示的字段,它在未来可能会增加【颜色(根据条件判断展示不同的颜色)】、【动作(可点击的行为)】等等诸多与之相关的属性。【内嵌表格配置】是在一列中配置表格中的多列,它在未来可能会增加【行高(控制表格行高)】、【支持排序(表格列支持排序)】等等诸多与之相关的属性。 在这里希望读者可以理解一点:相似并不代表相关。组件的抽象与归纳整理的不同点在于,抽象更需要关心其本身所代表的含义,而不是仅关注其相似程度。将多个相似度高但含义不同的组件进行归纳整理得到的只是一个含义不明,无法适应变化的组件。 因此,我们仍然使用两个不同的组件进行实现。 准备工作 此处你应该已经准备好了【商品】和【库存】两个模型,并且可以完整执行【商品】模型的全部【增删改查】操作。 业务模型定义(此处模型定义并非业务中正常使用的模型定义,仅作为演示使用) (以下仅展示本文章用到的模型字段,忽略其他无关字段。) 关联字段:-左侧表示当前模型中的字段API名称,右侧表示关联模型中的字段API名称。 商品(Item) 名称 API名称 业务类型 是否多值 长度(单值长度) 关联模型 关联字段 ID id 整数 否 – – – 编码 code 文本 否 128 – – 名称 name 文本 否 128 – – 库存信息 inventoryInfo 一对多 是 – 库存(Inventory) id – itemId 库存(Inventory) 名称 API名称 业务类型 是否多值 长度(单值长度) 关联模型 关联字段 ID id 整数 否 128 – – 商品 item 多对一 否 – 商品(Item) itemId – id 商品ID itemId 整数 否 – – – 尺码 size 文本 否 128 – – 库存 count 整数 否 – – – PS:如果是使用【模型设计器】创建这两个模型,在创建关联关系字段时必须使用双向关联,才能正确建立关联关系。 实现页面效果展示 表格视图 表单视图 库存信息配置 创建组件、元件 准备工作完成后,我们需要根据【业务背景】确定【组件】以及【元件】相关信息,并在【界面设计器】中进行创建。 以下操作过程将省略详细步骤,仅展示可能需要确认的关键页面。 创建内嵌表格组件 创建内嵌表格元件(一对多) 创建内嵌表格元件(多对多) 创建内嵌表格配置组件 创建内嵌表格配置元件 设计内嵌表格元件属性面板 (两个元件的属性面板可以完全一致) 创建tableConfig字段,并切换至【内嵌表格配置】组件。 再拖入【透出字段(选项字段列表)】,并设置为隐藏。 设计内嵌表格配置元件属性面板 启动SDK工程进行组件基本功能开发 开发步骤参考 打开【表格】视图,将【库存信息】字段的组件切换为【内嵌表格】 在属性面板中看到【内嵌表格配置】组件,并优先实现【内嵌表格配置】组件。这里的属性面板就是【内嵌表格配置】组件对应的【执行页面】。 当【内嵌表格配置】组件可以按照预先设计的数据结构正确保存tableConfig属性时,可以在【内嵌表格】组件中的props定义中直接获取该属性,接下来就可以进行【内嵌表格】组件的开发。 代码实现参考 工程结构 typing.ts export interface InlineTableConfig { key: string; label?: string; field?: string; } FieldService.ts import { GenericFunctionService, IModelField, QueryPageResult } from '@kunlun/dependencies'; export class FieldService { private static readonly FIELD_MODEL = 'base.Field'; public static async fetchFieldsByModel(model: string, filter?: string): Promise<IModelField[]> { let rsql = `model == "${model}"`; if (filter) {…

    2023年11月1日
    1.1K00
  • 如何使用位运算的数据字典

    场景举例 日常有很多项目,数据库中都有表示“多选状态标识”的字段。在这里用我们项目中的一个例子进行说明一下: 示例一: 表示某个商家是否支持多种会员卡打折(如有金卡、银卡、其他卡等),项目中的以往的做法是:在每条商家记录中为每种会员卡建立一个标志位字段。如图: 用多字段来表示“多选标识”存在一定的缺点:首先这种设置方式很明显不符合数据库设计第一范式,增加了数据冗余和存储空间。再者,当业务发生变化时,不利于灵活调整。比如,增加了一种新的会员卡类型时,需要在数据表中增加一个新的字段,以适应需求的变化。  – 改进设计:标签位flag设计二进制的“位”本来就有表示状态的作用。可以用各个位来分别表示不同种类的会员卡打折支持:这样,“MEMBERCARD”字段仍采用整型。当某个商家支持金卡打折时,则保存“1(0001)”,支持银卡时,则保存“2(0010)”,两种都支持,则保存“3(0011)”。其他类似。表结构如图: 我们在编写SQL语句时,只需要通过“位”的与运算,就能简单的查询出想要数据。通过这样的处理方式既节省存储空间,查询时又简单方便。 //查询支持金卡打折的商家信息:   select * from factory where MEMBERCARD & b'0001'; // 或者:   select * from factory where MEMBERCARD & 1;    // 查询支持银卡打折的商家信息:   select * from factory where MEMBERCARD & b'0010'; // 或者:   select * from factory where MEMBERCARD & 2; 二进制( 位运算)枚举 可以通过@Dict注解设置数据字典的bit属性或者实现BitEnum接口来标识该枚举值为2的次幂。二进制枚举最大的区别在于值的序列化和反序列化方式是不一样的。 位运算的枚举定义示例 import pro.shushi.pamirs.meta.annotation.Dict; import pro.shushi.pamirs.meta.common.enmu.BitEnum; @Dict(dictionary = ClientTypeEnum.DICTIONARY, displayName = "客户端类型枚举", summary = "客户端类型枚举") public enum ClientTypeEnum implements BitEnum { PC(1L, "PC端", "PC端"), MOBILE(1L << 1, "移动端", "移动端"), ; public static final String DICTIONARY = "base.ClientTypeEnum"; private final Long value; private final String displayName; private final String help; ClientTypeEnum(Long value, String displayName, String help) { this.value = value; this.displayName = displayName; this.help = help; } @Override public Long value() { return value; } @Override public String displayName() { return displayName; } @Override public String help() { return help; } } 使用方法示例 API: addTo 和 removeFrom List<ClientTypeEnum> clientTypes = module.getClientTypes(); // addTo ClientTypeEnum.PC.addTo(clientTypes); // removeFrom ClientTypeEnum.PC.removeFrom(clientTypes); 在查询条件中的使用 List<Menu> moduleMenus = new Menu().queryListByWrapper(menuPage, LoaderUtils.authQuery(wrapper).eq(Menu::getClientTypes, ClientTypeEnum.PC));

    2023年11月24日
    1.8K00
  • 如何在自有前端工程中使用gql请求?

    场景 客户在个性化程度比较高的h5工程中想使用平台的服务端能力,这个时候需要调用后端的gql请求,此时需要引入平台的请求库依赖 npm的package.json中加入依赖 此文档以4.x举例,使用其他版本的请自行修改版本号 "dependencies": { "@kunlun/request": "~4.3.0", "@kunlun/state": "~4.3.0" } 使用示例 import { HttpClient } from ‘@kunlun/request’ const http = HttpClient.getInstance() http.setBaseURL(”) export const login = (data) => { const gqlBody = `mutation { pamirsUserTransientMutation { login(user: {login: “${data.username}”, password: “${data.password}”}) { broken errorMsg errorCode errorField } } }` return http.mutate(‘user’, gqlBody) } 注意事项 开发环境记得配置oinone请求的路由转发规则,这里以vite.config.js举例 import { defineConfig } from ‘vite’ export default defineConfig({ server: { port: 8088, open: true, proxy: { ‘/pamirs’: { changeOrigin: true, // 服务端请求地址 target: ‘http://127.0.0.1:8091’ } } } })

    2023年11月1日
    1.3K00

Leave a Reply

登录后才能评论