【界面设计器】自定义字段组件实战——轮播图

阅读之前

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

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

业务背景

用户需要从【创建/编辑】页面中上传多张图片,并且在【详情】页面将这多张图片进行【轮播】展示。

业务分析

从需求来看,我们需要实现一个【轮播图】组件,并且该组件允许在【详情】视图中使用。在其他视图中,我们可以直接使用平台内置的【图片】组件,实现基础的编辑和展示功能。

名词解释

  • 业务模型:需要进行可视化管理的存储模型或代理模型。

准备工作

你需要在某个业务模型下创建一个【表格视图】用于查看全部数据,创建【表单视图】用于创建/编辑数据,并创建【详情视图】展示必要的信息。(为了方便起见,你可以在所有视图中仅使用编码和名称两个字段)

你需要将【表格视图】绑定到某个菜单上,并通过【跳转动作】将三个视图进行关联,可以完整执行当前模型的全部【增删改查】操作。

业务模型定义

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

DemoModel
名称 API名称 业务类型 是否多值 长度(单值长度)
编码 code 文本 128
名称 name 文本 128
轮播图 carouselImages 文本 512

实现页面效果展示

表格视图

image.png

表单视图-创建

image.png

表单视图-编辑

image.png

详情视图

image.png

根据业务背景添加轮播图字段到所有视图

轮播图字段信息:

  • 字段业务类型:文本
  • 多值:是

使用组件:图片

无代码模型

在模型设计器创建轮播图字段,并从【组件库】-【模型】拖放至视图中即可。

PS:这里需要注意的是,在模型设计器中需要切换至专家模式,并确认字段长度为512,否则当URL超长时将无法保存。

低代码模型

与服务端同学确认字段,并从【组件库】-【模型】中拖放至视图中即可。

将上图中的【演示】数据进行【编辑】,并上传三张图片,在【详情视图】查看默认展示效果。

演示图片下载

image.png

创建组件、元件

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

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

创建轮播图组件

image.png

创建轮播图元件

根据业务背景,我们需要根据模型中的字段确定业务类型,在这个场景中,可以使用如下配置。(暂时可以不进行属性面板的设计)

image.png

在【详情视图】中将【轮播图字段】的组件切换为我们新创建的【轮播图组件】

image.png

PS:这里会发现组件变成了【输入框】的样式,这是由于我们没有提供对应元件的代码实现,使得SPI找到了默认组件。

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

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

Carousel.vue
<template>
  <a-carousel class="carousel" effect="fade" autoplay>
    <div class="carousel-item" v-for="image in images" :key="image">
      <img :src="image" :alt="image" />
    </div>
  </a-carousel>
</template>
<script lang="ts">
import { Carousel as ACarousel } from 'ant-design-vue';
import { computed, defineComponent, PropType } from 'vue';

export default defineComponent({
  name: 'Carousel',
  components: {
    ACarousel
  },
  props: {
    value: {
      type: Array as PropType<string[]>
    }
  },
  setup(props) {
    const images = computed(() => props.value || []);

    return {
      images
    };
  }
});
</script>
<style lang="scss">
.carousel {
  .slick-slide {
    height: 160px;

    & > div,
    .carousel-item {
      width: 100%;
      height: 100%;
    }

    img {
      max-width: 100%;
      max-height: 100%;
      margin: auto;
    }
  }
}
</style>
效果展示

开发完成后,我们将重新打包生成的JS文件和CSS文件在【界面设计器】的【低无一体】进行上传,就可以在【设计器环境】中正常使用了。

image.png

设计轮播图的属性面板

通过我们使用的a-carousel组件,我们发现组件中提供了很多【属性】或【功能】可以进行配置,比如是否自动切换(autoplay)、面板指示点位置(dotPosition)、是否显示面板指示点(dots)等。在这里我们将对这三个属性的配置方式进行演示,其他更多属性可以自行设计并开发。

我们可以在【界面设计器】的【属性面板设计】中根据这三个属性的字段类型确定以下信息:

功能 API名称 业务类型 选用组件 可选项
是否自动切换 autoplay 布尔 开关 -
是否显示面板指示点 dots 布尔 开关 -
面板指示点位置 dotPosition 数据字典 下拉单选 上方(top)、下方(bottom)、左侧(left)、右侧(right)

确定了这些信息后,我们在【属性面板设计】中拖入对应组件,并创建【指定API名称】的字段。

PS:数据字典类型的字段需要先在模型设计器中创建对应的数据字典,才能创建该字段。由于设计器本身的依赖关系,建议将数据字典创建在【资源】模块中,这样才可以在设计器被选中。

数据字典

image.png

image.png

PS:前端获取的值为【API名称】,并非【字典项值】。

属性面板设计

image.png

在SDK中为组件新增的属性补充代码实现

typing.ts
export enum CarouselPosition {
  top = 'top',
  bottom = 'bottom',
  left = 'left',
  right = 'right'
}
DetailStringMultiCarouselFieldWidget.ts
import { BooleanHelper, FormFieldWidget, ModelFieldType, SPI, ViewType, Widget } from '@kunlun/dependencies';
import Carousel from './Carousel.vue';
import { CarouselPosition } from './typing';

@SPI.ClassFactory(
  FormFieldWidget.Token({
    viewType: ViewType.Detail,
    ttype: ModelFieldType.String,
    widget: 'Carousel',
    multi: true
  })
)
export class DetailStringMultiCarouselFieldWidget extends FormFieldWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(Carousel);
    return this;
  }

  @Widget.Reactive()
  protected get autoplay() {
    return BooleanHelper.toBoolean(this.getDsl().autoplay);
  }

  @Widget.Reactive()
  protected get dots() {
    return BooleanHelper.toBoolean(this.getDsl().dots);
  }

  @Widget.Reactive()
  protected get dotPosition(): CarouselPosition | undefined {
    return this.getDsl().dotPosition;
  }
}
Carousel.vue
<template>
  <a-carousel class="carousel" effect="fade" :autoplay="autoplay" :dots="dots" :dotPosition="dotPosition">
    <div class="carousel-item" v-for="image in images" :key="image">
      <img :src="image" :alt="image" />
    </div>
  </a-carousel>
</template>
<script lang="ts">
import { Carousel as ACarousel } from 'ant-design-vue';
import { computed, defineComponent, PropType } from 'vue';
import { CarouselPosition } from './typing';

export default defineComponent({
  name: 'Carousel',
  components: {
    ACarousel
  },
  props: {
    value: {
      type: Array as PropType<string[]>
    },
    autoplay: {
      type: Boolean
    },
    dots: {
      type: Boolean
    },
    dotPosition: {
      type: String as PropType<CarouselPosition>
    }
  },
  setup(props) {
    const images = computed(() => props.value || []);

    const dotPosition = computed(() => props.dotPosition?.toLowerCase());

    return {
      images,
      dotPosition
    };
  }
});
</script>
<style lang="scss">
.carousel.ant-carousel {
  .slick-slide,
  .slick-vertical {
    height: 160px;
  }

  .slick-slide {
    & > div,
    .carousel-item {
      width: 100%;
      height: 100%;
    }

    img {
      max-width: 100%;
      max-height: 100%;
      margin: auto;
    }
  }
}
</style>

开发完成后,我们将重新打包生成的JS文件和CSS文件在【界面设计器】的【低无一体】进行上传,就可以在【设计器环境】中正常使用了。

设计组件优化

在执行到以上步骤之后,我们发现执行页面和设计页面的属性面板都可以正常运行,美中不足的是,我们无法在设计器直观看到预览效果。为了解决这一问题,我们需要对设计组件进行相关的优化。

细心的同学可能也会发现,我们在组件中对高度的设定是160px,这个设置会导致用户无法根据需求进行定制。不仅如此,由于宽度属性未进行配置,用户也无法根据需求调整宽度。

为了解决上述问题,我们可以将属性面板稍作调整。

宽度使用内置的数据字典类型的宽度即可,高度需要我们新建一个高度(height)的整数字段,并添加后缀提示用户高度的单位。

效果如下图所示:

image.png

接下来,我们仅需实现【预览效果】和【高度】属性即可,内置的【宽度】属性是通过外部控制的,组件本身无需关心。

实现思路:

  • 由于设计器的预览功能移除了Class Component(ts)组件的相关功能,而是直接将属性面板的值传递到Vue组件中,因此,我们需要在Vue组件中判断当前组件是否在设计器的预览环境中,并且根据这个判断提供相应的预览效果。我们可以使用代码示例中的isDesignComponent属性,来实现这一功能。
  • 高度在用户输入时为【整数】,因此是没有单位的,可以使用平台内置的StyleHelper#px方法进行转换。
Carousel.vue
<template>
  <a-carousel class="carousel" effect="fade" :autoplay="autoplay" :dots="dots" :dotPosition="dotPosition">
    <template v-if="isDesignComponent">
      <div class="carousel-item carousel-item-demo"><h3>1</h3></div>
      <div class="carousel-item carousel-item-demo"><h3>2</h3></div>
      <div class="carousel-item carousel-item-demo"><h3>3</h3></div>
    </template>
    <template v-else>
      <div class="carousel-item" v-for="image in images" :key="image">
        <img :src="image" :alt="image" />
      </div>
    </template>
  </a-carousel>
</template>
<script lang="ts">
import { StyleHelper } from '@kunlun/dependencies';
import { Carousel as ACarousel } from 'ant-design-vue';
import { computed, defineComponent, PropType } from 'vue';
import { CarouselPosition } from './typing';

export default defineComponent({
  name: 'Carousel',
  components: {
    ACarousel
  },
  props: {
    value: {
      type: Array as PropType<string[]>
    },
    autoplay: {
      type: Boolean
    },
    dots: {
      type: Boolean
    },
    dotPosition: {
      type: String as PropType<CarouselPosition>
    },
    height: {
      type: [Number, String]
    },
    isDesignComponent: {
      type: Boolean,
      default: true
    }
  },
  setup(props) {
    const images = computed(() => props.value || []);

    const dotPosition = computed(() => props.dotPosition?.toLowerCase());

    const height = computed(() => StyleHelper.px(props.height) || '160px');

    return {
      images,
      dotPosition,
      height
    };
  }
});
</script>
<style lang="scss">
.carousel.ant-carousel {
  .slick-slide,
  .slick-vertical {
    height: v-bind(height);
  }

  .slick-slide {
    & > div,
    .carousel-item {
      width: 100%;
      height: 100%;
    }

    img {
      max-width: 100%;
      max-height: 100%;
      margin: auto;
    }
  }

  .carousel-item-demo {
    display: flex !important;
    align-items: center;
    justify-content: center;
    background-color: #364d79;

    h3 {
      color: #ffffff;
    }
  }
}
</style>
DetailStringMultiCarouselFieldWidget.ts
import { BooleanHelper, FormFieldWidget, ModelFieldType, SPI, ViewType, Widget } from '@kunlun/dependencies';
import Carousel from './Carousel.vue';
import { CarouselPosition } from './typing';

@SPI.ClassFactory(
  FormFieldWidget.Token({
    viewType: ViewType.Detail,
    ttype: ModelFieldType.String,
    widget: 'Carousel',
    multi: true
  })
)
export class DetailStringMultiCarouselFieldWidget extends FormFieldWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(Carousel);
    return this;
  }

  @Widget.Reactive()
  protected get autoplay() {
    return BooleanHelper.toBoolean(this.getDsl().autoplay);
  }

  @Widget.Reactive()
  protected get dots() {
    return BooleanHelper.toBoolean(this.getDsl().dots);
  }

  @Widget.Reactive()
  protected get dotPosition(): CarouselPosition | undefined {
    return this.getDsl().dotPosition;
  }

  @Widget.Reactive()
  protected get height(): number | string | undefined {
    return this.getDsl().height;
  }

  @Widget.Reactive()
  protected get isDesignComponent() {
    return false;
  }
}

开发完成后,我们将重新打包生成的JS文件和CSS文件在【界面设计器】的【低无一体】进行上传,就可以在【设计器环境】中正常使用了。

除了上述我们演示的功能实现外,其实轮播图组件还可以有其他更多的功能。比如:轮播图中的图片展示方式可以是拉伸、平铺、等比缩放等,轮播图分页样式,轮播图是否展示左右翻页按钮以及翻页按钮的样式,轮播动画效果等等。

结语

至此,我们已经基本完成了一个简单的轮播图组件。

从轮播图组件的实现来看,对我们传统开发思维提出了一些挑战。

在传统组件开发中,我们开发的一个一个Vue组件都是提供给其他开发人员使用的,甚至绝大多数组件是由于单个组件的复杂性进行拆分的一个一个小组件。

在传统开发思维中,我们很难感受到当一个组件具备用户输入能力时,它所展示的最终效果。甚至在某些场景中,用户输入的结果并非是我们所能预知的。

对于低代码组件而言,其使得用户对组件可以有一定程度的输入能力。正是由于存在这样的差异,我们更需要站在用户角度去思考组件该通过怎样的输入得到怎样的输出

从结果上来看,低代码组件为用户提供的输入能力可以使得组件应用的场景更加广泛,再结合界面设计器的在线设计能力,可以促使我们在开发组件时使其具有更高的复用能力。

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

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

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

相关推荐

  • 自定义组件之自动渲染(组件插槽的使用)(v4)

    阅读之前 你应该: 了解DSL相关内容。母版-布局-DSL 渲染基础(v4) 了解SPI机制相关内容。组件SPI机制(v4.3.0) 自定义组件简介 前面我们简单介绍过一个简单的自定义组件该如何被定义,并应用于页面中。这篇文章将对自定义组件进行详细介绍。 自定义一个带有具名插槽的容器组件(一般用于Object数据类型的视图中) 使用BasePackWidget组件进行注册,最终体现在DSL模板中为<pack widget="SlotDemo">。 SlotDemoWidget.ts import { BasePackWidget, SPI } from '@kunlun/dependencies'; import SlotDemo from './SlotDemo.vue'; @SPI.ClassFactory(BasePackWidget.Token({ widget: 'SlotDemo' })) export class SlotDemoWidget extends BasePackWidget { public initialize(props) { super.initialize(props); this.setComponent(SlotDemo); return this; } } 定义一个Vue组件,包含三个插槽,分别是default不具名插槽、title具名插槽、footer具名插槽。 SlotDemo.vue <template> <div class="slot-demo-wrapper" v-show="!invisible"> <div class="title"> <slot name="title" /> </div> <div class="content"> <slot /> </div> <div class="footer"> <slot name="footer" /> </div> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; export default defineComponent({ name: 'SlotDemo', props: { invisible: { type: Boolean, default: undefined } } }); </script> 在一个表单(FORM)的DSL模板中,我们可以这样使用这三个插槽: <view type="FORM"> <pack widget="SlotDemo"> <template slot="default"> <field data="id" /> </template> <template slot="title"> <field data="name" /> </template> <template slot="footer"> <field data="isEnabled" /> </template> </pack> </view> 这样定义的一个组件插槽和DSL模板就进行了渲染上的结合。 针对不具名插槽的特性,我们可以缺省slot="default"标签,缺少template标签包裹的所有元素都将被收集到default不具名插槽中进行渲染,则上述DSL模板可以改为: <view type="FORM"> <pack widget="SlotDemo"> <field data="id" /> <template slot="title"> <field data="name" /> </template> <template slot="footer"> <field data="isEnabled" /> </template> </pack> </view> 自定义一个数组渲染组件(一般用于List数据类型的视图中) 由于表格无法体现DSL模板渲染的相关能力,因此我们以画廊视图(GALLERY)进行演示。 先定义一个数组每一项的数据结构: typing.ts export interface ListItem { key: string; data: Record<string, unknown>; index: number; } ListRenderDemoWidget.ts import { BaseElementListViewWidget, BaseElementWidget, SPI } from '@kunlun/dependencies'; import ListRenderDemo from './ListRenderDemo.vue'; @SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ListRenderDemo' })) export class ListRenderDemoWidget extends BaseElementListViewWidget { public initialize(props)…

    2023年11月1日
    1.0K00
  • 自定义视图部分区域渲染设计器的配置

    自定义视图与界面设计器配置对接 在日常开发中,我们经常会遇到自定义视图的需求。自定义视图不仅需要与平台机制结合,还要实现与界面设计器中配置的字段和动作的无缝对接。本文将介绍如何将自定义视图与界面设计器中配置的字段和动作的无缝对接,实现字段和动作的渲染。 用大白话来讲就是:当前页面一部分是自定义的,一部分是设计器生成的 代码地址 目录 自定义表单视图与字段、动作的结合 自定义表格视图与字段、动作的结合 自定义表单视图与字段、动作的结合 首先需要在界面设计器配置好对应界面,虽然配置的页面样式跟期望展示的 UI 的不一样,但是数据的分发、汇总以及动作的交互也是一致的,所以我们可以通过自定义的方式替换这个页面的 UI,但是数据以及动作,是完全可以通过平台的能力获取的。 注册表单对应的 SPI // CustomFormWidget.ts import CustomForm from './CustomForm.vue'; @SPI.ClassFactory( BaseElementWidget.Token({ viewType: ViewType.Form, widget: 'CustomFormWidget' }) ) export class CustomFormWidget extends FormWidget { public initialize(props: BaseElementObjectViewWidgetProps): this { super.initialize(props); this.setComponent(CustomForm); return this; } } <!– CustomForm.vue –> <template> <div class="custom-form-container"> <div class="custom-form-tip">自定义视图</div> </div> </template> <script lang="ts"> import { DslRender, DslRenderDefinition, PropRecordHelper } from '@kunlun/dependencies'; import { createVNode, defineComponent, onMounted, PropType, ref, VNode } from 'vue'; export default defineComponent({ inheritAttrs: false, props: { formData: { type: Object as PropType<Record<string, any>>, default: () => ({}) } } }); </script> 在上述的代码中,我们继承的是 FormWidget,那么这个页面会自动发起对应的请求,所有的数据都在 formData 中。 表单视图渲染动作 在最开始我们讲到过,当前页面是在界面设计器配置过,所有在CustomFormWidget里面是可以拿到当前页面配置的元数据信息,所以我们可以拿到界面设计器配置的字段跟动作 /// CustomFormWidget.ts @Widget.Method() protected renderActionVNodes() { // 从dsl中获取actionBar,actionBar里面包含了界面设计器配置的动作 const actionBar = this.metadataRuntimeContext.viewDsl?.widgets.find((w) => w.slot === 'actionBar'); if (actionBar) { // actionBar.widgets 就是界面设计器配置的动作,借助平台提供的DslRender.render方法,将对应的dsl转换成VNode return actionBar.widgets.map((w, index) => DslRender.render({ …w, __index: index }) ); } return null; } 因为 renderActionVNodes 方法返回的是 VNode,所以我们必须借助 vue 的 render 函数才能渲染。 <!– ActionRender.vue –> <script lang="ts"> import { defineComponent } from 'vue'; export default defineComponent({ inheritAttrs: false, props: { renderActionVNodes: { type: Function, required: true } }, render() { return this.renderActionVNodes(); } });…

    2024年9月12日
    1.3K01
  • 自定义的复杂字段配置透出字段

    学习这篇文章之前,需要先学会使用在界面设计器自定义一个前端组件,如果您还不会,可以先看这篇文章 默认情况下,当开前端发人员自定义了一个复杂字段,比如M2O、O2M、M2M的字段,那么Graphql查询的时候,只会查询id跟name这两个字段,如果还想查询字段的字段,那么可以通过配置化的方式来处理 1: 在界面设计器的组件区域中新增对应的字段 2: 设计元件,在模型区域中搜索选项字段列表,拖到设计区域,然后保存 3: 去对应的设计页面,刷新下页面,选中对应的字段,可以看到右侧有选项字段列表4: 输入期望Graphql查询字段,保存发布

    2023年11月9日
    1.5K00
  • 多对多的表格 点击添加按钮打开一个表单弹窗

    多对多的表格 点击添加按钮打开一个表单弹窗 默认情况下,多对多的表格上方的添加按钮点击后,打开的是个表格 ,如果您期望点击添加按钮打开的是个表单页面,那么可以按照下方的操作来 1: 先从界面设计器拖一个多对多的字段进来 2: 将该字段切换成表格,并拖入一些字段到表格上 3: 选中添加按钮,将其隐藏 4: 从组件区域的动作分组中拖一个跳转动作,并且进行如下的配置 5: 属性填写好后进行保存,然后在设计弹窗 6: 拖入对应的字段到弹窗中, 当弹窗界面设计完成后,再把保存的按钮拖入进来 这样多对多的添加弹窗就变成了表单

    前端 2023年11月1日
    2.4K00
  • 表格新增空行或者按照数据如何复制行

    场景 描述 新增按钮点击后表格出现空行,不带业务数据,需要有行内编辑 如何实现 第一步 在layout目录下新增copyTable组件,组件代码如下 import { BaseElementWidget, SPI, TableWidget, Widget } from '@kunlun/dependencies'; import { OioNotification } from '@kunlun/vue-ui-antd'; @SPI.ClassFactory(BaseElementWidget.Token({ widget: 'copy-table-row' })) export class CopyTableWidget extends TableWidget { @Widget.BehaviorSubContext(Symbol("$$TABLE_COPY_CB"), {}) private tableCopySub; @Widget.BehaviorSubContext(Symbol("$$TABLE_DELETE_CB")) private tableDeleteSub; @Widget.Reactive() @Widget.Provide() protected get editorMode(): any { return 'manual' } public async copyRowData(row,currentRow) { // 获取vxetable 实例 const tableRef = this.getTableInstance()!.getOrigin(); if (tableRef) { // 有复制未保存数据,如何处理? const insertData = tableRef.getInsertRecords(); if(insertData.length > 0){ OioNotification.warning("警告","请检查未保存数据!") return; } const { row: newRow } = await tableRef.insertAt(row,currentRow) // 插入一条数据并触发校验, 其中字段名称可以替换 await tableRef.setEditCell(newRow, 'city') } } public async deleteRowData(row) { // 获取vxetable 实例 const tableRef = this.getTableInstance()!.getOrigin(); if (tableRef) { // 有复制未保存数据,如何处理? console.log(row, 'remove row') tableRef.remove(row) // 插入一条数据并触发校验 } } async mounted() { super.mounted(); this.tableCopySub.subject.next({copyCb: (row,currentRow) => this.copyRowData(row,currentRow)}) this.tableDeleteSub.subject.next({deleteCb: (row) => this.deleteRowData(row)}) } } 第二步 在action目录下覆盖新增按钮或者复制行按钮;代码如下 import {ActionWidget, ClickResult, ReturnPromise, SPI, Widget} from "@kunlun/dependencies"; @SPI.ClassFactory( ActionWidget.Token({ model: 'resource.k2.Model0000001211', // 替换对应模型 name: 'uiView57c25f66fac9439089d590a4ac47f027' // 替换对应action的name }) ) export class CopyRow extends ActionWidget{ @Widget.BehaviorSubContext(Symbol("$$TABLE_COPY_CB")) private tableCopySub; private tableCopyCb; @Widget.Method() public clickAction(): ReturnPromise<ClickResult> { // 按照某一条数据复制行, 按钮在行内 // let data = JSON.parse(JSON.stringify(this.activeRecords?.[0])); // 复制行删除id // if(data) { // delete…

    2024年7月15日
    1.7K00

Leave a Reply

登录后才能评论