【界面设计器】自定义字段组件实战——表格字段组合展示

阅读之前

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

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

业务背景

表格中的一列使用多个字段组合展示。

演示内容:表格中存在两列,【编码】和【基础信息】。将【名称】、【创建时间】、【更新时间】在【基础信息】一列展示。

业务分析及实现思路

从需求来看,我们需要实现一个【组合列】组件,并且该组件允许在【表格】视图中使用。由于【组合列】本身也是一个字段,因此这里需要选择需要组合字段中的其中一个字段作为组件切换的基础字段,比如我们可以选择【名称】字段作为基础字段。

在【组合列】组件的属性面板中,我们需要再自定义一个【组合列配置】组件,用来选择需要将哪些字段进行组合,以及为每个组合提供一些基础配置。

这里需要理解一个基本概念,即【组合列】的属性面板是【组合列配置】的【执行页面】。所有组件的属性面板在【执行页面】时都是【表单】视图。

因此我们可以实现一个【组合列配置】组件,并且该组件允许在【表单】视图中使用。其业务类型使用【文本】,我们在保存配置数据时,可以使用JSON数据结构来存储复杂结构。(这里的实现思路并非是最符合协议设定的,但可以满足绝大多数组件场景)

在【组合列配置】组件中,我们可以允许用户添加/移除组合,并且每个组合有两个属性,【标题】和【字段】。

准备工作

此处你应该已经在某个业务模型下,可以完整执行当前模型的全部【增删改查】操作。

业务模型定义

(以下仅展示本文章用到的模型字段,忽略其他无关字段。)

名称 API名称 业务类型 是否多值 长度(单值长度)
编码 code 文本 128
名称 name 文本 128
创建时间 createDate 日期时间 -
更新时间 updateDate 日期时间 -

实现页面效果展示

表格视图

image.png

创建组件、元件

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

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

创建组合列组件

image.png

创建组合列元件

image.png

创建组合列配置组件

image.png

创建组合列配置元件

image.png

设计组合列元件属性面板

创建compositeConfig字段,并切换至【组合配置】组件。

image.png

image.png

设计组合列配置元件属性面板

image.png

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

PS:这里由于我们创建了两个组件,因此,将SDK分开下载后,然后将组合列配置SDK中的演示代码(kunlun-plugin/src)移动到组合列SDK中,在同一工程中进行开发,最后只需将相关JS文件CSS文件上传到组合列组件中即可,组合列配置组件可以不进行上传。这里需要注意的是,上传多个包含相同组件功能的JS文件和CSS文件可能在运行时导致无法正常替换、冲突等问题。

(npm相关操作请自行查看SDK工程中内置的README.MD)

开发步骤参考

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

代码实现参考

工程结构

image.png

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: Object as PropType<WidgetInstance>
    },
    value: {
      type: String
    },
    change: {
      type: Function
    }
  },
  setup(props) {
    const list = ref<CompositeConfig[]>([]);
    if (props.value) {
      list.value = JSON.parse(props.value);
    }

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

    const removeItem = (item: CompositeConfig) => {
      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 = computed(() => {
      return Array.from(props.currentInstance?.root?.fieldCollection.values() || []).map((v) => {
        return {
          label: v.element?.widgetData?.displayName,
          value: v.element?.name
        };
      });
    });

    watch(
      list,
      (val) => {
        props.change?.(JSON.stringify(val));
      },
      { deep: true }
    );

    return {
      list,
      addItem,
      removeItem,
      fields
    };
  }
});
</script>
FormStringCompositeColumnConfigFieldWidget.ts
import { FormFieldWidget, ModelFieldType, SPI, ViewType, Widget } from '@kunlun/dependencies';
import { WidgetInstance } from '@kunlun/ui-designer-dependencies';
import CompositeColumnConfig from './CompositeColumnConfig.vue';

@SPI.ClassFactory(
  FormFieldWidget.Token({
    viewType: ViewType.Form,
    ttype: ModelFieldType.String,
    widget: 'CompositeColumnConfig',
    multi: false
  })
)
export class FormStringCompositeColumnConfigFieldWidget extends FormFieldWidget {
  @Widget.Reactive()
  @Widget.Inject()
  protected currentInstance: WidgetInstance | undefined;

  public initialize(props) {
    super.initialize(props);
    this.setComponent(CompositeColumnConfig);
    return this;
  }
}
CompositeColumn.vue(需新增文件)
<template>
  <div class="composite-column-wrapper">
    <div class="composite-field" v-for="item in fields" :key="item.key">
      <span>{{ item.label }}</span>
      <span class="composite-colon">:</span>
      <span>{{ item.value }}</span>
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { CompositeConfig } from '../../typing';

export default defineComponent({
  name: 'CompositeColumn',
  inheritAttrs: false,
  props: {
    fields: {
      type: Array as PropType<CompositeConfig[]>
    }
  }
});
</script>
<style lang="scss">
.composite-column-wrapper {
  display: flex;
  flex-direction: column;
  row-gap: 4px;

  .composite-field {
    & > .composite-colon {
      margin-left: 2px;
      margin-right: 4px;
    }
  }
}
</style>
TableStringCompositeColumnFieldWidget.ts
import {
  BaseFieldWidget,
  BaseTableFieldWidget,
  ModelFieldType,
  RowContext,
  SPI,
  ViewType,
  Widget
} from '@kunlun/dependencies';
import { toString } from 'lodash-es';
import { createVNode, VNode } from 'vue';
import { CompositeConfig } from '../../typing';
import CompositeColumn from './CompositeColumn.vue';

@SPI.ClassFactory(
  BaseFieldWidget.Token({
    viewType: ViewType.Table,
    ttype: ModelFieldType.String,
    widget: 'CompositeColumn',
    multi: false
  })
)
export class TableStringCompositeColumnFieldWidget extends BaseTableFieldWidget {
  @Widget.Reactive()
  protected get compositeConfig(): CompositeConfig[] {
    const { compositeConfig } = this.getDsl();
    if (compositeConfig) {
      return JSON.parse(compositeConfig);
    }
    return [];
  }

  protected getFields(context: RowContext) {
    return this.compositeConfig
      .filter((v) => !!v.field)
      .map((v) => {
        return {
          ...v,
          value: toString(context.data[v.field!])
        };
      });
  }

  @Widget.Method()
  public renderDefaultSlot(context: RowContext): VNode[] | string {
    return [
      createVNode(CompositeColumn, {
        fields: this.getFields(context)
      })
    ];
  }
}

实现效果展示

配置组合列

image.png

表格展示

image.png

组合列属性面板

image.png

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

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

Like (0)
数式-海波's avatar数式-海波数式管理员
Previous 2023年6月20日 pm4:07
Next 2023年11月2日 pm1:58

相关推荐

  • 【前端】工程结构最佳实践(v4)

    阅读之前 你应该: 了解node与npm相关内容 了解lerna包管理工具的相关内容 官方文档 了解git仓库的相关内容 了解rollup的相关内容 工程结构包示例 Vue项目结构包下载 工程结构详解 工程结构 ├── packages │   ├── kunlun-boot │   │   ├── package.json │   │   ├── public │   │   │   ├── favicon.ico │   │   │   └── index.html │   │   ├── src │   │   │   ├── main.ts │   │   │   └── shim-vue.d.ts │   │   ├── tsconfig.json │   │   └── vue.config.js │   ├── kunlun-module-demo │   │   ├── scripts │   │   │   ├── postpublish.js │   │   │   └── prepublish-only.js │   │   ├── src │   │   │   ├── index.ts │   │   │   └── shim-vue.d.ts │   │   ├── index.ts │   │   ├── package.json │   │   ├── rollup.config.js │   │   └── tsconfig.json │   └── kunlun-modules-demo │   ├── scripts │   │   ├── build.config.js │   │   ├── postpublish.js │   │   └── prepublish-only.js │   ├── packages │   │   ├── module-demo1 │   │   │   ├── index.ts │   │   │   ├── package.json │   │   │   ├── rollup.config.js │   │   │   └── src │   │   │   ├── index.ts │   │   │   └── shim-vue.d.ts │   │   ├── module-demo2 │   │   │   ├── index.ts │   │   │   ├── package.json │   │   │   ├── rollup.config.js │   │   │  …

    前端 2023年11月1日
    1.5K00
  • 前端 SPI 注册 + 渲染

    在阅读本篇文章之前,您需要学习以下知识点: 1: TS 结合 Vue 实现动态注册和响应式管理 前端开发者在使用 oinone 平台的时候会发现,不管是自定义字段还是视图,对应的 typescript 都会用到@SPI.ClassFactory(参数),然后在对用的class中重写initialize方法`: @SPI.ClassFactory(参数) export class CustomClass extends xxx { public initialize(props) { super.initialize(props); this.setComponent(FormString); return this; } } 本文将带您熟悉 oinone 前端的 SPI 注册机制以及 TS + Vue 的渲染过程。 不管是自定义字段还是视图@SPI.ClassFactory(参数)都是固定写法,区别在于参数不同,这篇文章里面详细描述了参数的定义。 SPI 注册机制 有自定义过字段、视图经验的开发者可能会发现,字段(表单字段)SPI 注册用的是FormFieldWidget.Token生成对应的参数,视图 SPI 注册用的是BaseElementWidget.Token,那么为什么要这样定义呢? 大家可以想象成现在有一个大的房子,房子里面有很多房间,每个房间都有自己的名字,比如FormFieldWidget就是房间的名字,BaseElementWidget也是房间的名字,这样一来我们就可以根据不同的房间存放不同的东西。 下面给大家展示下伪代码实现: class SPI { static container = new Map<string, WeakMap<object, object>>() static ClassFactory(token) { return (target) => { if(!SPI.container.get(token.type)) { SPI.container.set(token.type, new WeakMap()) } const services = SPI.container.get(token.type) services?.set(token, target) } } } class FormFieldWidget { static Token(options) { return { …options, type: 'Field' } } static Selector(options) { const fieldWidgets = SPI.container.get('Field') if(fieldWidgets) { return fieldWidgets.get(options)! } return null } } @SPI.ClassFactory(FormFieldWidget.Token({ viewType: 'Form', ttype: 'String', widget: 'Input' })) class StringWidget { } // 字段元数据 const fieldMeta = { name: "name", field: "name", mode: 'demo.model', widget: 'Input', ttype: 'String', viewType: 'Form' } // 找到对应的widget const widget = FormFieldWidget.Selector({ viewType: fieldMeta.viewType, ttype: fieldMeta.ttype, widget: fieldMeta.widget, }) 在上述代码中,我们主要是做了这么写事情: 1.SPI class class SPI { static container = new Map<string, WeakMap<object, object>>() } SPI 类是一个静态类,用于管理服务的注册和获取。 container 是一个静态属性,类型是 Map,它的键是字符串,值是 WeakMap。这个结构允许我们为每个服务类型(例如,Field)管理多个服务实例。 2.ClassFactory 方法 static ClassFactory(token) { return (target) => { if…

    2024年9月26日
    1.9K00
  • 前端元数据介绍

    模型 属性名 类型 描述 id string 模型id model string 模型编码 name string 技术名称 modelFields RuntimeModelField[] 模型字段 modelActions RuntimeAction[] 模型动作 type ModelType 模型类型 module string 模块编码 moduleName string 模块名称 moduleDefinition RuntimeModule 模块定义 pks string[] 主键 uniques string[][] 唯一键 indexes string[][] 索引 sorting string 排序 label string 显示标题 labelFields string[] 标题字段 模型字段 属性名 类型 描述 model string 模型编码 modelName string 模型名称 data string 属性名称 name string API名称 ttype ModelFieldType 字段业务类型 multi boolean (可选) 是否多值 store boolean 是否存储 displayName string (可选) 字段显示名称 label string (可选) 字段页面显示名称(优先于displayName) required boolean | string (可选) 必填规则 readonly boolean | string (可选) 只读规则 invisible boolean | string (可选) 隐藏规则 disabled boolean | string (可选) 禁用规则 字段业务类型 字段类型 值 描述 String ‘STRING’ 文本 Text ‘TEXT’ 多行文本 HTML ‘HTML’ 富文本 Phone ‘PHONE’ 手机 Email ‘EMAIL’ 邮箱 Integer ‘INTEGER’ 整数 Long ‘LONG’ 长整型 Float ‘FLOAT’ 浮点数 Currency ‘MONEY’ 金额 DateTime ‘DATETIME’ 时间日期 Date ‘DATE’ 日期 Time ‘TIME’ 时间 Year ‘YEAR’ 年份 Boolean ‘BOOLEAN’ 布尔型 Enum ‘ENUM’ 数据字典 Map ‘MAP’ 键值对 Related ‘RELATED’ 引用类型 OneToOne ‘O2O’ 一对一 OneToMany ‘O2M’ 一对多 ManyToOne ‘M2O’ 多对一 ManyToMany ‘M2M’ 多对多 模型动作 属性名 类型 描述 name string…

    2024年9月21日
    1.8K00
  • 前端自定义组件之多页面步骤条

    本文将讲解如何通过自定义,实现多页面的步骤条组件。其中每个步骤的元素里都对应界面设计器的一个页面。以下是代码实现和原理分析。 代码实现 NextStepWidget 多页面步骤条 ts 组件 import { CallChaining, createRuntimeContextByView, customMutation, customQuery, RuntimeView, SPI, ViewCache, Widget, DefaultTabsWidget, BasePackWidget } from '@oinone/kunlun-dependencies'; import { isEmpty } from 'lodash-es'; import { MyMetadataViewWidget } from './MyMetadataViewWidget'; import NextStep from './NextStep.vue'; import { IStepConfig, StepDirection } from './type'; @SPI.ClassFactory(BasePackWidget.Token({ widget: 'NextStep' })) export class NextStepWidget extends DefaultTabsWidget { public initialize(props) { this.titles = props.template?.widgets?.map((item) => item.title) || []; props.template && (props.template.widgets = []); super.initialize(props); this.setComponent(NextStep); return this; } @Widget.Reactive() public get invisible() { return false; } // 配置的每一步名称 @Widget.Reactive() public titles = []; // region 上一步下一步配置 // 步骤配置,切换的顺序就是数组的顺序,模型没有限制 @Widget.Reactive() public get stepJsonConfig() { let data = JSON.parse( this.getDsl().stepJsonConfig || '[{"model":"resource.ResourceCountry","viewName":"国家form"},{"model":"resource.ResourceProvince","viewName":"省form"},{"model":"resource.ResourceCity","viewName":"市form"}]' ); return data as IStepConfig[]; } // 切换上一步下一步 @Widget.Method() public async onStepChange(stepDirection: StepDirection) { // 没有激活的,说明是初始化,取第一步 if (!this.activeStepKey) { const step = this.stepJsonConfig[0]; if (step) { this.activeStepKey = `${step.model}_${step.viewName}`; await this.initStepView(step); } return; } // 获取当前步骤的索引 if (this.currentActiveKeyIndex > -1) { await this.onSave(); // 获取下一步索引 const nextStepIndex = stepDirection === StepDirection.NEXT ? this.currentActiveKeyIndex + 1 : this.currentActiveKeyIndex – 1; // 在索引范围内,则渲染视图 if (nextStepIndex >= 0 && nextStepIndex < this.stepJsonConfig.length) { const nextStep = this.stepJsonConfig[nextStepIndex];…

    2025年7月21日
    88000
  • 移动端5.0.x启动、打包代码报错

    在5.0.x版本中,移动端mobile-base包是源码开放的,所以项目在启动的时候可能会报错,请按照下面的步骤修改。 打开boot工程中的package.json "dependencies"中添加 "lodash-es": "4.17.21" "devDependencies"中添加 "@types/lodash-es": "4.17.12" 在main.ts中删除 import '@kunlun/vue-mobile-base/dist/kunlun-vue-mobile-base.css'

    2024年7月17日
    2.0K00

Leave a Reply

Please Login to Comment