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

阅读之前

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

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

业务背景

表格中的一对多(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

``` ts
export interface InlineTableConfig {
key: string;
label?: string;
field?: string;
}
```

FieldService.ts

``` 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,
&#039;queryPage&#039;,
{
size: -1
},
{
rsql
}
);
return res?.content || [];
}
}
```

InlineTableConfig.vue

``` html
<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 &#039;@kunlun/dependencies&#039;;
import { OioButton, OioForm, OioFormItem, OioInput } from &#039;@kunlun/vue-ui-antd&#039;;
import { Select as ASelect } from &#039;ant-design-vue&#039;;
import { defineComponent, onMounted, PropType, ref, watch } from &#039;vue&#039;;
import { InlineTableConfig } from &#039;../../typing&#039;;
import { FieldService } from &#039;./service/FieldService&#039;;

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

export default defineComponent({
name: &#039;InlineTableConfig&#039;,
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, &#039;ttype =out= (O2O, O2M, M2O, M2M)&#039;).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

``` ts
import { FormFieldWidget, ModelFieldType, SPI, ViewType, Widget } from &#039;@kunlun/dependencies&#039;;
import InlineTableConfig from &#039;./InlineTableConfig.vue&#039;;

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

@SPI.ClassFactory(
FormFieldWidget.Token({
viewType: ViewType.Form,
ttype: ModelFieldType.String,
widget: &#039;InlineTableConfig&#039;,
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(需新增文件)

``` html
<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 &#039;@kunlun/dependencies&#039;;
import { Table as ATable } from &#039;ant-design-vue&#039;;
import { computed, defineComponent, PropType } from &#039;vue&#039;;
import { InlineTableConfig } from &#039;../../typing&#039;;

export default defineComponent({
name: &#039;InlineTable&#039;,
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

``` ts
import { RowContext, TableListFieldWidget, Widget } from &#039;@kunlun/dependencies&#039;;
import { createVNode, VNode } from &#039;vue&#039;;
import { InlineTableConfig } from &#039;../../typing&#039;;
import InlineTable from &#039;./InlineTable.vue&#039;;

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

``` ts
import { BaseFieldWidget, ModelFieldType, SPI, ViewType } from &#039;@kunlun/dependencies&#039;;
import { AbstractInlineTableFieldWidget } from &#039;./AbstractInlineTableFieldWidget&#039;;

@SPI.ClassFactory(
BaseFieldWidget.Token({
viewType: ViewType.Table,
ttype: ModelFieldType.OneToMany,
widget: &#039;InlineTable&#039;,
multi: true
})
)
export class TableO2mMultiInlineTableFieldWidget extends AbstractInlineTableFieldWidget {}
```

TableM2mMultiInlineTableFieldWidget.ts

``` ts
import { BaseFieldWidget, ModelFieldType, SPI, ViewType } from &#039;@kunlun/dependencies&#039;;
import { AbstractInlineTableFieldWidget } from &#039;./AbstractInlineTableFieldWidget&#039;;

@SPI.ClassFactory(
BaseFieldWidget.Token({
viewType: ViewType.Table,
ttype: ModelFieldType.ManyToMany,
widget: &#039;InlineTable&#039;,
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日 下午4:07
下一篇 2023年11月2日 下午1:58

相关推荐

  • 前端低无一体使用教程

    介绍 客户在使用oinone平台的时候,有一些个性化的前端展示或交互需求,oinone作为开发平台,不可能提前预置好一个跟客户需求一模一样的组件,这个时候我们提供了一个“低无一体”模块,可以反向生成API代码,生成对应的扩展工程和API依赖包,再由专业前端研发人员基于扩展工程(kunlun-sdk),利用API包进行开发并上传至平台,这样就可以在界面设计器的…

    2023年11月6日
    70200
  • 如何在表格的字段内添加动作

    介绍 在日常的业务中,我们经常需要在表格内直接点击动作完成一些操作,而不是只能在操作栏中,例如:订单的表格内点击商品名称或者里面的按钮跳转到商品详情页面,这里我们将带来大家来通过自定义表格字段来实现这个功能。 1.编写表格字段组件 组件ts文件TableBtnFieldWidget.ts import { ActionWidget, ActiveRecord…

    2024年5月16日
    37400
  • 【界面设计器】自定义字段组件基础

    阅读之前 本文档属于高阶实战文档,已假定你了解了所有必读文档中的全部内容,并了解过界面设计器的一些基本操作。 如果在阅读过程中出现的部分概念无法理解,请自行学习相关内容。【前端】文章目录 概述 平台提供的字段组件(以下简称组件)是通过SPI机制进行查找并最终渲染在页面中。虽然平台内置了众多组件,但无法避免的是,对于业务场景复杂多变的实际情况下,我们无法完全提…

    2023年11月1日
    39100
  • TS 结合 Vue 实现动态注册和响应式管理

    基础知识 1: 面向对象 面向对象编程是 JavaScript 中一种重要的编程范式,它帮助开发者通过类和对象组织代码,提高代码的复用性。 2: 装饰器 装饰器在 JavaScript 中是用于包装 class 或方法的高阶函数 为了统一术语,下面的内容会把装饰器讲成注解 在 oinone 平台中,无论是字段还是动作,都是通过 ts + vue 来实现的,t…

    2024年9月21日
    45700
  • 创建与编辑一体化

    在业务操作中,用户通常期望能够在创建页面后立即进行编辑,以减少频繁切换页面的步骤。我们可以充分利用Oinone平台提供的创建与编辑一体化功能,使操作更加高效便捷。 通过拖拽实现表单页面设计 在界面设计器中,我们首先需要设计出对应的页面。完成页面设计后,将需要的动作拖入设计好的页面。这个动作的关键在于支持一个功能,即根据前端传入的数据是否包含id来判断是创建操…

    2023年11月21日
    73600

发表回复

登录后才能评论