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

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

点击下载对应的代码

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

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.0K00
  • 自定义视图组件(v4)

    阅读之前 你应该: 了解DSL相关内容。母版-布局-DSL 渲染基础(v4) 了解SPI机制相关内容。组件SPI机制(v4) 什么是视图组件 我们将一个视图中提供数据源的组件称为视图组件。 下面,我们将根据提供的示例布局进行进一步介绍。 示例布局(默认表格视图布局) <view type="TABLE"> <pack widget="group"> <view type="SEARCH"> <element widget="search" slot="search" /> </view> </pack> <pack widget="group" slot="tableGroup"> <element widget="actionBar" slot="actionBar"> <xslot name="actions" /> </element> <element widget="table" slot="table"> <element widget="expandColumn" slot="expandRow" /> <xslot name="fields" /> <element widget="rowActions" slot="rowActions" /> </element> </pack> </view> view: 视图标签;一个视图中的所有组件将共享数据源,视图的数据源通过视图组件进行提供。(在这个示例中,该视图的数据源通过widget="table"(TableWidget)提供) pack: 容器组件标签; element: 通用元素组件标签; xslot:dsl插槽; 根据标签性质,我们可以将这个示例布局进一步简化,只留下我们目前要关注的主要内容。 <view type="TABLE"> <element widget="table" slot="table"> <xslot name="fields" /> </element> </view> 在以上示例布局中,有且仅有一个组件会向视图提供数据源,那就是widget="table"(TableWidget)这个组件。我们接下来将对这个组件进行自定义,以实现业务中所需的列表(List)数据源展示方式。 1 平台组件简介 平台提供的基础组件有如下几种: 组件名称 描述 BaseElement element标签通用组件 BaseElementViewWidget 通用视图组件 BaseElementObjectViewWidget 对象(Object)数据源通用视图组件 BaseElementListViewWidget 列表(List)数据源通用组件 平台提供的内置组件有如下几种:(均使用element标签) 组件名称 标签 视图类型 描述 TableWidget widget="table" TABLE 内置表格组件 FormWidget widget="form" FORM 内置表单组件 DetailWidget widget="detail" DETAIL 内置详情组件 GallertWidget widget="gallery" GALLERY 内置画廊组件 TreeWidget/CardCascaderWidget widget="tree/cardCascader" TREE 内置树/卡片级联组件 我们可以根据业务场景,继承不同的组件,来实现自己的业务场景。在自定义过程中,我们建议尽可能的将逻辑控制在组件内部。如果场景是唯一且确定的,也可以进行一些特殊逻辑处理。 2 场景:实现一个虚拟滚动表格(不使用分页器) 2.1 确定组件名称 widget="VirtualTable" 通过布局设置自定义组件名称 我们将原表格布局中的widget="table"改为我们所需要的自定义组件名称即可。 多个视图可以绑定同一个布局,所以这种修改方式更适用于大范围使用相同布局的情况。 <view type="TABLE"> <pack widget="group"> <view type="SEARCH"> <element widget="search" slot="search /> </view> </pack> <pack widget="group" slot="tableGroup"> <element widget="actionBar" slot="actionBar> <xslot name="actions" /> </element> <element widget="VirtualTable" slot="table"> <element widget="expandColumn" slot="expandRow" /> <xslot name="fields" /> <element widget="rowActions" slot="rowActions" /> </element> </pack> </view> 通过DSL设置自定义组件名称 我们使用了slot="table"这个插槽,通过属性合并覆盖的方式,在DSL上直接指定我们所需要的自定义组件名称即可。 这种修改方式更适用于个别几个视图需要使用该组件的情况。 <view type="TABLE"> <template slot="table" widget="VirtualTable"> …… </template> </view> 2.2 简单实现一个基础功能的虚拟滚动表格 定义一个VirtualTable.vue文件,使用平台提供的oio-table组件。目前内部采用vxe-table封装,相关api文档 点击查看 props定义: showDataSource: 当前展示数据;通过平台内置BaseElementListViewWidget组件提供。 <template> <oio-table ref="table" border show-overflow height="400" :row-config="{ isHover: true…

    2023年11月1日
    1.0K00
  • 如何改变调度策略,让Schedule独立执行线程

    schedule里,相同的taskType跑多个业务任务,如果其中一个任务大量重试占满了调度线程,会影响别的业务任务及时被执行,如下面截图中,taskType用的是平台内置的常量,这个常量会被其他任务也使用,如果当前任务出现了异常占用了这个taskType的所有线程,那么这个taskType下面的其他任务就会被阻塞延后执行。应该给需要业务及时性的任务单独建立自定义的taskType,这样每个taskType的线程就是独立的,A任务异常不会影响B任务的执行。 1、后台创建task type相关的类,继承BaseScheduleNoTransactionTask,要加springbean的注解,参考:task type建议使用类名 2、提交任务的时候,设置tasktype为步骤1的TaskType 3、控制台新增策略和任务bean名称为步骤1的spring beanName,任务名称 $xxx,右边的占位符内容为yml里面配置的ownSign字段任务的名称也是步骤1的 spring beanName 4、配置完成后,控制台启动任务,就可以测试了

    2024年2月20日
    91400
  • 自定义字段的数据联动

    某种情况下,开发人员期望自定以的字段发生变化后,需要修改其他的字段,这篇文章从两个维度来讲解如果处理数据的联动 界面设计器配置 1: 在界面设计器页面中的的组件区域找到自定义的字段,设计元件 2: 在模型区域,搜索提交方式,如果找到了,就把该字段拖拽进来, 如果找不到,就在元件中的组件区域,拖拽一个文本字段,按照下面的配置进行配置,然后保存 图一是找到了对应的字段图二是找不到对应的字段 【图一】 【图二】 图二的字段编码必须是constructDataTrigger 3: 从模型区搜索联动函数,将其拖拽进来 3: 从模型区搜索提交数据,将其拖拽进来4: 从模型区搜索提交字段,将其拖拽进来 5: 发布 (记得刷新页面哦) 最后再到对应的设计器页面,选中该字段,进行配置 提交方式为blur或者change , 需要开发者手动调用该方法 this.blur()或者this.change(value) // 字段对应的ts文件 class MyField extends FormFieldWidget { onChangeValue(val) { // this.change(val) // this.blur() } } 联动函数就是要调用的后端函数 提交数据分为:变更字段 -> 发生变化后的字段当前视图字段 -> 当前视图所有的字段指定字段 -> 指定字段,如果配置的指定字段,那么提交字段的配置就要输入对应的字段 代码配置 平台也支持通过代码的方式修改字段 // 字段对应的ts文件 class MyField extends FormFieldWidget { onChangeValue(val) { // 修改字段本身的值 this.change(val) // 修改其他字段的值 this.formData.otherField = 'value' this.reloadFormData$.subject.next(true); } }

    2023年11月9日
    1.6K00
  • oinone的rsql与传统sql语法对照表

    rsql sql 描述 field01 == "name" field01 = "name" 等于 field01 != "name" field01 != "name" 不等于 field01 =gt= 1 field01 > 1 大于 field01 =ge= 1 field01 >= 1 大于等于 field01 =lt= 1 field01 < 1 小于 field01 =le= 1 field01 <= 1 小于等于 field01 =isnull=true field01 is null 字段为null field01 =notnull= 1 field01 is not null 字段不为null field01 =in= ("foo") field01 in ("foo") 多条件 field01 =out= ("foo") field01 not in ("foo") 不在多条件中 field01 =cole= field02 field01 = field02 字段作为查询参数 field01 =colnt= field02 field01 != field02 字段作为查询参数 field01 =like="foo" field01 like "%foo%" 全模糊匹配,rsql语法中无需拼接通配符”%“ field01 =starts="foo" field01 like "foo%" 前缀模糊匹配,rsql语法中无需拼接通配符”%“ field01 =ends="foo" field01 like “%foo" 后缀模糊匹配,rsql语法中无需拼接通配符”%“ field01 =notlike="foo" field01 not like "%foo%" 全模糊不匹配,rsql语法中无需拼接通配符”%“ field01 =notstarts="foo" field01 not like "foo%" 前缀模糊不匹配,rsql语法中无需拼接通配符”%“ field01 =notends="foo" field01 not like “%foo" 后缀模糊不匹配,rsql语法中无需拼接通配符”%“ field01 =has=(ENUM_NAME1, ENUM_NAME2) 有多值枚举中的几个值 field01 =hasnt=(ENUM_NAME1,ENUM_NAME2) 没有多值枚举中的几个值 field01 =bit=ENUM_NAME1 有二进制枚举中的单个值 field01 =notbit=ENUM_NAME1 没有二进制枚举中的单个值 前端代码中使用工具类拼接rsql 该工具类在oinone的前端基础框架中提供 import { Condition } from '@kunlun/dependencies'; const rsqlCondition = new Condition('field01').equal('foo') .and(new Condition('field02').in(['bar'])) .and(new Condition('field03').notIn(['foo'])) .or(new Condition('field04').greaterThanOrEuqalTo(12)) .or(new Condition('field05').like('foo')) .or(new Condition('field06').notStarts('bar')) .or(new Condition('field07').isNull()) .or(new Condition('field08').notNull()) .and(new Condition('field09').bitEqual('BIT_ENUM_1')) .and(new Condition('field10').bitNotEqual('BIT_ENUM_2')) .and(new Condition('field11').has('ENUM_NAME_1')) .and(new Condition('field12').hasNot(['ENUM_NAME_2', 'ENUM_NAME_3'])); const rsqlStr = rsqlCondition.toString();…

    2023年11月1日
    4.1K00

Leave a Reply

登录后才能评论