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

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

点击下载对应的代码

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

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

相关推荐

  • 「前端」动作API

    概述 在 oinone 前端平台中,提供了四种动作 跳转动作(页面跳转、打开弹窗、抽屉) 服务端动作(调用接口) 客户端动作(返回上一页、关闭弹窗等) 链接动作(打开执行的链接) 快速开始 // 基础使用示例 import { executeViewAction, executeServerAction, executeUrlAction } from '@kunlun/dependencies'; // 示例 1: 基础页面跳转(去创建页面) executeViewAction(action); // 示例 2: 带参数的页面跳转(查询ID为123的数据),去编辑、详情页 executeViewAction(action, undefined, undefined, { id: '123' }); // 示例 3: 页面跳转的参数,用最新的,防止当前页面的参数被带到下一个页面 executeViewAction(action, undefined, undefined, { id: '123' , preserveParameter: true}); // 示例 4: 调用服务端接口 const params = { id: 'xxx', name: 'xxx' }; await executeServerAction(action, params); await executeServerAction(action, params, { maxDepth: 2 }); // 接口数据返回的数据层级是3层 -> 从0开始计算, 默认是2层 // 执行链接动作 executeUrlAction(action); API 详解 executeViewAction 参数名 描述 类型 必填 默认值 — action 视图动作 RuntimeViewAction true router 路由实例 Router false undefined matched 路由匹配参数 Matched false undefined extra 扩展参数 object false {} target 规定在何处打开被链接文档(可参考 a 标签的 target) string false undefined executeServerAction 参数名 描述 类型 必填 默认值 ​action 服务端动作 RuntimeServerAction true param 传递给后端的参数 object true context 配置接口返回的数据层级(默认是两层) {maxDepth: number} false executeUrlAction 参数名 描述 类型 必填 默认值 ​action 链接动作 IURLAction true

    2025年3月21日
    64000
  • 【界面设计器】自定义字段组件实战——表格字段组合展示

    阅读之前 此文章为实战教程,已假定你熟悉了【界面设计器】较为完整的【自定义组件】相关内容。 如果在阅读过程中出现的部分概念无法理解,请自行学习相关内容。【前端】文章目录 业务背景 表格中的一列使用多个字段组合展示。 演示内容:表格中存在两列,【编码】和【基础信息】。将【名称】、【创建时间】、【更新时间】在【基础信息】一列展示。 业务分析及实现思路 从需求来看,我们需要实现一个【组合列】组件,并且该组件允许在【表格】视图中使用。由于【组合列】本身也是一个字段,因此这里需要选择需要组合字段中的其中一个字段作为组件切换的基础字段,比如我们可以选择【名称】字段作为基础字段。 在【组合列】组件的属性面板中,我们需要再自定义一个【组合列配置】组件,用来选择需要将哪些字段进行组合,以及为每个组合提供一些基础配置。 这里需要理解一个基本概念,即【组合列】的属性面板是【组合列配置】的【执行页面】。所有组件的属性面板在【执行页面】时都是【表单】视图。 因此我们可以实现一个【组合列配置】组件,并且该组件允许在【表单】视图中使用。其业务类型使用【文本】,我们在保存配置数据时,可以使用JSON数据结构来存储复杂结构。(这里的实现思路并非是最符合协议设定的,但可以满足绝大多数组件场景) 在【组合列配置】组件中,我们可以允许用户添加/移除组合,并且每个组合有两个属性,【标题】和【字段】。 准备工作 此处你应该已经在某个业务模型下,可以完整执行当前模型的全部【增删改查】操作。 业务模型定义 (以下仅展示本文章用到的模型字段,忽略其他无关字段。) 名称 API名称 业务类型 是否多值 长度(单值长度) 编码 code 文本 否 128 名称 name 文本 否 128 创建时间 createDate 日期时间 否 – 更新时间 updateDate 日期时间 否 – 实现页面效果展示 表格视图 创建组件、元件 准备工作完成后,我们需要根据【业务背景】确定【组件】以及【元件】相关信息,并在【界面设计器】中进行创建。 以下操作过程将省略详细步骤,仅展示可能需要确认的关键页面。 创建组合列组件 创建组合列元件 创建组合列配置组件 创建组合列配置元件 设计组合列元件属性面板 创建compositeConfig字段,并切换至【组合配置】组件。 设计组合列配置元件属性面板 启动SDK工程进行组件基本功能开发 PS:这里由于我们创建了两个组件,因此,将SDK分开下载后,然后将组合列配置SDK中的演示代码(kunlun-plugin/src)移动到组合列SDK中,在同一工程中进行开发,最后只需将相关JS文件和CSS文件上传到组合列组件中即可,组合列配置组件可以不进行上传。这里需要注意的是,上传多个包含相同组件功能的JS文件和CSS文件可能在运行时导致无法正常替换、冲突等问题。 (npm相关操作请自行查看SDK工程中内置的README.MD) 开发步骤参考 打开【表格】视图,将【名称】字段的组件切换为【组合列】组件。 在属性面板中看到【组合列配置】组件,并优先实现【组合列配置】组件。这里的属性面板就是【组合列配置】对应的【执行页面】。 当【组合列配置】组件可以按照预先设计的数据结构正确保存compositeConfig属性时,可以在【组合列】组件中的props定义中直接获取该属性,接下来就可以进行【组合列】组件的开发。 代码实现参考 工程结构 typing.ts export interface CompositeConfig { key: string; label?: string; field?: string; value?: string; } CompositeColumnConfig.vue <template> <div class="composite-column-config"> <oio-form v-for="item in list" :data="item" :key="item.key"> <oio-form-item label="标题" name="label"> <oio-input v-model:value="item.label" /> </oio-form-item> <oio-form-item label="字段" name="field"> <a-select class="oio-select" dropdownClassName="oio-select-dropdown" v-model:value="item.field" :options="fields" /> </oio-form-item> <oio-button type="link" @click="() => removeItem(item)">移除</oio-button> </oio-form> <oio-button type="primary" block @click="addItem">添加</oio-button> </div> </template> <script lang="ts"> import { uniqueKeyGenerator } from '@kunlun/dependencies'; import { WidgetInstance } from '@kunlun/ui-designer-dependencies'; import { OioButton, OioForm, OioFormItem, OioInput } from '@kunlun/vue-ui-antd'; import { Select as ASelect } from 'ant-design-vue'; import { computed, defineComponent, PropType, ref, watch } from 'vue'; import { CompositeConfig } from '../../typing'; export default defineComponent({ name: 'CompositeColumnConfig', components: { OioForm, OioFormItem, OioInput, OioButton, ASelect }, props: { currentInstance: { type:…

    2023年11月1日
    1.0K00
  • 表单字段API

    FormFieldWidget 表单字段的基类,包含了表单字段通用的属性跟方法 示例 class MyFieldClass extends FormFieldWidget{ } 字段属性 属性名 说明 类型 可选值 默认值 value 当前字段的值 any – null formData 当前表单视图的数据 Object – {} rootData 跟视图的数据,如果当前只有一个视图,那么与formData是一样的 Array – [] metadataRuntimeContext 当前视图运行时的上下文,可以获取当前模型、字段、动作、视图等所有的数据 Object – – urlParameters 当前url参数 Object – – field 当前字段的元数据 Object – – model 当前模型 Object – – view 当前视图 Object – – disabled 是否禁用 Boolean – false invisible 当前字段是否不可见 Boolean – false required 当前字段是否必填,如果当前字段是在详情页,那么是false Boolean – false readonly 当前字段是否只读,如果当前字段是在详情页、搜索,那么是false Boolean – false placeholder 占位符 String – 当前字段的displayName label 字段的标题 String – 当前字段的displayName 方法 方法名 说明 参数 例子 getDsl 获取当前字段所有的配置 – change 修改当前字段的值 any focus 获取焦点触发的方法 – blur 失去焦点触发的方法 – executeValidator 执行当前字段的校验,异步的 – submit 重写当前字段的提交逻辑 – submit() { return ‘value’ } reloadActiveRecords 替换当前视图的数据 Array this.reloadActiveRecords([{code: xxx, name: 111}]) reloadRootData 替换根视图的数据 Array this.reloadRootData([{code: xxx, name: 111}])

    2023年11月15日
    1.2K00
  • Excel添加水印功能

    实现ExcelWriteHandlerExtendApi接口从而实现对Excel增加复杂功能的操作。如添加水印。具体实现请自行百度 /** * 根据上下文判断是否执行 * * @param context Excel定义上下文 * @return 是否执行该扩展 */ boolean match(ExcelDefinitionContext context); /** * 构建出一个WriteWorkbook对象,即一个工作簿对象,对应的是一个Excel文件; * * @param builder 可用于设置inMemory=true实现复杂功能(如添加水印) */ default void extendBuilder(ExcelWriterBuilder builder) { } 例:Excel添加水印 本例参考文章:Java使用EasyExcel导出添加水印 实现ExcelWriteHandlerExtendApi接口,并添加@Component注解 添加依赖包 <!– eaysexcel –> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.3.2</version> </dependency> <!– poi 添加水印 –> <dependency> <groupId>org.apache.poi</groupId> <artifactId>ooxml-schemas</artifactId> <version>1.4</version> </dependency> <!– 使用了hutool的工具类 –> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.20</version> </dependency> package pro.shushi.pamirs.top.core.temp; import cn.hutool.core.img.ImgUtil; import com.alibaba.excel.write.builder.ExcelWriterBuilder; import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; import org.apache.poi.openxml4j.opc.PackagePartName; import org.apache.poi.openxml4j.opc.PackageRelationship; import org.apache.poi.openxml4j.opc.TargetMode; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFPictureData; import org.apache.poi.xssf.usermodel.XSSFRelation; import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.stereotype.Component; import pro.shushi.pamirs.file.api.context.ExcelDefinitionContext; import pro.shushi.pamirs.file.api.easyexcel.ExcelWriteHandlerExtendApi; import java.awt.*; import java.awt.image.BufferedImage; @Component public class CustomWaterMarkHandler implements ExcelWriteHandlerExtendApi { private final WaterMark watermark; public CustomWaterMarkHandler() { this.watermark = new WaterMark().setContent("ABC"); } @Override public void extendBuilder(ExcelWriterBuilder builder) { builder.inMemory(true); } @Override public boolean match(ExcelDefinitionContext context) { return DemoTemplate.TEMPLATE_NAME.equals(context.getName()); } @Override public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { try { BufferedImage bufferedImage = createWatermarkImage(); setWaterMarkToExcel((XSSFWorkbook) writeWorkbookHolder.getWorkbook(), bufferedImage); } catch (Exception e) { throw new RuntimeException("添加水印出错",e); } } private BufferedImage createWatermarkImage() { final Font font = watermark.getFont(); final int width = watermark.getWidth(); final int height = watermark.getHeight(); String[] textArray…

    2024年9月6日
    1.6K00
  • GraphQL请求详解(v4)

    阅读之前 什么是GraphQL? Oinone官方解读GraphQL入门 可视化请求工具 insomnia下载 概述 (以下内容简称GQL) 众所周知,平台的所有功能都是通过一系列元数据定义来驱动的,而GQL作为服务端和客户端交互协议,其重要性不言而喻。下面会从以下几个方面介绍GQL在平台中是如何运作的: 服务端定义元数据生成GQL对应的schema 通过HttpClient发起一个GQL请求 通过window.open使用GET方式发起一个GQL请求 客户端泛化调用任意API服务 客户端通过运行时上下文RuntimeContext发起GQL请求 准备工作 在开始介绍GQL之前,我们需要定义一个可以被GQL请求的服务端函数,以此来介绍我们的相关内容。(对服务端不感兴趣的同学可以跳过代码部分) DemoModel.java @Model.model(DemoModel.MODEL_MODEL) @Model(displayName = "演示模型", labelFields = {"name"}) public class DemoModel extends IdModel { private static final long serialVersionUID = -7211802945795866154L; public static final String MODEL_MODEL = "demo.DemoModel"; @Field(displayName = "名称") private String name; @Field(displayName = "是否启用") private Boolean isEnabled; } DemoModelAction.java @Model.model(DemoModel.MODEL_MODEL) public class DemoModelAction { @Action(displayName = "启用") public DemoModel enable(DemoModel data) { data.setIsEnabled(true); data.updateById(); return data; } @Action(displayName = "禁用") public DemoModel disable(DemoModel data) { data.setIsEnabled(false); data.updateById(); return data; } } 上面的java代码定义了演示模型的字段和动作: 字段 field id:ID 整数 Integer name:名称 字符串 String isEnabled:是否启用 布尔 Boolean 动作 action enable:启用 提交动作 ServerAction disable:禁用 提交动作 ServerAction 服务端定义元数据生成GQL对应的schema 模型和字段 type DemoModel { id: Long name: String isEnabled: Boolean } 动作 type DemoModelInput { id: Long name: String isEnabled: Boolean } type DemoModelQuery { queryOne(query: DemoModelInput): DemoModel …… } type DemoModelMutation { enable(data: DemoModelInput): DemoModel disable(data: DemoModelInput): DemoModel …… } PS:平台内置了多种Query和Mutation定义,通过模型继承关系将自动生成,无需手动定义。比如Query定义包括queryOne、queryPage等;Mutation定义包括create、update等。特殊情况下,默认逻辑无法满足时,服务端通常采用函数重载的方式进行替换,客户端则无需关心。 生成规则 type DemoModel:通过模型编码demo.DemoModel取.分隔后的最后一位,并转换为大驼峰格式。字段与声明类型一致。 type DemoModelInput:动作入参定义,未做特殊声明的情况下与模型定义一致。 type DemoModelQuery和type DemoModelMutation:Query和Mutation为固定后缀,分别生成动作相关类型。当函数类型为QUERY时,使用Query后缀,其他情况使用Mutation后缀。 (此处仅做简单解释,详细生成规则过于复杂,客户端无需关心) Query类型的GQL示例 query { demoModelQuery { queryOne(query: { id: ${id} }) { id name isEnabled } } } Mutation类型的GQL示例 mutation…

    2023年11月1日
    2.1K00

Leave a Reply

登录后才能评论