【界面设计器】自定义字段组件实战——表格字段内嵌表格

阅读之前

此文章为实战教程,已假定你熟悉了【界面设计器】较为完整的【自定义组件】相关内容。

如果在阅读过程中出现的部分概念无法理解,请自行学习相关内容。【前端】文章目录

业务背景

表格中的一对多(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:如果是使用【模型设计器】创建这两个模型,在创建关联关系字段时必须使用双向关联,才能正确建立关联关系。

实现页面效果展示

表格视图

image.png

表单视图

image.png

库存信息配置

image.png

创建组件、元件

准备工作完成后,我们需要根据【业务背景】确定【组件】以及【元件】相关信息,并在【界面设计器】中进行创建。

以下操作过程将省略详细步骤,仅展示可能需要确认的关键页面。

创建内嵌表格组件

image.png

创建内嵌表格元件(一对多)

image.png

创建内嵌表格元件(多对多)

image.png

创建内嵌表格配置组件

image.png

创建内嵌表格配置元件

image.png

设计内嵌表格元件属性面板

(两个元件的属性面板可以完全一致)

创建tableConfig字段,并切换至【内嵌表格配置】组件。

image.png

image.png

再拖入【透出字段(选项字段列表)】,并设置为隐藏。

image.png

设计内嵌表格配置元件属性面板

image.png

启动SDK工程进行组件基本功能开发

开发步骤参考

  • 打开【表格】视图,将【库存信息】字段的组件切换为【内嵌表格】
  • 在属性面板中看到【内嵌表格配置】组件,并优先实现【内嵌表格配置】组件。这里的属性面板就是【内嵌表格配置】组件对应的【执行页面】。
  • 当【内嵌表格配置】组件可以按照预先设计的数据结构正确保存tableConfig属性时,可以在【内嵌表格】组件中的props定义中直接获取该属性,接下来就可以进行【内嵌表格】组件的开发。

代码实现参考

工程结构

image.png

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) {
      rsql = `${rsql} and ${filter}`;
    }
    const res = await GenericFunctionService.INSTANCE.simpleExecuteByName<QueryPageResult<IModelField>>(
      FieldService.FIELD_MODEL,
      'queryPage',
      {
        size: -1
      },
      {
        rsql
      }
    );
    return res?.content || [];
  }
}
InlineTableConfig.vue
<template>
  <div class="inline-table-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 { OioButton, OioForm, OioFormItem, OioInput } from '@kunlun/vue-ui-antd';
import { Select as ASelect } from 'ant-design-vue';
import { defineComponent, onMounted, PropType, ref, watch } from 'vue';
import { InlineTableConfig } from '../../typing';
import { FieldService } from './service/FieldService';

interface SelectItem {
  label: string;
  value: string;
}

export default defineComponent({
  name: 'InlineTableConfig',
  components: {
    OioForm,
    OioFormItem,
    OioInput,
    OioButton,
    ASelect
  },
  props: {
    referenceModel: {
      type: String
    },
    value: {
      type: String
    },
    change: {
      type: Function
    },
    setOptionFields: {
      type: Function as PropType<(optionFields: string[]) => void>
    }
  },
  setup(props) {
    const list = ref<InlineTableConfig[]>([]);
    if (props.value) {
      list.value = JSON.parse(props.value);
    }

    const addItem = () => {
      list.value = [...list.value, { key: uniqueKeyGenerator() }];
    };

    const removeItem = (item: InlineTableConfig) => {
      const { key } = item;
      if (!key) {
        return;
      }
      const index = list.value.findIndex((v) => v.key === key);
      if (index >= 0) {
        list.value.splice(index, 1);
      }
    };

    const fields = ref<SelectItem[]>([]);

    onMounted(() => {
      if (props.referenceModel) {
        FieldService.fetchFieldsByModel(props.referenceModel, 'ttype =out= (O2O, O2M, M2O, M2M)').then((res) => {
          fields.value = res.map((v) => ({
            label: v.displayName || v.field!,
            value: v.field!
          }));
        });
      }
    });

    watch(
      list,
      (val) => {
        if (props.referenceModel) {
          props.setOptionFields?.(val.filter((v) => !!v.field).map((v) => v.field) as string[]);
        }
        props.change?.(JSON.stringify(val));
      },
      { deep: true }
    );

    return {
      list,
      addItem,
      removeItem,
      fields
    };
  }
});
</script>
FormStringInlineTableConfigFieldWidget.ts
import { FormFieldWidget, ModelFieldType, SPI, ViewType, Widget } from '@kunlun/dependencies';
import InlineTableConfig from './InlineTableConfig.vue';

interface InternalMetadata {
  modelReferences?: {
    model?: string;
  };
}

@SPI.ClassFactory(
  FormFieldWidget.Token({
    viewType: ViewType.Form,
    ttype: ModelFieldType.String,
    widget: 'InlineTableConfig',
    multi: false
  })
)
export class FormStringInlineTableConfigFieldWidget extends FormFieldWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(InlineTableConfig);
    return this;
  }

  @Widget.Reactive()
  protected get referenceModel(): string | undefined {
    return (this.formData._metadata as InternalMetadata)?.modelReferences?.model;
  }

  @Widget.Method()
  public setOptionFields(optionFields: string[]) {
    this.formData.optionFields = optionFields;
  }
}
InlineTable.vue(需新增文件)
<template>
  <a-table
    class="column-inline-table"
    :data-source="dataSource"
    :columns="columns"
    :pagination="false"
    size="small"
    :scroll="{ y: 140 }"
  />
</template>
<script lang="ts">
import { ActiveRecord } from '@kunlun/dependencies';
import { Table as ATable } from 'ant-design-vue';
import { computed, defineComponent, PropType } from 'vue';
import { InlineTableConfig } from '../../typing';

export default defineComponent({
  name: 'InlineTable',
  inheritAttrs: false,
  components: {
    ATable
  },
  props: {
    dataSource: {
      type: Array as PropType<ActiveRecord[]>
    },
    fields: {
      type: Array as PropType<InlineTableConfig[]>
    }
  },
  setup(props) {
    const dataSource = computed(() => props.dataSource || []);

    const columns = computed(() =>
      (props.fields || []).map((v) => ({
        key: v.key,
        title: v.label,
        dataIndex: v.field
      }))
    );

    return {
      dataSource,
      columns
    };
  }
});
</script>
<style lang="scss">
.column-inline-table.ant-table-wrapper {
  height: 100%;
}
</style>

PS:此处由于平台使用了inline-table的class名称,为了避免冲突,使用column-inline-table

AbstractInlineTableFieldWidget.ts
import { RowContext, TableListFieldWidget, Widget } from '@kunlun/dependencies';
import { createVNode, VNode } from 'vue';
import { InlineTableConfig } from '../../typing';
import InlineTable from './InlineTable.vue';

export class AbstractInlineTableFieldWidget extends TableListFieldWidget {
  @Widget.Reactive()
  protected get tableConfig(): InlineTableConfig[] {
    const { tableConfig } = this.getDsl();
    if (tableConfig) {
      return JSON.parse(tableConfig);
    }
    return [];
  }

  protected getFields() {
    return this.tableConfig.filter((v) => !!v.field);
  }

  @Widget.Method()
  public renderDefaultSlot(context: RowContext): VNode[] | string {
    return [
      createVNode(InlineTable, {
        dataSource: this.compute(context),
        fields: this.getFields()
      })
    ];
  }
}
TableO2mMultiInlineTableFieldWidget.ts
import { BaseFieldWidget, ModelFieldType, SPI, ViewType } from '@kunlun/dependencies';
import { AbstractInlineTableFieldWidget } from './AbstractInlineTableFieldWidget';

@SPI.ClassFactory(
  BaseFieldWidget.Token({
    viewType: ViewType.Table,
    ttype: ModelFieldType.OneToMany,
    widget: 'InlineTable',
    multi: true
  })
)
export class TableO2mMultiInlineTableFieldWidget extends AbstractInlineTableFieldWidget {}
TableM2mMultiInlineTableFieldWidget.ts
import { BaseFieldWidget, ModelFieldType, SPI, ViewType } from '@kunlun/dependencies';
import { AbstractInlineTableFieldWidget } from './AbstractInlineTableFieldWidget';

@SPI.ClassFactory(
  BaseFieldWidget.Token({
    viewType: ViewType.Table,
    ttype: ModelFieldType.ManyToMany,
    widget: 'InlineTable',
    multi: true
  })
)
export class TableM2mMultiInlineTableFieldWidget extends AbstractInlineTableFieldWidget {}

实现效果展示

配置内嵌表格

image.png

表格展示

image.png

内嵌表格属性面板

image.png

Oinone社区 作者:数式-海波原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/59.html

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

(0)
数式-海波的头像数式-海波数式管理员
上一篇 2023年6月20日 pm4:07
下一篇 2023年11月2日 pm1:58

相关推荐

  • 如何通过浏览器开发者工具提高调试效率

    1.通过vue devtool查看vue组件和oinone组件的信息 平台字段、动作、视图组件都是以Widget结尾的,可以通过关键字快速找到组件 安装vue devtool插件 chrome安装最新版的vue devtool插件 谷歌应用商店插件地址,隐藏窗口需要在扩展程序的详情页额外设置才能使用该插件 安装好插件后,可以通过插件选中html页面中的元素查看相关信息 相关特性了解 组件自动创建的vue组件以组件的class类名命名,据此特性可以得出自定义组件的时候,可继承该命名的父类 一般oinone的组件以Widget后缀命名,也推荐大家也以此为后缀,方便在vue调试面板快速看出哪些是oinone组件 选中oinone组件后可以在右侧面板看到相关的组件信息,部分属性介绍 template属性为dsl的配置 activeRecords、formData、rootData、parentData、dataSource等属性为常用数据属性 action为动作的元数据 field为字段的元数据 2.直接在浏览器开发者工具的源码处调试源码 打开浏览器开发者工具,进入标签页源代码/来源(英文版为Source),win系统用快捷键ctrl+O,mac系统用快捷键cmd+O, 可以打开文件搜索面板,通过关键字搜索文件名找到代码文件,可直接在里面debug调试或者查看执行逻辑

    2024年9月9日
    1.4K00
  • 创建与编辑一体化

    在业务操作中,用户通常期望能够在创建页面后立即进行编辑,以减少频繁切换页面的步骤。我们可以充分利用Oinone平台提供的创建与编辑一体化功能,使操作更加高效便捷。 通过拖拽实现表单页面设计 在界面设计器中,我们首先需要设计出对应的页面。完成页面设计后,将需要的动作拖入设计好的页面。这个动作的关键在于支持一个功能,即根据前端传入的数据是否包含id来判断是创建操作还是编辑操作。 动作的属性配置如下: 前端自定义动作 一旦页面配置完成,前端需要对这个动作进行自定义。以下是一个示例的代码: @SPI.ClassFactory( ActionWidget.Token({ actionType: [ActionType.Server], model: '模型', name: '动作的名称' }) ) export class CreateOrUpdateServerActionWidget extends ServerActionWidget { @Widget.Reactive() protected get updateData(): boolean { return true; } } 通过以上步骤,我们实现了一个更智能的操作流程,使用户能够在创建页面的同时进行即时的编辑,从而提高了整体操作效率。这种创建与编辑一体化的功能不仅使操作更加顺畅,同时也为用户提供了更灵活的工作流程。

    2023年11月21日
    1.8K00
  • 左树右表默认选择第一行

    import { BaseElementWidget, Widget, SPI, ViewType, TableSearchTreeWidget } from '@kunlun/dependencies'; @SPI.ClassFactory( BaseElementWidget.Token({ viewType: ViewType.Table, widget: 'tree', model: '改成当前视图的模型' }) ) export class CustomTableSearchTreeWidget extends TableSearchTreeWidget { protected hasExe = false; @Widget.Watch('rootNode.children.length') protected watchRootNode(len) { if (len && !this.hasExe) { this.hasExe = true; const firstChild = this.rootNode?.children?.[0]; if (firstChild) { this.onNodeSelected(firstChild); this.selectedKeys = [firstChild.key]; } } } }

    2024年11月26日
    1.2K00
  • oio-grid 栅格

    24 栅格系统。 <oio-row :gutter="24"> <oio-col :span="12"></oio-col> <oio-col :span="12"></oio-col> </oio-row> 概述 布局的栅格化系统,我们是基于行(row)和列(col)来定义信息区块的外部框架,以保证页面的每个区域能够稳健地排布起来。下面简单介绍一下它的工作原理: 通过\row\在水平方向建立一组\column\(简写 col) 你的内容应当放置于\col\内,并且,只有\col\可以作为\row\的直接元素 栅格系统中的列是指 1 到 24 的值来表示其跨越的范围。例如,三个等宽的列可以使用 \<a-col :span="8" />\ 来创建 如果一个\row\中的\col\总和超过 24,那么多余的\col\会作为一个整体另起一行排列 Flex 布局 我们的栅格化系统支持 Flex 布局,允许子元素在父节点内的水平对齐方式 – 居左、居中、居右、等宽排列、分散排列。子元素与子元素之间,支持顶部对齐、垂直居中对齐、底部对齐的方式。同时,支持使用 order 来定义元素的排列顺序。 Flex 布局是基于 24 栅格来定义每一个『盒子』的宽度,但不拘泥于栅格。 API Row 成员 说明 类型 默认值 align flex 布局下的垂直对齐方式:top middle bottom string top gutter 栅格间隔,可以写成像素值或支持响应式的对象写法来设置水平间隔 { xs: 8, sm: 16, md: 24}。或者使用数组形式同时设置 [水平间距, 垂直间距](1.5.0 后支持)。 number/object/array 0 justify flex 布局下的水平排列方式:start end center space-around space-between string start wrap 是否自动换行 boolean false Col 成员 说明 类型 默认值 版本 flex flex 布局填充 string|number – offset 栅格左侧的间隔格数,间隔内不可以有栅格 number 0 order 栅格顺序,flex 布局模式下有效 number 0 pull 栅格向左移动格数 number 0 push 栅格向右移动格数 number 0 span 栅格占位格数,为 0 时相当于 display: none number – xxxl ≥2000px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – xs <576px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – sm ≥576px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – md ≥768px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – lg ≥992px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – xl ≥1200px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – xxl ≥1600px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object –

    2023年12月18日
    89400
  • 表格字段API

    BaseTableFieldWidget 表格字段的基类. 示例 class MyTableFieldClass extends BaseTableFieldWidget{ } 内置常见的属性 dataSource 当前表格数据 rootData 根视图数据 activeRecords 当前选中行 userPrefer 用户偏好 width 单元格宽度 minWidth 单元格最小宽度 align 内容对齐方式 headerAlign 头部内容对齐方式 metadataRuntimeContext 当前视图运行时的上下文,可以获取当前模型、字段、动作、视图等所有的数据 urlParameters 获取当前的url field 当前字段 详细信息 用来获取当前字段的元数据 model 当前模型 详细信息 用来获取当前模型的元数据 view 当前视图 详细信息 界面设计器配置的视图dsl disabled 是否禁用 详细信息 来源于界面设计器的配置 invisible 当前字段是否不可见 详细信息 来源于界面设计器的配置,true -> 不可见, false -> 可见 required 是否必填 详细信息 来源于界面设计器的配置,如果当前字段是在详情页,那么是false readonly 是否只读 详细信息 来源于界面设计器的配置,如果当前字段是在详情页、搜索,那么是false label 当前字段的标题 详细信息 用来获取当前字段的标题 内置常见的方法 renderDefaultSlot 渲染单元格内容 示例 @Widget.Method() public renderDefaultSlot(context): VNode[] | string { // 当前单元格的数据 const currentValue = this.compute(context) as string[]; return [createVNode('div', { class: 'table-string-tag' }, currentValue)]; } renderHeaderSlot 自定义渲染头部 示例 @Widget.Method() public renderHeaderSlot(context: RowContext): VNode[] | string { const children = [createVNode('span', { class: 'oio-column-header-title' }, this.label)]; return children; } getTableInstance 获取当前表格实例(vxe-table) getDsl 获取界面设计器的配置

    2023年11月16日
    1.1K00

Leave a Reply

登录后才能评论